diff --git a/.cursor/rules/vars-usage.mdc b/.cursor/rules/vars-usage.mdc new file mode 100644 index 00000000..233e0aba --- /dev/null +++ b/.cursor/rules/vars-usage.mdc @@ -0,0 +1,18 @@ +--- +description: Restricts usage of the global Mineflayer `bot` variable to only src/ files; prohibits usage in renderer/. Specifies correct usage of player state and appViewer globals. +globs: src/**/*.ts,renderer/**/*.ts +alwaysApply: false +--- +Ask AI + +- The global variable `bot` refers to the Mineflayer bot instance. +- You may use `bot` directly in any file under the `src/` directory (e.g., `src/mineflayer/playerState.ts`). +- Do **not** use `bot` directly in any file under the `renderer/` directory or its subfolders (e.g., `renderer/viewer/three/worldrendererThree.ts`). +- In renderer code, all bot/player state and events must be accessed via explicit interfaces, state managers, or passed-in objects, never by referencing `bot` directly. +- In renderer code (such as in `WorldRendererThree`), use the `playerState` property (e.g., `worldRenderer.playerState.gameMode`) to access player state. The implementation for `playerState` lives in `src/mineflayer/playerState.ts`. +- In `src/` code, you may use the global variable `appViewer` from `src/appViewer.ts` directly. Do **not** import `appViewer` or use `window.appViewer`; use the global `appViewer` variable as-is. +- Some other global variables that can be used without window prefixes are listed in src/globals.d.ts + +Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state. + +For more general project contributing guides see CONTRIBUTING.md on like how to setup the project. Use pnpm tsc if needed to validate result with typechecking the whole project. diff --git a/.dockerignore b/.dockerignore index 285d1303..38ca0016 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,3 @@ -# we dont want default config to be loaded in the dockerfile, but rather using a volume -config.json # build stuff node_modules public \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 3c3629e6..9aa16166 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,9 @@ node_modules +rsbuild.config.ts +*.module.css.d.ts +*.generated.ts +generated +dist +public +**/*/rsbuildSharedConfig.ts +src/mcDataTypes.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 98388260..63f6749a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,32 +1,76 @@ { - "extends": "zardoy", + "extends": [ + "zardoy", + "plugin:@stylistic/disable-legacy" + ], "ignorePatterns": [ - "!*.js", - "prismarine-viewer/" + "!*.js" + ], + "plugins": [ + "@stylistic" ], "rules": { - "space-infix-ops": "error", - "no-multi-spaces": "error", - "space-before-function-paren": "error", - "space-in-parens": [ + // style + "@stylistic/space-infix-ops": "error", + "@stylistic/no-multi-spaces": "error", + "@stylistic/no-trailing-spaces": "error", + "@stylistic/space-before-function-paren": "error", + "@stylistic/array-bracket-spacing": "error", + // would be great to have but breaks TS code like (url?) => ... + // "@stylistic/arrow-parens": [ + // "error", + // "as-needed" + // ], + "@stylistic/arrow-spacing": "error", + "@stylistic/block-spacing": "error", + "@typescript-eslint/no-this-alias": "off", + "@stylistic/brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + // too annoying to be forced to multi-line, probably should be enforced to never + // "@stylistic/comma-dangle": [ + // "error", + // "always-multiline" + // ], + "@stylistic/computed-property-spacing": "error", + "@stylistic/dot-location": [ + "error", + "property" + ], + "@stylistic/eol-last": "error", + "@stylistic/function-call-spacing": "error", + "@stylistic/function-paren-newline": [ + "error", + "consistent" + ], + "@stylistic/generator-star-spacing": "error", + "@stylistic/implicit-arrow-linebreak": "error", + "@stylistic/indent-binary-ops": [ + "error", + 2 + ], + "@stylistic/function-call-argument-newline": [ + "error", + "consistent" + ], + "@stylistic/space-in-parens": [ "error", "never" ], - "object-curly-spacing": [ + "@stylistic/object-curly-spacing": [ "error", "always" ], - "comma-spacing": "error", - "semi": [ + "@stylistic/comma-spacing": "error", + "@stylistic/semi": [ "error", "never" ], - "comma-dangle": [ - "error", - // todo maybe "always-multiline"? - "only-multiline" - ], - "indent": [ + "@stylistic/indent": [ "error", 2, { @@ -36,13 +80,72 @@ ] } ], - "quotes": [ + "@stylistic/quotes": [ "error", "single", { "allowTemplateLiterals": true } ], + "@stylistic/key-spacing": "error", + "@stylistic/keyword-spacing": "error", + // "@stylistic/line-comment-position": "error", // not needed + // "@stylistic/lines-around-comment": "error", // also not sure if needed + // "@stylistic/max-len": "error", // also not sure if needed + // "@stylistic/linebreak-style": "error", // let git decide + "@stylistic/max-statements-per-line": [ + "error", + { + "max": 5 + } + ], + // "@stylistic/member-delimiter-style": "error", + // "@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", + "@stylistic/type-generic-spacing": "error", + "@stylistic/template-tag-spacing": "error", + "@stylistic/template-curly-spacing": "error", + "@stylistic/type-annotation-spacing": "error", + "@stylistic/jsx-child-element-spacing": "error", + // buggy + // "@stylistic/jsx-closing-bracket-location": "error", + // "@stylistic/jsx-closing-tag-location": "error", + "@stylistic/jsx-curly-brace-presence": "error", + "@stylistic/jsx-curly-newline": "error", + "@stylistic/jsx-curly-spacing": "error", + "@stylistic/jsx-equals-spacing": "error", + "@stylistic/jsx-first-prop-new-line": "error", + "@stylistic/jsx-function-call-newline": "error", + "@stylistic/jsx-max-props-per-line": [ + "error", + { + "maximum": 7 + } + ], + "@stylistic/jsx-pascal-case": "error", + "@stylistic/jsx-props-no-multi-spaces": "error", + "@stylistic/jsx-self-closing-comp": "error", + // "@stylistic/jsx-sort-props": [ + // "error", + // { + // "callbacksLast": false, + // "shorthandFirst": true, + // "shorthandLast": false, + // "multiline": "ignore", + // "ignoreCase": true, + // "noSortAlphabetically": true, + // "reservedFirst": [ + // "key", + // "className" + // ], + // "locale": "auto" + // } + // ], // perf "import/no-deprecated": "off", // --- @@ -52,6 +155,7 @@ // intentional: improve readability in some cases "no-else-return": "off", "@typescript-eslint/padding-line-between-statements": "off", + "@typescript-eslint/no-dynamic-delete": "off", "arrow-body-style": "off", "unicorn/prefer-ternary": "off", "unicorn/switch-case-braces": "off", @@ -88,13 +192,32 @@ "@typescript-eslint/no-confusing-void-expression": "off", "unicorn/no-empty-file": "off", "unicorn/prefer-event-target": "off", + "@typescript-eslint/member-ordering": "off", // needs to be fixed actually "complexity": "off", "@typescript-eslint/no-floating-promises": "warn", "no-async-promise-executor": "off", "no-bitwise": "off", "unicorn/filename-case": "off", - "max-depth": "off" + "max-depth": "off", + "unicorn/no-typeof-undefined": "off" }, + "overrides": [ + { + "files": [ + "*.js" + ], + "rules": { + "@stylistic/space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + } + ] + } + } + ], "root": true } diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..e80b7100 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,59 @@ +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<> $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 }} diff --git a/.github/workflows/build-single-file.yml b/.github/workflows/build-single-file.yml new file mode 100644 index 00000000..5f9800db --- /dev/null +++ b/.github/workflows/build-single-file.yml @@ -0,0 +1,33 @@ +name: build-single-file + +on: + workflow_dispatch: + +jobs: + build-and-bundle: + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout repository + uses: actions/checkout@master + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + + - 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 + with: + name: minecraft.html + path: minecraft.html diff --git a/.github/workflows/build-zip.yml b/.github/workflows/build-zip.yml new file mode 100644 index 00000000..76ca65ca --- /dev/null +++ b/.github/workflows/build-zip.yml @@ -0,0 +1,45 @@ +name: Make Self Host Zip + +on: + workflow_dispatch: + +jobs: + build-and-bundle: + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout repository + uses: actions/checkout@master + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + + - name: Build project + run: pnpm build + env: + LOCAL_CONFIG_FILE: config.mcraft-only.json + + - name: Bundle server.js + run: | + pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'" + + - name: Create distribution package + run: | + mkdir -p package + cp -r dist package/ + cp bundled-server.js package/server.js + cd package + zip -r ../self-host.zip . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: self-host + path: self-host.zip diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe7c8ba6..8fc56ea9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,19 +8,170 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@master + - name: Setup Java JDK + uses: actions/setup-java@v1.4.3 + with: + java-version: 17 + java-package: jre + - uses: actions/setup-node@v4 + with: + node-version: 22 + # cache: "pnpm" - name: Install pnpm - run: npm i -g pnpm - # todo this needs investigating fixing + uses: pnpm/action-setup@v4 - run: pnpm install - - run: pnpm lint + - 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 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 & - uses: cypress-io/github-action@v5 with: install: false - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-images - path: cypress/integration/__image_snapshots__/ + path: cypress/screenshots/ + # - run: node scripts/outdatedGitPackages.mjs + # 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' + # steps: + # - name: Checkout repository + # uses: actions/checkout@v2 + + # - name: Install pnpm + # run: npm install -g pnpm@9.0.4 + + # - name: Run pnpm dedupe + # run: pnpm dedupe + + # - name: Check for changes + # run: | + # if ! git diff --exit-code --quiet pnpm-lock.yaml; then + # echo "pnpm dedupe introduced changes:" + # git diff --color=always pnpm-lock.yaml + # exit 1 + # else + # echo "No changes detected after pnpm dedupe in pnpm-lock.yaml" + # fi diff --git a/.github/workflows/fix-lint.yml b/.github/workflows/fix-lint.yml new file mode 100644 index 00000000..da2cf87d --- /dev/null +++ b/.github/workflows/fix-lint.yml @@ -0,0 +1,29 @@ +name: Fix Lint Command +on: + issue_comment: + types: [created] +jobs: + deploy: + runs-on: ubuntu-latest + if: >- + github.event.issue.pull_request != '' && + ( + contains(github.event.comment.body, '/fix') + ) + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v2 + with: + ref: refs/pull/${{ github.event.issue.number }}/head + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - run: pnpm install + - run: pnpm lint --fix + - name: Push Changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge-next.yml b/.github/workflows/merge-next.yml new file mode 100644 index 00000000..ee02789b --- /dev/null +++ b/.github/workflows/merge-next.yml @@ -0,0 +1,28 @@ +name: Update Base Branch Command +on: + issue_comment: + types: [created] +jobs: + deploy: + runs-on: ubuntu-latest + if: >- + github.event.issue.pull_request != '' && + ( + contains(github.event.comment.body, '/update') + ) + permissions: + pull-requests: write + contents: write + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all history so we can merge branches + ref: refs/pull/${{ github.event.issue.number }}/head + - name: Fetch All Branches + run: git fetch --all + # - name: Checkout PR + # run: git checkout ${{ github.event.issue.pull_request.head.ref }} + - name: Merge From Next + run: git merge origin/next --strategy-option=theirs + - name: Push Changes + run: git push diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml new file mode 100644 index 00000000..75b39f6c --- /dev/null +++ b/.github/workflows/next-deploy.yml @@ -0,0 +1,91 @@ +name: Vercel Deploy Next +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + ALIASES: ${{ vars.ALIASES }} + MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }} +on: + push: + branches: + - next +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install Global Dependencies + run: pnpm add -g vercel + - name: Install Dependencies + run: pnpm install + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + - 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 + - name: Copy playground files + run: | + mkdir -p .vercel/output/static/playground + pnpm build-playground + cp -r renderer/dist/* .vercel/output/static/playground/ + - name: Deploy Project Artifacts to Vercel + uses: mathiasvr/command-output@v2.0.0 + with: + run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} + id: deploy + - name: Start servers for testing + run: | + nohup pnpm prod-start & + nohup pnpm test-mc-server & + - name: Run Cypress smoke tests + uses: cypress-io/github-action@v5 + with: + install: false + spec: cypress/e2e/smoke.spec.ts + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-smoke-test-screenshots + path: cypress/screenshots/ + - name: Set deployment aliases + run: | + for alias in $(echo ${{ secrets.TEST_PREVIEW_DOMAIN }} | tr "," "\n"); do + vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro + done + + - name: Create Release Pull Request + uses: actions/github-script@v6 + with: + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:next`, + base: 'release', + state: 'open' + }); + + if (pulls.length === 0) { + await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Release', + head: 'next', + base: 'release', + body: 'PR was created automatically by the release workflow, hope you release it as soon as possible!', + }); + } diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 1db1bb9e..89fd6698 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -1,47 +1,114 @@ -name: Vercel Deploy Preview +name: Vercel PR Deploy (Preview) env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + ALIASES: ${{ vars.ALIASES }} on: issue_comment: types: [created] + pull_request_target: jobs: deploy: runs-on: ubuntu-latest - # todo skip already created deploys on that commit if: >- - github.event.issue.pull_request != '' && ( - contains(github.event.comment.body, '/deploy') + ( + github.event_name == 'issue_comment' && + contains(github.event.comment.body, '/deploy') && + github.event.issue.pull_request != null + ) || + ( + github.event_name == 'pull_request_target' && + contains(fromJson(vars.AUTO_DEPLOY_PRS), github.event.pull_request.number) + ) ) permissions: pull-requests: write steps: - - name: Checkout + - name: Checkout Base To Temp uses: actions/checkout@v2 + with: + path: temp-base-repo + - name: Get deployment alias + run: node temp-base-repo/scripts/githubActions.mjs getAlias + id: alias + env: + ALIASES: ${{ env.ALIASES }} + PULL_URL: ${{ github.event.issue.pull_request.url || github.event.pull_request.url }} + - name: Checkout PR (comment) + uses: actions/checkout@v2 + if: github.event_name == 'issue_comment' with: ref: refs/pull/${{ github.event.issue.number }}/head + - name: Checkout PR (pull_request) + uses: actions/checkout@v2 + if: github.event_name == 'pull_request_target' + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + + - name: Install pnpm + uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + 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: npm install --global vercel pnpm + run: pnpm add -g vercel - name: Pull Vercel Environment Information run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - - name: Build Project Artifacts - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - - run: pnpm build-storybook - - name: Copy playground files - run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js + - 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 + - name: Copy playground files + run: | + mkdir -p .vercel/output/static/playground + pnpm build-playground + cp -r renderer/dist/* .vercel/output/static/playground/ + - name: Write pr redirect index.html + run: | + mkdir -p .vercel/output/static/pr + echo "" > .vercel/output/static/pr/index.html + - name: Write commit redirect index.html + run: | + mkdir -p .vercel/output/static/commit + echo "" > .vercel/output/static/commit/index.html - name: Deploy Project Artifacts to Vercel uses: mathiasvr/command-output@v2.0.0 with: run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} id: deploy - - name: Set deployment alias - # only if on branch next - if: github.ref == 'refs/heads/next' - run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ secrets.TEST_PREVIEW_DOMAIN }} --token=${{ secrets.VERCEL_TOKEN }} - uses: mshick/add-pr-comment@v2 + # if: github.event_name == 'issue_comment' with: + allow-repeats: true message: | Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }} + [Playground](${{ steps.deploy.outputs.stdout }}/playground/) + [Storybook](${{ steps.deploy.outputs.stdout }}/storybook/) + # - run: git checkout next scripts/githubActions.mjs + - name: Set deployment alias + if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }} + 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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 3a898779..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Deploy to GitHub pages -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} -on: - push: - branches: [main] -jobs: - build-and-deploy: - runs-on: ubuntu-latest - permissions: write-all - steps: - - name: Checkout repository - uses: actions/checkout@master - - name: Install pnpm - run: npm i -g vercel pnpm - # - run: pnpm install - # - run: pnpm build - - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - # will install + build to .vercel/output/static - - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod - - run: pnpm build-storybook - - name: Copy playground files - run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js - - 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 - - run: | - pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: cp vercel.json .vercel/output/static/vercel.json - - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: .vercel/output/static - force_orphan: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3e8c4136 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,116 @@ +name: Release +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }} +on: + push: + branches: [release] +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout repository + uses: actions/checkout@master + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install Global Dependencies + run: pnpm add -g vercel + # - run: pnpm install + # - run: pnpm build + - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + - run: node scripts/replaceFavicon.mjs ${{ secrets.FAVICON_MAIN }} + # will install + build to .vercel/output/static + - name: Get Release Info + 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 + - name: Copy playground files + run: | + mkdir -p .vercel/output/static/playground + pnpm build-playground + cp -r renderer/dist/* .vercel/output/static/playground/ + + # publish to github + - run: cp vercel.json .vercel/output/static/vercel.json + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + 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 Minecraft Web Client to Minecraft Web Client — Free Online Browser Version + sed -i 's/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 + run: pnpm build + - name: Bundle server.js + run: | + pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'" + + - name: Create zip package + run: | + mkdir -p package + cp -r dist package/ + cp bundled-server.js package/server.js + cd package + 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 }})" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # has possible output: tag + id: release + + # has output + - name: Set publishing config + run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + # - run: pnpm tsx scripts/buildNpmReact.ts ${{ steps.release.outputs.tag }} + # if: steps.release.outputs.tag + # env: + # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 665722ec..33734572 100644 --- a/.gitignore +++ b/.gitignore @@ -7,12 +7,18 @@ package-lock.json Thumbs.db build localSettings.mjs -dist +dist* .DS_Store .idea/ -world +/world +data*.json out *.iml .vercel generated storybook-static +server-jar +config.local.json +logs/ + +src/react/npmReactComponents.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 1241b47c..05c36eba 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,17 +1,19 @@ import React from 'react' -import type { Preview } from "@storybook/react"; +import type { Preview } from "@storybook/react" -import '../src/styles.css' import './storybook.css' +import '../src/styles.css' +import '../src/scaleInterface' const preview: Preview = { decorators: [ - (Story) => ( - <div id='ui-root'> + (Story, c) => { + const noScaling = c.parameters.noScaling + return <div id={noScaling ? '' : 'ui-root'}> <Story /> </div> - ), + }, ], parameters: { actions: { argTypesRegex: "^on[A-Z].*" }, @@ -22,6 +24,6 @@ const preview: Preview = { }, }, }, -}; +} -export default preview; +export default preview diff --git a/.vscode/launch.json b/.vscode/launch.json index 6bbd4198..dec88163 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,5 @@ { "configurations": [ - // UPDATED: all configs below are misconfigured and will crash vscode, open dist/index.html and use live preview debug instead // recommended as much faster { // to launch "C:\Program Files\Google\Chrome Beta\Application\chrome.exe" --remote-debugging-port=9222 @@ -29,7 +28,7 @@ "type": "chrome", "name": "Launch Chrome", "request": "launch", - "url": "http://localhost:8080/", + "url": "http://localhost:3000/", "pathMapping": { "/": "${workspaceFolder}/dist" }, @@ -50,7 +49,7 @@ "name": "Attach Firefox", "request": "attach", // comment if using webpack - "url": "http://localhost:8080/", + "url": "http://localhost:3000/", "webRoot": "${workspaceFolder}/", "skipFiles": [ // "<node_internals>/**/*vendors*" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22958d3f..a5a3482d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,16 +2,194 @@ After forking the repository, run the following commands to get started: -0. Ensure you have [Node.js](https://nodejs.org) and `pnpm` installed. To install pnpm run `npm i -g pnpm`. +0. Ensure you have [Node.js](https://nodejs.org) installed. Enable corepack with `corepack enable` *(1). 1. Install dependencies: `pnpm i` -2. Start the project in development mode: `pnpm start` +2. Start the project in development mode: `pnpm start` or build the project for production: `pnpm build` +3. Read the [Tasks Categories](#tasks-categories) and [Workflow](#workflow) sections below +4. Let us know if you are working on something and be sure to open a PR if you got any changes. Happy coding! -A few notes: +*(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 --> + +## Project Structure + +There are 3 main parts of the project: + +### Core (`src`) + +This is the main app source code which reuses all the other parts of the project. + +> The first version used Webpack, then was migrated to Esbuild and now is using Rsbuild! + +- Scripts: + - Start: `pnpm start`, `pnpm dev-rsbuild` (if you don't need proxy server also running) + - Build: `pnpm build` (note that `build` script builds only the core app, not the whole project!) + +Paths: + +- `src` - main app source code +- `src/react` - React components - almost all UI is in this folder. Almost every component has its base (reused in app and storybook) and `Provider` - which is a component that provides context to its children. Consider looking at DeathScreen component to see how it's used. + +### Renderer: Playground & Mesher (`renderer`) + +- Playground Scripts: + - Start: `pnpm run-playground` (playground, mesher + server) or `pnpm watch-playground` + - Build: `pnpm build-playground` or `node renderer/esbuild.mjs` + +- Mesher Scripts: + - Start: `pnpm watch-mesher` + - Build: `pnpm build-mesher` + +Paths: + +- `renderer` - Improved and refactored version of <https://github.com/PrismarineJS/prismarine-viewer>. Here is everything related to rendering the game world itself (no ui at all). Two most important parts here are: +- `renderer/viewer/lib/worldrenderer.ts` - adding new objects to three.js happens here (sections) +- `renderer/viewer/lib/models.ts` - preparing data for rendering (blocks) - happens in worker: out file - `worker.js`, building - `renderer/buildWorker.mjs` +- `renderer/playground/playground.ts` - Playground (source of <mcraft.fun/playground.html>) Use this for testing any rendering changes. You can also modify the playground code. + +### Storybook (`.storybook`) + +Storybook is a tool for easier developing and testing React components. +Path of all Storybook stories is `src/react/**/*.stories.tsx`. + +- Scripts: + - Start: `pnpm storybook` + - Build: `pnpm build-storybook` + +## Core-related + +How different modules are used: + +- `mineflayer` - provider `bot` variable and as mineflayer states it is a wrapper for the `node-minecraft-protocol` module and is used to connect and interact with real Java Minecraft servers. However not all events & properties are exposed and sometimes you have to use `bot._client.on('packet_name', data => ...)` to handle packets that are not handled via mineflayer API. Also you can use almost any mineflayer plugin. + +## Running Main App + Playground + +To start the main web app and playground, run `pnpm run-all`. Note is doesn't start storybook and tests. + +## Cypress Tests (E2E) + +Cypress tests are located in `cypress` folder. To run them, run `pnpm test-mc-server` and then `pnpm test:cypress` when the `pnpm prod-start` is running (or change the port to 3000 to test with the dev server). Usually you don't need to run these until you get issues on the CI. + +## Unit Tests + +There are not many unit tests for now (which we are trying to improve). +Location of unit tests: `**/*.test.ts` files in `src` folder and `renderer` folder. +Start them with `pnpm test-unit`. + +## Making protocol-related changes + +You can get a description of packets for the latest protocol version from <https://wiki.vg/Protocol> and for previous protocol versions from <https://wiki.vg/Protocol_version_numbers> (look for *Page* links that have *Protocol* in URL). + +Also there are [src/generatedClientPackets.ts](src/generatedClientPackets.ts) and [src/generatedServerPackets.ts](src/generatedServerPackets.ts) files that have definitions of packets that come from the server and the client respectively. These files are generated from the protocol files. Protocol, blocks info and other data go from <https://github.com/prismarineJS/minecraft-data> repository. + +## A few other notes -- Use `next` branch for development and as base & target branch for pull requests if possible. - To link dependency locally e.g. flying-squid add this to `pnpm` > `overrides` of root package.json: `"flying-squid": "file:../space-squid",` (with some modules `pnpm link` also works) +- Press `Y` to reload application into the same world (server, local world or random singleplayer world) +- To start React profiling disable `REACT_APP_PROFILING` code first. - It's recommended to use debugger for debugging. VSCode has a great debugger built-in. If debugger is slow, you can use `--no-sources` flag that would allow browser to speedup .map file parsing. - Some data are cached between restarts. If you see something doesn't work after upgrading dependencies, try to clear the by simply removing the `dist` folder. - The same folder `dist` is used for both development and production builds, so be careful when deploying the project. -- Use `start-prod` script to start the project in production mode after running `build` script to build the project. +- Use `start-prod` script to start the project in production mode after running the `build` script to build the project. +- If CI is failing on the next branch for some reason, feel free to use the latest commit for release branch. We will update the base branch asap. Please, always make sure to allow maintainers do changes when opening PRs. + +## Tasks Categories + +(most important for now are on top). + +## 1. Client-side Logic (most important right now) + +Everything related to the client side packets. Investigate issues when something goes wrong with some server. It's much easier to work on these types of tasks when you have experience in Java with Minecraft, a deep understanding of the original client, and know how to debug it (which is not hard actually). Right now the client is easily detectable by anti-cheat plugins, and the main goal is to fix it (mostly because of wrong physics implementation). + +Priority tasks: + +- Rewrite or fix the physics logic (Botcraft or Grim can be used as a reference as well) +- Implement basic minecart / boat / horse riding +- Fix auto jump module (false triggers, performance issues) +- Investigate connection issues to some servers +- Setup a platform for automatic cron testing against the latest version of the anti-cheat plugins +- ... + +Goals: + +- Make more servers playable. Right now on hypixel-like servers (servers with minigames), only tnt run (and probably ) is fully playable. + +Notes: + +- You can see the incoming/outgoing packets in the console (F12 in Chrome) by enabling `options.debugLogNotFrequentPackets = true`. However, if you need a FULL log of all packets, you can start recording the packets by going into `Settings` > `Advanced` > `Enable Packets Replay` and then you can download the file and use it to replay the packets. +- You can use mcraft-e2e studio to send the same packets over and over again (which is useful for testing) or use the packets replayer (which is useful for debugging). + +## 2. Three.js Renderer + +Example tasks: + +- Improve / fix entity rendering +- Better update entities on specific packets +- Investigate performance issues under different conditions (instructions provided) +- Work on the playground code + +Goals: + +- Fix a lot of entity rendering issues (including position updates) +- Implement switching camera mode (first person, third person, etc) +- Animated blocks +- Armor rendering +- ... + +Note: + +- It's useful to know how to use helpers & additional cameras (e.g. setScissor) + +## 3. Server-side Logic + +Flying squid fork (space-squid). +Example tasks: + +- Add missing commands (e.g. /scoreboard) +- Basic physics (player fall damage, falling blocks & entities) +- Basic entities AI (spawning, attacking) +- Pvp +- Emit more packets on some specific events (e.g. when a player uses an item) +- Make more maps playable (e.g. fix when something is not implemented in both server and client and blocking map interaction) +- ... + +Long Term Goals: + +- Make most adventure maps playable +- Make a way to complete the game from the scratch (crafting, different dimensions, terrain generation, etc) +- Make bedwars playable! +Most of the tasks are straightforward to implement, just be sure to use a debugger ;). If you feel you are stuck, ask for help on Discord. Absolutely any tests / refactor suggestions are welcome! + +## 4. Frontend + +New React components, improve UI (including mobile support). + +## Workflow + +1. Locate the problem on the public test server & make an easily reproducible environment (you can also use local packets replay server or your custom server setup). Dm me for details on public test server / replay server +2. Debug the code, find an issue in the code, isolate the problem +3. Develop, try to fix and test. Finally we should find a way to fix it. It's ideal to have an automatic test but it's not necessary for now +3. Repeat step 1 to make sure the task is done and the problem is fixed (or the feature is implemented) + +## 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` + +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 + 3. Apply the patch manually in this directory: `patch -p1 < minecraft-protocol@<version>.patch` + 4. Run the suggested command from `pnpm patch ...` (previous step) to update the patch + +### Would be useful to have + +- cleanup folder & modules structure, cleanup playground code diff --git a/Dockerfile b/Dockerfile index aa9eb3dc..22bcfac6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,43 @@ -FROM node:14-alpine +# ---- Build Stage ---- +FROM node:18-alpine AS build # Without git installing the npm packages fails RUN apk add git -RUN mkdir /app WORKDIR /app COPY . /app -RUN npm install -RUN npm run build -ENTRYPOINT ["npm", "run", "prod-start"] +# install pnpm with corepack +RUN corepack enable +# Build arguments +ARG DOWNLOAD_SOUNDS=false +ARG DISABLE_SERVICE_WORKER=false +ARG CONFIG_JSON_SOURCE=REMOTE +# TODO need flat --no-root-optional +RUN node ./scripts/dockerPrepare.mjs +RUN pnpm i +# Download sounds if flag is enabled +RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs ; fi + +# TODO for development +# EXPOSE 9090 +# VOLUME /app/src +# VOLUME /app/renderer +# ENTRYPOINT ["pnpm", "run", "run-all"] + +# only for prod +RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ + CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \ + pnpm run build + +# ---- Run Stage ---- +FROM node:18-alpine +RUN apk add git +WORKDIR /app +# Copy build artifacts from the build stage +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 init -yp +RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors +EXPOSE 8080 +VOLUME /app/public +ENTRYPOINT ["node", "server.js", "--prod"] diff --git a/Dockerfile.proxy b/Dockerfile.proxy new file mode 100644 index 00000000..746eef72 --- /dev/null +++ b/Dockerfile.proxy @@ -0,0 +1,11 @@ +# ---- Run Stage ---- +FROM node:18-alpine +RUN apk add git +WORKDIR /app +COPY server.js /app/server.js +# Install server dependencies +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 +ENTRYPOINT ["node", "server.js"] diff --git a/README.MD b/README.MD index d4c3c6e8..018784e3 100644 --- a/README.MD +++ b/README.MD @@ -2,41 +2,114 @@ ![banner](./docs-assets/banner.jpg) -A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using modern web technologies. +Minecraft **clone** rewritten in TypeScript using the best modern web technologies. Minecraft vanilla-compatible client and integrated server packaged into a single web app. -This project is a work in progress, but I consider it to be usable. If you encounter any bugs or usability issues, please report them! +You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the default (`develop`) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) and [s.pcm.gg](https://s.pcm.gg/) - so it's usually newer, but might be less stable. -You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link) [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the `develop` (default) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) - 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, meanwhile this project is aimed for *device-compatiiblity* and better performance so it feels portable, flexible and lightweight. It's also a very strong example on how to build true HTML games for the web at scale entirely with the JS ecosystem. Have fun! + +For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft). + +> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere) ### Big Features -- Connect to any offline server* (it's possible because of proxy servers, see below) +- 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! -- Singleplayer mode with simple world generation +- 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) +- Singleplayer mode with simple world generations! - Works offline -- Play with friends over global network! (P2P is powered by Peer.js servers) - First-class touch (mobile) & controller support -- Resource pack support +- First-class keybindings configuration +- Advanced Resource pack support: Custom GUI, all textures. Server resource packs are supported with proper CORS configuration. +- Builtin JEI with recipes & descriptions for almost every item (JEI is creative inventory replacement) +- Custom protocol channel extensions (eg for custom block models in the world) +- Play with friends over internet! (P2P is powered by Peer.js discovery servers) +- ~~Google Drive support for reading / saving worlds back to the cloud~~ +- 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) + +### 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: + +**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. In case of opening zip files they are stored in your ram entirely, so there is a ~300mb file limit on IOS. Whatever offline mode you used (zip, folder, just single player), you can always export world with the `/export` command typed in the game chat. -### Servers +![docs-assets/singleplayer-future-city-1-10-2.jpg](./docs-assets/singleplayer-future-city-1-10-2.jpg) -You can play almost on any server, supporting offline connections. +### Servers & Proxy + +You can play almost on any Java server, vanilla servers are fully supported. See the [Mineflayer](https://github.com/PrismarineJS/mineflayer) repo for the list of supported versions (should support majority of versions). -There is a builtin proxy, but you can also host a your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field. -MS account authentication will be supported soon. +There is a builtin proxy, but you can also host your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field. Or you can deploy it to the cloud service: -<!-- TODO proxy server communication graph --> +[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](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) -### Things that are not planned yet +> **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. -- Mods, plugins (basically JARs) support, shaders - since they all are related to specific game pipelines +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 +graph LR + A[Web App - Client] --> C[Proxy Server] + C --> B[Minecraft Server] + style A fill:#f9d,stroke:#333,stroke-width:2px + style B fill:#fc0,stroke:#333,stroke-width:2px + style C fill:#fff,stroke:#333,stroke-width:2px +``` + +So if the server is located in Europe and you are connecting from Europe, you will have ~40ms ping (~180ms with residential proxy version), however if you are in the US and connecting to the server located in US, you will have >200ms ping, which is the worst case scenario. + +Again, the proxy server is not a part of the client, it is a separate service that you can host yourself. + +### Docker Files + +- [Main Dockerfile](./Dockerfile) - for production build & offline/private usage. Includes full web-app + proxy server for connecting to Minecraft servers. +- [Proxy Dockerfile](./Dockerfile.proxy) - for proxy server only - that one you would be able to specify in the proxy field on the client (`docker build . -f Dockerfile.proxy -t minecraft-web-proxy`) + +In case of using docker, you don't have to follow preparation steps from CONTRIBUTING.MD, like installing Node.js, pnpm, etc. + +### Rendering + +#### Three.js Renderer + +- Uses WebGL2. Chunks are rendered using Geometry Buffers prepared by 4 mesher workers. +- Entities & text rendering +- Supports resource packs +- Doesn't support occlusion culling ### Advanced Settings @@ -44,31 +117,31 @@ There are many many settings, that are not exposed in the UI yet. You can find o ### Console -To open the console, press `F12`, or if you are on mobile, you can type `#debug` in the URL (browser address bar), it wont't reload the page, but you will see a button to open the console. This way you can change advanced settings and see all errors or warnings. Also this way you can access global variables (described below). +To open the console, press `F12`, or if you are on mobile, you can type `#dev` in the URL (browser address bar), it wont't reload the page, but you will see a button to open the console. This way you can change advanced settings and see all errors or warnings. Also this way you can access global variables (described below). -### Debugging +### Development, Debugging & Contributing -It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info. +It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info. Also you can look at Dockerfile for reference. -There is storybook for fast UI development. Run `pnpm storybook` to start it. -There is world renderer playground ([link](https://mcon.vercel.app/playground.html)). +There is world renderer playground ([link](https://mcon.vercel.app/playground/)). -However, there are many things that can be done in online version. You can access some global variables in the console and useful examples: +However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples: -- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam. +- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam. Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name - `bot` - Mineflayer bot instance. See Mineflayer documentation for more. -- `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. +- `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. - `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` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more. +- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more. - `localServer.overworld.storageProvider.regions` - See ALL LOADED region files with all raw data. +- `localServer.levelData.LevelName = 'test'; localServer.writeLevelDat()` - Change name of the world - `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read. -The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `viewer.camera.position` to see the camera position and so on. +The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `world.getCameraPosition()` to see the camera position and so on. <img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/> @@ -87,22 +160,79 @@ world chunks have a *yellow* border, hostile mobs have a *red* outline, passive Press `Y` to set query parameters to url of your current game state. -- `?server=<server_address>` - Display connect screen to the server on load -- `?username=<username>` - Set the username on load -- `?proxy=<proxy_address>` - Set the proxy server address on load -- `?version=<version>` - Set the version on load -- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing. -<!-- - `?password=<password>` - Set the password on load --> +There are some parameters you can set in the url to archive some specific behaviors: + +General: + +- **`?setting=<setting_name>:<setting_value>`** - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4` +- `?modal=<modal>` - Open specific modal on page load eg `keybindings`. Very useful on UI changes testing during dev. For path use `,` as separator. To get currently opened modal type this in the console: `activeModalStack.at(-1).reactType` +- `?replayFileUrl=<url>` - Load and start a packet replay session from a URL with a integrated server. For debugging / previewing recorded sessions. The file must be CORS enabled. + +Server specific: + +- `?ip=<server_address>` - Display connect screen to the server on load with predefined server ip. `:<port>` is optional and can be added to the ip. +- `?name=<name>` - Set the server name for saving to the server list +- `?version=<version>` - Set the version for the server +- `?proxy=<proxy_address>` - Set the proxy server address to use for the server +- `?username=<username>` - Set the username for the server +- `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes. +- `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes. +- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs. +- `?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: + - `?loadSave=<save_name>` - Load the save on load with the specified folder name (not title) -- `?singleplayer=1` - Create empty world on load. Nothing will be saved -- `?noSave=true` - Disable auto save on unload / disconnect / export. Only manual save with `/save` command will work +- `?singleplayer=1` or `?sp=1` - Create empty world on load. Nothing will be saved +- `?version=<version>` - Set the version for the singleplayer world (when used with `?singleplayer=1`) +- `?noSave=true` - Disable auto save on unload / disconnect / export whenever a world is loaded. Only manual save with `/save` command will work. +- `?serverSetting=<key>:<value>` - Set local server [options](https://github.com/zardoy/space-squid/tree/everything/src/modules.ts#L51). For example `?serverSetting=noInitialChunksSend:true` will disable initial chunks loading on the loading screen. +- `?map=<map_url>` - Load the map from ZIP. You can use any url, but it must be **CORS enabled**. +- `?mapDir=<index_file_url>` - Load the map from a file descriptor. It's recommended and the fastest way to load world but requires additional setup. The file must be in the following format: + +```json +{ + "baseUrl": "<url>", + "index": { + "level.dat": null, + "region": { + "r.-1.-1.mca": null, + "r.-1.0.mca": null, + "r.0.-1.mca": null, + "r.0.0.mca": null, + } + } +} +``` + +Note that `mapDir` also accepts base64 encoded JSON like so: +`?mapDir=data:application/json;base64,...` where `...` is the base64 encoded JSON of the index file. +In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the files from index. + +- `?mapDirBaseUrl` - See above. + +Only during development: + +- `?reconnect=true` - Reconnect to the server on page reloads. Very useful on server testing. + +<!-- - `?mapDirGuess=<base_url>` - Load the map from the provided URL and paths will be guessed with a few additional fetch requests. --> ### Notable Things that Power this Project - [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked -- [Flying Squid](https://github.com/prismarineJS/flying-squid) - The builtin server that makes single player possible! Here forked version is used. +- [Forked Flying Squid (Space Squid)](https://github.com/zardoy/space-squid) - The builtin offline server that makes single player & P2P possible! - [Prismarine Provider Anvil](https://github.com/PrismarineJS/prismarine-provider-anvil) - Handles world loading (region format) - [Prismarine Physics](https://github.com/PrismarineJS/prismarine-physics) - Does all the physics calculations - [Minecraft Protocol](https://github.com/PrismarineJS/node-minecraft-protocol) - Makes connections to servers possible - [Peer.js](https://peerjs.com/) - P2P networking (when you open to wan) - [Three.js](https://threejs.org/) - Helping in 3D rendering + +### Things that are not planned yet + +- Mods, plugins (basically JARs) support, shaders - since they all are related to specific game pipelines + +### Alternatives + +- [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 diff --git a/README.NPM.MD b/README.NPM.MD new file mode 100644 index 00000000..dc2c7c72 --- /dev/null +++ b/README.NPM.MD @@ -0,0 +1,36 @@ +# Minecraft React + +Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun) project. + +```bash +pnpm i minecraft-react +``` + +![demo](./docs-assets/npm-banner.jpeg) + +## Usage + +```jsx +import { Scoreboard } from 'minecraft-react' + +const App = () => { + return ( + <Scoreboard + open + title="Scoreboard" + items={[ + { name: 'Player 1', value: 10 }, + { name: 'Player 2', value: 20 }, + { name: 'Player 3', value: 30 }, + ]} + /> + ) +} +``` + +See [Storybook](https://mcraft.fun/storybook/) or [Storybook (Mirror link)](https://mcon.vercel.app/storybook/) for more examples and full components list. Also take a look at the full [standalone example](https://github.com/zardoy/minecraft-web-client/tree/experiments/UiStandaloneExample.tsx). + +There are two types of components: + +- Small UI components or HUD components +- Full screen components (like sign editor, worlds selector) diff --git a/TECH.md b/TECH.md new file mode 100644 index 00000000..2d15993a --- /dev/null +++ b/TECH.md @@ -0,0 +1,58 @@ +### Eaglercraft Comparison + +This project uses proxies so you can connect to almost any vanilla server. Though proxies have some limitations such as increased latency and servers will complain about using VPN (though we have a workaround for that, but ping will be much higher). +This client generally has better performance but some features reproduction might be inaccurate eg its less stable and more buggy in some cases. + +| Feature | This project | Eaglercraft | Description | +| --------------------------------- | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| General | | | | +| Mobile Support (touch) | ✅(+) | ✅ | | +| 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 (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 | +| 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) | +| 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 | +| 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 | + +Features available to only this project: + +- CSS & JS Customization +- JS Real Time Debugging & Console Scripting (eg Devtools) + +### Tech Stack + +Bundler: Rsbuild! +UI: powered by React and css modules. Storybook helps with UI development. + +### Rare WEB Features + +There are a number of web features that are not commonly used but you might be interested in them if you decide to build your own game in the web. + +TODO + +| API | Usage & Description | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `Crypto` API | Used to make chat features work when joining online servers with authentication. | +| `requestPointerLock({ unadjustedMovement: true })` API | Required for games. Disables system mouse acceleration (important for Mac users). Aka mouse raw input | +| `navigator.keyboard.lock()` | (only in Chromium browsers) When entering fullscreen it allows to use any key combination like ctrl+w in the game | +| `navigator.keyboard.getLayoutMap()` | (only in Chromium browsers) To display the right keyboard symbol for the key keybinding on different keyboard layouts (e.g. QWERTY vs AZERTY) | diff --git a/assets/extra-textures/background/panorama_0.png b/assets/background/panorama_0.png similarity index 100% rename from assets/extra-textures/background/panorama_0.png rename to assets/background/panorama_0.png diff --git a/assets/extra-textures/background/panorama_1.png b/assets/background/panorama_1.png similarity index 100% rename from assets/extra-textures/background/panorama_1.png rename to assets/background/panorama_1.png diff --git a/assets/extra-textures/background/panorama_2.png b/assets/background/panorama_2.png similarity index 100% rename from assets/extra-textures/background/panorama_2.png rename to assets/background/panorama_2.png diff --git a/assets/extra-textures/background/panorama_3.png b/assets/background/panorama_3.png similarity index 100% rename from assets/extra-textures/background/panorama_3.png rename to assets/background/panorama_3.png diff --git a/assets/extra-textures/background/panorama_4.png b/assets/background/panorama_4.png similarity index 100% rename from assets/extra-textures/background/panorama_4.png rename to assets/background/panorama_4.png diff --git a/assets/extra-textures/background/panorama_5.png b/assets/background/panorama_5.png similarity index 100% rename from assets/extra-textures/background/panorama_5.png rename to assets/background/panorama_5.png diff --git a/assets/config.html b/assets/config.html new file mode 100644 index 00000000..9bd2dd8e --- /dev/null +++ b/assets/config.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Configure client + + + +
+ + + + + +
+ + + diff --git a/assets/customTextures/readme.md b/assets/customTextures/readme.md new file mode 100644 index 00000000..e2a78c20 --- /dev/null +++ b/assets/customTextures/readme.md @@ -0,0 +1,2 @@ +here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png +get file names from here (blocks/items) https://zardoy.github.io/mc-assets/ diff --git a/assets/debug-inputs.html b/assets/debug-inputs.html new file mode 100644 index 00000000..584fe4d7 --- /dev/null +++ b/assets/debug-inputs.html @@ -0,0 +1,237 @@ + + + + + + Web Input Debugger + + + +
+ +
+ +
+
W
+
A
+
S
+
D
+
+ +
+
Ctrl
+
+ +
+
Space
+
+ + + + diff --git a/assets/destroy_stage_0.png b/assets/destroy_stage_0.png new file mode 100644 index 00000000..f65b7ede Binary files /dev/null and b/assets/destroy_stage_0.png differ diff --git a/assets/destroy_stage_1.png b/assets/destroy_stage_1.png new file mode 100644 index 00000000..7c915961 Binary files /dev/null and b/assets/destroy_stage_1.png differ diff --git a/assets/destroy_stage_2.png b/assets/destroy_stage_2.png new file mode 100644 index 00000000..dadd6b05 Binary files /dev/null and b/assets/destroy_stage_2.png differ diff --git a/assets/destroy_stage_3.png b/assets/destroy_stage_3.png new file mode 100644 index 00000000..52a40b65 Binary files /dev/null and b/assets/destroy_stage_3.png differ diff --git a/assets/destroy_stage_4.png b/assets/destroy_stage_4.png new file mode 100644 index 00000000..e37c88a2 Binary files /dev/null and b/assets/destroy_stage_4.png differ diff --git a/assets/destroy_stage_5.png b/assets/destroy_stage_5.png new file mode 100644 index 00000000..9590d2f7 Binary files /dev/null and b/assets/destroy_stage_5.png differ diff --git a/assets/destroy_stage_6.png b/assets/destroy_stage_6.png new file mode 100644 index 00000000..fb00ade5 Binary files /dev/null and b/assets/destroy_stage_6.png differ diff --git a/assets/destroy_stage_7.png b/assets/destroy_stage_7.png new file mode 100644 index 00000000..0b40c789 Binary files /dev/null and b/assets/destroy_stage_7.png differ diff --git a/assets/destroy_stage_8.png b/assets/destroy_stage_8.png new file mode 100644 index 00000000..c0bf1dec Binary files /dev/null and b/assets/destroy_stage_8.png differ diff --git a/assets/destroy_stage_9.png b/assets/destroy_stage_9.png new file mode 100644 index 00000000..e3185f82 Binary files /dev/null and b/assets/destroy_stage_9.png differ diff --git a/assets/extra-textures/edition.png b/assets/edition.png similarity index 100% rename from assets/extra-textures/edition.png rename to assets/edition.png diff --git a/assets/extra-textures/loading.png b/assets/extra-textures/loading.png deleted file mode 100644 index 4f6a121a..00000000 Binary files a/assets/extra-textures/loading.png and /dev/null differ diff --git a/assets/favicon.ico b/assets/favicon.ico deleted file mode 100644 index 27f48c13..00000000 Binary files a/assets/favicon.ico and /dev/null differ diff --git a/assets/favicon.png b/assets/favicon.png index 0b46a032..046cacd0 100644 Binary files a/assets/favicon.png and b/assets/favicon.png differ diff --git a/assets/favicon.svg b/assets/favicon.svg deleted file mode 100644 index 6b9f69bf..00000000 --- a/assets/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/extra-textures/gui.png b/assets/gui.png similarity index 100% rename from assets/extra-textures/gui.png rename to assets/gui.png diff --git a/assets/invsprite.png b/assets/invsprite.png deleted file mode 100644 index d3022e5e..00000000 Binary files a/assets/invsprite.png and /dev/null differ diff --git a/assets/manifest.json b/assets/manifest.json index e6e2068e..4310ae7f 100644 --- a/assets/manifest.json +++ b/assets/manifest.json @@ -1,12 +1,12 @@ { - "name": "Prismarine Web Client", - "short_name": "Prismarine Web Client", + "name": "Minecraft Web Client", + "short_name": "Minecraft Web Client", "scope": "./", "start_url": "./", "icons": [ { "src": "favicon.png", - "sizes": "512x512" + "sizes": "720x720" } ], "background_color": "#349474", diff --git a/assets/playground.html b/assets/playground.html new file mode 100644 index 00000000..8c394f91 --- /dev/null +++ b/assets/playground.html @@ -0,0 +1,4 @@ + + diff --git a/config.json b/config.json index b7fa1d7e..2bfa9cfe 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,80 @@ { "version": 1, "defaultHost": "", - "defaultProxy": "proxy.mcraft.fun", - "defaultVersion": "1.18.2", - "mapsProvider": "https://maps.mcraft.fun/" + "defaultProxy": "https://proxy.mcraft.fun", + "mapsProvider": "https://maps.mcraft.fun/", + "skinTexturesProxy": "", + "peerJsServer": "", + "peerJsServerFallback": "https://p2p.mcraft.fun", + "promoteServers": [ + { + "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": [ + [ + { + "type": "github" + }, + { + "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": "" + } + ] } diff --git a/config.mcraft-only.json b/config.mcraft-only.json new file mode 100644 index 00000000..52a3aa2c --- /dev/null +++ b/config.mcraft-only.json @@ -0,0 +1,5 @@ +{ + "alwaysReconnectButton": true, + "reportBugButtonWithReconnect": true, + "allowAutoConnect": true +} diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000..3bf2c720 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from 'cypress' + +const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true' + +export default defineConfig({ + video: false, + chromeWebSecurity: false, + screenshotOnRunFailure: true, // Enable screenshots on test failures + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents (on, config) { + // https://medium.com/automation-with-donald/get-memory-consumption-of-web-app-with-cypress-84e2656e5a0f + on('before:browser:launch', (browser = { + name: "", + family: "chromium", + channel: "", + displayName: "", + version: "", + majorVersion: "", + path: "", + isHeaded: false, + isHeadless: false + }, launchOptions) => { + if (browser.family === 'chromium' && browser.name !== 'electron') { + // auto open devtools + launchOptions.args.push('--enable-precise-memory-info') + } + + return launchOptions + + }) + + 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', + excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'], + }, +}) diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 63bfb351..00000000 --- a/cypress.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/cypress-io/cypress/188b9a742ee2ef51102167bfd84b3696a3f72a26/cli/schema/cypress.schema.json", - "baseUrl": "http://localhost:8080", - "testFiles": "**/*.spec.ts", - "video": false, - "chromeWebSecurity": false, - "ignoreTestFiles": [ - "**/__snapshots__/*", - "**/__image_snapshots__/*" - ] -} diff --git a/cypress/integration/__image_snapshots__/superflat-world #0.png b/cypress/e2e/__image_snapshots__/superflat-world #0.png similarity index 100% rename from cypress/integration/__image_snapshots__/superflat-world #0.png rename to cypress/e2e/__image_snapshots__/superflat-world #0.png diff --git a/cypress/integration/__image_snapshots__/superflat-world #1.png b/cypress/e2e/__image_snapshots__/superflat-world #1.png similarity index 100% rename from cypress/integration/__image_snapshots__/superflat-world #1.png rename to cypress/e2e/__image_snapshots__/superflat-world #1.png diff --git a/cypress/integration/__image_snapshots__/superflat-world #2.png b/cypress/e2e/__image_snapshots__/superflat-world #2.png similarity index 100% rename from cypress/integration/__image_snapshots__/superflat-world #2.png rename to cypress/e2e/__image_snapshots__/superflat-world #2.png diff --git a/cypress/e2e/rendering_performance.spec.ts b/cypress/e2e/rendering_performance.spec.ts new file mode 100644 index 00000000..2ca85329 --- /dev/null +++ b/cypress/e2e/rendering_performance.spec.ts @@ -0,0 +1,32 @@ +/// +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')) + }) + }) +}) diff --git a/cypress/e2e/shared.ts b/cypress/e2e/shared.ts new file mode 100644 index 00000000..47518f1b --- /dev/null +++ b/cypress/e2e/shared.ts @@ -0,0 +1,18 @@ +import { AppOptions } from '../../src/optionsStorage' + +export const cleanVisit = (url?) => { + cy.clearLocalStorage() + visit(url) + window.localStorage.options = { + chatOpacity: 0 + } +} +export const visit = (url = '/') => { + window.localStorage.cypress = 'true' + cy.visit(url) +} +export const setOptions = (options: Partial) => { + cy.window().then(win => { + Object.assign(win['options'], options) + }) +} diff --git a/cypress/e2e/smoke.spec.ts b/cypress/e2e/smoke.spec.ts new file mode 100644 index 00000000..ae110155 --- /dev/null +++ b/cypress/e2e/smoke.spec.ts @@ -0,0 +1,108 @@ +/* eslint-disable max-nested-callbacks */ +/// +import supportedVersions from '../../src/supportedVersions.mjs' +import { setOptions, cleanVisit, visit } from './shared' + +// todo use ssl + +const compareRenderedFlatWorld = () => { + // wait for render + // cy.wait(6000) + // cy.get('body').toMatchImageSnapshot({ + // name: 'superflat-world', + // }) +} + +const testWorldLoad = () => { + return cy.document().then({ timeout: 35_000 }, doc => { + return new Cypress.Promise(resolve => { + doc.addEventListener('cypress-world-ready', resolve) + }) + }).then(() => { + compareRenderedFlatWorld() + }) +} + +it('Loads & renders singleplayer', () => { + cleanVisit('/?singleplayer=1') + setOptions({ + localServerOptions: { + generation: { + name: 'superflat', + // eslint-disable-next-line unicorn/numeric-separators-style + options: { seed: 250869072 } + }, + }, + renderDistance: 2 + }) + testWorldLoad() +}) + +it.skip('Joins to local flying-squid server', () => { + visit('/?ip=localhost&version=1.16.1') + window.localStorage.version = '' + // todo replace with data-test + // cy.get('[data-test-id="servers-screen-button"]').click() + // cy.get('[data-test-id="server-ip"]').clear().focus().type('localhost') + // cy.get('[data-test-id="version"]').clear().focus().type('1.16.1') // todo needs to fix autoversion + cy.get('[data-test-id="connect-qs"]').click() // todo! cypress sometimes doesn't click + testWorldLoad() +}) + +it.skip('Joins to local latest Java vanilla server', () => { + const version = supportedVersions.at(-1)! + cy.task('startServer', [version, 25_590]).then(() => { + visit('/?ip=localhost:25590&username=bot') + cy.get('[data-test-id="connect-qs"]').click() + testWorldLoad().then(() => { + let x = 0 + let z = 0 + cy.window().then((win) => { + x = win.bot.entity.position.x + z = win.bot.entity.position.z + }) + cy.document().trigger('keydown', { code: 'KeyW' }) + cy.wait(1500).then(() => { + cy.document().trigger('keyup', { code: 'KeyW' }) + cy.window().then(async (win) => { + // eslint-disable-next-line prefer-destructuring + const bot: typeof __type_bot = win.bot + // todo use f3 stats instead + if (bot.entity.position.x === x && bot.entity.position.z === z) { + throw new Error('Player not moved') + } + + bot.chat('Hello') // todo assert + bot.chat('/gamemode creative') + // bot.on('message', () => { + void bot.creative.setInventorySlot(bot.inventory.hotbarStart, new win.PrismarineItem(1, 1, 0)) + // }) + await bot.lookAt(bot.entity.position.offset(1, 0, 1)) + }).then(() => { + cy.document().trigger('mousedown', { button: 2, isTrusted: true, force: true }) // right click + cy.document().trigger('mouseup', { button: 2, isTrusted: true, force: true }) + cy.wait(1000) + }) + }) + }) + }) +}) + +it('Loads & renders zip world', () => { + cleanVisit() + cy.get('[data-test-id="select-file-folder"]').click({ shiftKey: true }) + cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true }) + testWorldLoad() +}) + + +it.skip('Loads & renders world from folder', () => { + cleanVisit() + // dragndrop folder + cy.get('[data-test-id="select-file-folder"]').click() + cy.get('input[type="file"]').selectFile('server-jar/world', { + force: true, + // action: 'drag-drop', + }) + testWorldLoad() +}) diff --git a/cypress/integration/index.spec.ts b/cypress/integration/index.spec.ts deleted file mode 100644 index 8b168bf1..00000000 --- a/cypress/integration/index.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -/// -import type { AppOptions } from '../../src/optionsStorage' - -const cleanVisit = (url?) => { - cy.clearLocalStorage() - visit(url) -} - -const visit = (url = '/') => { - window.localStorage.cypress = 'true' - cy.visit(url) -} - -// todo use ssl - -const compareRenderedFlatWorld = () => { - // wait for render - // cy.wait(6000) - // cy.get('body').toMatchImageSnapshot({ - // name: 'superflat-world', - // }) -} - -const testWorldLoad = () => { - cy.document().then({ timeout: 20_000 }, doc => { - return new Cypress.Promise(resolve => { - doc.addEventListener('cypress-world-ready', resolve) - }) - }).then(() => { - compareRenderedFlatWorld() - }) -} - -const setOptions = (options: Partial) => { - cy.window().then(win => { - Object.assign(win['options'], options) - }) -} - -it('Loads & renders singleplayer', () => { - cleanVisit('/?singleplayer=1') - setOptions({ - localServerOptions: { - generation: { - name: 'superflat', - // eslint-disable-next-line unicorn/numeric-separators-style - options: { seed: 250869072 } - }, - }, - renderDistance: 2 - }) - testWorldLoad() -}) - -it.only('Joins to server', () => { - // visit('/?version=1.16.1') - window.localStorage.version = '' - visit() - // todo replace with data-test - cy.get('[data-test-id="connect-screen-button"]', { includeShadowDom: true }).click() - cy.get('input#serverip', { includeShadowDom: true }).clear().focus().type('localhost') - cy.get('input#botversion', { includeShadowDom: true }).clear().focus().type('1.16.1') // todo needs to fix autoversion - cy.get('[data-test-id="connect-to-server"]', { includeShadowDom: true }).click() - testWorldLoad() -}) - -it('Loads & renders zip world', () => { - cleanVisit() - cy.get('[data-test-id="select-file-folder"]', { includeShadowDom: true }).click({ shiftKey: true }) - cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true }) - testWorldLoad() -}) - -it.skip('Performance test', () => { - // select that world - // from -2 85 24 - // await bot.loadPlugin(pathfinder.pathfinder) - // bot.pathfinder.goto(new pathfinder.goals.GoalXZ(28, -28)) -}) diff --git a/cypress/minecraft-server.mjs b/cypress/minecraft-server.mjs index 18f4e3db..ea7bbcd1 100644 --- a/cypress/minecraft-server.mjs +++ b/cypress/minecraft-server.mjs @@ -1,7 +1,8 @@ //@ts-check import mcServer from 'flying-squid' -import defaultOptions from 'flying-squid/config/default-settings.json' assert { type: 'json' } +import defaultOptions from 'flying-squid/config/default-settings.json' with { type: 'json' } +/** @type {Options} */ const serverOptions = { ...defaultOptions, 'online-mode': false, diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 35dc2989..e55f5d26 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -2,11 +2,13 @@ const { cypressEsbuildPreprocessor } = require('cypress-esbuild-preprocessor') const { initPlugin } = require('cypress-plugin-snapshots/plugin') const polyfill = require('esbuild-plugin-polyfill-node') +const { startMinecraftServer } = require('./startServer') module.exports = (on, config) => { initPlugin(on, config) on('file:preprocessor', cypressEsbuildPreprocessor({ esbuildOptions: { + sourcemap: true, plugins: [ polyfill.polyfillNode({ polyfills: { @@ -17,10 +19,15 @@ module.exports = (on, config) => { }, })) on('task', { - log (message) { + log(message) { console.log(message) return null }, }) + on('task', { + async startServer([version, port]) { + return startMinecraftServer(version, port) + } + }) return config } diff --git a/cypress/plugins/ops.json b/cypress/plugins/ops.json new file mode 100644 index 00000000..ade5aaa5 --- /dev/null +++ b/cypress/plugins/ops.json @@ -0,0 +1,8 @@ +[ + { + "uuid": "67128b5b-2e6b-3ad1-baa0-1b937b03e5c5", + "name": "bot", + "level": 4, + "bypassesPlayerLimit": false + } +] diff --git a/cypress/plugins/server.properties b/cypress/plugins/server.properties new file mode 100644 index 00000000..5873a1aa --- /dev/null +++ b/cypress/plugins/server.properties @@ -0,0 +1,61 @@ +#Minecraft server properties +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=peaceful +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=false +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=flat +log-ips=true +max-build-height=256 +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=false +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=25565 +rate-limit=0 +rcon.password= +rcon.port=25575 +require-resource-pack=false +resource-pack= +resource-pack-id= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=25565 +simulation-distance=10 +snooper-enabled=true +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false diff --git a/cypress/plugins/startServer.ts b/cypress/plugins/startServer.ts new file mode 100644 index 00000000..ecf0d210 --- /dev/null +++ b/cypress/plugins/startServer.ts @@ -0,0 +1,45 @@ +import { ChildProcess, spawn } from 'child_process' +import * as fs from 'fs' +import * as path from 'path' +import { promisify } from 'util' +import { downloadServer } from 'minecraft-wrap' +import * as waitOn from 'wait-on' + +let prevProcess: ChildProcess | null = null +export const startMinecraftServer = async (version: string, port: number) => { + if (prevProcess) return null + const jar = `./server-jar/${version}.jar` + + const start = () => { + // if (prevProcess) { + // prevProcess.kill() + // } + + prevProcess = spawn('java', ['-jar', path.basename(jar), 'nogui', '--port', `${port}`], { + stdio: 'inherit', + cwd: path.dirname(jar), + }) + } + + let coldStart = false + if (fs.existsSync(jar)) { + start() + } else { + coldStart = true + promisify(downloadServer)(version, jar).then(() => { + // add eula.txt + fs.writeFileSync(path.join(path.dirname(jar), 'eula.txt'), 'eula=true') + // copy cypress/plugins/server.properties + fs.copyFileSync(path.join(__dirname, 'server.properties'), path.join(path.dirname(jar), 'server.properties')) + // copy ops.json + fs.copyFileSync(path.join(__dirname, 'ops.json'), path.join(path.dirname(jar), 'ops.json')) + start() + }) + } + + return new Promise((res) => { + waitOn({ resources: [`tcp:localhost:${port}`] }, () => { + setTimeout(() => res(null), coldStart ? 6500 : 2000) // todo retry instead of timeout + }) + }) +} diff --git a/cypress/support/index.js b/cypress/support/e2e.js similarity index 100% rename from cypress/support/index.js rename to cypress/support/e2e.js diff --git a/docs-assets/handled-packets.md b/docs-assets/handled-packets.md new file mode 100644 index 00000000..497ec5ec --- /dev/null +++ b/docs-assets/handled-packets.md @@ -0,0 +1,169 @@ +# Handled Packets + +## Server -> Client + +❌ statistics +❌ advancements +❌ face_player +❌ nbt_query_response +❌ chat_suggestions +❌ trade_list +❌ vehicle_move +❌ open_book +❌ craft_recipe_response +❌ end_combat_event +❌ enter_combat_event +❌ unlock_recipes +❌ camera +❌ update_view_position +❌ update_view_distance +❌ entity_sound_effect +❌ stop_sound +❌ feature_flags +❌ select_advancement_tab +❌ declare_recipes +❌ tags +❌ acknowledge_player_digging +❌ initialize_world_border +❌ world_border_center +❌ world_border_lerp_size +❌ world_border_size +❌ world_border_warning_delay +❌ world_border_warning_reach +❌ simulation_distance +❌ chunk_biomes +❌ hurt_animation +✅ damage_event +✅ spawn_entity +✅ spawn_entity_experience_orb +✅ named_entity_spawn +✅ animation +✅ block_break_animation +✅ tile_entity_data +✅ block_action +✅ block_change +✅ boss_bar +✅ difficulty +✅ tab_complete +✅ declare_commands +✅ multi_block_change +✅ close_window +✅ open_window +✅ window_items +✅ craft_progress_bar +✅ set_slot +✅ set_cooldown +✅ custom_payload +✅ hide_message +✅ kick_disconnect +✅ profileless_chat +✅ entity_status +✅ explosion +✅ unload_chunk +✅ game_state_change +✅ open_horse_window +✅ keep_alive +✅ map_chunk +✅ world_event +✅ world_particles +✅ update_light +✅ login +✅ map +✅ rel_entity_move +✅ entity_move_look +✅ entity_look +✅ open_sign_entity +✅ abilities +✅ player_chat +✅ death_combat_event +✅ player_remove +✅ player_info +✅ position +✅ entity_destroy +✅ remove_entity_effect +✅ resource_pack_send +✅ respawn +✅ entity_head_rotation +✅ held_item_slot +✅ scoreboard_display_objective +✅ entity_metadata +✅ attach_entity +✅ entity_velocity +✅ entity_equipment +✅ experience +✅ update_health +✅ scoreboard_objective +✅ set_passengers +✅ teams +✅ scoreboard_score +✅ spawn_position +✅ update_time +✅ sound_effect +✅ system_chat +✅ playerlist_header +✅ collect +✅ entity_teleport +✅ entity_update_attributes +✅ entity_effect +✅ server_data +✅ clear_titles +✅ action_bar +✅ ping +✅ set_title_subtitle +✅ set_title_text +✅ set_title_time +✅ packet + +## Client -> Server + +❌ query_block_nbt +❌ set_difficulty +❌ query_entity_nbt +❌ pick_item +❌ set_beacon_effect +❌ update_command_block_minecart +❌ update_structure_block +❌ generate_structure +❌ lock_difficulty +❌ craft_recipe_request +❌ displayed_recipe +❌ recipe_book +❌ update_jigsaw_block +❌ spectate +❌ advancement_tab +✅ teleport_confirm +✅ chat_command +✅ chat_message +✅ message_acknowledgement +✅ edit_book +✅ name_item +✅ select_trade +✅ update_command_block +✅ tab_complete +✅ client_command +✅ settings +✅ enchant_item +✅ window_click +✅ close_window +✅ custom_payload +✅ use_entity +✅ keep_alive +✅ position +✅ position_look +✅ look +✅ flying +✅ vehicle_move +✅ steer_boat +✅ abilities +✅ block_dig +✅ entity_action +✅ steer_vehicle +✅ resource_pack_receive +✅ held_item_slot +✅ set_creative_slot +✅ update_sign +✅ arm_animation +✅ block_place +✅ use_item +✅ pong +✅ chat_session_update diff --git a/docs-assets/npm-banner.jpeg b/docs-assets/npm-banner.jpeg new file mode 100644 index 00000000..95de07b8 Binary files /dev/null and b/docs-assets/npm-banner.jpeg differ diff --git a/docs-assets/singleplayer-future-city-1-10-2.jpg b/docs-assets/singleplayer-future-city-1-10-2.jpg new file mode 100644 index 00000000..e5be2ada Binary files /dev/null and b/docs-assets/singleplayer-future-city-1-10-2.jpg differ diff --git a/esbuild.mjs b/esbuild.mjs deleted file mode 100644 index 30480e0c..00000000 --- a/esbuild.mjs +++ /dev/null @@ -1,131 +0,0 @@ -//@ts-check -import * as esbuild from 'esbuild' -import fs from 'fs' -// import htmlPlugin from '@chialab/esbuild-plugin-html' -import server from './server.js' -import { clients, plugins, setSingleFileBuild } from './scripts/esbuildPlugins.mjs' -import { generateSW } from 'workbox-build' -import { getSwAdditionalEntries } from './scripts/build.js' -import { build } from 'esbuild' - -//@ts-ignore -try { await import('./localSettings.mjs') } catch { } - -const singleFileBuild = false - -setSingleFileBuild(singleFileBuild) - -fs.writeFileSync('./dist/index.html', fs.readFileSync('index.html', 'utf8').replace('', ''), 'utf8') - -const watch = process.argv.includes('--watch') || process.argv.includes('-w') -const prod = process.argv.includes('--prod') -const dev = !prod - -const banner = [ - 'window.global = globalThis;', - // report reload time - dev && 'if (sessionStorage.lastReload) { const [rebuild, reloadStart] = sessionStorage.lastReload.split(","); const now = Date.now(); console.log(`rebuild + reload:`, +rebuild, "+", now - reloadStart, "=", ((+rebuild + (now - reloadStart)) / 1000).toFixed(1) + "s");sessionStorage.lastReload = ""; }', - // auto-reload - dev && 'window.noAutoReload ??= false;(() => new EventSource("/esbuild").onmessage = ({ data: _data }) => { if (!_data) return; const data = JSON.parse(_data); if (!data.update) return;console.log("[esbuild] Page is outdated");document.title = `[O] ${document.title}`;if (window.noAutoReload || localStorage.noAutoReload) return; if (localStorage.autoReloadVisible && document.visibilityState !== "visible") return; sessionStorage.lastReload = `${data.update.time},${Date.now()}`; location.reload() })();' -].filter(Boolean) - -const buildingVersion = new Date().toISOString().split(':')[0] - -/** @type {import('esbuild').BuildOptions} */ -const buildOptions = { - bundle: true, - entryPoints: ['src/index.ts'], - target: ['es2020'], - jsx: 'automatic', - jsxDev: dev, - // logLevel: 'debug', - logLevel: 'info', - platform: 'browser', - sourcemap: prod ? true : 'inline', - outdir: 'dist', - mainFields: [ - 'browser', 'module', 'main' - ], - keepNames: true, - banner: { - // using \n breaks sourcemaps! - js: banner.join(';'), - }, - alias: { - events: 'events', // make explicit - buffer: 'buffer', - 'fs': 'browserfs/dist/shims/fs.js', - http: 'http-browserify', - perf_hooks: './src/perf_hooks_replacement.js', - crypto: './src/crypto.js', - stream: 'stream-browserify', - net: 'net-browserify', - assert: 'assert', - dns: './src/dns.js' - }, - inject: [ - './src/shims.js' - ], - metafile: true, - plugins, - sourcesContent: !process.argv.includes('--no-sources'), - minify: process.argv.includes('--minify'), - define: { - 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), - 'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'), - 'process.env.GITHUB_URL': - JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`), - 'process.env.DEPS_VERSIONS': JSON.stringify({}) - }, - loader: { - // todo use external or resolve issues with duplicating - '.png': 'dataurl', - '.map': 'empty' - }, - write: false, - // todo would be better to enable? - // preserveSymlinks: true, -} - -if (watch) { - const ctx = await esbuild.context(buildOptions) - await ctx.watch() - server.app.get('/esbuild', (req, res, next) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }) - - // Send a comment to keep the connection alive - res.write(': ping\n\n') - - // Add the client response to the clients array - clients.push(res) - - // Handle any client disconnection logic - res.on('close', () => { - const index = clients.indexOf(res) - if (index !== -1) { - clients.splice(index, 1) - } - }) - }) -} else { - const result = await build(buildOptions) - // console.log(await esbuild.analyzeMetafile(result.metafile)) - - if (prod) { - fs.writeFileSync('dist/version.txt', buildingVersion, 'utf-8') - - const { count, size, warnings } = await generateSW({ - // dontCacheBustURLsMatching: [new RegExp('...')], - globDirectory: 'dist', - skipWaiting: true, - clientsClaim: true, - additionalManifestEntries: getSwAdditionalEntries(), - globPatterns: [], - swDest: 'dist/service-worker.js', - }) - } -} diff --git a/experiments/UiStandaloneExample.tsx b/experiments/UiStandaloneExample.tsx new file mode 100644 index 00000000..80f68e8d --- /dev/null +++ b/experiments/UiStandaloneExample.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react' +import { createRoot } from 'react-dom/client' +import { + Button, + Slider, + ArmorBar, + BreathBar, + Chat, + HealthBar, + PlayerListOverlay, + Scoreboard, + MessageFormattedString, + XPBar, + FoodBar +} from '../dist-npm' + +const ExampleDemo = () => { + const [sliderValue, setSliderValue] = useState(0) + + return ( +
+ + setSliderValue(value)} /> + + { + console.log('typed', message) + // close + }} + /> + + + + + "§bRed" displays as + + +
+ ) +} + +createRoot(document.body as Element).render() diff --git a/experiments/decode.html b/experiments/decode.html new file mode 100644 index 00000000..fd55e622 --- /dev/null +++ b/experiments/decode.html @@ -0,0 +1 @@ + diff --git a/experiments/decode.ts b/experiments/decode.ts new file mode 100644 index 00000000..6d0f876d --- /dev/null +++ b/experiments/decode.ts @@ -0,0 +1,26 @@ +// Include the pako library +import pako from 'pako'; +import compressedJsRaw from './compressed.js?raw' + +function decompressFromBase64(input) { + // Decode the Base64 string + const binaryString = atob(input); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + // Convert the binary string to a byte array + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Decompress the byte array + const decompressedData = pako.inflate(bytes, { to: 'string' }); + + return decompressedData; +} + +// Use the function +console.time('decompress'); +const decompressedData = decompressFromBase64(compressedJsRaw); +console.timeEnd('decompress') +console.log(decompressedData) diff --git a/experiments/ios-safe-area-bottom-bug.html b/experiments/ios-safe-area-bottom-bug.html new file mode 100644 index 00000000..53d867f5 --- /dev/null +++ b/experiments/ios-safe-area-bottom-bug.html @@ -0,0 +1,15 @@ + +
+ bottom: env(safe-area-inset-bottom) +
diff --git a/experiments/state.html b/experiments/state.html new file mode 100644 index 00000000..7a5282b7 --- /dev/null +++ b/experiments/state.html @@ -0,0 +1 @@ + diff --git a/experiments/state.ts b/experiments/state.ts new file mode 100644 index 00000000..b01523fc --- /dev/null +++ b/experiments/state.ts @@ -0,0 +1,37 @@ +import { SmoothSwitcher } from '../renderer/viewer/lib/smoothSwitcher' + +const div = document.createElement('div') +div.style.width = '100px' +div.style.height = '100px' +div.style.backgroundColor = 'red' +document.body.appendChild(div) + +const pos = {x: 0, y: 0} + +const positionSwitcher = new SmoothSwitcher(() => pos, (key, value) => { + pos[key] = value +}) +globalThis.positionSwitcher = positionSwitcher + +document.body.addEventListener('keydown', e => { + if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { + const to = { + x: e.code === 'ArrowLeft' ? -100 : 100 + } + console.log(pos, to) + positionSwitcher.transitionTo(to, e.code === 'ArrowLeft' ? 'Left' : 'Right', () => { + console.log('Switched to ', e.code === 'ArrowLeft' ? 'Left' : 'Right') + }) + } + if (e.code === 'Space') { + pos.x = 200 + } +}) + +const render = () => { + positionSwitcher.update() + div.style.transform = `translate(${pos.x}px, ${pos.y}px)` + requestAnimationFrame(render) +} + +render() diff --git a/experiments/texture-render.html b/experiments/texture-render.html deleted file mode 100644 index be406102..00000000 --- a/experiments/texture-render.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - Document - - - - - - - diff --git a/experiments/three-item.html b/experiments/three-item.html new file mode 100644 index 00000000..70155c50 --- /dev/null +++ b/experiments/three-item.html @@ -0,0 +1,13 @@ + + + + Minecraft Item Viewer + + + + + + diff --git a/experiments/three-item.ts b/experiments/three-item.ts new file mode 100644 index 00000000..b9d492fe --- /dev/null +++ b/experiments/three-item.ts @@ -0,0 +1,108 @@ +import * as THREE from 'three' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' +import itemsAtlas from 'mc-assets/dist/itemsAtlasLegacy.png' +import { createItemMeshFromCanvas, createItemMesh } from '../renderer/viewer/three/itemMesh' + +// Create scene, camera and renderer +const scene = new THREE.Scene() +const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) +const renderer = new THREE.WebGLRenderer({ antialias: true }) +renderer.setSize(window.innerWidth, window.innerHeight) +document.body.appendChild(renderer.domElement) + +// Setup camera and controls +camera.position.set(0, 0, 3) +const controls = new OrbitControls(camera, renderer.domElement) +controls.enableDamping = true + +// Background and lights +scene.background = new THREE.Color(0x333333) +const ambientLight = new THREE.AmbientLight(0xffffff, 0.7) +scene.add(ambientLight) + +// Animation loop +function animate () { + requestAnimationFrame(animate) + controls.update() + renderer.render(scene, camera) +} + +async function setupItemMesh () { + try { + const loader = new THREE.TextureLoader() + const atlasTexture = await loader.loadAsync(itemsAtlas) + + // Pixel-art configuration + atlasTexture.magFilter = THREE.NearestFilter + atlasTexture.minFilter = THREE.NearestFilter + atlasTexture.generateMipmaps = false + atlasTexture.wrapS = atlasTexture.wrapT = THREE.ClampToEdgeWrapping + + // Extract the tile at x=2, y=0 (16x16) + const tileSize = 16 + const tileX = 2 + const tileY = 0 + + const canvas = document.createElement('canvas') + canvas.width = tileSize + canvas.height = tileSize + const ctx = canvas.getContext('2d')! + + ctx.imageSmoothingEnabled = false + ctx.drawImage( + atlasTexture.image, + tileX * tileSize, + tileY * tileSize, + tileSize, + tileSize, + 0, + 0, + tileSize, + tileSize + ) + + // Test both approaches - working manual extraction: + const meshOld = createItemMeshFromCanvas(canvas, { depth: 0.1 }) + meshOld.position.x = -1 + meshOld.rotation.x = -Math.PI / 12 + meshOld.rotation.y = Math.PI / 12 + scene.add(meshOld) + + // And new unified function: + const atlasWidth = atlasTexture.image.width + const atlasHeight = atlasTexture.image.height + const u = (tileX * tileSize) / atlasWidth + const v = (tileY * tileSize) / atlasHeight + const sizeX = tileSize / atlasWidth + const sizeY = tileSize / atlasHeight + + console.log('Debug texture coords:', {u, v, sizeX, sizeY, atlasWidth, atlasHeight}) + + const resultNew = createItemMesh(atlasTexture, { + u, v, sizeX, sizeY + }, { + faceCamera: false, + use3D: true, + depth: 0.1 + }) + + resultNew.mesh.position.x = 1 + resultNew.mesh.rotation.x = -Math.PI / 12 + resultNew.mesh.rotation.y = Math.PI / 12 + scene.add(resultNew.mesh) + + animate() + } catch (err) { + console.error('Failed to create item mesh:', err) + } +} + +// Handle window resize +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() + renderer.setSize(window.innerWidth, window.innerHeight) +}) + +// Start +setupItemMesh() diff --git a/experiments/three-labels.html b/experiments/three-labels.html new file mode 100644 index 00000000..2b25bc23 --- /dev/null +++ b/experiments/three-labels.html @@ -0,0 +1,5 @@ + + diff --git a/experiments/three-labels.ts b/experiments/three-labels.ts new file mode 100644 index 00000000..b69dc95b --- /dev/null +++ b/experiments/three-labels.ts @@ -0,0 +1,67 @@ +import * as THREE from 'three' +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js' +import { createWaypointSprite, WAYPOINT_CONFIG } from '../renderer/viewer/three/waypointSprite' + +// Create scene, camera and renderer +const scene = new THREE.Scene() +const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) +const renderer = new THREE.WebGLRenderer({ antialias: true }) +renderer.setSize(window.innerWidth, window.innerHeight) +document.body.appendChild(renderer.domElement) + +// Add FirstPersonControls +const controls = new FirstPersonControls(camera, renderer.domElement) +controls.lookSpeed = 0.1 +controls.movementSpeed = 10 +controls.lookVertical = true +controls.constrainVertical = true +controls.verticalMin = 0.1 +controls.verticalMax = Math.PI - 0.1 + +// Position camera +camera.position.y = 1.6 // Typical eye height +camera.lookAt(0, 1.6, -1) + +// Create a helper grid and axes +const grid = new THREE.GridHelper(20, 20) +scene.add(grid) +const axes = new THREE.AxesHelper(5) +scene.add(axes) + +// Create waypoint sprite via utility +const waypoint = createWaypointSprite({ + position: new THREE.Vector3(0, 0, -5), + color: 0xff0000, + label: 'Target', +}) +scene.add(waypoint.group) + +// Use built-in offscreen arrow from utils +waypoint.enableOffscreenArrow(true) +waypoint.setArrowParent(scene) + +// Animation loop +function animate() { + requestAnimationFrame(animate) + + const delta = Math.min(clock.getDelta(), 0.1) + controls.update(delta) + + // Unified camera update (size, distance text, arrow, visibility) + const sizeVec = renderer.getSize(new THREE.Vector2()) + waypoint.updateForCamera(camera.position, camera, sizeVec.width, sizeVec.height) + + renderer.render(scene, camera) +} + +// Handle window resize +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() + renderer.setSize(window.innerWidth, window.innerHeight) +}) + +// Add clock for controls +const clock = new THREE.Clock() + +animate() diff --git a/experiments/three.html b/experiments/three.html new file mode 100644 index 00000000..8765081b --- /dev/null +++ b/experiments/three.html @@ -0,0 +1 @@ + diff --git a/experiments/three.ts b/experiments/three.ts new file mode 100644 index 00000000..21142b5f --- /dev/null +++ b/experiments/three.ts @@ -0,0 +1,60 @@ +import * as THREE from 'three' + +// 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() +renderer.setSize(window.innerWidth, window.innerHeight) +document.body.appendChild(renderer.domElement) + +// Position camera +camera.position.z = 5 + +// Create a canvas with some content +const canvas = document.createElement('canvas') +canvas.width = 256 +canvas.height = 256 +const ctx = canvas.getContext('2d') + +scene.background = new THREE.Color(0x444444) + +// 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) + +// 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) +} +animate() diff --git a/index.html b/index.html index 0cc59068..b2fa3dbd 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,22 @@ + - +
diff --git a/renderer/playground/allEntitiesDebug.ts b/renderer/playground/allEntitiesDebug.ts new file mode 100644 index 00000000..5bc56ca6 --- /dev/null +++ b/renderer/playground/allEntitiesDebug.ts @@ -0,0 +1,170 @@ +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) +} diff --git a/renderer/playground/baseScene.ts b/renderer/playground/baseScene.ts new file mode 100644 index 00000000..b9e7791d --- /dev/null +++ b/renderer/playground/baseScene.ts @@ -0,0 +1,414 @@ +//@ts-nocheck +import { Vec3 } from 'vec3' +import * as THREE from 'three' +import '../../src/getCollisionShapes' +import { IndexedData } from 'minecraft-data' +import BlockLoader from 'prismarine-block' +import blockstatesModels from 'mc-assets/dist/blockStatesModels.json' +import ChunkLoader from 'prismarine-chunk' +import WorldLoader from 'prismarine-world' + +//@ts-expect-error +import { OrbitControls } from 'three/addons/controls/OrbitControls.js' +// eslint-disable-next-line import/no-named-as-default +import GUI from 'lil-gui' +import _ from 'lodash' +import { toMajorVersion } from '../../src/utils' +import { WorldDataEmitter } from '../viewer' +import { Viewer } from '../viewer/lib/viewer' +import { BlockNames } from '../../src/mcDataTypes' +import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats' +import { defaultWorldRendererConfig } from '../viewer/lib/worldrendererCommon' +import { getSyncWorld } from './shared' + +window.THREE = THREE + +export class BasePlaygroundScene { + continuousRender = false + stopRender = false + guiParams = {} + viewDistance = 0 + targetPos = new Vec3(2, 90, 2) + params = {} as Record + paramOptions = {} as Partial> + version = new URLSearchParams(window.location.search).get('version') || globalThis.includedVersions.at(-1) + Chunk: typeof import('prismarine-chunk/types/index').PCChunk + Block: typeof import('prismarine-block').Block + ignoreResize = false + enableCameraControls = true // not finished + enableCameraOrbitControl = true + gui = new GUI() + onParamUpdate = {} as Record void> + alwaysIgnoreQs = [] as string[] + skipUpdateQs = false + controls: any + windowHidden = false + world: ReturnType + + _worldConfig = defaultWorldRendererConfig + get worldConfig () { + return this._worldConfig + } + set worldConfig (value) { + this._worldConfig = value + viewer.world.config = value + } + + constructor () { + void this.initData().then(() => { + this.addKeyboardShortcuts() + }) + } + + onParamsUpdate (paramName: string, object: any) {} + updateQs (paramName: string, valueSet: any) { + if (this.skipUpdateQs) return + const newQs = new URLSearchParams(window.location.search) + // if (oldQs.get('scene')) { + // newQs.set('scene', oldQs.get('scene')!) + // } + for (const [key, value] of Object.entries({ [paramName]: valueSet })) { + if (typeof value === 'function' || this.params.skipQs?.includes(key) || this.alwaysIgnoreQs.includes(key)) continue + if (value) { + newQs.set(key, value) + } else { + newQs.delete(key) + } + } + window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`) + } + + // async initialSetup () {} + renderFinish () { + this.render() + } + + initGui () { + const qs = new URLSearchParams(window.location.search) + for (const key of Object.keys(this.params)) { + const value = qs.get(key) + if (!value) continue + const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value + this.params[key] = parsed + } + + for (const param of Object.keys(this.params)) { + const option = this.paramOptions[param] + if (option?.hide) continue + this.gui.add(this.params, param, option?.options ?? option?.min, option?.max) + } + if (window.innerHeight < 700) { + this.gui.open(false) + } else { + // const observer = new MutationObserver(() => { + // this.gui.domElement.classList.remove('transition') + // }) + // observer.observe(this.gui.domElement, { + // attributes: true, + // attributeFilter: ['class'], + // }) + setTimeout(() => { + this.gui.domElement.classList.remove('transition') + }, 500) + } + + this.gui.onChange(({ property, object }) => { + if (object === this.params) { + this.onParamUpdate[property]?.() + this.onParamsUpdate(property, object) + const value = this.params[property] + if (this.paramOptions[property]?.reloadOnChange && (typeof value === 'boolean' || this.paramOptions[property].options)) { + setTimeout(() => { + window.location.reload() + }) + } + this.updateQs(property, value) + } else { + this.onParamsUpdate(property, object) + } + }) + } + + // mainChunk: import('prismarine-chunk/types/index').PCChunk + + // overridables + setupWorld () { } + sceneReset () {} + + // eslint-disable-next-line max-params + addWorldBlock (xOffset: number, yOffset: number, zOffset: number, blockName: BlockNames, properties?: Record) { + if (xOffset > 16 || yOffset > 16 || zOffset > 16) throw new Error('Offset too big') + const block = + properties ? + this.Block.fromProperties(loadedData.blocksByName[blockName].id, properties ?? {}, 0) : + this.Block.fromStateId(loadedData.blocksByName[blockName].defaultState, 0) + this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block) + } + + resetCamera () { + const { targetPos } = this + this.controls?.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) + + const cameraPos = targetPos.offset(2, 2, 2) + const pitch = THREE.MathUtils.degToRad(-45) + const yaw = THREE.MathUtils.degToRad(45) + viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') + viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) + viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5) + this.controls?.update() + } + + async initData () { + await window._LOAD_MC_DATA() + const mcData: IndexedData = require('minecraft-data')(this.version) + window.loadedData = window.mcData = mcData + + this.Chunk = (ChunkLoader as any)(this.version) + this.Block = (BlockLoader as any)(this.version) + + const world = getSyncWorld(this.version) + world.setBlockStateId(this.targetPos, 0) + this.world = world + + this.initGui() + + const worldView = new WorldDataEmitter(world, this.viewDistance, this.targetPos) + worldView.addWaitTime = 0 + window.worldView = worldView + + // Create three.js context, add to page + const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] }) + renderer.setPixelRatio(window.devicePixelRatio || 1) + renderer.setSize(window.innerWidth, window.innerHeight) + + // Create viewer + const viewer = new Viewer(renderer, this.worldConfig) + window.viewer = viewer + window.world = window.viewer.world + const isWebgpu = false + const promises = [] as Array> + if (isWebgpu) { + // promises.push(initWebgpuRenderer(() => { }, true, true)) // todo + } else { + initWithRenderer(renderer.domElement) + renderer.domElement.id = 'viewer-canvas' + document.body.appendChild(renderer.domElement) + } + viewer.addChunksBatchWaitTime = 0 + viewer.world.blockstatesModels = blockstatesModels + viewer.entities.setDebugMode('basic') + viewer.setVersion(this.version) + viewer.entities.onSkinUpdate = () => { + viewer.render() + } + viewer.world.mesherConfig.enableLighting = false + await Promise.all(promises) + this.setupWorld() + + viewer.connect(worldView) + + await worldView.init(this.targetPos) + + if (this.enableCameraControls) { + const { targetPos } = this + const canvas = document.querySelector('#viewer-canvas') + const controls = this.enableCameraOrbitControl ? new OrbitControls(viewer.camera, canvas) : undefined + this.controls = controls + + this.resetCamera() + + // #region camera rotation param + const cameraSet = this.params.camera || localStorage.camera + if (cameraSet) { + const [x, y, z, rx, ry] = cameraSet.split(',').map(Number) + viewer.camera.position.set(x, y, z) + viewer.camera.rotation.set(rx, ry, 0, 'ZYX') + this.controls?.update() + } + const throttledCamQsUpdate = _.throttle(() => { + const { camera } = viewer + // params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}` + // this.updateQs() + localStorage.camera = [ + camera.position.x.toFixed(2), + camera.position.y.toFixed(2), + camera.position.z.toFixed(2), + camera.rotation.x.toFixed(2), + camera.rotation.y.toFixed(2), + ].join(',') + }, 200) + if (this.controls) { + this.controls.addEventListener('change', () => { + throttledCamQsUpdate() + this.render() + }) + } else { + setInterval(() => { + throttledCamQsUpdate() + }, 200) + } + // #endregion + } + + if (!this.enableCameraOrbitControl) { + // mouse + let mouseMoveCounter = 0 + const mouseMove = (e: PointerEvent) => { + if ((e.target as HTMLElement).closest('.lil-gui')) return + if (e.buttons === 1 || e.pointerType === 'touch') { + mouseMoveCounter++ + viewer.camera.rotation.x -= e.movementY / 100 + //viewer.camera. + viewer.camera.rotation.y -= e.movementX / 100 + if (viewer.camera.rotation.x < -Math.PI / 2) viewer.camera.rotation.x = -Math.PI / 2 + if (viewer.camera.rotation.x > Math.PI / 2) viewer.camera.rotation.x = Math.PI / 2 + + // yaw += e.movementY / 20; + // pitch += e.movementX / 20; + } + if (e.buttons === 2) { + viewer.camera.position.set(0, 0, 0) + } + } + setInterval(() => { + // updateTextEvent(`Mouse Events: ${mouseMoveCounter}`) + mouseMoveCounter = 0 + }, 1000) + window.addEventListener('pointermove', mouseMove) + } + + // await this.initialSetup() + this.onResize() + window.addEventListener('resize', () => this.onResize()) + void viewer.waitForChunksToRender().then(async () => { + this.renderFinish() + }) + + viewer.world.renderUpdateEmitter.addListener('update', () => { + this.render() + }) + + this.loop() + } + + loop () { + if (this.continuousRender && !this.windowHidden) { + this.render(true) + requestAnimationFrame(() => this.loop()) + } + } + + render (fromLoop = false) { + if (!fromLoop && this.continuousRender) return + if (this.stopRender) return + statsStart() + viewer.render() + statsEnd() + } + + addKeyboardShortcuts () { + document.addEventListener('keydown', (e) => { + if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + if (e.code === 'KeyR') { + this.controls?.reset() + this.resetCamera() + } + if (e.code === 'KeyE') { // refresh block (main) + worldView!.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos)) + } + if (e.code === 'KeyF') { // reload all chunks + this.sceneReset() + worldView!.unloadAllChunks() + void worldView!.init(this.targetPos) + } + } + }) + document.addEventListener('visibilitychange', () => { + this.windowHidden = document.visibilityState === 'hidden' + }) + document.addEventListener('blur', () => { + this.windowHidden = true + }) + document.addEventListener('focus', () => { + this.windowHidden = false + }) + + const updateKeys = () => { + if (pressedKeys.has('ControlLeft') || pressedKeys.has('MetaLeft')) { + return + } + // if (typeof viewer === 'undefined') return + // Create a vector that points in the direction the camera is looking + const direction = new THREE.Vector3(0, 0, 0) + if (pressedKeys.has('KeyW')) { + direction.z = -0.5 + } + if (pressedKeys.has('KeyS')) { + direction.z += 0.5 + } + if (pressedKeys.has('KeyA')) { + direction.x -= 0.5 + } + if (pressedKeys.has('KeyD')) { + direction.x += 0.5 + } + + + if (pressedKeys.has('ShiftLeft')) { + viewer.camera.position.y -= 0.5 + } + if (pressedKeys.has('Space')) { + viewer.camera.position.y += 0.5 + } + direction.applyQuaternion(viewer.camera.quaternion) + direction.y = 0 + + if (pressedKeys.has('ShiftLeft')) { + direction.y *= 2 + direction.x *= 2 + direction.z *= 2 + } + // Add the vector to the camera's position to move the camera + viewer.camera.position.add(direction.normalize()) + this.controls?.update() + this.render() + } + setInterval(updateKeys, 1000 / 30) + + const pressedKeys = new Set() + const keys = (e) => { + const { code } = e + const pressed = e.type === 'keydown' + if (pressed) { + pressedKeys.add(code) + } else { + pressedKeys.delete(code) + } + } + + window.addEventListener('keydown', keys) + window.addEventListener('keyup', keys) + window.addEventListener('blur', (e) => { + for (const key of pressedKeys) { + keys(new KeyboardEvent('keyup', { code: key })) + } + }) + } + + onResize () { + if (this.ignoreResize) return + + const { camera, renderer } = viewer + viewer.camera.aspect = window.innerWidth / window.innerHeight + viewer.camera.updateProjectionMatrix() + renderer.setSize(window.innerWidth, window.innerHeight) + + this.render() + } +} diff --git a/renderer/playground/playground.ts b/renderer/playground/playground.ts new file mode 100644 index 00000000..de201d8f --- /dev/null +++ b/renderer/playground/playground.ts @@ -0,0 +1,12 @@ +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' + +// 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 diff --git a/renderer/playground/playgroundUi.tsx b/renderer/playground/playgroundUi.tsx new file mode 100644 index 00000000..ed183d78 --- /dev/null +++ b/renderer/playground/playgroundUi.tsx @@ -0,0 +1,175 @@ +import { renderToDom } from '@zardoy/react-util' +import { useEffect } from 'react' +import { proxy, useSnapshot } from 'valtio' +import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface' +import { css } from '@emotion/css' +import { Vec3 } from 'vec3' +import useLongPress from '../../src/react/useLongPress' +import { isMobile } from '../viewer/lib/simpleUtils' + +export const playgroundGlobalUiState = proxy({ + scenes: [] as string[], + selected: '', + selectorOpened: false, + actions: {} as Record void>, +}) + +renderToDom() + +function Playground () { + useEffect(() => { + const style = document.createElement('style') + style.innerHTML = /* css */ ` + .lil-gui { + top: 60px !important; + right: 0 !important; + } + ` + document.body.appendChild(style) + return () => { + style.remove() + } + }, []) + + return
+ + + +
+} + +function SceneSelector () { + const mobile = isMobile() + const { scenes, selected } = useSnapshot(playgroundGlobalUiState) + const longPressEvents = useLongPress(() => { + playgroundGlobalUiState.selectorOpened = true + }, () => { }) + + return
+ {scenes.map(scene =>
{ + const qs = new URLSearchParams(window.location.search) + qs.set('scene', scene) + location.search = qs.toString() + }} + >{scene}
)} +
+} + +const ActionsSelector = () => { + const { actions, selectorOpened } = useSnapshot(playgroundGlobalUiState) + + if (!selectorOpened) return null + return
{Object.entries({ + ...actions, + 'Close' () { + playgroundGlobalUiState.selectorOpened = false + } + }).map(([name, action]) =>
{ + action() + playgroundGlobalUiState.selectorOpened = false + }} + >{name}
)}
+} + +const Controls = () => { + // todo setting + const usingTouch = navigator.maxTouchPoints > 0 + + useEffect(() => { + window.addEventListener('touchstart', (e) => { + e.preventDefault() + }) + + const pressedKeys = new Set() + useInterfaceState.setState({ + isFlying: false, + uiCustomization: { + touchButtonSize: 40, + }, + updateCoord ([coord, state]) { + const vec3 = new Vec3(0, 0, 0) + vec3[coord] = state + let key: string | undefined + if (vec3.z < 0) key = 'KeyW' + if (vec3.z > 0) key = 'KeyS' + if (vec3.y > 0) key = 'Space' + if (vec3.y < 0) key = 'ShiftLeft' + if (vec3.x < 0) key = 'KeyA' + if (vec3.x > 0) key = 'KeyD' + if (key) { + if (!pressedKeys.has(key)) { + pressedKeys.add(key) + window.dispatchEvent(new KeyboardEvent('keydown', { code: key })) + } + } + for (const k of pressedKeys) { + if (k !== key) { + window.dispatchEvent(new KeyboardEvent('keyup', { code: k })) + pressedKeys.delete(k) + } + } + } + }) + }, []) + + if (!usingTouch) return null + return ( +
div { + pointer-events: auto; + } + `} + > + +
+ +
+ ) +} diff --git a/renderer/playground/scenes/allEntities.ts b/renderer/playground/scenes/allEntities.ts new file mode 100644 index 00000000..281af807 --- /dev/null +++ b/renderer/playground/scenes/allEntities.ts @@ -0,0 +1,13 @@ +import { BasePlaygroundScene } from '../baseScene' +import { EntityDebugFlags, EntityMesh, rendererSpecialHandled } from '../../viewer/three/entity/EntityMesh' +import { displayEntitiesDebugList } from '../allEntitiesDebug' + +export default class AllEntities extends BasePlaygroundScene { + continuousRender = false + enableCameraControls = false + + async initData () { + await super.initData() + displayEntitiesDebugList(this.version) + } +} diff --git a/renderer/playground/scenes/entities.ts b/renderer/playground/scenes/entities.ts new file mode 100644 index 00000000..5b5d0582 --- /dev/null +++ b/renderer/playground/scenes/entities.ts @@ -0,0 +1,37 @@ +//@ts-nocheck +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' +import { WorldRendererThree } from '../../viewer/three/worldrendererThree' + +export default class extends BasePlaygroundScene { + continuousRender = true + + override initGui (): void { + this.params = { + starfield: false, + entity: 'player', + count: 4 + } + } + + override renderFinish (): void { + if (this.params.starfield) { + ;(viewer.world as WorldRendererThree).scene.background = new THREE.Color(0x00_00_00) + ;(viewer.world as WorldRendererThree).starField.enabled = true + ;(viewer.world as WorldRendererThree).starField.addToScene() + } + + for (let i = 0; i < this.params.count; i++) { + for (let j = 0; j < this.params.count; j++) { + for (let k = 0; k < this.params.count; k++) { + viewer.entities.update({ + id: i * 1000 + j * 100 + k, + name: this.params.entity, + pos: this.targetPos.offset(i, j, k) + } as any, {}) + } + } + } + } +} diff --git a/renderer/playground/scenes/floorRandom.ts b/renderer/playground/scenes/floorRandom.ts new file mode 100644 index 00000000..c6d2ccf1 --- /dev/null +++ b/renderer/playground/scenes/floorRandom.ts @@ -0,0 +1,33 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class RailsCobwebScene extends BasePlaygroundScene { + viewDistance = 5 + continuousRender = true + + override initGui (): void { + this.params = { + squareSize: 50 + } + + super.initGui() + } + + setupWorld () { + const squareSize = this.params.squareSize ?? 30 + const maxSquareSize = this.viewDistance * 16 * 2 + if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`) + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + const fullBlocks = loadedData.blocksArray.filter(block => { + const b = this.Block.fromStateId(block.defaultState, 0) + if (b.shapes?.length !== 1) return false + const shape = b.shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + }) + for (let x = -squareSize; x <= squareSize; x++) { + for (let z = -squareSize; z <= squareSize; z++) { + const i = Math.abs(x + z) * squareSize + worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(fullBlocks[i % fullBlocks.length].defaultState, 0)) + } + } + } +} diff --git a/renderer/playground/scenes/frequentUpdates.ts b/renderer/playground/scenes/frequentUpdates.ts new file mode 100644 index 00000000..caaf7207 --- /dev/null +++ b/renderer/playground/scenes/frequentUpdates.ts @@ -0,0 +1,148 @@ +//@ts-nocheck +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' + +export default class extends BasePlaygroundScene { + viewDistance = 5 + continuousRender = true + + override initGui (): void { + this.params = { + testActive: false, + testUpdatesPerSecond: 10, + testInitialUpdate: false, + stopGeometryUpdate: false, + manualTest: () => { + this.updateBlock() + }, + testNeighborUpdates: () => { + this.testNeighborUpdates() + } + } + + super.initGui() + } + + lastUpdatedOffset = 0 + lastUpdatedId = 2 + updateBlock () { + const x = this.lastUpdatedOffset % 16 + const z = Math.floor(this.lastUpdatedOffset / 16) + const y = 90 + worldView!.setBlockStateId(new Vec3(x, y, z), this.lastUpdatedId++) + this.lastUpdatedOffset++ + if (this.lastUpdatedOffset > 16 * 16) this.lastUpdatedOffset = 0 + if (this.lastUpdatedId > 500) this.lastUpdatedId = 1 + } + + testNeighborUpdates () { + viewer.world.setBlockStateId(new Vec3(15, 95, 15), 1) + viewer.world.setBlockStateId(new Vec3(0, 95, 15), 1) + viewer.world.setBlockStateId(new Vec3(15, 95, 0), 1) + viewer.world.setBlockStateId(new Vec3(0, 95, 0), 1) + + viewer.world.setBlockStateId(new Vec3(16, 95, 15), 1) + viewer.world.setBlockStateId(new Vec3(-1, 95, 15), 1) + viewer.world.setBlockStateId(new Vec3(15, 95, -1), 1) + viewer.world.setBlockStateId(new Vec3(-1, 95, 0), 1) + setTimeout(() => { + viewer.world.setBlockStateId(new Vec3(16, 96, 16), 1) + viewer.world.setBlockStateId(new Vec3(-1, 96, 16), 1) + viewer.world.setBlockStateId(new Vec3(16, 96, -1), 1) + viewer.world.setBlockStateId(new Vec3(-1, 96, -1), 1) + }, 3000) + } + + setupTimer () { + // this.stopRender = true + + let lastTime = 0 + const tick = () => { + viewer.world.debugStopGeometryUpdate = this.params.stopGeometryUpdate + const updateEach = 1000 / this.params.testUpdatesPerSecond + requestAnimationFrame(tick) + if (!this.params.testActive) return + const updateCount = Math.floor(performance.now() - lastTime) / updateEach + for (let i = 0; i < updateCount; i++) { + this.updateBlock() + } + lastTime = performance.now() + } + + requestAnimationFrame(tick) + + // const limit = 1000 + // const limit = 100 + // const limit = 1 + // const updatedChunks = new Set() + // const updatedBlocks = new Set() + // let lastSecond = 0 + // setInterval(() => { + // const second = Math.floor(performance.now() / 1000) + // if (lastSecond !== second) { + // lastSecond = second + // updatedChunks.clear() + // updatedBlocks.clear() + // } + // const isEven = second % 2 === 0 + // if (updatedBlocks.size > limit) { + // return + // } + // const changeBlock = (x, z) => { + // const chunkKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}` + // const key = `${x},${z}` + // if (updatedBlocks.has(chunkKey)) return + + // updatedChunks.add(chunkKey) + // worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(isEven ? 2 : 3, 0)) + // updatedBlocks.add(key) + // } + // const { squareSize } = this.params + // const xStart = -squareSize + // const zStart = -squareSize + // const xEnd = squareSize + // const zEnd = squareSize + // for (let x = xStart; x <= xEnd; x += 16) { + // for (let z = zStart; z <= zEnd; z += 16) { + // const key = `${x},${z}` + // if (updatedChunks.has(key)) continue + // changeBlock(x, z) + // return + // } + // } + // for (let x = xStart; x <= xEnd; x += 16) { + // for (let z = zStart; z <= zEnd; z += 16) { + // const key = `${x},${z}` + // if (updatedChunks.has(key)) continue + // changeBlock(x, z) + // return + // } + // } + // }, 1) + } + + setupWorld () { + this.worldConfig.showChunkBorders = true + + const maxSquareRadius = this.viewDistance * 16 + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + const squareSize = maxSquareRadius + for (let x = -squareSize; x <= squareSize; x++) { + for (let z = -squareSize; z <= squareSize; z++) { + const i = Math.abs(x + z) * squareSize + worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(1, 0)) + } + } + let done = false + viewer.world.renderUpdateEmitter.on('update', () => { + if (!viewer.world.allChunksFinished || done) return + done = true + this.setupTimer() + }) + setTimeout(() => { + if (this.params.testInitialUpdate) { + this.updateBlock() + } + }) + } +} diff --git a/renderer/playground/scenes/index.ts b/renderer/playground/scenes/index.ts new file mode 100644 index 00000000..bf881812 --- /dev/null +++ b/renderer/playground/scenes/index.ts @@ -0,0 +1,11 @@ +// export { default as rotation } from './rotation' +export { default as main } from './main' +export { default as railsCobweb } from './railsCobweb' +export { default as floorRandom } from './floorRandom' +export { default as lightingStarfield } from './lightingStarfield' +export { default as transparencyIssue } from './transparencyIssue' +export { default as rotationIssue } from './rotationIssue' +export { default as entities } from './entities' +export { default as frequentUpdates } from './frequentUpdates' +export { default as slabsOptimization } from './slabsOptimization' +export { default as allEntities } from './allEntities' diff --git a/renderer/playground/scenes/lightingStarfield.ts b/renderer/playground/scenes/lightingStarfield.ts new file mode 100644 index 00000000..eec0a7d3 --- /dev/null +++ b/renderer/playground/scenes/lightingStarfield.ts @@ -0,0 +1,40 @@ +//@ts-nocheck +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' +import { WorldRendererThree } from '../../viewer/three/worldrendererThree' + +export default class extends BasePlaygroundScene { + continuousRender = true + + override setupWorld (): void { + viewer.world.mesherConfig.enableLighting = true + viewer.world.mesherConfig.skyLight = 0 + this.addWorldBlock(0, 0, 0, 'stone') + this.addWorldBlock(0, 0, 1, 'stone') + this.addWorldBlock(1, 0, 0, 'stone') + this.addWorldBlock(1, 0, 1, 'stone') + // chess like + worldView?.world.setBlockLight(this.targetPos.offset(0, 1, 0), 15) + worldView?.world.setBlockLight(this.targetPos.offset(0, 1, 1), 0) + worldView?.world.setBlockLight(this.targetPos.offset(1, 1, 0), 0) + worldView?.world.setBlockLight(this.targetPos.offset(1, 1, 1), 15) + } + + override renderFinish (): void { + viewer.scene.background = new THREE.Color(0x00_00_00) + // starfield and test entities + ;(viewer.world as WorldRendererThree).starField.enabled = true + ;(viewer.world as WorldRendererThree).starField.addToScene() + viewer.entities.update({ + id: 0, + name: 'player', + pos: this.targetPos.clone() + } as any, {}) + viewer.entities.update({ + id: 1, + name: 'creeper', + pos: this.targetPos.offset(1, 0, 0) + } as any, {}) + } +} diff --git a/renderer/playground/scenes/main.ts b/renderer/playground/scenes/main.ts new file mode 100644 index 00000000..64925f61 --- /dev/null +++ b/renderer/playground/scenes/main.ts @@ -0,0 +1,314 @@ +//@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' + +class MainScene extends BasePlaygroundScene { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor (...args) { + //@ts-expect-error + super(...args) + } + + override initGui (): void { + // initial values + this.params = { + version: globalThis.includedVersions.at(-1), + skipQs: '', + block: '', + metadata: 0, + supportBlock: false, + entity: '', + removeEntity () { + this.entity = '' + }, + entityRotate: false, + camera: '', + playSound () { }, + blockIsomorphicRenderBundle () { }, + modelVariant: 0 + } + this.metadataGui = this.gui.add(this.params, 'metadata') + this.paramOptions = { + version: { + options: globalThis.includedVersions, + hide: false + }, + block: { + options: mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b)) + }, + entity: { + options: mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b)) + }, + camera: { + hide: true, + } + } + super.initGui() + } + + blockProps = {} + metadataFolder: GUI | undefined + metadataGui: Controller + + override onParamUpdate = { + version () { + // if (initialUpdate) return + // viewer.world.texturesVersion = params.version + // viewer.world.updateTexturesData() + // todo warning + }, + block: () => { + this.blockProps = {} + this.metadataFolder?.destroy() + const block = mcData.blocksByName[this.params.block] + if (!block) return + console.log('block', block.name) + const props = new this.Block(block.id, 0, 0).getProperties() + const { states } = mcData.blocksByStateId[this.getBlock()?.minStateId] ?? {} + this.metadataFolder = this.gui.addFolder('metadata') + if (states) { + for (const state of states) { + let defaultValue: string | number | boolean + if (state.values) { // int, enum + defaultValue = state.values[0] + } else { + switch (state.type) { + case 'bool': + defaultValue = false + break + case 'int': + defaultValue = 0 + break + case 'direction': + defaultValue = 'north' + break + + default: + continue + } + } + this.blockProps[state.name] = defaultValue + if (state.values) { + this.metadataFolder.add(this.blockProps, state.name, state.values) + } else { + this.metadataFolder.add(this.blockProps, state.name) + } + } + } else { + for (const [name, value] of Object.entries(props)) { + this.blockProps[name] = value + this.metadataFolder.add(this.blockProps, name) + } + } + console.log('props', this.blockProps) + this.metadataFolder.open() + }, + entity: () => { + this.continuousRender = this.params.entity === 'player' + this.entityUpdateShared() + if (!this.params.entity) return + if (this.params.entity === 'player') { + viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, undefined, true, true) + viewer.entities.playAnimation('id', 'running') + } + // let prev = false + // setInterval(() => { + // viewer.entities.playAnimation('id', prev ? 'running' : 'idle') + // prev = !prev + // }, 1000) + + EntityMesh.getStaticData(this.params.entity) + // entityRotationFolder.destroy() + // entityRotationFolder = gui.addFolder('entity metadata') + // entityRotationFolder.add(params, 'entityRotate') + // entityRotationFolder.open() + }, + supportBlock: () => { + viewer.setBlockStateId(this.targetPos.offset(0, -1, 0), this.params.supportBlock ? 1 : 0) + }, + modelVariant: () => { + viewer.world.mesherConfig.debugModelVariant = this.params.modelVariant === 0 ? undefined : [this.params.modelVariant] + } + } + + entityUpdateShared () { + viewer.entities.clear() + if (!this.params.entity) return + worldView!.emit('entity', { + id: 'id', name: this.params.entity, pos: this.targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0 + }) + const enableSkeletonDebug = (obj) => { + const { children, isSkeletonHelper } = obj + if (!Array.isArray(children)) return + if (isSkeletonHelper) { + obj.visible = true + return + } + for (const child of children) { + if (typeof child === 'object') enableSkeletonDebug(child) + } + } + enableSkeletonDebug(viewer.entities.entities['id']) + setTimeout(() => { + viewer.render() + }, TWEEN_DURATION) + } + + blockIsomorphicRenderBundle () { + const { renderer } = viewer + + const canvas = renderer.domElement + const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one') + const sizeRaw = prompt('Size', '512') + if (!sizeRaw) return + const size = parseInt(sizeRaw, 10) + // const size = 512 + + this.ignoreResize = true + canvas.width = size + canvas.height = size + renderer.setSize(size, size) + + viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10) + viewer.scene.background = null + + const rad = THREE.MathUtils.degToRad(-120) + viewer.directionalLight.position.set( + Math.cos(rad), + Math.sin(rad), + 0.2 + ).normalize() + viewer.directionalLight.intensity = 1 + + const cameraPos = this.targetPos.offset(2, 2, 2) + const pitch = THREE.MathUtils.degToRad(-30) + const yaw = THREE.MathUtils.degToRad(45) + viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') + // viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5) + viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1) + + const allBlocks = mcData.blocksArray.map(b => b.name) + // const allBlocks = ['stone', 'warped_slab'] + + let blockCount = 1 + let blockName = allBlocks[0] + + const updateBlock = () => { + // viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId) + this.params.block = blockName + // todo cleanup (introduce getDefaultState) + // TODO + // onUpdate.block() + // applyChanges(false, true) + } + void viewer.waitForChunksToRender().then(async () => { + // wait for next macro task + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + if (onlyCurrent) { + viewer.render() + onWorldUpdate() + } else { + // will be called on every render update + viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate) + updateBlock() + } + }) + + const zip = new JSZip() + zip.file('description.txt', 'Generated with mcraft.fun/playground') + + const end = async () => { + // download zip file + + const a = document.createElement('a') + const blob = await zip.generateAsync({ type: 'blob' }) + const dataUrlZip = URL.createObjectURL(blob) + a.href = dataUrlZip + a.download = 'blocks_render.zip' + a.click() + URL.revokeObjectURL(dataUrlZip) + console.log('end') + + viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate) + } + + async function onWorldUpdate () { + // await new Promise(resolve => { + // setTimeout(resolve, 50) + // }) + const dataUrl = canvas.toDataURL('image/png') + + zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true }) + + if (onlyCurrent) { + end() + } else { + nextBlock() + } + } + const nextBlock = async () => { + blockName = allBlocks[blockCount++] + console.log(allBlocks.length, '/', blockCount, blockName) + if (blockCount % 5 === 0) { + await new Promise(resolve => { + setTimeout(resolve, 100) + }) + } + if (blockName) { + updateBlock() + } else { + end() + } + } + } + + getBlock () { + return mcData.blocksByName[this.params.block || 'air'] + } + + // applyChanges (metadataUpdate = false, skipQs = false) { + override onParamsUpdate (paramName: string, object: any) { + const metadataUpdate = paramName === 'metadata' + + const blockId = this.getBlock()?.id + let block: import('prismarine-block').Block + if (metadataUpdate) { + block = new this.Block(blockId, 0, this.params.metadata) + Object.assign(this.blockProps, block.getProperties()) + for (const _child of this.metadataFolder!.children) { + const child = _child as import('lil-gui').Controller + child.updateDisplay() + } + } else { + try { + block = this.Block.fromProperties(blockId ?? -1, this.blockProps, 0) + } catch (err) { + console.error(err) + block = this.Block.fromStateId(0, 0) + } + } + + worldView!.setBlockStateId(this.targetPos, block.stateId ?? 0) + console.log('up stateId', block.stateId) + this.params.metadata = block.metadata + this.metadataGui.updateDisplay() + } + + override renderFinish () { + for (const update of Object.values(this.onParamUpdate)) { + // update(true) + update() + } + this.onParamsUpdate('', {}) + this.gui.openAnimated() + } +} + +export default MainScene diff --git a/renderer/playground/scenes/railsCobweb.ts b/renderer/playground/scenes/railsCobweb.ts new file mode 100644 index 00000000..bc1c271a --- /dev/null +++ b/renderer/playground/scenes/railsCobweb.ts @@ -0,0 +1,14 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class RailsCobwebScene extends BasePlaygroundScene { + setupWorld () { + this.addWorldBlock(0, 0, 0, 'cobweb') + this.addWorldBlock(0, -1, 0, 'cobweb') + this.addWorldBlock(1, -1, 0, 'cobweb') + this.addWorldBlock(1, 0, 0, 'cobweb') + + this.addWorldBlock(0, 0, 1, 'powered_rail', { shape: 'north_south', waterlogged: false }) + this.addWorldBlock(0, 0, 2, 'powered_rail', { shape: 'ascending_south', waterlogged: false }) + this.addWorldBlock(0, 1, 3, 'powered_rail', { shape: 'north_south', waterlogged: false }) + } +} diff --git a/renderer/playground/scenes/rotationIssue.ts b/renderer/playground/scenes/rotationIssue.ts new file mode 100644 index 00000000..2c56876a --- /dev/null +++ b/renderer/playground/scenes/rotationIssue.ts @@ -0,0 +1,7 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class RotationIssueScene extends BasePlaygroundScene { + setupWorld () { + // todo + } +} diff --git a/renderer/playground/scenes/slabsOptimization.ts b/renderer/playground/scenes/slabsOptimization.ts new file mode 100644 index 00000000..9035a777 --- /dev/null +++ b/renderer/playground/scenes/slabsOptimization.ts @@ -0,0 +1,15 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class extends BasePlaygroundScene { + expectedNumberOfFaces = 30 + + setupWorld () { + this.addWorldBlock(0, 1, 0, 'stone_slab') + this.addWorldBlock(0, 0, 0, 'stone') + this.addWorldBlock(0, -1, 0, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(0, -1, -1, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(0, -1, 1, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(-1, -1, 0, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(1, -1, 0, 'stone_slab', { type: 'top', waterlogged: false }) + } +} diff --git a/renderer/playground/scenes/transparencyIssue.ts b/renderer/playground/scenes/transparencyIssue.ts new file mode 100644 index 00000000..9ce1b967 --- /dev/null +++ b/renderer/playground/scenes/transparencyIssue.ts @@ -0,0 +1,11 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class extends BasePlaygroundScene { + setupWorld () { + this.addWorldBlock(0, 0, 0, 'water') + this.addWorldBlock(0, 1, 0, 'lime_stained_glass') + this.addWorldBlock(0, 0, -1, 'lime_stained_glass') + this.addWorldBlock(0, -1, 0, 'lime_stained_glass') + this.addWorldBlock(0, -1, -1, 'stone') + } +} diff --git a/renderer/playground/shared.ts b/renderer/playground/shared.ts new file mode 100644 index 00000000..9d12fae9 --- /dev/null +++ b/renderer/playground/shared.ts @@ -0,0 +1,79 @@ +import WorldLoader, { world } from 'prismarine-world' +import ChunkLoader from 'prismarine-chunk' + +export type BlockFaceType = { + side: number + textureIndex: number + tint?: [number, number, number] + isTransparent?: boolean + + // for testing + face?: string + neighbor?: string + light?: number +} + +export type BlockType = { + faces: BlockFaceType[] + + // for testing + block: string +} + +export const makeError = (str: string) => { + reportError?.(str) +} +export const makeErrorCritical = (str: string) => { + throw new Error(str) +} + +export const getSyncWorld = (version: string): world.WorldSync => { + const World = (WorldLoader as any)(version) + const Chunk = (ChunkLoader as any)(version) + + const world = new World(version).sync + + const methods = getAllMethods(world) + for (const method of methods) { + if (method.startsWith('set') && method !== 'setColumn') { + const oldMethod = world[method].bind(world) + world[method] = (...args) => { + const arg = args[0] + if (arg.x !== undefined && !world.getColumnAt(arg)) { + world.setColumn(Math.floor(arg.x / 16), Math.floor(arg.z / 16), new Chunk(undefined as any)) + } + oldMethod(...args) + } + } + } + + return world +} + +function getAllMethods (obj) { + const methods = new Set() + let currentObj = obj + + do { + for (const name of Object.getOwnPropertyNames(currentObj)) { + if (typeof obj[name] === 'function' && name !== 'constructor') { + methods.add(name) + } + } + } while ((currentObj = Object.getPrototypeOf(currentObj))) + + return [...methods] as string[] +} + +export const delayedIterator = async (arr: T[], delay: number, exec: (item: T, index: number) => Promise, chunkSize = 1) => { + // if delay is 0 then don't use setTimeout + for (let i = 0; i < arr.length; i += chunkSize) { + if (delay) { + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + setTimeout(resolve, delay) + }) + } + await exec(arr[i], i) + } +} diff --git a/renderer/rsbuild.config.ts b/renderer/rsbuild.config.ts new file mode 100644 index 00000000..2b40e79c --- /dev/null +++ b/renderer/rsbuild.config.ts @@ -0,0 +1,59 @@ +import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core'; +import supportedVersions from '../src/supportedVersions.mjs' +import childProcess from 'child_process' +import path, { dirname, join } from 'path' +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'; +import fs from 'fs' +import fsExtra from 'fs-extra' +import { appAndRendererSharedConfig, rspackViewerConfig } from './rsbuildSharedConfig'; + +const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json') + +// if (!fs.existsSync('./playground/textures')) { +// fsExtra.copySync('node_modules/mc-assets/dist/other-textures/latest/entity', './playground/textures/entity') +// } + +if (!fs.existsSync(mcDataPath)) { + childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: path.join(__dirname, '..') }) +} + +export default mergeRsbuildConfig( + appAndRendererSharedConfig(), + defineConfig({ + html: { + template: join(__dirname, './playground.html'), + }, + output: { + cleanDistPath: false, + distPath: { + root: join(__dirname, './dist'), + }, + }, + server: { + port: 9090, + }, + source: { + entry: { + index: join(__dirname, './playground/playground.ts') + }, + define: { + 'globalThis.includedVersions': JSON.stringify(supportedVersions), + }, + }, + plugins: [ + { + name: 'test', + setup (build: RsbuildPluginAPI) { + const prep = async () => { + fsExtra.copySync(join(__dirname, '../node_modules/mc-assets/dist/other-textures/latest/entity'), join(__dirname, './dist/textures/entity')) + } + build.onBeforeBuild(async () => { + await prep() + }) + build.onBeforeStartDevServer(() => prep()) + }, + }, + ], + }) +) diff --git a/renderer/rsbuildSharedConfig.ts b/renderer/rsbuildSharedConfig.ts new file mode 100644 index 00000000..45da30b1 --- /dev/null +++ b/renderer/rsbuildSharedConfig.ts @@ -0,0 +1,124 @@ +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: { + progressBar: true, + writeToDisk: true, + watchFiles: { + paths: [ + path.join(__dirname, './dist/webgpuRendererWorker.js'), + path.join(__dirname, './dist/mesher.js'), + ] + }, + }, + output: { + polyfill: 'usage', + // 50kb limit for data uri + dataUriLimit: 50 * 1024, + assetPrefix: './', + }, + source: { + alias: { + fs: path.join(__dirname, `../src/shims/fs.js`), + http: 'http-browserify', + stream: 'stream-browserify', + net: 'net-browserify', + 'minecraft-protocol$': 'minecraft-protocol/src/index.js', + 'buffer$': 'buffer', + // avoid bundling, not used on client side + 'prismarine-auth': path.join(__dirname, `../src/shims/prismarineAuthReplacement.ts`), + perf_hooks: path.join(__dirname, `../src/shims/perf_hooks_replacement.js`), + crypto: path.join(__dirname, `../src/shims/crypto.js`), + dns: path.join(__dirname, `../src/shims/dns.js`), + yggdrasil: path.join(__dirname, `../src/shims/yggdrasilReplacement.ts`), + 'three$': 'three/src/Three.js', + 'stats.js$': 'stats.js/src/Stats.js', + }, + define: { + 'process.platform': '"browser"', + }, + decorators: { + version: 'legacy', // default is a lie + }, + }, + server: { + htmlFallback: false, + // publicDir: false, + headers: { + // enable shared array buffer + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + open: process.env.OPEN_BROWSER === 'true', + }, + plugins: [ + pluginReact(), + pluginNodePolyfill() + ], + 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) => { + 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) + process.exit(1) + // throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`) + } + if (absolute.endsWith('/minecraft-data/data.js')) { + resource.request = path.join(__dirname, `../src/shims/minecraftData.ts`) + } + if (absolute.endsWith('/minecraft-data/data/bedrock/common/legacy.json')) { + resource.request = path.join(__dirname, `../src/shims/empty.ts`) + } + if (absolute.endsWith('/minecraft-data/data/pc/common/legacy.json')) { + resource.request = path.join(__dirname, `../src/preflatMap.json`) + } + })) + addRules([ + { + test: /\.obj$/, + type: 'asset/source', + }, + { + test: /\.wgsl$/, + type: 'asset/source', + }, + { + test: /\.mp3$/, + type: 'asset/source', + }, + { + test: /\.txt$/, + type: 'asset/source', + }, + { + test: /\.log$/, + type: 'asset/source', + } + ]) + config.ignoreWarnings = [ + /the request of a dependency is an expression/, + /Unsupported pseudo class or element: xr-overlay/ + ] + if (process.env.SINGLE_FILE_BUILD === 'true') { + config.module!.parser!.javascript!.dynamicImportMode = 'eager' + } +} diff --git a/prismarine-viewer/viewer/.gitignore b/renderer/viewer/.gitignore similarity index 100% rename from prismarine-viewer/viewer/.gitignore rename to renderer/viewer/.gitignore diff --git a/renderer/viewer/baseGraphicsBackend.ts b/renderer/viewer/baseGraphicsBackend.ts new file mode 100644 index 00000000..486c930f --- /dev/null +++ b/renderer/viewer/baseGraphicsBackend.ts @@ -0,0 +1,27 @@ +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, + } + } + } +} diff --git a/renderer/viewer/common/utils.ts b/renderer/viewer/common/utils.ts new file mode 100644 index 00000000..958273f3 --- /dev/null +++ b/renderer/viewer/common/utils.ts @@ -0,0 +1,18 @@ +export const versionToNumber = (ver: string) => { + const [x, y = '0', z = '0'] = ver.split('.') + return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` +} + +export const versionToMajor = (version: string) => { + const [x, y = '0'] = version.split('.') + return `${x.padStart(2, '0')}.${y.padStart(2, '0')}` +} + +export const versionsMapToMajor = (versionsMap: Record) => { + const majorVersions = {} as Record + for (const [ver, data] of Object.entries(versionsMap)) { + const major = versionToMajor(ver) + majorVersions[major] = data + } + return majorVersions +} diff --git a/renderer/viewer/lib/DebugGui.ts b/renderer/viewer/lib/DebugGui.ts new file mode 100644 index 00000000..f296c873 --- /dev/null +++ b/renderer/viewer/lib/DebugGui.ts @@ -0,0 +1,174 @@ +// eslint-disable-next-line import/no-named-as-default +import GUI from 'lil-gui' + +export interface ParamMeta { + min?: number + max?: number + step?: number +} + +export class DebugGui { + private gui: GUI + private readonly storageKey: string + private target: any + private readonly params: string[] + private readonly paramsMeta: Record + private _visible = false // Default to not visible + private readonly initialValues: Record = {} // Store initial values + private initialized = false + + constructor (id: string, target: any, params?: string[], paramsMeta?: Record) { + this.gui = new GUI() + this.storageKey = `debug_params_${id}` + this.target = target + this.paramsMeta = paramsMeta ?? {} + this.params = params ?? Object.keys(target) + + // Store initial values + for (const param of this.params) { + this.initialValues[param] = target[param] + } + + // Hide by default + this.gui.domElement.style.display = 'none' + } + + // Initialize and show the GUI + activate () { + if (!this.initialized) { + this.loadSavedValues() + this.setupControls() + this.initialized = true + } + this.show() + return this + } + + // Getter for visibility + get visible (): boolean { + return this._visible + } + + // Setter for visibility + set visible (value: boolean) { + this._visible = value + this.gui.domElement.style.display = value ? 'block' : 'none' + this.saveVisibility() + } + + private loadSavedValues () { + try { + const saved = localStorage.getItem(this.storageKey) + if (saved) { + const values = JSON.parse(saved) + // Apply saved values to target + for (const param of this.params) { + if (param in values) { + const value = values[param] + if (value !== null) { + this.target[param] = value + } + } + } + } + } catch (e) { + console.warn('Failed to load debug values:', e) + } + } + + private saveValues (deleteKey = false) { + try { + const values = {} + for (const param of this.params) { + values[param] = this.target[param] + } + if (deleteKey) { + localStorage.removeItem(this.storageKey) + } else { + localStorage.setItem(this.storageKey, JSON.stringify(values)) + } + } catch (e) { + console.warn('Failed to save debug values:', e) + } + } + + private saveVisibility () { + try { + localStorage.setItem(`${this.storageKey}_visible`, this._visible.toString()) + } catch (e) { + console.warn('Failed to save debug visibility:', e) + } + } + + private setupControls () { + // Add visibility toggle at the top + this.gui.add(this, 'visible').name('Show Controls') + this.gui.add({ resetAll: () => { + for (const param of this.params) { + this.target[param] = this.initialValues[param] + } + this.saveValues(true) + this.gui.destroy() + this.gui = new GUI() + this.setupControls() + } }, 'resetAll').name('Reset All Parameters') + + for (const param of this.params) { + const value = this.target[param] + const meta = this.paramsMeta[param] ?? {} + + if (typeof value === 'number') { + // For numbers, use meta values or calculate reasonable defaults + const min = meta.min ?? value - Math.abs(value * 2) + const max = meta.max ?? value + Math.abs(value * 2) + const step = meta.step ?? Math.abs(value) / 100 + + this.gui.add(this.target, param, min, max, step) + .onChange(() => this.saveValues()) + } else if (typeof value === 'boolean') { + // For booleans, create a checkbox + this.gui.add(this.target, param) + .onChange(() => this.saveValues()) + } else if (typeof value === 'string' && ['x', 'y', 'z'].includes(param)) { + // Special case for xyz coordinates + const min = meta.min ?? -10 + const max = meta.max ?? 10 + const step = meta.step ?? 0.1 + + this.gui.add(this.target, param, min, max, step) + .onChange(() => this.saveValues()) + } else if (Array.isArray(value)) { + // For arrays, create a dropdown + this.gui.add(this.target, param, value) + .onChange(() => this.saveValues()) + } + } + } + + // Method to manually trigger save + save () { + this.saveValues() + this.saveVisibility() + } + + // Method to destroy the GUI and clean up + destroy () { + this.saveVisibility() + this.gui.destroy() + } + + // Toggle visibility + toggle () { + this.visible = !this.visible + } + + // Show the GUI + show () { + this.visible = true + } + + // Hide the GUI + hide () { + this.visible = false + } +} diff --git a/renderer/viewer/lib/animationController.ts b/renderer/viewer/lib/animationController.ts new file mode 100644 index 00000000..329d0e24 --- /dev/null +++ b/renderer/viewer/lib/animationController.ts @@ -0,0 +1,85 @@ +import * as tweenJs from '@tweenjs/tween.js' + +export class AnimationController { + private currentAnimation: tweenJs.Group | null = null + private isAnimating = false + private cancelRequested = false + private completionCallbacks: Array<() => void> = [] + private currentCancelCallback: (() => void) | null = null + + /** Main method */ + async startAnimation (createAnimation: () => tweenJs.Group, onCancelled?: () => void): Promise { + if (this.isAnimating) { + await this.cancelCurrentAnimation() + } + + return new Promise((resolve) => { + this.isAnimating = true + this.cancelRequested = false + this.currentCancelCallback = onCancelled ?? null + this.currentAnimation = createAnimation() + + this.completionCallbacks.push(() => { + this.isAnimating = false + this.currentAnimation = null + resolve() + }) + }) + } + + /** Main method */ + async cancelCurrentAnimation (): Promise { + if (!this.isAnimating) return + + if (this.currentCancelCallback) { + const callback = this.currentCancelCallback + this.currentCancelCallback = null + callback() + } + + return new Promise((resolve) => { + this.cancelRequested = true + this.completionCallbacks.push(() => { + resolve() + }) + }) + } + + animationCycleFinish () { + if (this.cancelRequested) this.forceFinish() + } + + forceFinish (callComplete = true) { + if (!this.isAnimating) return + + if (this.currentAnimation) { + for (const tween of this.currentAnimation.getAll()) tween.stop() + this.currentAnimation.removeAll() + this.currentAnimation = null + } + + this.isAnimating = false + this.cancelRequested = false + + const callbacks = [...this.completionCallbacks] + this.completionCallbacks = [] + if (callComplete) { + for (const cb of callbacks) cb() + } + } + + /** Required method */ + update () { + if (this.currentAnimation) { + this.currentAnimation.update() + } + } + + get isActive () { + return this.isAnimating + } + + get shouldCancel () { + return this.cancelRequested + } +} diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts new file mode 100644 index 00000000..9cf1350a --- /dev/null +++ b/renderer/viewer/lib/basePlayerState.ts @@ -0,0 +1,87 @@ +import { ItemSelector } from 'mc-assets/dist/itemDefinitions' +import { GameMode, Team } from 'mineflayer' +import { proxy } from 'valtio' +import type { HandItemBlock } from '../three/holdingBlock' + +export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING' +export type ItemSpecificContextProperties = Partial> +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, + + cameraSpectatingEntity: undefined as number | undefined, + + team: undefined as Team | undefined, +}) + +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 +export type PlayerStateUtils = ReturnType + +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, + } +} diff --git a/renderer/viewer/lib/cameraBobbing.ts b/renderer/viewer/lib/cameraBobbing.ts new file mode 100644 index 00000000..6bf32c76 --- /dev/null +++ b/renderer/viewer/lib/cameraBobbing.ts @@ -0,0 +1,94 @@ +export class CameraBobbing { + private walkDistance = 0 + private prevWalkDistance = 0 + private bobAmount = 0 + private prevBobAmount = 0 + private readonly gameTimer = new GameTimer() + + // eslint-disable-next-line max-params + constructor ( + private readonly BOB_FREQUENCY: number = Math.PI, // How fast the bob cycles + private readonly BOB_BASE_AMPLITUDE: number = 0.5, // Base amplitude of the bob + private readonly VERTICAL_MULTIPLIER: number = 1, // Vertical movement multiplier + private readonly ROTATION_MULTIPLIER_Z: number = 3, // Roll rotation multiplier + private readonly ROTATION_MULTIPLIER_X: number = 5 // Pitch rotation multiplier + ) {} + + // Call this when player is moving + public updateWalkDistance (distance: number): void { + this.prevWalkDistance = this.walkDistance + this.walkDistance = distance + } + + // Call this when player is moving to update bob amount + public updateBobAmount (isMoving: boolean): void { + const targetBob = isMoving ? 1 : 0 + this.prevBobAmount = this.bobAmount + + // Update timing + const ticks = this.gameTimer.update() + const deltaTime = ticks / 20 // Convert ticks to seconds assuming 20 TPS + + // Smooth transition for bob amount + const bobDelta = (targetBob - this.bobAmount) * Math.min(1, deltaTime * 10) + this.bobAmount += bobDelta + } + + // Call this in your render/animation loop + public getBobbing (): { position: { x: number, y: number }, rotation: { x: number, z: number } } { + // Interpolate walk distance + const walkDist = this.prevWalkDistance + + (this.walkDistance - this.prevWalkDistance) * this.gameTimer.partialTick + + // Interpolate bob amount + const bob = this.prevBobAmount + + (this.bobAmount - this.prevBobAmount) * this.gameTimer.partialTick + + // Calculate total distance for bob cycle + const totalDist = -(walkDist * this.BOB_FREQUENCY) + + // Calculate offsets + const xOffset = Math.sin(totalDist) * bob * this.BOB_BASE_AMPLITUDE + const yOffset = -Math.abs(Math.cos(totalDist) * bob) * this.VERTICAL_MULTIPLIER + + // Calculate rotations (in radians) + const zRot = (Math.sin(totalDist) * bob * this.ROTATION_MULTIPLIER_Z) * (Math.PI / 180) + const xRot = (Math.abs(Math.cos(totalDist - 0.2) * bob) * this.ROTATION_MULTIPLIER_X) * (Math.PI / 180) + + return { + position: { x: xOffset, y: yOffset }, + rotation: { x: xRot, z: zRot } + } + } +} + +class GameTimer { + private readonly msPerTick: number + private lastMs: number + public partialTick = 0 + + constructor (tickRate = 20) { + this.msPerTick = 1000 / tickRate + this.lastMs = performance.now() + } + + update (): number { + const currentMs = performance.now() + const deltaSinceLastTick = currentMs - this.lastMs + + // Calculate how much of a tick has passed + const tickDelta = deltaSinceLastTick / this.msPerTick + this.lastMs = currentMs + + // Add to accumulated partial ticks + this.partialTick += tickDelta + + // Get whole number of ticks that should occur + const wholeTicks = Math.floor(this.partialTick) + + // Keep the remainder as the new partial tick + this.partialTick -= wholeTicks + + return wholeTicks + } +} diff --git a/renderer/viewer/lib/cleanupDecorator.ts b/renderer/viewer/lib/cleanupDecorator.ts new file mode 100644 index 00000000..79b35828 --- /dev/null +++ b/renderer/viewer/lib/cleanupDecorator.ts @@ -0,0 +1,29 @@ +export function buildCleanupDecorator (cleanupMethod: string) { + return function () { + return function (_target: { snapshotInitialValues }, propertyKey: string) { + const target = _target as any + // Store the initial value of the property + if (!target._snapshotMethodPatched) { + target.snapshotInitialValues = function () { + this._initialValues = {} + for (const key of target._toCleanup) { + this._initialValues[key] = this[key] + } + } + target._snapshotMethodPatched = true + } + (target._toCleanup ??= []).push(propertyKey) + if (!target._cleanupPatched) { + const originalMethod = target[cleanupMethod] + target[cleanupMethod] = function () { + for (const key of target._toCleanup) { + this[key] = this._initialValues[key] + } + // eslint-disable-next-line prefer-rest-params + Reflect.apply(originalMethod, this, arguments) + } + } + target._cleanupPatched = true + } + } +} diff --git a/renderer/viewer/lib/createPlayerObject.ts b/renderer/viewer/lib/createPlayerObject.ts new file mode 100644 index 00000000..836c8062 --- /dev/null +++ b/renderer/viewer/lib/createPlayerObject.ts @@ -0,0 +1,55 @@ +import { PlayerObject, PlayerAnimation } from 'skinview3d' +import * as THREE from 'three' +import { WalkingGeneralSwing } from '../three/entity/animations' +import { loadSkinImage, stevePngUrl } from './utils/skins' + +export type PlayerObjectType = PlayerObject & { + animation?: PlayerAnimation + realPlayerUuid: string + realUsername: string +} + +export function createPlayerObject (options: { + username?: string + uuid?: string + scale?: number +}): { + playerObject: PlayerObjectType + wrapper: THREE.Group + } { + const wrapper = new THREE.Group() + const playerObject = new PlayerObject() as PlayerObjectType + + playerObject.realPlayerUuid = options.uuid ?? '' + playerObject.realUsername = options.username ?? '' + playerObject.position.set(0, 16, 0) + + // fix issues with starfield + playerObject.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) { + obj.material.transparent = true + } + }) + + wrapper.add(playerObject as any) + const scale = options.scale ?? (1 / 16) + wrapper.scale.set(scale, scale, scale) + wrapper.rotation.set(0, Math.PI, 0) + + // Set up animation + playerObject.animation = new WalkingGeneralSwing() + ;(playerObject.animation as WalkingGeneralSwing).isMoving = false + playerObject.animation.update(playerObject, 0) + + return { playerObject, wrapper } +} + +export const applySkinToPlayerObject = async (playerObject: PlayerObjectType, skinUrl: string) => { + return loadSkinImage(skinUrl || stevePngUrl).then(({ canvas }) => { + const skinTexture = new THREE.CanvasTexture(canvas) + skinTexture.magFilter = THREE.NearestFilter + skinTexture.minFilter = THREE.NearestFilter + skinTexture.needsUpdate = true + playerObject.skin.map = skinTexture as any + }).catch(console.error) +} diff --git a/renderer/viewer/lib/guiRenderer.ts b/renderer/viewer/lib/guiRenderer.ts new file mode 100644 index 00000000..709941dc --- /dev/null +++ b/renderer/viewer/lib/guiRenderer.ts @@ -0,0 +1,282 @@ +// Import placeholders - replace with actual imports for your environment +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 { 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' + +export const getNonFullBlocksModels = () => { + let version = appViewer.resourcesManager.currentResources!.version ?? 'latest' + if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13' + const itemsDefinitions = appViewer.resourcesManager.itemsDefinitionsStore.data.latest + const blockModelsResolved = {} as Record + const itemsModelsResolved = {} as Record + const fullBlocksWithNonStandardDisplay = [] as string[] + const handledItemsWithDefinitions = new Set() + const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(appViewer.resourcesManager.currentResources!.blockstatesModels), getLoadedModelsStore(appViewer.resourcesManager.currentResources!.blockstatesModels)) + + const standardGuiDisplay = { + 'rotation': [ + 30, + 225, + 0 + ], + 'translation': [ + 0, + 0, + 0 + ], + 'scale': [ + 0.625, + 0.625, + 0.625 + ] + } + + const arrEqual = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i]) + const addModelIfNotFullblock = (name: string, model: BlockModelMcAssets) => { + if (blockModelsResolved[name]) return + 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, { + version, + name, + properties: { + 'minecraft:display_context': 'gui', + }, + }) + if (item) { + const { resolvedModel } = assetsParser.getResolvedModelsByModel((item.special ? name : item.model).replace('minecraft:', '')) ?? {} + if (resolvedModel) { + handledItemsWithDefinitions.add(name) + } + if (resolvedModel?.elements) { + let hasStandardDisplay = true + if (resolvedModel['display']?.gui) { + hasStandardDisplay = + arrEqual(resolvedModel['display'].gui.rotation, standardGuiDisplay.rotation) + && arrEqual(resolvedModel['display'].gui.translation, standardGuiDisplay.translation) + && arrEqual(resolvedModel['display'].gui.scale, standardGuiDisplay.scale) + } + + addModelIfNotFullblock(name, resolvedModel) + + if (!blockModelsResolved[name] && !hasStandardDisplay) { + fullBlocksWithNonStandardDisplay.push(name) + } + const notSideLight = resolvedModel['gui_light'] && resolvedModel['gui_light'] !== 'side' + if (!hasStandardDisplay || notSideLight) { + blockModelsResolved[name] = resolvedModel + } + } + if (!blockModelsResolved[name] && item.tints && resolvedModel) { + resolvedModel['tints'] = item.tints + if (resolvedModel.elements) { + blockModelsResolved[name] = resolvedModel + } else { + itemsModelsResolved[name] = resolvedModel + } + } + } + } + + for (const [name, blockstate] of Object.entries(appViewer.resourcesManager.currentResources!.blockstatesModels.blockstates.latest)) { + if (handledItemsWithDefinitions.has(name)) { + continue + } + const resolvedModel = assetsParser.getResolvedModelFirst({ name: name.replace('minecraft:', ''), properties: {} }, true) + if (resolvedModel) { + addModelIfNotFullblock(name, resolvedModel[0]) + } + } + + return { + blockModelsResolved, + itemsModelsResolved + } +} + +// customEvents.on('gameLoaded', () => { +// const res = getNonFullBlocksModels() +// }) + +const RENDER_SIZE = 64 + +const generateItemsGui = async (models: Record, isItems = false) => { + const { currentResources } = appViewer.resourcesManager + const imgBitmap = isItems ? currentResources!.itemsAtlasImage : currentResources!.blocksAtlasImage + const canvasTemp = document.createElement('canvas') + canvasTemp.width = imgBitmap.width + canvasTemp.height = imgBitmap.height + canvasTemp.style.imageRendering = 'pixelated' + const ctx = canvasTemp.getContext('2d')! + ctx.imageSmoothingEnabled = false + ctx.drawImage(imgBitmap, 0, 0) + + const atlasParser = isItems ? appViewer.resourcesManager.itemsAtlasParser : appViewer.resourcesManager.blocksAtlasParser + const textureAtlas = new TextureAtlas( + ctx.getImageData(0, 0, imgBitmap.width, imgBitmap.height), + Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => { + return [key, [ + value.u, + value.v, + (value.u + (value.su ?? atlasParser.atlas.latest.suSv)), + (value.v + (value.sv ?? atlasParser.atlas.latest.suSv)), + ]] as [string, [number, number, number, number]] + })) + ) + + 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 = { + getBlockModel (id) { + if (id.equals(PREVIEW_ID)) { + return BlockModel.fromJson(modelData ?? {}) + } + return null + }, + getTextureUV (texture) { + textureWasRequested = true + return textureAtlas.getTextureUV(texture.toString().replace('minecraft:', '').replace('block/', '').replace('item/', '').replace('blocks/', '').replace('items/', '') as any) + }, + getTextureAtlas () { + return textureAtlas.getTextureAtlas() + }, + getItemComponents (id) { + return new Map() + }, + getItemModel (id) { + // const isSpecial = currentModelName === 'shield' || currentModelName === 'conduit' || currentModelName === 'trident' + const isSpecial = false + if (id.equals(PREVIEW_ID)) { + return ItemModel.fromJson({ + type: isSpecial ? 'minecraft:special' : 'minecraft:model', + model: isSpecial ? { + type: currentModelName, + } : PREVIEW_ID.toString(), + base: PREVIEW_ID.toString(), + tints: modelData?.tints, + }) + } + return null + }, + } + + const canvas = document.createElement('canvas') + canvas.width = RENDER_SIZE + canvas.height = RENDER_SIZE + const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true }) + if (!gl) { + throw new Error('Cannot get WebGL2 context') + } + + function resetGLContext (gl) { + gl.clearColor(0, 0, 0, 0) + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } + + // const includeOnly = ['powered_repeater', 'wooden_door'] + const includeOnly = [] as string[] + + const images: Record = {} + const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({ + 'minecraft:item_model': new NbtString(PREVIEW_ID.toString()), + }))) + 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 = () => { + for (const element of model.elements ?? []) { + for (const [faceName, face] of Object.entries(element.faces)) { + if (face.texture.startsWith('#')) { + missingTextures.add(`${modelName} ${faceName}: ${face.texture}`) + face.texture = 'block/unknown' + } + } + } + } + patchMissingTextures() + // TODO eggs + + modelData = model + currentModelName = modelName + resetGLContext(gl) + 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) + images[modelName] = img + } + + if (missingTextures.size) { + console.warn(`[guiRenderer] Missing textures in ${[...missingTextures].join(', ')}`) + } + + return images +} + +/** + * @mainThread + */ +const generateAtlas = async (images: Record) => { + const atlas = makeTextureAtlas({ + input: Object.keys(images), + tileSize: RENDER_SIZE, + getLoadedImage (name) { + return { + image: images[name], + } + }, + }) + + // const atlasParser = new AtlasParser({ latest: atlas.json }, atlas.canvas.toDataURL()) + // const a = document.createElement('a') + // a.href = await atlasParser.createDebugImage(true) + // a.download = 'blocks_atlas.png' + // a.click() + + appViewer.resourcesManager.currentResources!.guiAtlas = { + json: atlas.json, + image: await createImageBitmap(atlas.canvas), + } + + return atlas +} + +export const generateGuiAtlas = async () => { + const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels() + + // Generate blocks atlas + console.time('generate blocks gui atlas') + const blockImages = await generateItemsGui(blockModelsResolved, false) + console.timeEnd('generate blocks gui atlas') + console.time('generate items gui atlas') + const itemImages = await generateItemsGui(itemsModelsResolved, true) + console.timeEnd('generate items gui atlas') + await generateAtlas({ ...blockImages, ...itemImages }) + appViewer.resourcesManager.currentResources!.guiAtlasVersion++ + // await generateAtlas(blockImages) +} diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts new file mode 100644 index 00000000..a063d77f --- /dev/null +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -0,0 +1,239 @@ +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)) + +if (module.require) { + // If we are in a node environement, we need to fake some env variables + const r = module.require + const { parentPort } = r('worker_threads') + global.self = parentPort + global.postMessage = (value, transferList) => { parentPort.postMessage(value, transferList) } + global.performance = r('perf_hooks').performance +} + +let workerIndex = 0 +let world: World +let dirtySections = new Map() +let allDataReady = false + +function sectionKey (x, y, z) { + return `${x},${y},${z}` +} + +const batchMessagesLimit = 100 + +let queuedMessages = [] as any[] +let queueWaiting = false +const postMessage = (data, transferList = []) => { + queuedMessages.push({ data, transferList }) + if (queuedMessages.length > batchMessagesLimit) { + drainQueue(0, batchMessagesLimit) + } + if (queueWaiting) return + queueWaiting = true + setTimeout(() => { + queueWaiting = false + drainQueue(0, queuedMessages.length) + }) +} + +function drainQueue (from, to) { + const messages = queuedMessages.slice(from, to) + global.postMessage(messages.map(m => m.data), messages.flatMap(m => m.transferList) as unknown as string) + queuedMessages = queuedMessages.slice(to) +} + +function setSectionDirty (pos, value = true) { + const x = Math.floor(pos.x / 16) * 16 + const y = Math.floor(pos.y / 16) * 16 + const z = Math.floor(pos.z / 16) * 16 + const key = sectionKey(x, y, z) + if (!value) { + dirtySections.delete(key) + postMessage({ type: 'sectionFinished', key, workerIndex }) + return + } + + const chunk = world.getColumn(x, z) + if (chunk?.getSection(pos)) { + dirtySections.set(key, (dirtySections.get(key) || 0) + 1) + } else { + postMessage({ type: 'sectionFinished', key, workerIndex }) + } +} + +const softCleanup = () => { + // clean block cache and loaded chunks + world = new World(world.config.version) + globalThis.world = world +} + +const handleMessage = data => { + const globalVar: any = globalThis + + if (data.type === 'mcData') { + globalVar.mcData = data.mcData + globalVar.loadedData = data.mcData + } + + if (data.config) { + if (data.type === 'mesherData' && world) { + // reset models + world.blockCache = {} + world.erroredBlockModel = undefined + } + + world ??= new World(data.config.version) + world.config = { ...world.config, ...data.config } + globalThis.world = world + globalThis.Vec3 = Vec3 + } + + switch (data.type) { + case 'mesherData': { + setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu') + allDataReady = true + workerIndex = data.workerIndex + + break + } + case 'dirty': { + const loc = new Vec3(data.x, data.y, data.z) + setSectionDirty(loc, data.value) + + break + } + case 'chunk': { + world.addColumn(data.x, data.z, data.chunk) + if (data.customBlockModels) { + const chunkKey = `${data.x},${data.z}` + world.customBlockModels.set(chunkKey, data.customBlockModels) + } + break + } + case 'unloadChunk': { + world.removeColumn(data.x, data.z) + world.customBlockModels.delete(`${data.x},${data.z}`) + if (Object.keys(world.columns).length === 0) softCleanup() + break + } + case 'blockUpdate': { + const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored() + if (data.stateId !== undefined && data.stateId !== null) { + world?.setBlockStateId(loc, data.stateId) + } + + const chunkKey = `${Math.floor(loc.x / 16) * 16},${Math.floor(loc.z / 16) * 16}` + if (data.customBlockModels) { + world?.customBlockModels.set(chunkKey, data.customBlockModels) + } + break + } + case 'reset': { + world = undefined as any + // blocksStates = null + 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 + } +} + +// eslint-disable-next-line no-restricted-globals -- TODO +self.onmessage = ({ data }) => { + if (Array.isArray(data)) { + // eslint-disable-next-line unicorn/no-array-for-each + data.forEach(handleMessage) + return + } + + handleMessage(data) +} + +setInterval(() => { + if (world === null || !allDataReady) return + + if (dirtySections.size === 0) return + // console.log(sections.length + ' dirty sections') + + // const start = performance.now() + for (const key of dirtySections.keys()) { + const [x, y, z] = key.split(',').map(v => parseInt(v, 10)) + const chunk = world.getColumn(x, z) + let processTime = 0 + if (chunk?.getSection(new Vec3(x, y, z))) { + const start = performance.now() + const geometry = getSectionGeometry(x, y, z, world) + const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) + //@ts-expect-error + postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable) + processTime = performance.now() - start + } else { + // console.info('[mesher] Missing section', x, y, z) + } + const dirtyTimes = dirtySections.get(key) + if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy') + for (let i = 0; i < dirtyTimes; i++) { + postMessage({ type: 'sectionFinished', key, workerIndex, processTime }) + processTime = 0 + } + dirtySections.delete(key) + } + + // Send new block state model info if any + if (world.blockStateModelInfo.size > 0) { + const newBlockStateInfo: Record = {} + for (const [cacheKey, info] of world.blockStateModelInfo) { + if (!world.sentBlockStateModels.has(cacheKey)) { + newBlockStateInfo[cacheKey] = info + world.sentBlockStateModels.add(cacheKey) + } + } + if (Object.keys(newBlockStateInfo).length > 0) { + postMessage({ type: 'blockStateModelInfo', info: newBlockStateInfo }) + } + } + + // const time = performance.now() - start + // console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`) +}, 50) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts new file mode 100644 index 00000000..aca47e15 --- /dev/null +++ b/renderer/viewer/lib/mesher/models.ts @@ -0,0 +1,745 @@ +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 { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon' +import { INVISIBLE_BLOCKS } from './worldConstants' +import { MesherGeometryOutput, HighestBlockInfo } from './shared' + + +let blockProvider: WorldBlockProvider + +const tints: any = {} +let needTiles = false + +let tintsData +try { + tintsData = require('esbuild-data').tints +} catch (err) { + tintsData = require('minecraft-data/minecraft-data/data/pc/1.16.2/tints.json') +} +for (const key of Object.keys(tintsData)) { + tints[key] = prepareTints(tintsData[key]) +} + +type Tiles = { + [blockPos: string]: BlockType +} + +function prepareTints (tints) { + const map = new Map() + const defaultValue = tintToGl(tints.default) + for (let { keys, color } of tints.data) { + color = tintToGl(color) + for (const key of keys) { + map.set(`${key}`, color) + } + } + return new Proxy(map, { + get (target, key) { + return target.has(key) ? target.get(key) : defaultValue + } + }) +} + +const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks) +export function preflatBlockCalculation (block: Block, world: World, position: Vec3) { + const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0] + if (!type) return + switch (type) { + case 'directional': { + const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire') + const neighbors = [ + world.getBlock(position.offset(0, 0, 1)), + world.getBlock(position.offset(0, 0, -1)), + world.getBlock(position.offset(1, 0, 0)), + world.getBlock(position.offset(-1, 0, 0)) + ] + // set needed props to true: east:'false',north:'false',south:'false',west:'false' + const props = {} + let changed = false + for (const [i, neighbor] of neighbors.entries()) { + const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false + if (isConnectedToSolid || neighbor?.name === block.name) { + props[['south', 'north', 'east', 'west'][i]] = 'true' + changed = true + } + } + return changed ? props : undefined + } + // case 'gate_in_wall': {} + case 'block_snowy': { + const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow' + if (aboveIsSnow) { + return { + snowy: `${aboveIsSnow}` + } + } else { + return + } + } + case 'door': { + // upper half matches lower in + const { half } = block.getProperties() + if (half === 'upper') { + // copy other properties + const lower = world.getBlock(position.offset(0, -1, 0)) + if (lower?.name === block.name) { + return { + ...lower.getProperties(), + half: 'upper' + } + } + } + } + } +} + +function tintToGl (tint) { + const r = (tint >> 16) & 0xff + const g = (tint >> 8) & 0xff + const b = tint & 0xff + 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 + if (!block || block.type !== type) return 1 / 9 + if (block.metadata === 0) { // source block + const blockAbove = world.getBlock(pos.offset(0, 1, 0)) + if (blockAbove && blockAbove.type === type) return 1 + return 8 / 9 + } + return ((block.metadata >= 8 ? 8 : 7 - block.metadata) + 1) / 9 +} + + +const isCube = (block: Block) => { + if (!block || block.transparent) return false + if (block.isCube) return true + if (!block.models?.length || block.models.length !== 1) return false + // all variants + return block.models[0].every(v => v.elements.every(e => { + return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16 + })) +} + +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) { + 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)) + } + } + const cornerHeights = [ + Math.max(Math.max(heights[0], heights[1]), Math.max(heights[3], heights[4])), + Math.max(Math.max(heights[1], heights[2]), Math.max(heights[4], heights[5])), + Math.max(Math.max(heights[3], heights[4]), Math.max(heights[6], heights[7])), + Math.max(Math.max(heights[4], heights[5]), Math.max(heights[7], heights[8])) + ] + + // eslint-disable-next-line guard-for-in + for (const face in elemFaces) { + const { dir, corners, mask1, mask2 } = 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 + + let tint = [1, 1, 1] + if (water) { + let m = 1 // Fake lighting to improve lisibility + if (Math.abs(dir[0]) > 0) m = 0.6 + else if (Math.abs(dir[2]) > 0) m = 0.8 + tint = tints.water[biome] + tint = [tint[0] * m, tint[1] * m, tint[2] * m] + } + + if (needTiles) { + const tiles = attr.tiles as Tiles + tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + block: 'water', + faces: [], + } + tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + face, + neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, + side: 0, // todo + textureIndex: 0, + // texture: eFace.texture.name, + }) + } + + const { u } = texture + const { v } = texture + 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_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) + } + } +} + +const identicalCull = (currentElement: BlockElement, neighbor: Block, direction: Vec3) => { + const dirStr = `${direction.x},${direction.y},${direction.z}` + const lookForOppositeSide = { + '0,1,0': 'down', + '0,-1,0': 'up', + '1,0,0': 'east', + '-1,0,0': 'west', + '0,0,1': 'south', + '0,0,-1': 'north', + }[dirStr]! + const elemCompareForm = { + '0,1,0': (e: BlockElement) => `${e.from[0]},${e.from[2]}:${e.to[0]},${e.to[2]}`, + '0,-1,0': (e: BlockElement) => `${e.to[0]},${e.to[2]}:${e.from[0]},${e.from[2]}`, + '1,0,0': (e: BlockElement) => `${e.from[2]},${e.from[1]}:${e.to[2]},${e.to[1]}`, + '-1,0,0': (e: BlockElement) => `${e.to[2]},${e.to[1]}:${e.from[2]},${e.from[1]}`, + '0,0,1': (e: BlockElement) => `${e.from[1]},${e.from[2]}:${e.to[1]},${e.to[2]}`, + '0,0,-1': (e: BlockElement) => `${e.to[1]},${e.to[2]}:${e.from[1]},${e.from[2]}`, + }[dirStr]! + const elementEdgeValidator = { + '0,1,0': (e: BlockElement) => currentElement.from[1] === 0 && e.to[2] === 16, + '0,-1,0': (e: BlockElement) => currentElement.from[1] === 0 && e.to[2] === 16, + '1,0,0': (e: BlockElement) => currentElement.from[0] === 0 && e.to[1] === 16, + '-1,0,0': (e: BlockElement) => currentElement.from[0] === 0 && e.to[1] === 16, + '0,0,1': (e: BlockElement) => currentElement.from[2] === 0 && e.to[0] === 16, + '0,0,-1': (e: BlockElement) => currentElement.from[2] === 0 && e.to[0] === 16, + }[dirStr]! + const useVar = 0 + const models = neighbor.models?.map(m => m[useVar] ?? m[0]) ?? [] + // TODO we should support it! rewrite with optimizing general pipeline + if (models.some(m => m.x || m.y || m.z)) return + return models.every(model => { + return (model.elements ?? []).every(element => { + // todo check alfa on texture + return !!(element.faces[lookForOppositeSide]?.cullface && elemCompareForm(currentElement) === elemCompareForm(element) && elementEdgeValidator(element)) + }) + }) +} + +let needSectionRecomputeOnChange = false + +function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) { + const position = cursor + // const key = `${position.x},${position.y},${position.z}` + // if (!globalThis.allowedBlocks.includes(key)) return + const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice') + + // eslint-disable-next-line guard-for-in + for (const face in element.faces) { + const eFace = element.faces[face] + const { corners, mask1, mask2, side } = elemFaces[face] + const dir = matmul3(globalMatrix, elemFaces[face].dir) + + if (eFace.cullface) { + const neighbor = world.getBlock(cursor.plus(new Vec3(...dir)), blockProvider, {}) + if (neighbor) { + if (cullIfIdentical && neighbor.stateId === block.stateId) continue + if (!neighbor.transparent && (isCube(neighbor) || identicalCull(element, neighbor, new Vec3(...dir)))) continue + } else { + needSectionRecomputeOnChange = true + // continue + } + } + + const minx = element.from[0] + const miny = element.from[1] + const minz = element.from[2] + const maxx = element.to[0] + const maxy = element.to[1] + const maxz = element.to[2] + + const texture = eFace.texture as any + const { u } = texture + const { v } = texture + const { su } = texture + const { sv } = texture + + const ndx = Math.floor(attr.positions.length / 3) + + let tint = [1, 1, 1] + if (eFace.tintindex !== undefined) { + if (eFace.tintindex === 0) { + if (block.name === 'redstone_wire') { + tint = tints.redstone[`${block.getProperties().power}`] + } else if (block.name === 'birch_leaves' || + block.name === 'spruce_leaves' || + block.name === 'lily_pad') { + tint = tints.constant[block.name] + } else if (block.name.includes('leaves') || block.name === 'vine') { + tint = tints.foliage[biome] + } else { + tint = tints.grass[biome] + } + } + } + + // UV rotation + let r = eFace.rotation || 0 + if (face === 'down') { + r += 180 + } + const uvcs = Math.cos(r * Math.PI / 180) + const uvsn = -Math.sin(r * Math.PI / 180) + + let localMatrix = null as any + let localShift = null as any + + if (element.rotation && !needTiles) { + // Rescale support for block model rotations + localMatrix = buildRotationMatrix( + element.rotation.axis, + element.rotation.angle + ) + + localShift = vecsub3( + element.rotation.origin, + matmul3( + localMatrix, + element.rotation.origin + ) + ) + + // Apply rescale if specified + if (element.rotation.rescale) { + const FIT_TO_BLOCK_SCALE_MULTIPLIER = 2 - Math.sqrt(2) + const angleRad = element.rotation.angle * Math.PI / 180 + const scale = Math.abs(Math.sin(angleRad)) * FIT_TO_BLOCK_SCALE_MULTIPLIER + + // Get axis vector components (1 for the rotation axis, 0 for others) + const axisX = element.rotation.axis === 'x' ? 1 : 0 + const axisY = element.rotation.axis === 'y' ? 1 : 0 + const axisZ = element.rotation.axis === 'z' ? 1 : 0 + + // Create scale matrix: scale = (1 - axisComponent) * scaleFactor + 1 + const scaleMatrix = [ + [(1 - axisX) * scale + 1, 0, 0], + [0, (1 - axisY) * scale + 1, 0], + [0, 0, (1 - axisZ) * scale + 1] + ] + + // Apply scaling to the transformation matrix + localMatrix = matmulmat3(localMatrix, scaleMatrix) + + // Recalculate shift with the new matrix + localShift = vecsub3( + element.rotation.origin, + matmul3( + localMatrix, + element.rotation.origin + ) + ) + } + } + + const aos: number[] = [] + const neighborPos = position.plus(new Vec3(...dir)) + // 10% + const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15 + for (const pos of corners) { + let vertex = [ + (pos[0] ? maxx : minx), + (pos[1] ? maxy : miny), + (pos[2] ? maxz : minz) + ] + + if (!needTiles) { // 10% + vertex = vecadd3(matmul3(localMatrix, vertex), localShift) + vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) + vertex = vertex.map(v => v / 16) + + attr.positions.push( + vertex[0] + (cursor.x & 15) - 8, + vertex[1] + (cursor.y & 15) - 8, + vertex[2] + (cursor.z & 15) - 8 + ) + + attr.normals.push(...dir) + + const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 + const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 + attr.uvs.push(baseu * su + u, basev * sv + v) + } + + let light = 1 + const { smoothLighting } = world.config + // const smoothLighting = true + if (doAO) { + const dx = pos[0] * 2 - 1 + const dy = pos[1] * 2 - 1 + const dz = pos[2] * 2 - 1 + const cornerDir = matmul3(globalMatrix, [dx, dy, dz]) + const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]]) + const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]]) + const side1 = world.getBlock(cursor.offset(...side1Dir)) + const side2 = world.getBlock(cursor.offset(...side2Dir)) + const corner = world.getBlock(cursor.offset(...cornerDir)) + + let cornerLightResult = baseLight * 15 + + if (smoothLighting) { + const dirVec = new Vec3(...dir) + const getVec = (v: Vec3) => { + for (const coord of ['x', 'y', 'z']) { + if (Math.abs(dirVec[coord]) > 0) v[coord] = 0 + } + return v.plus(dirVec) + } + const side1LightDir = getVec(new Vec3(...side1Dir)) + const side1Light = world.getLight(cursor.plus(side1LightDir)) + const side2DirLight = getVec(new Vec3(...side2Dir)) + const side2Light = world.getLight(cursor.plus(side2DirLight)) + const cornerLightDir = getVec(new Vec3(...cornerDir)) + const cornerLight = world.getLight(cursor.plus(cornerLightDir)) + // interpolate + const lights = [side1Light, side2Light, cornerLight, baseLight * 15] + cornerLightResult = lights.reduce((acc, cur) => acc + cur, 0) / lights.length + } + + const side1Block = world.shouldMakeAo(side1) ? 1 : 0 + const side2Block = world.shouldMakeAo(side2) ? 1 : 0 + const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0 + + // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block) + + const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock)) + // todo light should go upper on lower blocks + light = (ao + 1) / 4 * (cornerLightResult / 15) + aos.push(ao) + } + + if (!needTiles) { + attr.colors.push(tint[0] * light, tint[1] * light, tint[2] * light) + } + } + + const lightWithColor = [baseLight * tint[0], baseLight * tint[1], baseLight * tint[2]] as [number, number, number] + + if (needTiles) { + const tiles = attr.tiles as Tiles + tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + block: block.name, + faces: [], + } + const needsOnlyOneFace = false + const isTilesEmpty = tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.length < 1 + if (isTilesEmpty || !needsOnlyOneFace) { + tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + face, + side, + textureIndex: eFace.texture.tileIndex, + neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, + light: baseLight, + tint: lightWithColor, + //@ts-expect-error debug prop + texture: eFace.texture.debugName || block.name, + } satisfies BlockType['faces'][number]) + } + } + + 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 + } 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 + } + } + } +} + +const ALWAYS_WATERLOGGED = new Set([ + 'seagrass', + 'tall_seagrass', + 'kelp', + 'kelp_plant', + 'bubble_column' +]) +const isBlockWaterlogged = (block: Block) => { + return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name) +} + +let unknownBlockModel: BlockModelPartsResolved +export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) { + let delayedRender = [] as Array<() => void> + + const attr: MesherGeometryOutput = { + sx: sx + 8, + sy: sy + 8, + sz: sz + 8, + positions: [], + normals: [], + colors: [], + uvs: [], + t_positions: [], + t_normals: [], + t_colors: [], + t_uvs: [], + indices: [], + indicesCount: 0, // Track current index position + using32Array: true, + tiles: {}, + // todo this can be removed here + heads: {}, + signs: {}, + // isFull: true, + hadErrors: false, + blocksCount: 0 + } + + const cursor = new Vec3(0, 0, 0) + for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) { + 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)) continue + if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) { + const key = `${cursor.x},${cursor.y},${cursor.z}` + const props: any = block.getProperties() + const facingRotationMap = { + 'north': 2, + 'south': 0, + 'west': 1, + 'east': 3 + } + const isWall = block.name.endsWith('wall_sign') || block.name.endsWith('wall_hanging_sign') + const isHanging = block.name.endsWith('hanging_sign') + attr.signs[key] = { + isWall, + isHanging, + rotation: isWall ? facingRotationMap[props.facing] : +props.rotation + } + } else if (block.name === 'player_head' || block.name === 'player_wall_head') { + const key = `${cursor.x},${cursor.y},${cursor.z}` + const props: any = block.getProperties() + const facingRotationMap = { + 'north': 0, + 'south': 2, + 'west': 3, + 'east': 1 + } + const isWall = block.name === 'player_wall_head' + attr.heads[key] = { + isWall, + rotation: isWall ? facingRotationMap[props.facing] : +props.rotation + } + } + const biome = block.biome.name + + if (world.preflat) { // 10% perf + const patchProperties = preflatBlockCalculation(block, world, cursor) + if (patchProperties) { + block._originalProperties ??= block._properties + block._properties = { ...block._originalProperties, ...patchProperties } + if (block.models && JSON.stringify(block._originalProperties) !== JSON.stringify(block._properties)) { + // recompute models + block.models = undefined + block = world.getBlock(cursor, blockProvider, attr)! + } + } else { + block._properties = block._originalProperties ?? block._properties + block._originalProperties = undefined + } + } + + const isWaterlogged = isBlockWaterlogged(block) + if (block.name === 'water' || isWaterlogged) { + 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) + }) + attr.blocksCount++ + } else if (block.name === 'lava') { + renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false) + attr.blocksCount++ + } + if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) { + // cache + let { models } = block + + models ??= unknownBlockModel + + const firstForceVar = world.config.debugModelVariant?.[0] + let part = 0 + for (const modelVars of models ?? []) { + const pos = cursor.clone() + // const variantRuntime = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), modelVars.length) + const variantRuntime = 0 + const useVariant = world.config.debugModelVariant?.[part] ?? firstForceVar ?? variantRuntime + part++ + const model = modelVars[useVariant] ?? modelVars[0] + if (!model) continue + + // #region 10% + let globalMatrix = null as any + let globalShift = null as any + for (const axis of ['x', 'y', 'z'] as const) { + if (axis in model) { + globalMatrix = globalMatrix ? + matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0))) : + buildRotationMatrix(axis, -(model[axis] ?? 0)) + } + } + if (globalMatrix) { + globalShift = [8, 8, 8] + globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) + } + // #endregion + + for (const element of model.elements ?? []) { + const ao = model.ao ?? true + if (block.transparent) { + const pos = cursor.clone() + delayedRender.push(() => { + renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome) + }) + } else { + // 60% + renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome) + } + } + } + if (part > 0) attr.blocksCount++ + } + } + } + } + + for (const render of delayedRender) { + render() + } + delayedRender = [] + + 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 + ndx += 4 + } + + attr.positions.push(...attr.t_positions!) + attr.normals.push(...attr.t_normals!) + attr.colors.push(...attr.t_colors!) + attr.uvs.push(...attr.t_uvs!) + + delete attr.t_positions + delete attr.t_normals + delete attr.t_colors + delete attr.t_uvs + + attr.positions = new Float32Array(attr.positions) as any + 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 + delete attr.normals + delete attr.colors + delete attr.uvs + } + + 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 + if (useUnknownBlockModel) { + unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} }) + } + + needTiles = _needTiles +} diff --git a/renderer/viewer/lib/mesher/modelsGeometryCommon.ts b/renderer/viewer/lib/mesher/modelsGeometryCommon.ts new file mode 100644 index 00000000..3df20556 --- /dev/null +++ b/renderer/viewer/lib/mesher/modelsGeometryCommon.ts @@ -0,0 +1,142 @@ +import { BlockModelPartsResolved } from './world' + +export type BlockElement = NonNullable[0] + + +export function buildRotationMatrix (axis, degree) { + const radians = degree / 180 * Math.PI + const cos = Math.cos(radians) + const sin = Math.sin(radians) + + const axis0 = { x: 0, y: 1, z: 2 }[axis] + const axis1 = (axis0 + 1) % 3 + const axis2 = (axis0 + 2) % 3 + + const matrix = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ] + + matrix[axis0][axis0] = 1 + matrix[axis1][axis1] = cos + matrix[axis1][axis2] = -sin + matrix[axis2][axis1] = +sin + matrix[axis2][axis2] = cos + + return matrix +} + +export function vecadd3 (a, b) { + if (!b) return a + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]] +} + +export function vecsub3 (a, b) { + if (!b) return a + return [a[0] - b[0], a[1] - b[1], a[2] - b[2]] +} + +export function matmul3 (matrix, vector): [number, number, number] { + if (!matrix) return vector + return [ + matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2], + matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2], + matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2] + ] +} + +export function matmulmat3 (a, b) { + const te = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + + const a11 = a[0][0]; const a12 = a[1][0]; const a13 = a[2][0] + const a21 = a[0][1]; const a22 = a[1][1]; const a23 = a[2][1] + const a31 = a[0][2]; const a32 = a[1][2]; const a33 = a[2][2] + + const b11 = b[0][0]; const b12 = b[1][0]; const b13 = b[2][0] + const b21 = b[0][1]; const b22 = b[1][1]; const b23 = b[2][1] + const b31 = b[0][2]; const b32 = b[1][2]; const b33 = b[2][2] + + te[0][0] = a11 * b11 + a12 * b21 + a13 * b31 + te[1][0] = a11 * b12 + a12 * b22 + a13 * b32 + te[2][0] = a11 * b13 + a12 * b23 + a13 * b33 + + te[0][1] = a21 * b11 + a22 * b21 + a23 * b31 + te[1][1] = a21 * b12 + a22 * b22 + a23 * b32 + te[2][1] = a21 * b13 + a22 * b23 + a23 * b33 + + te[0][2] = a31 * b11 + a32 * b21 + a33 * b31 + te[1][2] = a31 * b12 + a32 * b22 + a33 * b32 + te[2][2] = a31 * b13 + a32 * b23 + a33 * b33 + + return te +} + +export const elemFaces = { + up: { + dir: [0, 1, 0], + mask1: [1, 1, 0], + mask2: [0, 1, 1], + corners: [ + [0, 1, 1, 0, 1], + [1, 1, 1, 1, 1], + [0, 1, 0, 0, 0], + [1, 1, 0, 1, 0] + ] + }, + down: { + dir: [0, -1, 0], + mask1: [1, 1, 0], + mask2: [0, 1, 1], + corners: [ + [1, 0, 1, 0, 1], + [0, 0, 1, 1, 1], + [1, 0, 0, 0, 0], + [0, 0, 0, 1, 0] + ] + }, + east: { + dir: [1, 0, 0], + mask1: [1, 1, 0], + mask2: [1, 0, 1], + corners: [ + [1, 1, 1, 0, 0], + [1, 0, 1, 0, 1], + [1, 1, 0, 1, 0], + [1, 0, 0, 1, 1] + ] + }, + west: { + dir: [-1, 0, 0], + mask1: [1, 1, 0], + mask2: [1, 0, 1], + corners: [ + [0, 1, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 1, 1, 1, 0], + [0, 0, 1, 1, 1] + ] + }, + north: { + dir: [0, 0, -1], + mask1: [1, 0, 1], + mask2: [0, 1, 1], + corners: [ + [1, 0, 0, 1, 1], + [0, 0, 0, 0, 1], + [1, 1, 0, 1, 0], + [0, 1, 0, 0, 0] + ] + }, + south: { + dir: [0, 0, 1], + mask1: [1, 0, 1], + mask2: [0, 1, 1], + corners: [ + [0, 0, 1, 0, 1], + [1, 0, 1, 1, 1], + [0, 1, 1, 0, 0], + [1, 1, 1, 1, 0] + ] + } +} diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts new file mode 100644 index 00000000..230db6b9 --- /dev/null +++ b/renderer/viewer/lib/mesher/shared.ts @@ -0,0 +1,70 @@ +import { BlockType } from '../../../playground/shared' + +// only here for easier testing +export const defaultMesherConfig = { + version: '', + worldMaxY: 256, + worldMinY: 0, + enableLighting: true, + skyLight: 15, + smoothLighting: true, + outputFormat: 'threeJs' as 'threeJs' | 'webgpu', + // textureSize: 1024, // for testing + debugModelVariant: undefined as undefined | number[], + clipWorldBelowY: undefined as undefined | number, + disableSignsMapsSupport: false +} + +export type CustomBlockModels = { + [blockPosKey: string]: string // blockPosKey is "x,y,z" -> model name +} + +export type MesherConfig = typeof defaultMesherConfig + +export type MesherGeometryOutput = { + sx: number, + sy: number, + sz: number, + // resulting: float32array + positions: any, + normals: any, + colors: any, + uvs: any, + t_positions?: number[], + t_normals?: number[], + t_colors?: number[], + t_uvs?: number[], + + indices: Uint32Array | Uint16Array | number[], + indicesCount: number, + using32Array: boolean, + tiles: Record, + heads: Record, + signs: Record, + // isFull: boolean + hadErrors: boolean + blocksCount: number + customBlockModels?: CustomBlockModels +} + +export interface MesherMainEvents { + geometry: { type: 'geometry'; key: string; geometry: MesherGeometryOutput; workerIndex: number }; + sectionFinished: { type: 'sectionFinished'; key: string; workerIndex: number; processTime?: number }; + blockStateModelInfo: { type: 'blockStateModelInfo'; info: Record }; + heightmap: { type: 'heightmap'; key: string; heightmap: Uint8Array }; +} + +export type MesherMainEvent = MesherMainEvents[keyof MesherMainEvents] + +export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined } + +export type BlockStateModelInfo = { + cacheKey: string + issues: string[] + modelNames: string[] + conditions: string[] +} + +export const getBlockAssetsCacheKey = (stateId: number, modelNameOverride?: string) => { + return modelNameOverride ? `${stateId}:${modelNameOverride}` : String(stateId) +} diff --git a/renderer/viewer/lib/mesher/standaloneRenderer.ts b/renderer/viewer/lib/mesher/standaloneRenderer.ts new file mode 100644 index 00000000..3d468dce --- /dev/null +++ b/renderer/viewer/lib/mesher/standaloneRenderer.ts @@ -0,0 +1,270 @@ +/* eslint-disable @stylistic/function-call-argument-newline */ +import { Vec3 } from 'vec3' +import { Block } from 'prismarine-block' +import { IndexedData } from 'minecraft-data' +import * as THREE from 'three' +import { BlockModelPartsResolved } from './world' +import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon' + +type NeighborSide = 'up' | 'down' | 'east' | 'west' | 'north' | 'south' + +function tintToGl (tint) { + const r = (tint >> 16) & 0xff + const g = (tint >> 8) & 0xff + const b = tint & 0xff + return [r / 255, g / 255, b / 255] +} + +type Neighbors = Partial> +function renderElement (element: BlockElement, doAO: boolean, attr, globalMatrix, globalShift, block: Block | undefined, biome: string, neighbors: Neighbors) { + const cursor = new Vec3(0, 0, 0) + + // const key = `${position.x},${position.y},${position.z}` + // if (!globalThis.allowedBlocks.includes(key)) return + // const cullIfIdentical = block.name.indexOf('glass') >= 0 + + // eslint-disable-next-line guard-for-in + for (const face in element.faces) { + const eFace = element.faces[face] + const { corners, mask1, mask2 } = elemFaces[face] + const dir = matmul3(globalMatrix, elemFaces[face].dir) + + if (eFace.cullface) { + if (neighbors[face]) continue + } + + const minx = element.from[0] + const miny = element.from[1] + const minz = element.from[2] + const maxx = element.to[0] + const maxy = element.to[1] + const maxz = element.to[2] + + const texture = eFace.texture as any + const { u } = texture + const { v } = texture + const { su } = texture + const { sv } = texture + + const ndx = Math.floor(attr.positions.length / 3) + + let tint = [1, 1, 1] + if (eFace.tintindex !== undefined) { + if (eFace.tintindex === 0) { + // TODO + // if (block.name === 'redstone_wire') { + // tint = tints.redstone[`${block.getProperties().power}`] + // } else if (block.name === 'birch_leaves' || + // block.name === 'spruce_leaves' || + // block.name === 'lily_pad') { + // tint = tints.constant[block.name] + // } else if (block.name.includes('leaves') || block.name === 'vine') { + // tint = tints.foliage[biome] + // } else { + // tint = tints.grass[biome] + // } + const grassTint = [145 / 255, 189 / 255, 89 / 255] + tint = grassTint + } + } + + // UV rotation + const r = eFace.rotation || 0 + const uvcs = Math.cos(r * Math.PI / 180) + const uvsn = -Math.sin(r * Math.PI / 180) + + let localMatrix = null as any + let localShift = null as any + + if (element.rotation) { + // todo do we support rescale? + localMatrix = buildRotationMatrix( + element.rotation.axis, + element.rotation.angle + ) + + localShift = vecsub3( + element.rotation.origin, + matmul3( + localMatrix, + element.rotation.origin + ) + ) + } + + const aos: number[] = [] + // const neighborPos = position.plus(new Vec3(...dir)) + // const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15 + const baseLight = 1 + for (const pos of corners) { + let vertex = [ + (pos[0] ? maxx : minx), + (pos[1] ? maxy : miny), + (pos[2] ? maxz : minz) + ] + + vertex = vecadd3(matmul3(localMatrix, vertex), localShift) + vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) + vertex = vertex.map(v => v / 16) + + attr.positions.push( + vertex[0]/* + (cursor.x & 15) - 8 */, + vertex[1]/* + (cursor.y & 15) x */, + vertex[2]/* + (cursor.z & 15) - 8 */ + ) + + attr.normals.push(...dir) + + const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 + const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 + attr.uvs.push(baseu * su + u, basev * sv + v) + + let light = 1 + if (doAO) { + const cornerLightResult = 15 + + const side1Block = 0 + const side2Block = 0 + const cornerBlock = 0 + + const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock)) + // todo light should go upper on lower blocks + light = (ao + 1) / 4 * (cornerLightResult / 15) + aos.push(ao) + } + + attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light) + } + + // if (needTiles) { + // attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + // block: block.name, + // faces: [], + // } + // attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + // face, + // neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, + // light: baseLight + // // texture: eFace.texture.name, + // }) + // } + + if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { + attr.indices.push( + + ndx, ndx + 3, ndx + 2, + ndx, ndx + 1, ndx + 3 + ) + } else { + attr.indices.push( + + ndx, ndx + 1, ndx + 2, + ndx + 2, ndx + 1, ndx + 3 + ) + } + } +} + +export const renderBlockThreeAttr = (models: BlockModelPartsResolved, block: Block | undefined, biome: string, mcData: IndexedData, variants = [], neighbors: Neighbors = {}) => { + const sx = 0 + const sy = 0 + const sz = 0 + + const attr = { + sx: sx + 0.5, + sy: sy + 0.5, + sz: sz + 0.5, + positions: [], + normals: [], + colors: [], + uvs: [], + t_positions: [], + t_normals: [], + t_colors: [], + t_uvs: [], + indices: [], + tiles: {}, + } as Record + + for (const [i, modelVars] of models.entries()) { + const model = modelVars[variants[i]] ?? modelVars[0] + if (!model) continue + let globalMatrix = null as any + let globalShift = null as any + for (const axis of ['x', 'y', 'z'] as const) { + if (axis in model) { + if (globalMatrix) { globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0))) } else { globalMatrix = buildRotationMatrix(axis, -(model[axis] ?? 0)) } + } + } + if (globalMatrix) { + globalShift = [8, 8, 8] + globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) + } + + const ao = model.ao ?? true + + for (const element of model.elements ?? []) { + renderElement(element, ao, attr, globalMatrix, globalShift, block, biome, neighbors) + } + } + + let ndx = attr.positions.length / 3 + for (let i = 0; i < attr.t_positions.length / 12; i++) { + attr.indices.push( + ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3, + // back face + ndx, ndx + 2, ndx + 1, ndx + 2, ndx + 3, ndx + 1 + ) + ndx += 4 + } + + attr.positions.push(...attr.t_positions) + attr.normals.push(...attr.t_normals) + attr.colors.push(...attr.t_colors) + attr.uvs.push(...attr.t_uvs) + + delete attr.t_positions + delete attr.t_normals + delete attr.t_colors + delete attr.t_uvs + + attr.positions = new Float32Array(attr.positions) as any + attr.normals = new Float32Array(attr.normals) as any + attr.colors = new Float32Array(attr.colors) as any + attr.uvs = new Float32Array(attr.uvs) as any + + return attr +} + +export const renderBlockThree = (...args: Parameters) => { + const attr = renderBlockThreeAttr(...args) + const data = { + geometry: attr + } + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3)) + geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3)) + geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3)) + geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2)) + geometry.setIndex(data.geometry.indices) + geometry.name = 'block-geometry' + + return geometry +} + +export const getThreeBlockModelGroup = (material: THREE.Material, ...args: Parameters) => { + const geometry = renderBlockThree(...args) + const mesh = new THREE.Mesh(geometry, material) + mesh.position.set(-0.5, -0.5, -0.5) + const group = new THREE.Group() + group.add(mesh) + group.rotation.set(0, -THREE.MathUtils.degToRad(90), 0, 'ZYX') + globalThis.mesh = group + return group + // return new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 })) +} + +export const setBlockPosition = (object: THREE.Object3D, position: { x: number, y: number, z: number }) => { + object.position.set(position.x + 0.5, position.y + 0.5, position.z + 0.5) +} diff --git a/renderer/viewer/lib/mesher/test/mesherTester.ts b/renderer/viewer/lib/mesher/test/mesherTester.ts new file mode 100644 index 00000000..e75d803d --- /dev/null +++ b/renderer/viewer/lib/mesher/test/mesherTester.ts @@ -0,0 +1,76 @@ +import ChunkLoader, { PCChunk } from 'prismarine-chunk' +import { Vec3 } from 'vec3' +import MinecraftData from 'minecraft-data' +import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json' +import { World as MesherWorld } from '../world' +import { setBlockStatesData, getSectionGeometry } from '../models' + +export const setup = (version, initialBlocks: Array<[number[], string]>) => { + const mcData = MinecraftData(version) + const blockStatesModels = require(`mc-assets/dist/blockStatesModels.json`) + const mesherWorld = new MesherWorld(version) + const Chunk = ChunkLoader(version) + const chunk1 = new Chunk(undefined as any) + + const pos = new Vec3(2, 5, 2) + for (const [addPos, name] of initialBlocks) { + chunk1.setBlockStateId(pos.offset(addPos[0], addPos[1], addPos[2]), mcData.blocksByName[name].defaultState) + } + + const getGeometry = () => { + const sectionGeometry = getSectionGeometry(0, 0, 0, mesherWorld) + const centerFaces = sectionGeometry.tiles[`${pos.x},${pos.y},${pos.z}`]?.faces.length ?? 0 + const totalTiles = Object.values(sectionGeometry.tiles).reduce((acc, val: any) => acc + val.faces.length, 0) + const centerTileNeighbors = Object.entries(sectionGeometry.tiles).reduce((acc, [key, val]: any) => { + return acc + val.faces.filter((face: any) => face.neighbor === `${pos.x},${pos.y},${pos.z}`).length + }, 0) + return { + centerFaces, + totalTiles, + centerTileNeighbors, + faces: sectionGeometry.tiles[`${pos.x},${pos.y},${pos.z}`]?.faces ?? [], + attr: sectionGeometry + } + } + + setBlockStatesData(blockStatesModels, blocksAtlasesJson, true, false, version) + const reload = () => { + mesherWorld.removeColumn(0, 0) + mesherWorld.addColumn(0, 0, chunk1.toJson()) + } + reload() + + const getLights = () => { + return Object.fromEntries(getGeometry().faces.map(({ face, light }) => ([face, (light ?? 0) * 15 - 2]))) + } + + const setLight = (x: number, y: number, z: number, val = 0) => { + // create columns first + chunk1.setBlockLight(pos.offset(x, y, z), 15) + chunk1.setSkyLight(pos.offset(x, y, z), 15) + chunk1.setBlockLight(pos.offset(x, y, z), val) + chunk1.setSkyLight(pos.offset(x, y, z), 0) + } + + return { + mesherWorld, + setLight, + getLights, + getGeometry, + pos, + mcData, + reload, + chunk: chunk1 as PCChunk + } +} + +// surround it +const addPositions = [ + // [[0, 0, 0], 'diamond_block'], + [[1, 0, 0], 'stone'], + [[-1, 0, 0], 'stone'], + [[0, 1, 0], 'stone'], + [[0, -1, 0], 'stone'], + [[0, 0, 1], 'stone'], + [[0, 0, -1], 'stone'], +] diff --git a/renderer/viewer/lib/mesher/test/playground.ts b/renderer/viewer/lib/mesher/test/playground.ts new file mode 100644 index 00000000..0441dd60 --- /dev/null +++ b/renderer/viewer/lib/mesher/test/playground.ts @@ -0,0 +1,20 @@ +import { BlockNames } from '../../../../../src/mcDataTypes' +import { setup } from './mesherTester' + +const addPositions = [ + // [[0, 0, 0], 'diamond_block'], + [[1, 0, 0], 'stone'], + [[-1, 0, 0], 'stone'], + [[0, 1, 0], 'stone'], + [[0, -1, 0], 'stone'], + [[0, 0, 1], 'stone'], + [[0, 0, -1], 'stone'], +] as const + +const { mesherWorld, getGeometry, pos, mcData } = setup('1.21.1', addPositions as any) + +// mesherWorld.setBlockStateId(pos, 712) +// mesherWorld.setBlockStateId(pos, mcData.blocksByName.stone_slab.defaultState) +mesherWorld.setBlockStateId(pos, 11_225) + +console.log(getGeometry().centerTileNeighbors) diff --git a/renderer/viewer/lib/mesher/test/tests.test.ts b/renderer/viewer/lib/mesher/test/tests.test.ts new file mode 100644 index 00000000..2c3dc6a5 --- /dev/null +++ b/renderer/viewer/lib/mesher/test/tests.test.ts @@ -0,0 +1,56 @@ +import { test, expect } from 'vitest' +import supportedVersions from '../../../../../src/supportedVersions.mjs' +import { INVISIBLE_BLOCKS } from '../worldConstants' +import { setup } from './mesherTester' + +const lastVersion = supportedVersions.at(-1) + +const addPositions = [ + // [[0, 0, 0], 'diamond_block'], + // [[1, 0, 0], 'stone'], + // [[-1, 0, 0], 'stone'], + // [[0, 1, 0], 'stone'], + // [[0, -1, 0], 'stone'], + // [[0, 0, 1], 'stone'], + // [[0, 0, -1], 'stone'], +] as const + +test('Known blocks are not rendered', () => { + const { mesherWorld, getGeometry, pos, mcData } = setup(lastVersion, addPositions as any) + const ignoreAsExpected = new Set([...INVISIBLE_BLOCKS, 'water', 'lava']) + + let time = 0 + let times = 0 + const missingBlocks = {}/* as {[number, number]} */ + const erroredBlocks = {}/* as {[number, number]} */ + for (const block of mcData.blocksArray) { + if (ignoreAsExpected.has(block.name)) continue + // if (block.maxStateId! - block.minStateId! > 100) continue + // for (let i = block.minStateId!; i <= block.maxStateId!; i++) { + for (let i = block.defaultState; i <= block.defaultState; i++) { + // if (block.transparent) continue + mesherWorld.setBlockStateId(pos, i) + const start = performance.now() + const { centerFaces, totalTiles, centerTileNeighbors, attr } = getGeometry() + time += performance.now() - start + times++ + if (centerFaces === 0) { + const objAdd = attr.hadErrors ? erroredBlocks : missingBlocks + if (objAdd[block.name]) continue + objAdd[block.name] = true + // invalidBlocks[block.name] = [i - block.defaultState!, centerTileNeighbors] + // console.log('INVALID', block.name, centerTileNeighbors, i - block.minStateId) + } + } + } + console.log('Checking blocks of version', lastVersion) + console.log('Average time', time / times) + // should be fixed, but to avoid regressions & for visibility + // TODO resolve creaking_heart issue (1.21.3) + expect(missingBlocks).toMatchInlineSnapshot(` + { + "structure_void": true, + } + `) + expect(erroredBlocks).toMatchInlineSnapshot('{}') +}) diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts new file mode 100644 index 00000000..f2757ae6 --- /dev/null +++ b/renderer/viewer/lib/mesher/world.ts @@ -0,0 +1,270 @@ +import Chunks from 'prismarine-chunk' +import mcData from 'minecraft-data' +import { Block } from 'prismarine-block' +import { Vec3 } from 'vec3' +import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' +import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json' +import legacyJson from '../../../../src/preflatMap.json' +import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey } from './shared' +import { INVISIBLE_BLOCKS } from './worldConstants' + +const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions) + +function columnKey (x, z) { + return `${x},${z}` +} + +function isCube (shapes) { + if (!shapes || shapes.length !== 1) return false + const shape = shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 +} + +export type BlockModelPartsResolved = ReturnType + +export type WorldBlock = Omit & { + // todo + isCube: boolean + /** cache */ + models?: BlockModelPartsResolved | null + _originalProperties?: Record + _properties?: Record +} + +export class World { + config = defaultMesherConfig + Chunk: typeof import('prismarine-chunk/types/index').PCChunk + columns = {} as { [key: string]: import('prismarine-chunk/types/index').PCChunk } + blockCache = {} + biomeCache: { [id: number]: mcData.Biome } + preflat: boolean + erroredBlockModel?: BlockModelPartsResolved + customBlockModels = new Map() // chunkKey -> blockModels + sentBlockStateModels = new Set() + blockStateModelInfo = new Map() + + constructor (version) { + this.Chunk = Chunks(version) as any + this.biomeCache = mcData(version).biomes + this.preflat = !mcData(version).supportFeature('blockStateId') + this.config.version = version + } + + getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') { + // for easier testing + if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number]) + const { enableLighting, skyLight } = this.config + if (!enableLighting) return 15 + // const key = `${pos.x},${pos.y},${pos.z}` + // if (lightsCache.has(key)) return lightsCache.get(key) + const column = this.getColumnByPos(pos) + if (!column || !hasChunkSection(column, pos)) return 15 + let result = Math.min( + 15, + Math.max( + column.getBlockLight(posInChunk(pos)), + Math.min(skyLight, column.getSkyLight(posInChunk(pos))) + ) + 2 + ) + // lightsCache.set(key, result) + if (result === 2 && [this.getBlock(pos)?.name ?? '', curBlockName].some(x => /_stairs|slab|glass_pane/.exec(x)) && !skipMoreChecks) { // todo this is obviously wrong + const lights = [ + this.getLight(pos.offset(0, 1, 0), undefined, true), + this.getLight(pos.offset(0, -1, 0), undefined, true), + this.getLight(pos.offset(0, 0, 1), undefined, true), + this.getLight(pos.offset(0, 0, -1), undefined, true), + this.getLight(pos.offset(1, 0, 0), undefined, true), + this.getLight(pos.offset(-1, 0, 0), undefined, true) + ].filter(x => x !== 2) + if (lights.length) { + const min = Math.min(...lights) + result = min + } + } + if (isNeighbor && result === 2) result = 15 // TODO + return result + } + + addColumn (x, z, json) { + const chunk = this.Chunk.fromJson(json) + this.columns[columnKey(x, z)] = chunk as any + return chunk + } + + removeColumn (x, z) { + delete this.columns[columnKey(x, z)] + } + + getColumn (x, z) { + return this.columns[columnKey(x, z)] + } + + setBlockStateId (pos: Vec3, stateId) { + if (stateId === undefined) throw new Error('stateId is undefined') + const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) + + const column = this.columns[key] + // null column means chunk not loaded + if (!column) return false + + column.setBlockStateId(posInChunk(pos.floored()), stateId) + + return true + } + + getColumnByPos (pos: Vec3) { + return this.getColumn(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) + } + + getBlock (pos: Vec3, blockProvider?: WorldBlockProvider, attr?: { hadErrors?: boolean }): WorldBlock | null { + // for easier testing + if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number]) + const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) + const blockPosKey = `${pos.x},${pos.y},${pos.z}` + const modelOverride = this.customBlockModels.get(key)?.[blockPosKey] + + const column = this.columns[key] + // null column means chunk not loaded + if (!column) return null + + const loc = pos.floored() + const locInChunk = posInChunk(loc) + const stateId = column.getBlockStateId(locInChunk) + + const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride) + + if (!this.blockCache[cacheKey]) { + const b = column.getBlock(locInChunk) as unknown as WorldBlock + if (modelOverride) { + b.name = modelOverride + } + b.isCube = isCube(b.shapes) + this.blockCache[cacheKey] = b + Object.defineProperty(b, 'position', { + get () { + throw new Error('position is not reliable, use pos parameter instead of block.position') + } + }) + if (this.preflat) { + b._properties = {} + + const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, pos) + if (namePropsStr) { + b.name = namePropsStr.split('[')[0] + const propsStr = namePropsStr.split('[')?.[1]?.split(']') + if (propsStr) { + const newProperties = Object.fromEntries(propsStr.join('').split(',').map(x => { + let [key, val] = x.split('=') + if (!isNaN(val)) val = parseInt(val, 10) + return [key, val] + })) + b._properties = newProperties + } + } + } + } + + const block: WorldBlock = this.blockCache[cacheKey] + + if (block.models === undefined && blockProvider) { + if (!attr) throw new Error('attr is required') + const props = block.getProperties() + + try { + // fixme + if (this.preflat) { + if (block.name === 'cobblestone_wall') { + props.up = 'true' + for (const key of ['north', 'south', 'east', 'west']) { + const val = props[key] + if (val === 'false' || val === 'true') { + props[key] = val === 'true' ? 'low' : 'none' + } + } + } + } + + const useFallbackModel = !!(this.preflat || modelOverride) + const issues = [] as string[] + const resolvedModelNames = [] as string[] + const resolvedConditions = [] as string[] + block.models = blockProvider.getAllResolvedModels0_1( + { + name: block.name, + properties: props, + }, + useFallbackModel, + issues, + resolvedModelNames, + resolvedConditions + )! + + // Track block state model info + if (!this.sentBlockStateModels.has(cacheKey)) { + this.blockStateModelInfo.set(cacheKey, { + cacheKey, + issues, + modelNames: resolvedModelNames, + conditions: resolvedConditions + }) + } + + if (!block.models.length) { + if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) { + console.debug('[mesher] block to render not found', block.name, props) + } + block.models = null + } + + if (block.models && modelOverride) { + const model = block.models[0] + block.transparent = model[0]?.['transparent'] ?? block.transparent + } + } catch (err) { + this.erroredBlockModel ??= blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} }) + block.models ??= this.erroredBlockModel + console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(props)}]: ` + err.message, err.stack) + attr.hadErrors = true + } + } + + if (block.name === 'flowing_water') block.name = 'water' + if (block.name === 'flowing_lava') block.name = 'lava' + if (block.name === 'bubble_column') block.name = 'water' // TODO need to distinguish between water and bubble column + // block.position = loc // it overrides position of all currently loaded blocks + //@ts-expect-error + block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0] + if (block.name === 'redstone_ore') block.transparent = false + return block + } + + shouldMakeAo (block: WorldBlock | null) { + return block?.isCube && !ignoreAoBlocks.includes(block.name) + } +} + +const findClosestLegacyBlockFallback = (id, metadata, pos) => { + console.warn(`[mesher] Unknown block with ${id}:${metadata} at ${pos}, falling back`) // todo has known issues + for (const [key, value] of Object.entries(legacyJson.blocks)) { + const [idKey, meta] = key.split(':') + if (idKey === id) return value + } + return null +} + +// todo export in chunk instead +const hasChunkSection = (column, pos) => { + if (column._getSection) return column._getSection(pos) + if (column.skyLightSections) { + return column.skyLightSections[getLightSectionIndex(pos, column.minY)] || column.blockLightSections[getLightSectionIndex(pos, column.minY)] + } + if (column.sections) return column.sections[pos.y >> 4] +} + +function posInChunk (pos) { + return new Vec3(Math.floor(pos.x) & 15, Math.floor(pos.y), Math.floor(pos.z) & 15) +} + +function getLightSectionIndex (pos, minY = 0) { + return Math.floor((pos.y - minY) / 16) + 1 +} diff --git a/renderer/viewer/lib/mesher/worldConstants.ts b/renderer/viewer/lib/mesher/worldConstants.ts new file mode 100644 index 00000000..6aa0e0fc --- /dev/null +++ b/renderer/viewer/lib/mesher/worldConstants.ts @@ -0,0 +1 @@ +export const INVISIBLE_BLOCKS = new Set(['air', 'void_air', 'cave_air', 'barrier', 'light', 'moving_piston']) diff --git a/renderer/viewer/lib/mesherlogReader.ts b/renderer/viewer/lib/mesherlogReader.ts new file mode 100644 index 00000000..0f1e74c0 --- /dev/null +++ b/renderer/viewer/lib/mesherlogReader.ts @@ -0,0 +1,131 @@ +/* eslint-disable no-await-in-loop */ +import { Vec3 } from 'vec3' + +// import log from '../../../../../Downloads/mesher (2).log' +import { WorldRendererCommon } from './worldrendererCommon' +const log = '' + + +export class MesherLogReader { + chunksToReceive: Array<{ + x: number + z: number + chunkLength: number + }> = [] + messagesQueue: Array<{ + fromWorker: boolean + workerIndex: number + message: any + }> = [] + + sectionFinishedToReceive = null as { + messagesLeft: string[] + resolve: () => void + } | null + replayStarted = false + + constructor (private readonly worldRenderer: WorldRendererCommon) { + this.parseMesherLog() + } + + chunkReceived (x: number, z: number, chunkLength: number) { + // remove existing chunks with same x and z + const existingChunkIndex = this.chunksToReceive.findIndex(chunk => chunk.x === x && chunk.z === z) + if (existingChunkIndex === -1) { + // console.error('Chunk not found', x, z) + } else { + // warn if chunkLength is different + if (this.chunksToReceive[existingChunkIndex].chunkLength !== chunkLength) { + // console.warn('Chunk length mismatch', x, z, this.chunksToReceive[existingChunkIndex].chunkLength, chunkLength) + } + // remove chunk + this.chunksToReceive = this.chunksToReceive.filter((chunk, index) => chunk.x !== x || chunk.z !== z) + } + this.maybeStartReplay() + } + + async maybeStartReplay () { + if (this.chunksToReceive.length !== 0 || this.replayStarted) return + const lines = log.split('\n') + console.log('starting replay') + this.replayStarted = true + const waitForWorkersMessages = async () => { + if (!this.sectionFinishedToReceive) return + await new Promise(resolve => { + this.sectionFinishedToReceive!.resolve = resolve + }) + } + + for (const line of lines) { + if (line.includes('dispatchMessages dirty')) { + await waitForWorkersMessages() + this.worldRenderer.stopMesherMessagesProcessing = true + const message = JSON.parse(line.slice(line.indexOf('{'), line.lastIndexOf('}') + 1)) + if (!message.value) continue + const index = line.split(' ')[1] + const type = line.split(' ')[3] + // console.log('sending message', message.x, message.y, message.z) + this.worldRenderer.forceCallFromMesherReplayer = true + this.worldRenderer.setSectionDirty(new Vec3(message.x, message.y, message.z), message.value) + this.worldRenderer.forceCallFromMesherReplayer = false + } + if (line.includes('-> blockUpdate')) { + await waitForWorkersMessages() + this.worldRenderer.stopMesherMessagesProcessing = true + const message = JSON.parse(line.slice(line.indexOf('{'), line.lastIndexOf('}') + 1)) + this.worldRenderer.forceCallFromMesherReplayer = true + this.worldRenderer.setBlockStateIdInner(new Vec3(message.pos.x, message.pos.y, message.pos.z), message.stateId) + this.worldRenderer.forceCallFromMesherReplayer = false + } + + if (line.includes(' sectionFinished ')) { + if (!this.sectionFinishedToReceive) { + console.log('starting worker message processing validating') + this.worldRenderer.stopMesherMessagesProcessing = false + this.sectionFinishedToReceive = { + messagesLeft: [], + resolve: () => { + this.sectionFinishedToReceive = null + } + } + } + const parts = line.split(' ') + const coordsPart = parts.find(part => part.split(',').length === 3) + if (!coordsPart) throw new Error(`no coords part found ${line}`) + const [x, y, z] = coordsPart.split(',').map(Number) + this.sectionFinishedToReceive.messagesLeft.push(`${x},${y},${z}`) + } + } + } + + workerMessageReceived (type: string, message: any) { + if (type === 'sectionFinished') { + const { key } = message + if (!this.sectionFinishedToReceive) { + console.warn(`received sectionFinished message but no sectionFinishedToReceive ${key}`) + return + } + + const idx = this.sectionFinishedToReceive.messagesLeft.indexOf(key) + if (idx === -1) { + console.warn(`received sectionFinished message for non-outstanding section ${key}`) + return + } + this.sectionFinishedToReceive.messagesLeft.splice(idx, 1) + if (this.sectionFinishedToReceive.messagesLeft.length === 0) { + this.sectionFinishedToReceive.resolve() + } + } + } + + parseMesherLog () { + const lines = log.split('\n') + for (const line of lines) { + if (line.startsWith('-> chunk')) { + const chunk = JSON.parse(line.slice('-> chunk'.length)) + this.chunksToReceive.push(chunk) + continue + } + } + } +} diff --git a/prismarine-viewer/viewer/lib/moreBlockDataGenerated.json b/renderer/viewer/lib/moreBlockDataGenerated.json similarity index 100% rename from prismarine-viewer/viewer/lib/moreBlockDataGenerated.json rename to renderer/viewer/lib/moreBlockDataGenerated.json diff --git a/renderer/viewer/lib/simpleUtils.ts b/renderer/viewer/lib/simpleUtils.ts new file mode 100644 index 00000000..2d0b6255 --- /dev/null +++ b/renderer/viewer/lib/simpleUtils.ts @@ -0,0 +1,35 @@ +export async function getBufferFromStream (stream) { + return new Promise((resolve, reject) => { + let buffer = Buffer.from([]) + stream.on('data', buf => { + buffer = Buffer.concat([buffer, buf]) + }) + stream.on('end', () => resolve(buffer)) + stream.on('error', reject) + }) +} + +export function openURL (url, newTab = true) { + if (newTab) { + window.open(url, '_blank', 'noopener,noreferrer') + } else { + window.open(url, '_self') + } +} + +export const isMobile = () => { + return window.matchMedia('(pointer: coarse)').matches || navigator.userAgent.includes('Mobile') +} + +export function chunkPos (pos: { x: number, z: number }) { + const x = Math.floor(pos.x / 16) + const z = Math.floor(pos.z / 16) + return [x, z] +} + +export function sectionPos (pos: { x: number, y: number, z: number }) { + const x = Math.floor(pos.x / 16) + const y = Math.floor(pos.y / 16) + const z = Math.floor(pos.z / 16) + return [x, y, z] +} diff --git a/renderer/viewer/lib/smoothSwitcher.ts b/renderer/viewer/lib/smoothSwitcher.ts new file mode 100644 index 00000000..74eb1171 --- /dev/null +++ b/renderer/viewer/lib/smoothSwitcher.ts @@ -0,0 +1,168 @@ +import * as tweenJs from '@tweenjs/tween.js' +import { AnimationController } from './animationController' + +export type StateProperties = Record +export type StateGetterFn = () => StateProperties +export type StateSetterFn = (property: string, value: number) => void + +// Speed in units per second for each property type +const DEFAULT_SPEEDS = { + x: 3000, // pixels/units per second + y: 3000, + z: 3000, + rotation: Math.PI, // radians per second + scale: 1, // scale units per second + default: 3000 // default speed for unknown properties +} + +export class SmoothSwitcher { + private readonly animationController = new AnimationController() + // private readonly currentState: StateProperties = {} + private readonly defaultState: StateProperties + private readonly speeds: Record + public currentStateName = '' + public transitioningToStateName = '' + + constructor ( + public getState: StateGetterFn, + public setState: StateSetterFn, + speeds?: Partial> + ) { + + // Initialize speeds with defaults and overrides + this.speeds = { ...DEFAULT_SPEEDS } + if (speeds) { + Object.assign(this.speeds, speeds) + } + + // Store initial values + this.defaultState = this.getState() + } + + /** + * Calculate transition duration based on the largest property change + */ + private calculateDuration (newState: Partial): number { + let maxDuration = 0 + const currentState = this.getState() + + for (const [key, targetValue] of Object.entries(newState)) { + const currentValue = currentState[key] + const diff = Math.abs(targetValue! - currentValue) + const speed = this.getPropertySpeed(key) + const duration = (diff / speed) * 1000 // Convert to milliseconds + + maxDuration = Math.max(maxDuration, duration) + } + + // Ensure minimum duration of 50ms and maximum of 2000ms + return Math.min(Math.max(maxDuration, 200), 2000) + } + + private getPropertySpeed (property: string): number { + // Check for specific property speed + if (property in this.speeds) { + return this.speeds[property] + } + + // Check for property type (rotation, scale, etc.) + if (property.toLowerCase().includes('rotation')) return this.speeds.rotation + if (property.toLowerCase().includes('scale')) return this.speeds.scale + if (property.toLowerCase() === 'x' || property.toLowerCase() === 'y' || property.toLowerCase() === 'z') { + return this.speeds[property] + } + + return this.speeds.default + } + + /** + * Start a transition to a new state + * @param newState Partial state - only need to specify properties that change + * @param easing Easing function to use + */ + startTransition ( + newState: Partial, + stateName?: string, + onEnd?: () => void, + easing: (amount: number) => number = tweenJs.Easing.Linear.None, + onCancelled?: () => void + ): void { + if (this.isTransitioning) { + this.animationController.forceFinish(false) + } + + this.transitioningToStateName = stateName ?? '' + const state = this.getState() + + const duration = this.calculateDuration(newState) + // console.log('duration', duration, JSON.stringify(state), JSON.stringify(newState)) + + void this.animationController.startAnimation(() => { + const group = new tweenJs.Group() + new tweenJs.Tween(state, group) + .to(newState, duration) + .easing(easing) + .onUpdate((obj) => { + for (const key of Object.keys(obj)) { + this.setState(key, obj[key]) + } + }) + .onComplete(() => { + this.animationController.forceFinish() + this.currentStateName = this.transitioningToStateName + this.transitioningToStateName = '' + onEnd?.() + }) + .start() + return group + }, onCancelled) + } + + /** + * Reset to default state + */ + reset (): void { + this.startTransition(this.defaultState) + } + + + /** + * Update the animation (should be called in your render/update loop) + */ + update (): void { + this.animationController.update() + } + + /** + * Force finish the current transition + */ + forceFinish (): void { + this.animationController.forceFinish() + } + + /** + * Start a new transition to the specified state + */ + transitionTo ( + newState: Partial, + stateName?: string, + onEnd?: () => void, + onCancelled?: () => void + ): void { + this.startTransition(newState, stateName, onEnd, tweenJs.Easing.Linear.None, onCancelled) + } + + /** + * Get the current value of a property + */ + getCurrentValue (property: string): number { + return this.getState()[property] + } + + /** + * Check if currently transitioning + */ + get isTransitioning (): boolean { + return this.animationController.isActive + } +} diff --git a/renderer/viewer/lib/ui/newStats.ts b/renderer/viewer/lib/ui/newStats.ts new file mode 100644 index 00000000..4a1b0a0f --- /dev/null +++ b/renderer/viewer/lib/ui/newStats.ts @@ -0,0 +1,112 @@ +/* eslint-disable unicorn/prefer-dom-node-text-content */ +const rightOffset = 0 + +const stats = {} + +let lastY = 40 +export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) => { + const pane = document.createElement('div') + pane.style.position = 'fixed' + pane.style.top = `${y ?? lastY}px` + pane.style.right = `${x}px` + // gray bg + pane.style.backgroundColor = 'rgba(0, 0, 0, 0.7)' + pane.style.color = 'white' + pane.style.padding = '2px' + pane.style.fontFamily = 'monospace' + pane.style.fontSize = '12px' + pane.style.zIndex = '100' + pane.style.pointerEvents = 'none' + document.body.appendChild(pane) + stats[id] = pane + if (y === undefined && x === rightOffset) { // otherwise it's a custom position + // rightOffset += width + lastY += 20 + } + + return { + updateText (text: string) { + if (pane.innerText === text) return + pane.innerText = text + }, + setVisibility (visible: boolean) { + pane.style.display = visible ? 'block' : 'none' + } + } +} + +export const addNewStat2 = (id: string, { top, bottom, right, left, displayOnlyWhenWider }: { top?: number, bottom?: number, right?: number, left?: number, displayOnlyWhenWider?: number }) => { + if (top === undefined && bottom === undefined) top = 0 + const pane = document.createElement('div') + pane.style.position = 'fixed' + if (top !== undefined) { + pane.style.top = `${top}px` + } + if (bottom !== undefined) { + pane.style.bottom = `${bottom}px` + } + if (left !== undefined) { + pane.style.left = `${left}px` + } + if (right !== undefined) { + pane.style.right = `${right}px` + } + // gray bg + pane.style.backgroundColor = 'rgba(0, 0, 0, 0.7)' + pane.style.color = 'white' + pane.style.padding = '2px' + pane.style.fontFamily = 'monospace' + pane.style.fontSize = '12px' + pane.style.zIndex = '10000' + pane.style.pointerEvents = 'none' + document.body.appendChild(pane) + stats[id] = pane + + const resizeCheck = () => { + if (!displayOnlyWhenWider) return + pane.style.display = window.innerWidth > displayOnlyWhenWider ? 'block' : 'none' + } + window.addEventListener('resize', resizeCheck) + resizeCheck() + + return { + updateText (text: string) { + pane.innerText = text + }, + setVisibility (visible: boolean) { + pane.style.display = visible ? 'block' : 'none' + } + } +} + +export const updateStatText = (id, text) => { + if (!stats[id]) return + stats[id].innerText = text +} + +export const updatePanesVisibility = (visible: boolean) => { + // eslint-disable-next-line guard-for-in + for (const id in stats) { + stats[id].style.display = visible ? 'block' : 'none' + } +} + +export const removeAllStats = () => { + // eslint-disable-next-line guard-for-in + for (const id in stats) { + removeStat(id) + } +} + +export const removeStat = (id) => { + if (!stats[id]) return + stats[id].remove() + delete stats[id] +} + +if (typeof customEvents !== 'undefined') { + customEvents.on('gameLoaded', () => { + const chunksLoaded = addNewStat('chunks-loaded', 80, 0, 0) + const chunksTotal = addNewStat('chunks-read', 80, 0, 0) + }) +} diff --git a/renderer/viewer/lib/utils.ts b/renderer/viewer/lib/utils.ts new file mode 100644 index 00000000..f471aa9d --- /dev/null +++ b/renderer/viewer/lib/utils.ts @@ -0,0 +1,57 @@ +export const loadScript = async function (scriptSrc: string, highPriority = true): Promise { + const existingScript = document.querySelector(`script[src="${scriptSrc}"]`) + if (existingScript) { + return existingScript + } + + return new Promise((resolve, reject) => { + const scriptElement = document.createElement('script') + scriptElement.src = scriptSrc + + if (highPriority) { + scriptElement.fetchPriority = 'high' + } + scriptElement.async = true + + scriptElement.addEventListener('load', () => { + resolve(scriptElement) + }) + + scriptElement.onerror = (error) => { + reject(new Error(typeof error === 'string' ? error : (error as any).message)) + scriptElement.remove() + } + + document.head.appendChild(scriptElement) + }) +} + +const detectFullOffscreenCanvasSupport = () => { + if (typeof OffscreenCanvas === 'undefined') return false + try { + const canvas = new OffscreenCanvas(1, 1) + // Try to get a WebGL context - this will fail on iOS where only 2D is supported (iOS 16) + const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') + return gl !== null + } catch (e) { + return false + } +} + +const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport() + +export const createCanvas = (width: number, height: number): OffscreenCanvas => { + if (hasFullOffscreenCanvasSupport) { + return new OffscreenCanvas(width, height) + } + const canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + return canvas as unknown as OffscreenCanvas // todo-low +} + +export async function loadImageFromUrl (imageUrl: string): Promise { + const response = await fetch(imageUrl) + const blob = await response.blob() + return createImageBitmap(blob) +} diff --git a/renderer/viewer/lib/utils/proxy.ts b/renderer/viewer/lib/utils/proxy.ts new file mode 100644 index 00000000..d30ceb7e --- /dev/null +++ b/renderer/viewer/lib/utils/proxy.ts @@ -0,0 +1,23 @@ +import { subscribeKey } from 'valtio/utils' + +// eslint-disable-next-line max-params +export function watchProperty, K> (asyncGetter: (value: T[keyof T]) => Promise, valtioProxy: T, key: keyof T, readySetter: (res: K) => void, cleanup?: (res: K) => void) { + let i = 0 + let lastRes: K | undefined + const request = async () => { + const req = ++i + const res = await asyncGetter(valtioProxy[key]) + if (req === i) { + if (lastRes) { + cleanup?.(lastRes) + } + readySetter(res) + lastRes = res + } else { + // rejected + cleanup?.(res) + } + } + void request() + return subscribeKey(valtioProxy, key, request) +} diff --git a/renderer/viewer/lib/utils/skins.ts b/renderer/viewer/lib/utils/skins.ts new file mode 100644 index 00000000..3163702c --- /dev/null +++ b/renderer/viewer/lib/utils/skins.ts @@ -0,0 +1,59 @@ +import { loadSkinToCanvas } from 'skinview-utils' +import { createCanvas, loadImageFromUrl } from '../utils' + +export { default as stevePngUrl } from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png' + +const config = { + apiEnabled: true, +} + +export const setSkinsConfig = (newConfig: Partial) => { + Object.assign(config, newConfig) +} + +export async function loadSkinFromUsername (username: string, type: 'skin' | 'cape'): Promise { + if (!config.apiEnabled) return + + if (type === 'cape') return + const url = `https://playerdb.co/api/player/minecraft/${username}` + const response = await fetch(url) + if (!response.ok) return + + const data: { + data: { + player: { + skin_texture: string + } + } + } = await response.json() + return data.data.player.skin_texture +} + +export const parseSkinTexturesValue = (value: string) => { + const decodedData: { + textures: { + SKIN: { + url: string + } + } + } = JSON.parse(Buffer.from(value, 'base64').toString()) + return decodedData.textures?.SKIN?.url +} + +export async function loadSkinImage (skinUrl: string): Promise<{ canvas: OffscreenCanvas, image: ImageBitmap }> { + if (!skinUrl.startsWith('data:')) { + skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://')) + } + + const image = await loadImageFromUrl(skinUrl) + const skinCanvas = createCanvas(64, 64) + loadSkinToCanvas(skinCanvas, image) + return { canvas: skinCanvas, image } +} + +const fetchAndConvertBase64Skin = async (skinUrl: string) => { + const response = await fetch(skinUrl, { }) + const arrayBuffer = await response.arrayBuffer() + const base64 = Buffer.from(arrayBuffer).toString('base64') + return `data:image/png;base64,${base64}` +} diff --git a/renderer/viewer/lib/workerProxy.ts b/renderer/viewer/lib/workerProxy.ts new file mode 100644 index 00000000..2b38dca9 --- /dev/null +++ b/renderer/viewer/lib/workerProxy.ts @@ -0,0 +1,92 @@ +import { proxy, getVersion, subscribe } from 'valtio' + +export function createWorkerProxy void | Promise>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { + const target = channel ?? globalThis + target.addEventListener('message', (event: any) => { + const { type, args, msgId } = event.data + if (handlers[type]) { + const result = handlers[type](...args) + if (result instanceof Promise) { + void result.then((result) => { + target.postMessage({ + type: 'result', + msgId, + args: [result] + }) + }) + } + } + }) + return null as any +} + +/** + * in main thread + * ```ts + * // either: + * import type { importedTypeWorkerProxy } from './worker' + * // or: + * type importedTypeWorkerProxy = import('./worker').importedTypeWorkerProxy + * + * const workerChannel = useWorkerProxy(worker) + * ``` + */ +export const useWorkerProxy = void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & { + transfer: (...args: Transferable[]) => T['__workerProxy'] +} => { + let messageId = 0 + // in main thread + return new Proxy({} as any, { + get (target, prop) { + if (prop === 'transfer') { + return (...transferable: Transferable[]) => { + return new Proxy({}, { + get (target, prop) { + return (...args: any[]) => { + worker.postMessage({ + type: prop, + args, + }, transferable) + } + } + }) + } + } + return (...args: any[]) => { + const msgId = messageId++ + const transfer = autoTransfer ? args.filter(arg => { + return arg instanceof ArrayBuffer || arg instanceof MessagePort + || (typeof ImageBitmap !== 'undefined' && arg instanceof ImageBitmap) + || (typeof OffscreenCanvas !== 'undefined' && arg instanceof OffscreenCanvas) + || (typeof ImageData !== 'undefined' && arg instanceof ImageData) + }) : [] + worker.postMessage({ + type: prop, + msgId, + args, + }, transfer) + return { + // eslint-disable-next-line unicorn/no-thenable + then (onfulfilled: (value: any) => void) { + const handler = ({ data }: MessageEvent): void => { + if (data.type === 'result' && data.msgId === msgId) { + onfulfilled(data.args[0]) + worker.removeEventListener('message', handler as EventListener) + } + } + worker.addEventListener('message', handler as EventListener) + } + } + } + } + }) +} + +// const workerProxy = createWorkerProxy({ +// startRender (canvas: HTMLCanvasElement) { +// }, +// }) + +// const worker = useWorkerProxy(null, workerProxy) + +// worker. diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts new file mode 100644 index 00000000..dfbdb35c --- /dev/null +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -0,0 +1,431 @@ +/* eslint-disable guard-for-in */ + +// todo refactor into its own commons module +import { EventEmitter } from 'events' +import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils' +import { Vec3 } from 'vec3' +import { BotEvents } from 'mineflayer' +import { proxy } from 'valtio' +import TypedEmitter from 'typed-emitter' +import { Biome } from 'minecraft-data' +import { delayedIterator } from '../../playground/shared' +import { chunkPos } from './simpleUtils' + +export type ChunkPosKey = string // like '16,16' +type ChunkPos = { x: number, z: number } // like { x: 16, z: 16 } + +export type WorldDataEmitterEvents = { + chunkPosUpdate: (data: { pos: Vec3 }) => void + blockUpdate: (data: { pos: Vec3, stateId: number }) => void + entity: (data: any) => void + entityMoved: (data: any) => void + playerEntity: (data: any) => void + time: (data: number) => void + renderDistance: (viewDistance: number) => void + blockEntities: (data: Record | { blockEntities: Record }) => void + markAsLoaded: (data: { x: number, z: number }) => void + unloadChunk: (data: { x: number, z: number }) => void + loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void + updateLight: (data: { pos: Vec3 }) => void + onWorldSwitch: () => void + end: () => void + biomeUpdate: (data: { biome: Biome }) => void + biomeReset: () => void +} + +export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter) { + static readonly restorerName = 'WorldDataEmitterWorker' +} + +export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter) { + spiralNumber = 0 + gotPanicLastTime = false + panicChunksReload = () => {} + loadedChunks: Record + private inLoading = false + private chunkReceiveTimes: number[] = [] + private lastChunkReceiveTime = 0 + public lastChunkReceiveTimeAvg = 0 + private panicTimeout?: NodeJS.Timeout + readonly lastPos: Vec3 + private eventListeners: Record = {} + private readonly emitter: WorldDataEmitter + debugChunksInfo: Record + // blockUpdates: number + }> = {} + + waitingSpiralChunksLoad = {} as Record void> + + addWaitTime = 1 + /* config */ keepChunksDistance = 0 + /* config */ isPlayground = false + /* config */ allowPositionUpdate = true + + constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { + // eslint-disable-next-line constructor-super + super() + this.loadedChunks = {} + this.lastPos = new Vec3(0, 0, 0).update(position) + // todo + this.emitter = this + } + + setBlockStateId (position: Vec3, stateId: number) { + const val = this.world.setBlockStateId(position, stateId) as Promise | void + if (val) throw new Error('setBlockStateId returned promise (not supported)') + // const chunkX = Math.floor(position.x / 16) + // const chunkZ = Math.floor(position.z / 16) + // if (!this.loadedChunks[`${chunkX},${chunkZ}`] && !this.waitingSpiralChunksLoad[`${chunkX},${chunkZ}`]) { + // void this.loadChunk({ x: chunkX, z: chunkZ }) + // return + // } + + this.emit('blockUpdate', { pos: position, stateId }) + } + + updateViewDistance (viewDistance: number) { + this.viewDistance = viewDistance + this.emitter.emit('renderDistance', viewDistance) + } + + listenToBot (bot: typeof __type_bot) { + const entitiesObjectData = new Map() + bot._client.prependListener('spawn_entity', (data) => { + if (data.objectData && data.entityId !== undefined) { + entitiesObjectData.set(data.entityId, data.objectData) + } + }) + + const emitEntity = (e, name = 'entity') => { + if (!e) return + if (e === bot.entity) { + if (name === 'entity') { + this.emitter.emit('playerEntity', e) + } + return + } + if (!e.name) return // mineflayer received update for not spawned entity + e.objectData = entitiesObjectData.get(e.id) + this.emitter.emit(name as any, { + ...e, + pos: e.position, + username: e.username, + team: bot.teamMap[e.username] || bot.teamMap[e.uuid], + // set debugTree (obj) { + // e.debugTree = obj + // } + }) + } + + this.eventListeners = { + // 'move': botPosition, + entitySpawn (e: any) { + if (e.name === 'item_frame' || e.name === 'glow_item_frame') { + // Item frames use block positions in the protocol, not their center. Fix that. + e.position.translate(0.5, 0.5, 0.5) + } + emitEntity(e) + }, + entityUpdate (e: any) { + emitEntity(e) + }, + entityEquip (e: any) { + emitEntity(e) + }, + entityMoved (e: any) { + emitEntity(e, 'entityMoved') + }, + entityGone: (e: any) => { + this.emitter.emit('entity', { id: e.id, delete: true }) + }, + chunkColumnLoad: (pos: Vec3) => { + const now = performance.now() + if (this.lastChunkReceiveTime) { + this.chunkReceiveTimes.push(now - this.lastChunkReceiveTime) + } + this.lastChunkReceiveTime = now + + if (this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]) { + this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`](true) + delete this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] + } else if (this.loadedChunks[`${pos.x},${pos.z}`]) { + void this.loadChunk(pos, false, 'Received another chunkColumnLoad event while already loaded') + } + this.chunkProgress() + }, + chunkColumnUnload: (pos: Vec3) => { + this.unloadChunk(pos) + }, + blockUpdate: (oldBlock: any, newBlock: any) => { + const stateId = newBlock.stateId ?? ((newBlock.type << 4) | newBlock.metadata) + this.emitter.emit('blockUpdate', { pos: oldBlock.position, stateId }) + }, + time: () => { + this.emitter.emit('time', bot.time.timeOfDay) + }, + end: () => { + this.emitter.emit('end') + }, + // when dimension might change + login: () => { + void this.updatePosition(bot.entity.position, true) + this.emitter.emit('playerEntity', bot.entity) + }, + respawn: () => { + void this.updatePosition(bot.entity.position, true) + this.emitter.emit('playerEntity', bot.entity) + this.emitter.emit('onWorldSwitch') + }, + } satisfies Partial + + + bot._client.on('update_light', ({ chunkX, chunkZ }) => { + const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16) + if (!this.waitingSpiralChunksLoad[`${chunkX},${chunkZ}`] && this.loadedChunks[`${chunkX},${chunkZ}`]) { + void this.loadChunk(chunkPos, true, 'update_light') + } + }) + + for (const [evt, listener] of Object.entries(this.eventListeners)) { + bot.on(evt as any, listener) + } + + for (const id in bot.entities) { + const e = bot.entities[id] + try { + emitEntity(e) + } catch (err) { + // reportError?.(err) + console.error('error processing entity', err) + } + } + } + + emitterGotConnected () { + this.emitter.emit('blockEntities', new Proxy({}, { + get (_target, posKey, receiver) { + if (typeof posKey !== 'string') return + const [x, y, z] = posKey.split(',').map(Number) + return bot.world.getBlock(new Vec3(x, y, z))?.entity + }, + })) + } + + removeListenersFromBot (bot: import('mineflayer').Bot) { + for (const [evt, listener] of Object.entries(this.eventListeners)) { + bot.removeListener(evt as any, listener) + } + } + + async init (pos: Vec3) { + this.updateViewDistance(this.viewDistance) + this.emitter.emit('chunkPosUpdate', { pos }) + if (bot?.time?.timeOfDay) { + this.emitter.emit('time', bot.time.timeOfDay) + } + if (bot?.entity) { + this.emitter.emit('playerEntity', bot.entity) + } + this.emitterGotConnected() + const [botX, botZ] = chunkPos(pos) + + const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) + + this.lastPos.update(pos) + await this._loadChunks(positions, pos) + } + + chunkProgress () { + if (this.panicTimeout) clearTimeout(this.panicTimeout) + if (this.chunkReceiveTimes.length >= 5) { + const avgReceiveTime = this.chunkReceiveTimes.reduce((a, b) => a + b, 0) / this.chunkReceiveTimes.length + this.lastChunkReceiveTimeAvg = avgReceiveTime + const timeoutDelay = avgReceiveTime * 2 + 1000 // 2x average + 1 second + + // Clear any existing timeout + if (this.panicTimeout) clearTimeout(this.panicTimeout) + + // Set new timeout for panic reload + this.panicTimeout = setTimeout(() => { + if (!this.gotPanicLastTime && this.inLoading) { + console.warn('Chunk loading seems stuck, triggering panic reload') + this.gotPanicLastTime = true + this.panicChunksReload() + } + }, timeoutDelay) + } + } + + async _loadChunks (positions: Vec3[], centerPos: Vec3) { + this.spiralNumber++ + const { spiralNumber } = this + // stop loading previous chunks + for (const pos of Object.keys(this.waitingSpiralChunksLoad)) { + this.waitingSpiralChunksLoad[pos](false) + delete this.waitingSpiralChunksLoad[pos] + } + + let continueLoading = true + this.inLoading = true + await delayedIterator(positions, this.addWaitTime, async (pos) => { + if (!continueLoading || this.loadedChunks[`${pos.x},${pos.z}`]) return + + // Wait for chunk to be available from server + if (!this.world.getColumnAt(pos)) { + continueLoading = await new Promise(resolve => { + this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve + }) + } + if (!continueLoading) return + await this.loadChunk(pos, undefined, `spiral ${spiralNumber} from ${centerPos.x},${centerPos.z}`) + this.chunkProgress() + }) + if (this.panicTimeout) clearTimeout(this.panicTimeout) + this.inLoading = false + this.gotPanicLastTime = false + this.chunkReceiveTimes = [] + this.lastChunkReceiveTime = 0 + } + + readdDebug () { + const clonedLoadedChunks = { ...this.loadedChunks } + this.unloadAllChunks() + console.time('readdDebug') + for (const loadedChunk in clonedLoadedChunks) { + const [x, z] = loadedChunk.split(',').map(Number) + void this.loadChunk(new Vec3(x, 0, z)) + } + const interval = setInterval(() => { + if (appViewer.rendererState.world.allChunksLoaded) { + clearInterval(interval) + console.timeEnd('readdDebug') + } + }, 100) + } + + // debugGotChunkLatency = [] as number[] + // lastTime = 0 + + async loadChunk (pos: ChunkPos, isLightUpdate = false, reason = 'spiral') { + const [botX, botZ] = chunkPos(this.lastPos) + + const dx = Math.abs(botX - Math.floor(pos.x / 16)) + const dz = Math.abs(botZ - Math.floor(pos.z / 16)) + if (dx <= this.viewDistance && dz <= this.viewDistance) { + // eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed + const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z)) + if (column) { + // const latency = Math.floor(performance.now() - this.lastTime) + // this.debugGotChunkLatency.push(latency) + // this.lastTime = performance.now() + // todo optimize toJson data, make it clear why it is used + const chunk = column.toJson() + // TODO: blockEntities + const worldConfig = { + minY: column['minY'] ?? 0, + worldHeight: column['worldHeight'] ?? 256, + } + //@ts-expect-error + this.emitter.emit('loadChunk', { x: pos.x, z: pos.z, chunk, blockEntities: column.blockEntities, worldConfig, isLightUpdate }) + this.loadedChunks[`${pos.x},${pos.z}`] = true + + this.debugChunksInfo[`${pos.x},${pos.z}`] ??= { + loads: [] + } + this.debugChunksInfo[`${pos.x},${pos.z}`].loads.push({ + dataLength: chunk.length, + reason, + time: Date.now(), + }) + } else if (this.isPlayground) { // don't allow in real worlds pre-flag chunks as loaded to avoid race condition when the chunk might still be loading. In playground it's assumed we always pre-load all chunks first + this.emitter.emit('markAsLoaded', { x: pos.x, z: pos.z }) + } + } else { + // console.debug('skipped loading chunk', dx, dz, '>', this.viewDistance) + } + } + + unloadAllChunks () { + for (const coords of Object.keys(this.loadedChunks)) { + const [x, z] = coords.split(',').map(Number) + this.unloadChunk({ x, z }) + } + } + + unloadChunk (pos: ChunkPos) { + this.emitter.emit('unloadChunk', { x: pos.x, z: pos.z }) + delete this.loadedChunks[`${pos.x},${pos.z}`] + delete this.debugChunksInfo[`${pos.x},${pos.z}`] + } + + lastBiomeId: number | null = null + + udpateBiome (pos: Vec3) { + try { + const biomeId = this.world.getBiome(pos) + if (biomeId !== this.lastBiomeId) { + this.lastBiomeId = biomeId + const biomeData = loadedData.biomes[biomeId] + if (biomeData) { + this.emitter.emit('biomeUpdate', { + biome: biomeData + }) + } else { + // unknown biome + this.emitter.emit('biomeReset') + } + } + } catch (e) { + console.error('error updating biome', e) + } + } + + lastPosCheck: Vec3 | null = null + async updatePosition (pos: Vec3, force = false) { + if (!this.allowPositionUpdate) return + const posFloored = pos.floored() + if (!force && this.lastPosCheck && this.lastPosCheck.equals(posFloored)) return + this.lastPosCheck = posFloored + + this.udpateBiome(pos) + + const [lastX, lastZ] = chunkPos(this.lastPos) + const [botX, botZ] = chunkPos(pos) + if (lastX !== botX || lastZ !== botZ || force) { + this.emitter.emit('chunkPosUpdate', { pos }) + + // unload chunks that are no longer in view + const newViewToUnload = new ViewRect(botX, botZ, this.viewDistance + this.keepChunksDistance) + const chunksToUnload: Vec3[] = [] + for (const coords of Object.keys(this.loadedChunks)) { + const x = parseInt(coords.split(',')[0], 10) + const z = parseInt(coords.split(',')[1], 10) + const p = new Vec3(x, 0, z) + const [chunkX, chunkZ] = chunkPos(p) + if (!newViewToUnload.contains(chunkX, chunkZ)) { + chunksToUnload.push(p) + } + } + for (const p of chunksToUnload) { + this.unloadChunk(p) + } + + // load new chunks + const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => { + const pos = new Vec3((botX + x) * 16, 0, (botZ + z) * 16) + if (!this.loadedChunks[`${pos.x},${pos.z}`]) return pos + return undefined! + }).filter(a => !!a) + this.lastPos.update(pos) + void this._loadChunks(positions, pos) + } else { + this.emitter.emit('chunkPosUpdate', { pos }) // todo-low + this.lastPos.update(pos) + } + } +} diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts new file mode 100644 index 00000000..4140e3fa --- /dev/null +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -0,0 +1,1087 @@ +/* eslint-disable guard-for-in */ +import { EventEmitter } from 'events' +import { Vec3 } from 'vec3' +import mcDataRaw from 'minecraft-data/data.js' // note: using alias +import TypedEmitter from 'typed-emitter' +import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' +import { generateSpiralMatrix } from 'flying-squid/dist/utils' +import { subscribeKey } from 'valtio/utils' +import { proxy } from 'valtio' +import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' +import type { ResourcesManagerTransferred } from '../../../src/resourcesManager' +import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer' +import { SoundSystem } from '../three/threeJsSound' +import { buildCleanupDecorator } from './cleanupDecorator' +import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' +import { chunkPos } from './simpleUtils' +import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats' +import { WorldDataEmitterWorker } from './worldDataEmitter' +import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState' +import { MesherLogReader } from './mesherlogReader' +import { setSkinsConfig } from './utils/skins' + +function mod (x, n) { + return ((x % n) + n) % n +} + +const toMajorVersion = version => { + const [a, b] = (String(version)).split('.') + return `${a}.${b}` +} + +export const worldCleanup = buildCleanupDecorator('resetWorld') + +export const defaultWorldRendererConfig = { + // Debug settings + showChunkBorders: false, + enableDebugOverlay: false, + + // Performance settings + mesherWorkers: 4, + addChunksBatchWaitTime: 200, + _experimentalSmoothChunkLoading: true, + _renderByChunks: false, + + // Rendering engine settings + dayCycle: true, + smoothLighting: true, + enableLighting: true, + starfield: true, + defaultSkybox: true, + renderEntities: true, + extraBlockRenderers: true, + foreground: true, + fov: 75, + volume: 1, + + // Camera visual related settings + showHand: false, + viewBobbing: false, + renderEars: true, + highlightBlockColor: 'blue', + + // Player models + fetchPlayerSkins: true, + skinTexturesProxy: undefined as string | undefined, + + // VR settings + vrSupport: true, + vrPageGameRendering: true, + + // World settings + clipWorldBelowY: undefined as number | undefined, + isPlayground: false +} + +export type WorldRendererConfig = typeof defaultWorldRendererConfig + +export abstract class WorldRendererCommon { + worldReadyResolvers = Promise.withResolvers() + worldReadyPromise = this.worldReadyResolvers.promise + timeOfTheDay = 0 + worldSizeParams = { minY: 0, worldHeight: 256 } + reactiveDebugParams = proxy({ + stopRendering: false, + chunksRenderAboveOverride: undefined as number | undefined, + chunksRenderAboveEnabled: false, + chunksRenderBelowOverride: undefined as number | undefined, + chunksRenderBelowEnabled: false, + chunksRenderDistanceOverride: undefined as number | undefined, + chunksRenderDistanceEnabled: false, + disableEntities: false, + // disableParticles: false + }) + + active = false + + // #region CHUNK & SECTIONS TRACKING + @worldCleanup() + loadedChunks = {} as Record // data is added for these chunks and they might be still processing + + @worldCleanup() + finishedChunks = {} as Record // these chunks are fully loaded into the world (scene) + + @worldCleanup() + finishedSections = {} as Record // these sections are fully loaded into the world (scene) + + @worldCleanup() + // loading sections (chunks) + sectionsWaiting = new Map() + + @worldCleanup() + queuedChunks = new Set() + queuedFunctions = [] as Array<() => void> + // #endregion + + renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{ + dirty (pos: Vec3, value: boolean): void + update (/* pos: Vec3, value: boolean */): void + chunkFinished (key: string): void + heightmap (key: string, heightmap: Uint8Array): void + }> + customTexturesDataUrl = undefined as string | undefined + workers: any[] = [] + viewerChunkPosition?: Vec3 + lastCamUpdate = 0 + droppedFpsPercentage = 0 + initialChunkLoadWasStartedIn: number | undefined + initialChunksLoad = true + enableChunksLoadDelay = false + texturesVersion?: string + viewDistance = -1 + chunksLength = 0 + allChunksFinished = false + messageQueue: any[] = [] + isProcessingQueue = false + ONMESSAGE_TIME_LIMIT = 30 // ms + + handleResize = () => { } + highestBlocksByChunks = new Map() + blockEntities = {} + + workersProcessAverageTime = 0 + workersProcessAverageTimeCount = 0 + maxWorkersProcessTime = 0 + geometryReceiveCount = {} as Record + allLoadedIn: undefined | number + onWorldSwitched = [] as Array<() => void> + renderTimeMax = 0 + renderTimeAvg = 0 + renderTimeAvgCount = 0 + edgeChunks = {} as Record + lastAddChunk = null as null | { + timeout: any + x: number + z: number + } + neighborChunkUpdates = true + lastChunkDistance = 0 + debugStopGeometryUpdate = false + + protocolCustomBlocks = new Map() + + @worldCleanup() + blockStateModelInfo = new Map() + + abstract outputFormat: 'threeJs' | 'webgpu' + worldBlockProvider: WorldBlockProvider + soundSystem: SoundSystem | undefined + + abstract changeBackgroundColor (color: [number, number, number]): void + + worldRendererConfig: WorldRendererConfig + playerStateReactive: PlayerStateReactive + playerStateUtils: PlayerStateUtils + reactiveState: RendererReactiveState + mesherLogReader: MesherLogReader | undefined + forceCallFromMesherReplayer = false + stopMesherMessagesProcessing = false + + abortController = new AbortController() + lastRendered = 0 + renderingActive = true + geometryReceiveCountPerSec = 0 + mesherLogger = { + contents: [] as string[], + active: new URL(location.href).searchParams.get('mesherlog') === 'true' + } + currentRenderedFrames = 0 + fpsAverage = 0 + lastFps = 0 + fpsWorst = undefined as number | undefined + fpsSamples = 0 + mainThreadRendering = true + backendInfoReport = '-' + chunksFullInfo = '-' + workerCustomHandleTime = 0 + + get version () { + return this.displayOptions.version + } + + get displayAdvancedStats () { + return (this.initOptions.config.statsVisible ?? 0) > 1 + } + + constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) { + this.snapshotInitialValues() + this.worldRendererConfig = displayOptions.inWorldRenderingConfig + this.playerStateReactive = displayOptions.playerStateReactive + this.playerStateUtils = getPlayerStateUtils(this.playerStateReactive) + this.reactiveState = displayOptions.rendererState + // this.mesherLogReader = new MesherLogReader(this) + this.renderUpdateEmitter.on('update', () => { + const loadedChunks = Object.keys(this.finishedChunks).length + updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`) + }) + + addNewStat('downloaded-chunks', 100, 140, 20) + + this.connect(this.displayOptions.worldView) + + const interval = setInterval(() => { + this.geometryReceiveCountPerSec = Object.values(this.geometryReceiveCount).reduce((acc, curr) => acc + curr, 0) + this.geometryReceiveCount = {} + updatePanesVisibility(this.displayAdvancedStats) + this.updateChunksStats() + if (this.mainThreadRendering) { + this.fpsUpdate() + } + }, 500) + this.abortController.signal.addEventListener('abort', () => { + clearInterval(interval) + }) + } + + fpsUpdate () { + this.fpsSamples++ + this.fpsAverage = (this.fpsAverage * (this.fpsSamples - 1) + this.currentRenderedFrames) / this.fpsSamples + if (this.fpsWorst === undefined) { + this.fpsWorst = this.currentRenderedFrames + } else { + this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames) + } + this.lastFps = this.currentRenderedFrames + this.currentRenderedFrames = 0 + } + + logWorkerWork (message: string | (() => string)) { + if (!this.mesherLogger.active) return + this.mesherLogger.contents.push(typeof message === 'function' ? message() : message) + } + + async init () { + if (this.active) throw new Error('WorldRendererCommon is already initialized') + + await Promise.all([ + this.resetWorkers(), + (async () => { + if (this.resourcesManager.currentResources?.allReady) { + await this.updateAssetsData() + } + })() + ]) + + this.resourcesManager.on('assetsTexturesUpdated', async () => { + if (!this.active) return + await this.updateAssetsData() + }) + + this.watchReactivePlayerState() + this.watchReactiveConfig() + this.worldReadyResolvers.resolve() + } + + snapshotInitialValues () { } + + wasChunkSentToWorker (chunkKey: string) { + return this.loadedChunks[chunkKey] + } + + async getHighestBlocks (chunkKey: string) { + return this.highestBlocksByChunks.get(chunkKey) + } + + updateCustomBlock (chunkKey: string, blockPos: string, model: string) { + this.protocolCustomBlocks.set(chunkKey, { + ...this.protocolCustomBlocks.get(chunkKey), + [blockPos]: model + }) + this.logWorkerWork(() => `-> updateCustomBlock ${chunkKey} ${blockPos} ${model} ${this.wasChunkSentToWorker(chunkKey)}`) + if (this.wasChunkSentToWorker(chunkKey)) { + const [x, y, z] = blockPos.split(',').map(Number) + this.setBlockStateId(new Vec3(x, y, z), undefined) + } + } + + async getBlockInfo (blockPos: { x: number, y: number, z: number }, stateId: number) { + const chunkKey = `${Math.floor(blockPos.x / 16) * 16},${Math.floor(blockPos.z / 16) * 16}` + const customBlockName = this.protocolCustomBlocks.get(chunkKey)?.[`${blockPos.x},${blockPos.y},${blockPos.z}`] + const cacheKey = getBlockAssetsCacheKey(stateId, customBlockName) + const modelInfo = this.blockStateModelInfo.get(cacheKey) + return { + customBlockName, + modelInfo + } + } + + initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) { + // init workers + for (let i = 0; i < numWorkers + 1; i++) { + const worker = initMesherWorker((data) => { + if (Array.isArray(data)) { + this.messageQueue.push(...data) + } else { + this.messageQueue.push(data) + } + void this.processMessageQueue('worker') + }) + this.workers.push(worker) + } + } + + onReactivePlayerStateUpdated(key: T, callback: (value: PlayerStateReactive[T]) => void, initial = true) { + if (initial) { + callback(this.playerStateReactive[key]) + } + subscribeKey(this.playerStateReactive, key, callback) + } + + onReactiveConfigUpdated(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) { + callback(this.worldRendererConfig[key]) + subscribeKey(this.worldRendererConfig, key, callback) + } + + onReactiveDebugUpdated(key: T, callback: (value: typeof this.reactiveDebugParams[T]) => void) { + callback(this.reactiveDebugParams[key]) + subscribeKey(this.reactiveDebugParams, key, callback) + } + + watchReactivePlayerState () { + this.onReactivePlayerStateUpdated('backgroundColor', (value) => { + this.changeBackgroundColor(value) + }) + } + + watchReactiveConfig () { + this.onReactiveConfigUpdated('fetchPlayerSkins', (value) => { + setSkinsConfig({ apiEnabled: value }) + }) + } + + async processMessageQueue (source: string) { + if (this.isProcessingQueue || this.messageQueue.length === 0) return + this.logWorkerWork(`# ${source} processing queue`) + if (this.lastRendered && performance.now() - this.lastRendered > this.ONMESSAGE_TIME_LIMIT && this.worldRendererConfig._experimentalSmoothChunkLoading && this.renderingActive) { + const start = performance.now() + await new Promise(resolve => { + requestAnimationFrame(resolve) + }) + this.logWorkerWork(`# processing got delayed by ${performance.now() - start}ms`) + } + this.isProcessingQueue = true + + const startTime = performance.now() + let processedCount = 0 + + while (this.messageQueue.length > 0) { + const processingStopped = this.stopMesherMessagesProcessing + if (!processingStopped) { + const data = this.messageQueue.shift()! + this.handleMessage(data) + processedCount++ + } + + // Check if we've exceeded the time limit + if (processingStopped || (performance.now() - startTime > this.ONMESSAGE_TIME_LIMIT && this.renderingActive && this.worldRendererConfig._experimentalSmoothChunkLoading)) { + // If we have more messages and exceeded time limit, schedule next batch + if (this.messageQueue.length > 0) { + requestAnimationFrame(async () => { + this.isProcessingQueue = false + void this.processMessageQueue('queue-delay') + }) + return + } + break + } + } + + this.isProcessingQueue = false + } + + handleMessage (rawData: any) { + const data = rawData as MesherMainEvent + if (!this.active) return + this.mesherLogReader?.workerMessageReceived(data.type, data) + if (data.type !== 'geometry' || !this.debugStopGeometryUpdate) { + const start = performance.now() + this.handleWorkerMessage(data as WorkerReceive) + this.workerCustomHandleTime += performance.now() - start + } + if (data.type === 'geometry') { + this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`) + this.geometryReceiveCount[data.workerIndex] ??= 0 + this.geometryReceiveCount[data.workerIndex]++ + const chunkCoords = data.key.split(',').map(Number) + this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2]))) + } + if (data.type === 'sectionFinished') { // on after load & unload section + this.logWorkerWork(`<- ${data.workerIndex} sectionFinished ${data.key} ${JSON.stringify({ processTime: data.processTime })}`) + if (!this.sectionsWaiting.has(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`) + this.sectionsWaiting.set(data.key, this.sectionsWaiting.get(data.key)! - 1) + if (this.sectionsWaiting.get(data.key) === 0) { + this.sectionsWaiting.delete(data.key) + this.finishedSections[data.key] = true + } + + const chunkCoords = data.key.split(',').map(Number) + const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}` + if (this.loadedChunks[chunkKey]) { // ensure chunk data was added, not a neighbor chunk update + let loaded = true + for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) { + if (!this.finishedSections[`${chunkCoords[0]},${y},${chunkCoords[2]}`]) { + loaded = false + break + } + } + if (loaded) { + // CHUNK FINISHED + this.finishedChunks[chunkKey] = true + this.reactiveState.world.chunksLoaded.add(`${Math.floor(chunkCoords[0] / 16)},${Math.floor(chunkCoords[2] / 16)}`) + this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0]},${chunkCoords[2]}`) + this.checkAllFinished() + // merge highest blocks by sections into highest blocks by chunks + // for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) { + // const sectionKey = `${chunkCoords[0]},${y},${chunkCoords[2]}` + // for (let x = 0; x < 16; x++) { + // for (let z = 0; z < 16; z++) { + // const posInsideKey = `${chunkCoords[0] + x},${chunkCoords[2] + z}` + // let block = null as HighestBlockInfo | null + // const highestBlock = this.highestBlocksBySections[sectionKey]?.[posInsideKey] + // if (!highestBlock) continue + // if (!block || highestBlock.y > block.y) { + // block = highestBlock + // } + // if (block) { + // this.highestBlocksByChunks[chunkKey] ??= {} + // this.highestBlocksByChunks[chunkKey][posInsideKey] = block + // } + // } + // } + // delete this.highestBlocksBySections[sectionKey] + // } + } + } + + this.renderUpdateEmitter.emit('update') + if (data.processTime) { + this.workersProcessAverageTimeCount++ + this.workersProcessAverageTime = ((this.workersProcessAverageTime * (this.workersProcessAverageTimeCount - 1)) + data.processTime) / this.workersProcessAverageTimeCount + this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime) + } + } + + if (data.type === 'blockStateModelInfo') { + for (const [cacheKey, info] of Object.entries(data.info)) { + this.blockStateModelInfo.set(cacheKey, info) + } + } + + if (data.type === 'heightmap') { + this.reactiveState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap)) + } + } + + downloadMesherLog () { + const a = document.createElement('a') + a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(this.mesherLogger.contents.join('\n')) + a.download = 'mesher.log' + a.click() + } + + checkAllFinished () { + if (this.sectionsWaiting.size === 0) { + this.reactiveState.world.mesherWork = false + } + // todo check exact surrounding chunks + const allFinished = Object.keys(this.finishedChunks).length >= this.chunksLength + if (allFinished) { + this.allChunksLoaded?.() + this.allChunksFinished = true + this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn! + } + this.updateChunksStats() + } + + changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { } + + abstract handleWorkerMessage (data: WorkerReceive): void + + abstract updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void + + abstract render (): void + + /** + * Optionally update data that are depedendent on the viewer position + */ + updatePosDataChunk? (key: string): void + + allChunksLoaded? (): void + + timeUpdated? (newTime: number): void + + biomeUpdated? (biome: any): void + + biomeReset? (): void + + updateViewerPosition (pos: Vec3) { + this.viewerChunkPosition = pos + for (const [key, value] of Object.entries(this.loadedChunks)) { + if (!value) continue + this.updatePosDataChunk?.(key) + } + } + + sendWorkers (message: WorkerSend) { + for (const worker of this.workers) { + worker.postMessage(message) + } + } + + getDistance (posAbsolute: Vec3) { + const [botX, botZ] = chunkPos(this.viewerChunkPosition!) + const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16)) + const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16)) + return [dx, dz] as [number, number] + } + + abstract updateShowChunksBorder (value: boolean): void + + resetWorld () { + // destroy workers + for (const worker of this.workers) { + worker.terminate() + } + this.workers = [] + } + + async resetWorkers () { + this.resetWorld() + + // for workers in single file build + if (typeof document !== 'undefined' && document?.readyState === 'loading') { + await new Promise(resolve => { + document.addEventListener('DOMContentLoaded', resolve) + }) + } + + this.initWorkers() + this.active = true + + this.sendMesherMcData() + } + + getMesherConfig (): MesherConfig { + let skyLight = 15 + const timeOfDay = this.timeOfTheDay + if (timeOfDay < 0 || timeOfDay > 24_000) { + // + } else if (timeOfDay <= 6000 || timeOfDay >= 18_000) { + skyLight = 15 + } else if (timeOfDay > 6000 && timeOfDay < 12_000) { + skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15 + } else if (timeOfDay >= 12_000 && timeOfDay < 18_000) { + skyLight = ((timeOfDay - 12_000) / 6000) * 15 + } + + skyLight = Math.floor(skyLight) + return { + version: this.version, + enableLighting: this.worldRendererConfig.enableLighting, + skyLight, + smoothLighting: this.worldRendererConfig.smoothLighting, + outputFormat: this.outputFormat, + // textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, + debugModelVariant: undefined, + clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, + disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, + worldMinY: this.worldMinYRender, + worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight, + } + } + + sendMesherMcData () { + const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)] + const mcData = { + version: JSON.parse(JSON.stringify(allMcData.version)) + } + for (const key of dynamicMcDataFiles) { + mcData[key] = allMcData[key] + } + + for (const worker of this.workers) { + worker.postMessage({ type: 'mcData', mcData, config: this.getMesherConfig() }) + } + this.logWorkerWork('# mcData sent') + } + + async updateAssetsData () { + const resources = this.resourcesManager.currentResources + + if (this.workers.length === 0) throw new Error('workers not initialized yet') + for (const [i, worker] of this.workers.entries()) { + const { blockstatesModels } = resources + + worker.postMessage({ + type: 'mesherData', + workerIndex: i, + blocksAtlas: { + latest: resources.blocksAtlasJson + }, + blockstatesModels, + config: this.getMesherConfig(), + }) + } + + this.logWorkerWork('# mesherData sent') + console.log('textures loaded') + } + + get worldMinYRender () { + return Math.floor(Math.max(this.worldSizeParams.minY, this.worldRendererConfig.clipWorldBelowY ?? -Infinity) / 16) * 16 + } + + updateChunksStats () { + const loadedChunks = Object.keys(this.finishedChunks) + this.displayOptions.nonReactiveState.world.chunksLoaded = new Set(loadedChunks) + this.displayOptions.nonReactiveState.world.chunksTotalNumber = this.chunksLength + this.reactiveState.world.allChunksLoaded = this.allChunksFinished + + const text = `Q: ${this.messageQueue.length} ${Object.keys(this.loadedChunks).length}/${Object.keys(this.finishedChunks).length}/${this.chunksLength} chunks (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.geometryReceiveCountPerSec}ss/${this.allLoadedIn?.toFixed(1) ?? '-'}s)` + this.chunksFullInfo = text + updateStatText('downloaded-chunks', text) + } + + addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) { + if (!this.active) return + if (this.workers.length === 0) throw new Error('workers not initialized yet') + this.initialChunksLoad = false + this.initialChunkLoadWasStartedIn ??= Date.now() + this.loadedChunks[`${x},${z}`] = true + this.updateChunksStats() + + const chunkKey = `${x},${z}` + const customBlockModels = this.protocolCustomBlocks.get(chunkKey) + + for (const worker of this.workers) { + worker.postMessage({ + type: 'chunk', + x, + z, + chunk, + customBlockModels: customBlockModels || undefined + }) + } + this.workers[0].postMessage({ + type: 'getHeightmap', + x, + z, + }) + this.logWorkerWork(() => `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`) + this.mesherLogReader?.chunkReceived(x, z, chunk.length) + for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) { + const loc = new Vec3(x, y, z) + this.setSectionDirty(loc) + if (this.neighborChunkUpdates && (!isLightUpdate || this.worldRendererConfig.smoothLighting)) { + this.setSectionDirty(loc.offset(-16, 0, 0)) + this.setSectionDirty(loc.offset(16, 0, 0)) + this.setSectionDirty(loc.offset(0, 0, -16)) + this.setSectionDirty(loc.offset(0, 0, 16)) + } + } + } + + markAsLoaded (x, z) { + this.loadedChunks[`${x},${z}`] = true + this.finishedChunks[`${x},${z}`] = true + this.logWorkerWork(`-> markAsLoaded ${JSON.stringify({ x, z })}`) + this.checkAllFinished() + } + + removeColumn (x, z) { + delete this.loadedChunks[`${x},${z}`] + for (const worker of this.workers) { + worker.postMessage({ type: 'unloadChunk', x, z }) + } + this.logWorkerWork(`-> unloadChunk ${JSON.stringify({ x, z })}`) + delete this.finishedChunks[`${x},${z}`] + this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength + if (Object.keys(this.finishedChunks).length === 0) { + this.allLoadedIn = undefined + this.initialChunkLoadWasStartedIn = undefined + } + for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) { + this.setSectionDirty(new Vec3(x, y, z), false) + delete this.finishedSections[`${x},${y},${z}`] + } + this.highestBlocksByChunks.delete(`${x},${z}`) + + this.updateChunksStats() + + if (Object.keys(this.loadedChunks).length === 0) { + this.mesherLogger.contents = [] + this.logWorkerWork('# all chunks unloaded. New log started') + void this.mesherLogReader?.maybeStartReplay() + } + } + + setBlockStateId (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) { + const set = async () => { + const sectionX = Math.floor(pos.x / 16) * 16 + const sectionZ = Math.floor(pos.z / 16) * 16 + if (this.queuedChunks.has(`${sectionX},${sectionZ}`)) { + await new Promise(resolve => { + this.queuedFunctions.push(() => { + resolve() + }) + }) + } + if (!this.loadedChunks[`${sectionX},${sectionZ}`]) { + // console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos) + } + this.setBlockStateIdInner(pos, stateId, needAoRecalculation) + } + void set() + } + + updateEntity (e: any, isUpdate = false) { } + + abstract updatePlayerEntity? (e: any): void + + lightUpdate (chunkX: number, chunkZ: number) { } + + connect (worldView: WorldDataEmitterWorker) { + const worldEmitter = worldView + + worldEmitter.on('entity', (e) => { + this.updateEntity(e, false) + }) + worldEmitter.on('entityMoved', (e) => { + this.updateEntity(e, true) + }) + worldEmitter.on('playerEntity', (e) => { + this.updatePlayerEntity?.(e) + }) + + let currentLoadChunkBatch = null as { + timeout + data + } | null + worldEmitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => { + this.worldSizeParams = worldConfig + this.queuedChunks.add(`${x},${z}`) + const args = [x, z, chunk, isLightUpdate] + if (!currentLoadChunkBatch) { + // add a setting to use debounce instead + currentLoadChunkBatch = { + data: [], + timeout: setTimeout(() => { + for (const args of currentLoadChunkBatch!.data) { + this.queuedChunks.delete(`${args[0]},${args[1]}`) + this.addColumn(...args as Parameters) + } + for (const fn of this.queuedFunctions) { + fn() + } + this.queuedFunctions = [] + currentLoadChunkBatch = null + }, this.worldRendererConfig.addChunksBatchWaitTime) + } + } + currentLoadChunkBatch.data.push(args) + }) + // todo remove and use other architecture instead so data flow is clear + worldEmitter.on('blockEntities', (blockEntities) => { + this.blockEntities = blockEntities + }) + + worldEmitter.on('unloadChunk', ({ x, z }) => { + this.removeColumn(x, z) + }) + + worldEmitter.on('blockUpdate', ({ pos, stateId }) => { + this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId) + }) + + worldEmitter.on('chunkPosUpdate', ({ pos }) => { + this.updateViewerPosition(pos) + }) + + worldEmitter.on('end', () => { + this.worldStop?.() + }) + + + worldEmitter.on('renderDistance', (d) => { + this.viewDistance = d + this.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length + }) + + worldEmitter.on('renderDistance', (d) => { + this.viewDistance = d + this.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length + this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength + }) + + worldEmitter.on('markAsLoaded', ({ x, z }) => { + this.markAsLoaded(x, z) + }) + + worldEmitter.on('updateLight', ({ pos }) => { + this.lightUpdate(pos.x, pos.z) + }) + + worldEmitter.on('onWorldSwitch', () => { + for (const fn of this.onWorldSwitched) { + try { + fn() + } catch (e) { + setTimeout(() => { + console.log('[Renderer Backend] Error in onWorldSwitched:') + throw e + }, 0) + } + } + }) + + worldEmitter.on('time', (timeOfDay) => { + if (!this.worldRendererConfig.dayCycle) return + this.timeUpdated?.(timeOfDay) + + this.timeOfTheDay = timeOfDay + + // if (this.worldRendererConfig.skyLight === skyLight) return + // this.worldRendererConfig.skyLight = skyLight + // if (this instanceof WorldRendererThree) { + // (this).rerenderAllChunks?.() + // } + }) + + worldEmitter.on('biomeUpdate', ({ biome }) => { + this.biomeUpdated?.(biome) + }) + + worldEmitter.on('biomeReset', () => { + this.biomeReset?.() + }) + } + + setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) { + const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}` + const blockPosKey = `${pos.x},${pos.y},${pos.z}` + const customBlockModels = this.protocolCustomBlocks.get(chunkKey) || {} + + for (const worker of this.workers) { + worker.postMessage({ + type: 'blockUpdate', + pos, + stateId, + customBlockModels + }) + } + this.logWorkerWork(`-> blockUpdate ${JSON.stringify({ pos, stateId, customBlockModels })}`) + this.setSectionDirty(pos, true, true) + if (this.neighborChunkUpdates) { + if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, true) + if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, true) + if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, true) + if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, true) + if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, true) + if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, true) + + if (needAoRecalculation) { + // top view neighbors + if ((pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, -16), true, true) + if ((pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(16, 0, -16), true, true) + if ((pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-16, 0, 16), true, true) + if ((pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 16), true, true) + + // side view neighbors (but ignore updates above) + // z view neighbors + if ((pos.x & 15) === 0 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(-16, -16, 0), true, true) + if ((pos.x & 15) === 15 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(16, -16, 0), true, true) + + // x view neighbors + if ((pos.z & 15) === 0 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, -16), true, true) + if ((pos.z & 15) === 15 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 16), true, true) + + // x & z neighbors + if ((pos.y & 15) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-16, -16, -16), true, true) + if ((pos.y & 15) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(16, -16, -16), true, true) + if ((pos.y & 15) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-16, -16, 16), true, true) + if ((pos.y & 15) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(16, -16, 16), true, true) + } + } + } + + abstract worldStop? () + + queueAwaited = false + toWorkerMessagesQueue = {} as { [workerIndex: string]: any[] } + + getWorkerNumber (pos: Vec3, updateAction = false) { + if (updateAction) { + const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}` + const cantUseChangeWorker = this.sectionsWaiting.get(key) && !this.finishedSections[key] + if (!cantUseChangeWorker) return 0 + } + + const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1) + return hash + 1 + } + + async debugGetWorkerCustomBlockModel (pos: Vec3) { + const data = [] as Array> + for (const worker of this.workers) { + data.push(new Promise((resolve) => { + worker.addEventListener('message', (e) => { + if (e.data.type === 'customBlockModel') { + resolve(e.data.customBlockModel) + } + }) + })) + worker.postMessage({ + type: 'getCustomBlockModel', + pos + }) + } + return Promise.all(data) + } + + setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks + if (!this.forceCallFromMesherReplayer && this.mesherLogReader) return + + if (this.viewDistance === -1) throw new Error('viewDistance not set') + this.reactiveState.world.mesherWork = true + const distance = this.getDistance(pos) + // todo shouldnt we check loadedChunks instead? + if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return + const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}` + // if (this.sectionsOutstanding.has(key)) return + this.renderUpdateEmitter.emit('dirty', pos, value) + // Dispatch sections to workers based on position + // This guarantees uniformity accross workers and that a given section + // is always dispatched to the same worker + const hash = this.getWorkerNumber(pos, useChangeWorker && this.mesherLogger.active) + this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1) + if (this.forceCallFromMesherReplayer) { + this.workers[hash].postMessage({ + type: 'dirty', + x: pos.x, + y: pos.y, + z: pos.z, + value, + config: this.getMesherConfig(), + }) + } else { + this.toWorkerMessagesQueue[hash] ??= [] + this.toWorkerMessagesQueue[hash].push({ + // this.workers[hash].postMessage({ + type: 'dirty', + x: pos.x, + y: pos.y, + z: pos.z, + value, + config: this.getMesherConfig(), + }) + this.dispatchMessages() + } + } + + dispatchMessages () { + if (this.queueAwaited) return + this.queueAwaited = true + setTimeout(() => { + // group messages and send as one + for (const workerIndex in this.toWorkerMessagesQueue) { + const worker = this.workers[Number(workerIndex)] + worker.postMessage(this.toWorkerMessagesQueue[workerIndex]) + for (const message of this.toWorkerMessagesQueue[workerIndex]) { + this.logWorkerWork(`-> ${workerIndex} dispatchMessages ${message.type} ${JSON.stringify({ x: message.x, y: message.y, z: message.z, value: message.value })}`) + } + } + this.toWorkerMessagesQueue = {} + this.queueAwaited = false + }) + } + + // Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number + // of sections not rendered are 0 + async waitForChunksToRender () { + return new Promise((resolve, reject) => { + if ([...this.sectionsWaiting].length === 0) { + resolve() + return + } + + const updateHandler = () => { + if (this.sectionsWaiting.size === 0) { + this.renderUpdateEmitter.removeListener('update', updateHandler) + resolve() + } + } + this.renderUpdateEmitter.on('update', updateHandler) + }) + } + + async waitForChunkToLoad (pos: Vec3) { + return new Promise((resolve, reject) => { + const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}` + if (this.loadedChunks[key]) { + resolve() + return + } + const updateHandler = () => { + if (this.loadedChunks[key]) { + this.renderUpdateEmitter.removeListener('update', updateHandler) + resolve() + } + } + this.renderUpdateEmitter.on('update', updateHandler) + }) + } + + destroy () { + // Stop all workers + for (const worker of this.workers) { + worker.terminate() + } + this.workers = [] + + // Stop and destroy sound system + if (this.soundSystem) { + this.soundSystem.destroy() + this.soundSystem = undefined + } + + this.active = false + + this.renderUpdateEmitter.removeAllListeners() + this.abortController.abort() + removeAllStats() + } +} + +export const initMesherWorker = (onGotMessage: (data: any) => void) => { + // Node environment needs an absolute path, but browser needs the url of the file + const workerName = 'mesher.js' + + let worker: any + if (process.env.SINGLE_FILE_BUILD) { + const workerCode = document.getElementById('mesher-worker-code')!.textContent! + const blob = new Blob([workerCode], { type: 'text/javascript' }) + worker = new Worker(window.URL.createObjectURL(blob)) + } else { + worker = new Worker(workerName) + } + + worker.onmessage = ({ data }) => { + onGotMessage(data) + } + if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) + return worker +} + +export const meshersSendMcData = (workers: Worker[], version: string, addData = {} as Record) => { + const allMcData = mcDataRaw.pc[version] ?? mcDataRaw.pc[toMajorVersion(version)] + const mcData = { + version: JSON.parse(JSON.stringify(allMcData.version)) + } + for (const key of dynamicMcDataFiles) { + mcData[key] = allMcData[key] + } + + for (const worker of workers) { + worker.postMessage({ type: 'mcData', mcData, ...addData }) + } +} diff --git a/prismarine-viewer/viewer/sign-renderer/index.html b/renderer/viewer/sign-renderer/index.html similarity index 100% rename from prismarine-viewer/viewer/sign-renderer/index.html rename to renderer/viewer/sign-renderer/index.html diff --git a/renderer/viewer/sign-renderer/index.ts b/renderer/viewer/sign-renderer/index.ts new file mode 100644 index 00000000..f14b9b4c --- /dev/null +++ b/renderer/viewer/sign-renderer/index.ts @@ -0,0 +1,216 @@ +import type { ChatMessage } from 'prismarine-chat' +import { createCanvas } from '../lib/utils' + +type SignBlockEntity = { + Color?: string + GlowingText?: 0 | 1 + Text1?: string + Text2?: string + Text3?: string + Text4?: string +} | { + // todo + is_waxed?: 0 | 1 + front_text: { + color: string + messages: string[] + // todo + has_glowing_text?: 0 | 1 + } + // todo + // back_text: {} +} + +type JsonEncodedType = string | null | Record + +const parseSafe = (text: string, task: string) => { + try { + return JSON.parse(text) + } catch (e) { + console.warn(`Failed to parse ${task}`, e) + return null + } +} + +const LEGACY_COLORS = { + black: '#000000', + dark_blue: '#0000AA', + dark_green: '#00AA00', + dark_aqua: '#00AAAA', + dark_red: '#AA0000', + dark_purple: '#AA00AA', + gold: '#FFAA00', + gray: '#AAAAAA', + dark_gray: '#555555', + blue: '#5555FF', + green: '#55FF55', + aqua: '#55FFFF', + red: '#FF5555', + light_purple: '#FF55FF', + yellow: '#FFFF55', + white: '#FFFFFF', +} + +export const renderSign = ( + blockEntity: SignBlockEntity, + isHanging: boolean, + PrismarineChat: typeof ChatMessage, + ctxHook = (ctx) => { }, + canvasCreator = (width, height): OffscreenCanvas => { return createCanvas(width, height) } +) => { + // todo don't use texture rendering, investigate the font rendering when possible + // or increase factor when needed + const factor = 40 + const fontSize = 1.6 * factor + const signboardY = [16, 9] + const heightOffset = signboardY[0] - signboardY[1] + const heightScalar = heightOffset / 16 + // todo the text should be clipped based on it's render width (needs investigate) + + const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [ + blockEntity.Text1, + blockEntity.Text2, + blockEntity.Text3, + blockEntity.Text4 + ] + + if (!texts.some((text) => text !== 'null')) { + return undefined + } + + const canvas = canvasCreator(16 * factor, heightOffset * factor) + + const _ctx = canvas.getContext('2d')! + + ctxHook(_ctx) + const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black' + for (const [lineNum, text] of texts.slice(0, 4).entries()) { + if (text === 'null') continue + renderComponent(text, PrismarineChat, canvas, fontSize, defaultColor, fontSize * (lineNum + 1) + (isHanging ? 0 : -8)) + } + return canvas +} + +export const renderComponent = ( + text: JsonEncodedType | string | undefined, + PrismarineChat: typeof ChatMessage, + canvas: OffscreenCanvas, + fontSize: number, + defaultColor: string, + offset = 0 +) => { + // todo: in pre flatenning it seems the format was not json + const parsed = typeof text === 'string' && (text?.startsWith('{') || text?.startsWith('"')) ? parseSafe(text ?? '""', 'sign text') : text + if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) return + // todo fix type + + const ctx = canvas.getContext('2d')! + if (!ctx) throw new Error('Could not get 2d context') + ctx.imageSmoothingEnabled = false + ctx.font = `${fontSize}px mojangles` + + type Formatting = { + color: string | undefined + underlined: boolean | undefined + strikethrough: boolean | undefined + bold: boolean | undefined + italic: boolean | undefined + } + + type Message = ChatMessage & Formatting & { text: string } + + const message = new PrismarineChat(parsed) as Message + + const toRenderCanvas: Array<{ + fontStyle: string + fillStyle: string + underlineStyle: boolean + strikeStyle: boolean + offset: number + text: string + }> = [] + let visibleFormatting = false + let plainText = '' + let textOffset = offset + const textWidths: number[] = [] + + const renderText = (component: Message, parentFormatting?: Formatting | undefined) => { + const { text } = component + const formatting = { + color: component.color ?? parentFormatting?.color, + underlined: component.underlined ?? parentFormatting?.underlined, + strikethrough: component.strikethrough ?? parentFormatting?.strikethrough, + bold: component.bold ?? parentFormatting?.bold, + italic: component.italic ?? parentFormatting?.italic + } + visibleFormatting = visibleFormatting || formatting.underlined || formatting.strikethrough || false + if (text?.includes('\n')) { + for (const line of text.split('\n')) { + addTextPart(line, formatting) + textOffset += fontSize + plainText = '' + } + } else if (text) { + addTextPart(text, formatting) + } + if (component.extra) { + for (const child of component.extra) { + renderText(child as Message, formatting) + } + } + } + + const addTextPart = (text: string, formatting: Formatting) => { + plainText += text + textWidths[textOffset] = ctx.measureText(plainText).width + let color = formatting.color ?? defaultColor + if (!color.startsWith('#')) { + color = LEGACY_COLORS[color.toLowerCase()] || color + } + toRenderCanvas.push({ + fontStyle: `${formatting.bold ? 'bold' : ''} ${formatting.italic ? 'italic' : ''}`, + fillStyle: color, + underlineStyle: formatting.underlined ?? false, + strikeStyle: formatting.strikethrough ?? false, + offset: textOffset, + text + }) + } + + renderText(message) + + // skip rendering empty lines + if (!visibleFormatting && !message.toString().trim()) return + + let renderedWidth = 0 + let previousOffsetY = 0 + for (const { fillStyle, fontStyle, underlineStyle, strikeStyle, offset: offsetY, text } of toRenderCanvas) { + if (previousOffsetY !== offsetY) { + renderedWidth = 0 + } + previousOffsetY = offsetY + ctx.fillStyle = fillStyle + ctx.textRendering = 'optimizeLegibility' + ctx.font = `${fontStyle} ${fontSize}px mojangles` + const textWidth = textWidths[offsetY] ?? ctx.measureText(text).width + const offsetX = (canvas.width - textWidth) / 2 + renderedWidth + ctx.fillText(text, offsetX, offsetY) + if (strikeStyle) { + ctx.lineWidth = fontSize / 8 + ctx.strokeStyle = fillStyle + ctx.beginPath() + ctx.moveTo(offsetX, offsetY - ctx.lineWidth * 2.5) + ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY - ctx.lineWidth * 2.5) + ctx.stroke() + } + if (underlineStyle) { + ctx.lineWidth = fontSize / 8 + ctx.strokeStyle = fillStyle + ctx.beginPath() + ctx.moveTo(offsetX, offsetY + ctx.lineWidth) + ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY + ctx.lineWidth) + ctx.stroke() + } + renderedWidth += ctx.measureText(text).width + } +} diff --git a/prismarine-viewer/viewer/sign-renderer/noop.js b/renderer/viewer/sign-renderer/noop.js similarity index 100% rename from prismarine-viewer/viewer/sign-renderer/noop.js rename to renderer/viewer/sign-renderer/noop.js diff --git a/prismarine-viewer/viewer/sign-renderer/package.json b/renderer/viewer/sign-renderer/package.json similarity index 100% rename from prismarine-viewer/viewer/sign-renderer/package.json rename to renderer/viewer/sign-renderer/package.json diff --git a/prismarine-viewer/viewer/sign-renderer/playground.ts b/renderer/viewer/sign-renderer/playground.ts similarity index 69% rename from prismarine-viewer/viewer/sign-renderer/playground.ts rename to renderer/viewer/sign-renderer/playground.ts index 4182b1c9..a7438092 100644 --- a/prismarine-viewer/viewer/sign-renderer/playground.ts +++ b/renderer/viewer/sign-renderer/playground.ts @@ -1,5 +1,5 @@ -import { renderSign } from '.' import PrismarineChatLoader from 'prismarine-chat' +import { renderSign } from '.' const PrismarineChat = PrismarineChatLoader({ language: {} } as any) @@ -11,19 +11,24 @@ await new Promise(resolve => { }) const blockEntity = { - "GlowingText": 0, - "Color": "black", - "Text4": "{\"text\":\"\"}", - "Text3": "{\"text\":\"\"}", - "Text2": "{\"text\":\"\"}", - "Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}" + 'GlowingText': 0, + 'Color': 'black', + 'Text4': '{"text":""}', + 'Text3': '{"text":""}', + 'Text2': '{"text":""}', + 'Text1': '{"extra":[{"color":"dark_green","text":"Minecraft "},{"text":"Tools"}],"text":""}' } as const await document.fonts.load('1em mojangles') -const canvas = renderSign(blockEntity, PrismarineChat, (ctx) => { +const canvas = renderSign(blockEntity, false, PrismarineChat, (ctx) => { ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height) -}) +}, (width, height) => { + const canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + return canvas as unknown as OffscreenCanvas +}) as unknown as HTMLCanvasElement if (canvas) { canvas.style.imageRendering = 'pixelated' diff --git a/prismarine-viewer/viewer/sign-renderer/tests.test.ts b/renderer/viewer/sign-renderer/tests.test.ts similarity index 70% rename from prismarine-viewer/viewer/sign-renderer/tests.test.ts rename to renderer/viewer/sign-renderer/tests.test.ts index 4549c6f4..ab268849 100644 --- a/prismarine-viewer/viewer/sign-renderer/tests.test.ts +++ b/renderer/viewer/sign-renderer/tests.test.ts @@ -1,6 +1,6 @@ import { test, expect } from 'vitest' -import { renderSign } from '.' import PrismarineChatLoader from 'prismarine-chat' +import { renderSign } from '.' const PrismarineChat = PrismarineChatLoader({ language: {} } as any) let ctxTexts = [] as any[] @@ -22,25 +22,21 @@ global.document = { const render = (entity) => { ctxTexts = [] - renderSign(entity, PrismarineChat) + renderSign(entity, true, PrismarineChat) return ctxTexts.map(({ text, y }) => [y / 64, text]) } test('sign renderer', () => { let blockEntity = { - "GlowingText": 0, - "Color": "black", - "Text4": "{\"text\":\"\"}", - "Text3": "{\"text\":\"\"}", - "Text2": "{\"text\":\"\"}", - "Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}" + 'GlowingText': 0, + 'Color': 'black', + 'Text4': '{"text":""}', + 'Text3': '{"text":""}', + 'Text2': '{"text":""}', + 'Text1': '{"extra":[{"color":"dark_green","text":"Minecraft "},{"text":"Tools"}],"text":""}' } as any expect(render(blockEntity)).toMatchInlineSnapshot(` [ - [ - 1, - "", - ], [ 1, "Minecraft ", @@ -53,10 +49,10 @@ test('sign renderer', () => { `) blockEntity = { // pre flatenning - "Text1": "Welcome to", - "Text2": "", - "Text3": "null", - "Text4": "\"Version 2.1\"", + 'Text1': 'Welcome to', + 'Text2': '', + 'Text3': 'null', + 'Text4': '"Version 2.1"', } as const expect(render(blockEntity)).toMatchInlineSnapshot(` [ diff --git a/prismarine-viewer/viewer/sign-renderer/vite.config.ts b/renderer/viewer/sign-renderer/vite.config.ts similarity index 58% rename from prismarine-viewer/viewer/sign-renderer/vite.config.ts rename to renderer/viewer/sign-renderer/vite.config.ts index 896ac865..aebfc20c 100644 --- a/prismarine-viewer/viewer/sign-renderer/vite.config.ts +++ b/renderer/viewer/sign-renderer/vite.config.ts @@ -3,8 +3,8 @@ import { defineConfig } from 'vite' export default defineConfig({ resolve: { alias: { - 'prismarine-registry': "./noop.js", - 'prismarine-nbt': "./noop.js" + 'prismarine-registry': './noop.js', + 'prismarine-nbt': './noop.js' }, }, }) diff --git a/renderer/viewer/three/appShared.ts b/renderer/viewer/three/appShared.ts new file mode 100644 index 00000000..5be9e10b --- /dev/null +++ b/renderer/viewer/three/appShared.ts @@ -0,0 +1,74 @@ +import { BlockModel } from 'mc-assets/dist/types' +import { ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items' +import { ResourcesManager, ResourcesManagerTransferred } from '../../../src/resourcesManager' +import { renderSlot } from './renderSlot' + +export const getItemUv = (item: Record, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): { + u: number + v: number + su: number + sv: number + renderInfo?: ReturnType + // texture: ImageBitmap + modelName: string +} | { + resolvedModel: BlockModel + modelName: string +} => { + const resources = resourcesManager.currentResources + if (!resources) throw new Error('Resources not loaded') + const idOrName = item.itemId ?? item.blockId ?? item.name + const { blockState } = item + try { + const name = + blockState + ? loadedData.blocksByStateId[blockState]?.name + : typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName + if (!name) throw new Error(`Item not found: ${idOrName}`) + + const model = getItemModelName({ + ...item, + name, + } as GeneralInputItem, specificProps, resourcesManager, playerState) + + const renderInfo = renderSlot({ + modelName: model, + }, resourcesManager, false, true) + + if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`) + + const img = renderInfo.texture === 'blocks' ? resources.blocksAtlasImage : resources.itemsAtlasImage + + if (renderInfo.blockData) { + return { + resolvedModel: renderInfo.blockData.resolvedModel, + modelName: renderInfo.modelName! + } + } + if (renderInfo.slice) { + // Get slice coordinates from either block or item texture + const [x, y, w, h] = renderInfo.slice + const [u, v, su, sv] = [x / img.width, y / img.height, (w / img.width), (h / img.height)] + return { + u, v, su, sv, + renderInfo, + // texture: img, + modelName: renderInfo.modelName! + } + } + + throw new Error(`Invalid render info for item ${name}`) + } catch (err) { + reportError?.(err) + // Return default UV coordinates for missing texture + return { + u: 0, + v: 0, + su: 16 / resources.blocksAtlasImage.width, + sv: 16 / resources.blocksAtlasImage.width, + // texture: resources.blocksAtlasImage, + modelName: 'missing' + } + } +} diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts new file mode 100644 index 00000000..7b159509 --- /dev/null +++ b/renderer/viewer/three/cameraShake.ts @@ -0,0 +1,120 @@ +import * as THREE from 'three' +import { WorldRendererThree } from './worldrendererThree' + +export class CameraShake { + private rollAngle = 0 + private get damageRollAmount () { return 5 } + private get damageAnimDuration () { return 200 } + private rollAnimation?: { startTime: number, startRoll: number, targetRoll: number, duration: number, returnToZero?: boolean } + private basePitch = 0 + private baseYaw = 0 + + constructor (public worldRenderer: WorldRendererThree, public onRenderCallbacks: Array<() => void>) { + onRenderCallbacks.push(() => { + this.update() + }) + } + + setBaseRotation (pitch: number, yaw: number) { + this.basePitch = pitch + this.baseYaw = yaw + this.update() + } + + getBaseRotation () { + return { pitch: this.basePitch, yaw: this.baseYaw } + } + + shakeFromDamage (yaw?: number) { + // Add roll animation + const startRoll = this.rollAngle + const targetRoll = startRoll + (yaw ?? (Math.random() < 0.5 ? -1 : 1)) * this.damageRollAmount + + this.rollAnimation = { + startTime: performance.now(), + startRoll, + targetRoll, + duration: this.damageAnimDuration / 2 + } + } + + update () { + if (this.worldRenderer.playerStateUtils.isSpectatingEntity()) { + // Remove any shaking when spectating + this.rollAngle = 0 + this.rollAnimation = undefined + } + // Update roll animation + if (this.rollAnimation) { + const now = performance.now() + const elapsed = now - this.rollAnimation.startTime + const progress = Math.min(elapsed / this.rollAnimation.duration, 1) + + if (this.rollAnimation.returnToZero) { + // Ease back to zero + this.rollAngle = this.rollAnimation.startRoll * (1 - this.easeInOut(progress)) + if (progress === 1) { + this.rollAnimation = undefined + } + } else { + // Initial roll + this.rollAngle = this.rollAnimation.startRoll + (this.rollAnimation.targetRoll - this.rollAnimation.startRoll) * this.easeOut(progress) + if (progress === 1) { + // Start return to zero animation + this.rollAnimation = { + startTime: now, + startRoll: this.rollAngle, + targetRoll: 0, + duration: this.damageAnimDuration / 2, + returnToZero: true + } + } + } + } + + const camera = this.worldRenderer.cameraObject + + if (this.worldRenderer.cameraGroupVr) { + // For VR camera, only apply yaw rotation + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw) + camera.setRotationFromQuaternion(yawQuat) + } else { + // For regular camera, apply all rotations + // Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees) + const pitchOffset = this.addAntiZfightingOffset(this.basePitch) + const yawOffset = this.addAntiZfightingOffset(this.baseYaw) + + const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset) + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset) + const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle)) + // Combine rotations in the correct order: pitch -> yaw -> roll + const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) + camera.setRotationFromQuaternion(finalQuat) + } + } + + private easeOut (t: number): number { + return 1 - (1 - t) * (1 - t) + } + + private easeInOut (t: number): number { + return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2 + } + + private addAntiZfightingOffset (angle: number): number { + const offset = 0.001 // Very small offset in radians (about 0.057 degrees) + + // Check if the angle is close to ideal angles (0, π/2, π, 3π/2) + const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2) + const tolerance = 0.01 // Tolerance for considering an angle "ideal" + + if (Math.abs(normalizedAngle) < tolerance || + Math.abs(normalizedAngle - Math.PI / 2) < tolerance || + Math.abs(normalizedAngle - Math.PI) < tolerance || + Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) { + return angle + offset + } + + return angle + } +} diff --git a/renderer/viewer/three/documentRenderer.ts b/renderer/viewer/three/documentRenderer.ts new file mode 100644 index 00000000..a5dc060d --- /dev/null +++ b/renderer/viewer/three/documentRenderer.ts @@ -0,0 +1,328 @@ +import * as THREE from 'three' +import Stats from 'stats.js' +import StatsGl from 'stats-gl' +import * as tween from '@tweenjs/tween.js' +import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appViewer' +import { WorldRendererConfig } from '../lib/worldrendererCommon' + +export class DocumentRenderer { + canvas: HTMLCanvasElement | OffscreenCanvas + readonly renderer: THREE.WebGLRenderer + private animationFrameId?: number + private timeoutId?: number + private lastRenderTime = 0 + + private previousCanvasWidth = 0 + private previousCanvasHeight = 0 + private currentWidth = 0 + private currentHeight = 0 + + private renderedFps = 0 + private fpsInterval: any + private readonly stats: TopRightStats | undefined + private paused = false + disconnected = false + preRender = () => { } + render = (sizeChanged: boolean) => { } + postRender = () => { } + sizeChanged = () => { } + droppedFpsPercentage: number + config: GraphicsBackendConfig + onRender = [] as Array<(sizeChanged: boolean) => void> + inWorldRenderingConfig: WorldRendererConfig | undefined + + constructor (initOptions: GraphicsInitOptions, public externalCanvas?: OffscreenCanvas) { + this.config = initOptions.config + + // Handle canvas creation/transfer based on context + if (externalCanvas) { + this.canvas = externalCanvas + } else { + this.addToPage() + } + + try { + this.renderer = new THREE.WebGLRenderer({ + canvas: this.canvas, + preserveDrawingBuffer: true, + logarithmicDepthBuffer: true, + powerPreference: this.config.powerPreference + }) + } catch (err) { + initOptions.callbacks.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)) + throw err + } + this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace + if (!externalCanvas) { + this.updatePixelRatio() + } + this.sizeUpdated() + // Initialize previous dimensions + this.previousCanvasWidth = this.canvas.width + this.previousCanvasHeight = this.canvas.height + + const supportsWebGL2 = 'WebGL2RenderingContext' in window + // Only initialize stats and DOM-related features in main thread + if (!externalCanvas && supportsWebGL2) { + this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible) + this.setupFpsTracking() + } + + this.startRenderLoop() + } + + updatePixelRatio () { + let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable + if (!this.renderer.capabilities.isWebGL2) { + pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped) + } + this.renderer.setPixelRatio(pixelRatio) + } + + sizeUpdated () { + this.renderer.setSize(this.currentWidth, this.currentHeight, false) + } + + private addToPage () { + this.canvas = addCanvasToPage() + this.updateCanvasSize() + } + + updateSizeExternal (newWidth: number, newHeight: number, pixelRatio: number) { + this.currentWidth = newWidth + this.currentHeight = newHeight + this.renderer.setPixelRatio(pixelRatio) + this.sizeUpdated() + } + + private updateCanvasSize () { + if (!this.externalCanvas) { + const innnerWidth = window.innerWidth + const innnerHeight = window.innerHeight + if (this.currentWidth !== innnerWidth) { + this.currentWidth = innnerWidth + } + if (this.currentHeight !== innnerHeight) { + this.currentHeight = innnerHeight + } + } + } + + private setupFpsTracking () { + let max = 0 + this.fpsInterval = setInterval(() => { + if (max > 0) { + this.droppedFpsPercentage = this.renderedFps / max + } + max = Math.max(this.renderedFps, max) + this.renderedFps = 0 + }, 1000) + } + + private startRenderLoop () { + const animate = () => { + if (this.disconnected) return + + if (this.config.timeoutRendering) { + this.timeoutId = setTimeout(animate, this.config.fpsLimit ? 1000 / this.config.fpsLimit : 0) as unknown as number + } else { + this.animationFrameId = requestAnimationFrame(animate) + } + + if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return + + // Handle FPS limiting + if (this.config.fpsLimit) { + const now = performance.now() + const elapsed = now - this.lastRenderTime + const fpsInterval = 1000 / this.config.fpsLimit + + if (elapsed < fpsInterval) { + return + } + + this.lastRenderTime = now - (elapsed % fpsInterval) + } + + let sizeChanged = false + this.updateCanvasSize() + if (this.previousCanvasWidth !== this.currentWidth || this.previousCanvasHeight !== this.currentHeight) { + this.previousCanvasWidth = this.currentWidth + this.previousCanvasHeight = this.currentHeight + this.sizeUpdated() + sizeChanged = true + } + + this.frameRender(sizeChanged) + + // Update stats visibility each frame (main thread only) + if (this.config.statsVisible !== undefined) { + this.stats?.setVisibility(this.config.statsVisible) + } + } + + animate() + } + + frameRender (sizeChanged: boolean) { + this.preRender() + this.stats?.markStart() + tween.update() + if (!globalThis.freezeRender) { + this.render(sizeChanged) + } + for (const fn of this.onRender) { + fn(sizeChanged) + } + this.renderedFps++ + this.stats?.markEnd() + this.postRender() + } + + setPaused (paused: boolean) { + this.paused = paused + } + + dispose () { + this.disconnected = true + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId) + } + if (this.timeoutId) { + clearTimeout(this.timeoutId) + } + if (this.canvas instanceof HTMLCanvasElement) { + this.canvas.remove() + } + clearInterval(this.fpsInterval) + this.stats?.dispose() + this.renderer.dispose() + } +} + +class TopRightStats { + private readonly stats: Stats + private readonly stats2: Stats + private readonly statsGl: StatsGl + private total = 0 + private readonly denseMode: boolean + + constructor (private readonly canvas: HTMLCanvasElement, initialStatsVisible = 0) { + this.stats = new Stats() + this.stats2 = new Stats() + this.statsGl = new StatsGl({ minimal: true }) + this.stats2.showPanel(2) + this.denseMode = process.env.NODE_ENV === 'production' || window.innerHeight < 500 + + this.initStats() + this.setVisibility(initialStatsVisible) + } + + private addStat (dom: HTMLElement, size = 80) { + dom.style.position = 'absolute' + if (this.denseMode) dom.style.height = '12px' + dom.style.overflow = 'hidden' + dom.style.left = '' + dom.style.top = '0' + dom.style.right = `${this.total}px` + dom.style.width = '80px' + dom.style.zIndex = '1' + dom.style.opacity = '0.8' + document.body.appendChild(dom) + this.total += size + } + + private initStats () { + const hasRamPanel = this.stats2.dom.children.length === 3 + + this.addStat(this.stats.dom) + if (process.env.NODE_ENV === 'development' && document.exitPointerLock) { + this.stats.dom.style.top = '' + this.stats.dom.style.bottom = '0' + } + if (hasRamPanel) { + this.addStat(this.stats2.dom) + } + + this.statsGl.init(this.canvas) + this.statsGl.container.style.display = 'flex' + this.statsGl.container.style.justifyContent = 'flex-end' + + let i = 0 + for (const _child of this.statsGl.container.children) { + const child = _child as HTMLElement + if (i++ === 0) { + child.style.display = 'none' + } + child.style.position = '' + } + } + + setVisibility (level: number) { + const visible = level > 0 + if (visible) { + this.stats.dom.style.display = 'block' + this.stats2.dom.style.display = level >= 2 ? 'block' : 'none' + this.statsGl.container.style.display = level >= 2 ? 'block' : 'none' + } else { + this.stats.dom.style.display = 'none' + this.stats2.dom.style.display = 'none' + this.statsGl.container.style.display = 'none' + } + } + + markStart () { + this.stats.begin() + this.stats2.begin() + this.statsGl.begin() + } + + markEnd () { + this.stats.end() + this.stats2.end() + this.statsGl.end() + } + + dispose () { + this.stats.dom.remove() + this.stats2.dom.remove() + this.statsGl.container.remove() + } +} + +const addCanvasToPage = () => { + const canvas = document.createElement('canvas') + canvas.id = 'viewer-canvas' + document.body.appendChild(canvas) + return canvas +} + +export const addCanvasForWorker = () => { + const canvas = addCanvasToPage() + const transferred = canvas.transferControlToOffscreen() + let removed = false + let onSizeChanged = (w, h) => { } + let oldSize = { width: 0, height: 0 } + const checkSize = () => { + if (removed) return + if (oldSize.width !== window.innerWidth || oldSize.height !== window.innerHeight) { + onSizeChanged(window.innerWidth, window.innerHeight) + oldSize = { width: window.innerWidth, height: window.innerHeight } + } + requestAnimationFrame(checkSize) + } + requestAnimationFrame(checkSize) + return { + canvas: transferred, + destroy () { + removed = true + canvas.remove() + }, + onSizeChanged (cb: (width: number, height: number) => void) { + onSizeChanged = cb + }, + get size () { + return { width: window.innerWidth, height: window.innerHeight } + } + } +} diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts new file mode 100644 index 00000000..fad30182 --- /dev/null +++ b/renderer/viewer/three/entities.ts @@ -0,0 +1,1493 @@ +//@ts-check +import { UnionToIntersection } from 'type-fest' +import nbt from 'prismarine-nbt' +import * as TWEEN from '@tweenjs/tween.js' +import * as THREE from 'three' +import { PlayerAnimation, PlayerObject } from 'skinview3d' +import { inferModelType, loadCapeToCanvas, loadEarsToCanvasFromSkin } from 'skinview-utils' +// todo replace with url +import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils' +import { NameTagObject } from 'skinview3d/libs/nametag' +import { flat, fromFormattedString } from '@xmcl/text-component' +import mojangson from 'mojangson' +import { snakeCase } from 'change-case' +import { Item } from 'prismarine-item' +import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity' +import { Team } from 'mineflayer' +import PrismarineChatLoader from 'prismarine-chat' +import { EntityMetadataVersions } from '../../../src/mcDataTypes' +import { ItemSpecificContextProperties } from '../lib/basePlayerState' +import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins' +import { renderComponent } from '../sign-renderer' +import { createCanvas } from '../lib/utils' +import { PlayerObjectType } from '../lib/createPlayerObject' +import { getBlockMeshFromModel } from './holdingBlock' +import { createItemMesh } from './itemMesh' +import * as Entity from './entity/EntityMesh' +import { getMesh } from './entity/EntityMesh' +import { WalkingGeneralSwing } from './entity/animations' +import { disposeObject, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils' +import { armorModel, armorTextures, elytraTexture } from './entity/armorModels' +import { WorldRendererThree } from './worldrendererThree' + +export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl) + +export const TWEEN_DURATION = 120 + +function convert2sComplementToHex (complement: number) { + if (complement < 0) { + complement = (0xFF_FF_FF_FF + complement + 1) >>> 0 + } + return complement.toString(16) +} + +function toRgba (color: string | undefined) { + if (color === undefined) { + return undefined + } + if (parseInt(color, 10) === 0) { + return 'rgba(0, 0, 0, 0)' + } + const hex = convert2sComplementToHex(parseInt(color, 10)) + if (hex.length === 8) { + return `#${hex.slice(2, 8)}${hex.slice(0, 2)}` + } else { + return `#${hex}` + } +} + +function toQuaternion (quaternion: any, defaultValue?: THREE.Quaternion) { + if (quaternion === undefined) { + return defaultValue + } + if (quaternion instanceof THREE.Quaternion) { + return quaternion + } + if (Array.isArray(quaternion)) { + return new THREE.Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3]) + } + return new THREE.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w) +} + +function poseToEuler (pose: any, defaultValue?: THREE.Euler) { + if (pose === undefined) { + return defaultValue ?? new THREE.Euler() + } + if (pose instanceof THREE.Euler) { + return pose + } + if (pose['yaw'] !== undefined && pose['pitch'] !== undefined && pose['roll'] !== undefined) { + // Convert Minecraft pitch, yaw, roll definitions to our angle system + return new THREE.Euler(-degreesToRadians(pose.pitch), -degreesToRadians(pose.yaw), degreesToRadians(pose.roll), 'ZYX') + } + if (pose['x'] !== undefined && pose['y'] !== undefined && pose['z'] !== undefined) { + return new THREE.Euler(pose.z, pose.y, pose.x, 'ZYX') + } + if (Array.isArray(pose)) { + return new THREE.Euler(pose[0], pose[1], pose[2]) + } + return defaultValue ?? new THREE.Euler() +} + +function getUsernameTexture ({ + username, + nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', + nameTagTextOpacity = 255 +}: any, { fontFamily = 'mojangles' }: any, version: string) { + const canvas = createCanvas(64, 64) + + const PrismarineChat = PrismarineChatLoader(version) + + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Could not get 2d context') + + const fontSize = 48 + const padding = 5 + ctx.font = `${fontSize}px ${fontFamily}` + + const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n') + let textWidth = 0 + for (const line of plainLines) { + const width = ctx.measureText(line).width + padding * 2 + if (width > textWidth) textWidth = width + } + + canvas.width = textWidth + canvas.height = (fontSize + padding) * plainLines.length + + ctx.fillStyle = nameTagBackgroundColor + ctx.fillRect(0, 0, canvas.width, canvas.height) + + ctx.globalAlpha = nameTagTextOpacity / 255 + + renderComponent(username, PrismarineChat, canvas, fontSize, 'white', -padding + fontSize) + + ctx.globalAlpha = 1 + + return canvas +} + +const addNametag = (entity, options: { fontFamily: string }, mesh, version: string) => { + for (const c of mesh.children) { + if (c.name === 'nametag') { + c.removeFromParent() + } + } + if (entity.username !== undefined) { + const canvas = getUsernameTexture(entity, options, version) + const tex = new THREE.Texture(canvas) + tex.needsUpdate = true + let nameTag: THREE.Object3D + if (entity.nameTagFixed) { + const geometry = new THREE.PlaneGeometry() + const material = new THREE.MeshBasicMaterial({ map: tex }) + material.transparent = true + nameTag = new THREE.Mesh(geometry, material) + nameTag.rotation.set(entity.pitch, THREE.MathUtils.degToRad(entity.yaw + 180), 0) + nameTag.position.y += entity.height + 0.3 + } else { + const spriteMat = new THREE.SpriteMaterial({ map: tex }) + nameTag = new THREE.Sprite(spriteMat) + nameTag.position.y += entity.height + 0.6 + } + nameTag.renderOrder = 1000 + nameTag.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1) + if (entity.nameTagRotationRight) { + nameTag.applyQuaternion(entity.nameTagRotationRight) + } + if (entity.nameTagScale) { + nameTag.scale.multiply(entity.nameTagScale) + } + if (entity.nameTagRotationLeft) { + nameTag.applyQuaternion(entity.nameTagRotationLeft) + } + if (entity.nameTagTranslation) { + nameTag.position.add(entity.nameTagTranslation) + } + nameTag.name = 'nametag' + + mesh.add(nameTag) + return nameTag + } +} + +// todo cleanup +const nametags = {} + +const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase() + +function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree, options: { fontFamily: string }, overrides) { + if (entity.name) { + try { + // https://github.com/PrismarineJS/prismarine-viewer/pull/410 + const entityName = (isFirstUpperCase(entity.name) ? snakeCase(entity.name) : entity.name).toLowerCase() + const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides) + + if (e.mesh) { + addNametag(entity, options, e.mesh, world.version) + return e.mesh + } + } catch (err) { + reportError?.(err) + } + } + + if (!isEntityAttackable(loadedData, entity)) return + const geometry = new THREE.BoxGeometry(entity.width, entity.height, entity.width) + geometry.translate(0, entity.height / 2, 0) + const material = new THREE.MeshBasicMaterial({ color: 0xff_00_ff }) + const cube = new THREE.Mesh(geometry, material) + const nametagCount = (nametags[entity.name] = (nametags[entity.name] || 0) + 1) + if (nametagCount < 6) { + addNametag({ + username: entity.name, + height: entity.height, + }, options, cube, world.version) + } + return cube +} + +export type SceneEntity = THREE.Object3D & { + playerObject?: PlayerObjectType + username?: string + uuid?: string + additionalCleanup?: () => void + originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name, team?: Team } +} + +export class Entities { + entities = {} as Record + playerEntity: SceneEntity | null = null // Special entity for the player in third person + entitiesOptions = { + fontFamily: 'mojangles' + } + debugMode: string + onSkinUpdate: () => void + clock = new THREE.Clock() + currentlyRendering = true + cachedMapsImages = {} as Record + itemFrameMaps = {} as Record>> + + get entitiesByName (): Record { + const byName: Record = {} + for (const entity of Object.values(this.entities)) { + if (!entity['realName']) continue + byName[entity['realName']] = byName[entity['realName']] || [] + byName[entity['realName']].push(entity) + } + return byName + } + + get entitiesRenderingCount (): number { + return Object.values(this.entities).filter(entity => entity.visible).length + } + + getDebugString (): string { + const totalEntities = Object.keys(this.entities).length + const visibleEntities = this.entitiesRenderingCount + + const playerEntities = Object.values(this.entities).filter(entity => entity.playerObject) + const visiblePlayerEntities = playerEntities.filter(entity => entity.visible) + + return `${visibleEntities}/${totalEntities} ${visiblePlayerEntities.length}/${playerEntities.length}` + } + + constructor (public worldRenderer: WorldRendererThree) { + this.debugMode = 'none' + this.onSkinUpdate = () => { } + this.watchResourcesUpdates() + } + + handlePlayerEntity (playerData: SceneEntity['originalEntity']) { + // Create player entity if it doesn't exist + if (!this.playerEntity) { + // Create the player entity similar to how normal entities are created + const group = new THREE.Group() as unknown as SceneEntity + group.originalEntity = { ...playerData, name: 'player' } as SceneEntity['originalEntity'] + + const wrapper = new THREE.Group() + const playerObject = this.setupPlayerObject(playerData, wrapper, {}) + group.playerObject = playerObject + group.add(wrapper) + + group.name = 'player_entity' + this.playerEntity = group + this.worldRenderer.scene.add(group) + + void this.updatePlayerSkin(playerData.id, playerData.username, playerData.uuid ?? undefined, stevePngUrl) + } + + // Update position and rotation + if (playerData.position) { + this.playerEntity.position.set(playerData.position.x, playerData.position.y, playerData.position.z) + } + if (playerData.yaw !== undefined) { + this.playerEntity.rotation.y = playerData.yaw + } + + this.updateEntityEquipment(this.playerEntity, playerData) + } + + clear () { + for (const mesh of Object.values(this.entities)) { + this.worldRenderer.scene.remove(mesh) + disposeObject(mesh) + } + this.entities = {} + + // Clean up player entity + if (this.playerEntity) { + this.worldRenderer.scene.remove(this.playerEntity) + disposeObject(this.playerEntity) + this.playerEntity = null + } + } + + reloadEntities () { + for (const entity of Object.values(this.entities)) { + // update all entities textures like held items, armour, etc + // todo update entity textures itself + this.update({ ...entity.originalEntity, delete: true, } as SceneEntity['originalEntity'], {}) + this.update(entity.originalEntity, {}) + } + } + + watchResourcesUpdates () { + this.worldRenderer.resourcesManager.on('assetsTexturesUpdated', () => this.reloadEntities()) + this.worldRenderer.resourcesManager.on('assetsInventoryReady', () => this.reloadEntities()) + } + + setDebugMode (mode: string, entity: THREE.Object3D | null = null) { + this.debugMode = mode + for (const mesh of entity ? [entity] : Object.values(this.entities)) { + const boxHelper = mesh.children.find(c => c.name === 'debug')! + boxHelper.visible = false + if (this.debugMode === 'basic') { + boxHelper.visible = true + } + // todo advanced + } + } + + setRendering (rendering: boolean, entity: THREE.Object3D | null = null) { + this.currentlyRendering = rendering + for (const ent of entity ? [entity] : Object.values(this.entities)) { + if (rendering) { + if (!this.worldRenderer.scene.children.includes(ent)) this.worldRenderer.scene.add(ent) + } else { + this.worldRenderer.scene.remove(ent) + } + } + } + + render () { + const renderEntitiesConfig = this.worldRenderer.worldRendererConfig.renderEntities + if (renderEntitiesConfig !== this.currentlyRendering) { + this.setRendering(renderEntitiesConfig) + } + + const dt = this.clock.getDelta() + const botPos = this.worldRenderer.viewerChunkPosition + const VISIBLE_DISTANCE = 10 * 10 + + // Update regular entities + for (const [entityId, entity] of [...Object.entries(this.entities), ['player_entity', this.playerEntity] as [string, SceneEntity | null]]) { + if (!entity) continue + const { playerObject } = entity + + // Update animations + if (playerObject?.animation) { + playerObject.animation.update(playerObject, dt) + } + + // Update visibility based on distance and chunk load status + if (botPos && entity.position) { + const dx = entity.position.x - botPos.x + const dy = entity.position.y - botPos.y + const dz = entity.position.z - botPos.z + const distanceSquared = dx * dx + dy * dy + dz * dz + + // Entity is visible if within 20 blocks OR in a finished chunk + entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity)) + + this.maybeRenderPlayerSkin(entityId) + } + + if (entity.visible) { + // Update armor positions + this.syncArmorPositions(entity) + } + + if (entityId === 'player_entity') { + entity.visible = this.worldRenderer.playerStateUtils.isThirdPerson() + + if (entity.visible) { + // sync + const yOffset = this.worldRenderer.playerStateReactive.eyeHeight + const pos = this.worldRenderer.cameraObject.position.clone().add(new THREE.Vector3(0, -yOffset, 0)) + entity.position.set(pos.x, pos.y, pos.z) + + const rotation = this.worldRenderer.cameraShake.getBaseRotation() + entity.rotation.set(0, rotation.yaw, 0) + + // Sync head rotation + entity.traverse((c) => { + if (c.name === 'head') { + c.rotation.set(-rotation.pitch, 0, 0) + } + }) + } + } + } + } + + private syncArmorPositions (entity: SceneEntity) { + if (!entity.playerObject) return + + // todo-low use property access for less loop iterations (small performance gain) + entity.traverse((armor) => { + if (!armor.name.startsWith('geometry_armor_')) return + + const { skin } = entity.playerObject! + + switch (armor.name) { + case 'geometry_armor_head': + // Head armor sync + if (armor.children[0]?.children[0]) { + armor.children[0].children[0].rotation.set( + -skin.head.rotation.x, + skin.head.rotation.y, + skin.head.rotation.z, + skin.head.rotation.order + ) + } + break + + case 'geometry_armor_legs': + // Legs armor sync + if (armor.children[0]) { + // Left leg + if (armor.children[0].children[2]) { + armor.children[0].children[2].rotation.set( + -skin.leftLeg.rotation.x, + skin.leftLeg.rotation.y, + skin.leftLeg.rotation.z, + skin.leftLeg.rotation.order + ) + } + // Right leg + if (armor.children[0].children[1]) { + armor.children[0].children[1].rotation.set( + -skin.rightLeg.rotation.x, + skin.rightLeg.rotation.y, + skin.rightLeg.rotation.z, + skin.rightLeg.rotation.order + ) + } + } + break + + case 'geometry_armor_feet': + // Boots armor sync + if (armor.children[0]) { + // Right boot + if (armor.children[0].children[0]) { + armor.children[0].children[0].rotation.set( + -skin.rightLeg.rotation.x, + skin.rightLeg.rotation.y, + skin.rightLeg.rotation.z, + skin.rightLeg.rotation.order + ) + } + // Left boot (reversed Z rotation) + if (armor.children[0].children[1]) { + armor.children[0].children[1].rotation.set( + -skin.leftLeg.rotation.x, + skin.leftLeg.rotation.y, + -skin.leftLeg.rotation.z, + skin.leftLeg.rotation.order + ) + } + } + break + } + }) + } + + getPlayerObject (entityId: string | number) { + if (this.playerEntity?.originalEntity.id === entityId) return this.playerEntity?.playerObject + const playerObject = this.entities[entityId]?.playerObject + return playerObject + } + + uuidPerSkinUrlsCache = {} as Record + + private isCanvasBlank (canvas: HTMLCanvasElement): boolean { + return !canvas.getContext('2d') + ?.getImageData(0, 0, canvas.width, canvas.height).data + .some(channel => channel !== 0) + } + + // todo true/undefined doesnt reset the skin to the default one + // eslint-disable-next-line max-params + async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) { + const isCustomSkin = skinUrl !== stevePngUrl + if (isCustomSkin) { + this.loadedSkinEntityIds.add(String(entityId)) + } + if (uuidCache) { + if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {} + if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl + if (typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].capeUrl = capeUrl + if (skinUrl === true) { + skinUrl = this.uuidPerSkinUrlsCache[uuidCache]?.skinUrl ?? skinUrl + } + capeUrl ??= this.uuidPerSkinUrlsCache[uuidCache]?.capeUrl + } + + const playerObject = this.getPlayerObject(entityId) + if (!playerObject) return + + if (skinUrl === true) { + if (!username) return + const newSkinUrl = await loadSkinFromUsername(username, 'skin') + if (!this.getPlayerObject(entityId)) return + if (!newSkinUrl) return + skinUrl = newSkinUrl + } + + if (typeof skinUrl !== 'string') throw new Error('Invalid skin url') + const renderEars = this.worldRenderer.worldRendererConfig.renderEars || username === 'deadmau5' + void this.loadAndApplySkin(entityId, skinUrl, renderEars).then(async () => { + if (capeUrl) { + if (capeUrl === true && username) { + const newCapeUrl = await loadSkinFromUsername(username, 'cape') + if (!this.getPlayerObject(entityId)) return + if (!newCapeUrl) return + capeUrl = newCapeUrl + } + if (typeof capeUrl === 'string') { + void this.loadAndApplyCape(entityId, capeUrl) + } + } + }) + + + playerObject.cape.visible = false + if (!capeUrl) { + playerObject.backEquipment = null + playerObject.elytra.map = null + if (playerObject.cape.map) { + playerObject.cape.map.dispose() + } + playerObject.cape.map = null + } + } + + private async loadAndApplySkin (entityId: string | number, skinUrl: string, renderEars: boolean) { + let playerObject = this.getPlayerObject(entityId) + if (!playerObject) return + + try { + let playerCustomSkinImage: ImageBitmap | undefined + + playerObject = this.getPlayerObject(entityId) + if (!playerObject) return + + let skinTexture: THREE.Texture + let skinCanvas: OffscreenCanvas + if (skinUrl === stevePngUrl) { + skinTexture = await steveTexture + const canvas = createCanvas(64, 64) + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Failed to get context') + ctx.drawImage(skinTexture.image, 0, 0) + skinCanvas = canvas + } else { + const { canvas, image } = await loadSkinImage(skinUrl) + playerCustomSkinImage = image + skinTexture = new THREE.CanvasTexture(canvas) + skinCanvas = canvas + } + + skinTexture.magFilter = THREE.NearestFilter + skinTexture.minFilter = THREE.NearestFilter + skinTexture.needsUpdate = true + playerObject.skin.map = skinTexture as any + playerObject.skin.modelType = inferModelType(skinCanvas) + + let earsCanvas: HTMLCanvasElement | undefined + if (!playerCustomSkinImage) { + renderEars = false + } else if (renderEars) { + earsCanvas = document.createElement('canvas') + loadEarsToCanvasFromSkin(earsCanvas, playerCustomSkinImage) + renderEars = !this.isCanvasBlank(earsCanvas) + } + if (renderEars) { + const earsTexture = new THREE.CanvasTexture(earsCanvas!) + earsTexture.magFilter = THREE.NearestFilter + earsTexture.minFilter = THREE.NearestFilter + earsTexture.needsUpdate = true + //@ts-expect-error + playerObject.ears.map = earsTexture + playerObject.ears.visible = true + } else { + playerObject.ears.map = null + playerObject.ears.visible = false + } + this.onSkinUpdate?.() + } catch (error) { + console.error('Error loading skin:', error) + } + } + + private async loadAndApplyCape (entityId: string | number, capeUrl: string) { + let playerObject = this.getPlayerObject(entityId) + if (!playerObject) return + + try { + const { canvas: capeCanvas, image: capeImage } = await loadSkinImage(capeUrl) + + playerObject = this.getPlayerObject(entityId) + if (!playerObject) return + + loadCapeToCanvas(capeCanvas, capeImage) + const capeTexture = new THREE.CanvasTexture(capeCanvas) + capeTexture.magFilter = THREE.NearestFilter + capeTexture.minFilter = THREE.NearestFilter + capeTexture.needsUpdate = true + //@ts-expect-error + playerObject.cape.map = capeTexture + playerObject.cape.visible = true + //@ts-expect-error + playerObject.elytra.map = capeTexture + this.onSkinUpdate?.() + + if (!playerObject.backEquipment) { + playerObject.backEquipment = 'cape' + } + } catch (error) { + console.error('Error loading cape:', error) + } + } + + debugSwingArm () { + const playerObject = Object.values(this.entities).find(entity => entity.playerObject?.animation instanceof WalkingGeneralSwing) + if (!playerObject) return + (playerObject.playerObject!.animation as WalkingGeneralSwing).swingArm() + } + + playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') { + // TODO CLEANUP! + // Handle special player entity ID for bot entity in third person + if (entityPlayerId === 'player_entity' && this.playerEntity?.playerObject) { + const { playerObject } = this.playerEntity + if (animation === 'oneSwing') { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.swingArm() + return + } + + if (playerObject.animation instanceof WalkingGeneralSwing) { + playerObject.animation.switchAnimationCallback = () => { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' + playerObject.animation.isRunning = animation === 'running' + playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + } + } + return + } + + // Handle regular entities + const playerObject = this.getPlayerObject(entityPlayerId) + if (playerObject) { + if (animation === 'oneSwing') { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.swingArm() + return + } + + if (playerObject.animation instanceof WalkingGeneralSwing) { + playerObject.animation.switchAnimationCallback = () => { + if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' + playerObject.animation.isRunning = animation === 'running' + playerObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + } + } + return + } + + // Handle player entity (for third person view) - fallback for backwards compatibility + if (this.playerEntity?.playerObject) { + const { playerObject: playerEntityObject } = this.playerEntity + if (animation === 'oneSwing') { + if (!(playerEntityObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerEntityObject.animation.swingArm() + return + } + + if (playerEntityObject.animation instanceof WalkingGeneralSwing) { + playerEntityObject.animation.switchAnimationCallback = () => { + if (!(playerEntityObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing') + playerEntityObject.animation.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking' + playerEntityObject.animation.isRunning = animation === 'running' + playerEntityObject.animation.isCrouched = animation === 'crouch' || animation === 'crouchWalking' + } + } + } + } + + parseEntityLabel (jsonLike) { + if (!jsonLike) return + try { + if (jsonLike.type === 'string') { + return jsonLike.value + } + const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike) + const text = flat(parsed).map(this.textFromComponent) + return text.join('') + } catch (err) { + return jsonLike + } + } + + private textFromComponent (component) { + return typeof component === 'string' ? component : component.text ?? '' + } + + getItemMesh (item, specificProps: ItemSpecificContextProperties, faceCamera = false, previousModel?: string) { + if (!item.nbt && item.nbtData) item.nbt = item.nbtData + const textureUv = this.worldRenderer.getItemRenderData(item, specificProps) + if (previousModel && previousModel === textureUv?.modelName) return undefined + + if (textureUv && 'resolvedModel' in textureUv) { + const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources.worldBlockProvider!) + let SCALE = 1 + if (specificProps['minecraft:display_context'] === 'ground') { + SCALE = 0.5 + } else if (specificProps['minecraft:display_context'] === 'thirdperson') { + SCALE = 6 + } + mesh.scale.set(SCALE, SCALE, SCALE) + const outerGroup = new THREE.Group() + outerGroup.add(mesh) + return { + mesh: outerGroup, + isBlock: true, + modelName: textureUv.modelName, + } + } + + // Render proper 3D model for items + if (textureUv) { + const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture + const { u, v, su, sv } = textureUv + const sizeX = su ?? 1 // su is actually width + const sizeY = sv ?? 1 // sv is actually height + + // Use the new unified item mesh function + const result = createItemMesh(textureThree, { + u, + v, + sizeX, + sizeY + }, { + faceCamera, + use3D: !faceCamera, // Only use 3D for non-camera-facing items + }) + + let SCALE = 1 + if (specificProps['minecraft:display_context'] === 'ground') { + SCALE = 0.5 + } else if (specificProps['minecraft:display_context'] === 'thirdperson') { + SCALE = 6 + } + result.mesh.scale.set(SCALE, SCALE, SCALE) + + return { + mesh: result.mesh, + isBlock: false, + modelName: textureUv.modelName, + cleanup: result.cleanup + } + } + } + + setVisible (mesh: THREE.Object3D, visible: boolean) { + //mesh.visible = visible + //TODO: Fix workaround for visibility setting + if (visible) { + mesh.scale.set(1, 1, 1) + } else { + mesh.scale.set(0, 0, 0) + } + } + + update (entity: SceneEntity['originalEntity'], overrides) { + const isPlayerModel = entity.name === 'player' + if (entity.name === 'zombie_villager' || entity.name === 'husk') { + overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}` + } + if (entity.name === 'glow_item_frame') { + if (!overrides.textures) overrides.textures = [] + overrides.textures['background'] = 'block:glow_item_frame' + } + // this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted) + let e = this.entities[entity.id] + const justAdded = !e + + if (entity.delete) { + if (!e) return + if (e.additionalCleanup) e.additionalCleanup() + e.traverse(c => { + if (c['additionalCleanup']) c['additionalCleanup']() + }) + this.onRemoveEntity(entity) + this.worldRenderer.scene.remove(e) + disposeObject(e) + // todo dispose textures as well ? + delete this.entities[entity.id] + return + } + + let mesh: THREE.Object3D | undefined + if (e === undefined) { + const group = new THREE.Group() as unknown as SceneEntity + group.originalEntity = entity + if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block' || entity.name === 'snowball' + || entity.name === 'egg' || entity.name === 'ender_pearl' || entity.name === 'experience_bottle' + || entity.name === 'splash_potion' || entity.name === 'lingering_potion') { + const item = entity.name === 'tnt' || entity.type === 'projectile' + ? { name: entity.name } + : entity.name === 'falling_block' + ? { blockState: entity['objectData'] } + : entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount) + if (item) { + const object = this.getItemMesh(item, { + 'minecraft:display_context': 'ground', + }, entity.type === 'projectile') + if (object) { + mesh = object.mesh + if (entity.name === 'item' || entity.type === 'projectile') { + mesh.scale.set(0.5, 0.5, 0.5) + mesh.position.set(0, entity.name === 'item' ? 0.2 : 0.1, 0) + } else { + mesh.scale.set(2, 2, 2) + mesh.position.set(0, 0.5, 0) + } + // set faces + // mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5) + // viewer.scene.add(mesh) + if (entity.name === 'item') { + const clock = new THREE.Clock() + mesh.onBeforeRender = () => { + const delta = clock.getDelta() + mesh!.rotation.y += delta + } + } + + // TNT blinking + // if (entity.name === 'tnt') { + // let lastBlink = 0 + // const blinkInterval = 500 // ms between blinks + // mesh.onBeforeRender = () => { + // const now = Date.now() + // if (now - lastBlink > blinkInterval) { + // lastBlink = now + // mesh.traverse((child) => { + // if (child instanceof THREE.Mesh) { + // const material = child.material as THREE.MeshLambertMaterial + // material.color.set(material.color?.equals(new THREE.Color(0xff_ff_ff)) + // ? new THREE.Color(0xff_00_00) + // : new THREE.Color(0xff_ff_ff)) + // } + // }) + // } + // } + // } + + group.additionalCleanup = () => { + // important: avoid texture memory leak and gpu slowdown + if (object.cleanup) { + object.cleanup() + } + } + } + } + } else if (isPlayerModel) { + const wrapper = new THREE.Group() + const playerObject = this.setupPlayerObject(entity, wrapper, overrides) + group.playerObject = playerObject + mesh = wrapper + + if (entity.username) { + const nametag = addNametag(entity, { fontFamily: 'mojangles' }, wrapper, this.worldRenderer.version) + if (nametag) { + nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3 + nametag.scale.multiplyScalar(12) + } + } + } else { + mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] }) + } + if (!mesh) return + mesh.name = 'mesh' + // set initial position so there are no weird jumps update after + const pos = entity.pos ?? entity.position + group.position.set(pos.x, pos.y, pos.z) + + // todo use width and height instead + const boxHelper = new THREE.BoxHelper( + mesh, + entity.type === 'hostile' ? 0xff_00_00 : + entity.type === 'mob' ? 0x00_ff_00 : + entity.type === 'player' ? 0x00_00_ff : + 0xff_a5_00, + ) + boxHelper.name = 'debug' + group.add(mesh) + group.add(boxHelper) + boxHelper.visible = false + this.worldRenderer.scene.add(group) + + e = group + e.name = 'entity' + e['realName'] = entity.name + this.entities[entity.id] = e + + this.onAddEntity(entity) + + if (isPlayerModel) { + void this.updatePlayerSkin(entity.id, entity.username, overrides?.texture ? entity.uuid : undefined, overrides?.texture || stevePngUrl) + } + this.setDebugMode(this.debugMode, group) + this.setRendering(this.currentlyRendering, group) + } else { + mesh = e.children.find(c => c.name === 'mesh') + } + + // Update equipment + this.updateEntityEquipment(e, entity) + + const meta = getGeneralEntitiesMetadata(entity) + + const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator()) + for (const child of mesh!.children ?? []) { + if (child.name !== 'nametag') { + child.visible = !isInvisible + } + } + // --- + // set baby size + if (meta.baby) { + e.scale.set(0.5, 0.5, 0.5) + } else { + e.scale.set(1, 1, 1) + } + // entity specific meta + const textDisplayMeta = getSpecificEntityMetadata('text_display', entity) + const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name + if (entity.name !== 'player' && displayTextRaw) { + const nameTagFixed = textDisplayMeta && (textDisplayMeta.billboard_render_constraints === 'fixed' || !textDisplayMeta.billboard_render_constraints) + const nameTagBackgroundColor = (textDisplayMeta && (parseInt(textDisplayMeta.style_flags, 10) & 0x04) === 0) ? toRgba(textDisplayMeta.background_color) : undefined + let nameTagTextOpacity: any + if (textDisplayMeta?.text_opacity) { + const rawOpacity = parseInt(textDisplayMeta?.text_opacity, 10) + nameTagTextOpacity = rawOpacity > 0 ? rawOpacity : 256 - rawOpacity + } + addNametag( + { ...entity, username: typeof displayTextRaw === 'string' ? mojangson.simplify(mojangson.parse(displayTextRaw)) : nbt.simplify(displayTextRaw), + nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed, + nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)), + nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation) }, + this.entitiesOptions, + mesh, + this.worldRenderer.version + ) + } + + const armorStandMeta = getSpecificEntityMetadata('armor_stand', entity) + if (armorStandMeta) { + const isSmall = (parseInt(armorStandMeta.client_flags, 10) & 0x01) !== 0 + const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0 + const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0 + const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0 + mesh!.castShadow = !isMarker + mesh!.receiveShadow = !isMarker + if (isSmall) { + e.scale.set(0.5, 0.5, 0.5) + } else { + e.scale.set(1, 1, 1) + } + e.traverse(c => { + switch (c.name) { + case 'bone_baseplate': + this.setVisible(c, hasBasePlate) + c.rotation.y = -e.rotation.y + break + case 'bone_head': + if (armorStandMeta.head_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.head_pose)) + } + break + case 'bone_body': + if (armorStandMeta.body_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.body_pose)) + } + break + case 'bone_rightarm': + if (c.parent?.name !== 'bone_armor') { + this.setVisible(c, hasArms) + } + if (armorStandMeta.left_arm_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.left_arm_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': -10, 'pitch': -10, 'roll': 0 })) + } + break + case 'bone_leftarm': + if (c.parent?.name !== 'bone_armor') { + this.setVisible(c, hasArms) + } + if (armorStandMeta.right_arm_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.right_arm_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': 10, 'pitch': -10, 'roll': 0 })) + } + break + case 'bone_rightleg': + if (armorStandMeta.left_leg_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.left_leg_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': -1, 'pitch': -1, 'roll': 0 })) + } + break + case 'bone_leftleg': + if (armorStandMeta.right_leg_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.right_leg_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': 1, 'pitch': 1, 'roll': 0 })) + } + break + } + }) + } + + // todo handle map, map_chunks events + let itemFrameMeta = getSpecificEntityMetadata('item_frame', entity) + if (!itemFrameMeta) { + itemFrameMeta = getSpecificEntityMetadata('glow_item_frame', entity) + } + if (itemFrameMeta) { + // TODO: fix type + // todo! fix errors in mc-data (no entities data prior 1.18.2) + const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } } + mesh!.scale.set(1, 1, 1) + mesh!.position.set(0, 0, -0.5) + + e.rotation.x = -entity.pitch + e.children.find(c => { + if (c.name.startsWith('map_')) { + disposeObject(c) + const existingMapNumber = parseInt(c.name.split('_')[1], 10) + this.itemFrameMaps[existingMapNumber] = this.itemFrameMaps[existingMapNumber]?.filter(mesh => mesh !== c) + if (c instanceof THREE.Mesh) { + c.material?.map?.dispose() + } + return true + } else if (c.name === 'item') { + disposeObject(c) + return true + } + return false + })?.removeFromParent() + + if (item && (item.itemId ?? item.blockId ?? 0) !== 0) { + // Get rotation from metadata, default to 0 if not present + // Rotation is stored in 45° increments (0-7) for items, 90° increments (0-3) for maps + const rotation = (itemFrameMeta.rotation as any as number) ?? 0 + const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data + if (mapNumber) { + // TODO: Use proper larger item frame model when a map exists + mesh!.scale.set(16 / 12, 16 / 12, 1) + // Handle map rotation (4 possibilities, 90° increments) + this.addMapModel(e, mapNumber, rotation) + } else { + // Handle regular item rotation (8 possibilities, 45° increments) + const itemMesh = this.getItemMesh(item, { + 'minecraft:display_context': 'fixed', + }) + if (itemMesh) { + itemMesh.mesh.position.set(0, 0, -0.05) + // itemMesh.mesh.position.set(0, 0, 0.43) + if (itemMesh.isBlock) { + itemMesh.mesh.scale.set(0.25, 0.25, 0.25) + } else { + itemMesh.mesh.scale.set(0.5, 0.5, 0.5) + } + // Rotate 180° around Y axis first + itemMesh.mesh.rotateY(Math.PI) + // Then apply the 45° increment rotation + itemMesh.mesh.rotateZ(-rotation * Math.PI / 4) + itemMesh.mesh.name = 'item' + e.add(itemMesh.mesh) + } + } + } + } + + if (entity.username !== undefined) { + e.username = entity.username + } + + this.updateNameTagVisibility(e) + + this.updateEntityPosition(entity, justAdded, overrides) + } + + updateEntityPosition (entity: import('prismarine-entity').Entity, justAdded: boolean, overrides: { rotation?: { head?: { y: number, x: number } } }) { + const e = this.entities[entity.id] + if (!e) return + const ANIMATION_DURATION = justAdded ? 0 : TWEEN_DURATION + if (entity.position) { + new TWEEN.Tween(e.position).to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION).start() + } + if (entity.yaw) { + const da = (entity.yaw - e.rotation.y) % (Math.PI * 2) + const dy = 2 * da % (Math.PI * 2) - da + new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, ANIMATION_DURATION).start() + } + + if (e?.playerObject && overrides?.rotation?.head) { + const { playerObject } = e + const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0 + playerObject.skin.head.rotation.y = -headRotationDiff + playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0 + } + } + + onAddEntity (entity: import('prismarine-entity').Entity) { + } + + loadedSkinEntityIds = new Set() + maybeRenderPlayerSkin (entityId: string) { + let mesh = this.entities[entityId] + if (entityId === 'player_entity') { + mesh = this.playerEntity! + entityId = this.playerEntity?.originalEntity.id as any + } + if (!mesh) return + if (!mesh.playerObject) return + if (!mesh.visible) return + + const MAX_DISTANCE_SKIN_LOAD = 128 + const cameraPos = this.worldRenderer.cameraObject.position + const distance = mesh.position.distanceTo(cameraPos) + if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) { + if (this.loadedSkinEntityIds.has(String(entityId))) return + void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true) + } + } + + playerPerAnimation = {} as Record + onRemoveEntity (entity: import('prismarine-entity').Entity) { + this.loadedSkinEntityIds.delete(entity.id.toString()) + } + + updateMap (mapNumber: string | number, data: string) { + this.cachedMapsImages[mapNumber] = data + let itemFrameMeshes = this.itemFrameMaps[mapNumber] + if (!itemFrameMeshes) return + itemFrameMeshes = itemFrameMeshes.filter(mesh => mesh.parent) + this.itemFrameMaps[mapNumber] = itemFrameMeshes + if (itemFrameMeshes) { + for (const mesh of itemFrameMeshes) { + mesh.material.map = this.loadMap(data) + mesh.material.needsUpdate = true + mesh.visible = true + } + } + } + + updateNameTagVisibility (entity: SceneEntity) { + const playerTeam = this.worldRenderer.playerStateReactive.team + const entityTeam = entity.originalEntity.team + const nameTagVisibility = entityTeam?.nameTagVisibility || 'always' + const showNameTag = nameTagVisibility === 'always' || + (nameTagVisibility === 'hideForOwnTeam' && entityTeam?.team !== playerTeam?.team) || + (nameTagVisibility === 'hideForOtherTeams' && (entityTeam?.team === playerTeam?.team || playerTeam === undefined)) + entity.traverse(c => { + if (c.name === 'nametag') { + c.visible = showNameTag + } + }) + } + + addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) { + const imageData = this.cachedMapsImages?.[mapNumber] + let texture: THREE.Texture | null = null + if (imageData) { + texture = this.loadMap(imageData) + } + const parameters = { + transparent: true, + alphaTest: 0.1, + } + if (texture) { + parameters['map'] = texture + } + const material = new THREE.MeshLambertMaterial(parameters) + + const mapMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material) + + mapMesh.rotation.set(0, Math.PI, 0) + entityMesh.add(mapMesh) + let isInvisible = true + entityMesh.traverseVisible(c => { + if (c.name === 'geometry_frame') { + isInvisible = false + } + }) + if (isInvisible) { + mapMesh.position.set(0, 0, 0.499) + } else { + mapMesh.position.set(0, 0, 0.437) + } + // Apply 90° increment rotation for maps (0-3) + mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2) + mapMesh.name = `map_${mapNumber}` + + if (!texture) { + mapMesh.visible = false + } + + if (!this.itemFrameMaps[mapNumber]) { + this.itemFrameMaps[mapNumber] = [] + } + this.itemFrameMaps[mapNumber].push(mapMesh) + } + + loadMap (data: any) { + const texture = new THREE.TextureLoader().load(data) + if (texture) { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.needsUpdate = true + } + return texture + } + + addItemModel (entityMesh: SceneEntity, hand: 'left' | 'right', item: Item, isPlayer = false) { + const bedrockParentName = `bone_${hand}item` + const itemName = `custom_item_${hand}` + + // remove existing item + entityMesh.traverse(c => { + if (c.name === itemName) { + c.removeFromParent() + if (c['additionalCleanup']) c['additionalCleanup']() + } + }) + if (!item) return + + const itemObject = this.getItemMesh(item, { + 'minecraft:display_context': 'thirdperson', + }) + if (itemObject?.mesh) { + entityMesh.traverse(c => { + if (c.name.toLowerCase() === bedrockParentName || c.name === `${hand}Arm`) { + const group = new THREE.Object3D() + group['additionalCleanup'] = () => { + // important: avoid texture memory leak and gpu slowdown + if (itemObject.cleanup) { + itemObject.cleanup() + } + } + const itemMesh = itemObject.mesh + group.rotation.z = -Math.PI / 16 + if (itemObject.isBlock) { + group.rotation.y = Math.PI / 4 + } else { + itemMesh.rotation.z = -Math.PI / 4 + group.rotation.y = Math.PI / 2 + group.scale.multiplyScalar(2) + } + + // if player, move item below and forward a bit + if (isPlayer) { + group.position.y = -8 + group.position.z = 5 + group.position.x = hand === 'left' ? 1 : -1 + group.rotation.x = Math.PI + } + + group.add(itemMesh) + + group.name = itemName + c.add(group) + } + }) + } + } + + handleDamageEvent (entityId, damageAmount) { + const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh') + if (entityMesh) { + entityMesh.traverse((child) => { + if (child instanceof THREE.Mesh && child.material.clone) { + const clonedMaterial = child.material.clone() + clonedMaterial.dispose() + child.material = child.material.clone() + const originalColor = child.material.color.clone() + child.material.color.set(0xff_00_00) + new TWEEN.Tween(child.material.color) + .to(originalColor, 500) + .start() + } + }) + } + } + + raycastSceneDebug () { + // return any object from scene. raycast from camera + const raycaster = new THREE.Raycaster() + raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera) + const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children) + return intersects[0]?.object + } + + private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType { + const playerObject = new PlayerObject() as PlayerObjectType + playerObject.realPlayerUuid = entity.uuid ?? '' + playerObject.realUsername = entity.username ?? '' + playerObject.position.set(0, 16, 0) + + // fix issues with starfield + playerObject.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) { + obj.material.transparent = true + } + }) + + wrapper.add(playerObject as any) + const scale = 1 / 16 + wrapper.scale.set(scale, scale, scale) + wrapper.rotation.set(0, Math.PI, 0) + + // Set up animation + playerObject.animation = new WalkingGeneralSwing() + //@ts-expect-error + playerObject.animation.isMoving = false + + return playerObject + } + + private updateEntityEquipment (entityMesh: SceneEntity, entity: SceneEntity['originalEntity']) { + if (!entityMesh || !entity.equipment) return + + const isPlayer = entity.type === 'player' + this.addItemModel(entityMesh, isPlayer ? 'right' : 'left', entity.equipment[0], isPlayer) + this.addItemModel(entityMesh, isPlayer ? 'left' : 'right', entity.equipment[1], isPlayer) + addArmorModel(this.worldRenderer, entityMesh, 'feet', entity.equipment[2]) + addArmorModel(this.worldRenderer, entityMesh, 'legs', entity.equipment[3], 2) + addArmorModel(this.worldRenderer, entityMesh, 'chest', entity.equipment[4]) + addArmorModel(this.worldRenderer, entityMesh, 'head', entity.equipment[5]) + + // Update player-specific equipment + if (isPlayer && entityMesh.playerObject) { + const { playerObject } = entityMesh + playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape' + if (playerObject.backEquipment === 'elytra') { + void this.loadAndApplyCape(entity.id, elytraTexture) + } + if (playerObject.cape.map === null) { + playerObject.cape.visible = false + } + } + } +} + +function getGeneralEntitiesMetadata (entity: { name; metadata }): Partial> { + const entityData = loadedData.entitiesByName[entity.name] + return new Proxy({}, { + get (target, p, receiver) { + if (typeof p !== 'string' || !entityData) return + const index = entityData.metadataKeys?.indexOf(p) + return entity.metadata?.[index ?? -1] + }, + }) +} + +function getSpecificEntityMetadata (name: T, entity): EntityMetadataVersions[T] | undefined { + if (entity.name !== name) return + return getGeneralEntitiesMetadata(entity) as any +} + +function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) { + if (!item) { + removeArmorModel(entityMesh, slotType) + return + } + const itemParts = item.name.split('_') + let texturePath + const isPlayerHead = slotType === 'head' && item.name === 'player_head' + if (isPlayerHead) { + removeArmorModel(entityMesh, slotType) + if (item.nbt) { + const itemNbt = nbt.simplify(item.nbt) + try { + let textureData + if (itemNbt.SkullOwner) { + textureData = itemNbt.SkullOwner.Properties.textures[0]?.Value + } else { + textureData = itemNbt['minecraft:profile']?.Properties?.find(p => p.name === 'textures')?.value + } + if (textureData) { + const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString()) + texturePath = decodedData.textures?.SKIN?.url + const { skinTexturesProxy } = this.worldRenderer.worldRendererConfig + if (skinTexturesProxy) { + texturePath = texturePath?.replace('http://textures.minecraft.net/', skinTexturesProxy) + .replace('https://textures.minecraft.net/', skinTexturesProxy) + } + } + } catch (err) { + console.error('Error decoding player head texture:', err) + } + } else { + texturePath = stevePngUrl + } + } + const armorMaterial = itemParts[0] + if (!texturePath) { + // TODO: Support mirroring on certain parts of the model + const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}` + texturePath = worldRenderer.resourcesManager.currentResources.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName] + } + if (!texturePath || !armorModel[slotType]) { + removeArmorModel(entityMesh, slotType) + return + } + + const meshName = `geometry_armor_${slotType}${overlay ? '_overlay' : ''}` + let mesh = entityMesh.children.findLast(c => c.name === meshName) as THREE.Mesh + let material + if (mesh) { + material = mesh.material + void loadTexture(texturePath, texture => { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.flipY = false + texture.wrapS = THREE.MirroredRepeatWrapping + texture.wrapT = THREE.MirroredRepeatWrapping + material.map = texture + }) + } else { + mesh = getMesh(worldRenderer, texturePath, armorModel[slotType]) + // // enable debug mode to see the mesh + // mesh.traverse(c => { + // if (c instanceof THREE.Mesh) { + // c.material.wireframe = true + // } + // }) + if (slotType === 'head') { + // avoid z-fighting with the head + mesh.children[0].position.y += 0.01 + } + mesh.name = meshName + material = mesh.material + if (!isPlayerHead) { + material.side = THREE.DoubleSide + } + } + if (armorMaterial === 'leather' && !overlay) { + const color = (item.nbt?.value as any)?.display?.value?.color?.value + if (color) { + const r = color >> 16 & 0xff + const g = color >> 8 & 0xff + const b = color & 0xff + material.color.setRGB(r / 255, g / 255, b / 255) + } else { + material.color.setHex(0xB5_6D_51) // default brown color + } + addArmorModel(worldRenderer, entityMesh, slotType, item, layer, true) + } else { + material.color.setHex(0xFF_FF_FF) + } + const group = new THREE.Object3D() + group.name = `armor_${slotType}${overlay ? '_overlay' : ''}` + group.add(mesh) + + entityMesh.add(mesh) +} + +function removeArmorModel (entityMesh: THREE.Object3D, slotType: string) { + for (const c of entityMesh.children) { + if (c.name === `geometry_armor_${slotType}` || c.name === `geometry_armor_${slotType}_overlay`) { + c.removeFromParent() + } + } +} diff --git a/renderer/viewer/three/entity/EntityMesh.ts b/renderer/viewer/three/entity/EntityMesh.ts new file mode 100644 index 00000000..229da6d5 --- /dev/null +++ b/renderer/viewer/three/entity/EntityMesh.ts @@ -0,0 +1,550 @@ +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 entities from './entities.json' +import { externalModels } from './objModels' +import externalTexturesJson from './externalTextures.json' + +interface ElemFace { + dir: [number, number, number] + u0: [number, number, number] + v0: [number, number, number] + u1: [number, number, number] + v1: [number, number, number] + corners: Array<[number, number, number, number, number]> +} + +interface GeoData { + positions: number[] + normals: number[] + uvs: number[] + indices: number[] + skinIndices: number[] + skinWeights: number[] +} + +interface JsonBone { + name: string + pivot?: [number, number, number] + bind_pose_rotation?: [number, number, number] + rotation?: [number, number, number] + parent?: string + cubes?: JsonCube[] + mirror?: boolean +} + +interface JsonCube { + origin: [number, number, number] + size: [number, number, number] + uv: [number, number] + inflate?: number + rotation?: [number, number, number] +} + +interface JsonModel { + texturewidth?: number + textureheight?: number + bones: JsonBone[] +} + +interface EntityOverrides { + textures?: Record + rotation?: Record +} + +const elemFaces: Record = { + up: { + dir: [0, 1, 0], + u0: [0, 0, 1], + v0: [0, 0, 0], + u1: [1, 0, 1], + v1: [0, 0, 1], + corners: [ + [0, 1, 1, 0, 0], + [1, 1, 1, 1, 0], + [0, 1, 0, 0, 1], + [1, 1, 0, 1, 1] + ] + }, + down: { + dir: [0, -1, 0], + u0: [1, 0, 1], + v0: [0, 0, 0], + u1: [2, 0, 1], + v1: [0, 0, 1], + corners: [ + [1, 0, 1, 0, 0], + [0, 0, 1, 1, 0], + [1, 0, 0, 0, 1], + [0, 0, 0, 1, 1] + ] + }, + east: { + dir: [1, 0, 0], + u0: [0, 0, 0], + v0: [0, 0, 1], + u1: [0, 0, 1], + v1: [0, 1, 1], + corners: [ + [1, 1, 1, 0, 0], + [1, 0, 1, 0, 1], + [1, 1, 0, 1, 0], + [1, 0, 0, 1, 1] + ] + }, + west: { + dir: [-1, 0, 0], + u0: [1, 0, 1], + v0: [0, 0, 1], + u1: [1, 0, 2], + v1: [0, 1, 1], + corners: [ + [0, 1, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 1, 1, 1, 0], + [0, 0, 1, 1, 1] + ] + }, + north: { + dir: [0, 0, -1], + u0: [0, 0, 1], + v0: [0, 0, 1], + u1: [1, 0, 1], + v1: [0, 1, 1], + corners: [ + [1, 0, 0, 0, 1], + [0, 0, 0, 1, 1], + [1, 1, 0, 0, 0], + [0, 1, 0, 1, 0] + ] + }, + south: { + dir: [0, 0, 1], + u0: [1, 0, 2], + v0: [0, 0, 1], + u1: [2, 0, 2], + v1: [0, 1, 1], + corners: [ + [0, 0, 1, 0, 1], + [1, 0, 1, 1, 1], + [0, 1, 1, 0, 0], + [1, 1, 1, 1, 0] + ] + } +} + +function dot (a: number[], b: number[]): number { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +function addCube ( + attr: GeoData, + boneId: number, + bone: THREE.Bone, + cube: JsonCube, + sameTextureForAllFaces = false, + texWidth = 64, + texHeight = 64, + mirror = false, + errors: string[] = [] +): void { + const cubeRotation = new THREE.Euler(0, 0, 0) + if (cube.rotation) { + cubeRotation.x = -cube.rotation[0] * Math.PI / 180 + cubeRotation.y = -cube.rotation[1] * Math.PI / 180 + cubeRotation.z = -cube.rotation[2] * Math.PI / 180 + } + for (const { dir, corners, u0, v0, u1, v1 } of Object.values(elemFaces)) { + const ndx = Math.floor(attr.positions.length / 3) + + const eastOrWest = dir[0] !== 0 + const faceUvs: number[] = [] + for (const pos of corners) { + let u: number + let v: number + if (sameTextureForAllFaces) { + u = (cube.uv[0] + pos[3] * cube.size[0]) / texWidth + v = (cube.uv[1] + pos[4] * cube.size[1]) / texHeight + } else { + u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth + v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight + } + // if (isNaN(u) || isNaN(v)) { + // errors.push(`NaN u: ${u}, v: ${v}`) + // continue + // } + // if (u < 0 || u > 1 || v < 0 || v > 1) { + // errors.push(`u: ${u}, v: ${v} out of range`) + // continue + // } + + const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0] + const posY = pos[1] + const posZ = eastOrWest && mirror ? pos[2] ^ 1 : pos[2] + const inflate = cube.inflate ?? 0 + let vecPos = new THREE.Vector3( + cube.origin[0] + posX * cube.size[0] + (posX ? inflate : -inflate), + cube.origin[1] + posY * cube.size[1] + (posY ? inflate : -inflate), + cube.origin[2] + posZ * cube.size[2] + (posZ ? inflate : -inflate) + ) + + vecPos = vecPos.applyEuler(cubeRotation) + vecPos = vecPos.sub(bone.position) + vecPos = vecPos.applyEuler(bone.rotation) + vecPos = vecPos.add(bone.position) + + attr.positions.push(vecPos.x, vecPos.y, vecPos.z) + attr.normals.push(dir[0], dir[1], dir[2]) + faceUvs.push(u, v) + attr.skinIndices.push(boneId, 0, 0, 0) + attr.skinWeights.push(1, 0, 0, 0) + } + + if (mirror) { + for (let i = 0; i + 1 < corners.length; i += 2) { + const faceIndex = i * 2 + const tempFaceUvs = faceUvs.slice(faceIndex, faceIndex + 4) + faceUvs[faceIndex] = tempFaceUvs[2] + faceUvs[faceIndex + 1] = tempFaceUvs[eastOrWest ? 1 : 3] + faceUvs[faceIndex + 2] = tempFaceUvs[0] + faceUvs[faceIndex + 3] = tempFaceUvs[eastOrWest ? 3 : 1] + } + } + attr.uvs.push(...faceUvs) + + attr.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3) + } +} + +export function getMesh ( + worldRenderer: WorldRendererThree | undefined, + texture: string, + jsonModel: JsonModel, + overrides: EntityOverrides = {}, + debugFlags: EntityDebugFlags = {} +): THREE.SkinnedMesh { + let textureWidth = jsonModel.texturewidth ?? 64 + let textureHeight = jsonModel.textureheight ?? 64 + let textureOffset: number[] | undefined + const useBlockTexture = texture.startsWith('block:') + const blocksTexture = worldRenderer?.material.map + if (useBlockTexture) { + if (!worldRenderer) throw new Error('worldRenderer is required for block textures') + const blockName = texture.slice(6) + const textureInfo = worldRenderer.resourcesManager.currentResources.blocksAtlasJson.textures[blockName] + if (textureInfo) { + textureWidth = blocksTexture?.image.width ?? textureWidth + textureHeight = blocksTexture?.image.height ?? textureHeight + // todo support su/sv + textureOffset = [textureInfo.u, textureInfo.v] + } else { + console.error(`Unknown block ${blockName}`) + } + } + + const bones: Record = {} + + const geoData: GeoData = { + positions: [], + normals: [], + uvs: [], + indices: [], + skinIndices: [], + skinWeights: [] + } + let i = 0 + for (const jsonBone of jsonModel.bones) { + const bone = new THREE.Bone() + if (jsonBone.pivot) { + bone.position.x = jsonBone.pivot[0] + bone.position.y = jsonBone.pivot[1] + bone.position.z = jsonBone.pivot[2] + } + if (jsonBone.bind_pose_rotation) { + bone.rotation.x = -jsonBone.bind_pose_rotation[0] * Math.PI / 180 + bone.rotation.y = -jsonBone.bind_pose_rotation[1] * Math.PI / 180 + bone.rotation.z = -jsonBone.bind_pose_rotation[2] * Math.PI / 180 + } else if (jsonBone.rotation) { + bone.rotation.x = -jsonBone.rotation[0] * Math.PI / 180 + bone.rotation.y = -jsonBone.rotation[1] * Math.PI / 180 + bone.rotation.z = -jsonBone.rotation[2] * Math.PI / 180 + } + if (overrides.rotation?.[jsonBone.name]) { + bone.rotation.x -= (overrides.rotation[jsonBone.name].x ?? 0) * Math.PI / 180 + bone.rotation.y -= (overrides.rotation[jsonBone.name].y ?? 0) * Math.PI / 180 + bone.rotation.z -= (overrides.rotation[jsonBone.name].z ?? 0) * Math.PI / 180 + } + bone.name = `bone_${jsonBone.name}` + bones[jsonBone.name] = bone + + if (jsonBone.cubes) { + for (const cube of jsonBone.cubes) { + const errors: string[] = [] + addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror, errors) + if (errors.length) { + debugFlags.errors ??= [] + debugFlags.errors.push(...errors.map(error => `Bone ${jsonBone.name}: ${error}`)) + } + } + } + i++ + } + + const rootBones: THREE.Object3D[] = [] + for (const jsonBone of jsonModel.bones) { + if (jsonBone.parent && bones[jsonBone.parent]) { + bones[jsonBone.parent].add(bones[jsonBone.name]) + } else { + rootBones.push(bones[jsonBone.name]) + } + } + + const skeleton = new THREE.Skeleton(Object.values(bones)) + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.Float32BufferAttribute(geoData.positions, 3)) + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(geoData.normals, 3)) + geometry.setAttribute('uv', new THREE.Float32BufferAttribute(geoData.uvs, 2)) + geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(geoData.skinIndices, 4)) + geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(geoData.skinWeights, 4)) + geometry.setIndex(geoData.indices) + + const material = new THREE.MeshLambertMaterial({ transparent: true, alphaTest: 0.1 }) + const mesh = new THREE.SkinnedMesh(geometry, material) + mesh.add(...rootBones) + mesh.bind(skeleton) + mesh.scale.set(1 / 16, 1 / 16, 1 / 16) + + if (textureOffset) { + // todo(memory) dont clone + const loadedTexture = blocksTexture!.clone() + loadedTexture.offset.set(textureOffset[0], textureOffset[1]) + loadedTexture.needsUpdate = true + material.map = loadedTexture + } else { + void loadTexture(texture, loadedTexture => { + if (material.map) { + // texture is already loaded + return + } + loadedTexture.magFilter = THREE.NearestFilter + loadedTexture.minFilter = THREE.NearestFilter + loadedTexture.flipY = false + loadedTexture.wrapS = THREE.RepeatWrapping + loadedTexture.wrapT = THREE.RepeatWrapping + material.map = loadedTexture + }, () => { + // This callback runs after the texture is fully loaded + const actualWidth = material.map!.image.width + if (actualWidth && textureWidth !== actualWidth) { + material.map!.repeat.x = textureWidth / actualWidth + } + const actualHeight = material.map!.image.height + if (actualHeight && textureHeight !== actualHeight) { + material.map!.repeat.y = textureHeight / actualHeight + } + material.needsUpdate = true + }) + } + + return mesh +} + +export const rendererSpecialHandled = ['item_frame', 'item', 'player'] + +type EntityMapping = { + pattern: string | RegExp + target: string +} + +const temporaryMappings: EntityMapping[] = [ + // Exact matches + { pattern: 'furnace_minecart', target: 'minecart' }, + { pattern: 'spawner_minecart', target: 'minecart' }, + { pattern: 'chest_minecart', target: 'minecart' }, + { pattern: 'hopper_minecart', target: 'minecart' }, + { pattern: 'command_block_minecart', target: 'minecart' }, + { pattern: 'tnt_minecart', target: 'minecart' }, + { pattern: 'glow_item_frame', target: 'item_frame' }, + { pattern: 'glow_squid', target: 'squid' }, + { pattern: 'trader_llama', target: 'llama' }, + { pattern: 'chest_boat', target: 'boat' }, + { pattern: 'spectral_arrow', target: 'arrow' }, + { pattern: 'husk', target: 'zombie' }, + { pattern: 'zombie_horse', target: 'horse' }, + { pattern: 'donkey', target: 'horse' }, + { pattern: 'skeleton_horse', target: 'horse' }, + { pattern: 'mule', target: 'horse' }, + { pattern: 'ocelot', target: 'cat' }, + // Regex patterns + { pattern: /_minecraft$/, target: 'minecraft' }, + { pattern: /_boat$/, target: 'boat' }, + { pattern: /_raft$/, target: 'boat' }, + { pattern: /_horse$/, target: 'horse' }, + { pattern: /_zombie$/, target: 'zombie' }, + { pattern: /_arrow$/, target: 'zombie' }, +] + +function getEntityMapping (type: string): string | undefined { + for (const mapping of temporaryMappings) { + if (typeof mapping.pattern === 'string') { + if (mapping.pattern === type) return mapping.target + } else if (mapping.pattern.test(type)) { return mapping.target } + } + return undefined +} + +const getEntity = (name: string) => { + return entities[name] +} + +const scaleEntity: Record = { + zombie: 1.85, + husk: 1.85, + arrow: 0.0025 +} + +const offsetEntity: Record = { + zombie: new Vec3(0, 1, 0), + husk: new Vec3(0, 1, 0), + boat: new Vec3(0, -1, 0), + arrow: new Vec3(0, -0.9, 0) +} + +interface EntityGeometry { + geometry: Array<{ + name: string; + [key: string]: any; + }>; +} + +export type EntityDebugFlags = { + type?: 'obj' | 'bedrock' + tempMap?: string + textureMap?: boolean + errors?: string[] + isHardcodedTexture?: boolean +} + +export class EntityMesh { + mesh: THREE.Object3D + + constructor ( + version: string, + type: string, + worldRenderer?: WorldRendererThree, + overrides: EntityOverrides = {}, + debugFlags: EntityDebugFlags = {} + ) { + const originalType = type + const mappedValue = getEntityMapping(type) + if (mappedValue) { + type = mappedValue + debugFlags.tempMap = mappedValue + } + + if (externalModels[type]) { + const objLoader = new OBJLoader() + const texturePathMap = { + 'zombie_horse': `textures/${version}/entity/horse/horse_zombie.png`, + 'husk': huskPng, + 'skeleton_horse': `textures/${version}/entity/horse/horse_skeleton.png`, + 'donkey': `textures/${version}/entity/horse/donkey.png`, + 'mule': `textures/${version}/entity/horse/mule.png`, + 'ocelot': ocelotPng, + 'arrow': arrowTexture, + 'spectral_arrow': spectralArrowTexture, + 'tipped_arrow': tippedArrowTexture + } + const tempTextureMap = texturePathMap[originalType] || texturePathMap[type] + if (tempTextureMap) { + debugFlags.textureMap = true + } + const texturePath = tempTextureMap || externalTexturesJson[type] + if (externalTexturesJson[type]) { + debugFlags.isHardcodedTexture = true + } + if (!texturePath) throw new Error(`No texture for ${type}`) + const texture = new THREE.TextureLoader().load(texturePath) + texture.minFilter = THREE.NearestFilter + texture.magFilter = THREE.NearestFilter + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + alphaTest: 0.1 + }) + const obj = objLoader.parse(externalModels[type]) + const scale = scaleEntity[originalType] || scaleEntity[type] + if (scale) obj.scale.set(scale, scale, scale) + const offset = offsetEntity[originalType] + if (offset) obj.position.set(offset.x, offset.y, offset.z) + obj.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.material = material + // todo + if (child.name === 'Head layer') child.visible = false + if (child.name === 'Head' && overrides.rotation?.head) { // todo + child.rotation.x -= (overrides.rotation.head.x ?? 0) * Math.PI / 180 + child.rotation.y -= (overrides.rotation.head.y ?? 0) * Math.PI / 180 + child.rotation.z -= (overrides.rotation.head.z ?? 0) * Math.PI / 180 + } + } + }) + this.mesh = obj + debugFlags.type = 'obj' + return + } + + if (originalType === 'arrow') { + // overrides.textures = { + // 'default': testArrow, + // ...overrides.textures, + // } + } + + const e = getEntity(type) + if (!e) { + // if (knownNotHandled.includes(type)) return + // throw new Error(`Unknown entity ${type}`) + return + } + + this.mesh = new THREE.Object3D() + for (const [name, jsonModel] of Object.entries(e.geometry)) { + const texture = overrides.textures?.[name] ?? e.textures[name] + if (!texture) continue + // console.log(JSON.stringify(jsonModel, null, 2)) + const mesh = getMesh(worldRenderer, + texture.endsWith('.png') || texture.startsWith('data:image/') || texture.startsWith('block:') + ? texture : texture + '.png', + jsonModel, + overrides, + debugFlags) + mesh.name = `geometry_${name}` + this.mesh.add(mesh) + } + debugFlags.type = 'bedrock' + } + + static getStaticData (name: string): { boneNames: string[] } { + name = getEntityMapping(name) || name + if (externalModels[name]) { + return { + boneNames: [] // todo + } + } + const e = getEntity(name) as EntityGeometry + if (!e) throw new Error(`Unknown entity ${name}`) + return { + boneNames: Object.values(e.geometry).flatMap(x => x.name) + } + } +} +globalThis.EntityMesh = EntityMesh diff --git a/renderer/viewer/three/entity/animations.js b/renderer/viewer/three/entity/animations.js new file mode 100644 index 00000000..3295556f --- /dev/null +++ b/renderer/viewer/three/entity/animations.js @@ -0,0 +1,171 @@ +//@ts-check +import { PlayerAnimation } from 'skinview3d' + +export class WalkingGeneralSwing extends PlayerAnimation { + + switchAnimationCallback + + isRunning = false + isMoving = true + isCrouched = false + + _startArmSwing + + swingArm() { + this._startArmSwing = this.progress + } + + animate(player) { + // Multiply by animation's natural speed + let t = 0 + 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 + + croughAnimation(player, this.isCrouched) + + 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 + HitAnimation.animate((this.progress - this._startArmSwing), player, this.isMoving) + + if (tHand > Math.PI + Math.PI) { + 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 + } + } +} + +const HitAnimation = { + animate(progress, player, isMovingOrRunning) { + const t = progress * 18 + player.skin.rightArm.rotation.x = -0.453_786_055_2 * 2 + 2 * Math.sin(t + Math.PI) * 0.3 + + if (!isMovingOrRunning) { + const basicArmRotationZ = 0.01 * Math.PI + 0.06 + player.skin.rightArm.rotation.z = -Math.cos(t) * 0.403 + basicArmRotationZ + player.skin.body.rotation.y = -Math.cos(t) * 0.06 + player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.077 + player.skin.leftArm.rotation.z = -Math.cos(t) * 0.015 + 0.13 - 0.05 + player.skin.leftArm.position.z = Math.cos(t) * 0.3 + player.skin.leftArm.position.x = 5 - Math.cos(t) * 0.05 + } + }, +} + +const croughAnimation = (player, isCrouched) => { + const erp = 0 + + // let pr = this.progress * 8; + let pr = isCrouched ? 1 : 0 + const showProgress = false + if (showProgress) { + pr = Math.floor(pr) + } + player.skin.body.rotation.x = 0.453_786_055_2 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.body.position.z = + 1.325_618_1 * Math.abs(Math.sin((pr * Math.PI) / 2)) - 3.450_031_037_7 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.body.position.y = -6 - 2.103_677_462 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.cape.position.y = 8 - 1.851_236_166_577_372 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.cape.rotation.x = (10.8 * Math.PI) / 180 + 0.294_220_265_771 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.cape.position.z = + -2 + 3.786_619_432 * Math.abs(Math.sin((pr * Math.PI) / 2)) - 3.450_031_037_7 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.elytra.position.x = player.cape.position.x + player.elytra.position.y = player.cape.position.y + player.elytra.position.z = player.cape.position.z + player.elytra.rotation.x = player.cape.rotation.x - (10.8 * Math.PI) / 180 + // const pr1 = this.progress / this.speed; + const pr1 = 1 + if (Math.abs(Math.sin((pr * Math.PI) / 2)) === 1) { + player.elytra.leftWing.rotation.z = + 0.261_799_44 + 0.458_200_6 * Math.abs(Math.sin((Math.min(pr1 - erp, 1) * Math.PI) / 2)) + player.elytra.updateRightWing() + } else if (isCrouched !== undefined) { + player.elytra.leftWing.rotation.z = + 0.72 - 0.458_200_6 * Math.abs(Math.sin((Math.min(pr1 - erp, 1) * Math.PI) / 2)) + player.elytra.updateRightWing() + } + player.skin.head.position.y = -3.618_325_234_674 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.leftArm.position.z = + 3.618_325_234_674 * Math.abs(Math.sin((pr * Math.PI) / 2)) - 3.450_031_037_7 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.rightArm.position.z = player.skin.leftArm.position.z + player.skin.leftArm.rotation.x = 0.410_367_746_202 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.rightArm.rotation.x = player.skin.leftArm.rotation.x + player.skin.leftArm.rotation.z = 0.1 + player.skin.rightArm.rotation.z = -player.skin.leftArm.rotation.z + player.skin.leftArm.position.y = -2 - 2.539_433_18 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.rightArm.position.y = player.skin.leftArm.position.y + player.skin.rightLeg.position.z = -3.450_031_037_7 * Math.abs(Math.sin((pr * Math.PI) / 2)) + player.skin.leftLeg.position.z = player.skin.rightLeg.position.z +} diff --git a/renderer/viewer/three/entity/armorModels.json b/renderer/viewer/three/entity/armorModels.json new file mode 100644 index 00000000..d7a77d4c --- /dev/null +++ b/renderer/viewer/three/entity/armorModels.json @@ -0,0 +1,204 @@ +{ + "skull": { + "bones": [ + { + "name": "head", + "pivot": [0, 12, 0], + "cubes": [ + { + "origin": [-4, 0, -4], + "size": [8, 8, 8], + "uv": [0, 0], + "inflate": 1 + } + ] + }, + { + "name": "overlay", + "parent": "head", + "pivot": [0, 12, 0], + "cubes": [ + { + "origin": [-4, 0, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 1.2 + } + ] + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "head": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "head", + "parent": "armor", + "pivot": [0, 12, 0], + "cubes": [ + { + "origin": [-4, 23, -4], + "size": [8, 8, 8], + "uv": [0, 0], + "inflate": 1 + } + ] + }, + { + "name": "overlay", + "parent": "head", + "pivot": [0, 12, 0], + "cubes": [ + { + "origin": [-4, 23, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 1.2 + } + ] + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "chest": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "body", + "parent": "armor", + "pivot": [0, 13, 0], + "cubes": [ + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 16], + "inflate": 1 + } + ] + }, + { + "name": "leftarm", + "parent": "armor", + "pivot": [5, 10, 0], + "cubes": [ + { + "origin": [4, 12, -2], + "size": [4, 12, 4], + "uv": [40, 16], + "inflate": 0.85 + } + ] + }, + { + "name": "rightarm", + "parent": "armor", + "pivot": [-5, 10, 0], + "cubes": [ + { + "origin": [-8, 12, -2], + "size": [4, 12, 4], + "uv": [40, 16], + "inflate": 0.85 + } + ], + "mirror": true + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "legs": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "body", + "parent": "armor", + "pivot": [0, 13, 0], + "cubes": [ + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 16], + "inflate": 0.5 + } + ] + }, + { + "name": "leftleg", + "parent": "armor", + "pivot": [1.9, 1, 0], + "cubes": [ + { + "origin": [-0.1, 0, -2], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.5 + } + ] + }, + { + "name": "rightleg", + "parent": "armor", + "pivot": [-1.9, 1, 0], + "cubes": [ + { + "origin": [-3.9, 0, -2.01], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.5 + } + ], + "mirror": true + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "feet": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "leftleg", + "parent": "armor", + "pivot": [1.9, 1, 0], + "cubes": [ + { + "origin": [-0.1, 0, -2], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.8 + } + ] + }, + { + "name": "rightleg", + "parent": "armor", + "pivot": [-1.9, 1, 0], + "cubes": [ + { + "origin": [-3.9, 0.01, -2.01], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.8 + } + ], + "mirror": true + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + } +} \ No newline at end of file diff --git a/renderer/viewer/three/entity/armorModels.ts b/renderer/viewer/three/entity/armorModels.ts new file mode 100644 index 00000000..3681344c --- /dev/null +++ b/renderer/viewer/three/entity/armorModels.ts @@ -0,0 +1,36 @@ +import { default as chainmailLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_1.png' +import { default as chainmailLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_2.png' +import { default as diamondLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_1.png' +import { default as diamondLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_2.png' +import { default as goldenLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_1.png' +import { default as goldenLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_2.png' +import { default as ironLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_1.png' +import { default as ironLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_2.png' +import { default as leatherLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1.png' +import { default as leatherLayer1Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1_overlay.png' +import { default as leatherLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2.png' +import { default as leatherLayer2Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2_overlay.png' +import { default as netheriteLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_1.png' +import { default as netheriteLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_2.png' +import { default as turtleLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/turtle_layer_1.png' + +export { default as elytraTexture } from 'mc-assets/dist/other-textures/latest/entity/elytra.png' +export { default as armorModel } from './armorModels.json' + +export const armorTextures = { + 'leather_layer_1': leatherLayer1, + 'leather_layer_1_overlay': leatherLayer1Overlay, + 'leather_layer_2': leatherLayer2, + 'leather_layer_2_overlay': leatherLayer2Overlay, + 'chainmail_layer_1': chainmailLayer1, + 'chainmail_layer_2': chainmailLayer2, + 'iron_layer_1': ironLayer1, + 'iron_layer_2': ironLayer2, + 'diamond_layer_1': diamondLayer1, + 'diamond_layer_2': diamondLayer2, + 'golden_layer_1': goldenLayer1, + 'golden_layer_2': goldenLayer2, + 'netherite_layer_1': netheriteLayer1, + 'netherite_layer_2': netheriteLayer2, + 'turtle_layer_1': turtleLayer1 +} diff --git a/renderer/viewer/three/entity/entities.json b/renderer/viewer/three/entity/entities.json new file mode 100644 index 00000000..feca5dc7 --- /dev/null +++ b/renderer/viewer/three/entity/entities.json @@ -0,0 +1,6230 @@ +{ + "armor_stand": { + "identifier": "minecraft:armor_stand", + "min_engine_version": "1.8.0", + "materials": {"default": "armor_stand"}, + "textures": {"default": "textures/entity/armorstand/wood"}, + "geometry": { + "default": { + "bones": [ + { + "name": "baseplate", + "parent": "waist", + "cubes": [ + {"origin": [-6, 0, -6], "size": [12, 1, 12], "uv": [0, 32]} + ] + }, + {"name": "waist", "pivot": [0, 12, 0]}, + { + "name": "body", + "parent": "waist", + "pivot": [0, 13, 0], + "cubes": [ + {"origin": [-6, 21, -1.5], "size": [12, 3, 3], "uv": [0, 26]}, + {"origin": [-3, 14, -1], "size": [2, 7, 2], "uv": [16, 0]}, + {"origin": [1, 14, -1], "size": [2, 7, 2], "uv": [48, 16]}, + {"origin": [-4, 12, -1], "size": [8, 2, 2], "uv": [0, 48]} + ] + }, + { + "name": "head", + "parent": "waist", + "pivot": [0, 12, 0], + "cubes": [{"origin": [-1, 24, -1], "size": [2, 7, 2], "uv": [0, 0]}] + }, + { + "name": "hat", + "parent": "head", + "pivot": [0, 12, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [32, 0]} + ] + }, + { + "name": "leftarm", + "parent": "waist", + "mirror": true, + "pivot": [5, 10, 0], + "cubes": [ + {"origin": [5, 12, -1], "size": [2, 12, 2], "uv": [32, 16]} + ] + }, + {"name": "leftitem", "parent": "leftarm", "pivot": [1, -9, -5]}, + { + "name": "leftleg", + "parent": "waist", + "mirror": true, + "pivot": [1.9, 1, 0], + "cubes": [ + {"origin": [0.9, 1, -1], "size": [2, 11, 2], "uv": [40, 16]} + ] + }, + { + "name": "rightarm", + "parent": "waist", + "pivot": [-5, 10, 0], + "cubes": [ + {"origin": [-7, 12, -1], "size": [2, 12, 2], "uv": [24, 0]} + ] + }, + {"name": "rightitem", "parent": "rightarm", "pivot": [-1, -9, -5]}, + { + "name": "rightleg", + "parent": "waist", + "pivot": [-1.9, 1, 0], + "cubes": [ + {"origin": [-2.9, 1, -1], "size": [2, 11, 2], "uv": [8, 0]} + ] + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 64 + } + }, + "render_controllers": ["controller.render.armor_stand"], + "enable_attachables": true + }, + "arrow": { + "identifier": "minecraft:arrow", + "materials": {"default": "arrow"}, + "textures": {"default": "textures/entity/arrow"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 1, 0], + "cubes": [ + { + "origin": [0, -2.5, -3], + "rotation": [0, 0, 45], + "size": [0, 5, 16], + "uv": {"east": {"uv": [0, 0]}} + }, + { + "origin": [0, -2.5, -3], + "rotation": [0, 0, -45], + "size": [0, 5, 16], + "uv": {"east": {"uv": [0, 0]}} + }, + { + "origin": [-2.5, -2.5, 12], + "rotation": [0, 0, 45], + "size": [5, 5, 0], + "uv": {"south": {"uv": [0, 5]}} + } + ] + } + ], + "texturewidth": 32, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.arrow"] + }, + "bat": { + "identifier": "minecraft:bat", + "materials": {"default": "bat"}, + "textures": {"default": "textures/entity/bat"}, + "geometry": { + "default": { + "visible_bounds_width": 1, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0.5, 0], + "bones": [ + { + "name": "head", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-3, 21, -3], "size": [6, 6, 6], "uv": [0, 0]}] + }, + { + "name": "rightEar", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 26, -2], "size": [3, 4, 1], "uv": [24, 0]} + ], + "parent": "head" + }, + { + "name": "leftEar", + "mirror": true, + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [1, 26, -2], "size": [3, 4, 1], "uv": [24, 0]} + ], + "parent": "head" + }, + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-3, 8, -3], "size": [6, 12, 6], "uv": [0, 16]}, + {"origin": [-5, -8, 0], "size": [10, 16, 1], "uv": [0, 34]} + ] + }, + { + "name": "rightWing", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-12, 7, 1.5], "size": [10, 16, 1], "uv": [42, 0]} + ], + "parent": "body" + }, + { + "name": "rightWingTip", + "pivot": [-12, 23, 1.5], + "cubes": [ + {"origin": [-20, 10, 1.5], "size": [8, 12, 1], "uv": [24, 16]} + ], + "parent": "rightWing" + }, + { + "name": "leftWing", + "mirror": true, + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [2, 7, 1.5], "size": [10, 16, 1], "uv": [42, 0]} + ], + "parent": "body" + }, + { + "name": "leftWingTip", + "mirror": true, + "pivot": [12, 23, 1.5], + "cubes": [ + {"origin": [12, 10, 1.5], "size": [8, 12, 1], "uv": [24, 16]} + ], + "parent": "leftWing" + } + ] + } + }, + "render_controllers": ["controller.render.bat"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 18} + }, + "bee": { + "identifier": "minecraft:bee", + "materials": {"default": "bee"}, + "textures": { + "default": "textures/entity/bee/bee", + "angry": "textures/entity/bee/bee_angry", + "nectar": "textures/entity/bee/bee_nectar", + "angry_nectar": "textures/entity/bee/bee_angry_nectar" + }, + "geometry": { + "default": { + "texturewidth": 64, + "textureheight": 64, + "visible_bounds_width": 1.5, + "visible_bounds_height": 1.5, + "visible_bounds_offset": [0, 0.25, 0], + "bones": [ + { + "name": "body", + "pivot": [0.5, 5, 0], + "cubes": [ + {"origin": [-3, 2, -5], "size": [7, 7, 10], "uv": [0, 0]}, + {"origin": [2, 7, -8], "size": [1, 2, 3], "uv": [2, 0]}, + {"origin": [-2, 7, -8], "size": [1, 2, 3], "uv": [2, 3]} + ], + "locators": {"lead": [0, 4, -1]} + }, + { + "name": "stinger", + "parent": "body", + "pivot": [0.5, 6, 1], + "cubes": [{"origin": [0.5, 5, 5], "size": [0, 1, 2], "uv": [26, 7]}] + }, + { + "name": "rightwing_bone", + "parent": "body", + "pivot": [-1, 9, -3], + "rotation": [15, -15, 0], + "cubes": [ + {"origin": [-10, 9, -3], "size": [9, 0, 6], "uv": [0, 18]} + ] + }, + { + "name": "leftwing_bone", + "parent": "body", + "pivot": [2, 9, -3], + "rotation": [15, 15, 0], + "cubes": [{"origin": [2, 9, -3], "size": [9, 0, 6], "uv": [9, 24]}] + }, + { + "name": "leg_front", + "parent": "body", + "pivot": [2, 2, -2], + "cubes": [{"origin": [-3, 0, -2], "size": [7, 2, 0], "uv": [26, 1]}] + }, + { + "name": "leg_mid", + "parent": "body", + "pivot": [2, 2, 0], + "cubes": [{"origin": [-3, 0, 0], "size": [7, 2, 0], "uv": [26, 3]}] + }, + { + "name": "leg_back", + "parent": "body", + "pivot": [2, 2, 2], + "cubes": [{"origin": [-3, 0, 2], "size": [7, 2, 0], "uv": [26, 5]}] + } + ] + } + }, + "particle_effects": {"nectar_dripping": "minecraft:nectar_drip_particle"}, + "render_controllers": ["controller.render.bee"], + "spawn_egg": {"texture": "egg_bee", "texture_index": 0} + }, + "cave_spider": { + "identifier": "minecraft:cave_spider", + "min_engine_version": "1.8.0", + "materials": {"default": "spider", "invisible": "spider_invisible"}, + "textures": {"default": "textures/entity/spider/cave_spider"}, + "geometry": { + "default": { + "visible_bounds_width": 2, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "head", + "pivot": [0, 9, -3], + "cubes": [ + {"origin": [-4, 5, -11], "size": [8, 8, 8], "uv": [32, 4]} + ], + "parent": "body0" + }, + { + "name": "body0", + "pivot": [0, 9, 0], + "cubes": [{"origin": [-3, 6, -3], "size": [6, 6, 6], "uv": [0, 0]}] + }, + { + "name": "body1", + "pivot": [0, 9, 9], + "cubes": [ + {"origin": [-5, 5, 3], "size": [10, 8, 12], "uv": [0, 12]} + ], + "parent": "body0" + }, + { + "name": "leg0", + "pivot": [-4, 9, 2], + "cubes": [ + {"origin": [-19, 8, 1], "size": [16, 2, 2], "uv": [18, 0]} + ], + "parent": "body0" + }, + { + "name": "leg1", + "pivot": [4, 9, 2], + "cubes": [{"origin": [3, 8, 1], "size": [16, 2, 2], "uv": [18, 0]}], + "parent": "body0" + }, + { + "name": "leg2", + "pivot": [-4, 9, 1], + "cubes": [ + {"origin": [-19, 8, 0], "size": [16, 2, 2], "uv": [18, 0]} + ], + "parent": "body0" + }, + { + "name": "leg3", + "pivot": [4, 9, 1], + "cubes": [{"origin": [3, 8, 0], "size": [16, 2, 2], "uv": [18, 0]}], + "parent": "body0" + }, + { + "name": "leg4", + "pivot": [-4, 9, 0], + "cubes": [ + {"origin": [-19, 8, -1], "size": [16, 2, 2], "uv": [18, 0]} + ], + "parent": "body0" + }, + { + "name": "leg5", + "pivot": [4, 9, 0], + "cubes": [ + {"origin": [3, 8, -1], "size": [16, 2, 2], "uv": [18, 0]} + ], + "parent": "body0" + }, + { + "name": "leg6", + "pivot": [-4, 9, -1], + "cubes": [ + {"origin": [-19, 8, -2], "size": [16, 2, 2], "uv": [18, 0]} + ], + "parent": "body0" + }, + { + "name": "leg7", + "pivot": [4, 9, -1], + "cubes": [ + {"origin": [3, 8, -2], "size": [16, 2, 2], "uv": [18, 0]} + ], + "parent": "body0" + } + ] + } + }, + "render_controllers": ["controller.render.spider"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 22} + }, + "chest_minecart": { + "identifier": "minecraft:chest_minecart", + "min_engine_version": "1.8.0", + "materials": {"default": "minecart"}, + "textures": {"default": "textures/entity/minecart"}, + "geometry": { + "default": { + "bones": [ + { + "name": "bottom", + "pivot": [0, 6, 0], + "cubes": [ + { + "origin": [-10, -6.5, -1], + "size": [20, 16, 2], + "rotation": [90, 0, 0], + "uv": [0, 10] + } + ] + }, + { + "name": "back", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-17, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 270, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "front", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [1, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 90, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "right", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, 2.5, -8], + "size": [16, 8, 2], + "rotation": [0, 180, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "left", + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-8, 2.5, 6], "size": [16, 8, 2], "uv": [0, 0]} + ], + "parent": "bottom" + } + ], + "texturewidth": 64, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.minecart"] + }, + "command_block_minecart": { + "identifier": "minecraft:command_block_minecart", + "min_engine_version": "1.8.0", + "materials": {"default": "minecart"}, + "textures": {"default": "textures/entity/minecart"}, + "geometry": { + "default": { + "bones": [ + { + "name": "bottom", + "pivot": [0, 6, 0], + "cubes": [ + { + "origin": [-10, -6.5, -1], + "size": [20, 16, 2], + "rotation": [90, 0, 0], + "uv": [0, 10] + } + ] + }, + { + "name": "back", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-17, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 270, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "front", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [1, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 90, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "right", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, 2.5, -8], + "size": [16, 8, 2], + "rotation": [0, 180, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "left", + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-8, 2.5, 6], "size": [16, 8, 2], "uv": [0, 0]} + ], + "parent": "bottom" + } + ], + "texturewidth": 64, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.minecart"] + }, + "cow": { + "identifier": "minecraft:cow", + "min_engine_version": "1.8.0", + "materials": {"default": "cow"}, + "textures": {"default": "textures/entity/cow/cow"}, + "geometry": { + "default": { + "visible_bounds_width": 2, + "visible_bounds_height": 1.75, + "visible_bounds_offset": [0, 0.75, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 19, 2], + "bind_pose_rotation": [90, 0, 0], + "cubes": [ + {"origin": [-6, 11, -5], "size": [12, 18, 10], "uv": [18, 4]}, + {"origin": [-2, 11, -6], "size": [4, 6, 1], "uv": [52, 0]} + ] + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 20, -8], + "locators": {"lead": [0, 20, -8]}, + "cubes": [ + {"origin": [-4, 16, -14], "size": [8, 8, 6], "uv": [0, 0]}, + {"origin": [-5, 22, -12], "size": [1, 3, 1], "uv": [22, 0]}, + {"origin": [4, 22, -12], "size": [1, 3, 1], "uv": [22, 0]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-4, 12, 7], + "cubes": [{"origin": [-6, 0, 5], "size": [4, 12, 4], "uv": [0, 16]}] + }, + { + "name": "leg1", + "parent": "body", + "mirror": true, + "pivot": [4, 12, 7], + "cubes": [{"origin": [2, 0, 5], "size": [4, 12, 4], "uv": [0, 16]}] + }, + { + "name": "leg2", + "parent": "body", + "pivot": [-4, 12, -6], + "cubes": [ + {"origin": [-6, 0, -7], "size": [4, 12, 4], "uv": [0, 16]} + ] + }, + { + "name": "leg3", + "parent": "body", + "mirror": true, + "pivot": [4, 12, -6], + "cubes": [{"origin": [2, 0, -7], "size": [4, 12, 4], "uv": [0, 16]}] + } + ] + } + }, + "render_controllers": ["controller.render.cow"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 1} + }, + "dragon_fireball": { + "identifier": "minecraft:dragon_fireball", + "materials": {"default": "fireball"}, + "textures": {"default": "textures/entity/enderdragon/dragon_fireball"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -4, 0], + "size": [16, 16, 0], + "uv": {"south": {"uv": [0, 0]}} + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.fireball"] + }, + "drowned": { + "identifier": "minecraft:drowned", + "min_engine_version": "1.16.0", + "materials": {"default": "drowned"}, + "textures": {"default": "textures/entity/zombie/drowned"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ] + }, + { + "name": "jacket", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 32], + "inflate": 0.5 + } + ] + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [0, 0], + "inflate": 0.5 + } + ] + }, + { + "name": "hat", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 1 + } + ] + }, + { + "name": "rightArm", + "parent": "body", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-7, 12, -2], "size": [4, 12, 4], "uv": [0, 16]} + ] + }, + { + "name": "leftArm", + "parent": "body", + "pivot": [5, 22, 0], + "cubes": [ + { + "origin": [4, 12, -2], + "size": [4, 12, 4], + "uv": [40, 16], + "mirror": true + } + ] + }, + { + "name": "rightSleeve", + "parent": "rightArm", + "pivot": [-5, 22, 0], + "cubes": [ + { + "origin": [-7, 12, -2], + "size": [4, 12, 4], + "uv": [48, 48], + "inflate": 0.5 + } + ] + }, + { + "name": "leftSleeve", + "parent": "leftArm", + "pivot": [5, 22, 0], + "cubes": [ + { + "origin": [4, 12, -2], + "size": [4, 12, 4], + "uv": [40, 32], + "inflate": 0.5, + "mirror": true + } + ] + }, + { + "name": "rightLeg", + "parent": "body", + "pivot": [-1.9, 12, 0], + "cubes": [ + {"origin": [-4.05, 0, -2], "size": [4, 12, 4], "uv": [16, 48]} + ] + }, + { + "name": "leftLeg", + "parent": "body", + "pivot": [1.9, 12, 0], + "cubes": [ + { + "origin": [0.05, 0, -2], + "size": [4, 12, 4], + "uv": [32, 48], + "mirror": true + } + ] + }, + { + "name": "rightPants", + "parent": "rightLeg", + "pivot": [-1.9, 12, 0], + "cubes": [ + { + "origin": [-4.25, 0, -2], + "size": [4, 12, 4], + "uv": [0, 48], + "inflate": 0.25 + } + ] + }, + { + "name": "leftPants", + "parent": "leftLeg", + "pivot": [1.9, 12, 0], + "cubes": [ + { + "origin": [0.25, 0, -2], + "size": [4, 12, 4], + "uv": [0, 32], + "inflate": 0.25, + "mirror": true + } + ] + }, + {"name": "waist", "parent": "body", "pivot": [0, 12, 0]}, + {"name": "rightItem", "parent": "rightArm", "pivot": [-1, -45, -5]}, + {"name": "leftItem", "parent": "leftArm", "pivot": [1, -45, -5]} + ], + "visible_bounds_width": 2.5, + "visible_bounds_height": 2.5, + "visible_bounds_offset": [0, 1.25, 0], + "texturewidth": 64, + "textureheight": 64 + } + }, + "render_controllers": ["controller.render.drowned"], + "enable_attachables": true, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 48} + }, + "egg": { + "identifier": "minecraft:egg", + "materials": {"default": "egg"}, + "textures": {"default": "textures/items/egg"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "size": [16, 16, 0], + "uv": [0, 0], + "rotation": [0, 0, 0] + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.item_sprite"] + }, + "elder_guardian": { + "identifier": "minecraft:elder_guardian", + "min_engine_version": "1.8.0", + "materials": {"default": "guardian", "ghost": "guardian_ghost"}, + "textures": { + "default": "textures/entity/guardian", + "elder": "textures/entity/guardian_elder", + "beam": "textures/entity/guardian_beam" + }, + "geometry": { + "default": { + "visible_bounds_width": 3.5, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "head", + "pivot": [0, 0, 0], + "mirror": true, + "cubes": [ + { + "mirror": false, + "origin": [-6, 2, -8], + "size": [12, 12, 16], + "uv": [0, 0] + }, + { + "mirror": false, + "origin": [-8, 2, -6], + "size": [2, 12, 12], + "uv": [0, 28] + }, + {"origin": [6, 2, -6], "size": [2, 12, 12], "uv": [0, 28]}, + {"origin": [-6, 14, -6], "size": [12, 2, 12], "uv": [16, 40]}, + {"origin": [-6, 0, -6], "size": [12, 2, 12], "uv": [16, 40]} + ] + }, + { + "name": "eye", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-1, 6, 0], "size": [2, 2, 1], "uv": [8, 0]}] + }, + { + "name": "tailpart0", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-2, 6, 7], "size": [4, 4, 8], "uv": [40, 0]}] + }, + { + "name": "tailpart1", + "parent": "tailpart0", + "pivot": [0, 24, 0], + "cubes": [{"origin": [0, 7, 0], "size": [3, 3, 7], "uv": [0, 54]}] + }, + { + "name": "tailpart2", + "parent": "tailpart1", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [0, 8, 0], "size": [2, 2, 6], "uv": [41, 32]}, + {"origin": [1, 4.5, 3], "size": [1, 9, 9], "uv": [25, 19]} + ] + }, + { + "name": "spikepart0", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart1", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart2", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart3", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart4", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart5", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart6", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart7", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart8", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart9", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart10", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart11", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + } + ] + }, + "ghost": { + "visible_bounds_width": 3.5, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "head", + "pivot": [0, 24, 0], + "mirror": true, + "cubes": [ + { + "mirror": false, + "origin": [-6, 2, -8], + "size": [12, 12, 16], + "uv": [0, 0] + }, + { + "mirror": false, + "origin": [-8, 2, -6], + "size": [2, 12, 12], + "uv": [0, 28] + }, + {"origin": [6, 2, -6], "size": [2, 12, 12], "uv": [0, 28]}, + {"origin": [-6, 14, -6], "size": [12, 2, 12], "uv": [16, 40]}, + {"origin": [-6, 0, -6], "size": [12, 2, 12], "uv": [16, 40]} + ] + }, + { + "name": "eye", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-1, 7, 0], "size": [2, 2, 1], "uv": [8, 0]}] + }, + { + "name": "tailpart0", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-2, 6, 7], "size": [4, 4, 8], "uv": [40, 0]}] + }, + { + "name": "tailpart1", + "parent": "tailpart0", + "pivot": [0, 24, 0], + "cubes": [{"origin": [0, 7, 0], "size": [3, 3, 7], "uv": [0, 54]}] + }, + { + "name": "tailpart2", + "parent": "tailpart1", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [0, 8, 0], "size": [2, 2, 6], "uv": [41, 32]}, + {"origin": [1, 4.5, 3], "size": [1, 9, 9], "uv": [25, 19]} + ] + }, + { + "name": "spikepart0", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart1", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart2", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart3", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart4", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart5", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart6", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart7", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart8", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart9", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart10", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + }, + { + "name": "spikepart11", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-1, 19.5, -1], "size": [2, 9, 2], "uv": [0, 0]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.guardian"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 36} + }, + "eye_of_ender": { + "identifier": "minecraft:eye_of_ender_signal", + "materials": {"default": "eye_of_ender_signal"}, + "textures": {"default": "textures/items/ender_eye"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "size": [16, 16, 0], + "uv": [0, 0], + "rotation": [0, 0, 0] + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.item_sprite"] + }, + "ender_pearl": { + "identifier": "minecraft:ender_pearl", + "materials": {"default": "ender_pearl"}, + "textures": {"default": "textures/items/ender_pearl"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "size": [16, 16, 0], + "uv": [0, 0], + "rotation": [0, 0, 0] + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.item_sprite"] + }, + "evoker_fangs": { + "identifier": "minecraft:evocation_fang", + "materials": {"default": "fang"}, + "textures": {"default": "textures/entity/illager/evoker_fangs"}, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 3, + "visible_bounds_offset": [0, 1.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "upper_jaw", + "parent": "base", + "pivot": [0, 11, 0], + "cubes": [ + { + "origin": [-1.5, 0, -4], + "size": [4, 14, 8], + "uv": [40, 0], + "inflate": 0.01 + } + ] + }, + { + "name": "lower_jaw", + "parent": "base", + "pivot": [0, 11, 0], + "bind_pose_rotation": [0, 180, 0], + "cubes": [ + {"origin": [-1.5, 0, -4], "size": [4, 14, 8], "uv": [40, 0]} + ] + }, + { + "name": "base", + "pivot": [0, 0, 0], + "bind_pose_rotation": [0, 90, 0], + "cubes": [ + {"origin": [-5, 0, -5], "size": [10, 12, 10], "uv": [0, 0]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.evocation_fang"] + }, + "evoker": { + "identifier": "minecraft:evocation_illager", + "min_engine_version": "1.8.0", + "materials": {"default": "evoker"}, + "textures": {"default": "textures/entity/illager/evoker"}, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 2.5, + "visible_bounds_offset": [0, 1.25, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 10, 8], "uv": [0, 0]} + ] + }, + { + "name": "nose", + "parent": "head", + "pivot": [0, 26, 0], + "cubes": [ + {"origin": [-1, 23, -6], "size": [2, 4, 2], "uv": [24, 0]} + ] + }, + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -3], "size": [8, 12, 6], "uv": [16, 20]}, + { + "origin": [-4, 6, -3], + "size": [8, 18, 6], + "uv": [0, 38], + "inflate": 0.5 + } + ] + }, + { + "name": "arms", + "parent": "body", + "pivot": [0, 22, 0], + "cubes": [ + {"origin": [-8, 16, -2], "size": [4, 8, 4], "uv": [44, 22]}, + {"origin": [4, 16, -2], "size": [4, 8, 4], "uv": [44, 22]}, + {"origin": [-4, 16, -2], "size": [8, 4, 4], "uv": [40, 38]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [0, 22]} + ] + }, + { + "name": "leg1", + "parent": "body", + "pivot": [2, 12, 0], + "mirror": true, + "cubes": [{"origin": [0, 0, -2], "size": [4, 12, 4], "uv": [0, 22]}] + }, + { + "name": "rightArm", + "parent": "body", + "pivot": [-5, 22, 0], + "locators": {"right_hand": [-6, 12, 0]}, + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 46]} + ] + }, + { + "name": "rightItem", + "pivot": [-5.5, 16, 0.5], + "neverRender": true, + "parent": "rightArm" + }, + { + "name": "leftArm", + "parent": "body", + "pivot": [5, 22, 0], + "locators": {"left_hand": [6, 12, 0]}, + "mirror": true, + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 46]} + ] + } + ] + } + }, + "particle_effects": {"spell": "minecraft:evoker_spell"}, + "render_controllers": ["controller.render.evoker"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 40} + }, + "experience_bottle": { + "identifier": "minecraft:xp_bottle", + "materials": {"default": "xp_bottle"}, + "textures": { + "default": "textures/items/experience_bottle", + "enchanted": "textures/misc/enchanted_item_glint" + }, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "size": [16, 16, 0], + "uv": [0, 0], + "rotation": [0, 0, 0] + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.experience_bottle"] + }, + "experience_orb": { + "identifier": "minecraft:xp_orb", + "materials": {"default": "experience_orb"}, + "textures": {"default": "textures/entity/experience_orb"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [8, 8, 0], + "cubes": [ + { + "origin": [0, 0, 0], + "size": [16, 16, 0], + "uv": {"south": {"uv": [0, 0]}} + } + ] + } + ], + "texturewidth": 64, + "textureheight": 64 + } + }, + "render_controllers": ["controller.render.experience_orb"] + }, + "fireball": { + "identifier": "minecraft:fireball", + "materials": {"default": "fireball"}, + "textures": {"default": "textures/items/fire_charge"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -4, 0], + "size": [16, 16, 0], + "uv": {"south": {"uv": [0, 0]}} + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.fireball"] + }, + "firework_rocket": { + "identifier": "minecraft:fireworks_rocket", + "materials": {"default": "fireworks_rocket"}, + "textures": {"default": "textures/entity/fireworks"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "rotation": [0, 90, 0], + "size": [16, 16, 0], + "uv": {"north": {"uv": [0, 0]}} + }, + { + "origin": [-8, -8, 0], + "rotation": [90, 90, 0], + "size": [16, 16, 0], + "uv": {"north": {"uv": [0, 0]}} + } + ] + } + ], + "texturewidth": 32, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.fireworks_rocket"] + }, + "fishing_bobber": { + "identifier": "minecraft:fishing_hook", + "materials": {"default": "fishing_hook"}, + "textures": {"default": "textures/entity/fishing_hook"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-1.5, -1.5, -1.5], + "size": [3, 3, 3], + "rotation": [0, 0, 180], + "uv": { + "up": {"uv": [0, 0]}, + "down": {"uv": [3, 0]}, + "south": {"uv": [9, 0], "uv_size": [-3, 3]}, + "north": {"uv": [9, 0]}, + "east": {"uv": [12, 0]}, + "west": {"uv": [15, 0]} + } + }, + { + "origin": [0, -4.5, -0.5], + "size": [0, 3, 3], + "uv": {"east": {"uv": [18, 0]}} + }, + { + "origin": [0, 1.5, -1.5], + "size": [0, 3, 3], + "uv": {"east": {"uv": [21, 0]}} + }, + { + "origin": [-1.5, 1.5, 0], + "size": [3, 3, 0], + "uv": {"north": {"uv": [21, 0]}} + } + ] + } + ], + "texturewidth": 24, + "textureheight": 3 + } + }, + "render_controllers": ["controller.render.fishing_hook"] + }, + "hoglin": { + "identifier": "minecraft:hoglin", + "materials": {"default": "hoglin"}, + "textures": {"default": "textures/entity/hoglin/hoglin"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 19, -3], + "cubes": [ + { + "origin": [-8, 11, -7], + "size": [16, 14, 26], + "inflate": 0.02, + "uv": [1, 1] + }, + { + "origin": [0, 22, -10], + "size": [0, 10, 19], + "inflate": 0.02, + "uv": [90, 33] + } + ], + "locators": {"lead": [0, 20, -5]} + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 22, -5], + "rotation": [50, 0, 0], + "cubes": [ + {"origin": [-7, 21, -24], "size": [14, 6, 19], "uv": [61, 1]}, + {"origin": [-8, 22, -19], "size": [2, 11, 2], "uv": [1, 13]}, + {"origin": [6, 22, -19], "size": [2, 11, 2], "uv": [1, 13]} + ] + }, + { + "name": "right_ear", + "parent": "head", + "pivot": [-7, 27, -7], + "rotation": [0, 0, -50], + "cubes": [ + {"origin": [-13, 26, -10], "size": [6, 1, 4], "uv": [1, 1]} + ] + }, + { + "name": "left_ear", + "parent": "head", + "pivot": [7, 27, -7], + "rotation": [0, 0, 50], + "cubes": [{"origin": [7, 26, -10], "size": [6, 1, 4], "uv": [1, 6]}] + }, + { + "name": "leg_back_right", + "pivot": [6, 8, 17], + "cubes": [ + {"origin": [-8, 0, 13], "size": [5, 11, 5], "uv": [21, 45]} + ] + }, + { + "name": "leg_back_left", + "pivot": [-6, 8, 17], + "cubes": [{"origin": [3, 0, 13], "size": [5, 11, 5], "uv": [0, 45]}] + }, + { + "name": "leg_front_right", + "pivot": [-6, 12, -3], + "cubes": [ + {"origin": [-8, 0, -6], "size": [6, 14, 6], "uv": [66, 42]} + ] + }, + { + "name": "leg_front_left", + "pivot": [6, 12, -3], + "cubes": [ + {"origin": [2, 0, -6], "size": [6, 14, 6], "uv": [41, 42]} + ] + } + ], + "visible_bounds_width": 4, + "visible_bounds_height": 3, + "visible_bounds_offset": [0, 1.5, 0], + "texturewidth": 128, + "textureheight": 64 + } + }, + "spawn_egg": {"base_color": "#C66E55", "overlay_color": "#5f6464"}, + "render_controllers": ["controller.render.hoglin"] + }, + "hopper_minecart": { + "identifier": "minecraft:hopper_minecart", + "min_engine_version": "1.8.0", + "materials": {"default": "minecart"}, + "textures": {"default": "textures/entity/minecart"}, + "geometry": { + "default": { + "bones": [ + { + "name": "bottom", + "pivot": [0, 6, 0], + "cubes": [ + { + "origin": [-10, -6.5, -1], + "size": [20, 16, 2], + "rotation": [90, 0, 0], + "uv": [0, 10] + } + ] + }, + { + "name": "back", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-17, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 270, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "front", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [1, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 90, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "right", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, 2.5, -8], + "size": [16, 8, 2], + "rotation": [0, 180, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "left", + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-8, 2.5, 6], "size": [16, 8, 2], "uv": [0, 0]} + ], + "parent": "bottom" + } + ], + "texturewidth": 64, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.minecart"] + }, + "husk": { + "identifier": "minecraft:husk", + "min_engine_version": "1.8.0", + "materials": {"default": "husk"}, + "textures": {"default": "textures/entity/zombie/husk"}, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 2.5, + "visible_bounds_offset": [0, 1.25, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ], + "parent": "waist" + }, + {"name": "waist", "neverRender": true, "pivot": [0, 12, 0]}, + { + "name": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]} + ], + "parent": "body" + }, + { + "name": "hat", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ], + "neverRender": true, + "parent": "head" + }, + { + "name": "rightArm", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]} + ], + "parent": "body" + }, + { + "name": "rightItem", + "pivot": [-1, -45, -5], + "neverRender": true, + "parent": "rightArm" + }, + { + "name": "leftArm", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 16]} + ], + "mirror": true, + "parent": "body" + }, + { + "name": "leftItem", + "pivot": [1, -45, -5], + "neverRender": true, + "parent": "leftArm" + }, + { + "name": "rightLeg", + "pivot": [-1.9, 12, 0], + "cubes": [ + {"origin": [-3.9, 0, -2], "size": [4, 12, 4], "uv": [0, 16]} + ], + "parent": "body" + }, + { + "name": "leftLeg", + "pivot": [1.9, 12, 0], + "cubes": [ + {"origin": [-0.1, 0, -2], "size": [4, 12, 4], "uv": [0, 16]} + ], + "mirror": true, + "parent": "body" + } + ] + } + }, + "scripts": { + "pre_animation": [ + "variable.tcos0 = (Math.cos(query.modified_distance_moved * 38.17) * query.modified_move_speed / variable.gliding_speed_value) * 57.3;" + ] + }, + "animations": { + "humanoid_big_head": {"loop": true, "bones": {"head": {"scale": 1.4}}}, + "look_at_target_default": { + "loop": true, + "bones": { + "head": { + "relative_to": {"rotation": "entity"}, + "rotation": [ + "query.target_x_rotation", + "query.target_y_rotation", + 0 + ] + } + } + }, + "look_at_target_gliding": { + "loop": true, + "bones": {"head": {"rotation": [-45, "query.target_y_rotation", 0]}} + }, + "look_at_target_swimming": { + "loop": true, + "bones": { + "head": { + "rotation": [ + "math.lerp(query.target_x_rotation, -45.0, variable.swim_amount)", + "query.target_y_rotation", + 0 + ] + } + } + }, + "move": { + "loop": true, + "bones": { + "leftarm": {"rotation": ["variable.tcos0", 0, 0]}, + "leftleg": {"rotation": ["variable.tcos0 * -1.4", 0, 0]}, + "rightarm": {"rotation": ["-variable.tcos0", 0, 0]}, + "rightleg": {"rotation": ["variable.tcos0 * 1.4", 0, 0]} + } + }, + "riding.arms": { + "loop": true, + "bones": { + "leftarm": {"rotation": [-36, 0, 0]}, + "rightarm": {"rotation": [-36, 0, 0]} + } + }, + "riding.legs": { + "loop": true, + "bones": { + "leftleg": {"rotation": ["-72.0 - this", "-18.0 - this", "-this"]}, + "rightleg": {"rotation": ["-72.0 - this", "18.0 - this", "-this"]} + } + }, + "holding": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + "variable.is_holding_left ? (-this * 0.5 - 18.0) : 0.0", + 0, + 0 + ] + }, + "rightarm": { + "rotation": [ + "variable.is_holding_right ? (-this * 0.5 - 18.0) : 0.0", + 0, + 0 + ] + } + } + }, + "brandish_spear": { + "loop": true, + "bones": { + "rightarm": { + "rotation": [ + "this * -0.5 - 157.5 - 22.5 * variable.charge_amount", + "-this", + 0 + ] + } + } + }, + "charging": { + "loop": true, + "bones": { + "rightarm": { + "rotation": ["22.5 * variable.charge_amount - this", "-this", 0] + } + } + }, + "attack.rotations": { + "loop": true, + "bones": { + "body": { + "rotation": [ + 0, + "math.sin(math.sqrt(variable.attack_time) * 360) * 11.46 - this", + 0 + ] + }, + "leftarm": { + "rotation": [ + "math.sin(math.sqrt(variable.attack_time) * 360) * 11.46", + 0, + 0 + ] + }, + "rightarm": { + "rotation": [ + "math.sin(1.0 - math.pow(1.0 - variable.attack_time, 3.0) * 180.0) * (variable.is_brandishing_spear ? -1.0 : 1.0 )", + "variable.is_brandishing_spear ? 0.0 : (math.sin(math.sqrt(variable.attack_time) * 360) * 11.46) * 2.0", + 0 + ] + } + } + }, + "sneaking": { + "loop": true, + "bones": { + "body": {"rotation": ["0.5 - this", 0, 0]}, + "head": {"position": [0, 1, 0]}, + "leftarm": {"rotation": [72, 0, 0]}, + "leftleg": {"position": [0, -3, 4]}, + "rightarm": {"rotation": [72, 0, 0]}, + "rightleg": {"position": [0, -3, 4]} + } + }, + "bob": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + 0, + 0, + "((math.cos(query.life_time * 103.2) * 2.865) + 2.865) *-1.0" + ] + }, + "rightarm": { + "rotation": [ + 0, + 0, + "(math.cos(query.life_time * 103.2) * 2.865) + 2.865" + ] + } + } + }, + "damage_nearby_mobs": { + "loop": true, + "bones": { + "leftarm": {"rotation": ["-45.0-this", "-this", "-this"]}, + "leftleg": {"rotation": ["45.0-this", "-this", "-this"]}, + "rightarm": {"rotation": ["45.0-this", "-this", "-this"]}, + "rightleg": {"rotation": ["-45.0-this", "-this", "-this"]} + } + }, + "bow_and_arrow": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + "query.target_x_rotation - 90.0 - math.sin(query.life_time * 76.8) * 2.865 - this", + "query.target_y_rotation + 28.65", + "-(math.cos(query.life_time * 103.2) * 2.865) - 2.865" + ] + }, + "rightarm": { + "rotation": [ + "query.target_x_rotation - 90.0 + math.sin(query.life_time * 76.8) * 2.865 - this", + "query.target_y_rotation - 5.73", + "(math.cos(query.life_time * 103.2) * 2.865) + 2.865" + ] + } + } + }, + "use_item_progress": { + "loop": true, + "bones": { + "rightarm": { + "rotation": [ + "variable.use_item_startup_progress * -60.0 + variable.use_item_interval_progress * 11.25", + "variable.use_item_startup_progress * -22.5 + variable.use_item_interval_progress * 11.25", + "variable.use_item_startup_progress * -5.625 + variable.use_item_interval_progress * 11.25" + ] + } + } + }, + "zombie_attack_bare_hand": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + "-90.0 - ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4) - (math.sin(query.life_time * 76.776372) * 2.865) - this", + "5.73 - ((math.sin(variable.attack_time * 180.0) * 57.3) * 0.6) - this", + "math.cos(query.life_time * 103.13244) * -2.865 - 2.865 - this" + ] + }, + "rightarm": { + "rotation": [ + "90.0 * (variable.is_brandishing_spear - 1.0) - ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4) + (math.sin(query.life_time * 76.776372) * 2.865) - this", + "(math.sin(variable.attack_time * 180.0) * 57.3) * 0.6 - 5.73 - this", + "math.cos(query.life_time * 103.13244) * 2.865 + 2.865 - this" + ] + } + } + }, + "swimming": { + "loop": true, + "bones": { + "body": { + "position": [ + 0, + "variable.swim_amount * -10.0 - this", + "variable.swim_amount * 9.0 - this" + ], + "rotation": [ + "variable.swim_amount * (90.0 + query.target_x_rotation)", + 0, + 0 + ] + }, + "leftarm": { + "rotation": [ + "math.lerp(this, -180.0, variable.swim_amount) - (variable.swim_amount * ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4)) - (variable.swim_amount * (math.sin(query.life_time * 76.776372) * 2.865)) - this", + "math.lerp(this, 14.325, variable.swim_amount) - this", + "math.lerp(this, 14.325, variable.swim_amount) - (variable.swim_amount * (math.cos(query.life_time * 103.13244) * 2.865 + 2.865)) - this" + ] + }, + "leftleg": { + "rotation": [ + "math.lerp(this, math.cos(query.life_time * 390.0 + 180.0) * 0.3, variable.swim_amount)", + 0, + 0 + ] + }, + "rightarm": { + "rotation": [ + "math.lerp(this, -180.0, variable.swim_amount) - (variable.swim_amount * ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4)) + (variable.swim_amount * (math.sin(query.life_time * 76.776372) * 2.865)) - this", + "math.lerp(this, 14.325, variable.swim_amount) - this", + "math.lerp(this, -14.325, variable.swim_amount) + (variable.swim_amount * (math.cos(query.life_time * 103.13244) * 2.865 + 2.865)) - this" + ] + }, + "rightleg": { + "rotation": [ + "math.lerp(this, math.cos(query.life_time * 390.0) * 0.3, variable.swim_amount)", + 0, + 0 + ] + } + } + } + }, + "animation_controllers": { + "humanoid_baby_big_head": { + "initial_state": "default", + "states": { + "baby": { + "animations": ["humanoid_big_head"], + "transitions": [{"default": "!query.is_baby"}] + }, + "default": {"transitions": [{"baby": "query.is_baby"}]} + } + }, + "look_at_target": { + "initial_state": "default", + "states": { + "default": { + "animations": ["look_at_target_default"], + "transitions": [ + {"gliding": "query.is_gliding"}, + {"swimming": "query.is_swimming"} + ] + }, + "gliding": { + "animations": ["look_at_target_gliding"], + "transitions": [ + {"swimming": "query.is_swimming"}, + {"default": "!query.is_gliding"} + ] + }, + "swimming": { + "animations": ["look_at_target_swimming"], + "transitions": [ + {"gliding": "query.is_gliding"}, + {"default": "!query.is_swimming"} + ] + } + } + }, + "move": { + "initial_state": "default", + "states": {"default": {"animations": ["move"]}} + }, + "riding": { + "initial_state": "default", + "states": { + "default": {"transitions": [{"riding": "query.is_riding"}]}, + "riding": { + "animations": ["riding.arms", "riding.legs"], + "transitions": [{"default": "!query.is_riding"}] + } + } + }, + "holding": { + "initial_state": "default", + "states": {"default": {"animations": ["holding"]}} + }, + "brandish_spear": { + "initial_state": "default", + "states": { + "brandish_spear": { + "animations": ["brandish_spear"], + "transitions": [{"default": "!variable.is_brandishing_spear"}] + }, + "default": { + "transitions": [{"brandish_spear": "variable.is_brandishing_spear"}] + } + } + }, + "charging": { + "initial_state": "default", + "states": { + "charging": { + "animations": ["charging"], + "transitions": [{"default": "!query.is_charging"}] + }, + "default": {"transitions": [{"charging": "query.is_charging"}]} + } + }, + "attack": { + "initial_state": "default", + "states": { + "attacking": { + "animations": ["attack.rotations"], + "transitions": [{"default": "variable.attack_time < 0.0"}] + }, + "default": { + "transitions": [{"attacking": "variable.attack_time >= 0.0"}] + } + } + }, + "sneaking": { + "initial_state": "default", + "states": { + "default": {"transitions": [{"sneaking": "query.is_sneaking"}]}, + "sneaking": { + "animations": ["sneaking"], + "transitions": [{"default": "!query.is_sneaking"}] + } + } + }, + "bob": { + "initial_state": "default", + "states": {"default": {"animations": ["bob"]}} + }, + "damage_nearby_mobs": { + "initial_state": "default", + "states": { + "damage_nearby_mobs": { + "animations": ["damage_nearby_mobs"], + "transitions": [{"default": "!variable.damage_nearby_mobs"}] + }, + "default": { + "transitions": [ + {"damage_nearby_mobs": "variable.damage_nearby_mobs"} + ] + } + } + }, + "bow_and_arrow": { + "initial_state": "default", + "states": { + "bow_and_arrow": { + "animations": ["bow_and_arrow"], + "transitions": [{"default": "!query.has_target"}] + }, + "default": {"transitions": [{"bow_and_arrow": "query.has_target"}]} + } + }, + "use_item_progress": { + "initial_state": "default", + "states": { + "default": { + "transitions": [ + { + "use_item_progress": "( variable.use_item_interval_progress > 0.0 ) || ( variable.use_item_startup_progress > 0.0 )" + } + ] + }, + "use_item_progress": { + "animations": ["use_item_progress"], + "transitions": [ + { + "default": "( variable.use_item_interval_progress <= 0.0 ) && ( variable.use_item_startup_progress <= 0.0 )" + } + ] + } + } + }, + "zombie_attack_bare_hand": { + "initial_state": "default", + "states": { + "default": { + "transitions": [{"is_bare_hand": "variable.is_holding_left != 1.0"}] + }, + "is_bare_hand": { + "animations": ["zombie_attack_bare_hand"], + "transitions": [{"default": "variable.is_holding_left == 1.0"}] + } + } + }, + "swimming": { + "initial_state": "default", + "states": { + "default": { + "transitions": [{"is_swimming": "variable.swim_amount > 0.0"}] + }, + "is_swimming": { + "animations": ["swimming"], + "transitions": [{"default": "variable.swim_amount <= 0.0"}] + } + } + } + }, + "render_controllers": ["controller.render.husk"], + "enable_attachables": true, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 28} + }, + "iron_golem": { + "identifier": "minecraft:iron_golem", + "materials": {"default": "iron_golem"}, + "textures": {"default": "textures/entity/iron_golem/iron_golem"}, + "geometry": { + "default": { + "visible_bounds_width": 3, + "visible_bounds_height": 3, + "visible_bounds_offset": [0, 1.5, 0], + "texturewidth": 128, + "textureheight": 128, + "bones": [ + { + "name": "body", + "pivot": [0, 31, 0], + "cubes": [ + {"origin": [-9, 21, -6], "size": [18, 12, 11], "uv": [0, 40]}, + { + "origin": [-4.5, 16, -3], + "size": [9, 5, 6], + "uv": [0, 70], + "inflate": 0.5 + } + ] + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 31, -2], + "locators": {"lead": [0, 31, -2]}, + "cubes": [ + {"origin": [-4, 33, -7.5], "size": [8, 10, 8], "uv": [0, 0]}, + {"origin": [-1, 32, -9.5], "size": [2, 4, 2], "uv": [24, 0]} + ] + }, + { + "name": "arm0", + "parent": "body", + "pivot": [0, 31, 0], + "cubes": [ + {"origin": [-13, 3.5, -3], "size": [4, 30, 6], "uv": [60, 21]} + ] + }, + { + "name": "arm1", + "parent": "body", + "pivot": [0, 31, 0], + "cubes": [ + {"origin": [9, 3.5, -3], "size": [4, 30, 6], "uv": [60, 58]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-4, 13, 0], + "cubes": [ + {"origin": [-7.5, 0, -3], "size": [6, 16, 5], "uv": [37, 0]} + ] + }, + { + "name": "leg1", + "parent": "body", + "mirror": true, + "pivot": [5, 13, 0], + "cubes": [ + {"origin": [1.5, 0, -3], "size": [6, 16, 5], "uv": [60, 0]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.iron_golem"] + }, + "item_frame": { + "identifier": "minecraft:item_frame", + "materials": {"default": "item_frame"}, + "textures": { + "background": "block:item_frame", + "frame": "block:oak_planks" + }, + "geometry": { + "background": { + "bones": [ + { + "name": "base" + }, + { + "name": "background", + "parent": "base", + "rotation": [0, 180, 0], + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-5, -5, -8], "size": [10, 10, 0.5], "uv": [3, 3]} + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + }, + "frame": { + "bones": [ + { + "name": "frame", + "parent": "base", + "rotation": [0, 180, 0], + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-6, -6, -8], "size": [12, 1, 1], "uv": [2, 2]}, + {"origin": [-6, 5, -8], "size": [12, 1, 1], "uv": [2, 13]}, + {"origin": [-6, -5, -8], "size": [1, 10, 1], "uv": [2, 3]}, + {"origin": [5, -5, -8], "size": [1, 10, 1], "uv": [13, 3]} + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.item_frame"] + }, + "leash_knot": { + "identifier": "minecraft:leash_knot", + "materials": {"default": "leash_knot"}, + "textures": {"default": "textures/entity/lead_knot"}, + "geometry": { + "default": { + "bones": [ + { + "name": "knot", + "rotation": [0, 180, 0], + "cubes": [{"origin": [5, 6, 5], "size": [6, 8, 6], "uv": [0, 0]}] + } + ], + "texturewidth": 32, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.leash_knot"] + }, + "llama_spit": { + "identifier": "minecraft:llama_spit", + "materials": {"default": "llama_spit"}, + "textures": {"default": "textures/entity/llama/spit"}, + "geometry": { + "default": { + "visible_bounds_width": 1, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 22, 0], "size": [2, 2, 2], "uv": [0, 0]}, + {"origin": [0, 26, 0], "size": [2, 2, 2], "uv": [0, 0]}, + {"origin": [0, 22, -4], "size": [2, 2, 2], "uv": [0, 0]}, + {"origin": [0, 22, 0], "size": [2, 2, 2], "uv": [0, 0]}, + {"origin": [2, 22, 0], "size": [2, 2, 2], "uv": [0, 0]}, + {"origin": [0, 20, 0], "size": [2, 2, 2], "uv": [0, 0]}, + {"origin": [0, 22, 2], "size": [2, 2, 2], "uv": [0, 0]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.llama_spit"] + }, + "magma_cube": { + "identifier": "minecraft:magma_cube", + "materials": {"default": "magma_cube"}, + "textures": {"default": "textures/entity/slime/magmacube"}, + "geometry": { + "default": { + "visible_bounds_width": 2.5, + "visible_bounds_height": 5, + "visible_bounds_offset": [0, 2.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "bodyCube_0", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 7, -4], "size": [8, 1, 8], "uv": [0, 0]}] + }, + { + "name": "bodyCube_1", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 6, -4], "size": [8, 1, 8], "uv": [0, 1]}] + }, + { + "name": "bodyCube_2", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 5, -4], "size": [8, 1, 8], "uv": [24, 10]} + ] + }, + { + "name": "bodyCube_3", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 4, -4], "size": [8, 1, 8], "uv": [24, 19]} + ] + }, + { + "name": "bodyCube_4", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 3, -4], "size": [8, 1, 8], "uv": [0, 4]}] + }, + { + "name": "bodyCube_5", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 2, -4], "size": [8, 1, 8], "uv": [0, 5]}] + }, + { + "name": "bodyCube_6", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 1, -4], "size": [8, 1, 8], "uv": [0, 6]}] + }, + { + "name": "bodyCube_7", + "parent": "insideCube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 0, -4], "size": [8, 1, 8], "uv": [0, 7]}] + }, + { + "name": "insideCube", + "pivot": [0, 0, 0], + "cubes": [{"origin": [-2, 2, -2], "size": [4, 4, 4], "uv": [0, 16]}] + } + ] + } + }, + "render_controllers": ["controller.render.magma_cube"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 20} + }, + "mooshroom": { + "identifier": "minecraft:mooshroom", + "min_engine_version": "1.8.0", + "materials": {"default": "mooshroom"}, + "textures": { + "default": "textures/entity/cow/red_mooshroom", + "brown": "textures/entity/cow/brown_mooshroom" + }, + "geometry": { + "default": { + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 19, 2], + "bind_pose_rotation": [90, 0, 0], + "cubes": [ + {"origin": [-6, 11, -5], "size": [12, 18, 10], "uv": [18, 4]}, + {"origin": [-2, 11, -6], "size": [4, 6, 1], "uv": [52, 0]} + ] + }, + { + "name": "head", + "pivot": [0, 20, -8], + "locators": {"lead": [0, 20, -8]}, + "cubes": [ + {"origin": [-4, 16, -14], "size": [8, 8, 6], "uv": [0, 0]}, + {"origin": [-5, 22, -12], "size": [1, 3, 1], "uv": [22, 0]}, + {"origin": [4, 22, -12], "size": [1, 3, 1], "uv": [22, 0]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-4, 12, 7], + "cubes": [{"origin": [-6, 0, 5], "size": [4, 12, 4], "uv": [0, 16]}] + }, + { + "name": "leg1", + "parent": "body", + "mirror": true, + "pivot": [4, 12, 7], + "cubes": [{"origin": [2, 0, 5], "size": [4, 12, 4], "uv": [0, 16]}] + }, + { + "name": "leg2", + "parent": "body", + "pivot": [-4, 12, -6], + "cubes": [ + {"origin": [-6, 0, -7], "size": [4, 12, 4], "uv": [0, 16]} + ] + }, + { + "name": "leg3", + "parent": "body", + "mirror": true, + "pivot": [4, 12, -6], + "cubes": [{"origin": [2, 0, -7], "size": [4, 12, 4], "uv": [0, 16]}] + } + ] + } + }, + "render_controllers": ["controller.render.mooshroom"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 5} + }, + "panda": { + "identifier": "minecraft:panda", + "materials": {"default": "panda"}, + "textures": { + "default": "textures/entity/panda/panda", + "lazy": "textures/entity/panda/lazy_panda", + "worried": "textures/entity/panda/worried_panda", + "playful": "textures/entity/panda/playful_panda", + "brown": "textures/entity/panda/brown_panda", + "weak": "textures/entity/panda/weak_panda", + "aggressive": "textures/entity/panda/aggressive_panda" + }, + "geometry": { + "default": { + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "head", + "parent": "body", + "pivot": [0, 12.5, -17], + "locators": {"lead": [0, 14, -16]}, + "cubes": [ + {"origin": [-6.5, 7.5, -21], "size": [13, 10, 9], "uv": [0, 6]}, + {"origin": [-3.5, 7.5, -23], "size": [7, 5, 2], "uv": [45, 16]}, + {"origin": [-8.5, 16.5, -18], "size": [5, 4, 1], "uv": [52, 25]}, + {"origin": [3.5, 16.5, -18], "size": [5, 4, 1], "uv": [52, 25]} + ] + }, + { + "name": "body", + "pivot": [0, 14, 0], + "bind_pose_rotation": [90, 0, 0], + "cubes": [ + {"origin": [-9.5, 1, -6.5], "size": [19, 26, 13], "uv": [0, 25]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-5.5, 9, 9], + "cubes": [ + {"origin": [-8.5, 0, 6], "size": [6, 9, 6], "uv": [40, 0]} + ] + }, + { + "name": "leg1", + "parent": "body", + "pivot": [5.5, 9, 9], + "cubes": [{"origin": [2.5, 0, 6], "size": [6, 9, 6], "uv": [40, 0]}] + }, + { + "name": "leg2", + "parent": "body", + "pivot": [-5.5, 9, -9], + "cubes": [ + {"origin": [-8.5, 0, -12], "size": [6, 9, 6], "uv": [40, 0]} + ] + }, + { + "name": "leg3", + "parent": "body", + "pivot": [5.5, 9, -9], + "cubes": [ + {"origin": [2.5, 0, -12], "size": [6, 9, 6], "uv": [40, 0]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.panda"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 54} + }, + "phantom": { + "identifier": "minecraft:phantom", + "materials": {"default": "phantom", "invisible": "phantom_invisible"}, + "textures": {"default": "textures/entity/phantom"}, + "geometry": { + "default": { + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "bind_pose_rotation": [0, 0, 0], + "cubes": [{"origin": [-3, 23, -8], "size": [5, 3, 9], "uv": [0, 8]}] + }, + { + "name": "wing0", + "pivot": [2, 26, -8], + "bind_pose_rotation": [0, 0, 5.7], + "cubes": [ + {"origin": [2, 24, -8], "size": [6, 2, 9], "uv": [23, 12]} + ], + "parent": "body" + }, + { + "name": "wingtip0", + "pivot": [8, 26, -8], + "bind_pose_rotation": [0, 0, 5.7], + "locators": {"left_wing": [21, 26, 0]}, + "cubes": [ + {"origin": [8, 25, -8], "size": [13, 1, 9], "uv": [16, 24]} + ], + "parent": "wing0" + }, + { + "name": "wing1", + "pivot": [-3, 26, -8], + "bind_pose_rotation": [0, 0, -5.7], + "mirror": true, + "cubes": [ + {"origin": [-9, 24, -8], "size": [6, 2, 9], "uv": [23, 12]} + ], + "parent": "body" + }, + { + "name": "wingtip1", + "pivot": [-9, 24, -8], + "bind_pose_rotation": [0, 0, -5.7], + "locators": {"right_wing": [-22, 24, 0]}, + "mirror": true, + "cubes": [ + {"origin": [-22, 25, -8], "size": [13, 1, 9], "uv": [16, 24]} + ], + "parent": "wing1" + }, + { + "name": "head", + "pivot": [0, 23, -7], + "bind_pose_rotation": [11.5, 0, 0], + "cubes": [ + {"origin": [-4, 22, -12], "size": [7, 3, 5], "uv": [0, 0]} + ], + "parent": "body" + }, + { + "name": "tail", + "pivot": [0, 26, 1], + "bind_pose_rotation": [0, 0, 0], + "cubes": [ + {"origin": [-2, 24, 1], "size": [3, 2, 6], "uv": [3, 20]} + ], + "parent": "body" + }, + { + "name": "tailtip", + "pivot": [0, 25.5, 7], + "bind_pose_rotation": [0, 0, 0], + "cubes": [ + {"origin": [-1, 24.5, 7], "size": [1, 1, 6], "uv": [4, 29]} + ], + "parent": "tail" + } + ] + } + }, + "particle_effects": {"wing_dust": "minecraft:phantom_trail_particle"}, + "sound_effects": {"flap": "mob.phantom.flap"}, + "render_controllers": ["controller.render.phantom"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 51} + }, + "pig": { + "identifier": "minecraft:pig", + "min_engine_version": "1.8.0", + "materials": {"default": "pig"}, + "textures": { + "default": "textures/entity/pig/pig", + "saddled": "textures/entity/pig/pig_saddle" + }, + "geometry": { + "default": { + "visible_bounds_width": 2, + "visible_bounds_height": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 13, 2], + "bind_pose_rotation": [90, 0, 0], + "cubes": [ + {"origin": [-5, 7, -5], "size": [10, 16, 8], "uv": [28, 8]} + ] + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 12, -6], + "locators": {"lead": [0, 14, -6]}, + "cubes": [ + {"origin": [-4, 8, -14], "size": [8, 8, 8], "uv": [0, 0]}, + {"origin": [-2, 9, -15], "size": [4, 3, 1], "uv": [16, 16]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-3, 6, 7], + "cubes": [{"origin": [-5, 0, 5], "size": [4, 6, 4], "uv": [0, 16]}] + }, + { + "name": "leg1", + "parent": "body", + "mirror": true, + "pivot": [3, 6, 7], + "cubes": [{"origin": [1, 0, 5], "size": [4, 6, 4], "uv": [0, 16]}] + }, + { + "name": "leg2", + "parent": "body", + "pivot": [-3, 6, -5], + "cubes": [{"origin": [-5, 0, -7], "size": [4, 6, 4], "uv": [0, 16]}] + }, + { + "name": "leg3", + "parent": "body", + "mirror": true, + "pivot": [3, 6, -5], + "cubes": [{"origin": [1, 0, -7], "size": [4, 6, 4], "uv": [0, 16]}] + } + ] + } + }, + "render_controllers": ["controller.render.pig"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 2} + }, + "piglin_brute": { + "identifier": "minecraft:piglin_brute", + "materials": {"default": "piglin_brute"}, + "textures": {"default": "textures/entity/piglin/piglin_brute"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]}, + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 32], + "inflate": 0.25 + } + ] + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-5, 24, -4], + "size": [10, 8, 8], + "uv": [0, 0], + "inflate": -0.02 + }, + {"origin": [-2, 24, -5], "size": [4, 4, 1], "uv": [31, 1]}, + {"origin": [2, 24, -5], "size": [1, 2, 1], "uv": [2, 4]}, + {"origin": [-3, 24, -5], "size": [1, 2, 1], "uv": [2, 0]} + ], + "inflate": -0.02 + }, + { + "name": "leftear", + "parent": "head", + "pivot": [5, 30, 0], + "rotation": [0, 0, -30], + "cubes": [{"origin": [4, 25, -2], "size": [1, 5, 4], "uv": [51, 6]}] + }, + { + "name": "rightear", + "parent": "head", + "pivot": [-5, 30, 0], + "rotation": [0, 0, 30], + "cubes": [ + {"origin": [-5, 25, -2], "size": [1, 5, 4], "uv": [39, 6]} + ] + }, + {"name": "hat", "parent": "head", "pivot": [0, 24, 0]}, + { + "name": "rightarm", + "parent": "body", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]}, + { + "origin": [-8, 12, -2], + "size": [4, 12, 4], + "uv": [40, 32], + "inflate": 0.25 + } + ] + }, + {"name": "rightItem", "parent": "rightarm", "pivot": [-1, -45, -5]}, + { + "name": "leftarm", + "parent": "body", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [32, 48]}, + { + "origin": [4, 12, -2], + "size": [4, 12, 4], + "uv": [48, 48], + "inflate": 0.25 + } + ] + }, + {"name": "leftItem", "parent": "leftArm", "pivot": [1, -45, -5]}, + { + "name": "rightleg", + "parent": "body", + "pivot": [-1.9, 12, 0], + "cubes": [ + {"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [0, 16]}, + { + "origin": [-4, 0, -2], + "size": [4, 12, 4], + "uv": [0, 32], + "inflate": 0.25 + } + ] + }, + { + "name": "leftleg", + "parent": "body", + "pivot": [1.9, 12, 0], + "cubes": [ + {"origin": [0, 0, -2], "size": [4, 12, 4], "uv": [16, 48]}, + { + "origin": [0, 0, -2], + "size": [4, 12, 4], + "uv": [0, 48], + "inflate": 0.25 + } + ] + } + ], + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 64 + } + }, + "spawn_egg": {"base_color": "#592A10", "overlay_color": "#F9F3A4"}, + "render_controllers": ["controller.render.piglin_brute"], + "enable_attachables": true + }, + "polar_bear": { + "identifier": "minecraft:polar_bear", + "materials": {"default": "polar_bear"}, + "textures": {"default": "textures/entity/bear/polarbear"}, + "geometry": { + "default": { + "visible_bounds_width": 3, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 128, + "textureheight": 64, + "bones": [ + { + "name": "head", + "parent": "body", + "pivot": [0, 14, -16], + "locators": {"lead": [0, 14, -16]}, + "mirror": true, + "cubes": [ + { + "mirror": false, + "origin": [-3.5, 10, -19], + "size": [7, 7, 7], + "uv": [0, 0] + }, + { + "mirror": false, + "origin": [-2.5, 10, -22], + "size": [5, 3, 3], + "uv": [0, 44] + }, + { + "mirror": false, + "origin": [-4.5, 16, -17], + "size": [2, 2, 1], + "uv": [26, 0] + }, + {"origin": [2.5, 16, -17], "size": [2, 2, 1], "uv": [26, 0]} + ] + }, + { + "name": "body", + "pivot": [-2, 15, 12], + "bind_pose_rotation": [90, 0, 0], + "cubes": [ + {"origin": [-7, 14, 5], "size": [14, 14, 11], "uv": [0, 19]}, + {"origin": [-6, 28, 5], "size": [12, 12, 10], "uv": [39, 0]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-4.5, 10, 6], + "cubes": [ + {"origin": [-6.5, 0, 4], "size": [4, 10, 8], "uv": [50, 22]} + ] + }, + { + "name": "leg1", + "parent": "body", + "pivot": [4.5, 10, 6], + "cubes": [ + {"origin": [2.5, 0, 4], "size": [4, 10, 8], "uv": [50, 22]} + ] + }, + { + "name": "leg2", + "parent": "body", + "pivot": [-3.5, 10, -8], + "cubes": [ + {"origin": [-5.5, 0, -10], "size": [4, 10, 6], "uv": [50, 40]} + ] + }, + { + "name": "leg3", + "parent": "body", + "pivot": [3.5, 10, -8], + "cubes": [ + {"origin": [1.5, 0, -10], "size": [4, 10, 6], "uv": [50, 40]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.polarbear"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 37} + }, + "pufferfish": { + "identifier": "minecraft:pufferfish", + "min_engine_version": "1.8.0", + "materials": {"default": "pufferfish"}, + "textures": {"default": "textures/entity/fish/pufferfish"}, + "geometry": { + "default": { + "visible_bounds_width": 0.5, + "visible_bounds_height": 0.5, + "texturewidth": 32, + "textureheight": 32, + "bones": [ + { + "name": "body", + "cubes": [ + {"origin": [-1.5, 0, -1.5], "size": [3, 2, 3], "uv": [0, 27]}, + {"origin": [0.5, 2, -1.5], "size": [1, 1, 1], "uv": [24, 6]}, + {"origin": [-1.5, 2, -1.5], "size": [1, 1, 1], "uv": [28, 6]} + ], + "locators": {"lead": [0, 0, 0]} + }, + { + "name": "tailfin", + "parent": "body", + "cubes": [ + {"origin": [-1.5, 1, 1.5], "size": [3, 0, 3], "uv": [-3, 0]} + ] + }, + { + "name": "leftFin", + "parent": "body", + "pivot": [6.5, 5, 0.5], + "cubes": [ + { + "origin": [1.5, 0, -1.5], + "size": [1, 1, 2], + "uv": [25, 0], + "mirror": true + } + ] + }, + { + "name": "rightFin", + "parent": "body", + "pivot": [-6.5, 5, 0.5], + "cubes": [ + {"origin": [-2.5, 0, -1.5], "size": [1, 1, 2], "uv": [25, 0]} + ] + } + ] + }, + "mid": { + "visible_bounds_width": 0.5, + "visible_bounds_height": 0.5, + "texturewidth": 32, + "textureheight": 32, + "bones": [ + { + "name": "body", + "cubes": [ + {"origin": [-2.5, 1, -2.5], "size": [5, 5, 5], "uv": [12, 22]} + ] + }, + { + "name": "leftFin", + "parent": "body", + "pivot": [2.5, 5, 0.5], + "cubes": [ + {"origin": [2.5, 4, -1.5], "size": [2, 1, 2], "uv": [24, 3]} + ] + }, + { + "name": "rightFin", + "parent": "body", + "pivot": [-2.5, 5, 0.5], + "cubes": [ + {"origin": [-4.5, 4, -1.5], "size": [2, 1, 2], "uv": [24, 0]} + ] + }, + { + "name": "spines_top_front", + "parent": "body", + "bind_pose_rotation": [45, 0, 0], + "pivot": [0, 6, -2.5], + "cubes": [ + {"origin": [-2.5, 6, -2.5], "size": [5, 1, 0], "uv": [19, 17]} + ] + }, + { + "name": "spines_top_back", + "parent": "body", + "bind_pose_rotation": [-45, 0, 0], + "pivot": [0, 6, 2.5], + "cubes": [ + {"origin": [-2.5, 6, 2.5], "size": [5, 1, 0], "uv": [11, 17]} + ] + }, + { + "name": "spines_bottom_front", + "parent": "body", + "bind_pose_rotation": [-45, 0, 0], + "pivot": [0, 1, -2.5], + "cubes": [ + {"origin": [-2.5, 0, -2.5], "size": [5, 1, 0], "uv": [18, 20]} + ] + }, + { + "name": "spines_bottom_back", + "parent": "body", + "bind_pose_rotation": [45, 0, 0], + "pivot": [0, 1, 2.5], + "rotation": [45, 0, 0], + "cubes": [ + {"origin": [-2.5, 0, 2.5], "size": [5, 1, 0], "uv": [18, 20]} + ] + }, + { + "name": "spines_left_front", + "parent": "body", + "bind_pose_rotation": [0, 45, 0], + "pivot": [2.5, 0, -2.5], + "rotation": [0, 45, 0], + "cubes": [ + {"origin": [2.5, 1, -2.5], "size": [1, 5, 0], "uv": [1, 17]} + ] + }, + { + "name": "spines_left_back", + "parent": "body", + "bind_pose_rotation": [0, -45, 0], + "pivot": [2.5, 0, 2.5], + "rotation": [0, -45, 0], + "cubes": [ + {"origin": [2.5, 1, 2.5], "size": [1, 5, 0], "uv": [1, 17]} + ] + }, + { + "name": "spines_right_front", + "parent": "body", + "bind_pose_rotation": [0, -45, 0], + "pivot": [-2.5, 0, -2.5], + "rotation": [0, -45, 0], + "cubes": [ + {"origin": [-3.5, 1, -2.5], "size": [1, 5, 0], "uv": [5, 17]} + ] + }, + { + "name": "spines_right_back", + "parent": "body", + "bind_pose_rotation": [0, 45, 0], + "pivot": [-2.5, 0, 2.5], + "rotation": [0, 45, 0], + "cubes": [ + {"origin": [-3.5, 1, 2.5], "size": [1, 5, 0], "uv": [9, 17]} + ] + } + ] + }, + "large": { + "visible_bounds_width": 0.5, + "visible_bounds_height": 0.5, + "texturewidth": 32, + "textureheight": 32, + "bones": [ + { + "name": "body", + "cubes": [{"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [0, 0]}] + }, + { + "name": "leftFin", + "parent": "body", + "pivot": [4, 7, 1], + "cubes": [ + {"origin": [4, 6, -2.9904], "size": [2, 1, 2], "uv": [24, 3]} + ] + }, + { + "name": "rightFin", + "parent": "body", + "pivot": [-4, 7, 1], + "cubes": [ + {"origin": [-5.9968, 6, -2.992], "size": [2, 1, 2], "uv": [24, 0]} + ] + }, + { + "name": "spines_top_front", + "parent": "body", + "pivot": [-4, 8, -4], + "bind_pose_rotation": [45, 0, 0], + "cubes": [ + {"origin": [-4, 8, -4], "size": [8, 1, 1], "uv": [14, 16]} + ] + }, + { + "name": "spines_top_mid", + "parent": "body", + "pivot": [0, 8, 0], + "cubes": [{"origin": [-4, 8, 0], "size": [8, 1, 1], "uv": [14, 16]}] + }, + { + "name": "spines_top_back", + "parent": "body", + "pivot": [0, 8, 4], + "bind_pose_rotation": [-45, 0, 0], + "cubes": [{"origin": [-4, 8, 4], "size": [8, 1, 1], "uv": [14, 16]}] + }, + { + "name": "spines_bottom_front", + "parent": "body", + "pivot": [0, 0, -4], + "bind_pose_rotation": [-45, 0, 0], + "cubes": [ + {"origin": [-4, -1, -4], "size": [8, 1, 1], "uv": [14, 19]} + ] + }, + { + "name": "spines_bottom_mid", + "parent": "body", + "pivot": [0, -1, 0], + "cubes": [ + {"origin": [-4, -1, 0], "size": [8, 1, 1], "uv": [14, 19]} + ] + }, + { + "name": "spines_bottom_back", + "parent": "body", + "pivot": [0, 0, 4], + "bind_pose_rotation": [45, 0, 0], + "cubes": [ + {"origin": [-4, -1, 4], "size": [8, 1, 1], "uv": [14, 19]} + ] + }, + { + "name": "spines_left_front", + "parent": "body", + "pivot": [4, 0, -4], + "bind_pose_rotation": [0, 45, 0], + "cubes": [{"origin": [4, 0, -4], "size": [1, 8, 1], "uv": [0, 16]}] + }, + { + "name": "spines_left_mid", + "parent": "body", + "pivot": [4, 0, 0], + "cubes": [ + { + "origin": [4, 0, 0], + "size": [1, 8, 1], + "uv": [4, 16], + "mirror": true + } + ] + }, + { + "name": "spines_left_back", + "parent": "body", + "pivot": [4, 0, 4], + "bind_pose_rotation": [0, -45, 0], + "cubes": [ + { + "origin": [4, 0, 4], + "size": [1, 8, 1], + "uv": [8, 16], + "mirror": true + } + ] + }, + { + "name": "spines_right_front", + "parent": "body", + "pivot": [-4, 0, -4], + "bind_pose_rotation": [0, -45, 0], + "cubes": [{"origin": [-5, 0, -4], "size": [1, 8, 1], "uv": [4, 16]}] + }, + { + "name": "spines_right_mid", + "parent": "body", + "pivot": [-4, 0, 0], + "cubes": [{"origin": [-5, 0, 0], "size": [1, 8, 1], "uv": [8, 16]}] + }, + { + "name": "spines_right_back", + "parent": "body", + "pivot": [-4, 0, 4], + "bind_pose_rotation": [0, 45, 0], + "cubes": [{"origin": [-5, 0, 4], "size": [1, 8, 1], "uv": [8, 16]}] + } + ] + } + }, + "render_controllers": [ + {"controller.render.pufferfish.small": "query.variant == 0"}, + {"controller.render.pufferfish.medium": "query.variant == 1"}, + {"controller.render.pufferfish.large": "query.variant == 2"} + ], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 46} + }, + "rabbit": { + "identifier": "minecraft:rabbit", + "min_engine_version": "1.8.0", + "materials": {"default": "rabbit"}, + "textures": { + "brown": "textures/entity/rabbit/brown", + "white": "textures/entity/rabbit/white", + "black": "textures/entity/rabbit/black", + "white_splotched": "textures/entity/rabbit/white_splotched", + "gold": "textures/entity/rabbit/gold", + "salt": "textures/entity/rabbit/salt", + "toast": "textures/entity/rabbit/toast" + }, + "geometry": { + "default": { + "visible_bounds_width": 1, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "rearFootLeft", + "pivot": [3, 6.5, 3.7], + "mirror": true, + "parent": "body", + "cubes": [{"origin": [2, 0, 0], "size": [2, 1, 7], "uv": [8, 24]}] + }, + { + "name": "rearFootRight", + "pivot": [-3, 6.5, 3.7], + "mirror": true, + "parent": "body", + "cubes": [{"origin": [-4, 0, 0], "size": [2, 1, 7], "uv": [26, 24]}] + }, + { + "name": "haunchLeft", + "pivot": [3, 6.5, 3.7], + "bind_pose_rotation": [-20, 0, 0], + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [2, 2.5, 3.7], "size": [2, 4, 5], "uv": [16, 15]} + ] + }, + { + "name": "haunchRight", + "pivot": [-3, 6.5, 3.7], + "bind_pose_rotation": [-20, 0, 0], + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [-4, 2.5, 3.7], "size": [2, 4, 5], "uv": [30, 15]} + ] + }, + { + "name": "body", + "pivot": [0, 5, 8], + "bind_pose_rotation": [-20, 0, 0], + "mirror": true, + "cubes": [{"origin": [-3, 2, -2], "size": [6, 5, 10], "uv": [0, 0]}] + }, + { + "name": "frontLegLeft", + "pivot": [3, 7, -1], + "bind_pose_rotation": [-10, 0, 0], + "mirror": true, + "parent": "body", + "cubes": [{"origin": [2, 0, -2], "size": [2, 7, 2], "uv": [8, 15]}] + }, + { + "name": "frontLegRight", + "pivot": [-3, 7, -1], + "bind_pose_rotation": [-10, 0, 0], + "mirror": true, + "parent": "body", + "cubes": [{"origin": [-4, 0, -2], "size": [2, 7, 2], "uv": [0, 15]}] + }, + { + "name": "head", + "pivot": [0, 8, -1], + "locators": {"lead": [0, 8, -1]}, + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [-2.5, 8, -6], "size": [5, 4, 5], "uv": [32, 0]} + ] + }, + { + "name": "earRight", + "pivot": [0, 8, -1], + "bind_pose_rotation": [0, -15, 0], + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [-2.5, 12, -2], "size": [2, 5, 1], "uv": [58, 0]} + ] + }, + { + "name": "earLeft", + "pivot": [0, 8, -1], + "bind_pose_rotation": [0, 15, 0], + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [0.5, 12, -2], "size": [2, 5, 1], "uv": [52, 0]} + ] + }, + { + "name": "tail", + "pivot": [0, 4, 7], + "bind_pose_rotation": [-20, 0, 0], + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [-1.5, 2.5, 7], "size": [3, 3, 2], "uv": [52, 6]} + ] + }, + { + "name": "nose", + "pivot": [0, 8, -1], + "mirror": true, + "parent": "body", + "cubes": [ + {"origin": [-0.5, 9.5, -6.5], "size": [1, 1, 1], "uv": [32, 9]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.rabbit"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 24} + }, + "ravager": { + "identifier": "minecraft:ravager", + "textures": {"default": "textures/entity/illager/ravager"}, + "materials": {"default": "ravager"}, + "geometry": { + "default": { + "bones": [ + { + "pivot": [0, 19, 2], + "rotation": [90, 0, 0], + "cubes": [ + {"origin": [-7, 10, -2], "size": [14, 16, 20], "uv": [0, 55]}, + {"origin": [-6, -3, -2], "size": [12, 13, 18], "uv": [0, 91]} + ], + "name": "body" + }, + { + "pivot": [0, 15, -10], + "cubes": [ + {"origin": [-8, 13, -24], "size": [16, 3, 16], "uv": [0, 36]} + ], + "name": "mouth", + "parent": "head" + }, + { + "pivot": [0, 20, -20], + "cubes": [ + {"origin": [-5, 21, -10], "size": [10, 10, 18], "uv": [68, 73]} + ], + "name": "neck" + }, + { + "locators": {"stun": [0, 32, -15]}, + "pivot": [0, 28, -10], + "cubes": [ + {"origin": [-8, 14, -24], "size": [16, 20, 16], "uv": [0, 0]}, + {"origin": [-2, 12, -28], "size": [4, 8, 4], "uv": [0, 0]} + ], + "name": "head", + "parent": "neck" + }, + { + "pivot": [-12, 30, 22], + "cubes": [ + {"origin": [-12, 0, 17], "size": [8, 37, 8], "uv": [96, 0]} + ], + "name": "leg0" + }, + { + "pivot": [4, 30, 22], + "cubes": [ + {"origin": [4, 0, 17], "size": [8, 37, 8], "uv": [96, 0]} + ], + "name": "leg1" + }, + { + "pivot": [-4, 26, -4], + "cubes": [ + {"origin": [-12, 0, -8], "size": [8, 37, 8], "uv": [64, 0]} + ], + "name": "leg2" + }, + { + "pivot": [-4, 26, -4], + "cubes": [ + {"origin": [4, 0, -8], "size": [8, 37, 8], "uv": [64, 0]} + ], + "name": "leg3" + }, + { + "pivot": [-5, 27, -19], + "rotation": [60, 0, 0], + "cubes": [ + {"origin": [-10, 27, -20], "size": [2, 14, 4], "uv": [74, 55]}, + {"origin": [8, 27, -20], "size": [2, 14, 4], "uv": [74, 55]} + ], + "name": "horns", + "parent": "head" + } + ], + "texturewidth": 128, + "textureheight": 128, + "visible_bounds_width": 4, + "visible_bounds_height": 3.5, + "visible_bounds_offset": [0, 1.25, 0] + } + }, + "render_controllers": ["controller.render.ravager"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 57}, + "particle_effects": {"stun_particles": "minecraft:stunned_emitter"} + }, + "salmon": { + "identifier": "minecraft:salmon", + "materials": {"default": "salmon"}, + "textures": {"default": "textures/entity/fish/salmon"}, + "geometry": { + "default": { + "visible_bounds_width": 0.5, + "visible_bounds_height": 0.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 32, + "textureheight": 32, + "bones": [ + { + "name": "body_front", + "pivot": [0, 0, -4], + "cubes": [ + {"origin": [-1.5, 3.5, -4], "size": [3, 5, 8], "uv": [0, 0]} + ] + }, + { + "name": "body_back", + "parent": "body_front", + "pivot": [0, 0, 4], + "cubes": [ + {"origin": [-1.5, 3.5, 4], "size": [3, 5, 8], "uv": [0, 13]} + ] + }, + { + "name": "dorsal_front", + "parent": "body_front", + "pivot": [0, 5, 2], + "cubes": [{"origin": [0, 8.5, 2], "size": [0, 2, 2], "uv": [4, 2]}] + }, + { + "name": "dorsal_back", + "parent": "body_back", + "pivot": [0, 5, 4], + "cubes": [{"origin": [0, 8.5, 4], "size": [0, 2, 3], "uv": [2, 3]}] + }, + { + "name": "tailfin", + "parent": "body_back", + "pivot": [0, 0, 12], + "cubes": [ + {"origin": [0, 3.5, 12], "size": [0, 5, 6], "uv": [20, 10]} + ] + }, + { + "name": "head", + "parent": "body_front", + "pivot": [0, 3, -4], + "locators": {"lead": [0, 3, -4]}, + "cubes": [ + {"origin": [-1, 4.5, -7], "size": [2, 4, 3], "uv": [22, 0]} + ] + }, + { + "name": "leftFin", + "parent": "body_front", + "pivot": [1.5, 1, -4], + "rotation": [0, 0, 35], + "cubes": [ + { + "origin": [-0.50752, 3.86703, -4], + "size": [2, 0, 2], + "uv": [2, 0] + } + ] + }, + { + "name": "rightFin", + "parent": "body_front", + "pivot": [-1.5, 1, -4], + "rotation": [0, 0, -35], + "cubes": [ + { + "origin": [-1.49258, 3.86703, -4], + "size": [2, 0, 2], + "uv": [-2, 0] + } + ] + } + ] + } + }, + "render_controllers": ["controller.render.salmon"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 47} + }, + "shulker_bullet": { + "identifier": "minecraft:shulker_bullet", + "materials": {"default": "shulker_bullet"}, + "textures": {"default": "textures/entity/shulker/spark"}, + "geometry": { + "default": { + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-4, -4, -1], "size": [8, 8, 2], "uv": [0, 0]}, + {"origin": [-1, -4, -4], "size": [2, 8, 8], "uv": [0, 10]}, + {"origin": [-4, -1, -4], "size": [8, 2, 8], "uv": [20, 0]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.shulker_bullet"] + }, + "silverfish": { + "identifier": "minecraft:silverfish", + "materials": {"default": "silverfish", "body_layer": "silverfish_layers"}, + "textures": {"default": "textures/entity/silverfish"}, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "bodyPart_0", + "parent": "bodyPart_2", + "pivot": [0, 2, -3.5], + "cubes": [ + {"origin": [-1.5, 0, -4.5], "size": [3, 2, 2], "uv": [0, 0]} + ] + }, + { + "name": "bodyPart_1", + "parent": "bodyPart_2", + "pivot": [0, 3, -1.5], + "cubes": [ + {"origin": [-2, 0, -2.5], "size": [4, 3, 2], "uv": [0, 4]} + ] + }, + { + "name": "bodyPart_2", + "pivot": [0, 4, 1], + "cubes": [ + {"origin": [-3, 0, -0.5], "size": [6, 4, 3], "uv": [0, 9]} + ] + }, + { + "name": "bodyPart_3", + "parent": "bodyPart_2", + "pivot": [0, 3, 4], + "cubes": [ + {"origin": [-1.5, 0, 2.5], "size": [3, 3, 3], "uv": [0, 16]} + ] + }, + { + "name": "bodyPart_4", + "parent": "bodyPart_2", + "pivot": [0, 2, 7], + "cubes": [ + {"origin": [-1, 0, 5.5], "size": [2, 2, 3], "uv": [0, 22]} + ] + }, + { + "name": "bodyPart_5", + "parent": "bodyPart_2", + "pivot": [0, 1, 9.5], + "cubes": [ + {"origin": [-1, 0, 8.5], "size": [2, 1, 2], "uv": [11, 0]} + ] + }, + { + "name": "bodyPart_6", + "parent": "bodyPart_2", + "pivot": [0, 1, 11.5], + "cubes": [ + {"origin": [-0.5, 0, 10.5], "size": [1, 1, 2], "uv": [13, 4]} + ] + }, + { + "name": "bodyLayer_0", + "parent": "bodyPart_2", + "pivot": [0, 8, 1], + "cubes": [ + {"origin": [-5, 0, -0.5], "size": [10, 8, 3], "uv": [20, 0]} + ] + }, + { + "name": "bodyLayer_1", + "parent": "bodyPart_4", + "pivot": [0, 4, 7], + "cubes": [ + {"origin": [-3, 0, 5.5], "size": [6, 4, 3], "uv": [20, 11]} + ] + }, + { + "name": "bodyLayer_2", + "parent": "bodyPart_1", + "pivot": [0, 5, -1.5], + "cubes": [ + {"origin": [-3, 0, -3], "size": [6, 5, 2], "uv": [20, 18]} + ] + } + ] + } + }, + "render_controllers": ["controller.render.silverfish"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 8} + }, + "skeleton": { + "identifier": "minecraft:skeleton", + "min_engine_version": "1.8.0", + "materials": {"default": "skeleton"}, + "textures": {"default": "textures/entity/skeleton/skeleton"}, + "geometry": { + "default": { + "texturewidth": 64, + "textureheight": 32, + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ], + "parent": "waist" + }, + {"name": "waist", "pivot": [0, 12, 0]}, + { + "name": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]} + ], + "parent": "body" + }, + { + "name": "hat", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ], + "neverRender": true, + "parent": "head" + }, + { + "name": "rightArm", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-6, 12, -1], "size": [2, 12, 2], "uv": [40, 16]} + ], + "parent": "body" + }, + { + "name": "rightItem", + "pivot": [-1, -45, -5], + "neverRender": true, + "parent": "rightArm" + }, + { + "name": "leftArm", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -1], "size": [2, 12, 2], "uv": [40, 16]} + ], + "mirror": true, + "parent": "body" + }, + { + "name": "leftItem", + "pivot": [1, -45, -5], + "neverRender": true, + "parent": "leftArm" + }, + { + "name": "rightLeg", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-3, 0, -1], "size": [2, 12, 2], "uv": [0, 16]} + ], + "parent": "body" + }, + { + "name": "leftLeg", + "pivot": [2, 12, 0], + "cubes": [ + {"origin": [1, 0, -1], "size": [2, 12, 2], "uv": [0, 16]} + ], + "mirror": true, + "parent": "body" + } + ] + } + }, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 9}, + "render_controllers": ["controller.render.skeleton"], + "enable_attachables": true + }, + "skeleton_horse": { + "identifier": "minecraft:skeleton_horse", + "textures": { + "base_brown": "textures/entity/horse/horse_brown", + "base_white": "textures/entity/horse/horse_white", + "base_chestnut": "textures/entity/horse/horse_chestnut", + "base_creamy": "textures/entity/horse/horse_creamy", + "base_black": "textures/entity/horse/horse_black", + "base_gray": "textures/entity/horse/horse_gray", + "base_darkbrown": "textures/entity/horse/horse_darkbrown", + "markings_none": "textures/entity/horse/horse_markings_none", + "markings_white": "textures/entity/horse/horse_markings_white", + "markings_whitefield": "textures/entity/horse/horse_markings_whitefield", + "markings_whitedots": "textures/entity/horse/horse_markings_whitedots", + "markings_blackdots": "textures/entity/horse/horse_markings_blackdots", + "mule": "textures/entity/horse/mule", + "donkey": "textures/entity/horse/donkey", + "skeleton": "textures/entity/horse/horse_skeleton", + "zombie": "textures/entity/horse/horse_zombie", + "armor_none": "textures/entity/horse/armor/horse_armor_none", + "armor_leather": "textures/entity/horse/armor/horse_armor_leather", + "armor_iron": "textures/entity/horse/armor/horse_armor_iron", + "armor_gold": "textures/entity/horse/armor/horse_armor_gold", + "armor_diamond": "textures/entity/horse/armor/horse_armor_diamond" + }, + "geometry": { + "default": { + "visible_bounds_width": 2, + "visible_bounds_height": 3, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 128, + "textureheight": 128, + "bones": [ + { + "name": "Body", + "pivot": [0, 13, 9], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5, 11, -10], "size": [10, 10, 24], "uv": [0, 34]} + ] + }, + { + "name": "TailA", + "pivot": [0, 21, 14], + "rotation": [-65, 0, 0], + "cubes": [ + {"origin": [-1, 20, 14], "size": [2, 2, 3], "uv": [44, 0]} + ] + }, + { + "name": "TailB", + "pivot": [0, 21, 14], + "rotation": [-65, 0, 0], + "cubes": [ + {"origin": [-1.5, 19, 17], "size": [3, 4, 7], "uv": [38, 7]} + ] + }, + { + "name": "TailC", + "pivot": [0, 21, 14], + "rotation": [-80.34, 0, 0], + "cubes": [ + {"origin": [-1.5, 21.5, 23], "size": [3, 4, 7], "uv": [24, 3]} + ] + }, + { + "name": "Leg1A", + "pivot": [4, 15, 11], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [1.5, 8, 8.5], "size": [4, 9, 5], "uv": [78, 29]} + ] + }, + { + "name": "Leg1B", + "pivot": [4, 8, 11], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [2, 3, 9.5], "size": [3, 5, 3], "uv": [78, 43]} + ] + }, + { + "name": "Leg1C", + "pivot": [4, 8, 11], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [1.5, -0.1, 9], "size": [4, 3, 4], "uv": [78, 51]} + ] + }, + { + "name": "Leg2A", + "pivot": [-4, 15, 11], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.5, 8, 8.5], "size": [4, 9, 5], "uv": [96, 29]} + ] + }, + { + "name": "Leg2B", + "pivot": [-4, 8, 11], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5, 3, 9.5], "size": [3, 5, 3], "uv": [96, 43]} + ] + }, + { + "name": "Leg2C", + "pivot": [-4, 8, 11], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.5, -0.1, 9], "size": [4, 3, 4], "uv": [96, 51]} + ] + }, + { + "name": "Leg3A", + "pivot": [4, 15, -8], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [2.1, 8, -10.1], "size": [3, 8, 4], "uv": [44, 29]} + ] + }, + { + "name": "Leg3B", + "pivot": [4, 8, -8], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [2.1, 3, -9.6], "size": [3, 5, 3], "uv": [44, 41]} + ] + }, + { + "name": "Leg3C", + "pivot": [4, 8, -8], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [1.6, -0.1, -10.1], "size": [4, 3, 4], "uv": [44, 51]} + ] + }, + { + "name": "Leg4A", + "pivot": [-4, 15, -8], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.1, 8, -10.1], "size": [3, 8, 4], "uv": [60, 29]} + ] + }, + { + "name": "Leg4B", + "pivot": [-4, 8, -8], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.1, 3, -9.6], "size": [3, 5, 3], "uv": [60, 41]} + ] + }, + { + "name": "Leg4C", + "pivot": [-4, 8, -8], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.6, -0.1, -10.1], "size": [4, 3, 4], "uv": [60, 51]} + ] + }, + { + "name": "Head", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-2.5, 25, -11.5], "size": [5, 5, 7], "uv": [0, 0]} + ] + }, + { + "name": "UMouth", + "pivot": [0, 20.05, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-2, 27.05, -17], "size": [4, 3, 6], "uv": [24, 18]} + ] + }, + { + "name": "LMouth", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-2, 25, -16.5], "size": [4, 2, 5], "uv": [24, 27]} + ] + }, + { + "name": "Ear1", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [0.45, 29, -6], "size": [2, 3, 1], "uv": [0, 0]} + ] + }, + { + "name": "Ear2", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-2.45, 29, -6], "size": [2, 3, 1], "uv": [0, 0]} + ] + }, + { + "name": "MuleEarL", + "pivot": [0, 20, -10], + "rotation": [30, 0, 15], + "cubes": [ + {"origin": [-2, 29, -6], "size": [2, 7, 1], "uv": [0, 12]} + ] + }, + { + "name": "MuleEarR", + "pivot": [0, 20, -10], + "rotation": [30, 0, -15], + "cubes": [{"origin": [0, 29, -6], "size": [2, 7, 1], "uv": [0, 12]}] + }, + { + "name": "Neck", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-2.05, 15.8, -12], "size": [4, 14, 8], "uv": [0, 12]} + ] + }, + { + "name": "Bag1", + "pivot": [-7.5, 21, 10], + "rotation": [0, 90, 0], + "cubes": [ + {"origin": [-10.5, 13, 10], "size": [8, 8, 3], "uv": [0, 34]} + ] + }, + { + "name": "Bag2", + "pivot": [4.5, 21, 10], + "rotation": [0, 90, 0], + "cubes": [ + {"origin": [1.5, 13, 10], "size": [8, 8, 3], "uv": [0, 47]} + ] + }, + { + "name": "Saddle", + "pivot": [0, 22, 2], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5, 21, -1], "size": [10, 1, 8], "uv": [80, 0]} + ] + }, + { + "name": "SaddleB", + "pivot": [0, 22, 2], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-1.5, 22, -1], "size": [3, 1, 2], "uv": [106, 9]} + ] + }, + { + "name": "SaddleC", + "pivot": [0, 22, 2], + "rotation": [0, 0, 0], + "cubes": [{"origin": [-4, 22, 5], "size": [8, 1, 2], "uv": [80, 9]}] + }, + { + "name": "SaddleL2", + "pivot": [5, 21, 2], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [4.5, 13, 1], "size": [1, 2, 2], "uv": [74, 0]} + ] + }, + { + "name": "SaddleL", + "pivot": [5, 21, 2], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [4.5, 15, 1.5], "size": [1, 6, 1], "uv": [70, 0]} + ] + }, + { + "name": "SaddleR2", + "pivot": [-5, 21, 2], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.5, 13, 1], "size": [1, 2, 2], "uv": [74, 4]} + ] + }, + { + "name": "SaddleR", + "pivot": [-5, 21, 2], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-5.5, 15, 1.5], "size": [1, 6, 1], "uv": [80, 0]} + ] + }, + { + "name": "SaddleMouthL", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [1.5, 26, -14], "size": [1, 2, 2], "uv": [74, 13]} + ] + }, + { + "name": "SaddleMouthR", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-2.5, 26, -14], "size": [1, 2, 2], "uv": [74, 13]} + ] + }, + { + "name": "SaddleMouthLine", + "pivot": [0, 20, -10], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [2.6, 23, -16], "size": [0, 3, 16], "uv": [44, 10]} + ] + }, + { + "name": "SaddleMouthLineR", + "pivot": [0, 20, -10], + "rotation": [0, 0, 0], + "cubes": [ + {"origin": [-2.6, 23, -16], "size": [0, 3, 16], "uv": [44, 5]} + ] + }, + { + "name": "Mane", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + {"origin": [-1, 15.5, -5], "size": [2, 16, 4], "uv": [58, 0]} + ] + }, + { + "name": "HeadSaddle", + "pivot": [0, 20, -10], + "rotation": [30, 0, 0], + "cubes": [ + { + "origin": [-2.5, 25.1, -17], + "size": [5, 5, 12], + "uv": [80, 12], + "inflate": 0.05 + } + ] + } + ] + } + }, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 32} + }, + "slime": { + "identifier": "minecraft:slime", + "materials": {"default": "slime", "outer": "slime_outer"}, + "textures": {"default": "textures/entity/slime/slime"}, + "geometry": { + "default": { + "visible_bounds_width": 5, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "cube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-3, 1, -3], "size": [6, 6, 6], "uv": [0, 16]}] + }, + { + "name": "eye0", + "parent": "cube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-3.3, 4, -3.5], "size": [2, 2, 2], "uv": [32, 0]} + ] + }, + { + "name": "eye1", + "parent": "cube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [1.3, 4, -3.5], "size": [2, 2, 2], "uv": [32, 4]} + ] + }, + { + "name": "mouth", + "parent": "cube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [0, 2, -3.5], "size": [1, 1, 1], "uv": [32, 8]} + ] + } + ] + }, + "armor": { + "visible_bounds_width": 1, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "cube", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [0, 0]}] + }, + { + "name": "eye0", + "parent": "cube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-3.3, 4, -3.5], "size": [2, 2, 2], "uv": [32, 0]} + ] + }, + { + "name": "eye1", + "parent": "cube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [1.3, 4, -3.5], "size": [2, 2, 2], "uv": [32, 4]} + ] + }, + { + "name": "mouth", + "parent": "cube", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [0, 2, -3.5], "size": [1, 1, 1], "uv": [32, 8]} + ] + } + ] + } + }, + "render_controllers": [ + "controller.render.slime", + "controller.render.slime_armor" + ], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 10} + }, + "small_fireball": { + "identifier": "minecraft:small_fireball", + "materials": {"default": "fireball"}, + "textures": {"default": "textures/items/fire_charge"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -4, 0], + "size": [16, 16, 0], + "uv": {"south": {"uv": [0, 0]}} + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.fireball"] + }, + "snow_golem": { + "identifier": "minecraft:snow_golem", + "min_engine_version": "1.8.0", + "materials": {"default": "snow_golem", "head": "snow_golem_pumpkin"}, + "textures": {"default": "textures/entity/snow_golem"}, + "geometry": { + "default": { + "visible_bounds_width": 1, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "bones": [ + { + "name": "head", + "parent": "piece1", + "pivot": [0, 20, 0], + "locators": {"lead": [0, 20, 0]}, + "cubes": [ + { + "origin": [-4, 20, -4], + "size": [8, 8, 8], + "uv": [0, 0], + "inflate": -0.5 + } + ] + }, + { + "name": "arm1", + "parent": "piece1", + "pivot": [0, 18, 0], + "bind_pose_rotation": [0, 0, 57.3], + "cubes": [ + { + "origin": [1, 20, -1], + "size": [12, 2, 2], + "uv": [32, 0], + "inflate": -0.5 + } + ] + }, + { + "name": "arm2", + "parent": "piece1", + "pivot": [0, 18, 0], + "bind_pose_rotation": [0, 180, -57.3], + "cubes": [ + { + "origin": [1, 20, -1], + "size": [12, 2, 2], + "uv": [32, 0], + "inflate": -0.5 + } + ] + }, + { + "name": "piece1", + "parent": "piece2", + "pivot": [0, 11, 0], + "cubes": [ + { + "origin": [-5, 11, -5], + "size": [10, 10, 10], + "uv": [0, 16], + "inflate": -0.5 + } + ] + }, + { + "name": "piece2", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-6, 0, -6], + "size": [12, 12, 12], + "uv": [0, 36], + "inflate": -0.5 + } + ] + } + ] + } + }, + "render_controllers": ["controller.render.snowgolem"] + }, + "snowball": { + "identifier": "minecraft:snowball", + "materials": {"default": "snowball"}, + "textures": {"default": "textures/items/snowball"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "size": [16, 16, 0], + "uv": [0, 0], + "rotation": [0, 0, 0] + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.item_sprite"] + }, + "potion": { + "identifier": "minecraft:splash_potion", + "materials": {"default": "splash_potion_enchanted"}, + "textures": { + "moveSlowdown": "textures/items/potion_bottle_splash_moveSlowdown", + "moveSpeed": "textures/items/potion_bottle_splash_moveSpeed", + "digSlowdown": "textures/items/potion_bottle_splash_digSlowdown", + "digSpeed": "textures/items/potion_bottle_splash_digSpeed", + "damageBoost": "textures/items/potion_bottle_splash_damageBoost", + "heal": "textures/items/potion_bottle_splash_heal", + "harm": "textures/items/potion_bottle_splash_harm", + "jump": "textures/items/potion_bottle_splash_jump", + "confusion": "textures/items/potion_bottle_splash_confusion", + "regeneration": "textures/items/potion_bottle_splash_regeneration", + "resistance": "textures/items/potion_bottle_splash_resistance", + "fireResistance": "textures/items/potion_bottle_splash_fireResistance", + "waterBreathing": "textures/items/potion_bottle_splash_waterBreathing", + "invisibility": "textures/items/potion_bottle_splash_invisibility", + "blindness": "textures/items/potion_bottle_splash_blindness", + "nightVision": "textures/items/potion_bottle_splash_nightVision", + "hunger": "textures/items/potion_bottle_splash_hunger", + "weakness": "textures/items/potion_bottle_splash_weakness", + "poison": "textures/items/potion_bottle_splash_poison", + "wither": "textures/items/potion_bottle_splash_wither", + "healthBoost": "textures/items/potion_bottle_splash_healthBoost", + "absorption": "textures/items/potion_bottle_splash_absorption", + "saturation": "textures/items/potion_bottle_splash_saturation", + "levitation": "textures/items/potion_bottle_splash_levitation", + "turtleMaster": "textures/items/potion_bottle_splash_turtleMaster", + "slowFall": "textures/items/potion_bottle_splash_slowFall", + "default": "textures/items/potion_bottle_splash", + "enchanted": "textures/misc/enchanted_item_glint" + }, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, -8, 0], + "size": [16, 16, 0], + "uv": [0, 0], + "rotation": [0, 0, 0] + } + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.splash_potion"] + }, + "squid": { + "identifier": "minecraft:squid", + "materials": {"default": "squid"}, + "textures": {"default": "textures/entity/squid"}, + "geometry": { + "default": { + "visible_bounds_width": 3, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "cubes": [ + {"origin": [-6, -8, -6], "size": [12, 16, 12], "uv": [0, 0]} + ] + }, + { + "name": "tentacle1", + "parent": "body", + "pivot": [5, -7, 0], + "cubes": [ + {"origin": [4, -25, -1], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, 90, 0] + }, + { + "name": "tentacle2", + "parent": "body", + "pivot": [3.5, -7, 3.5], + "cubes": [ + {"origin": [2.5, -25, 2.5], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, 45, 0] + }, + { + "name": "tentacle3", + "parent": "body", + "pivot": [0, -7, 5], + "cubes": [ + {"origin": [-1, -25, 4], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, 0, 0] + }, + { + "name": "tentacle4", + "parent": "body", + "pivot": [-3.5, -7, 3.5], + "cubes": [ + {"origin": [-4.5, -25, 2.5], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, -45, 0] + }, + { + "name": "tentacle5", + "parent": "body", + "pivot": [-5, -7, 0], + "cubes": [ + {"origin": [-6, -25, -1], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, -90, 0] + }, + { + "name": "tentacle6", + "parent": "body", + "pivot": [-3.5, -7, -3.5], + "cubes": [ + {"origin": [-4.5, -25, -4.5], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, -135, 0] + }, + { + "name": "tentacle7", + "parent": "body", + "pivot": [0, -7, -5], + "cubes": [ + {"origin": [-1, -25, -6], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, -180, 0] + }, + { + "name": "tentacle8", + "parent": "body", + "pivot": [3.5, -7, -3.5], + "cubes": [ + {"origin": [2.5, -25, -4.5], "size": [2, 18, 2], "uv": [48, 0]} + ], + "rotation": [0, -225, 0] + } + ] + } + }, + "render_controllers": ["controller.render.squid"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 15} + }, + "stray": { + "identifier": "minecraft:stray", + "min_engine_version": "1.8.0", + "materials": {"default": "stray", "overlay": "stray_clothes"}, + "textures": { + "default": "textures/entity/skeleton/stray", + "overlay": "textures/entity/skeleton/stray_overlay" + }, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ], + "parent": "waist" + }, + {"name": "waist", "pivot": [0, 12, 0]}, + { + "name": "head", + "pivot": [0, 24, 0], + "locators": {"lead": [0, 24, 0]}, + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]} + ], + "parent": "body" + }, + { + "name": "hat", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ], + "neverRender": true, + "parent": "head" + }, + { + "name": "rightArm", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-6, 12, -1], "size": [2, 12, 2], "uv": [40, 16]} + ], + "parent": "body" + }, + { + "name": "rightItem", + "pivot": [-1, -45, -5], + "neverRender": true, + "parent": "rightArm" + }, + { + "name": "leftArm", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -1], "size": [2, 12, 2], "uv": [40, 16]} + ], + "mirror": true, + "parent": "body" + }, + { + "name": "leftItem", + "pivot": [1, -45, -5], + "neverRender": true, + "parent": "leftArm" + }, + { + "name": "rightLeg", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-3, 0, -1], "size": [2, 12, 2], "uv": [0, 16]} + ], + "parent": "body" + }, + { + "name": "leftLeg", + "pivot": [2, 12, 0], + "cubes": [ + {"origin": [1, 0, -1], "size": [2, 12, 2], "uv": [0, 16]} + ], + "mirror": true, + "parent": "body" + } + ] + }, + "overlay": { + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "parent": "waist", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ], + "inflate": 0.25 + }, + {"name": "waist", "neverRender": true, "pivot": [0, 12, 0]}, + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]} + ], + "inflate": 0.25 + }, + { + "name": "hat", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ], + "neverRender": true + }, + { + "name": "rightArm", + "parent": "body", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]} + ], + "inflate": 0.25 + }, + { + "name": "rightItem", + "parent": "rightArm", + "pivot": [-6, 15, 1], + "neverRender": true + }, + { + "name": "leftArm", + "parent": "body", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 16]} + ], + "mirror": true, + "inflate": 0.25 + }, + { + "name": "rightLeg", + "parent": "body", + "pivot": [-1.9, 12, 0], + "cubes": [ + {"origin": [-3.9, 0, -2], "size": [4, 12, 4], "uv": [0, 16]} + ], + "inflate": 0.25 + }, + { + "name": "leftLeg", + "parent": "body", + "pivot": [1.9, 12, 0], + "cubes": [ + {"origin": [-0.1, 0, -2], "size": [4, 12, 4], "uv": [0, 16]} + ], + "inflate": 0.25, + "mirror": true + } + ] + } + }, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 27}, + "render_controllers": [ + "controller.render.stray_clothes", + "controller.render.stray" + ], + "enable_attachables": true + }, + "strider": { + "identifier": "minecraft:strider", + "materials": {"default": "strider"}, + "textures": { + "default": "textures/entity/strider/strider", + "saddled": "textures/entity/strider/strider", + "suffocated": "textures/entity/strider/strider_cold", + "suffocated_saddled": "textures/entity/strider/strider_cold" + }, + "geometry": { + "default": { + "bones": [ + { + "name": "right_leg", + "pivot": [-4, 16, 0], + "cubes": [ + {"origin": [-6, 0, -2], "size": [4, 16, 4], "uv": [0, 32]} + ] + }, + { + "name": "left_leg", + "pivot": [4, 16, 0], + "cubes": [{"origin": [2, 0, -2], "size": [4, 16, 4], "uv": [0, 55]}] + }, + { + "name": "body", + "pivot": [0, 16, 0], + "cubes": [ + {"origin": [-8, 14, -8], "size": [16, 14, 16], "uv": [0, 0]} + ], + "locators": {"lead": [0, 15, -1]} + }, + { + "name": "bristle5", + "parent": "body", + "pivot": [8, 19, 0], + "cubes": [ + { + "origin": [8, 19, -8], + "size": [12, 0, 16], + "pivot": [8, 19, 0], + "rotation": [0, 0, 70], + "uv": [16, 65] + } + ] + }, + { + "name": "bristle4", + "parent": "body", + "pivot": [8, 24, 0], + "cubes": [ + { + "origin": [8, 24, -8], + "size": [12, 0, 16], + "pivot": [8, 24, 0], + "rotation": [0, 0, 65], + "uv": [16, 49] + } + ] + }, + { + "name": "bristle3", + "parent": "body", + "pivot": [8, 28, 0], + "cubes": [ + { + "origin": [8, 28, -8], + "size": [12, 0, 16], + "pivot": [8, 28, 0], + "rotation": [0, 0, 50], + "uv": [16, 33] + } + ] + }, + { + "name": "bristle2", + "parent": "body", + "pivot": [-8, 28, 0], + "cubes": [ + { + "origin": [-20, 28, -8], + "size": [12, 0, 16], + "pivot": [-8, 28, 0], + "rotation": [0, 0, -50], + "uv": [16, 33], + "mirror": true + } + ] + }, + { + "name": "bristle1", + "parent": "body", + "pivot": [-8, 24, 0], + "cubes": [ + { + "origin": [-20, 24, -8], + "size": [12, 0, 16], + "pivot": [-8, 24, 0], + "rotation": [0, 0, -65], + "uv": [16, 49], + "mirror": true + } + ] + }, + { + "name": "bristle0", + "parent": "body", + "pivot": [-8, 19, 0], + "cubes": [ + { + "origin": [-20, 19, -8], + "size": [12, 0, 16], + "pivot": [-8, 19, 0], + "rotation": [0, 0, -70], + "uv": [16, 65], + "mirror": true + } + ] + } + ], + "visible_bounds_width": 3, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 128 + } + }, + "spawn_egg": {"base_color": "#9c3436", "overlay_color": "#4d494d"}, + "render_controllers": ["controller.render.strider"] + }, + "text_display": { + "identifier": "minecraft:text_display", + "geometry": {} + }, + "trident": { + "identifier": "minecraft:thrown_trident", + "textures": { + "default": "textures/entity/trident", + "loyalty_rope": "textures/entity/lead_knot" + }, + "geometry": { + "default": { + "texturewidth": 32, + "textureheight": 32, + "bones": [ + { + "name": "pole", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-0.5, -3, -0.5], + "size": [1, 31, 1], + "inflate": 0.01, + "uv": [0, 0] + }, + {"origin": [-1.5, 22, -0.5], "size": [3, 2, 1], "uv": [4, 0]}, + {"origin": [-2.5, 23, -0.5], "size": [1, 4, 1], "uv": [4, 3]}, + {"origin": [1.5, 23, -0.5], "size": [1, 4, 1], "uv": [4, 3]} + ] + } + ] + } + } + }, + "tnt_minecart": { + "identifier": "minecraft:tnt_minecart", + "min_engine_version": "1.8.0", + "materials": {"default": "minecart"}, + "textures": {"default": "textures/entity/minecart"}, + "geometry": { + "default": { + "bones": [ + { + "name": "bottom", + "pivot": [0, 6, 0], + "cubes": [ + { + "origin": [-10, -6.5, -1], + "size": [20, 16, 2], + "rotation": [90, 0, 0], + "uv": [0, 10] + } + ] + }, + { + "name": "back", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-17, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 270, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "front", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [1, 2.5, -1], + "size": [16, 8, 2], + "rotation": [0, 90, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "right", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-8, 2.5, -8], + "size": [16, 8, 2], + "rotation": [0, 180, 0], + "uv": [0, 0] + } + ], + "parent": "bottom" + }, + { + "name": "left", + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-8, 2.5, 6], "size": [16, 8, 2], "uv": [0, 0]} + ], + "parent": "bottom" + } + ], + "texturewidth": 64, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.minecart"] + }, + "tropical_fish": { + "identifier": "minecraft:tropicalfish", + "materials": {"default": "tropicalfish"}, + "textures": { + "typeA": "textures/entity/fish/tropical_a", + "typeB": "textures/entity/fish/tropical_b", + "aPattern1": "textures/entity/fish/tropical_a_pattern_1", + "aPattern2": "textures/entity/fish/tropical_a_pattern_2", + "aPattern3": "textures/entity/fish/tropical_a_pattern_3", + "aPattern4": "textures/entity/fish/tropical_a_pattern_4", + "aPattern5": "textures/entity/fish/tropical_a_pattern_5", + "aPattern6": "textures/entity/fish/tropical_a_pattern_6", + "bPattern1": "textures/entity/fish/tropical_b_pattern_1", + "bPattern2": "textures/entity/fish/tropical_b_pattern_2", + "bPattern3": "textures/entity/fish/tropical_b_pattern_3", + "bPattern4": "textures/entity/fish/tropical_b_pattern_4", + "bPattern5": "textures/entity/fish/tropical_b_pattern_5", + "bPattern6": "textures/entity/fish/tropical_b_pattern_6" + }, + "geometry": { + "typeA": { + "visible_bounds_width": 0.5, + "visible_bounds_height": 0.5, + "bones": [ + { + "pivot": [-0.5, 0, 0], + "cubes": [ + {"origin": [-1, 0, -3], "size": [2, 3, 6], "uv": [0, 0]}, + {"origin": [0, 3, -2.9992], "size": [0, 4, 6], "uv": [10, -6]} + ], + "name": "body" + }, + { + "pivot": [0, 0, 3], + "cubes": [{"origin": [0, 0, 3], "size": [0, 3, 4], "uv": [24, -4]}], + "name": "tailfin", + "parent": "body" + }, + { + "pivot": [0.5, 0, 1], + "bind_pose_rotation": [0, -35, 0], + "cubes": [ + {"origin": [0.336, 0, -0.10594], "size": [2, 2, 0], "uv": [2, 12]} + ], + "name": "leftFin", + "parent": "body" + }, + { + "pivot": [-0.5, 0, 1], + "bind_pose_rotation": [0, 35, 0], + "cubes": [ + { + "origin": [-2.336, 0, -0.10594], + "size": [2, 2, 0], + "uv": [2, 16] + } + ], + "name": "rightFin", + "parent": "body" + } + ], + "texturewidth": 32, + "textureheight": 32 + }, + "typeB": { + "visible_bounds_width": 0.5, + "visible_bounds_height": 0.5, + "bones": [ + { + "pivot": [-0.5, 0, 0], + "cubes": [ + {"origin": [-1, 0, -0.0008], "size": [2, 6, 6], "uv": [0, 20]}, + {"origin": [0, -5, -0.0008], "size": [0, 5, 6], "uv": [20, 21]}, + {"origin": [0, 6, -0.0008], "size": [0, 5, 6], "uv": [20, 10]} + ], + "name": "body" + }, + { + "pivot": [0, 0, 6], + "cubes": [ + {"origin": [0, 0.0008, 6], "size": [0, 6, 5], "uv": [21, 16]} + ], + "name": "tailfin", + "parent": "body" + }, + { + "pivot": [0.5, 0, 1], + "bind_pose_rotation": [0, -35, 0], + "cubes": [ + { + "origin": [2.05673, 0, 2.35152], + "size": [2, 2, 0], + "uv": [2, 12] + } + ], + "name": "leftFin", + "parent": "body" + }, + { + "pivot": [-0.5, 0, 1], + "bind_pose_rotation": [0, 35, 0], + "cubes": [ + { + "origin": [-4.05673, 0, 2.35152], + "size": [2, 2, 0], + "uv": [2, 16] + } + ], + "name": "rightFin", + "parent": "body" + } + ], + "texturewidth": 32, + "textureheight": 32 + } + }, + "render_controllers": ["controller.render.tropicalfish"], + "spawn_egg": {"texture": "spawn_egg", "texture_index": 44} + }, + "vindicator": { + "identifier": "minecraft:vindicator", + "min_engine_version": "1.8.0", + "materials": {"default": "vindicator"}, + "textures": {"default": "textures/entity/illager/vindicator"}, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 2.5, + "visible_bounds_offset": [0, 1.25, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 10, 8], "uv": [0, 0]} + ] + }, + { + "name": "nose", + "parent": "head", + "pivot": [0, 26, 0], + "cubes": [ + {"origin": [-1, 23, -6], "size": [2, 4, 2], "uv": [24, 0]} + ] + }, + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -3], "size": [8, 12, 6], "uv": [16, 20]}, + { + "origin": [-4, 6, -3], + "size": [8, 18, 6], + "uv": [0, 38], + "inflate": 0.5 + } + ] + }, + { + "name": "arms", + "parent": "body", + "pivot": [0, 22, 0], + "cubes": [ + {"origin": [-8, 16, -2], "size": [4, 8, 4], "uv": [44, 22]}, + {"origin": [4, 16, -2], "size": [4, 8, 4], "uv": [44, 22]}, + {"origin": [-4, 16, -2], "size": [8, 4, 4], "uv": [40, 38]} + ] + }, + { + "name": "leg0", + "parent": "body", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [0, 22]} + ] + }, + { + "name": "leg1", + "parent": "body", + "pivot": [2, 12, 0], + "mirror": true, + "cubes": [{"origin": [0, 0, -2], "size": [4, 12, 4], "uv": [0, 22]}] + }, + { + "name": "rightArm", + "parent": "body", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 46]} + ] + }, + { + "name": "rightItem", + "pivot": [-5.5, 16, 0.5], + "neverRender": true, + "parent": "rightArm" + }, + { + "name": "leftArm", + "parent": "body", + "pivot": [5, 22, 0], + "mirror": true, + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 46]} + ] + } + ] + } + }, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 39}, + "render_controllers": ["controller.render.vindicator"], + "enable_attachables": true + }, + "wandering_trader": { + "identifier": "minecraft:wandering_trader", + "materials": {"default": "wandering_trader"}, + "textures": {"default": "textures/entity/wandering_trader"}, + "geometry": { + "default": { + "visible_bounds_width": 1.5, + "visible_bounds_height": 2.5, + "visible_bounds_offset": [0, 1.25, 0], + "bones": [ + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 10, 8], "uv": [0, 0]} + ] + }, + { + "name": "helmet", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 10, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ] + }, + { + "name": "brim", + "parent": "head", + "pivot": [0, 24, 0], + "bind_pose_rotation": [-90, 0, 0], + "cubes": [ + { + "origin": [-8, 16, -6], + "size": [16, 16, 1], + "uv": [30, 47], + "inflate": 0.1 + } + ] + }, + { + "name": "nose", + "parent": "head", + "pivot": [0, 26, 0], + "cubes": [ + {"origin": [-1, 23, -6], "size": [2, 4, 2], "uv": [24, 0]} + ] + }, + { + "name": "body", + "locators": {"lead_hold": [0, 40, 0]}, + "cubes": [ + {"origin": [-4, 12, -3], "size": [8, 12, 6], "uv": [16, 20]}, + { + "origin": [-4, 6, -3], + "size": [8, 18, 6], + "uv": [0, 38], + "inflate": 0.5 + } + ] + }, + { + "name": "arms", + "parent": "body", + "pivot": [0, 22, 0], + "cubes": [ + {"origin": [-4, 16, -2], "size": [8, 4, 4], "uv": [40, 38]}, + {"origin": [-8, 16, -2], "size": [4, 8, 4], "uv": [44, 22]}, + { + "origin": [4, 16, -2], + "size": [4, 8, 4], + "uv": [44, 22], + "mirror": true + } + ] + }, + {"name": "held_item", "parent": "arms", "pivot": [0, 0, 0]}, + { + "name": "leg0", + "parent": "body", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [0, 22]} + ] + }, + { + "name": "leg1", + "parent": "body", + "pivot": [2, 12, 0], + "cubes": [ + { + "origin": [0, 0, -2], + "size": [4, 12, 4], + "uv": [0, 22], + "mirror": true + } + ] + } + ] + } + }, + "render_controllers": ["controller.render.wandering_trader"], + "spawn_egg": {"texture": "spawn_egg_wandering_trader"} + }, + "wither": { + "identifier": "minecraft:wither", + "min_engine_version": "1.8.0", + "materials": {"default": "wither_boss", "armor": "wither_boss_armor"}, + "textures": { + "default": "textures/entity/wither/wither", + "armor_white": "textures/entity/wither/wither_armor", + "armor_blue": "textures/entity/wither/wither_armor", + "invulnerable": "textures/entity/wither/wither_invulnerable" + }, + "geometry": { + "default": { + "visible_bounds_width": 3, + "visible_bounds_height": 4, + "visible_bounds_offset": [0, 2, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "upperBodyPart1", + "cubes": [ + {"origin": [-10, 17.1, -0.5], "size": [20, 3, 3], "uv": [0, 16]} + ] + }, + { + "name": "upperBodyPart2", + "parent": "upperBodyPart1", + "pivot": [-2, 17.1, -0.5], + "cubes": [ + {"origin": [-2, 7.1, -0.5], "size": [3, 10, 3], "uv": [0, 22]}, + {"origin": [-6, 13.6, 0], "size": [11, 2, 2], "uv": [24, 22]}, + {"origin": [-6, 11.1, 0], "size": [11, 2, 2], "uv": [24, 22]}, + {"origin": [-6, 8.6, 0], "size": [11, 2, 2], "uv": [24, 22]} + ] + }, + { + "name": "upperBodyPart3", + "parent": "upperBodyPart2", + "pivot": [0, 24, 0], + "cubes": [{"origin": [0, 18, 0], "size": [3, 6, 3], "uv": [12, 22]}] + }, + { + "name": "head1", + "parent": "upperBodyPart1", + "pivot": [0, 20, 0], + "cubes": [{"origin": [-4, 20, -4], "size": [8, 8, 8], "uv": [0, 0]}] + }, + { + "name": "head2", + "parent": "upperBodyPart1", + "pivot": [-9, 18, -1], + "cubes": [ + {"origin": [-12, 18, -4], "size": [6, 6, 6], "uv": [32, 0]} + ] + }, + { + "name": "head3", + "parent": "upperBodyPart1", + "pivot": [9, 18, -1], + "cubes": [{"origin": [6, 18, -4], "size": [6, 6, 6], "uv": [32, 0]}] + } + ] + }, + "armor": { + "visible_bounds_width": 3, + "visible_bounds_height": 4, + "visible_bounds_offset": [0, 2, 0], + "texturewidth": 64, + "textureheight": 64, + "bones": [ + { + "name": "upperBodyPart1", + "cubes": [ + {"origin": [-10, 17.1, -0.5], "size": [20, 3, 3], "uv": [0, 16]} + ], + "inflate": 2 + }, + { + "name": "upperBodyPart2", + "parent": "upperBodyPart1", + "pivot": [-2, 17.1, -0.5], + "cubes": [ + {"origin": [-2, 7.1, -0.5], "size": [3, 10, 3], "uv": [0, 22]}, + {"origin": [-6, 13.6, 0], "size": [11, 2, 2], "uv": [24, 22]}, + {"origin": [-6, 11.1, 0], "size": [11, 2, 2], "uv": [24, 22]}, + {"origin": [-6, 8.6, 0], "size": [11, 2, 2], "uv": [24, 22]} + ], + "inflate": 2 + }, + { + "name": "upperBodyPart3", + "parent": "upperBodyPart2", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [0, 18, 0], "size": [3, 6, 3], "uv": [12, 22]} + ], + "inflate": 2 + }, + { + "name": "head1", + "parent": "upperBodyPart1", + "pivot": [0, 20, 0], + "cubes": [ + {"origin": [-4, 20, -4], "size": [8, 8, 8], "uv": [0, 0]} + ], + "inflate": 2 + }, + { + "name": "head2", + "parent": "upperBodyPart1", + "pivot": [-9, 18, -1], + "cubes": [ + {"origin": [-12, 18, -4], "size": [6, 6, 6], "uv": [32, 0]} + ], + "inflate": 2 + }, + { + "name": "head3", + "parent": "upperBodyPart1", + "pivot": [9, 18, -1], + "cubes": [ + {"origin": [6, 18, -4], "size": [6, 6, 6], "uv": [32, 0]} + ], + "inflate": 2 + } + ] + } + }, + "render_controllers": [ + "controller.render.wither_boss", + "controller.render.wither_boss_armor_white", + "controller.render.wither_boss_armor_blue" + ] + }, + "wither_skeleton": { + "identifier": "minecraft:wither_skeleton", + "min_engine_version": "1.8.0", + "materials": {"default": "skeleton"}, + "textures": {"default": "textures/entity/skeleton/wither_skeleton"}, + "geometry": { + "default": { + "texturewidth": 64, + "textureheight": 32, + "visible_bounds_width": 1.5, + "visible_bounds_height": 3, + "visible_bounds_offset": [0, 1.5, 0], + "bones": [ + { + "name": "body", + "parent": "waist", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ] + }, + {"name": "waist", "pivot": [0, 12, 0]}, + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [{"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]}] + }, + { + "name": "hat", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ], + "neverRender": true + }, + { + "name": "rightArm", + "parent": "body", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-6, 12, -1], "size": [2, 12, 2], "uv": [40, 16]} + ] + }, + { + "name": "rightItem", + "parent": "rightArm", + "pivot": [-1, -45, -5], + "neverRender": true + }, + { + "name": "leftArm", + "parent": "body", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -1], "size": [2, 12, 2], "uv": [40, 16]} + ], + "mirror": true + }, + { + "name": "leftItem", + "parent": "leftArm", + "pivot": [1, -45, -5], + "neverRender": true + }, + { + "name": "rightLeg", + "parent": "body", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-3, 0, -1], "size": [2, 12, 2], "uv": [0, 16]} + ] + }, + { + "name": "leftLeg", + "parent": "body", + "pivot": [2, 12, 0], + "cubes": [ + {"origin": [1, 0, -1], "size": [2, 12, 2], "uv": [0, 16]} + ], + "mirror": true + } + ] + } + }, + "render_controllers": ["controller.render.wither_skeleton"], + "enable_attachables": true, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 29} + }, + "wither_skull": { + "identifier": "minecraft:wither_skull", + "materials": {"default": "wither_skull"}, + "textures": {"default": "textures/entity/wither/wither"}, + "geometry": { + "default": { + "bones": [ + { + "name": "head", + "cubes": [{"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [0, 35]}] + } + ], + "visible_bounds_width": 1, + "visible_bounds_height": 1, + "texturewidth": 64, + "textureheight": 64 + } + }, + "render_controllers": ["controller.render.wither_skull"] + }, + "zoglin": { + "identifier": "minecraft:zoglin", + "materials": {"default": "zoglin"}, + "textures": {"default": "textures/entity/hoglin/zoglin"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 19, -3], + "cubes": [ + { + "origin": [-8, 11, -7], + "size": [16, 14, 26], + "inflate": 0.02, + "uv": [1, 1] + }, + { + "origin": [0, 22, -10], + "size": [0, 10, 19], + "inflate": 0.02, + "uv": [90, 33] + } + ], + "locators": {"lead": [0, 20, -5]} + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 22, -5], + "rotation": [50, 0, 0], + "cubes": [ + {"origin": [-7, 21, -24], "size": [14, 6, 19], "uv": [61, 1]}, + {"origin": [-8, 22, -19], "size": [2, 11, 2], "uv": [1, 13]}, + {"origin": [6, 22, -19], "size": [2, 11, 2], "uv": [1, 13]} + ] + }, + { + "name": "right_ear", + "parent": "head", + "pivot": [-7, 27, -7], + "rotation": [0, 0, -50], + "cubes": [ + {"origin": [-13, 26, -10], "size": [6, 1, 4], "uv": [1, 1]} + ] + }, + { + "name": "left_ear", + "parent": "head", + "pivot": [7, 27, -7], + "rotation": [0, 0, 50], + "cubes": [{"origin": [7, 26, -10], "size": [6, 1, 4], "uv": [1, 6]}] + }, + { + "name": "leg_back_right", + "pivot": [6, 8, 17], + "cubes": [ + {"origin": [-8, 0, 13], "size": [5, 11, 5], "uv": [21, 45]} + ] + }, + { + "name": "leg_back_left", + "pivot": [-6, 8, 17], + "cubes": [{"origin": [3, 0, 13], "size": [5, 11, 5], "uv": [0, 45]}] + }, + { + "name": "leg_front_right", + "pivot": [-6, 12, -3], + "cubes": [ + {"origin": [-8, 0, -6], "size": [6, 14, 6], "uv": [66, 42]} + ] + }, + { + "name": "leg_front_left", + "pivot": [6, 12, -3], + "cubes": [ + {"origin": [2, 0, -6], "size": [6, 14, 6], "uv": [41, 42]} + ] + } + ], + "visible_bounds_width": 4, + "visible_bounds_height": 3, + "visible_bounds_offset": [0, 1.5, 0], + "texturewidth": 128, + "textureheight": 64 + } + }, + "spawn_egg": {"base_color": "#c66e55", "overlay_color": "#e6e6e6"}, + "render_controllers": ["controller.render.zoglin"] + }, + "zombie": { + "identifier": "minecraft:zombie", + "min_engine_version": "1.8.0", + "materials": {"default": "zombie"}, + "textures": {"default": "textures/entity/zombie/zombie"}, + "geometry": { + "default": { + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 32, + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]} + ], + "parent": "waist" + }, + {"name": "waist", "neverRender": true, "pivot": [0, 12, 0]}, + { + "name": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]} + ], + "parent": "body" + }, + { + "name": "hat", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-4, 24, -4], + "size": [8, 8, 8], + "uv": [32, 0], + "inflate": 0.5 + } + ], + "neverRender": true, + "parent": "head" + }, + { + "name": "rightArm", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]} + ], + "parent": "body" + }, + { + "name": "rightItem", + "pivot": [-1, -45, -5], + "neverRender": true, + "parent": "rightArm" + }, + { + "name": "leftArm", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 16]} + ], + "mirror": true, + "parent": "body" + }, + { + "name": "leftItem", + "pivot": [1, -45, -5], + "neverRender": true, + "parent": "leftArm" + }, + { + "name": "rightLeg", + "pivot": [-1.9, 12, 0], + "cubes": [ + {"origin": [-3.9, 0, -2], "size": [4, 12, 4], "uv": [0, 16]} + ], + "parent": "body" + }, + { + "name": "leftLeg", + "pivot": [1.9, 12, 0], + "cubes": [ + {"origin": [-0.1, 0, -2], "size": [4, 12, 4], "uv": [0, 16]} + ], + "mirror": true, + "parent": "body" + } + ] + } + }, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 12}, + "scripts": { + "pre_animation": [ + "variable.tcos0 = (Math.cos(query.modified_distance_moved * 38.17) * query.modified_move_speed / variable.gliding_speed_value) * 57.3;" + ] + }, + "animations": { + "humanoid_big_head": {"loop": true, "bones": {"head": {"scale": 1.4}}}, + "look_at_target_default": { + "loop": true, + "bones": { + "head": { + "relative_to": {"rotation": "entity"}, + "rotation": [ + "query.target_x_rotation", + "query.target_y_rotation", + 0 + ] + } + } + }, + "look_at_target_gliding": { + "loop": true, + "bones": {"head": {"rotation": [-45, "query.target_y_rotation", 0]}} + }, + "look_at_target_swimming": { + "loop": true, + "bones": { + "head": { + "rotation": [ + "math.lerp(query.target_x_rotation, -45.0, variable.swim_amount)", + "query.target_y_rotation", + 0 + ] + } + } + }, + "move": { + "loop": true, + "bones": { + "leftarm": {"rotation": ["variable.tcos0", 0, 0]}, + "leftleg": {"rotation": ["variable.tcos0 * -1.4", 0, 0]}, + "rightarm": {"rotation": ["-variable.tcos0", 0, 0]}, + "rightleg": {"rotation": ["variable.tcos0 * 1.4", 0, 0]} + } + }, + "riding.arms": { + "loop": true, + "bones": { + "leftarm": {"rotation": [-36, 0, 0]}, + "rightarm": {"rotation": [-36, 0, 0]} + } + }, + "riding.legs": { + "loop": true, + "bones": { + "leftleg": {"rotation": ["-72.0 - this", "-18.0 - this", "-this"]}, + "rightleg": {"rotation": ["-72.0 - this", "18.0 - this", "-this"]} + } + }, + "holding": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + "variable.is_holding_left ? (-this * 0.5 - 18.0) : 0.0", + 0, + 0 + ] + }, + "rightarm": { + "rotation": [ + "variable.is_holding_right ? (-this * 0.5 - 18.0) : 0.0", + 0, + 0 + ] + } + } + }, + "brandish_spear": { + "loop": true, + "bones": { + "rightarm": { + "rotation": [ + "this * -0.5 - 157.5 - 22.5 * variable.charge_amount", + "-this", + 0 + ] + } + } + }, + "charging": { + "loop": true, + "bones": { + "rightarm": { + "rotation": ["22.5 * variable.charge_amount - this", "-this", 0] + } + } + }, + "attack.rotations": { + "loop": true, + "bones": { + "body": { + "rotation": [ + 0, + "math.sin(math.sqrt(variable.attack_time) * 360) * 11.46 - this", + 0 + ] + }, + "leftarm": { + "rotation": [ + "math.sin(math.sqrt(variable.attack_time) * 360) * 11.46", + 0, + 0 + ] + }, + "rightarm": { + "rotation": [ + "math.sin(1.0 - math.pow(1.0 - variable.attack_time, 3.0) * 180.0) * (variable.is_brandishing_spear ? -1.0 : 1.0 )", + "variable.is_brandishing_spear ? 0.0 : (math.sin(math.sqrt(variable.attack_time) * 360) * 11.46) * 2.0", + 0 + ] + } + } + }, + "sneaking": { + "loop": true, + "bones": { + "body": {"rotation": ["0.5 - this", 0, 0]}, + "head": {"position": [0, 1, 0]}, + "leftarm": {"rotation": [72, 0, 0]}, + "leftleg": {"position": [0, -3, 4]}, + "rightarm": {"rotation": [72, 0, 0]}, + "rightleg": {"position": [0, -3, 4]} + } + }, + "bob": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + 0, + 0, + "((math.cos(query.life_time * 103.2) * 2.865) + 2.865) *-1.0" + ] + }, + "rightarm": { + "rotation": [ + 0, + 0, + "(math.cos(query.life_time * 103.2) * 2.865) + 2.865" + ] + } + } + }, + "damage_nearby_mobs": { + "loop": true, + "bones": { + "leftarm": {"rotation": ["-45.0-this", "-this", "-this"]}, + "leftleg": {"rotation": ["45.0-this", "-this", "-this"]}, + "rightarm": {"rotation": ["45.0-this", "-this", "-this"]}, + "rightleg": {"rotation": ["-45.0-this", "-this", "-this"]} + } + }, + "bow_and_arrow": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + "query.target_x_rotation - 90.0 - math.sin(query.life_time * 76.8) * 2.865 - this", + "query.target_y_rotation + 28.65", + "-(math.cos(query.life_time * 103.2) * 2.865) - 2.865" + ] + }, + "rightarm": { + "rotation": [ + "query.target_x_rotation - 90.0 + math.sin(query.life_time * 76.8) * 2.865 - this", + "query.target_y_rotation - 5.73", + "(math.cos(query.life_time * 103.2) * 2.865) + 2.865" + ] + } + } + }, + "use_item_progress": { + "loop": true, + "bones": { + "rightarm": { + "rotation": [ + "variable.use_item_startup_progress * -60.0 + variable.use_item_interval_progress * 11.25", + "variable.use_item_startup_progress * -22.5 + variable.use_item_interval_progress * 11.25", + "variable.use_item_startup_progress * -5.625 + variable.use_item_interval_progress * 11.25" + ] + } + } + }, + "zombie_attack_bare_hand": { + "loop": true, + "bones": { + "leftarm": { + "rotation": [ + "-90.0 - ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4) - (math.sin(query.life_time * 76.776372) * 2.865) - this", + "5.73 - ((math.sin(variable.attack_time * 180.0) * 57.3) * 0.6) - this", + "math.cos(query.life_time * 103.13244) * -2.865 - 2.865 - this" + ] + }, + "rightarm": { + "rotation": [ + "90.0 * (variable.is_brandishing_spear - 1.0) - ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4) + (math.sin(query.life_time * 76.776372) * 2.865) - this", + "(math.sin(variable.attack_time * 180.0) * 57.3) * 0.6 - 5.73 - this", + "math.cos(query.life_time * 103.13244) * 2.865 + 2.865 - this" + ] + } + } + }, + "swimming": { + "loop": true, + "bones": { + "body": { + "position": [ + 0, + "variable.swim_amount * -10.0 - this", + "variable.swim_amount * 9.0 - this" + ], + "rotation": [ + "variable.swim_amount * (90.0 + query.target_x_rotation)", + 0, + 0 + ] + }, + "leftarm": { + "rotation": [ + "math.lerp(this, -180.0, variable.swim_amount) - (variable.swim_amount * ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4)) - (variable.swim_amount * (math.sin(query.life_time * 76.776372) * 2.865)) - this", + "math.lerp(this, 14.325, variable.swim_amount) - this", + "math.lerp(this, 14.325, variable.swim_amount) - (variable.swim_amount * (math.cos(query.life_time * 103.13244) * 2.865 + 2.865)) - this" + ] + }, + "leftleg": { + "rotation": [ + "math.lerp(this, math.cos(query.life_time * 390.0 + 180.0) * 0.3, variable.swim_amount)", + 0, + 0 + ] + }, + "rightarm": { + "rotation": [ + "math.lerp(this, -180.0, variable.swim_amount) - (variable.swim_amount * ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4)) + (variable.swim_amount * (math.sin(query.life_time * 76.776372) * 2.865)) - this", + "math.lerp(this, 14.325, variable.swim_amount) - this", + "math.lerp(this, -14.325, variable.swim_amount) + (variable.swim_amount * (math.cos(query.life_time * 103.13244) * 2.865 + 2.865)) - this" + ] + }, + "rightleg": { + "rotation": [ + "math.lerp(this, math.cos(query.life_time * 390.0) * 0.3, variable.swim_amount)", + 0, + 0 + ] + } + } + } + }, + "animation_controllers": { + "humanoid_baby_big_head": { + "initial_state": "default", + "states": { + "baby": { + "animations": ["humanoid_big_head"], + "transitions": [{"default": "!query.is_baby"}] + }, + "default": {"transitions": [{"baby": "query.is_baby"}]} + } + }, + "look_at_target": { + "initial_state": "default", + "states": { + "default": { + "animations": ["look_at_target_default"], + "transitions": [ + {"gliding": "query.is_gliding"}, + {"swimming": "query.is_swimming"} + ] + }, + "gliding": { + "animations": ["look_at_target_gliding"], + "transitions": [ + {"swimming": "query.is_swimming"}, + {"default": "!query.is_gliding"} + ] + }, + "swimming": { + "animations": ["look_at_target_swimming"], + "transitions": [ + {"gliding": "query.is_gliding"}, + {"default": "!query.is_swimming"} + ] + } + } + }, + "move": { + "initial_state": "default", + "states": {"default": {"animations": ["move"]}} + }, + "riding": { + "initial_state": "default", + "states": { + "default": {"transitions": [{"riding": "query.is_riding"}]}, + "riding": { + "animations": ["riding.arms", "riding.legs"], + "transitions": [{"default": "!query.is_riding"}] + } + } + }, + "holding": { + "initial_state": "default", + "states": {"default": {"animations": ["holding"]}} + }, + "brandish_spear": { + "initial_state": "default", + "states": { + "brandish_spear": { + "animations": ["brandish_spear"], + "transitions": [{"default": "!variable.is_brandishing_spear"}] + }, + "default": { + "transitions": [{"brandish_spear": "variable.is_brandishing_spear"}] + } + } + }, + "charging": { + "initial_state": "default", + "states": { + "charging": { + "animations": ["charging"], + "transitions": [{"default": "!query.is_charging"}] + }, + "default": {"transitions": [{"charging": "query.is_charging"}]} + } + }, + "attack": { + "initial_state": "default", + "states": { + "attacking": { + "animations": ["attack.rotations"], + "transitions": [{"default": "variable.attack_time < 0.0"}] + }, + "default": { + "transitions": [{"attacking": "variable.attack_time >= 0.0"}] + } + } + }, + "sneaking": { + "initial_state": "default", + "states": { + "default": {"transitions": [{"sneaking": "query.is_sneaking"}]}, + "sneaking": { + "animations": ["sneaking"], + "transitions": [{"default": "!query.is_sneaking"}] + } + } + }, + "bob": { + "initial_state": "default", + "states": {"default": {"animations": ["bob"]}} + }, + "damage_nearby_mobs": { + "initial_state": "default", + "states": { + "damage_nearby_mobs": { + "animations": ["damage_nearby_mobs"], + "transitions": [{"default": "!variable.damage_nearby_mobs"}] + }, + "default": { + "transitions": [ + {"damage_nearby_mobs": "variable.damage_nearby_mobs"} + ] + } + } + }, + "bow_and_arrow": { + "initial_state": "default", + "states": { + "bow_and_arrow": { + "animations": ["bow_and_arrow"], + "transitions": [{"default": "!query.has_target"}] + }, + "default": {"transitions": [{"bow_and_arrow": "query.has_target"}]} + } + }, + "use_item_progress": { + "initial_state": "default", + "states": { + "default": { + "transitions": [ + { + "use_item_progress": "( variable.use_item_interval_progress > 0.0 ) || ( variable.use_item_startup_progress > 0.0 )" + } + ] + }, + "use_item_progress": { + "animations": ["use_item_progress"], + "transitions": [ + { + "default": "( variable.use_item_interval_progress <= 0.0 ) && ( variable.use_item_startup_progress <= 0.0 )" + } + ] + } + } + }, + "zombie_attack_bare_hand": { + "initial_state": "default", + "states": { + "default": { + "transitions": [{"is_bare_hand": "variable.is_holding_left != 1.0"}] + }, + "is_bare_hand": { + "animations": ["zombie_attack_bare_hand"], + "transitions": [{"default": "variable.is_holding_left == 1.0"}] + } + } + }, + "swimming": { + "initial_state": "default", + "states": { + "default": { + "transitions": [{"is_swimming": "variable.swim_amount > 0.0"}] + }, + "is_swimming": { + "animations": ["swimming"], + "transitions": [{"default": "variable.swim_amount <= 0.0"}] + } + } + } + }, + "render_controllers": ["controller.render.zombie"], + "enable_attachables": true + }, + "zombified_piglin": { + "identifier": "minecraft:zombie_pigman", + "min_engine_version": "1.8.0", + "materials": {"default": "zombie"}, + "textures": {"default": "textures/entity/piglin/zombified_piglin"}, + "geometry": { + "default": { + "bones": [ + { + "name": "body", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16]}, + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 32], + "inflate": 0.25 + } + ] + }, + { + "name": "head", + "parent": "body", + "pivot": [0, 24, 0], + "cubes": [ + { + "origin": [-5, 24, -4], + "size": [10, 8, 8], + "uv": [0, 0], + "inflate": -0.02 + }, + {"origin": [-2, 24, -5], "size": [4, 4, 1], "uv": [31, 1]}, + {"origin": [2, 24, -5], "size": [1, 2, 1], "uv": [2, 4]}, + {"origin": [-3, 24, -5], "size": [1, 2, 1], "uv": [2, 0]} + ], + "inflate": -0.02 + }, + { + "name": "leftear", + "parent": "head", + "pivot": [5, 30, 0], + "rotation": [0, 0, -30], + "cubes": [{"origin": [4, 25, -2], "size": [1, 5, 4], "uv": [51, 6]}] + }, + { + "name": "rightear", + "parent": "head", + "pivot": [-5, 30, 0], + "rotation": [0, 0, 30], + "cubes": [ + {"origin": [-5, 25, -2], "size": [1, 5, 4], "uv": [39, 6]} + ] + }, + {"name": "hat", "parent": "head", "pivot": [0, 24, 0]}, + { + "name": "rightarm", + "parent": "body", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]}, + { + "origin": [-8, 12, -2], + "size": [4, 12, 4], + "uv": [40, 32], + "inflate": 0.25 + } + ] + }, + {"name": "rightItem", "parent": "rightarm", "pivot": [-1, -45, -5]}, + { + "name": "leftarm", + "parent": "body", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [32, 48]}, + { + "origin": [4, 12, -2], + "size": [4, 12, 4], + "uv": [48, 48], + "inflate": 0.25 + } + ] + }, + {"name": "leftItem", "parent": "leftArm", "pivot": [1, -45, -5]}, + { + "name": "rightleg", + "parent": "body", + "pivot": [-1.9, 12, 0], + "cubes": [ + {"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [0, 16]}, + { + "origin": [-4, 0, -2], + "size": [4, 12, 4], + "uv": [0, 32], + "inflate": 0.25 + } + ] + }, + { + "name": "leftleg", + "parent": "body", + "pivot": [1.9, 12, 0], + "cubes": [ + {"origin": [0, 0, -2], "size": [4, 12, 4], "uv": [16, 48]}, + { + "origin": [0, 0, -2], + "size": [4, 12, 4], + "uv": [0, 48], + "inflate": 0.25 + } + ] + } + ], + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 1, 0], + "texturewidth": 64, + "textureheight": 64 + } + }, + "spawn_egg": {"texture": "spawn_egg", "texture_index": 13}, + "render_controllers": ["controller.render.zombie_pigman"], + "enable_attachables": true + } +} \ No newline at end of file diff --git a/renderer/viewer/three/entity/exportedModels.js b/renderer/viewer/three/entity/exportedModels.js new file mode 100644 index 00000000..fde9391f --- /dev/null +++ b/renderer/viewer/three/entity/exportedModels.js @@ -0,0 +1,38 @@ +export { default as allay } from './models/allay.obj' +export { default as axolotl } from './models/axolotl.obj' +export { default as blaze } from './models/blaze.obj' +export { default as camel } from './models/camel.obj' +export { default as cat } from './models/cat.obj' +export { default as chicken } from './models/chicken.obj' +export { default as cod } from './models/cod.obj' +export { default as creeper } from './models/creeper.obj' +export { default as dolphin } from './models/dolphin.obj' +export { default as ender_dragon } from './models/ender_dragon.obj' +export { default as enderman } from './models/enderman.obj' +export { default as endermite } from './models/endermite.obj' +export { default as fox } from './models/fox.obj' +export { default as frog } from './models/frog.obj' +export { default as ghast } from './models/ghast.obj' +export { default as goat } from './models/goat.obj' +export { default as guardian } from './models/guardian.obj' +export { default as horse } from './models/horse.obj' +export { default as llama } from './models/llama.obj' +export { default as minecart } from './models/minecart.obj' +export { default as parrot } from './models/parrot.obj' +export { default as piglin } from './models/piglin.obj' +export { default as pillager } from './models/pillager.obj' +export { default as rabbit } from './models/rabbit.obj' +export { default as sheep } from './models/sheep.obj' +export { default as arrow } from './models/arrow.obj' +export { default as shulker } from './models/shulker.obj' +export { default as sniffer } from './models/sniffer.obj' +export { default as spider } from './models/spider.obj' +export { default as tadpole } from './models/tadpole.obj' +export { default as turtle } from './models/turtle.obj' +export { default as vex } from './models/vex.obj' +export { default as villager } from './models/villager.obj' +export { default as warden } from './models/warden.obj' +export { default as witch } from './models/witch.obj' +export { default as wolf } from './models/wolf.obj' +export { default as zombie_villager } from './models/zombie_villager.obj' +export { default as boat } from './models/boat.obj' diff --git a/renderer/viewer/three/entity/externalTextures.json b/renderer/viewer/three/entity/externalTextures.json new file mode 100644 index 00000000..82ad82a8 --- /dev/null +++ b/renderer/viewer/three/entity/externalTextures.json @@ -0,0 +1 @@ +{"allay":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAV1BMVEUAAAA4jNkuh9Ypg9FBnNpCm9r///+c/P+A/P9k+/9N8/hM6f9Q2f8+5eo94PdG0P821+4s1ewiz/8syf8ox/8bxPNNq+wQwf8os+hCnNpCm9oep9s0h8D7j8AOAAAABnRSTlMAoKCg3PODq/XXAAAA60lEQVR42tWP0W7DIAxFb9OtwdA6xQ3rzZL//84BD0nRljzsZdqxZCx8hLio3Lz4GwtYMWLDi4jnPM+vwogNKTTC43Elr6vgRMQ1wvM5ktsbvRPXN38wGxKHMmnI9M65vgxavwsg2WCpCvlSg6iqlCF4ceKbFEGZVC+5NFFXYUtxZzILl1xmiffoxUcA5DUHKULZp6iqsQzU6KNWYcxBUMnp8A2mwWxAhTPR0qRYFs6sDT+zTMZpWj5pO8L0wamexJ9xXvm10J27rrQ94dSdOqC0PSFXPV4JRuwRzIxGHMB38O14j4N9Wf4/vgDzpRGDwkNeQQAAAABJRU5ErkJggg==","axolotl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAP1BMVEUAAACvsv+zsPmmqv+nn/+GkPXWhCt+iOyBb+d1b/9odutodOyTWuRbb+NlZfS0W0xsV+JpVuNNWc82QZ86AGw+Ir4cAAAAAXRSTlMAQObYZgAAAUdJREFUeNrtk9FugzAMRXG4twFS1jbl/791cbJUQnuAmE2VKk5cKpDukeNA9zeAhP4mwDlgahdAEoDm1dAukIzmlXaB/HBcUGbQJCBkBejgkMrt3j5WeYGD0iAglkXkehVZFlBQ2b0FkKjtE2CjIIYEUfIhESdMCUyWoyBFZPZI+Nks8OUs/f7kXSsLgCxwiRbBl9YRgddazcC5lhnc/T1VFdQObDMIwdJBRQQhYNWBTTCXYzR0AMZIyOy9c373FuQFGXLJLzYFEJQvQSHKgwKwJQAF1BWqIBC6hNlKbAg0GPXyjDHkeqoj6iVAn4DdP/NQhuHxsAr6HroAswB52QUcySTgeGQGg3J8lPY55h7Q9705XQdpEgwaHXVhtHZQX6Whew+X+mfldsn5W3fEYM9fVjfm/pXShd1g38UrdvuIGZycnJy8kW/CDQ5IMRlUUgAAAABJRU5ErkJggg==","blaze":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgBAMAAABQs2O3AAAAJ1BMVEUAAAD//////4T/+Ef/1Sj8shf8lgDReACrdQGLNAFsMQBfAgExDgs044iSAAAAAXRSTlMAQObYZgAAAPdJREFUeNrdzzFuAjEQheH3vNR4xtCkibyQA4CSCyAlfZocIjdKm4sFxAHwGKVmQo21Ts9X//IbE1evBfpBjG9ozXDlJZkKTkAr4IpShNVkMjiJ7wuUkwEJhcAmAzFGNcTJoFisJZph6heJiG7xDLT4XKRC4KiubvSf24kY6hiLnVPMVfE+NhPbo2Y3YVBfPA2/Q3PkchGGx++H4yZvd3GdNrjBr32FeDzPUdN88Mvn7QRHHiSX9SkMwnxYNTdkq7sDlMsLkZHRBEwBGaBpKnQ6GqsVegLIfrDOfOkGI0W7gQTN6JhJQlcYCOm+oJyjHyD+F7jg7v0B5TtIgi1mkx4AAAAASUVORK5CYII=","boat":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAABABAMAAAAg+GJMAAAAG1BMVEX///8AAADCbT+6YzetXTKgVjCZUCuPTCqIRyg96zNeAAAAAnRSTlMAAHaTzTgAAAPOSURBVHja7VTLbus2EBX6XOcPimGzL3jG7rbIzChdFjap3GUB65Fl0ZtY/oFr67NLqoljFVDkRXFXOZZnQI/nmOYcnqL4se/7U59xyGGy+qtI+OanHDdWvOLmEkXxnWp1DKZqj43mVB2jakiroH+8EfhZgu8t2BdVC/opWCbKq6hppfbbG8F2juApxqp9RRPDp+G0f112hyF9/9tfxr+wmiF4NvZE6VmNMdyfWoAB3oHRHs4ENrsDgzYChcYc4/3eHhq1YFWOXfr+D3/mrjueIdhHgVgVhMcYu2c1a/dRY473ZwKPGYLh6Df0MzndjvFxeFoDW1glOT4Q2n6/81TcydwOyqHvhqHfH3I6NN3jcBoSDmM6Oq2GUxAqUM5OQTjEIGBhtm28b7Sq2pgnGWJbslWJ/cnP6+C5jGKKlUXvaePrh7axJmjT1ow1r7ns0kZOvpAwt4PSOyFHxBpFtL6vYQIBx6oxq1zZJ4IvVGy2s2NkgEO0ynYcY7yvg2pVNe1zp0lh+RD7/u9EMDvGath3w6k7jOkxH2I/5AMc0/HcJavZMToLEI0rckSrx+GztcGsqncQcHfuWtmskG69c3TriDlaFbvPNUSwEw5V0/bnLtLZMSqiCrOM2wgP+1A2eYSat/G2A8YcgVngChaj5G3kyxSqJ2vbRrBrD+cux7O3cXKd4+/T69z4HZFLj3u5tGvXTwmKuEXGCv0rLn9COELBlfqXSxvlvwS0oYxbGl5xSbDsieS33hP8ypG/cyBP7C/qy54I2TFUsE5xJyrKpVzUlz0RWgsEWiprrQLWUi/qy57Y79Ohp6frcsro2ov6sidSAI8GagJhAURxUV/2RG1ijM/pXcUzLurLnlhgOyeDjGVPzDqYymBSd/owHM+eOKMDAmHlHYknELtJnfvDcIz6jici7FjApQlqQGUtk/qv2RFbfccTEWqooDRkGUxUkHGbCfYyeuK8DrqzDCYqyKC+Hw9x3hMpMNYKmIIFIuBJfdkTsw4eJjKY1Jc9sQBGJbykfoJUX/TEgjw2BHKgjSM/TJDqi55I3rmNzwyUkvPkSQj5gzvv6GbZEzH6ADgK1xCDCtISlhJUbpY9EVFWdVSwxUY4WgDXMQgHrsHxCk9sX+ygzcjpctneLCL7gVhAaRaE2QRQBpeM8TXfOPWDf6VwVsN5GRe6l3VwDYHztElvuJQchgmuIaCEDdFForu3dAUBtpIHBnAevKmkzAJAa6heRTB2ytgBE6juFGBBLSwzXf+zDsaBrwUQiAnALChFwIB+DR28X/4g+CD4ILgW/wAFIWww3uybGQAAAABJRU5ErkJggg==","camel":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAATlBMVEUAAAD8w2n1ul3prUrbnz3xi1ywmYjLkzfrdTmoilu4gieqg0Scf1DaZiyMcEV/b2OnTiFtX0h3XTNfUj5VSENfSCN/ORlJPzBRIQwvKBwTHWK4AAAAAXRSTlMAQObYZgAACApJREFUeNrtmItiqzYMho2kE7VeKbRJGX3/F110sQ0xlNC0y85Z/6aJLwJ92EYWhBC6U4iil5fjy0uYCppGPmEiRIyRtSBFkVVYC66maQCbs8JH+iuYDgngeDxuAwAwovQhInA0GpAazwAauBbgcAgGcBTNAUBOBLPrB0A+awrAaADMMwDYAkjmXQb4+wIAFQDDkgAQHKCWHnYNgGsdABpYBXgCYI58K0BUMRMicTRlAAQAXAbAx8dHfGLmpxUAaK4D4OwfkLkCwBpAqZ6ezgBPT1Iu/fsXoQOwAAAgOYH6yLeZFKQOJut8fX17ent9XQcAhD0ARAhYAAQH3t7ftfb+/gZClwHwTZgeXx8RpfxpgOcCQITqQ1YBJ4DJCCQAdIDz9aukgPjZNWBBJ68AI5DpiIEIVFEEKmEUFAyAKCswf8PyGsAtgBeLu+5fgG0VEHEQbwXA2QhFFNbUFBkANHMB6HVlgGMGkHa3YDIAZUKKMZIUzL+OwToAFE+N/+UmrQBOASTurgGgE5Ao+Qe7F8KaAJNrkTlzlLME3ucwE4QMgJCngIg5gBO4Y/sxrQMgpqtEQkLU7wwiVflUQ8h2E6iIWKRxAI1A/8mmAUQY1qReHbdIWmDWWgGwDjupSVQFVIfyXQq4FBdXMok9Nub+LPk2AA4I6ZILCC7dBSuZxB4b88zEImIDELcZIJWJkJAqgKVMYo+N+0ciGwsFmM5ALhLLnwJsbeQ7bAyAAIGYMgAnAiz+mVj11QDqH4EsCjrAljYziWJj3RyLagAktI8RMIUtbWQSRZHZt9lVAGZxTc+EhEZA2wBLmQQvAcy7DbkGiM9Hfubjc0S3C5uqMwmRpjIi9Hs7+AAROYAQ14P0fDw+H+XrYiAbFTBYQc/pIqLLTEIVfBNSC+JQ+tVadX2sUvcKAAbARUQXmYRfMjg0s1bNf14FeuD1sUq2J2iQUH8b4qnoMpMg8RIaE5OIA3m3E7AYXR+rfGdH39fTNfjv5UaOZxUAJhuB1A0GIEbboaIApG1U3bh/MoIagAjnI7AEQLQHAETEBCIFiHJmKyxlEgVANJkCQAMQo+tjlR+ZvHjcIbddzCTyIiRi4ckGnnCL0YeximcAlkawpRWN+08El5mENTqAj1SIeiw5tk3iZqwqAN7MYuMAZQSWMwmPAzGyAzAno+VoWcWq2RSgYzFj0/gApDNVmYSYCTSILZtN7k0cXAPMY5V8pgBGpABGkgGqTIItnRWB+YuBrBOdwGxmmscq8zGZAo4mRmjm/mOVSVhCb4rmTwDIbdhBLwDqWGWMDhCTHIBUalllEjOAyOs2M1WhQk1qgPouWM4kygjYFPDEhsxmH8D6CCxmEtMRiGs2M63HqnoNlEUgzcuZBJp0KtdsZlqPVfVd4P6dYDmTQJPBrtjMNI9VVSByaPYRyABmVCsDmMK2qlhVRULzawCRZkbogvwE0yBKgeQ3Nc9VAUxiVQ3gCbcGy2ozcgDKTzBi5G8TGqloq1mU73ChKlaJlwwAYHcOAfgUuOYAVN4IEBIgkdZUVACkWAOUWFUBgG2HbJsh1GtAz5sB0F9pIBFpbQ7gPVc/GdUJSfJfRiCfFwHBLpKJDZkQtZUEUcTWVwOsxyqwlIwtJQOOsQxAAdA29a5dFEk5jcHaDICjnfv6JyN7s4SMoIk5c0kIHIAFwNeQHipOSBxZFcVWARij7bY1wHqsUv+AhKAEpH3qRWTH6hVb3GPx7GKeNiMjl8fXfU9GnhVL0c5WQvEXyV+q6o8WSoMWbQq0XDq1Vo5LQv/dCeDnqAWz54LKJru/GcCvL6uUPVsldD+1XTm4AEDYI7DrnF0hpjKQrzzKLTgbIRsbx4MI+wHyO8lyJSAVL4p/JQD3KJ354ss7TDsuWh/uBGgAmTC/1wWgNOeAFE2EkNYEJTutMEKzC2BcADACBPRNldK6xwkApnuCiqH5NwABjwSwBTDUAHIK9sQIPLpaK6ImSfKFmJ9Bix0xa6t2ap8W9gHYhHuMRf2j6GsCiSPbh9DnPLqVZyZgrQAOJwUK6+pXXrJH4pxykzqBRgGSCHWWtS9bMkWwPduWMpMVkrP24eGh7YfhQwDf6/NG6/Eb9cxS1T9m5UTpy3Zi6f49qfWJrC67d79tGyrZLehZrCgngQAYI6f9V+v5CTbb6wSopO5coVb7IAr/ltqhFw1t+D7p/I6iUGsYx2GQr3A/Cdmt8on73DgO/RDuqr7t/1yAdq3WnSqAr/dSolldO5xuXgPbXsqOWtcOh677srtgWK2N7sTVjaXYnU5fEwfavu+G3gdRCl3ft63GnrYagTtEwjVqL/SC+uvXL8Gt274PSgfMh6w19Uttfy7AMPbmYRyG3jQstX3n9jOI9Ne11BZ+9KMf/eh30tj37XkLa/t+vBdAa7oXwJABhh+A/yVAOw69J3L9MPbjXGFRnrkmm9t0dpnTmHFsLxKNsKgzrxCb5a0AYXKGuzzq3x+gHwpL/wPwA/AD8F+OA22/0fW7ArTTveCjzait6KquT6mf7oYfbUb9sLGMvjsfuD/AuNH13QDDuNF1f4D7Pxd0J1F3pycjf4vbHQ4b8aKqjEPvMzDus9zWNoCfd7C3bTst9wMMVWWv5e37Xl3ZtvxjAA790FWn3WN5O8DYySry17ujlK0STKk+mqUbjl/22qAbxlM57eTNbnKQ62JZUL3/9kDUnfur8LnD8vZANI5V/L7J0r12G4XqtFXFtd/Sx32j8N0Ap9N24R/eOq3aGiGdPwAAAABJRU5ErkJggg==","cat":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAMAAACVQ462AAAATlBMVEXv9O3r8Oj72bPW1sz71Wf2ymD1yF7ywFnxv1jvt1Pqq0uvdh57bWJjSy9KMBc6Kh0RDxgAAADq6urOzs6vr7C+c3NanRJWU18kJDEcGCdfq47cAAAAEnRSTlMAAAAAAAAAAAAAAAAAAAAAAACVcz0/AAABUUlEQVR42t3T3VrCMAyA4Yi1SmtlSxSS+79Rkz6R0dEywTO/0rPyPst+IFhiMSJyAW+amJBFA08EEWBeBQtQ/1BKuRuY5yAusACo4ABSBUpFxbsCPkJKDgiLgAktANYYOFhyDlxQgFCFbUA6VaGg9lsAj3LExmBCi1i8MdDNBWJ5GBDmFmBhFuE7ANQWgInVY+q8B8wh58w8gy2rBxASIRJ2gVwBOOf3oAFQ6wL1kP0QeQU0AhRdIb5eAe+15V1vAWqB/X4E2PgrADtA3O/XgJ3WTSDeCJi8zcc4eAo+QroBsC7hC4AMOAspxGQNAUZdTDwAtkdAQrTdALQAMYX4FkIYA1VwoNZ+CynqCLeuIOSkAYyAqRZjFyDNAe8FytIOLuoCNv3hkLPtkP3k8xNc1RthZ6YiPwAMmmoAHUC028DGCKevkwg8DnxafwDmO/uHwDebkIAap1EwrQAAAABJRU5ErkJggg==","chicken":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgBAMAAABQs2O3AAAALVBMVEUAAAD////i4uL85XDT09PhzGLGxsbgu2nTrlrQqT/Bk0OWcjRjVkH/AAAAAADtfCsnAAAAAXRSTlMAQObYZgAAAO9JREFUeNrtzMFJxFAQxvFZrGAewaMwwxQg2IA7fMzZElQecxZswHZc7CAd2MM72IlJVoXEbATP+7998OMj2kmHW6LXQ9/T0AUt25UuB/D+dgQ317+AXLZ7OgxN4vmJ5vGxforGXhbgSj9ElFqjrxYXomN+GriqmqG1H3E3B7DmQN0A0Wpk0laqKlw2gKv9AdycbRNE1dgA1SOy0hTWQCIrgqaUlnGBqkLY4aeAq0FYfT/OFRBQVGERWQddAKh7ZuFVUCwqorow8166jgVzoBaZWV2FRZS5iM2BYwKwBzEfjJSYg/zOIx7hDk86948+AcUmS5dD8ZFCAAAAAElFTkSuQmCC","cod":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAJFBMVEX///8AAADr2sPWxa3KtJXCqIW2lmusiVmScVqEZVEiHyYAAAB2QJV6AAAAAnRSTlMAAHaTzTgAAACbSURBVHja7coxCsJAEIXh5w18RSRiZQT7sAS8Rgyz2kbYIN5CiDBbx8CewHM6QS3sLf1hivl4yMc626+ZCgKQzENVF6eSsaCVkRCRfCiZhFYwcM4t+13SqFdOwS6/Vw+5HA3qFyz7beOqriX9ezFsKmlcySn4lFKMMQiJGYWwR9VQyTnGmggh3M7eeyGBQ0u4TwZkR/C7P/weVk/OOkCzBdD8ogAAAABJRU5ErkJggg==","creeper":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAK2klEQVR42tWZ61NU5/3And+L3zTNdDpT08Q0NiamOtFG8cJlQa4uy22vsCzLLrssgtw0KooIMcaqFFFRXOSiQjA11lwIVRF1QRBW7gIuy9H1pLgpM9SZ/hmf7j4zzXT6Jn1jkTPzzNlzec6e7+f53s+yn9piOzdzwltJp3SdNrmZW1IHKc5E+ufvUDJhJndYR4/UTa5Pw8ePrWQ1mrjp/5qhhT76JRfLlvoWcXYzceei+EMAws7v06n070R9LpXxF0NktKSjuR1L7/wtSr3ZHBgsYNDfi8Gp5Za3gw7/1aUPIHkwjLSxKAytGg5KBZT1FOD3+/nkWTH75uwMzQ0w9sJNXk86X/nbyT5lpuqvhWSPJnFL/mrpAyh6bMI+osPUmEnetA7bPS0j824OeXdS6DbR4+ui23MDSZKYm5sT+3JvPoYuJU2e00sfQHrfdpTd4bh9A0LVi3stQsiqsV3sdtnF7wOTeZQ/ykeWZbzyY/IeGnDUOrgndy59AHG1CmxuHa1yPabmDF68eEHB5wVitXd2Z9Ev9XJkdhelXTYBQ1+nE5B2XilgcL536QPoePInrnmvsOz6MrT3NUzNj2NxWshx5uBodmB2ZmHo2c7M/BTGJgPWhmwRKfZM5nBwomDpAzD0a9gxYhQAXr/6Os8XvhcAhuUB0j/X4BjWUThnxNRjRD0Ry26fhYzzBg75Cyh/UPjqA1DPxKC/r6Td24RjVI+lJ42cOgtFY1nYh/Rcf/wFy7/9JZruBNrH24Wa1/gP8tn0XrTONEpnzOwbzaPl6VkB6Xe33iWpbjslwxby+4w898/hGDSwtzdPRI978i0+9lnZNW1FU5+2+ICyHqSgP6PjrHSUlBYV2t446qQTJN2PEgnQusa1ATAWElzh7H1UiKYhhTK/ncJ+k4Cx566D5AYl9z33gwDEuC13cGikhPl/PMfRZse90EvpkF1cW92+msrJQgrdWVzw1LL4Kt6ow+fz0em/ykXpnMj24k/GEn8+AMBTTvJoOKsy30KRvZlG+SzmS1m8q1sRPBYrWnilUHj/q1NX2dS1id989zY51VbMrZlIPi+H/1rMoYDArmnXj4CyhtScnq1h73j+4gOonCrih4UfMHuTyXmqpsN/DZd8gxb5LK2yE81UjBA4cCvFMyaSb0eK329FvUGVpzioDQJE/qwRi5TG9/5nYv7oC7fQkE/9JVQMltAr32PFzTf4xdev0/aojdimGJTuMP6vbdnPFhWAozsDkcpOWcXqKFyh2CU16uloqqUjnJZOUOgy0yTXs1L/Br/VvMmw5CblrIpcr4bM7kQhaPXf9lPeJ7RBCH3YU0qLVMdl2UnR7WwkeZY94zmUDxaKOsLQoqPoSebia0DecDoXZScJDbECwPKOX5E3qSX+dhjX59r4s9RK+kWdUPnA7WKsMq2gWDYxKPWxe9gmkh/9ZQ2Vo6UiTzgwvYMDDwuCgor0WFevERlj0YgZe4udPc9zMM4oGZOHFh9AbJOCK1Ije6d2EXVRQeaFDOJubqXaW0mDp0ZUdcVXi4Tg/z4c/QbSL2nRtKcKDbjmacXozMDeamP/QD4LCwtMShMMzw1gv2wjsS6Bsgc7xPlPPKWYXSkBGDYWXwMmdUEBRIJz1ddMu9yMsS+RG95vSGlMEit94m9lwuYbpT8Gzwsz2FYfQcmMGePkdjq912iW64MOVAh9yF3K7LwHq9PKvhk7H89YSW8w0Om/hnu+lz0PczFeNlA1Xfzq5QkZl/W0+xqEEzwmVfG+6h10Bi3bdfEoDKH8ZPXYmIA07xUFkk/2sacvlztyJ9f9X9Ar3eVLbyvai6lBkxCmUDCQyRVvCxVPCnCM6BYfSDAiqM7Fc9RTToOnmhXhb/KB4X00VdtZqfk1PzU/GDojzmyiyXdK+ABbgw31hWTOeY6z85kRw8x2kTdc9jYQcS6MzR3riHVFcdxXRlpf7OIDyD2Zy+GZErJGE0U4eztlOaEV61hr+oBVmrf5qfnbasJIm1BgH1YzK3kplIzYn6gxNOsolI0YHii5LrdRIBkJda1HRJ7eUHIGNDxaGFl8ADMLjyj63sTO/mBr6yve177DyqS3eCN0Oe9pVvxXGmCYiiPTpcITeNa+GRtlQ/kk1Megn44ht19H91xHAHAqb327nJQBFbWzxznmKRcR438ucLCo2ekxYvWoaXhaJ1bkva53yK4145YG+HjQjqYrhpOeCmESyV0Rwjeo7ilI7FaIVpnyYSiq4fDgPSJ9zn6YSlqtikfSGFGHN+NwGfjUWymumbtTRNTR9SuxdxuELyiczRDzj3krhSPdULWWNRnvCT/00gEE/1T7OJbtfwnn/PRJRNX3zWtY2yzMyjNknTeyz72DW/6vWd3+Hkc8VWgbU4WPiOvcjOF4CiFHP+DDPatIP5HGGtNqdrgzyGjS81R+QpgplKQj0XQG5oef28oV2RmYnyaSqdIHVlE3FDxLF6F395kS1ls/JGT/GrRNqQLuSwegcaZSMGIUL7Kx6SNCLm3gnq+LHZKBfRO5jM8PiZfXt2tYdWOlKIxCT29AUb+VyMBQnNmMyqUQqx92KoSPLOuprDuIZTKZ5PEoATTqQhgfFa0XYfCy9zza5lRMLUZszTnoz2pxPDBQefIgIZaPSL+kF5mpfUaDeiLm5QMom3Dw2Q+7eDo/i31Wzac/lOJocbB/No+Do0V4pRmsj1Ip8ZsIrRMtcsJPhwiBoxtCUT0MReOJFoVT5piSbF8qCls4m459yLbbm1D3JbHy9pusav0NIbYNNEunSGxMEN2irM+NZE4mElUTgq3GROa0EtOTJIQZ3n0b2+D/ICzW/r2C8gGRw1Mynv2vUPfjfmruEbl9OvIHM0W1eFE+i84TTfL9SCIDKq19vA3lnQihrmXj+aJrvKM+jy2V61HsD/mxAoy5t4XMaj364ymEVq8npGINH5jfZeOxNWwIqHxNYH6mW0VKb7S4/2df/D9bKta/fACHRoqxNdkEgP8QXuwDQxRE6XeVYvWD3wlSxxSo7oRR7SkXDdOw2hDhwAwXdFhdapHsZLgTOSkd5bWm1/j5n36OqnubmK9siEM7GUuyK1oci/mnN4lnJThjSOmKxdFsI6T2Q2Euy171LbIhTAgfcIoilT4tHSHmeAQR9VuFUOrxSKJrtlBRVyaupbQkkv/AyA3pujCbmEZFsM8gntEr3+XIuU+IyozC2mrmxvy3rz6AkKO/I++4GdWJSNLOxPJZ9RFR5YkGi+xENxVN0QML+2p2EXl0I6b7SSKcJl5IQDsdzw6fnrGFIYzjCcIciqrz6Jr/jpvSN8Fy/NUHYO7WCJtd17saU18SJdUFRBzfgPlJkugFnJKOkuPS0OG9TuSRjYSUrWVPTQlrzKtRnNhA6IH1VJ05yMZAKN1YsVZohmNCg8GTENSqVx/Altu/FwA2XQph273NQq1//+lq1u17H6fvlHBusefFt0VsPTp0dRoRYYyTcaJeUN0JF98RExpjUN6NEB2prEklCbe2cmwpAPjll78QAPJHLWxoWidyg8SuSMrO7KLs5G76JBc5ly3onGrS7kYJh2rxpKC8E0Z8QxTGVl0QlACnu6BG2RxP/M2tXJMuBfsRS6CtPhZNm+wU9noyMAKrJjrK33ivEGLdyOHnxRQ8y0DfrAmGUfElKfNuUgBUGFkziZgmVQzIvaIBUx+A853vS5J6FOI40Rn30gH8ExZbYyyH/1DEAAAAAElFTkSuQmCC","dolphin":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAP1BMVEX///+wwc2In7V8gp1ZV3NXVHBIRWP////D0uO/0Nu9y92wxNmwwc2lutCmuc2dssmSo7qKmrGAhqJrcY4ICR+8V/49AAAAB3RSTlMAAAAAAAAAVWTqWAAAAc1JREFUeNrt1GGPokAMxvFRz9sd2+kz5fb7f9ZrmYECnuDmkn3FL0oaSf8RJaT7iB6Pz8/Hg3J3u91+mzRCITtVIJwNS3JcUzMHSlkErtfr3fQAPQxB8uhVYPAA5xHf3fQNSguU6eSLwJ9hHXBpROTXR5S7F4GcLTBd5ioQP9BuwJF4gWU/wAt2tumBpnQRINoEhGUORIHeDggE0gJlWgLKPHUvLkEEqLUCIhZAVQXsaCoAP2q38yMG1Y8vdcOg7utD9VsBFgNf9zfEcO7eCuTMvdG3bb0X+K3AL3sDzGmlFNpVZskB22xxR9sRENkGUAqA4rDWPoqPk+OngIaKWdVnaVAVo2pDBKotGl+KQhurUa1zQAQjMRHICywCE3/vSrq2h0+/8KnDvC5s95m5D6ntR4BZLpfL+BgRv7WEKKY0Y03d3UWALZ5GlLMAkjPFdBQgskCOQKbl4ThQiEoEQHlEiGkRwItA3OsKalsa07cCg+/51hBTBGQvIIJxTWHUJ0Bk/V8AEXsKpD3M8Ux1ywCI8F5AwJD/CAhUtBUkFYeNMvlXQABVf3kCHgBq1YVagQgcid2N6tIxEWGXZ+ykScdie4En6XQ6nU6n00/7C3s4RNq7Xn9vAAAAAElFTkSuQmCC","ender_dragon":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAAAdVBMVEUAAADgefqKioqFhYV9fX17e3t2dnZ0dHRwcHBpaWliYmJdXV3MAPpXV1dNTU1LS0tHR0eWALwjIyMiIiIhISEgICAcHBwbGxsaGhoYGBgXFxcWFhYREREQEBAPDw8ODg4NDQ0MDAwLCwsKCgoJCQkICAgDAwM0IucAAAAAAXRSTlMAQObYZgAAEQRJREFUeNrtXYt22ygTdm13JSVxnKa/XQsS+1/U7fs/4goj9AkNFyFF9lqHOWcOFwFWycx8DAzqStHb2/5N5Rjn3Mfh9uH+GAc0rG+YRvZ92+/3zQCMH49HyadjnWfgaz2zt1dt6j4MZcknlT82KVP9rykI46g+jKlx9JhH/WzQBOBdkdYcmoDX11eVK0v5spp52S+72/v6oV71USmIjiPbkL4+ouOU5fC++7e3RqQ5/3U6nTQz3i+T9u0zVz/an3OZmkTac0b7DiDefYfhfZ+f3/bNAJhzLXxG2dPe7Mdc9eAViI7DWL8/GzQB5B0G9f3+/e0VAxiC2Ct72qMt6ddpU5aSf/9Tp9YXhxKRvkMnAL+FvFf493me/yXTmk3B47xftrRHW+7oB3aKIhX4vnINRQH8FvKevvvn3fPzfves0rc96wsi65Y5c7fnui0sP2Xmtt4hJRqKAuTd/X1fX9+enl53T69Pu9c6/xpCAdKetoUAU4YozoUCx0gUkIK8e3veve32V6EOoQBpT9oSAQ5bcqAAVSgw6RsYh3PaN0yc+VFgqNV1oYP/d8F8LAqopRQQB78bohgUAPHSKrTIe/rTcaajgEIZyquhRFAgYMUNy08t8GBLjvZQqDCCjPRHfMT8gmhZw6OtzQIPteSkPeMsqq8UeL3+N/0T7WMcdbvAWroVvkZ4ugLuWMNTxHD5AmEUgBKRvnGW/0RQaRCahASItrcKrdMX8K7hz5+y3fmslAh5WR/qSy1/D5WQJ1ZzGjtQgEWigHz++SnbfZ6hRDIv67197ejDnPm+1WyEBRZTCxKHEnTL2sqr8iAUQL13Aj4+ZJuPMwRY5mV9AEF8/git91hfziPKsPbUgY32BajAc24ohacv8UGQD/sFjBGr7y8zj68AjvcFjn2B56yvFGEE4SyQp+OUPUsbLJe+8iRfAAIPhYJSDEOQkuYpQlELCuZmmYXLHGV3u/Ca3BB4KNsgFHCgD3chFDU+YBbaEXK297YLr+cNgdfLGCgF42wYCtjzzMj7JqAcXSY+RBwKGAIPxPk4y/TjQ+aHoIADlcoACritN6fP3eVwvW+JRVFgOFutvQuhIlCAWnn6PJSnKKLGNPf9+8se9RwCDAe3aav3/Hvv7LT8zIVEAatOygQVSF/aDsx99UABn3JBsPFbkXnfTgyPKMPaU0sbjQ4WX4AHlAt9I/O+nRgWUYa1j/AFOEMf3jkEO/z8+fPw+fmzTj/PMv9TtjMPyGQf9Ee9f/1PEcG3E1OS8jH0fJgvgDZ2Pry/vx8+Pt7r9OMs8+94FmSMH3wfigJEwMaiAicWeIgv8CUnwp73CfsC/jV8EBWmngswhrbmya4qrwaQC4kGnRGUx5CVD6BCOe1cwIoy4NizAIJEwR0hzkO+QBgVgALxvgDHb3KO8azto30BO1qFUCBq7U9QINYX4F1rzsjvk/ZhXyCICBEoAKvqKyNfxvsC2FWSqf8cItoXsL93wBfg5FzAW55yOgwU0P1Jv0koQPNhX4BY+cCOUSQKcCsKYIlDfn8CCnDkZ/cFqNUdfC5Q6rYqlTwWBfA+9N2G7gixoC/gbR+upyig26oU449BgbBvQscM7viwAAqwqBghigIYkyEfiwK+9T9Fq8Bp8CTfgCJCwKqT8caiAP1dZz7mNDiqDOaDT4cZ6TcuLkhxB0lUvlUAKJr/XGC8L0DahX2BMApFRIdGMEEB/9o/phyut59GXwUUB3NN3oYa1v6aL79fvtX08vuCfmELGtr3D5V9J7OE+dTTaEv/E9fvggmIiAnCDg22HM2yFmpSPsWcDmPMSez1Ay6/f9Qz8PLj9wXt4mJq4tlhgX2Rosdmza9OpOXuT9O33g1SYzZl/RtqPFX2IsDl95+Xmv5ABeYnxuN8AXraXPNB5g8HxsOn097fryfgz48ff/5ABeanoC8QRBFZdzgef177hHwRf4ySnABJUIH5KTJSFKEYunz89vKtrCfgndeZkxGfZEMd724QJuB2KhARKQp0gDLU7b69vHxjdVmmsmwokM83wO8DBRTdUgVid4QCvkew7PMDMAE3VIHoc4H6Gay7HVXa/KBIVTjTnQkorZeCZqDoSFEWPC0m7QMogDU+IypALgVNp6mRojTmqM9xkarGCbNHBfb7t/08KFCOOh3WbenSCu08t9ZAxpbqpaXSci9iBhXwRIp6bpOZVp6zv1++XenlbziwQAke9iVaZcIEoJ1xNXQGIrsuodghRnyD/7cTwGzPwyjQKgEmgFmvhs5BFmsfE95wFdx2AkrynJQH+jI3RYH4GCFttS0ogOcnLtsj5UphRvLMKECtvbt+KlPnupeXKcrN786IAlxbc2LZ4QCjfvLveeKDfM9mQ4FYYfwK1OEaLbhCCSgVFFKVgSi7nQMFIFzL5vwvoIBzLQ22L2/6yxXkewrQllEPNhQETBSIXN6x+wLYByQoIC1/w9/z799l2jKx4kGO3P8fOW58jBEmQCpCixby2X632z0rllc9ZaoYiIAdnTDHWfsZxrX7AtgINU+A9b1oxfv6wr9MFQMRYFXjOLzzM8O4dl8AG6GGsrb3op92kl+fnpA3EIFHCGZ05Od4Hnz3mGMj1NiR0uv/mq9Cv9upVNZJJhsakRze+WFjxou/e8yxD0iiQeX6/8r6wr/MNzx5AsL7/5PGDZ8u0wnovkvkXeHx7LLWI1Eg/u5xdxeIRZwqT0WB8K0xNse4nHkmgESDRuzoRHH86XA8D480xQQQFIg/HY7nQIxQNMdHmmICjLV/5I5OPIdjhOZBF88E4Ddxihymuaz1SHSJjzS9tIQ+ZXkrFOCc+erH8/BIU0wA+vDZUSAcEToZXRz1ngnAVyJiUOD+vsD0GCPKYRS4my8wHV3GsedwcwYUwFp+RnRB3FIvL1OUm34zTABQ4Ia+AE6PZblU7ciuFNqGUYHx/6gv4KgfqwSeff15doQmjutEgTn29WGxSaiCbzuzbdf202M0ZYwJwcT4qMNYxju0bQ3hn2Vf/5F4tRQq8jy/Jbtf5F4TkGVZsV5nRbZeb400U7yuy0hlXf1cporrtiiD67p2PFlux1y5KF/dh/Jiu91sY7koxvZzv8j5cp8J2Gw3m2gucvSLYvffObvTBChhjeZsbD+3Ll7uMwNFAQGN4KLYjuvnfhFxPt8PBa4CmteMNM9VCguuhF/mNyhL1v2R5i33FCf3TsDlTihwIw6gQHW5qRLkRVHckakqbPLL+ZYykOcFEVZVhjDX9aYAq7LuhzpDYVR7jKvHqp/pvpuCqsI6u07AzeyAWsJgKRPF6De2P1WFbSEuUgluJQOFtuajUCAvSP+paLDNxVnJwG2EQAuzSuMY/fLR/ekEZGoCbmUJCr1WHyHA8Bmy0f2pESwqqQAt3wAF1JoeKeWu0KKueV6nqp9uo/2DXh2UBfVWFKjU3x88Nwrk92MrCmACuiyyOR1lup8/th1o3CfVgQImVxCWeYhzXGr3neaq57gY4z3kwyV6FUXqaU1RwGRR5Le4bIVQ57HtQKwTJh0Ik6AoQBVgK2aERZxKt1ue4XYq9YZ6tFu07jAJLwqAi21ezWsMGfb/h/y/A1y3DyoV2loVxo8CUIBNLmaeAOz/D/lqNGNBoeb4PwY8n2DxokCrDFmRiZn9A4Ro+4XVvEXmRQFczm8mIBYFKq0AVaEUYzUjDd3v56fP0+nMuEoDKMBlO8QnxaKAAALM5yTHfyHi83j6lNZdpj4UkG0YMz7CG4kCmIB1pvJzWf/eF+RUeiy59W7wx/H0IZ+pNLy0AkeiQNVRgKK65me2/jTlnLY/n36dT1ynY/+HmQEo0KY56mb74gSzp5zR9ufj8fPEdEqe98Mh8OFeogwBFKhaBUDdbF+c6N8Ndlr5o/yHf5yOOqWC3wusM1AAS6IwClTnJs23SgHmUQJ8PxQRpShTK3+6Wn/5TKVkPBKSwTinF2+4BwVyZQTFWaVy61DnZxABXHbvpviiHJkAZf2POiXjkfBqxjgNtmMeFMhEdwKqrFaAJi95FhSAcJKUTICy/keVuifACPMgwXoe9NAWX7QK0HOQ57hrDOG0pNzgX8r6OwPkIPhUGRBs50WBQnSWwUIqgMzPJQP4fqghpCgz8zulR2X9G8FGvSqbH/6iyqDrvCjQnYAq0+UOz+cDQEibMvmwbvn+fn5//zi805giy9VNxCupZ1AwDwps8wp+gCi2hWjy4Bm+OGFYf5SpgB8On4fD+X+H2WKLtptcSPTXCmDdIfpSFIDAQrBRXt2atutMQAGyNSxAl79wAmCpCRrAgb0hba7LnkohQNEoxIXwl/oCuE9A0eD2E4BlDxTAwnPtBDFmpqtb03otJ4AoAOUZfAFYf6SrGxJQwKUAMxyaEetPfIFbEVBAI0C+zYXKO/gLUQDWH+X7oYBWAHJGMIMlcPkAGh3uggJOBai+XhFg9a07QvdCgSrPN9oxBtvl4ct8AaAByndCAYUAPQWoxHkGRMA9gM7llxLpXVDAqgAV8hae6X7CfVBAK0AXAUSFvJUXcFcAKFBk6x4CIO/iL70rsJYp4v91GQFx9rsCTRtLH5nirkAIBVRMTgHhh0Ps5i++K1D062Q8EOoQF4Tn6Icx0Eazjg51nhGokGMshwSEf7YVwT3ignBoxlkPBZQCZLAAs04ABPm2XGK5VfZRYFszwiQG8mj9b4QbAq7TDRXg4vq8fdYKOSJF0V6PSxRL/fu4Wm5Rf2Obb4AA4NmMoD/mX+fRRrMZAm0wqeu1y+VvKuFXitBDASBADE9BgHswCZ6bSMXqYWiWJdclXz0AwQHXwo/8NMf7XGWrhyGOHaiym5/y97+IB1IB1u44cY78pO3Xy1k8hgpgG7YRfuQnbL+qSNLVw1DZuXyP/PjtVxVH9Ego0J4/MI78r5EqoDdLHh4Fjmyk8Csu1quHoc7/VgNFsKFAXvgPwbq+QL5dPQx1LT/jyNvcdGHf+dN14HyzegwCCjQHcMjTlhkOxgIssgdSgXYJBEVwBM5Vg53iqnggFYAjjAs69kM4kefYCveyKB5DBXAY16IA8rSlvCk2cAIeDQUQko38ilJVbYYpQfVQKMAh/MhbD+EuQ5VAPBQKsDYWyVgOWScASrAsFEDgnPejXLgsQ4X+cVEAjjAzlkP2pa5UAuvf/IFRABGprJt3TEC2tiqBiECBx90R6t4Y84ZKVA/qC4R2hJSji8C5LlcPjgLUKXZMwEXYY4Wqx0OB+A/v6ltjuDQDFgQFFkjN39oaMSoeDgUm7PhcQxKEdwLWqyVS5+6wvjgDrggKLI4MK69DJlBHUGBxZH5CZZ1lFVUCoMACqbfU2RZm4Fy1eBQw/975dtPzC8TSUYDs+vRukImFo0DfAaaxQ9WyUYB8Skd9+lB0jd+iUcCy9ZnJIDohMAGLRgG6CySaL74JKMGCUcD2PSlRIIiuKwP5ElHAuv9fNZ8nr4SxHMq2C1QB6xmAqAoVrVlV3fpiszwVcG2BV81HaSvRrdssRgXCEyD0h3AF6rL18lTAfRBWVSqcGbtEoi6vFkbecyDRfM1ZwE9YhAqEJwBwqC6piKa8PBSA1ZepfUmEXaJqcShgGD0hnEuiTS6qhaEAJiAwCXpJVIgFooB9CVSZdZW+pFJVS0MBt/UX5pKouZAiFoYCDutP7w8LfNt/USjgjweqKlGRJdECUMCvANQoYkmk0GABKhAxAdRBXgIKhBXA6yCvFkDYCYxhoRzkVaJEiRIlSpQoUaJEiRIlSpQoUaJEiRIlSpQoUaJEiRIlSpQoUaJEiRIlSpQoUaJEiRIlSpQoUaJEiRIlSpQo0X+M/gVoPrkgKWtroAAAAABJRU5ErkJggg==","enderman":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAABGdBTUEAALGPC/xhBQAAAY5JREFUaN7lWNESgzAI8yv8/z/tXjZPHSShYitb73rXedo1AQJ0WchY17WhudQZ7TS18Qb5AXtY/yUBO8tXIaCRqRNwXlcgwDJgmAALfBUP8AjYEdHnAZUIAGdvPy+CnobJIVw9DVIPEABawuEyyvYx1sMIMP8fAbUO7ukBImZmCCEP2AhglnRip8vio7MIxYEsaVkdeYNjYfbN/BBA1twP9AxpB0qlMwj48gBP5Ji1rXc8nfBImk6A5+KqShNwdTwgKy0xYRzdS4yoY651W8EDRwGVJEDVITGtjiEAaEBq3o4SwGqRVAKsdVYIsAzDCACV6VwCFMBCpqLvgudzQ6CnjL5afmeX4pdE0LIQuYCBzZbQfT4rC6COUQGn9B3MQ28pSIxDSDdNrKdQSZJ7lDurMeZm6iEjKVENh8cQgBowBFK5gEHhsO3xFA/oKXp6vg8RoHaD2QRkiaDnAYcZAcB+E6GTRVAhQCVJyVImKOUiBLW3KL4jzU2POHp64RIQ/ADO6D6Ry1gl9tlN1Xm+AK8s2jHadDijAAAAAElFTkSuQmCC","endermite":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAADR0lEQVRo3u2Y61ITQRCF5wGsEhESCrkloiEhITctQG4xiQQRuRuIKPrH93+Etr8OHQOloPyQbJKu6prNzu5mz+nuMz0bwpWtL+7LVqkl69lzWU5tS3IiJ9NjL+TZ0+cyF89I6LJ66di8+qoZ5mLpkIhnuqfD6KO4+e/stvse1OqFc9EXk1rpSCAD4G1Pysx46hoBq4Vv5mvZZpifzBuYvyXgtvseDHi9qF44k7N4yXwzeyicf1dsaTY0ZDW9K5WlExvVw1buOOg1YS2zF/TeUM03yaDwdun0Gvju47vuezDTP5dq/pMBfZP+YOPyy4bsvv5u55mnJJhTANKzQO5rRBaQ5fmqFBKbUkxWZGXhvUW/VmgaaH5TFmTGXFfd3kzz21K/501T3qKoGWCCSCbUVBNUtKwcOKeiJRNPZiU+MmOORiS6BDKSBADcvHjeIcEBVwqnspE9MgIgBLCTowkjITYyLVNj8zIbW2D+j7Xf80bd4lrvDv6fMyCSte/WKH0xEfMRcXPA1Hwlf3JnBiCKCOLG4kHnOZExIs8L+0gpAN6WR40+vQHgOefNEeBjj6esR/AMWEvvGQn+nChlgC53TfGRqOOoP/2BAlMCdkTB6crwWbZLF7Y6+HWsIlzLvGZA5zlRygB7YR8BbYAU+Eb2wIBxjHMe8FzD0klvwDzHdo0e+3MiQwAR5YV99PpfvVr7iaodZz7K/soP6w+cAL8G4LaXUIL8OZEigKj62O78diy9yQCAA5RmiPTmOgjgHPNEnBaZ+5wAnhMlAtidtZc7dYRONynmCjIoAUEjHzTyJpAInp3XJRPRY2QViKwRsc3cUafOKQGEDWdOI29LG0RBAOA18qb27AsgJNIEmKJfEcCIOwnUsjY7oa+NKLt6m+Jn2moPAZBDc0Oa9zMBmtotaZS/At7qmpRWAsgOAx+pxuY+GWDKXbwwEm7u7yHCxY7a7zv7BbxljupbVuimiBECEDxteoyIvrOd8qUBZROk67+pPEABTjZo+9sBjx70tfnX3tRUOSQnsmHgbCV/aV9t+Vw9M54aPAL4Xk8GaDfYG5+s/1MjZILHqE2Q7ev53Ra/Zuj7PgDxAyhC6B80BqoPGNrQhja0Qbaf/P4PM3XaxMkAAAAASUVORK5CYII=","fox":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAgBAMAAACm+uYvAAAAJ1BMVEWAgIAAAAD59PTn2dPVtp/nj0G0j4PifCHMaSCwUSKOPyRWPzQGBA4kh8LGAAAAAnRSTlMAAHaTzTgAAAGUSURBVHjahdAxb9NAFAfw8wRIDLkFEsSCG4kMHVAzwAiV5W9gXUdA1omugPWOEdpwzxthaP2qLhnrD9ClHaKuvQ/Vv2OnrZuhf1myrJ/+vvdO6YcZ8p/4jdZqoKLB87AchKCQqA9X2+F1HJa3sNuCUv/3ZfRV/q3hMP4E0A8DSABN46knV1prUYm4FJGK/d4KyJgyTfP74FZQZOZvkiSfG2AA86xYwyHg4x0U9AgUtAlDZiYibvN7Mknyb5PJeKxGVQelIPM4RjVG1Ahl7CF41XU9j8e3IG77bfb9yssJYKHXAWCf5kctoIwnArySra3pdLqzI3UD7RwK0f0cTD/cAS7rwL5TK0D5HjzJfmK7TYhedoBPDCsiHfgf4b0LQWvnOE9S9n5WuAaSXazVgO/AFRn1piIuARUTGQNQEtTg2ekyAghAmAz1wXXgjSGAVFwPWU4WOPyXTeyx95R1cAaQhSZyNrWeiIoWShkB5gDObcpEM9qEfAXG9MC50ua2whlmD3ARzu2Ly+vzL5pZjuy+MPvC3QC9wfXEFSoo+QAAAABJRU5ErkJggg==","frog":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAQlBMVEUAAAD9trb/ioq+rWqxnl+mkE9/oFjlXl5zlktjkC6NdEVWgCLMPj5NgDBGaypJax46Whk3VBkxRhRGKiktJi0uHRCwcSzMAAAAAXRSTlMAQObYZgAAAadJREFUeNrFleFShDAMhBEoZ1x1Qz3f/1VtQm9qgVoZf7h0pqWXr2k2M8eQJMBwSSJSvStpow0sUmdgCHMIbEQTIgCo5CMkzNM0zaEBgPf7x0SNUfUBhHmeQwOoiq4zTPOUhk32BF+VomvAlDAPT1jY+LRxXrQlqA638G3lHkKQxFL0i501vxzus2WIyunjfidUYwb8uClU98mr7CyRFGwrh4ecUwABj61Iwlt15zcwSeHjAGgS379fILxTk+I2mt0gCRmyXpNs5qeJZ4BQKQW43ToABITgEgAU4DaOt58Bwp/fAxChlAzj0zj2gOcKGK8Brh6wyBmw6bv9SCMpAybfy408ZPB4UlUpAp/pBAEbBTiqZO7V4Nm1NESU/BFYdsDSBaQCpAvIDpAWUOwnSBDwSSTt2foUICT7SqVPjx3qaeOUsHBTjOoiAdrr0BCEGK5Ilg7wtwwgrEj3ZKXscnM9AFRCBO5JZE0IGc/+xbIpCV53v8eVjQ9K3qdgV1zro8hrtuIKkC3Cr53dPFIqcxG9EjaPTIPb1DGpVmlEaUNHa924dfgHfQFwmyc0LyT5NwAAAABJRU5ErkJggg==","ghast":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgBAMAAABQs2O3AAAAIVBMVEUAAAD////5+fnw8PDl5OTk4uLd2trYzMydmZlzc3NLSkr5ldknAAAAAXRSTlMAQObYZgAAAYdJREFUeNp9ksGN2zAQRdmDO5iBfN95JlWAgeRuAkLOZuRNzgrcQYLdBoJVOoirjCmRYDYy8i//wIc/n+Q4J7gs6YgdJxFRBBNXVQENRE8yZZG187sKkEIGLi/z/HL5D/Blvt3mH+8Ay044ZgBeb3e90QDVCkz94QFgpbGugOmvDPy2vxNYgMPnDFCAloCoLgDXqf9wfJBgKwAfU/+M8S9AGQH9MXzDSskGYJQRgcMEurkmDSCxfSiABtCeegsssuJbQETFTKS6q6r/v/FOl/1wXSrRtQOESDh1F6aeZG12mKBvC6N9Bq6nBtA8xApM5vCJRxpXAAdgEfg6f/80/wS8wggrMCwjbCl33T09Zw+6dqolB5/Yw45p9xRAORMiaB2hQLTRlLSWHNTXBHICwD6Jv0Qg3E3CGTDWLffOJ7B9NFkTRP1ZYbTRkyCaUxiS6TCyaBhNwwgoOQGcMsp+RBRe38CE4axrhxhKAlYqALmEEqIpRA9D+SwTikQpt1n93R5IcTOKi/wBKsTOnkkgxrkAAAAASUVORK5CYII=","goat":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAMFBMVEUAAAD////6+vrw7+zq597b1cLArJCenp6ciHaDg4OFcmF0c3NXV1dNTU1APj40MzImjJ+xAAAAAXRSTlMAQObYZgAAA5JJREFUeNqNk71u7EQUx0eb9YqKZ4jOeFPchjAze4s0rObDQrcy9ngU3QKixDNaueAFaChIFFHQR4IGci2078ADoER5g0g8ANqKHs7YeONsyIqf5HP88fP5j3dt8r+ZAAOokPxVQaDgvc/J9Q8bgsQ24oCnlFehfVWYUMrAuT0CMAZVE8JpCFer8O1HfRtFCKVUFdbeVRVuZ7O+jSfQOGEdqkrHKwm2XQHAhRYFaYzOk76Nn0Kg0GCEBW2O8lnfxk8hMCK0rjJSSrNM+razBtc0vtAppOnhpG9PQlJFmjbYAiTY5bRvO4IL+BS1qWqXJ33bFVzjv7z//f5h88fHfXsREXxOxuwKIbhd4YDSyiDaDpQUAXb4TNAaBdxiQQGQ4+H5U7CZ1kbXeL0rRQqAznZCFDDC1tZWOpbzFCPE0wQKtdHZ5/H277pSUEjnczYIBxSsMVGwUbBRoGNhmrLaZeZdVdf2g4mlECk9eiuXg7Dg/tQZ8+Gytu+rWC4WgtIL9SRIu3KZD5d1cFkshRGpkmqISLLitlmFq6v2arXKYvk5M9qWehBmzrbfN6v21za2WG5xSbW3+fN3nkrgez9Myrgkw9DQ/c3h681WONCCA+wRJlQoyvcJAJwPE3zmry79zIbTlcuHCEbVdpE2tb62iTSZM8ttBJdCkZ4UZG3lRKZHRh+OItgQYebSGj0t7Fzb5RAhmIRBcM464xPvTf20BsqVGtbw5/2P95vN8NoPEamiQP4TCsAAIMUe97oXmo/d7rQCzijITq2QYiSkwAAFilWim9LT6+vrchwRBd7BUKAvBQZM/itIBhyi8CyCoUGV4lwA7okXE1IZ71NKCRFn6fnuBCrRUFpbISSTUnUTKoA363Uv8KKUkluFApOqKBcVMhbUpCykKpVdCCnL4sAQJKF8K9hJXZbleSEW87K059OcRIQ+GYR8mkxre14sjua2TqakF7R+txVwYELK6fz4GDseJwhRNApbEjIlx8tlMhwlREIU9sDYyX4B2Jsd4eGGjPnk79eF7v1N/+oaQnpuvrm5ITPnvD9be8Uov+DATXMLKem5++nujiSZ83W+/oqDMJVm9LP3t1yMJySLrC5z/wUHjjD6aeX5gvQ8/vb4SMjbk18u4mdhBLLQzjmddZ9dTvBTuCekyZOWtHi+MsY4pLWOEGqEJJuHzYaQszyZkbZFpaNt16EhhLuFIhuEoDF78ZP9A0Obf2KBegjhAAAAAElFTkSuQmCC","guardian":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAhFBMVEX///+rjEZQjnQtWUcAAADX1c/Y1cvX1czX1MzX08vW083X08rW08vX0snW0snV0crAuq7Auq3Auaq+ua3dkET/gi+DpZy5k2Syk0/wdyjecipqlYPsZCqFhmBmiYDLZSNdf3XeSSh4dVCmXCxOeWbUQSDKPBxBcVpkW1BgVko5ZVJ5CxKoxgN0AAAABXRSTlMAAAAAAMJrBrEAAAOASURBVHjapZaJcttIDAW5lvfIHtnLYiLDee4kzwsL+///F45I66AkW467akSoIHRhhjOkuq5bLper4fL/wHAhiscBItiMIYSgm+j76GOga1xdXXWD4PNy2epHQ9q1HrAz0+mneCswfeB+K/i8Wi6HsZoEA4vFojtP4E1vO8GmvvWw5QUBEd4TrFabBj53F9LbEfZOcLt8WN2ubi8WTEu7J1g9DPUPt5cLPBM83N3e3g2C6+tu4PqYmaCPxk5w99/9XRsvCaLR9+G0Y8ID3f3916/DuO8azwmMgTA4WuQWp7uvEy8KYFObOQpsR4K7S4kJ7Izod1MYaSvwetoivk2wWAyCGR4PUNmZbawHnOWLBTQeqxgZj3PBWQEHbhPRD2TfrlNuOqCmPQs2I6M/I0hCAleAgtwKNrmeMhIux1lBLwklknpy1l0rbfXinMD0SEDTxDwXLlVt2tubwrrbY02fNZFxlEMoKpB2gsfDHz2G/fev735+/6/NcU6yC0lxVtDbv7z78aff39s6EiRylRFxdgoB//z25x/v/kKa5ygD0T778/tAYMCS5rmKsoTKz9wFYRc4kee3OBU0byJtBZr9SFkViAqsnG8yQGpDz0whWqmi0DynPU4eJmy6xUDLffz06UueP+xnBMkueXPz4UvW9ws+thZEveaBkmV2325ubj59gXqFgPV6r4OhhQ9DD4cCIZ0XPK7Xj/MO8lDgFwWzNcjZDBC6cArddBcOkcyld6Htgx+6GXq+g2z6Z8HivEDPCSyxGchInN3K50A22Iim8ZNAESnFgKTjg5URaEyiAZA4mEK4CtPcCs97rngqEukcW5gJsgzGZTEThBKaYaynEjutQ0FRxs4T5z8kmbGF5nJVumYCtdo28NFDVEjpafZ22lQWsw5Mtmo4fg8IcE6fzjKm8EwALrBPvQckV1S5QrgYMDYHAoHBzjqeQogKRVRIY49ZTjQT2K288NEiQiE1C5AeV6o83wdNXRiYC4KUhFNIZELl0SLKgKn2oflG0saaaMCYsdOZYCQlceo9gJTQrttvB4JdOAVItCcTgl0sYIqRnuKUOBJk2i1pp/fi9BS3a/va4nK6uxqReqHFq7naCix5X3B9kiNB9yToUzJvEVi8SSBDvlXwpjVAAl19h6BX3wNm2ng4/SqB3MCVU5T1OgGokfb0/LVpczpVfuqPyjdt8tQvCjuwWwAAAABJRU5ErkJggg==","horse":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAh1BMVEUAAAD///+lpaWGhoareS2nbh92dnZxcXGPaR1jY2OKVS6AXCJyVBp2SShRUVFvQh9hOh1FPC9ZMxc2NDdOLBM2MCZTJQ5OIgsrKi1JHwkqJR0/Gwg6GAUhHiEfHBcuEwQpDwMjEQcmDQEiDgMbDQUcCAAZBgAPBwMNBgMQAgAIAAABAQEAAAAaegGbAAAAAXRSTlMAQObYZgAABHpJREFUeNqllouaojgQhTOzuurYitKsNCS7pR1lF2fe//n21CUBvMznN3MISYVQP5VKEN0LKitT6Z6rKovNZlMUm6KsnCtgVlVRpbFduYMw8qIKq0zlDkAgd+WrgNKqDNiIilcBmwXXi6rAPARQKOD1CCqroNVWAd++IYJfBVQ7+IOwq55M+PmF1WKLUlRlWRRlWRUPAbufXNiutihlIRE8m8KiwjLxQqHCWVaLtJgFlr6sxqtfoYstURQvRQBgwcAxgB/GusvY8wvVo8HX9Tog+IASRTfbetp9Dqjr3wL4+nCofYxEU8BisX3anQLgf/CRoOjqEELycCuzcvex6voARKQQKGImaPJWUmPoPpb41xQgIplJ3jvuJQXOQTAAKcCErTdspPtljMT5R+w1/CPbZB03SPfv47cRLl9+IHYR1T++AKEdDO7/Ev2TWqci6G8U7XiJAM6ASASeuKOA5Xw2my/b1I4AHyjaCd5z1BFCg04g7WCwnWoCiNEAsnJZ3LPufgh9ZCbA+8fH+yQRCB1Rj7Wftfv5DKHP25Hpa68FHnzYLmDdAdoW5etXhD4yzZGLVAZgZFAAP2e5tAiW6z//WC/bkalugpDWpnA6Se7uALP523qmUzDT3P10CieIRi/yev325vZwBA2lHZleUiA1ceN42U7fz+fvJ1kPKAN0/RFPOzLFjbimyC0DTud/z+f/zifyQnCm9qE83EicI/lIEQA6nUUAgKAAn0VpfxiY/YiiFhwOPichIAuyMl4BbB/QC1GVART1iAQBENAogOS9NACccaCPYQtBAeFGznSAyBLQNPIDpYBAcQLYrlar7Ttqad+3E0CMY4AiAKABcDwenQn29YrmBgAnTgNcFZEB4RlA36JHABQFxEcR9N3n8fjZ9RNAfpsBgIQShEB3EbA/E6QT4x0gzyFQAlxVvZYLnAG5TACQ8Q2AIhEIub/KgdKhvly6z8/ucuFbWYEoeC/mQWQNACFCaDq4dz28u65Hm3IgN3ueqnxf1c8A0wjgBoB4d5DlIEWARFoEYwQ6CMAAcNPH94zIOZBlgz8RI2ALoc4QW0YMX3sGcLniHHIAwS2IP9zNURGyjBkAP10CnNMcjJJoADFqi0AUCF62kHzc5MBLEjUJFoFUDEhz0EejdGxMc+DtwyxmbRGg4CAK9tGSh+dqkgOLIJg3BAMCwFNMe/k60ZCDNIc8AwPAm68TO0O3gCEH+isfIEFoIoM0zKXhbcTbi9OanIMUAE2SCJjNLAPIwdFOFOSg7zkHtvJp8sZLyyjmeEwb7/PX0Y23b9PoLmzYsMVoGrXQGgMyf4/Dwc67rpGmwc2ocBm12kIffic9hEpax3Zjg3Kr5L6BakHZ7jSKAOEScGraXEAl4UkcHDp8feOZYIZctBswPV0ye/0AgDSGPMXGs4TTsKFJys8PqvRXLAihEQQqGPII8UTRAGVA86H+pAcMRyR7WEOGPGTQZECNDcsgsYJVLnLrs9hBx1hqycUkHrUvqxgONt9iUv+p7ocjlD66/wNdcSNfhkmkngAAAABJRU5ErkJggg==","llama":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAABACAMAAADlCI9NAAAAVFBMVEUAAAD868b14rjx3K/v2anr057jypXMqX7EonmlpaW6mnOvkW2liWaGhoareS2nbh92dnaiayOPaR2AXCJyVBpjUT1FPC9BOy82MCYzKSMqJR0fHBcP+iOkAAAAAXRSTlMAQObYZgAAA3lJREFUeNrtV0t22zAMJDAg+0ulxm7URr3/PYsPmUiMXja0vPJYJMbyAiOAGjynN7ACCmVkPO0AButlC1ueRnEkgBydAHBA45afIICJ8bEG/htvdhHRcI4APIGU7wVAauUB4wJiUX6OgAIymjaACLNLEBjPxJRZTjkDh4Ag5EHEuCUvym8pgOgTAZFWdw3Gc4EU3LQCpPhEQJbs2TXGpxTbbyygKNIhcom8UpRVbiwNQxS0AytEuvaqMs+YlWz4jQQwMdEL8wuRUgaaAIFyhkjN27ILwgvcjIhsx5AAZiL+8kWDUiiqAHddAL0AAM2TiWvVBgXw9x9PTz++814Ac61H2SFMUAFiplq2dAaYTctRC/RCawCFbQ1B4oFaopwC4OZ9uwJ49+HpowWg0QqAHCgVKdCsf18CEYn7cWbIdx61QQf3AhQQRc67FrgEA9roHK0AVXQCwIAAbj8NSt2R2xvC8R6eI6COYOwFZEEMZmlngGm0BYi3Gf0hjFZDcu58KH7xJhA5GRmCBLCjFpRqTGK3cHgEAGZhrkYA8Fjx+etXIl1MtaDOkgCsS7JsBcRcZF+ul2ngDEROejUBrxTgGuMQKnojMgAMRdWMsRYwRSt1cSy/m9gSSVbsBCjEhHH1oiEfgCVn9h3vb3bMl3oCvABdE9pAYpACAxUAwTP7jhbIQvJBAM3fIfttqS4wZkQIv4+AoI0nWP2lHEDCDaoCDAgQ1ONskQXuL4ibSRD171sQXYCA6ykEBgRYqpAB1tWiHD/U8tfxr8U0imimeNoMiImp3+VtIG3cebnM0zRf1hbTKCQjQ0R0N6Iri4SoQwFrhzSKLJLjgywwgqzL4wcBy3vplf65SRcsP8xbxFjRLS7lBwKmdZknLf28bmgaQv2H4SEr8Ri8pN6ZlnXV69u31eNyky54Ul1xOQ1eckm9M9ljX66/fl4vq9Hn5+d5uAKezD7FVv1W99Q7k2Wd5t/XaV6DXqZpVEDpbCZvQuqdabHE02xZGx0X8AlS70xLvP/zrPGdpvPQO9N6hFMF9M50bxw4033RO9PRMDoVvTMdDaNT0TvT0TA6Fb0zbSbQcpcu9M7kE8hH0LqhZwrosJlAm7mU7od47FmH0Bpz6eev6+XuAqbpMmt0ev2t9J4CZheg17qh6X6ICTSHDzR6TwHrIdIDDzzwwAMPjOA/NfpDB1xAY2EAAAAASUVORK5CYII=","minecart":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAMAAACVQ462AAAAM1BMVEUAAADBxMW2ubqvsrOprq+epKaUl5iMkJKFiYt1g4ludXlsdHhham5SVVZNUVI/QkMaGx04QmSDAAAAAXRSTlMAQObYZgAAATFJREFUeNrtk9FuwyAMRVMa7jWzwf3/rx3QLmsiOlpV2lOPk+BYypFRzLIs53A6hXA617sSzsurEABTSkRPsLxGAIEYI0ACiCAC4tqp5fbARGApJavRsLoggLHGyrWvmAoOIPSWtiamgmw7MgIhcQuAM0G+U+TcBERcY+wNgJwJPO9wBAq3Blo+E+wM7ghSIRqkVGaCPV3w9RsTQUVAMRMC+MlkUHusISVfLlkoW8ZRbRmTRFXNTG9YZ1wdCoTqXsy8dNysuJtQ7Z6JoBTV0rlmrk3gB0zHW2gCU7XS6ZlrugrKxp+Ccuyg3ATljscC0eLtM/d+aX+tVfOy46GAQwGfFsiwA5HnBRwK+O4W/k/w/m+U4SDJ84MkMhxlmY7y8TCVGy8fpsR+WnXDGuPq8mHIN8fuL8tkDiT4AAAAAElFTkSuQmCC","parrot":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAP1BMVEUAAAD/////7nn/6j3/5TPGxsa7u7vZra23t7ewsLCoqKienp6ZmZmWlpaRkZGQkJD3cwmGhoYlJSUPDw8AAAD7udxiAAAAAXRSTlMAQObYZgAAANJJREFUeNrN0tEKwjAMBdDGxpnbZqtx/v+32jYMN9QKPnkhYdBDk0EDHSIQ0CGBDkEV/pFgahtgBhxEZu7nSGj1Ajg6SAmpVQe4M98hgHCMVUAgqklbObgyXwGhXczUWlXg10Z+bj/4i3X19gra1KJrDfVGakcARdFyqyEHS353w35J2oGiBRO8++miqrU2MJdZIN6pJy+Lz3Cgs04yeaees6qeByMypZQo548jLDdl9mnE8D18B+EyBiG4+AGMduBwCi2/g+BiAAJ/BTwEW/4XPABsMA1LwNN2rAAAAABJRU5ErkJggg==","piglin":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAWlBMVEUAAAD/gID/////7aj/5bX12iryuobMzIPCwnTooHTVlXreihzJgmWHh1e6ZkWtVTKiWTplXEOhPjiESzOXQyKNNQ1rOSR6JiF2Kw08OS5TKxpgIwo6GgwhHhyEOBusAAAAAnRSTlMABHH+CSEAAAKgSURBVHja7ZZxU9swDMXDWAMSRspwYiDQ7/81pyd5Vzzf3Oz4t78GRU2kF9m5Hm8y7h8sMM34zMQrcKZj3BvTRHMVoHVVJwRm41n1eSDwYFSB2QW0AoG4psY0JgRAK5BeiV7TWCD2wLt7AXkhehE1Du1BL6CPKT0quL4HgMgFxIBAy6xt3hICsYniHBRIzET+6Ij1jxg3/NUyXxEwiK0wiITZBJC3And3d10uzCzKjOkNOzGrX6wKJoUVpiTJQ5MbVseyqpX9SEQWmHUV+4obhI8LQCd5aHJDBALCRLIaQuRfWSRhBK51M7bKQ5MbqwpLjAyB5Kmw6AoFToZAgNCE0OQQMIXEGHkFtU3UUsUU9XX6DnlocgNNql4HvEuRAzWO/Kx342xMFXbibWwb81TJOS+oOyQg9reK8mafqbLkJR8UEFEW3nRjzDBV8nJwAgGs1qoY5DKB0QrEukrJZc9BcXz7S6AiolspC8AMIJeLQME9xC9JJppL3FmKen/J1moPsOhFzfi4gXNZvMuKdiL0xmPLVjBaHTBiK1DQH8lealfyB1mIsbEqIABcBETQKjiDYsRi84I0WwYp6yeilFgSp/iFBgJVSMchBeTsR82RVgFiCIAJgMRkihBn/w0XI9ZS+/GxtK1LXyaQVnkHn3acLDhlh6TIkQms6s05/Tw9vQUfJzs+3v45Aa4mXCTCCnh3nj73T0wQOY6oQyT080WAqcKO70H2F9tsQq3zpzdLOJ/P7+/nX+84I8SqvT//2QKEv+umGzdu3ABH/cLAFxwVGPuC4xMs353AGAmM/ELvC8b0fqHzBUN6v9D7gjG9X+h8wZjeL3S+YEzvFzpfMKb3C91/5TG9XxD5zgTy3xP0fqHzBWN6v9D5gjG9X7jmC34DNs5yRa6KJBYAAAAASUVORK5CYII=","pillager":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAflBMVEWnra0CAgIAAAD////Hzs6Trq2VoKCVnJyVm5uTmZmQlpaOk5OLkZGKkJCIjo6EiYlmh4VzeXlwdXVobm5nbm5SWFhQVlYyZjx2QypfNiEmR0daLzg5Ojk5OTlPKxlLJi0fNjYqKikeKio+Hg8lJSU2GB4yFwkeHRwjEBQUFBN24XvqAAAAA3RSTlMAAAD6dsTeAAACP0lEQVR42u2VYZOaMBCG1TOCDUYQemeqRDEq8v//YN9laQYckaAzbT/ckyPEmXsfdhlnnUwbwlBKGS7CBjmZfn6m6WQ6G2DiBHUegrTmFUHIpGeQSorT9fcEixANvCt4rwVm4V5ig7/AvQRJ+AuQQPOSgjjwzYHPw4Im267AnaW3wIXbdxINC9zDZUPbADwEHMnztZTrPOc467wEa+SJc54vl3l+bgqADPgJJHf78SXl1wd/oDgb1sMCeg4yifqxXC5Vwk93DAsSMoBE/QQqkZxPEvrDNSxIWJGyIOW4Y1gwG+BfCS4Nt4YXBJso2ngLTsBe7bUlQB6GMQLkrYfAvwJq4Z0KYEDeR3C6QzOGBEb/wQCtRwjMZbVabQ7G5Qk/weFCa3fYIb874Nxa/YLj0QkuN1r49/0eEZxbq1dw/AWDp8C/haLwbMGa7XarjTa2UwGWXwXNW7a6Zqu7EQ8BPfoKDDSEfcJDAYWtIQlTMBVvXR4KIDaoHApotLmWTFWUZVGVXXoEhquHgS4nAF6C2Qi+Bd8CYi60ibIoe1mAvAmy4C2BzWB4XSCszdDDaAGHxVzE2jCxGC2IYyHimAcOBPFoAWY34MFJuxMU+5pyPhei5PPDCcRD9niiHaP34AQlJg+oRFGIqgA9A6Q9XGlrC0oSxKCi/FiBfcJdC27R5gQ49662gN8VV0ibE7jCUOWzH5KiNrgOuwJXEZ7aJ7jd4QT3pfa1oJTKAqWCDPcsU9noL5IKAhUFUaRqAvX/TKTfnHu1gRak8lQAAAAASUVORK5CYII=","rabbit":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgBAMAAABQs2O3AAAAMFBMVEUAAAD02ezy2Ovx1unxzue0pZSkjXOUgWKUfWKLdVp7aUpqWUFjVENhU0NZTDtSSDkNYhzDAAAAAXRSTlMAQObYZgAAAYBJREFUeNp10L1uE0EUxfERBdhQ0PAivAFPgUJHFT5ewC7TBJ8LBW4i3TM0VEgz09B6ptiOV+ARkjJt5GJyNbubtZLdv7zro52fJWtdq5aqENe3prV37vl2v959dK2SCsERQBrYwtoNwOdAP4AVrAsDZ7iUAWSG6B/AZtsAvsgl6VqBjGkEL84/NcAPvBoBlSnFfmL37z/Exxi/xj8x9kBVUsptBsX1DRjtCIAbUkjqSptZYflSyikA0HVdm8dM0pdaK87x7WwC9qTNZ+/ckH7G1Xv7RVCIj+3emXrzdgT0geJczaH9JcKnarlHGSDhoynOg2PJQYUqPpVuDtRjUVGIip3PAedrf7kVLtz0NYXarkUAEBBgGQgVBBeBMlKC+kUQfLQPEyGK3WYLi3RT2adDyr7kYAAbWD6dgpJKLnYr2UBfKqegDh0jx2JH96TVz9tpzLX+MYJf8+Bl+juNuV7FEeQFcPg9jbleH/YDuNvPA3s+jcd9L+1FqEiqVlZMZ/eZmCL8uK8JdAAAAABJRU5ErkJggg==","sheep":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAMAAACVQ462AAAAM1BMVEX////w+fz49vUAAAD////49vXs7Oze3t7S0tL/uLjmlJTAnoa3lHuviGtXRjoICAgAAABdC2QvAAAABHRSTlMAAAAAs5NmmgAAAhFJREFUeNqdkgFy2zAMBOMqONC+hdv/v7YRqCSMPO0kvpFEUhKWwBEv29Rl5MgcUo7U5dDLf7Qdep9cFNJINeEpQCoyx8id8BxgSJHaAXoOIMWRQ16+oUfAyNSMH08BRg5lu/jDDC6HUhEjMzWGfgQYmRE5QhkairGv1PVo/GoFNrKjsLG3bX/7AZA0rjcL25R8u3YpzZgAsANbwvEI2H/lhn6/2q9/xI02oouZAFsIBTZRnAHz90psqKzUrrYz3wEdCzZlcwJ0sVIm12v1Yq73awKEsNMCYThnoOP863ar2Qf79mN/vpvIYWBgsk6Af+tyQaCQcTFnDso29vcAKDG2yxZ0KCaDFTBad7hvb/MVICTseZloK3ohFg+0jcwGDOmUQSJCJjFB0DxhFsCmdq4z0DkDu2MKBcZWBUQQWkvYs8j7m8bYthWAbRVk2SSeHRGmlhKUYx567piHDKiykacHiTAWZmnlJnS8RmoFkBK2FdgUvYjp62Jib69j+ArQh22JwGkC265PQG/erT8fK+AwHkOY6JEG+hPgclFlCoxZASZQKKd5CMsWRCwmGibDprBXgA0gCQKZBgWBlwzALkw15wxIEhMVMtjOjo71FIr5CSj8FYCxQVHYCQSAzdrKmIIy9aaTB02WfZwfIthhImLxwEf9xT48tDImpveRBFAErCUUZeZNA76lD8B9VdX9p4C/n54rtHuYBiAAAAAASUVORK5CYII=","shulker":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAzFBMVEWOYI4AAAD////m8K3h6KjT1ZrHw426tnmzr3GsqGujdqOjdKOjcqOec56ecp6ecZ6ecJ6ebp6ebZ6Xa5eXapeXaZeXZ5eXZpeXZZePZ4+PZo+PZY+PZI+OZI6PY4+OY46OYY6OYI6MYYyMYIyMXoyJYImHYYeJX4mHYIeHX4eHXoeHXIeHW4d/XH+BWoF/W3+BWYF/Wn9/WX9/WH9/V39/Vn93V3d3VXd3VHd3Und3UHdwUHBwT3BwTnBuT25wTHBuTG5uS25uSm4AAADM1DMFAAAAAnRSTlMAAHaTzTgAAASDSURBVHja5ZN/e5s2EMdpk2Xrthg8dKfYuMVet4U7sTr86CRskuL2/b+nnUiN/Xh7nhT2574guNOdPjoJEby6ULFYlEWSbGtp5Sr5ayM6xnr7QsFlR9I4JtswuYbZunITBOc5LwMWlhGZpOUKjK1HAwpCpTWAQq2U4v1oQM3gpQBiRMzay/iLgJUlRAWAKBi2xWjA0rGK1R2AFga5ajSgYI34fAMCP44H0B0opTQqQEQevweJywCw3wJUxpWjAe8aAwBKAfR7UE/YAwC8Q+URCBOWUFKMKlaACCJqzw+SmJtv+Iy50szSSEPWlKMBya5h43ZM1nJmXTEaYIu6bauybevicV9V7Yu/c7EMk+JduCyW0bJIwmybhCtvl8l8IbGsTEKxZ6tiEa2KJJK45C3mSz9uu5xlwdKxJUeWHTnecbtyxpkdu9yxv/ZLlzXkjDXea8w+OeY6FovaYE6NQdMwsDOKXBlJBMUG6t/lTOiaLSuWnMyVoXEEvS9viQeJ7Q8/+AuQWqkIUWkQyYPalTWAIF3gZSSegyiOAaVTKiiNQp+qfBa7emsQlEIEgdyxqwqGGJWSBNCKmqrIUAN6Oorv6iBxJI4IY60cp0njfQ+RHsebd45RpMUFbWm9sKRVjH4IgvhBRE5s6YoRFdsqMo1CD9DyMk01zy0o6NNjJFuGPl8jgC+LXOUrSDdp2jdQ1EoF6dr78lTQ70mart9u0rdpisD7lSXx5ZIbNbfB1sBwOEK/5uzkz8X/kzE9+rfUFIWBwZ/5Cu6dSQeAy9f3jk6+zHYvbQBak0r+egA43gRzY0/E3FUh2VMCuTLkZohHZMsod+sBmLsy+N09nBJ+/dDeu4chIfztw/7efThN8P7h031zlv/+YR9441Kbr/L2oTscum64uxf/xkt1xCLKmTNmw58nALr80PX6fOj4MAXAncmNlxgvL2Fz4XZ5ZwTiJSR+Gg/gjg7S2AjniSdUIOP6S9afT1qCr5sOxhdiDjQNcJDLc7ibAqDOPLEcJpnfs0YDpOp+C5npMKkCX7c/SE9yC2LCUc46zpmIc8PUTQHQ8U96kuckALHpb5GhlwGXv3Mnkqll+v7570d5dhtGURRGx85ZeBvezue3Rz+chdE8nIdDfC7ZMmQAhICgENNhACoFZ/6tUggAp3iM+hgPnhMkrPTwBSONWqtzICAinIAQK1QDwBMV4NkMMwRBxCfAnfZTDBP8on3B6hmwXcoKNUAMAD8nW1Mns9DPqATy07L4Y5tE8xkoP0D9uCqzIpHFgxaiBolnQWIpZkdgHGtyvF5YFtsAO5ZG6dLbO4O0Y2QrccnLXQ7kqI8HBTfsZIQ8M0u23LKTniZrSHrJlaWxfUTiufRVEheW5R1Jy3ZlsEyrtkzL9mNayPvjp9W6eqylr07rtko/7pO0fpT4XmJttSnbZVpLbvlYie/7vh6k6+urq+vrm5s3b25uXo3SEXAl+i+Aq17/V4Df/3Ndf9W3A64uNBbw/T/0Q68RS/B6/eXq6svrZ/u7XiMBNzd+CycDjhs5BfA3y7f3PUOzujsAAAAASUVORK5CYII=","sniffer":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAe1BMVEUAAAD83VH6yTfyuCrpsSvopSHfnzHejyfUjifRgR/GeRxFnYrDchU4jXspiGCqVAilVAEsc02SQQBXUDOTMR1xQQIdXTwcWzpKNE6BIRhbNglENCUTRzM8LEREKxFxExMPNCosIjdgDQ0kHi5QCAgaGSJBCAg8BwYIBwb3shenAAAAAXRSTlMAQObYZgAADgFJREFUeNrUmYGK4zgMhsPOsjdXXz0nanSIBgSmsPP+T3jSL3vTpl2oZ8zA/iO7iexQfYkVJZ3ll2QZkSxDygTl7M0MO96WeSrzZ29C8J0CoUP5TwLop5+JmbHr9qcAxOnPlFNC/L7hni8AUFEzEXx+GIBAkJMbCLBv3dcAaA0K/RQAzj9n4hBRSl8BIB57xUX4BEDP34Tw13Vl38lzllA1aa1iQph3AN0wFBvG1PR0Em83UAPgSORZAOoAimVeRO8BTBuAxMRBgLaCiF0rgodrEoCqtfIQQKV68OgB4NOKqmN7e/ouBIJMfQXZ1iyAfgGKI5QiOwB1QFGn9KFSYiKu2whALwW8sotSItMkAJOWokXsb5ee/14LAEV8IgCMbqAS4w7q8ZNvpGmPEjXWgwOg3QH80wQAAUApVUcAPNi0ZbDjoBbMAnArovIAAOH/bfJP5ERRlQCw9nwSk4shoowVNBcAyVlEdgAe/V+QMSAnYmKkwEgSE0MoAQYAojkAEYloUSToYr10u8UBgCDZawXCSBLHbXSrAXlSIdOKtaBSIrZFkMtF3ORutogWMxw0UAeoZQG3KmzNNQcg7qOlRGwO4MLHPUBpAAqAsdtoLCEGQZ6XA205e7gBUEK/AShugkMG6gARGBzAtzhPzIGzqrcAKLog9q57gJMNquls9iwAzna/BHib5Hk5UFnVGpdTEQ4AifX/GIAcgM/K6vZsHYhnoUzxJMF5Yg6sFsyZz0ynwkbBixhAtzsAOREXa+uZ7Rg9P7+EyOLfnuXMMQ+Az35eiE7MJwIA9BDAZpgRTqOhrwN1gBjLHwQzC9nKq5k1IjfcKcPkQRKzh0GYv7oGrkBUAiJnpxSeKQCrmTWPjRi1Svub8AMAXnxWO2Yd+1nFjNjk/cQl5IZI2P5Mx2vdAVgjHIYDR96JuT8MrZRBNPVnFY1ibDoSHekI+aa7xMZM/XWyWFuG1OPPCbUYKQDNA8BrMervkV394fEo8KLaAaB8CIBM2ysZT63EoUu9VFEVKXTjpyLurjZ+EdXiANZ0GVKkAJtBUY3zbICL4o3gdtkf1VeQXmIcO2UcIOInQ9hqGTnERABT9fjr7gpUD7peXCpGGBAyCNCV8tVLGdFsgJ/v7++Vb/xczfezYhjVIQDKaBL36AlJQKbJSwghimjVHYB63Ii/4vyL9zIOgCrGxCZcgZxoNkC9SK1COwBy58WH1XMYJW4cIH6V4Owk222IaCZA/LZCfLjxH5hKURsLACSwyihALJh4FGoinr2EUAiE395u/G9vLCgDDoBKp6Ki4wAhZm9GMj0HtIpZUT78uPH/OLAagJq5cAeSMpzErv5fDYbINBVAxUyUX7/f+L+/srsV1m6j4wC7+FES4JoHICoIkO4ACH7YJ26j/aW4P1TkaJMBrNHh9cb/eqAYCBMkgo4/C0ENotPMBnA7PQA4dQL0Ju8iAuu6bQ506LcOE9qMjtEpuudq5HocxG1rP/d6DaosAFj2AsAicuuN0NJN9BFjSi3qRH3pJ+s6wAbtI9ahx5xwowVBp06mvoeWcp++bBI0XvbiGNljEQAShWUEQXBkAgVCii/GFELXGKkPEGIjdOChcAZ2bOFM4ZDmQSMCwF7/PeFxUdxVMuf2PUwRnltbAJxSe+5n32cMpGDckiHFVpvXnRFj3qgzYyYAURAjguWjwtqwxpnCEGNzEJMLcBhhsj46XJk4ID6BFdMwBbg5BoOBOWM8INsARwTLA728fHv5Fnp5+S0AvgYhrejjuxHrytjj/lUYCQ/C4P/buQLdxHEg6tOqQidVp1XlXcUnegOG8v+feDPvzcjUC6E6jiyR/MD2eDx23nOSkinb/StsHwc8ZYZchGMQy5nDZNDFTaLQiwJI/w9KSFfAHcSjpQkwFdxWPmvy0wp81YYAOra+z4CL9JOD7pYiIXDL32ZjPreFmhmO5qIAsNc3SroCW9DKVmGsIUBbsIgXzg530Gp2GnmMw0W+W51iq5nTAlQsDUr2uC3l+8SrAv7Q96wA23uUrbGgCnLE2tQTbqRhIGDRcXLIQf0gF2o8Tg1EgCpX4eUUS5LBdQGKWQE4LPdnt/25gxoFyJMLHKiCJiQFeRgM4V6EgHCAMSrnzfmxbWSQLsDoA9pcFbClAJDZ7rY7XVE9O/XY2maYR99oYOCtheS5o/AwZKctJ7lyko6ZfuX4qoYZAYEZAT93WIynd7fb8SDkrYYKgh6G6csMeHAhNEnqcso78MJczuBk620pwH3mxOozAn78YDUngIspYQowAtZssXbQVS8ogpaWXRMDAwVjmE8BocHnoPLLUGNQqYcMrlxCP/5RAVrNCdhCgPMA1S14g0ATAGkmwApqbTkKpwXvUGMmunBDE31UprX54KTczwJe+OkFgDsqx7eXl5dvhBovKdlC2ECuqS8DxZjH+ZkvBCCQfpDh+YEb7Lgi9Pl0W9gFWIOFQ4DHpwawCwF/K3etAOd9jpTIE5vA9XxF2CBGBtFHTaahnQ6XxWUoQJu2LLiTdrhgeJP+K8AM6/qbhQcKJQpW9NKPxj1scF2xAwHhjmWbbW92wky3wH8oEf/UoIELJrDpjtIqepq3NbTaNG1SC2bT+oDPbwJQbgsAe+CCADNiXevEYYNH6o5Pf0qNS2PujRnWwB8zYKcu8IsCXtsJOBeAozQ7liIdVBy2DtugQDcMKAxvapPQY1RzekALZ+frAl4hYHV4VfgVtFoBfhOs9AzwJuZNsE4BTcFaz8Am8Oxn4AUPEgQeRLVyfFOkp8cGj2sQYZkMKoU/CG3S0wP8jbG/zYRFBekeHK/hxoyK+nRiBcwIMHxrAHcAI8sLOIUA8q+oZwRsyF8LDLTom7HZLCxA6SrlWo0zXtbTdk7A5kU1WIFBG4C5tABSrmAP5tU6dUbA29v3t+/Ea8A6bwqtlhZQlbdBOZuOit6sgD83DXECoq9jiwvAnmsDS1/szF1CvOKt2mjRl3q05j2x+E0cCtyIakYAuBLNao7fIICcz5Xc/jEK6h04sLCAuGxi86vf0fP3wMs16F2wuADSjh8+VlV8DtyB5QXwJJgA415Px1UJ4AcxuFtrAvQcrEeAsSZOZvNCqisSUMHbysls3MurFKCGk1+XgHiO45XTPgnWJEAVYP9XK6BCAF/AmgTUyud/sraGZr06gTh87Pf7j8Ot8SUE8CcnBbACbgj4AMGPW+OLC9AX6i8IOOwPIDg7vkhKGfdu+zxbq4D4PMM5uCngY69lfnwJAeBfIYJJPdrjVwQctMyOLyaAF77y9gz/hBlrEQCYBNzJJ1SqZTUCTiFAAe5QcUGASKD4X3WrwVZKTcl6Itlgbkf28AcKwHZHTsNaXfWCgCJaSpUslSQzRIjakhLEhIAsXfiDBVQXoIjUrKYOtRSQrdz2agKUP95G0HpmZXorzkyEP1KAopJ71HhdEJCLsa7YXxNQQT0IuhkCuvDHCUAOHNR9/y8LkKysRaoYNSWX1UMfBUAOBQiIl7Pwh6aU53t/RFHzVwFTybWITGKscYnUXE2AekwA5Ey55GwxXfgiH2TVf9F+OR+YJgEfazNqmQpYqq0E1aHsJ1Tq6cIf/kHGnz0w6zUB0/tk1TsrItqUaPbjEfLYS4g7j7u5XhXwbqze9/aQ9m6PaXs6+FaCaLvxFvHIDzLfeW3OBZxSh73ioMQUe9SHw77h1vhjP4lP8SRH9rwd1pKRnU4UYFCz3c9rycj460SQR6knltNaEho8uCm0QaGh9f8r4PjbcUdG9qwCbucDQ8AQcIYkAaZgx1xylSJMA/xxujL9ysdaIjYz4gkEFGZYkYJV5VbRtnwAQ1VbG9BOgYDyNAJAKVKwyIeVa2RkUOQRFGA+lPIEAvjHwpGCWSN+1eR6JkC9TZ+F12cS0FIwI4Wrp4rkWkIA0y8IKFDqAuQJBEyfUjAjVa1SW4VRwDQh7+QoxCK70d70BAK6FExJ2Q6raS9JJnDy9EsgzBO0yZpnEDCfcc1nbM8hYD7jms/YnkDAnRnZEwi4LyN7AgF3ZGTPI+D+hKZquRv1gmMhAVKlHRQVCiEsdNGu0mLQa4u02WIVl+vWaebdGZlgISmSj8VZFKfUaMlRxDgUuNRGYQyYlljEZ6BUEQx08yCMS4rF3ZkPFKlS8Y3CMevCYMEDoKKnHAuOntVmX8AAMRpdciyi8SSJ8Vw4A3Wsgy2BWXCgewVk4VNsrrnosGSxlruVcbBiY6JOi0O/GAPGwJPbIsovixGDomzUz+dlJc2hmtWucrcAiedvAQ01+gSoCMZQaWhLhjyG+6z1xYyqm5f90RluKVaSiEBJN8HDU+r5Kc75ZR5fEI2oLgGKb6icAIhgNz1GUEq+nFF188icywnFJ7CR0k/w8JR6fkUUpfEzF95cvHQJUOyLR4I0nB4TIq5lVKXNQ8soLkqG/n++9BNcSkq/8IPexg/ljHifAMUgCXCH23dUFKK+axlVE1C4vjXm6QT8MsHDU+r5UUDwcxE1CwOKXEqAGMLrvvtKC/2ZjEpSmwcCGUs1UqmfMPmEJqDnVz7zYzKUtJeqCNrUJ0AJK2UdlZos9bBAxKAvNRZpGVViRiWf5iUcVC1zqrtop0vBhJmWiIZPRadHspbzVDWqS9DsHsEbRpLENkmxo4JHUfY2bg0iBPQZ4/1UIEttwbxibnvJ+Tx40YMGsJPUpWCeaakFn8ZaSlYmqyf1tAStgN/yWOA7sgWwwHdky+D+78gGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBtaCfwE/iN7wd4XgKAAAAABJRU5ErkJggg==","spider":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgBAMAAABQs2O3AAAAKlBMVEUAAACSiX+MgndgVEhPRTxHPjWoDg49NC0yKyYmIBsmHxoaFA88AgIiAADotIQjAAAAAXRSTlMAQObYZgAAAgpJREFUeNp10rFu1EAQBuB7B8RDUCIs0tB5rUuUNAivfBKvkI5b/eNLyVkzg6hQYGfyAkn6oDwAFUUqUvA4rHTGthTlb7b4P+3urHZV4uqqsuKbHdNNz3S9A5V1NUVIVW0leQfOCuQvhNzTDJhF3W6RlUvJV5R7przjBSBxu5KbHmV73OJaUc7DBDIVIKs5zuSU4nSIkYotgSUox25z0GAtfe+qk+GWN0PCARhI1fgwyJhjpOb7QDQBN0DVJ9BRiJmhB8CQAlhlAfZNaxAfb8ziwuSLQeqmrdx8BMJi0qsvBrms6hP3AuZIFxNYRJBj6L5W9d8UYg8SGcGmjSlRFuOB1/vqz7v6mLYbcscIEoYOAjNLoB+/6urtbwarGabB2q4juBnR5dnj+fnZ4wlDM2QEbQhdVHXOoLr69PBwVFfM0sNmgEE9g5Ve34U3dTh9IcZu/0Gsw2atboDL6f37u48vL+7E4c4jaD80YU2ult0v/NvP01d+707CGMG2DVVH5uNrPk0a2oog7vYM4G1qgN6FngEAgZmc8aQKMTSBgY62KbShCwIKaTODGPhztU6csA11FTgJQhNpBuGoNM2QIGDsQx0ktfF4ARKDIiKYmUAhdMZtxzIDYN+g5xLJVO4Aw8BYAG9L7waQeiaQGwlIZ5ARu9g7a9Yc0wYFK+kCeAbgDvfegdK7qyy+4z9b0kGP3NmJngAAAABJRU5ErkJggg==","tadpole":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAFVBMVEUAAABtUz1jSTNTOSNJLxlEKhQWCgDfr9AwAAAAAXRSTlMAQObYZgAAADdJREFUeNpjYGAyCglgAAFGRVcHMINNzMgARBuliSUZGQEZzkZGxsHODNiAs6KwA5RlYsBAOQAAqGUGQPKmAvcAAAAASUVORK5CYII=","turtle":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAABABAMAAAAg+GJMAAAAMFBMVEUAAAD////K0JHCt4ComnNHv0qUi2OCiFQ/pEI4jTopfUkwcj8gakMlZDMbWzExMTA/pVLKAAAAAXRSTlMAQObYZgAABDNJREFUeNqd1s9r40YUB/DAZr1Z3JT+By2S6XnRiJyD9ZjmtBRSUbWnQITMQlgKXeJL6aGxlpltD2VZjKY2uRW2JodAlxAxm9DD4satYnKuI7OH0qUkEfQ/6Iwk/7isrNnvSUb2h5n3pHlekqkG9AVC60vvnLuEPJsCDxMRL01pYIWQ1hRwo+T6leeBixUA234MsJV98Cxdxx5YFuDSwG3b/tzFX8nLigtoDZngpdlSAHYbOYCMmoZMBaAa+LaIQ0/i+GKpsm1ouqYhFYARCRCWAZamGcLACkD4ITn+nvzXyQCUAWvlgdWQBQGlrBNLYFn8WO7BVABGTQ/Aa56kQAUAtBoAzgHl3DI0rV4XVVQDVvyWtT4FdE0dcDbr9yaAbiJ1wN80Pkqv7riWoWsIGmrAXX9Py4BbKN2CjhQBsolmQE0dWAlaZlaDZWTUTFNDpirwDNazGnhQr2mAVWsQMjf7cgWQ2IJuYjWgGoZTwHIxbIMqwHl7AmCrpmMFYBkAHhx5YfpSt971ZL7tt8yC35Y5Vfdq97JLxoiFkNwBqJyq5Ds9B8IgIIQ89lzLQGZ5wHmqTQBGbZ+0AOmarius4CnKAR4y33E+Qyapm6R8DeietZ4DzJft2CAy5YcjbYEEKp5nU992GKEAIMu4VRLotPGP2aWF0AZlbQCsMl2rnaMJQEQC9gIZCFQAfvTgMAd8/wvKfpU9MKHkXCSs7fE2iGx8uuQ4NmX8uGVstJA5edpNE0q/W7IHtHNMZPIVpPMaewtr8tYuWHJezwD1pPN6rRiocnbm22TQ4SPO/2XsivOLOcAwFgNhNyBdxnnMTznrnPITNWCV8796vd6IixE75pyfxvEMSOf1x8XAe6PLg2ESHYwuo+RG7CJOrn+b3kznNS4G7kT9YZIk/eh3r/FtEg2T5lzT5bxetIJlgETkbxDBjWbTA1ifA3StEKiOX0H28oDIcMSvB75t0+jqMAd0ExUC798IADdFdgVwniSsG9hOl+2/zu6n8xoXrSDuAzSaMi7g81HI6B+93oCyw5IPUvXkDVgZsA2f3PAwePLcdnpkBtQWAWNAzTRfm/djzsgvsgbPaX5EyHm9CHCnwMOYd3uB/Ov600EOuHJee4VFvGx4GbDb2ImTq5eBWAHp8kkR5bzGBcDqOJqtYGcYhzSSXRjQvAbpvMaFW7h8NAW+kTVwRBfOnEkN0nldCPB5YCQAW3Th5ylQcQGKpr0EmtMapMCXogZPJFCQghXsR2kbD4blANmFeUB0IRgQ26FnndclAdkFKwdAdiH44YyQSD6JpSK70NjNa9DciTlnpBtFAe2UB8YwXcF9CTCxggErD8RvADLgkXiZRjzsMkK7jJcFVuN/5l7nm3icRCFjL4fJRdkuXPfngCi54uxP1hEzonQX4v7sRMLnMefhfpIwXrAFhXyw4P7/kgmYSeJwxJwAAAAASUVORK5CYII=","vex":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAMFBMVEUAAABthJtgeY9Ta4H////z+Pzg6fHG2eqivNWUrMOGn7eCl6t5kqp6j6RthJtgcoMKluMvAAAABHRSTlMAoKCgEjok+wAAAPlJREFUeNq1yL1Kw1AAhuE3hxYrRYm5gmivQL0Hwa1LnAtCFpdOjuKY4KBjfnYhIUPWEnIBhgxZRSjHC8hp3KWxkg5ndfD5pu8FDqNAKQkwKXIE9Fuj3zCQjECYDCaPs4tOQP9pDME4B1MAbIwTgJEwv2yixPcCPw2igGkcljlZ3YfP33WSJTB9yhG25HaJPLUBtkCdvl5fpVlVVRy3ntdQZW+eX6e77UIcNqAke0eq+JCgGvam4apoUGqtHtC067K9Q1O+l2WMJl9RvPAnl7+0L5Ao9NBB16GxZpZloTGFeWYC47l7DzrXcUF3sHTQjW9YoHMW4zn/5QeNK1xt8XQSVQAAAABJRU5ErkJggg==","villager":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAbFBMVEX///++iGy3gnKze2J6enpra2thYWFUVFRUU1NISElMODM9LSkAAAD///++iGy3gnKUj4m1e2eze2KBfXl/e3eja01vbWqQXkNjYmAAlhFUU1N1Ry93QjVcQjpISElMODM9LSkzJBEjIyMjGBTjk4HHAAAADXRSTlMAAAAAAAAAAAAAAAAA7Uh4SAAAAn9JREFUeNrtlN1ymzAQRm3cuG5C9oMFJIW0idu+/zt2/wieiTNAk0ufgdEKs0cryWJXB0RguUEO1/Xzc871Irs3AViTQdnYLgAzGfmPkFnT9V4tiAI+IyCB/19AVgA+KyCaF9FZL3iDlfUCAl0AjpYmFgWagjkJfjNCtCwgml7niyrmcJ0gho7mQkDrBJALsAhwyYY1IAP4bahgdmKFgBme8PjE/PRI3mMGG1gWxMpnE3GetmFiWQA/yJl/CpzJutggYAYYIjAyQfu8XlAv8DWCb5sFqW2Btk2llJSKctwqIAFJ8oVFQRL+ChcCEKGlKwJSQLRfENBHFYCMZYG+2F6tAOauFgRtC8K1RYSdVqhgd6eCUmIUxTtdKknySXchOYOQEhkAmNmm4YJ5FPWM0nRdbKNGZRxHH2KElx8CRmWCvu+7roxd17nADE1qhdR0RfP1mQZMbAqwnxcT9KnYS5Lc903Tq0U6jVTt+Xo5SesWCUMvIheUMvh5GnwqqpKUJPU0jUSik57jnyswABWgitMYU2MjZlw0X9ppDbpOTMSkwCCQCXw+FBe4uGGUdehGy3950ScaEC4/duwCaWHPorEdGSM/0mMRRlKYogD2bZzGZvuJIa+/SFIQYfCjqvb7w6EKDiYAGe5h4DU4/3o9ezSzF94JGK4IC84Tr+d3PHzwTbTqvQVkqjMlSEa5KjhYLUElPJxOD8r9/cmjGQJxJmMgQVN2B0UCbUxxPJ2+OyI4BnfKEcAwxF95mARL1BcAnJmcTIRqq0DzQsCZwJsEUIgGFUCIKmiTQEceZsGwRlBPQGHY3EOQB2Jalz9XAB11crFWUH+Sm+AmuAlugi8T/AM2/NtEbUTDnAAAAABJRU5ErkJggg==","warden":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAOVBMVEUAAADR1ra7w5sp3+uir4aBmYhudXsAkpVAV2wFYl0HSFcDQVAXKC8GLjcLISoRGyERFRoNEhcFBBJsUoYuAAAAAXRSTlMAQObYZgAABmJJREFUeNrtmuti4ygMhWMChEYrjtn3f9iVEG7r4E7iSyf7wyfcbGetzxIixTuXbyqloICdiCHDJV1+U0UEgFNiAOUdAIZApObfBlAUoLwToBCVE+B1gNvl+laA6+12e68HPoTgvXOAru8FYNrpgbJCAJwIgLHwhW4Sgo8jAfCjcSMIzgXAnJGZSO1f9wFgydiP4pS4fSNnMX+Rx98JIMLUQNQYRD0Mknog2bnMdDXrh4UAOQPIZl/GvWuiGwYXJ4DL5VgAZKDkYhqBRwIwwCKA8dKqTCkE9xoAGkAZpf+QRjQHADjF4NQDLsTEwHOApBFb4QELey7lH+WwwyZoZX0gUQiJ0XtgfxZkoHa1GQuQH6YhGCyyCDwHsOdf4YEMMz9qJ70AzASNwTBoBPAKAKWwDgBoIBMA5vYtBhaBg0LQA4xFalaGDsAYRHYexwNkmGGhGBUDetQgMF8Jf2EOQAHQ7FehmwPMthIy/9YcUAoNv3R2+F2wRLQk/J05kAH1/fJK2BCZJ7DjFiJ8AejwwxC+AeDLCRwCl1cBSAnW/hjZ43+OUeZCSrDR8SHof45nMhb1gI4PBFgnMBvYGwC6deBoEd3vdyKWXkV/cVdmMrMiae/0FgC6VQQpSSj+OgCpWhDoHSEww1MM3uIBc32bjG/wgJrV5qZNeg5wvdyOzgJqifCaB2Rjfj02BDf6ss89QG//4/A5UC3fTU8BrnRo5IkSiexAC7VLVuyKXZo8QHwAADMR852ll08temCH2lpzl5rbN5pZ3Znf6MJ7AVRZpPeXponaqczI+mFmaUi/ALQkUAIizvsAckaWCjVXTRXpqvSoIGsjKmUc60mUCUCccBGEvI+gAMVqwchjgRRoq6q2pVfr9Vt6AANoDNe9LjDrKjNq4uW3JbDuG4BoL8BMI1jUUCZBy3e9fscVAFkbsCUFN5zZrs2EXwIwcfm3Sga9+r3i9Ot5EAAKq3ETd6Ff+CO52uf7/TAAyLOrxA8ovZjLXLY47gdosTb7ED0Q9CHIfwTI2zwwFuR6Tya2tyUwtv4d5aTpd2N+rz6BV2zNOAOQjtHfrP9/SXcmsU/fAHLptAoAmAYvzQF7fuJFD5jwIkC/De4VQ4gdgKbhPATo77BqRbTUIlr8DjP3bCYsjfMWAHusZYBhGLAUjHYSHcEKgH5164XBDeoF1K6UUYuJWejMNApg/VYAspndS9+Vs5c6DMkNbNlrzz4MSqUCSs6zNWMdABPRTyEIzg1JanCDspSmUXn0pDkggwhZR2s9MLY58GMIvHMuSaMM3g0oTc6FWmoIMiElUK72sSEE9/pZBhCrUaowBO8FIFdsOCcnXfUAQCknKZSBzZPwfuflEHjnozdzMm4GgODkggFkCi47KYHyawBLGc3Mi6tYEKvRe6cgUgGU0QCC90JQjIBCILP/IsD4sNhC0n0aPgL4GAQgSgkegHlAD/Tajr8Jx5bKDiySdEdjmANEr7a8lz7FYAAZiDGFmLYDmMRsqinupbo4OGEA8xwgBIEQ6z5SSp8eoETKsBnAHGBWw+CEw2kfKtFsIUoxqAvEGFEitOCBiUmv7fPA4LyYFPmkzZTb83e/lNT3UQZMjGICM4goxk0AaE+BwfsghoUjSpXeSTcHYOIkDDFxYhFKExh6aSMAGoDzwUz66M0Z0rnwAKBOSMykNjGFD9UJnDYCQM1XAK8Q3kfp1BnehzkAbNdO0j2+zFYA3gegGeakSO8FQKsM5wBgUbVf9FPy7I/6zQBjBQiaSGK7proUn2JI84mFwlLkU83PNEpFv7au8UBKFH2qa11QBrVOibqdNOx5l7QdoGAEJSIxqg4IMUWtxMRzgF77AQosgjbF1XBIOohEBH4GMFq7C6CJRYlTrHlGHLUB49c98Imv1kgYki1zicGMsgQAaw8DaAJQ85wsz2p5uIVZBSxk+A4xO8gbtmY1FQtamjPAsDs+TlilrC0BBqHVrphpbN6cVpMsH0jBwn/MRAARMZqj6hAytEFZElbvDdGs9wD0RL3/Ta9Mwp5jtLrln4Pgs8ULAE+0CmASNkzCowBW7w27hQ3WaYP1AP3e8DkAFt9NWIatA9i0NyyosGDriqV5aWmOVQD93lCV/3yPKYlrpdKl+RqATXtDeqI1ACv3hsfrx73hqVOnTp06derUqVOnTp06derU/1n/AR0CP3Q2vq+HAAAAAElFTkSuQmCC","witch":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAACACAMAAACMX59YAAAAmVBMVEUAAAD///+2qJWrn42rnYmmmoijlIOgkoCPj4+YiXeWdliWdFcltDWUc1Z6d3eAcl6PbFNra2uQXkN6XnBhYWFoWUwMixp1Ry8vaDVaTTZMTExHT0hQQy0/Pz9IPCc6OjpANSQkQCg1Lh0dPAkxKRwoKCgsKRg2F1guJxonJiYwFE0YKRoTLgEeHh4oEEIdCy8REREVByUKDAvCurIkAAAAAXRSTlMAQObYZgAAA1lJREFUeNrtloFS2kAQhiPphVJbKVq10XDG2Ba4mjvx/R+u/+5yR1U0d2SqYyefgcuO+T/2NoYxyw4PMyLPlVL5KN+gsuz8/Pg46+bw65dPtEoeginjBdFAkAvTEkwVxen1eoJRjg30FfTbgjAKQ0wlD0NQTEKQNq98ntcANhU3fQkVile1talogRJBkQsiIFG3QPnrVQHkNBhAt8BHCgKFxBURJSh8/hcjFeULECXwMzs4KYqTAwpLXgxFtwBXkWM6/gDGU0kHugUTMoDpeHx6Oh5PleQnExy0dAsmrICAmUp8S/ZeKQum/BFIFUyYWIEGP0GMYAEM+OeCgnkVQQNqCJrlcqlBDXBWqVGuKlSE1lVZVpW2wAtoeSxoWEB1XZVaKV2WqEBVleCBwNEaBFoEsm4Epaa3RgQlTvHmSGCQdxCYIKglWEOAbpdNAxclaoR8hWZwOGedgcABWkVQe0ENFY4lIIFmgVQaabw4CPzKglrX/MkNTuigAJpmQaUbrl4WIMXD07JKhHagMfiGigYmHFXVIahl9UNssANUAGdIw7FTgMslSAMAItAiCDpsCT/ZLubzppHcfD4D4tFVSV1UGwHycOwWXMwR/Hg0w3JBAuwaR83AQhORHtDjbgGYHc1mtH4DMjbN1DpUPKnseRZ+eRFjrbPWGKx88vev4gQGOQPYZJIFoQO7swNZXsSGLTzpYNEpcIA7gEHOxRY7RBFgBs4L/BycW6P0HaBar52VLxz7tAMJiUC6ySwiEBiZMucd8oR5IkAqCMS1EVi+2iwMFw5hEUTAAkuC2xsIaENUCdEC9EICRGy6wPEMsGkvWKcK1jKDrcAlbmENgoArt7/A7CFwDwRuDwFP0W+BJ5J+GyEwj+6CSRQYEZitgH6iBU4ExgtMssAaayCgR5QqK/8B2PgtWMsCePhhIB+wCX8H4PYGQdKJAG9JHYjAypNhKQ0ShshXs8DtIXCISMECrliYpbEK3AcSBS2zv6C9Y9pB8IaC/rfRkyZYRfKsoI0kspGOzrtuQdtHcEe8R0H/GfS/C23gd/uYNMH19+u9BFcbLs8+n11ePSSpgxU6WLUeea6TJsiz2xd88tsJ+m8h9osj+gvl3QnA6j7rBQ+uD9z2wMDAwMDAwH/EH571WWMJHeH4AAAAAElFTkSuQmCC","wolf":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAMAAACVQ462AAAAw1BMVEXt5+jk3t/c3Nzf2dnW0NHKxsfIwsO3t7ferIWzs7Nu1Je3q6WhoaGdnU10oIaBgYEatVh+dnBegyJlZi1iWkAodi5WVlZUVFRERERHQjs+PTBENiY5ODVDNSWAAHY/MCQzMzMtLSg1Kh8sLCo0KR4ZGRkAAAD////k2Nnd2tvV0NDTz8/Kx8jJwcLBvr7UtaTOr5bEq5ywqqefmpaalZGnj36ajIiVhnmheFuUe2iBdm2Mb1JJQjk5PD85ODUtLSgSFBY76smzAAAAJ3RSTlMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAi/HzzAAADR0lEQVR42s2UbXfbNgyF6Wli41WZqm3NtkQOZ3ZVwwCFbShO6r6k/v+/ahdwtjOp/dJvvZT5gqP7HAC2GYZhWFDBYBIilsvLy5Ty8HsPiY4yirIQBgtfX1/HLlbD/xUcQIXh51JI1ut1TumiXyz6SKKkAi4AzEQGCLH7AhCpFDxMmBmAlNIffYw9MlMgPewrAPDHKWCxiAtmEFjYyogIrNd//frixU8/lyKIF4htdUAIsxIETiEAfMPGQRmDC0HzGt1Xz6AKUwAXJxC8VAgkb+TgIgIacV//7UE1K8Fsb477/fGN+Q1HAFw9e5auErZi5KcVANcsA+D3H+7vP+yFvA9CvBZhr8qmU4dP9Ukx3AzABP/DAwjE9hbGb1TcYnYCynCOksJYpwCR/cM9QfcPe3lSApWwAgJ5AbazPhhqCkgpvdwddnjOzlLKOZnEatH/CKKEgJ0w5CuA7WHrgL8dkNMVLMKj18BWxegBIgVHZQrIOb/cHDYO+CGfAIl1VMHjDYRvFATYQ9hMAb9Am08bJHF2dg6Z/wqq+9j3nf3wYmhjFWOoY+xirOowBWw3Nzc3R3w22+1utz0fHqFl09Z919e1WAqS61hBDsA6B4Bg/g36cNidP5bySKVtYmeDCkSr6nl8jiy6PkYsU8Dh+Omk4+EEIHosVFV17LouFhKlEioQapj7LoZYTwE2NaucXl9cvLa9XwSLUHW9DaERDayRd4ghdH++whzngM+f373LKwA+fnz/dvCLIIaqRgv6qGKAEJxQhVcd0ghfBSQHvH2P0I8h4O3Tt6Cqo+riZK8i9MV98HThcVZMQsyDiLVO7CjYEGYqSlSFAABYUwDx6T/YwEesTANWOIRxNCuRFLZWLtGqpmmW7QxAzGZdqllUDFBOAMvA9pgVuTRt2yyXmOYZCDOsjYqjeGAqEBPEBSIERkCaJue2BWbeA7jwcYACMbAl4CWI+NYBVAyQVmkOuNPxTvVubAUWJZQAJxh2ZAD8RKNyaVv7r+c5AH6MUbMKwW5NZKtamP26RgiPjkwruLMxpgBhvr29ZTEAmwsAtq3gMdbpNDIjfSBWaTUFrFJOVlvGQHrAm9XNPmEAwCqc7UWbpoDh2/WdAf4B+LQj08eGOFAAAAAASUVORK5CYII=","zombie":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAclBMVEUAAAAAzMyPj495nGVxlVtvlVxtlVtnkFYAr69ph1Z6d3cAqKgAnp5ae0gAmZlra2tXdUdOezZhYWFIdTJJcTVWScwAf39KaS0+aS07Yi9MTExGOqU2WiUAaGg4UiYAW1s/Pz86MYkwKHImIVsoKCgaGho5R/XAAAAAAXRSTlMAQObYZgAAAkJJREFUeNrtk2FzmzAMhpPNm+0akmjVqFRGEsPy///iXtnlml27laRfeWwswZ0eI3NsZkRFVWwUdHMrIqzKitKC3C4gVjhgOQLVOwQEAcvdAmViwZQiuKOFsj2J3i1gZWEVrAqEbxaIlkHMAni5QK15VRSpwTI/UFuYFnx/q2ZCQYyMEq5PSKWMJW8gZSv//av75rneqcJFxMqLemdiiq1zLraR6llimEtk2fdnpvZycfFyaeGScggkqKclLQjqWShG1RhR/XIKCEgWtCByLJs96tOTPiIROoogMhEvO0QWqf+AWqXdsQIEWfhP/AS/wXy//VXYji/Mz8N2G5iIPxYcC28EW0CsnxOwqnyqhTf/yQhy3/fjNE096Lru1CRl0dR0oGlSCAn5bteZYI7vCcYiGIYuhUa1CQH5kFIAVRbAHP8S9FeCoQhC2XgAKSDB8h9BroIMQc79aRhOwQwoqzkyTMt3Ke3m+CrIVWAxwzacTiZIRQBCgwyXgAgEtGAzk/tcdh6R2ETNkEyAOSCfBcykLjovyuy9j68CVPUAS4lWZB3AkQa8b1WlJMzqW9eykvhD9O8IkM1foR5Bg3wwlzlQJ967loQ0Ru+vD7EKMjJggnJsuKosYZpA2TsXiVnjdQvnsxVa+fm8B93QdbanlRUBkoBFWMk574RF/PUbPJ9R+OVhj/BsgnEap1xIoZsmyO1EcyYI0IFXQTz4+CoA+4f93uIPAMHYF3JTcyRokkmodbFVEo6HeNisrKysrKysrPyDP68WY0lh7YK7AAAAAElFTkSuQmCC","zombie_villager":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAt1BMVEVskkNwbWdVczVjW1tWYC5jVC9IYC4/WChUSSg4TSM4OyExPSUuNyQAAAD///+Iqlt3oEd2kk9skkOCe3RyhFFmij9ffzdwbWdedUVXdUdVczVjW1tUZkBOazBgXjhOaC1HZTdQVzdUTU1PUTNBWSiZKys/WChcQjpURypDTCQ4TSNKPj42TCpJQSRMODNCNzcxRB8vQh44OyExPSUsPRw7MTEyNyQqOSE9LSk2MiAzJBEgKxgjIyNApSiHAAAADnRSTlMAAAAAAAAAAAAAAAAAAKroblcAAAMeSURBVHja7dF/X9s2EMfxrGs3oGtxHXTSztIuE+mtyG1+2HNKxp7/49pXIoHSUhxe65/52E6cmHs7MpMXuxxR3p0iFF6+3Gz+vPztlxdP9AZN9h8IfQ1c/vEMgMkgItV/kYbN5upys/nw4dWhAOarGkS4Ay6vthmYfL8HAHNViD2w3VxdbbfPAGoMV1VVuz2w/fTpeUBt6ro6r4yWwtnZycnJ2dmrgwHMwqjODbmADgeIqrx6Q5XBKvALSlRyOCZjAJM5ZyJig0dgKkN3sw4HjwMEwTAFNlRfuIpDnkIF4DAOMAsRgGAMuYvzEBwmQRTG0SGAGlYX5hfv8AjfzYMT54QzwJ4OAkRFHc//Kc2ZhJQ9xvHCBwDCAkGE3r4P4f1bkuxB9ThRFR4FNCVFqQ9zFHp8wr7OX616XBsFUt+vUwbmf6M5ZtI6rYHgwqpfrQ4BUhb6eanv0rpba5dS32G8GwdOT1+f5iYjjQG/PvmHTwE/o5/Q5EFWhVnUNnc9FxBCfChgUd91/et7gDGu9AjgyNHnm8833wIQvgDoe7+A6to9CnQQvgQYgDwCmLqmGwhjS1Bh4scAMiQPgCZnse2zdmqbiHkS2Qm2URG11iEict47T6gAbRObaNvYNjhpbIyxzIsw42War8a4U/M8Nod5vQdibNs4DC0MGzNgpzGlJEiTLfMQUIwYNo68I3WexO2BdgCgQ3awNVlJuzrcGpP5FVkhckCIRLGLuwVaTaopJLyBGuK9gPmptRbfNFMc1qozZfFCglQKoIgDOeagA7Ym3zEOXa7MN1gASBBRlQwZQ96JOBwF4MCeidg5puQ4YMELPNgO5UeXsWVEgAYVJlMALkIBKIAAwHnH0SI8FE2xybddYr5tmzZFJAwAeWYIorcACzE5KC4wBwDLYVgu24gwPeR57KlFHpN1XRsiZgehACKz2wUJ+xnOF7uu/1pcL77Kq6caZUD2/0ZFAk9zM9XrfYvrb1L1TPXvNbFnhwqQEPOKZbVKaZW0aWIpvy+bZVOypUYT7vFx9tGz9zOcFuDN/+gIHIEjcASOwBH44cB/1lQM4ZNhKecAAAAASUVORK5CYII="} \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/allay.obj b/renderer/viewer/three/entity/models/allay.obj new file mode 100644 index 00000000..69765c22 --- /dev/null +++ b/renderer/viewer/three/entity/models/allay.obj @@ -0,0 +1,325 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o head +v 0.15625 0.5631249999999999 0.15625 +v 0.15625 0.5631249999999999 -0.15625 +v 0.15625 0.250625 0.15625 +v 0.15625 0.250625 -0.15625 +v -0.15625 0.5631249999999999 -0.15625 +v -0.15625 0.5631249999999999 0.15625 +v -0.15625 0.250625 -0.15625 +v -0.15625 0.250625 0.15625 +vt 0.15625 0.84375 +vt 0.3125 0.84375 +vt 0.3125 0.6875 +vt 0.15625 0.6875 +vt 0 0.84375 +vt 0.15625 0.84375 +vt 0.15625 0.6875 +vt 0 0.6875 +vt 0.46875 0.84375 +vt 0.625 0.84375 +vt 0.625 0.6875 +vt 0.46875 0.6875 +vt 0.3125 0.84375 +vt 0.46875 0.84375 +vt 0.46875 0.6875 +vt 0.3125 0.6875 +vt 0.3125 0.84375 +vt 0.15625 0.84375 +vt 0.15625 1 +vt 0.3125 1 +vt 0.46875 1 +vt 0.3125 1 +vt 0.3125 0.84375 +vt 0.46875 0.84375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.09375 0.25 0.0625 +v 0.09375 0.25 -0.0625 +v 0.09375 0 0.0625 +v 0.09375 0 -0.0625 +v -0.09375 0.25 -0.0625 +v -0.09375 0.25 0.0625 +v -0.09375 0 -0.0625 +v -0.09375 0 0.0625 +vt 0.0625 0.625 +vt 0.15625 0.625 +vt 0.15625 0.5 +vt 0.0625 0.5 +vt 0 0.625 +vt 0.0625 0.625 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.21875 0.625 +vt 0.3125 0.625 +vt 0.3125 0.5 +vt 0.21875 0.5 +vt 0.15625 0.625 +vt 0.21875 0.625 +vt 0.21875 0.5 +vt 0.15625 0.5 +vt 0.15625 0.625 +vt 0.0625 0.625 +vt 0.0625 0.6875 +vt 0.15625 0.6875 +vt 0.25 0.6875 +vt 0.15625 0.6875 +vt 0.15625 0.625 +vt 0.25 0.625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o body +v 0.08125000000000004 0.23750000000000004 0.050000000000000044 +v 0.08125000000000004 0.23750000000000004 -0.04999999999999999 +v 0.08125000000000004 -0.04999999999999999 0.050000000000000044 +v 0.08125000000000004 -0.04999999999999999 -0.04999999999999999 +v -0.08124999999999999 0.23750000000000004 -0.04999999999999999 +v -0.08124999999999999 0.23750000000000004 0.050000000000000044 +v -0.08124999999999999 -0.04999999999999999 -0.04999999999999999 +v -0.08124999999999999 -0.04999999999999999 0.050000000000000044 +vt 0.0625 0.4375 +vt 0.15625 0.4375 +vt 0.15625 0.28125 +vt 0.0625 0.28125 +vt 0 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.28125 +vt 0 0.28125 +vt 0.21875 0.4375 +vt 0.3125 0.4375 +vt 0.3125 0.28125 +vt 0.21875 0.28125 +vt 0.15625 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.28125 +vt 0.15625 0.28125 +vt 0.15625 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.5 +vt 0.15625 0.5 +vt 0.25 0.5 +vt 0.15625 0.5 +vt 0.15625 0.4375 +vt 0.25 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o right_arm +v 0.15625 0.25 0.0625 +v 0.15625 0.25 -0.0625 +v 0.15625 0 0.0625 +v 0.15625 0 -0.0625 +v 0.09375 0.25 -0.0625 +v 0.09375 0.25 0.0625 +v 0.09375 0 -0.0625 +v 0.09375 0 0.0625 +vt 0.78125 0.9375 +vt 0.8125 0.9375 +vt 0.8125 0.8125 +vt 0.78125 0.8125 +vt 0.71875 0.9375 +vt 0.78125 0.9375 +vt 0.78125 0.8125 +vt 0.71875 0.8125 +vt 0.875 0.9375 +vt 0.90625 0.9375 +vt 0.90625 0.8125 +vt 0.875 0.8125 +vt 0.8125 0.9375 +vt 0.875 0.9375 +vt 0.875 0.8125 +vt 0.8125 0.8125 +vt 0.8125 0.9375 +vt 0.78125 0.9375 +vt 0.78125 1 +vt 0.8125 1 +vt 0.84375 1 +vt 0.8125 1 +vt 0.8125 0.9375 +vt 0.84375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o left_arm +v -0.09375 0.25 0.0625 +v -0.09375 0.25 -0.0625 +v -0.09375 0 0.0625 +v -0.09375 0 -0.0625 +v -0.15625 0.25 -0.0625 +v -0.15625 0.25 0.0625 +v -0.15625 0 -0.0625 +v -0.15625 0 0.0625 +vt 0.78125 0.75 +vt 0.8125 0.75 +vt 0.8125 0.625 +vt 0.78125 0.625 +vt 0.71875 0.75 +vt 0.78125 0.75 +vt 0.78125 0.625 +vt 0.71875 0.625 +vt 0.875 0.75 +vt 0.90625 0.75 +vt 0.90625 0.625 +vt 0.875 0.625 +vt 0.8125 0.75 +vt 0.875 0.75 +vt 0.875 0.625 +vt 0.8125 0.625 +vt 0.8125 0.75 +vt 0.78125 0.75 +vt 0.78125 0.8125 +vt 0.8125 0.8125 +vt 0.84375 0.8125 +vt 0.8125 0.8125 +vt 0.8125 0.75 +vt 0.84375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o left_wing +v -0.03125 0.1875 0.5625 +v -0.03125 0.1875 0.0625 +v -0.03125 -0.125 0.5625 +v -0.03125 -0.125 0.0625 +v -0.03125 0.1875 0.0625 +v -0.03125 0.1875 0.5625 +v -0.03125 -0.125 0.0625 +v -0.03125 -0.125 0.5625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 0.75 0.15625 +vt 1 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 1 0.15625 +vt 1 0.3125 +vt 1 0.3125 +vt 1 0.15625 +vt 1 0.15625 +vt 0.75 0.3125 +vt 0.5 0.3125 +vt 0.5 0.15625 +vt 0.75 0.15625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.75 0.5625 +vt 0.75 0.5625 +vt 0.75 0.5625 +vt 0.75 0.5625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o right_wing +v 0.03125 0.1875 0.5625 +v 0.03125 0.1875 0.0625 +v 0.03125 -0.125 0.5625 +v 0.03125 -0.125 0.0625 +v 0.03125 0.1875 0.0625 +v 0.03125 0.1875 0.5625 +v 0.03125 -0.125 0.0625 +v 0.03125 -0.125 0.5625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 0.75 0.15625 +vt 0.5 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 0.5 0.15625 +vt 1 0.3125 +vt 1 0.3125 +vt 1 0.15625 +vt 1 0.15625 +vt 0.75 0.3125 +vt 1 0.3125 +vt 1 0.15625 +vt 0.75 0.15625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.75 0.5625 +vt 0.75 0.5625 +vt 0.75 0.5625 +vt 0.75 0.5625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b432e290-ff98-8fcb-61cd-b0f5c289cd94 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/arrow.obj b/renderer/viewer/three/entity/models/arrow.obj new file mode 100644 index 00000000..469dd6cd --- /dev/null +++ b/renderer/viewer/three/entity/models/arrow.obj @@ -0,0 +1,60 @@ +# Aspose.3D Wavefront OBJ Exporter +# Copyright 2004-2024 Aspose Pty Ltd. +# File created: 02/12/2025 20:01:28 + +mtllib material.lib +g Arrow + +# +# object Arrow +# + +v -160 8.146034E-06 50 +v 160 8.146034E-06 50 +v -160 -8.146034E-06 -50 +v 160 -8.146034E-06 -50 +v -160 -50 1.1920929E-05 +v 160 -50 1.1920929E-05 +v -160 50 -1.1920929E-05 +v 160 50 -1.1920929E-05 +v -140 -49.999992 50.000008 +v -140 50.000008 49.999992 +v -140 -50.000008 -49.999992 +v -140 49.999992 -50.000008 +# 12 vertices + +vn 0 1 -1.6292068E-07 +vn 0 1 -1.6292068E-07 +vn 0 1 -1.6292068E-07 +vn 0 1 -1.6292068E-07 +vn 0 3.1391647E-07 1 +vn 0 3.1391647E-07 1 +vn 0 3.1391647E-07 1 +vn 0 3.1391647E-07 1 +vn -1 0 0 +vn -1 0 0 +vn -1 0 0 +vn -1 0 0 +# 12 vertex normals + +vt 0 0.84375 0 +vt 0.5 1 0 +vt 0.5 1 0 +vt 0.5 0.84375 0 +vt 0 1 0 +vt 0.15625 0.84375 0 +vt 0.15625 0.6875 0 +vt 0 0.84375 0 +vt 0.5 0.84375 0 +vt 0 1 0 +vt 0 0.6875 0 +vt 0 0.84375 0 +# 12 texture coords + +usemtl Arrow +s 1 +f 1/1/1 2/9/2 4/2/3 3/10/4 +f 5/8/5 6/4/6 8/3/7 7/5/8 +f 9/11/9 10/7/10 12/6/11 11/12/12 +#3 polygons + diff --git a/renderer/viewer/three/entity/models/axolotl.obj b/renderer/viewer/three/entity/models/axolotl.obj new file mode 100644 index 00000000..706474b3 --- /dev/null +++ b/renderer/viewer/three/entity/models/axolotl.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.25 0.25 0.3125 +v 0.25 0.25 -0.3125 +v 0.25 0 0.3125 +v 0.25 0 -0.3125 +v -0.25 0.25 -0.3125 +v -0.25 0.25 0.3125 +v -0.25 0 -0.3125 +v -0.25 0 0.3125 +vt 0.15625 0.671875 +vt 0.28125 0.671875 +vt 0.28125 0.609375 +vt 0.15625 0.609375 +vt 0 0.671875 +vt 0.15625 0.671875 +vt 0.15625 0.609375 +vt 0 0.609375 +vt 0.4375 0.671875 +vt 0.5625 0.671875 +vt 0.5625 0.609375 +vt 0.4375 0.609375 +vt 0.28125 0.671875 +vt 0.4375 0.671875 +vt 0.4375 0.609375 +vt 0.28125 0.609375 +vt 0.28125 0.671875 +vt 0.15625 0.671875 +vt 0.15625 0.828125 +vt 0.28125 0.828125 +vt 0.40625 0.828125 +vt 0.28125 0.828125 +vt 0.28125 0.671875 +vt 0.40625 0.671875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0 0.3125 0.25 +v 0 0.3125 -0.3125 +v 0 0 0.25 +v 0 0 -0.3125 +v 0 0.3125 -0.3125 +v 0 0.3125 0.25 +v 0 0 -0.3125 +v 0 0 0.25 +vt 0.171875 0.59375 +vt 0.171875 0.59375 +vt 0.171875 0.515625 +vt 0.171875 0.515625 +vt 0.03125 0.59375 +vt 0.171875 0.59375 +vt 0.171875 0.515625 +vt 0.03125 0.515625 +vt 0.3125 0.59375 +vt 0.3125 0.59375 +vt 0.3125 0.515625 +vt 0.3125 0.515625 +vt 0.171875 0.59375 +vt 0.3125 0.59375 +vt 0.3125 0.515625 +vt 0.171875 0.515625 +vt 0.171875 0.59375 +vt 0.171875 0.59375 +vt 0.171875 0.734375 +vt 0.171875 0.734375 +vt 0.171875 0.734375 +vt 0.171875 0.734375 +vt 0.171875 0.59375 +vt 0.171875 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o right_arm +v 0.25 0.0625 -0.375 +v 0.25 0.0625 -0.375 +v 0.5625 0.0625 -0.375 +v 0.5625 0.0625 -0.375 +v 0.25 0.0625 -0.1875 +v 0.25 0.0625 -0.1875 +v 0.5625 0.0625 -0.1875 +v 0.5625 0.0625 -0.1875 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.03125 0.71875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.71875 +vt 0.03125 0.71875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vn -2.220446049250313e-16 -1 -2.220446049250313e-16 +vn 4.930380657631324e-32 2.220446049250313e-16 -1 +vn 2.220446049250313e-16 1 2.220446049250313e-16 +vn -4.930380657631324e-32 -2.220446049250313e-16 1 +vn -1 2.220446049250313e-16 4.930380657631324e-32 +vn 1 -2.220446049250313e-16 -4.930380657631324e-32 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o right_leg +v 0.25 0.0625 0.3125 +v 0.25 0.0625 0.3125 +v 0.5625 0.0625 0.3125 +v 0.5625 0.0625 0.3125 +v 0.25 0.0625 0.125 +v 0.25 0.0625 0.125 +v 0.5625 0.0625 0.125 +v 0.5625 0.0625 0.125 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.03125 0.71875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.71875 +vt 0.03125 0.71875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vn 2.220446049250313e-16 1 -2.220446049250313e-16 +vn 4.930380657631324e-32 2.220446049250313e-16 1 +vn -2.220446049250313e-16 -1 2.220446049250313e-16 +vn -4.930380657631324e-32 -2.220446049250313e-16 -1 +vn -1 2.220446049250313e-16 -4.930380657631324e-32 +vn 1 -2.220446049250313e-16 4.930380657631324e-32 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o left_arm +v -0.25 0.0625 -0.1875 +v -0.25 0.0625 -0.1875 +v -0.5625 0.0625 -0.1875 +v -0.5625 0.0625 -0.1875 +v -0.25 0.0625 -0.375 +v -0.25 0.0625 -0.375 +v -0.5625 0.0625 -0.375 +v -0.5625 0.0625 -0.375 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.03125 0.71875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.71875 +vt 0.03125 0.71875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vn 2.220446049250313e-16 -1 -2.220446049250313e-16 +vn 4.930380657631324e-32 -2.220446049250313e-16 1 +vn -2.220446049250313e-16 1 2.220446049250313e-16 +vn -4.930380657631324e-32 2.220446049250313e-16 -1 +vn 1 2.220446049250313e-16 4.930380657631324e-32 +vn -1 -2.220446049250313e-16 -4.930380657631324e-32 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o left_leg +v -0.25 0.0625 0.125 +v -0.25 0.0625 0.125 +v -0.5625 0.0625 0.125 +v -0.5625 0.0625 0.125 +v -0.25 0.0625 0.3125 +v -0.25 0.0625 0.3125 +v -0.5625 0.0625 0.3125 +v -0.5625 0.0625 0.3125 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.03125 0.71875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.71875 +vt 0.03125 0.71875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.71875 +vt 0.078125 0.71875 +vt 0.078125 0.796875 +vt 0.03125 0.796875 +vt 0.03125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.796875 +vt 0.125 0.796875 +vn -2.220446049250313e-16 1 -2.220446049250313e-16 +vn 4.930380657631324e-32 -2.220446049250313e-16 -1 +vn 2.220446049250313e-16 -1 2.220446049250313e-16 +vn -4.930380657631324e-32 2.220446049250313e-16 1 +vn 1 2.220446049250313e-16 -4.930380657631324e-32 +vn -1 -2.220446049250313e-16 4.930380657631324e-32 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o tail +v 0 0.3125 1 +v 0 0.3125 0.25 +v 0 0 1 +v 0 0 0.25 +v 0 0.3125 0.25 +v 0 0.3125 1 +v 0 0 0.25 +v 0 0 1 +vt 0.21875 0.515625 +vt 0.21875 0.515625 +vt 0.21875 0.4375 +vt 0.21875 0.4375 +vt 0.03125 0.515625 +vt 0.21875 0.515625 +vt 0.21875 0.4375 +vt 0.03125 0.4375 +vt 0.40625 0.515625 +vt 0.40625 0.515625 +vt 0.40625 0.4375 +vt 0.40625 0.4375 +vt 0.21875 0.515625 +vt 0.40625 0.515625 +vt 0.40625 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.515625 +vt 0.21875 0.515625 +vt 0.21875 0.703125 +vt 0.21875 0.703125 +vt 0.21875 0.703125 +vt 0.21875 0.703125 +vt 0.21875 0.515625 +vt 0.21875 0.515625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o head +v 0.25 0.3125 -0.3125 +v 0.25 0.3125 -0.625 +v 0.25 0 -0.3125 +v 0.25 0 -0.625 +v -0.25 0.3125 -0.625 +v -0.25 0.3125 -0.3125 +v -0.25 0 -0.625 +v -0.25 0 -0.3125 +vt 0.078125 0.90625 +vt 0.203125 0.90625 +vt 0.203125 0.828125 +vt 0.078125 0.828125 +vt 0 0.90625 +vt 0.078125 0.90625 +vt 0.078125 0.828125 +vt 0 0.828125 +vt 0.28125 0.90625 +vt 0.40625 0.90625 +vt 0.40625 0.828125 +vt 0.28125 0.828125 +vt 0.203125 0.90625 +vt 0.28125 0.90625 +vt 0.28125 0.828125 +vt 0.203125 0.828125 +vt 0.203125 0.90625 +vt 0.078125 0.90625 +vt 0.078125 0.984375 +vt 0.203125 0.984375 +vt 0.328125 0.984375 +vt 0.203125 0.984375 +vt 0.203125 0.90625 +vt 0.328125 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o left_gills +v -0.25 0.4375 -0.375 +v -0.25 0.4375 -0.375 +v -0.25 0 -0.375 +v -0.25 0 -0.375 +v -0.4375 0.4375 -0.375 +v -0.4375 0.4375 -0.375 +v -0.4375 0 -0.375 +v -0.4375 0 -0.375 +vt 0.171875 0.375 +vt 0.21875 0.375 +vt 0.21875 0.265625 +vt 0.171875 0.265625 +vt 0.171875 0.375 +vt 0.171875 0.375 +vt 0.171875 0.265625 +vt 0.171875 0.265625 +vt 0.21875 0.375 +vt 0.265625 0.375 +vt 0.265625 0.265625 +vt 0.21875 0.265625 +vt 0.21875 0.375 +vt 0.21875 0.375 +vt 0.21875 0.265625 +vt 0.21875 0.265625 +vt 0.21875 0.375 +vt 0.171875 0.375 +vt 0.171875 0.375 +vt 0.21875 0.375 +vt 0.265625 0.375 +vt 0.21875 0.375 +vt 0.21875 0.375 +vt 0.265625 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o right_gills +v 0.4375 0.4375 -0.375 +v 0.4375 0.4375 -0.375 +v 0.4375 0 -0.375 +v 0.4375 0 -0.375 +v 0.25 0.4375 -0.375 +v 0.25 0.4375 -0.375 +v 0.25 0 -0.375 +v 0.25 0 -0.375 +vt 0 0.375 +vt 0.046875 0.375 +vt 0.046875 0.265625 +vt 0 0.265625 +vt 0 0.375 +vt 0 0.375 +vt 0 0.265625 +vt 0 0.265625 +vt 0.046875 0.375 +vt 0.09375 0.375 +vt 0.09375 0.265625 +vt 0.046875 0.265625 +vt 0.046875 0.375 +vt 0.046875 0.375 +vt 0.046875 0.265625 +vt 0.046875 0.265625 +vt 0.046875 0.375 +vt 0 0.375 +vt 0 0.375 +vt 0.046875 0.375 +vt 0.09375 0.375 +vt 0.046875 0.375 +vt 0.046875 0.375 +vt 0.09375 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o top_gills +v 0.25 0.5 -0.375 +v 0.25 0.5 -0.375 +v 0.25 0.3125 -0.375 +v 0.25 0.3125 -0.375 +v -0.25 0.5 -0.375 +v -0.25 0.5 -0.375 +v -0.25 0.3125 -0.375 +v -0.25 0.3125 -0.375 +vt 0.046875 0.421875 +vt 0.171875 0.421875 +vt 0.171875 0.375 +vt 0.046875 0.375 +vt 0.046875 0.421875 +vt 0.046875 0.421875 +vt 0.046875 0.375 +vt 0.046875 0.375 +vt 0.171875 0.421875 +vt 0.296875 0.421875 +vt 0.296875 0.375 +vt 0.171875 0.375 +vt 0.171875 0.421875 +vt 0.171875 0.421875 +vt 0.171875 0.375 +vt 0.171875 0.375 +vt 0.171875 0.421875 +vt 0.046875 0.421875 +vt 0.046875 0.421875 +vt 0.171875 0.421875 +vt 0.296875 0.421875 +vt 0.171875 0.421875 +vt 0.171875 0.421875 +vt 0.296875 0.421875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_eacaae49-c631-7fce-d186-9315d408fc43 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/blaze.obj b/renderer/viewer/three/entity/models/blaze.obj new file mode 100644 index 00000000..d4037c53 --- /dev/null +++ b/renderer/viewer/three/entity/models/blaze.obj @@ -0,0 +1,601 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o upperBodyParts0 +v -0.5 1.625 -0.0625 +v -0.5 1.625 -0.1875 +v -0.5 1.125 -0.0625 +v -0.5 1.125 -0.1875 +v -0.625 1.625 -0.1875 +v -0.625 1.625 -0.0625 +v -0.625 1.125 -0.1875 +v -0.625 1.125 -0.0625 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o upperBodyParts1 +v 0.625 1.625 0.1875 +v 0.625 1.625 0.0625 +v 0.625 1.125 0.1875 +v 0.625 1.125 0.0625 +v 0.5 1.625 0.0625 +v 0.5 1.625 0.1875 +v 0.5 1.125 0.0625 +v 0.5 1.125 0.1875 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o upperBodyParts2 +v -0.0625 1.625 0.625 +v -0.0625 1.625 0.5 +v -0.0625 1.125 0.625 +v -0.0625 1.125 0.5 +v -0.1875 1.625 0.5 +v -0.1875 1.625 0.625 +v -0.1875 1.125 0.5 +v -0.1875 1.125 0.625 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o upperBodyParts3 +v 0.1875 1.625 -0.5 +v 0.1875 1.625 -0.625 +v 0.1875 1.125 -0.5 +v 0.1875 1.125 -0.625 +v 0.0625 1.625 -0.625 +v 0.0625 1.625 -0.5 +v 0.0625 1.125 -0.625 +v 0.0625 1.125 -0.5 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o upperBodyParts4 +v -0.3125 1.125 0.0625 +v -0.3125 1.125 -0.0625 +v -0.3125 0.625 0.0625 +v -0.3125 0.625 -0.0625 +v -0.4375 1.125 -0.0625 +v -0.4375 1.125 0.0625 +v -0.4375 0.625 -0.0625 +v -0.4375 0.625 0.0625 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o upperBodyParts5 +v 0.4375 1.125 0.0625 +v 0.4375 1.125 -0.0625 +v 0.4375 0.625 0.0625 +v 0.4375 0.625 -0.0625 +v 0.3125 1.125 -0.0625 +v 0.3125 1.125 0.0625 +v 0.3125 0.625 -0.0625 +v 0.3125 0.625 0.0625 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o upperBodyParts6 +v 0.0625 1.125 0.4375 +v 0.0625 1.125 0.3125 +v 0.0625 0.625 0.4375 +v 0.0625 0.625 0.3125 +v -0.0625 1.125 0.3125 +v -0.0625 1.125 0.4375 +v -0.0625 0.625 0.3125 +v -0.0625 0.625 0.4375 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o upperBodyParts7 +v 0.0625 1.125 -0.3125 +v 0.0625 1.125 -0.4375 +v 0.0625 0.625 -0.3125 +v 0.0625 0.625 -0.4375 +v -0.0625 1.125 -0.4375 +v -0.0625 1.125 -0.3125 +v -0.0625 0.625 -0.4375 +v -0.0625 0.625 -0.3125 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o upperBodyParts8 +v -0.1875 0.5 0.25 +v -0.1875 0.5 0.125 +v -0.1875 0 0.25 +v -0.1875 0 0.125 +v -0.3125 0.5 0.125 +v -0.3125 0.5 0.25 +v -0.3125 0 0.125 +v -0.3125 0 0.25 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o upperBodyParts9 +v 0.3125 0.5 -0.125 +v 0.3125 0.5 -0.25 +v 0.3125 0 -0.125 +v 0.3125 0 -0.25 +v 0.1875 0.5 -0.25 +v 0.1875 0.5 -0.125 +v 0.1875 0 -0.25 +v 0.1875 0 -0.125 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o upperBodyParts10 +v 0.25 0.5 0.3125 +v 0.25 0.5 0.1875 +v 0.25 0 0.3125 +v 0.25 0 0.1875 +v 0.125 0.5 0.1875 +v 0.125 0.5 0.3125 +v 0.125 0 0.1875 +v 0.125 0 0.3125 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o upperBodyParts11 +v -0.125 0.5 -0.1875 +v -0.125 0.5 -0.3125 +v -0.125 0 -0.1875 +v -0.125 0 -0.3125 +v -0.25 0.5 -0.3125 +v -0.25 0.5 -0.1875 +v -0.25 0 -0.3125 +v -0.25 0 -0.1875 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.1875 +vt 0.03125 0.1875 +vt 0 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.1875 +vt 0 0.1875 +vt 0.09375 0.4375 +vt 0.125 0.4375 +vt 0.125 0.1875 +vt 0.09375 0.1875 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.09375 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.5 +vt 0.0625 0.5 +vt 0.09375 0.5 +vt 0.0625 0.5 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o Head +v 0.25 1.75 0.25 +v 0.25 1.75 -0.25 +v 0.25 1.25 0.25 +v 0.25 1.25 -0.25 +v -0.25 1.75 -0.25 +v -0.25 1.75 0.25 +v -0.25 1.25 -0.25 +v -0.25 1.25 0.25 +vt 0.125 0.75 +vt 0.25 0.75 +vt 0.25 0.5 +vt 0.125 0.5 +vt 0 0.75 +vt 0.125 0.75 +vt 0.125 0.5 +vt 0 0.5 +vt 0.375 0.75 +vt 0.5 0.75 +vt 0.5 0.5 +vt 0.375 0.5 +vt 0.25 0.75 +vt 0.375 0.75 +vt 0.375 0.5 +vt 0.25 0.5 +vt 0.25 0.75 +vt 0.125 0.75 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.75 +vt 0.375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e49346a5-14a5-1082-ba30-5d05bc57359f +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/boat.obj b/renderer/viewer/three/entity/models/boat.obj new file mode 100644 index 00000000..72b2ca2a --- /dev/null +++ b/renderer/viewer/three/entity/models/boat.obj @@ -0,0 +1,417 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o bottom +v 0.875 1.3125000000000002 -0.5 +v 0.875 1.1250000000000002 -0.5 +v 0.875 1.3125 0.5 +v 0.875 1.125 0.5 +v -0.875 1.1250000000000002 -0.5 +v -0.875 1.3125000000000002 -0.5 +v -0.875 1.125 0.5 +v -0.875 1.3125 0.5 +vt 0.2421875 0.953125 +vt 0.0234375 0.953125 +vt 0.0234375 0.703125 +vt 0.2421875 0.703125 +vt 0.265625 0.953125 +vt 0.2421875 0.953125 +vt 0.2421875 0.703125 +vt 0.265625 0.703125 +vt 0.484375 0.953125 +vt 0.265625 0.953125 +vt 0.265625 0.703125 +vt 0.484375 0.703125 +vt 0.0234375 0.953125 +vt 0 0.953125 +vt 0 0.703125 +vt 0.0234375 0.703125 +vt 0.0234375 0.953125 +vt 0.2421875 0.953125 +vt 0.2421875 1 +vt 0.0234375 1 +vt 0.2421875 1 +vt 0.4609375 1 +vt 0.4609375 0.953125 +vt 0.2421875 0.953125 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o front +v -0.9999999999999999 1.6875 0.5 +v -0.8749999999999999 1.6875 0.5 +v -0.9999999999999999 1.3125 0.5 +v -0.8749999999999999 1.3125 0.5 +v -0.8750000000000001 1.6875 -0.5 +v -1 1.6875 -0.5 +v -0.8750000000000001 1.3125 -0.5 +v -1 1.3125 -0.5 +vt 0.140625 0.546875 +vt 0.015625 0.546875 +vt 0.015625 0.453125 +vt 0.140625 0.453125 +vt 0.15625 0.546875 +vt 0.140625 0.546875 +vt 0.140625 0.453125 +vt 0.15625 0.453125 +vt 0.28125 0.546875 +vt 0.15625 0.546875 +vt 0.15625 0.453125 +vt 0.28125 0.453125 +vt 0.015625 0.546875 +vt 0 0.546875 +vt 0 0.453125 +vt 0.015625 0.453125 +vt 0.015625 0.546875 +vt 0.140625 0.546875 +vt 0.140625 0.578125 +vt 0.015625 0.578125 +vt 0.140625 0.578125 +vt 0.265625 0.578125 +vt 0.265625 0.546875 +vt 0.140625 0.546875 +vn 1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 1 +vn -1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 -1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o back +v 1 1.6875 -0.5625 +v 0.875 1.6875 -0.5625 +v 1 1.3125 -0.5625 +v 0.875 1.3125 -0.5625 +v 0.875 1.6875 0.5625 +v 1 1.6875 0.5625 +v 0.875 1.3125 0.5625 +v 1 1.3125 0.5625 +vt 0.15625 0.671875 +vt 0.015625 0.671875 +vt 0.015625 0.578125 +vt 0.15625 0.578125 +vt 0.171875 0.671875 +vt 0.15625 0.671875 +vt 0.15625 0.578125 +vt 0.171875 0.578125 +vt 0.3125 0.671875 +vt 0.171875 0.671875 +vt 0.171875 0.578125 +vt 0.3125 0.578125 +vt 0.015625 0.671875 +vt 0 0.671875 +vt 0 0.578125 +vt 0.015625 0.578125 +vt 0.015625 0.671875 +vt 0.15625 0.671875 +vt 0.15625 0.703125 +vt 0.015625 0.703125 +vt 0.15625 0.703125 +vt 0.296875 0.703125 +vt 0.296875 0.671875 +vt 0.15625 0.671875 +vn -1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 -1 +vn 1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o right +v -0.875 1.6875 -0.6250000000000001 +v -0.875 1.6875 -0.5000000000000001 +v -0.875 1.3125 -0.6250000000000001 +v -0.875 1.3125 -0.5000000000000001 +v 0.875 1.6875 -0.4999999999999999 +v 0.875 1.6875 -0.6249999999999999 +v 0.875 1.3125 -0.4999999999999999 +v 0.875 1.3125 -0.6249999999999999 +vt 0.234375 0.421875 +vt 0.015625 0.421875 +vt 0.015625 0.328125 +vt 0.234375 0.328125 +vt 0.25 0.421875 +vt 0.234375 0.421875 +vt 0.234375 0.328125 +vt 0.25 0.328125 +vt 0.46875 0.421875 +vt 0.25 0.421875 +vt 0.25 0.328125 +vt 0.46875 0.328125 +vt 0.015625 0.421875 +vt 0 0.421875 +vt 0 0.328125 +vt 0.015625 0.328125 +vt 0.015625 0.421875 +vt 0.234375 0.421875 +vt 0.234375 0.453125 +vt 0.015625 0.453125 +vt 0.234375 0.453125 +vt 0.453125 0.453125 +vt 0.453125 0.421875 +vt 0.234375 0.421875 +vn -1.2246467991473532e-16 0 1 +vn -1 0 -1.2246467991473532e-16 +vn 1.2246467991473532e-16 0 -1 +vn 1 0 1.2246467991473532e-16 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o left +v 0.875 1.6875 0.625 +v 0.875 1.6875 0.5 +v 0.875 1.3125 0.625 +v 0.875 1.3125 0.5 +v -0.875 1.6875 0.5 +v -0.875 1.6875 0.625 +v -0.875 1.3125 0.5 +v -0.875 1.3125 0.625 +vt 0.234375 0.296875 +vt 0.015625 0.296875 +vt 0.015625 0.203125 +vt 0.234375 0.203125 +vt 0.25 0.296875 +vt 0.234375 0.296875 +vt 0.234375 0.203125 +vt 0.25 0.203125 +vt 0.46875 0.296875 +vt 0.25 0.296875 +vt 0.25 0.203125 +vt 0.46875 0.203125 +vt 0.015625 0.296875 +vt 0 0.296875 +vt 0 0.203125 +vt 0.015625 0.203125 +vt 0.015625 0.296875 +vt 0.234375 0.296875 +vt 0.234375 0.328125 +vt 0.015625 0.328125 +vt 0.234375 0.328125 +vt 0.453125 0.328125 +vt 0.453125 0.296875 +vt 0.234375 0.296875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o paddle_left +v 0.21875 1.4135015877365276 1.2703323467065926 +v 0.21875 1.9760015877365271 0.29605376744909906 +v 0.21875 1.3052484122634724 1.2078323467065926 +v 0.21875 1.8677484122634724 0.23355376744909906 +v 0.09375 1.9760015877365271 0.29605376744909906 +v 0.09375 1.4135015877365276 1.2703323467065926 +v 0.09375 1.8677484122634724 0.23355376744909906 +v 0.09375 1.3052484122634724 1.2078323467065926 +vt 0.640625 0.71875 +vt 0.625 0.71875 +vt 0.625 0.6875 +vt 0.640625 0.6875 +vt 0.78125 0.71875 +vt 0.640625 0.71875 +vt 0.640625 0.6875 +vt 0.78125 0.6875 +vt 0.796875 0.71875 +vt 0.78125 0.71875 +vt 0.78125 0.6875 +vt 0.796875 0.6875 +vt 0.625 0.71875 +vt 0.484375 0.71875 +vt 0.484375 0.6875 +vt 0.625 0.6875 +vt 0.625 0.71875 +vt 0.640625 0.71875 +vt 0.640625 1 +vt 0.625 1 +vt 0.640625 1 +vt 0.65625 1 +vt 0.65625 0.71875 +vt 0.640625 0.71875 +vn 0 0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 -0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 0.49999999999999994 +vn 0 -0.8660254037844387 -0.49999999999999994 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o paddle_left +v 0.156875 1.4821313509461098 1.5264621099161748 +v 0.156875 1.7008813509461098 1.147575995760483 +v 0.156875 1.1573718245269453 1.3389621099161748 +v 0.156875 1.3761218245269453 0.960075995760483 +v 0.09437499999999999 1.7008813509461098 1.147575995760483 +v 0.09437499999999999 1.4821313509461098 1.5264621099161748 +v 0.09437499999999999 1.3761218245269453 0.960075995760483 +v 0.09437499999999999 1.1573718245269453 1.3389621099161748 +vt 0.546875 0.890625 +vt 0.5390625 0.890625 +vt 0.5390625 0.796875 +vt 0.546875 0.796875 +vt 0.6015625 0.890625 +vt 0.546875 0.890625 +vt 0.546875 0.796875 +vt 0.6015625 0.796875 +vt 0.609375 0.890625 +vt 0.6015625 0.890625 +vt 0.6015625 0.796875 +vt 0.609375 0.796875 +vt 0.5390625 0.890625 +vt 0.484375 0.890625 +vt 0.484375 0.796875 +vt 0.5390625 0.796875 +vt 0.5390625 0.890625 +vt 0.546875 0.890625 +vt 0.546875 1 +vt 0.5390625 1 +vt 0.546875 1 +vt 0.5546875 1 +vt 0.5546875 0.890625 +vt 0.546875 0.890625 +vn 0 0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 -0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 0.49999999999999994 +vn 0 -0.8660254037844387 -0.49999999999999994 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o paddle_right +v 0.09374999999999989 1.4135015877365273 -1.2703323467065928 +v 0.09375 1.9760015877365271 -0.29605376744909917 +v 0.09374999999999989 1.3052484122634724 -1.2078323467065928 +v 0.09375 1.8677484122634724 -0.23355376744909917 +v 0.21875 1.9760015877365271 -0.29605376744909917 +v 0.2187499999999999 1.4135015877365273 -1.2703323467065928 +v 0.21875 1.8677484122634724 -0.23355376744909917 +v 0.21875 1.3052484122634724 -1.2078323467065928 +vt 0.640625 0.40625 +vt 0.625 0.40625 +vt 0.625 0.375 +vt 0.640625 0.375 +vt 0.78125 0.40625 +vt 0.640625 0.40625 +vt 0.640625 0.375 +vt 0.78125 0.375 +vt 0.796875 0.40625 +vt 0.78125 0.40625 +vt 0.78125 0.375 +vt 0.796875 0.375 +vt 0.625 0.40625 +vt 0.484375 0.40625 +vt 0.484375 0.375 +vt 0.625 0.375 +vt 0.625 0.40625 +vt 0.640625 0.40625 +vt 0.640625 0.6875 +vt 0.625 0.6875 +vt 0.640625 0.6875 +vt 0.65625 0.6875 +vt 0.65625 0.40625 +vt 0.640625 0.40625 +vn 1.0605752387249069e-16 0.49999999999999994 0.8660254037844387 +vn -1 -1.848892746611746e-32 1.224646799147353e-16 +vn -1.0605752387249069e-16 -0.49999999999999994 -0.8660254037844387 +vn 1 1.848892746611746e-32 -1.224646799147353e-16 +vn -6.123233995736765e-17 0.8660254037844388 -0.49999999999999994 +vn 6.123233995736765e-17 -0.8660254037844388 0.49999999999999994 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o paddle_right +v 0.09437499999999988 1.4821313509461096 -1.526462109916175 +v 0.09437499999999988 1.7008813509461094 -1.1475759957604832 +v 0.09437499999999988 1.157371824526945 -1.338962109916175 +v 0.09437499999999988 1.376121824526945 -0.9600759957604832 +v 0.15687499999999988 1.7008813509461094 -1.1475759957604832 +v 0.15687499999999988 1.4821313509461096 -1.526462109916175 +v 0.15687499999999988 1.376121824526945 -0.9600759957604832 +v 0.15687499999999988 1.157371824526945 -1.338962109916175 +vt 0.546875 0.578125 +vt 0.5390625 0.578125 +vt 0.5390625 0.484375 +vt 0.546875 0.484375 +vt 0.6015625 0.578125 +vt 0.546875 0.578125 +vt 0.546875 0.484375 +vt 0.6015625 0.484375 +vt 0.609375 0.578125 +vt 0.6015625 0.578125 +vt 0.6015625 0.484375 +vt 0.609375 0.484375 +vt 0.5390625 0.578125 +vt 0.484375 0.578125 +vt 0.484375 0.484375 +vt 0.5390625 0.484375 +vt 0.5390625 0.578125 +vt 0.546875 0.578125 +vt 0.546875 0.6875 +vt 0.5390625 0.6875 +vt 0.546875 0.6875 +vt 0.5546875 0.6875 +vt 0.5546875 0.578125 +vt 0.546875 0.578125 +vn 1.0605752387249069e-16 0.49999999999999994 0.8660254037844387 +vn -1 -1.848892746611746e-32 1.224646799147353e-16 +vn -1.0605752387249069e-16 -0.49999999999999994 -0.8660254037844387 +vn 1 1.848892746611746e-32 -1.224646799147353e-16 +vn -6.123233995736765e-17 0.8660254037844388 -0.49999999999999994 +vn 6.123233995736765e-17 -0.8660254037844388 0.49999999999999994 +usemtl m_e12ce080-c1e9-6394-d577-2c59cc23ff2d +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/camel.obj b/renderer/viewer/three/entity/models/camel.obj new file mode 100644 index 00000000..8c8f050a --- /dev/null +++ b/renderer/viewer/three/entity/models/camel.obj @@ -0,0 +1,1061 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.46875 2 0.8125 +v 0.46875 2 -0.875 +v 0.46875 1.25 0.8125 +v 0.46875 1.25 -0.875 +v -0.46875 2 -0.875 +v -0.46875 2 0.8125 +v -0.46875 1.25 -0.875 +v -0.46875 1.25 0.8125 +vt 0.2109375 0.59375 +vt 0.328125 0.59375 +vt 0.328125 0.5 +vt 0.2109375 0.5 +vt 0 0.59375 +vt 0.2109375 0.59375 +vt 0.2109375 0.5 +vt 0 0.5 +vt 0.5390625 0.59375 +vt 0.65625 0.59375 +vt 0.65625 0.5 +vt 0.5390625 0.5 +vt 0.328125 0.59375 +vt 0.5390625 0.59375 +vt 0.5390625 0.5 +vt 0.328125 0.5 +vt 0.328125 0.59375 +vt 0.2109375 0.59375 +vt 0.2109375 0.8046875 +vt 0.328125 0.8046875 +vt 0.4453125 0.8046875 +vt 0.328125 0.8046875 +vt 0.328125 0.59375 +vt 0.4453125 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o saddle layer +v 0.2875 2.31875 0.31875 +v 0.2875 2.31875 -0.38125 +v 0.2875 1.99375 0.31875 +v 0.2875 1.99375 -0.38125 +v -0.2875 2.31875 -0.38125 +v -0.2875 2.31875 0.31875 +v -0.2875 1.99375 -0.38125 +v -0.2875 1.99375 0.31875 +vt 0.6640625 0.4140625 +vt 0.734375 0.4140625 +vt 0.734375 0.375 +vt 0.6640625 0.375 +vt 0.578125 0.4140625 +vt 0.6640625 0.4140625 +vt 0.6640625 0.375 +vt 0.578125 0.375 +vt 0.8203125 0.4140625 +vt 0.890625 0.4140625 +vt 0.890625 0.375 +vt 0.8203125 0.375 +vt 0.734375 0.4140625 +vt 0.8203125 0.4140625 +vt 0.8203125 0.375 +vt 0.734375 0.375 +vt 0.734375 0.4140625 +vt 0.6640625 0.4140625 +vt 0.6640625 0.5 +vt 0.734375 0.5 +vt 0.8046875 0.5 +vt 0.734375 0.5 +vt 0.734375 0.4140625 +vt 0.8046875 0.4140625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o saddle layer +v 0.22499999999999998 2.50625 0.31875 +v 0.22499999999999998 2.50625 -0.38125 +v 0.22499999999999998 2.30625 0.31875 +v 0.22499999999999998 2.30625 -0.38125 +v -0.22499999999999998 2.50625 -0.38125 +v -0.22499999999999998 2.50625 0.31875 +v -0.22499999999999998 2.30625 -0.38125 +v -0.22499999999999998 2.30625 0.31875 +vt 0.8046875 0.0234375 +vt 0.859375 0.0234375 +vt 0.859375 0 +vt 0.8046875 0 +vt 0.71875 0.0234375 +vt 0.8046875 0.0234375 +vt 0.8046875 0 +vt 0.71875 0 +vt 0.9453125 0.0234375 +vt 1 0.0234375 +vt 1 0 +vt 0.9453125 0 +vt 0.859375 0.0234375 +vt 0.9453125 0.0234375 +vt 0.9453125 0 +vt 0.859375 0 +vt 0.859375 0.0234375 +vt 0.8046875 0.0234375 +vt 0.8046875 0.109375 +vt 0.859375 0.109375 +vt 0.9140625 0.109375 +vt 0.859375 0.109375 +vt 0.859375 0.0234375 +vt 0.9140625 0.0234375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o saddle layer +v 0.475 2.00625 0.8187500000000001 +v 0.475 2.00625 -0.88125 +v 0.475 1.24375 0.8187500000000001 +v 0.475 1.24375 -0.88125 +v -0.475 2.00625 -0.88125 +v -0.475 2.00625 0.8187500000000001 +v -0.475 1.24375 -0.88125 +v -0.475 1.24375 0.8187500000000001 +vt 0.2109375 0.09375 +vt 0.328125 0.09375 +vt 0.328125 0 +vt 0.2109375 0 +vt 0 0.09375 +vt 0.2109375 0.09375 +vt 0.2109375 0 +vt 0 0 +vt 0.5390625 0.09375 +vt 0.65625 0.09375 +vt 0.65625 0 +vt 0.5390625 0 +vt 0.328125 0.09375 +vt 0.5390625 0.09375 +vt 0.5390625 0 +vt 0.328125 0 +vt 0.328125 0.09375 +vt 0.2109375 0.09375 +vt 0.2109375 0.3046875 +vt 0.328125 0.3046875 +vt 0.4453125 0.3046875 +vt 0.328125 0.3046875 +vt 0.328125 0.09375 +vt 0.4453125 0.09375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o tail +v -0.09375 1.8125 0.8125 +v -0.09375 1.8125 0.8125 +v -0.09375 0.9375 0.8125 +v -0.09375 0.9375 0.8125 +v 0.09375 1.8125 0.8125 +v 0.09375 1.8125 0.8125 +v 0.09375 0.9375 0.8125 +v 0.09375 0.9375 0.8125 +vt 0.953125 1 +vt 0.9765625 1 +vt 0.9765625 0.890625 +vt 0.953125 0.890625 +vt 0.953125 1 +vt 0.953125 1 +vt 0.953125 0.890625 +vt 0.953125 0.890625 +vt 0.9765625 1 +vt 1 1 +vt 1 0.890625 +vt 0.9765625 0.890625 +vt 0.9765625 1 +vt 0.9765625 1 +vt 0.9765625 0.890625 +vt 0.9765625 0.890625 +vt 0.9765625 1 +vt 0.953125 1 +vt 0.953125 1 +vt 0.9765625 1 +vt 1 1 +vt 0.9765625 1 +vt 0.9765625 1 +vt 1 1 +vn 1.2246467991473532e-16 0 1 +vn -1 0 1.2246467991473532e-16 +vn -1.2246467991473532e-16 0 -1 +vn 1 0 -1.2246467991473532e-16 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o head +v 0.21875 1.875 -0.375 +v 0.21875 1.875 -1.5625 +v 0.21875 1.375 -0.375 +v 0.21875 1.375 -1.5625 +v -0.21875 1.875 -1.5625 +v -0.21875 1.875 -0.375 +v -0.21875 1.375 -1.5625 +v -0.21875 1.375 -0.375 +vt 0.6171875 0.6640625 +vt 0.671875 0.6640625 +vt 0.671875 0.6015625 +vt 0.6171875 0.6015625 +vt 0.46875 0.6640625 +vt 0.6171875 0.6640625 +vt 0.6171875 0.6015625 +vt 0.46875 0.6015625 +vt 0.8203125 0.6640625 +vt 0.875 0.6640625 +vt 0.875 0.6015625 +vt 0.8203125 0.6015625 +vt 0.671875 0.6640625 +vt 0.8203125 0.6640625 +vt 0.8203125 0.6015625 +vt 0.671875 0.6015625 +vt 0.671875 0.6640625 +vt 0.6171875 0.6640625 +vt 0.6171875 0.8125 +vt 0.671875 0.8125 +vt 0.7265625 0.8125 +vt 0.671875 0.8125 +vt 0.671875 0.6640625 +vt 0.7265625 0.6640625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o head +v 0.21875 2.75 -1.125 +v 0.21875 2.75 -1.5625 +v 0.21875 1.875 -1.125 +v 0.21875 1.875 -1.5625 +v -0.21875 2.75 -1.5625 +v -0.21875 2.75 -1.125 +v -0.21875 1.875 -1.5625 +v -0.21875 1.875 -1.125 +vt 0.21875 0.9453125 +vt 0.2734375 0.9453125 +vt 0.2734375 0.8359375 +vt 0.21875 0.8359375 +vt 0.1640625 0.9453125 +vt 0.21875 0.9453125 +vt 0.21875 0.8359375 +vt 0.1640625 0.8359375 +vt 0.328125 0.9453125 +vt 0.3828125 0.9453125 +vt 0.3828125 0.8359375 +vt 0.328125 0.8359375 +vt 0.2734375 0.9453125 +vt 0.328125 0.9453125 +vt 0.328125 0.8359375 +vt 0.2734375 0.8359375 +vt 0.2734375 0.9453125 +vt 0.21875 0.9453125 +vt 0.21875 1 +vt 0.2734375 1 +vt 0.328125 1 +vt 0.2734375 1 +vt 0.2734375 0.9453125 +vt 0.328125 0.9453125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o head +v 0.15625 2.75 -1.5625 +v 0.15625 2.75 -1.9375 +v 0.15625 2.4375 -1.5625 +v 0.15625 2.4375 -1.9375 +v -0.15625 2.75 -1.9375 +v -0.15625 2.75 -1.5625 +v -0.15625 2.4375 -1.9375 +v -0.15625 2.4375 -1.5625 +vt 0.4375 0.953125 +vt 0.4765625 0.953125 +vt 0.4765625 0.9140625 +vt 0.4375 0.9140625 +vt 0.390625 0.953125 +vt 0.4375 0.953125 +vt 0.4375 0.9140625 +vt 0.390625 0.9140625 +vt 0.5234375 0.953125 +vt 0.5625 0.953125 +vt 0.5625 0.9140625 +vt 0.5234375 0.9140625 +vt 0.4765625 0.953125 +vt 0.5234375 0.953125 +vt 0.5234375 0.9140625 +vt 0.4765625 0.9140625 +vt 0.4765625 0.953125 +vt 0.4375 0.953125 +vt 0.4375 1 +vt 0.4765625 1 +vt 0.515625 1 +vt 0.4765625 1 +vt 0.4765625 0.953125 +vt 0.515625 0.953125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o bridle layer +v 0.22499999999999998 1.88125 -0.36875 +v 0.22499999999999998 1.88125 -1.56875 +v 0.22499999999999998 1.36875 -0.36875 +v 0.22499999999999998 1.36875 -1.56875 +v -0.22499999999999998 1.88125 -1.56875 +v -0.22499999999999998 1.88125 -0.36875 +v -0.22499999999999998 1.36875 -1.56875 +v -0.22499999999999998 1.36875 -0.36875 +vt 0.6171875 0.171875 +vt 0.671875 0.171875 +vt 0.671875 0.109375 +vt 0.6171875 0.109375 +vt 0.46875 0.171875 +vt 0.6171875 0.171875 +vt 0.6171875 0.109375 +vt 0.46875 0.109375 +vt 0.8203125 0.171875 +vt 0.875 0.171875 +vt 0.875 0.109375 +vt 0.8203125 0.109375 +vt 0.671875 0.171875 +vt 0.8203125 0.171875 +vt 0.8203125 0.109375 +vt 0.671875 0.109375 +vt 0.671875 0.171875 +vt 0.6171875 0.171875 +vt 0.6171875 0.3203125 +vt 0.671875 0.3203125 +vt 0.7265625 0.3203125 +vt 0.671875 0.3203125 +vt 0.671875 0.171875 +vt 0.7265625 0.171875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o bridle layer +v 0.22499999999999998 2.75625 -1.11875 +v 0.22499999999999998 2.75625 -1.56875 +v 0.22499999999999998 1.86875 -1.11875 +v 0.22499999999999998 1.86875 -1.56875 +v -0.22499999999999998 2.75625 -1.56875 +v -0.22499999999999998 2.75625 -1.11875 +v -0.22499999999999998 1.86875 -1.56875 +v -0.22499999999999998 1.86875 -1.11875 +vt 0.21875 0.4453125 +vt 0.2734375 0.4453125 +vt 0.2734375 0.3359375 +vt 0.21875 0.3359375 +vt 0.1640625 0.4453125 +vt 0.21875 0.4453125 +vt 0.21875 0.3359375 +vt 0.1640625 0.3359375 +vt 0.328125 0.4453125 +vt 0.3828125 0.4453125 +vt 0.3828125 0.3359375 +vt 0.328125 0.3359375 +vt 0.2734375 0.4453125 +vt 0.328125 0.4453125 +vt 0.328125 0.3359375 +vt 0.2734375 0.3359375 +vt 0.2734375 0.4453125 +vt 0.21875 0.4453125 +vt 0.21875 0.5 +vt 0.2734375 0.5 +vt 0.328125 0.5 +vt 0.2734375 0.5 +vt 0.2734375 0.4453125 +vt 0.328125 0.4453125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o bridle layer +v 0.16249999999999998 2.75625 -1.5625 +v 0.16249999999999998 2.75625 -1.9500000000000002 +v 0.16249999999999998 2.43125 -1.5625 +v 0.16249999999999998 2.43125 -1.9500000000000002 +v -0.16249999999999998 2.75625 -1.9500000000000002 +v -0.16249999999999998 2.75625 -1.5625 +v -0.16249999999999998 2.43125 -1.9500000000000002 +v -0.16249999999999998 2.43125 -1.5625 +vt 0.4375 0.453125 +vt 0.4765625 0.453125 +vt 0.4765625 0.4140625 +vt 0.4375 0.4140625 +vt 0.390625 0.453125 +vt 0.4375 0.453125 +vt 0.4375 0.4140625 +vt 0.390625 0.4140625 +vt 0.5234375 0.453125 +vt 0.5625 0.453125 +vt 0.5625 0.4140625 +vt 0.5234375 0.4140625 +vt 0.4765625 0.453125 +vt 0.5234375 0.453125 +vt 0.5234375 0.4140625 +vt 0.4765625 0.4140625 +vt 0.4765625 0.453125 +vt 0.4375 0.453125 +vt 0.4375 0.5 +vt 0.4765625 0.5 +vt 0.515625 0.5 +vt 0.4765625 0.5 +vt 0.4765625 0.453125 +vt 0.515625 0.453125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o bridle +v -0.15625 2.625 -1.625 +v -0.15625 2.625 -1.75 +v -0.15625 2.5 -1.625 +v -0.15625 2.5 -1.75 +v -0.21875 2.625 -1.75 +v -0.21875 2.625 -1.625 +v -0.21875 2.5 -1.75 +v -0.21875 2.5 -1.625 +vt 0.59375 0.4375 +vt 0.6015625 0.4375 +vt 0.6015625 0.421875 +vt 0.59375 0.421875 +vt 0.578125 0.4375 +vt 0.59375 0.4375 +vt 0.59375 0.421875 +vt 0.578125 0.421875 +vt 0.6171875 0.4375 +vt 0.625 0.4375 +vt 0.625 0.421875 +vt 0.6171875 0.421875 +vt 0.6015625 0.4375 +vt 0.6171875 0.4375 +vt 0.6171875 0.421875 +vt 0.6015625 0.421875 +vt 0.6015625 0.4375 +vt 0.59375 0.4375 +vt 0.59375 0.453125 +vt 0.6015625 0.453125 +vt 0.609375 0.453125 +vt 0.6015625 0.453125 +vt 0.6015625 0.4375 +vt 0.609375 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o bridle +v 0.21875 2.625 -1.625 +v 0.21875 2.625 -1.75 +v 0.21875 2.5 -1.625 +v 0.21875 2.5 -1.75 +v 0.15625 2.625 -1.75 +v 0.15625 2.625 -1.625 +v 0.15625 2.5 -1.75 +v 0.15625 2.5 -1.625 +vt 0.6015625 0.4375 +vt 0.59375 0.4375 +vt 0.59375 0.421875 +vt 0.6015625 0.421875 +vt 0.6171875 0.4375 +vt 0.6015625 0.4375 +vt 0.6015625 0.421875 +vt 0.6171875 0.421875 +vt 0.625 0.4375 +vt 0.6171875 0.4375 +vt 0.6171875 0.421875 +vt 0.625 0.421875 +vt 0.59375 0.4375 +vt 0.578125 0.4375 +vt 0.578125 0.421875 +vt 0.59375 0.421875 +vt 0.59375 0.4375 +vt 0.6015625 0.4375 +vt 0.6015625 0.453125 +vt 0.59375 0.453125 +vt 0.6015625 0.453125 +vt 0.609375 0.453125 +vt 0.609375 0.4375 +vt 0.6015625 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o left_ear +v -0.1875 2.71875 -1.15625 +v -0.1875 2.71875 -1.28125 +v -0.1875 2.65625 -1.15625 +v -0.1875 2.65625 -1.28125 +v -0.375 2.71875 -1.28125 +v -0.375 2.71875 -1.15625 +v -0.375 2.65625 -1.28125 +v -0.375 2.65625 -1.15625 +vt 0.3671875 0.984375 +vt 0.390625 0.984375 +vt 0.390625 0.9765625 +vt 0.3671875 0.9765625 +vt 0.3515625 0.984375 +vt 0.3671875 0.984375 +vt 0.3671875 0.9765625 +vt 0.3515625 0.9765625 +vt 0.40625 0.984375 +vt 0.4296875 0.984375 +vt 0.4296875 0.9765625 +vt 0.40625 0.9765625 +vt 0.390625 0.984375 +vt 0.40625 0.984375 +vt 0.40625 0.9765625 +vt 0.390625 0.9765625 +vt 0.390625 0.984375 +vt 0.3671875 0.984375 +vt 0.3671875 1 +vt 0.390625 1 +vt 0.4140625 1 +vt 0.390625 1 +vt 0.390625 0.984375 +vt 0.4140625 0.984375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o right_ear +v 0.375 2.71875 -1.15625 +v 0.375 2.71875 -1.28125 +v 0.375 2.65625 -1.15625 +v 0.375 2.65625 -1.28125 +v 0.1875 2.71875 -1.28125 +v 0.1875 2.71875 -1.15625 +v 0.1875 2.65625 -1.28125 +v 0.1875 2.65625 -1.15625 +vt 0.5390625 0.984375 +vt 0.5625 0.984375 +vt 0.5625 0.9765625 +vt 0.5390625 0.9765625 +vt 0.5234375 0.984375 +vt 0.5390625 0.984375 +vt 0.5390625 0.9765625 +vt 0.5234375 0.9765625 +vt 0.578125 0.984375 +vt 0.6015625 0.984375 +vt 0.6015625 0.9765625 +vt 0.578125 0.9765625 +vt 0.5625 0.984375 +vt 0.578125 0.984375 +vt 0.578125 0.9765625 +vt 0.5625 0.9765625 +vt 0.5625 0.984375 +vt 0.5390625 0.984375 +vt 0.5390625 1 +vt 0.5625 1 +vt 0.5859375 1 +vt 0.5625 1 +vt 0.5625 0.984375 +vt 0.5859375 0.984375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 +o reins layer +v -0.23125 2.5625 -0.75 +v -0.23125 2.5625 -1.6875 +v -0.23125 2.125 -0.75 +v -0.23125 2.125 -1.6875 +v -0.23125 2.5625 -1.6875 +v -0.23125 2.5625 -0.75 +v -0.23125 2.125 -1.6875 +v -0.23125 2.125 -0.75 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5 +vt 0.8828125 0.5 +vt 0.765625 0.5546875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5 +vt 0.765625 0.5 +vt 1 0.5546875 +vt 1 0.5546875 +vt 1 0.5 +vt 1 0.5 +vt 0.8828125 0.5546875 +vt 1 0.5546875 +vt 1 0.5 +vt 0.8828125 0.5 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.671875 +vt 0.8828125 0.671875 +vt 0.8828125 0.671875 +vt 0.8828125 0.671875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5546875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 124/364/91 127/363/91 125/362/91 122/361/91 +f 123/368/92 124/367/92 122/366/92 121/365/92 +f 128/372/93 123/371/93 121/370/93 126/369/93 +f 127/376/94 128/375/94 126/374/94 125/373/94 +f 126/380/95 121/379/95 122/378/95 125/377/95 +f 127/384/96 124/383/96 123/382/96 128/381/96 +o reins layer +v 0.23124999999999996 2.5625 -0.75 +v 0.23124999999999996 2.5625 -0.75 +v 0.23124999999999996 2.125 -0.75 +v 0.23124999999999996 2.125 -0.75 +v -0.23125 2.5625 -0.75 +v -0.23125 2.5625 -0.75 +v -0.23125 2.125 -0.75 +v -0.23125 2.125 -0.75 +vt 0.65625 0.5546875 +vt 0.7109375 0.5546875 +vt 0.7109375 0.5 +vt 0.65625 0.5 +vt 0.65625 0.5546875 +vt 0.65625 0.5546875 +vt 0.65625 0.5 +vt 0.65625 0.5 +vt 0.7109375 0.5546875 +vt 0.765625 0.5546875 +vt 0.765625 0.5 +vt 0.7109375 0.5 +vt 0.7109375 0.5546875 +vt 0.7109375 0.5546875 +vt 0.7109375 0.5 +vt 0.7109375 0.5 +vt 0.7109375 0.5546875 +vt 0.65625 0.5546875 +vt 0.65625 0.5546875 +vt 0.7109375 0.5546875 +vt 0.765625 0.5546875 +vt 0.7109375 0.5546875 +vt 0.7109375 0.5546875 +vt 0.765625 0.5546875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 132/388/97 135/387/97 133/386/97 130/385/97 +f 131/392/98 132/391/98 130/390/98 129/389/98 +f 136/396/99 131/395/99 129/394/99 134/393/99 +f 135/400/100 136/399/100 134/398/100 133/397/100 +f 134/404/101 129/403/101 130/402/101 133/401/101 +f 135/408/102 132/407/102 131/406/102 136/405/102 +o reins layer +v 0.23124999999999996 2.5625 -0.75 +v 0.23124999999999996 2.5625 -1.6875 +v 0.23124999999999996 2.125 -0.75 +v 0.23124999999999996 2.125 -1.6875 +v 0.23124999999999996 2.5625 -1.6875 +v 0.23124999999999996 2.5625 -0.75 +v 0.23124999999999996 2.125 -1.6875 +v 0.23124999999999996 2.125 -0.75 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5 +vt 0.8828125 0.5 +vt 0.765625 0.5546875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5 +vt 0.765625 0.5 +vt 1 0.5546875 +vt 1 0.5546875 +vt 1 0.5 +vt 1 0.5 +vt 0.8828125 0.5546875 +vt 1 0.5546875 +vt 1 0.5 +vt 0.8828125 0.5 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.671875 +vt 0.8828125 0.671875 +vt 0.8828125 0.671875 +vt 0.8828125 0.671875 +vt 0.8828125 0.5546875 +vt 0.8828125 0.5546875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 140/412/103 143/411/103 141/410/103 138/409/103 +f 139/416/104 140/415/104 138/414/104 137/413/104 +f 144/420/105 139/419/105 137/418/105 142/417/105 +f 143/424/106 144/423/106 142/422/106 141/421/106 +f 142/428/107 137/427/107 138/426/107 141/425/107 +f 143/432/108 140/431/108 139/430/108 144/429/108 +o hump +v 0.28125 2.3125 0.3125 +v 0.28125 2.3125 -0.375 +v 0.28125 2 0.3125 +v 0.28125 2 -0.375 +v -0.28125 2.3125 -0.375 +v -0.28125 2.3125 0.3125 +v -0.28125 2 -0.375 +v -0.28125 2 0.3125 +vt 0.6640625 0.9140625 +vt 0.734375 0.9140625 +vt 0.734375 0.875 +vt 0.6640625 0.875 +vt 0.578125 0.9140625 +vt 0.6640625 0.9140625 +vt 0.6640625 0.875 +vt 0.578125 0.875 +vt 0.8203125 0.9140625 +vt 0.890625 0.9140625 +vt 0.890625 0.875 +vt 0.8203125 0.875 +vt 0.734375 0.9140625 +vt 0.8203125 0.9140625 +vt 0.8203125 0.875 +vt 0.734375 0.875 +vt 0.734375 0.9140625 +vt 0.6640625 0.9140625 +vt 0.6640625 1 +vt 0.734375 1 +vt 0.8046875 1 +vt 0.734375 1 +vt 0.734375 0.9140625 +vt 0.8046875 0.9140625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 148/436/109 151/435/109 149/434/109 146/433/109 +f 147/440/110 148/439/110 146/438/110 145/437/110 +f 152/444/111 147/443/111 145/442/111 150/441/111 +f 151/448/112 152/447/112 150/446/112 149/445/112 +f 150/452/113 145/451/113 146/450/113 149/449/113 +f 151/456/114 148/455/114 147/454/114 152/453/114 +o right_front_leg +v 0.4625 1.3125 -0.5 +v 0.4625 1.3125 -0.8125 +v 0.4625 0 -0.5 +v 0.4625 0 -0.8125 +v 0.15000000000000002 1.3125 -0.8125 +v 0.15000000000000002 1.3125 -0.5 +v 0.15000000000000002 0 -0.8125 +v 0.15000000000000002 0 -0.5 +vt 0.0390625 0.7578125 +vt 0.078125 0.7578125 +vt 0.078125 0.59375 +vt 0.0390625 0.59375 +vt 0 0.7578125 +vt 0.0390625 0.7578125 +vt 0.0390625 0.59375 +vt 0 0.59375 +vt 0.1171875 0.7578125 +vt 0.15625 0.7578125 +vt 0.15625 0.59375 +vt 0.1171875 0.59375 +vt 0.078125 0.7578125 +vt 0.1171875 0.7578125 +vt 0.1171875 0.59375 +vt 0.078125 0.59375 +vt 0.078125 0.7578125 +vt 0.0390625 0.7578125 +vt 0.0390625 0.796875 +vt 0.078125 0.796875 +vt 0.1171875 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.7578125 +vt 0.1171875 0.7578125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 156/460/115 159/459/115 157/458/115 154/457/115 +f 155/464/116 156/463/116 154/462/116 153/461/116 +f 160/468/117 155/467/117 153/466/117 158/465/117 +f 159/472/118 160/471/118 158/470/118 157/469/118 +f 158/476/119 153/475/119 154/474/119 157/473/119 +f 159/480/120 156/479/120 155/478/120 160/477/120 +o left_front_leg +v -0.15000000000000002 1.3125 -0.5 +v -0.15000000000000002 1.3125 -0.8125 +v -0.15000000000000002 0 -0.5 +v -0.15000000000000002 0 -0.8125 +v -0.4625 1.3125 -0.8125 +v -0.4625 1.3125 -0.5 +v -0.4625 0 -0.8125 +v -0.4625 0 -0.5 +vt 0.0390625 0.9609375 +vt 0.078125 0.9609375 +vt 0.078125 0.796875 +vt 0.0390625 0.796875 +vt 0 0.9609375 +vt 0.0390625 0.9609375 +vt 0.0390625 0.796875 +vt 0 0.796875 +vt 0.1171875 0.9609375 +vt 0.15625 0.9609375 +vt 0.15625 0.796875 +vt 0.1171875 0.796875 +vt 0.078125 0.9609375 +vt 0.1171875 0.9609375 +vt 0.1171875 0.796875 +vt 0.078125 0.796875 +vt 0.078125 0.9609375 +vt 0.0390625 0.9609375 +vt 0.0390625 1 +vt 0.078125 1 +vt 0.1171875 1 +vt 0.078125 1 +vt 0.078125 0.9609375 +vt 0.1171875 0.9609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 164/484/121 167/483/121 165/482/121 162/481/121 +f 163/488/122 164/487/122 162/486/122 161/485/122 +f 168/492/123 163/491/123 161/490/123 166/489/123 +f 167/496/124 168/495/124 166/494/124 165/493/124 +f 166/500/125 161/499/125 162/498/125 165/497/125 +f 167/504/126 164/503/126 163/502/126 168/501/126 +o left_hind_leg +v -0.15000000000000002 1.3125 0.75 +v -0.15000000000000002 1.3125 0.4375 +v -0.15000000000000002 0 0.75 +v -0.15000000000000002 0 0.4375 +v -0.4625 1.3125 0.4375 +v -0.4625 1.3125 0.75 +v -0.4625 0 0.4375 +v -0.4625 0 0.75 +vt 0.4921875 0.8359375 +vt 0.53125 0.8359375 +vt 0.53125 0.671875 +vt 0.4921875 0.671875 +vt 0.453125 0.8359375 +vt 0.4921875 0.8359375 +vt 0.4921875 0.671875 +vt 0.453125 0.671875 +vt 0.5703125 0.8359375 +vt 0.609375 0.8359375 +vt 0.609375 0.671875 +vt 0.5703125 0.671875 +vt 0.53125 0.8359375 +vt 0.5703125 0.8359375 +vt 0.5703125 0.671875 +vt 0.53125 0.671875 +vt 0.53125 0.8359375 +vt 0.4921875 0.8359375 +vt 0.4921875 0.875 +vt 0.53125 0.875 +vt 0.5703125 0.875 +vt 0.53125 0.875 +vt 0.53125 0.8359375 +vt 0.5703125 0.8359375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 172/508/127 175/507/127 173/506/127 170/505/127 +f 171/512/128 172/511/128 170/510/128 169/509/128 +f 176/516/129 171/515/129 169/514/129 174/513/129 +f 175/520/130 176/519/130 174/518/130 173/517/130 +f 174/524/131 169/523/131 170/522/131 173/521/131 +f 175/528/132 172/527/132 171/526/132 176/525/132 +o right_hind_leg +v 0.4625 1.3125 0.75 +v 0.4625 1.3125 0.4375 +v 0.4625 0 0.75 +v 0.4625 0 0.4375 +v 0.15000000000000002 1.3125 0.4375 +v 0.15000000000000002 1.3125 0.75 +v 0.15000000000000002 0 0.4375 +v 0.15000000000000002 0 0.75 +vt 0.7734375 0.8359375 +vt 0.8125 0.8359375 +vt 0.8125 0.671875 +vt 0.7734375 0.671875 +vt 0.734375 0.8359375 +vt 0.7734375 0.8359375 +vt 0.7734375 0.671875 +vt 0.734375 0.671875 +vt 0.8515625 0.8359375 +vt 0.890625 0.8359375 +vt 0.890625 0.671875 +vt 0.8515625 0.671875 +vt 0.8125 0.8359375 +vt 0.8515625 0.8359375 +vt 0.8515625 0.671875 +vt 0.8125 0.671875 +vt 0.8125 0.8359375 +vt 0.7734375 0.8359375 +vt 0.7734375 0.875 +vt 0.8125 0.875 +vt 0.8515625 0.875 +vt 0.8125 0.875 +vt 0.8125 0.8359375 +vt 0.8515625 0.8359375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_ef8cbfee-18ba-edcd-78d9-3f4313675f2e +f 180/532/133 183/531/133 181/530/133 178/529/133 +f 179/536/134 180/535/134 178/534/134 177/533/134 +f 184/540/135 179/539/135 177/538/135 182/537/135 +f 183/544/136 184/543/136 182/542/136 181/541/136 +f 182/548/137 177/547/137 178/546/137 181/545/137 +f 183/552/138 180/551/138 179/550/138 184/549/138 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/cat.obj b/renderer/viewer/three/entity/models/cat.obj new file mode 100644 index 00000000..8067b5c6 --- /dev/null +++ b/renderer/viewer/three/entity/models/cat.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.125 0.625 -0.4375 +v 0.125 0.2500000000000001 -0.4375 +v 0.125 0.625 0.5625 +v 0.125 0.2499999999999999 0.5625 +v -0.125 0.2500000000000001 -0.4375 +v -0.125 0.625 -0.4375 +v -0.125 0.2499999999999999 0.5625 +v -0.125 0.625 0.5625 +vt 0.40625 0.8125 +vt 0.46875 0.8125 +vt 0.46875 0.3125 +vt 0.40625 0.3125 +vt 0.3125 0.8125 +vt 0.40625 0.8125 +vt 0.40625 0.3125 +vt 0.3125 0.3125 +vt 0.5625 0.8125 +vt 0.625 0.8125 +vt 0.625 0.3125 +vt 0.5625 0.3125 +vt 0.46875 0.8125 +vt 0.5625 0.8125 +vt 0.5625 0.3125 +vt 0.46875 0.3125 +vt 0.46875 0.8125 +vt 0.40625 0.8125 +vt 0.40625 1 +vt 0.46875 1 +vt 0.53125 1 +vt 0.46875 1 +vt 0.46875 0.8125 +vt 0.53125 0.8125 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.15625 0.6875 -0.4375 +v 0.15625 0.6875 -0.75 +v 0.15625 0.4375 -0.4375 +v 0.15625 0.4375 -0.75 +v -0.15625 0.6875 -0.75 +v -0.15625 0.6875 -0.4375 +v -0.15625 0.4375 -0.75 +v -0.15625 0.4375 -0.4375 +vt 0.078125 0.84375 +vt 0.15625 0.84375 +vt 0.15625 0.71875 +vt 0.078125 0.71875 +vt 0 0.84375 +vt 0.078125 0.84375 +vt 0.078125 0.71875 +vt 0 0.71875 +vt 0.234375 0.84375 +vt 0.3125 0.84375 +vt 0.3125 0.71875 +vt 0.234375 0.71875 +vt 0.15625 0.84375 +vt 0.234375 0.84375 +vt 0.234375 0.71875 +vt 0.15625 0.71875 +vt 0.15625 0.84375 +vt 0.078125 0.84375 +vt 0.078125 1 +vt 0.15625 1 +vt 0.234375 1 +vt 0.15625 1 +vt 0.15625 0.84375 +vt 0.234375 0.84375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.09375 0.5634762499999999 -0.6875 +v 0.09375 0.5634762499999999 -0.8125 +v 0.09375 0.43847625 -0.6875 +v 0.09375 0.43847625 -0.8125 +v -0.09375 0.5634762499999999 -0.8125 +v -0.09375 0.5634762499999999 -0.6875 +v -0.09375 0.43847625 -0.8125 +v -0.09375 0.43847625 -0.6875 +vt 0.03125 0.1875 +vt 0.078125 0.1875 +vt 0.078125 0.125 +vt 0.03125 0.125 +vt 0 0.1875 +vt 0.03125 0.1875 +vt 0.03125 0.125 +vt 0 0.125 +vt 0.109375 0.1875 +vt 0.15625 0.1875 +vt 0.15625 0.125 +vt 0.109375 0.125 +vt 0.078125 0.1875 +vt 0.109375 0.1875 +vt 0.109375 0.125 +vt 0.078125 0.125 +vt 0.078125 0.1875 +vt 0.03125 0.1875 +vt 0.03125 0.25 +vt 0.078125 0.25 +vt 0.125 0.25 +vt 0.078125 0.25 +vt 0.078125 0.1875 +vt 0.125 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.125 0.75 -0.4375 +v 0.125 0.75 -0.5625 +v 0.125 0.6875 -0.4375 +v 0.125 0.6875 -0.5625 +v 0.0625 0.75 -0.5625 +v 0.0625 0.75 -0.4375 +v 0.0625 0.6875 -0.5625 +v 0.0625 0.6875 -0.4375 +vt 0.03125 0.625 +vt 0.046875 0.625 +vt 0.046875 0.59375 +vt 0.03125 0.59375 +vt 0 0.625 +vt 0.03125 0.625 +vt 0.03125 0.59375 +vt 0 0.59375 +vt 0.078125 0.625 +vt 0.09375 0.625 +vt 0.09375 0.59375 +vt 0.078125 0.59375 +vt 0.046875 0.625 +vt 0.078125 0.625 +vt 0.078125 0.59375 +vt 0.046875 0.59375 +vt 0.046875 0.625 +vt 0.03125 0.625 +vt 0.03125 0.6875 +vt 0.046875 0.6875 +vt 0.0625 0.6875 +vt 0.046875 0.6875 +vt 0.046875 0.625 +vt 0.0625 0.625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head +v -0.0625 0.75 -0.4375 +v -0.0625 0.75 -0.5625 +v -0.0625 0.6875 -0.4375 +v -0.0625 0.6875 -0.5625 +v -0.125 0.75 -0.5625 +v -0.125 0.75 -0.4375 +v -0.125 0.6875 -0.5625 +v -0.125 0.6875 -0.4375 +vt 0.125 0.625 +vt 0.140625 0.625 +vt 0.140625 0.59375 +vt 0.125 0.59375 +vt 0.09375 0.625 +vt 0.125 0.625 +vt 0.125 0.59375 +vt 0.09375 0.59375 +vt 0.171875 0.625 +vt 0.1875 0.625 +vt 0.1875 0.59375 +vt 0.171875 0.59375 +vt 0.140625 0.625 +vt 0.171875 0.625 +vt 0.171875 0.59375 +vt 0.140625 0.59375 +vt 0.140625 0.625 +vt 0.125 0.625 +vt 0.125 0.6875 +vt 0.140625 0.6875 +vt 0.15625 0.6875 +vt 0.140625 0.6875 +vt 0.140625 0.625 +vt 0.15625 0.625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o tail1 +v 0.03125 0.6066941738241591 0.5441941738241594 +v 0.03125 0.5625 0.5 +v 0.03125 0.2531407832308854 0.8977475644174331 +v 0.03125 0.20894660940672616 0.853553390593274 +v -0.03125 0.5625 0.5 +v -0.03125 0.6066941738241591 0.5441941738241594 +v -0.03125 0.20894660940672616 0.853553390593274 +v -0.03125 0.2531407832308854 0.8977475644174331 +vt 0.015625 0.5 +vt 0.03125 0.5 +vt 0.03125 0.25 +vt 0.015625 0.25 +vt 0 0.5 +vt 0.015625 0.5 +vt 0.015625 0.25 +vt 0 0.25 +vt 0.046875 0.5 +vt 0.0625 0.5 +vt 0.0625 0.25 +vt 0.046875 0.25 +vt 0.03125 0.5 +vt 0.046875 0.5 +vt 0.046875 0.25 +vt 0.03125 0.25 +vt 0.03125 0.5 +vt 0.015625 0.5 +vt 0.015625 0.53125 +vt 0.03125 0.53125 +vt 0.046875 0.53125 +vt 0.03125 0.53125 +vt 0.03125 0.5 +vt 0.046875 0.5 +vn 0 -0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 -0.7071067811865476 +vn 0 -0.7071067811865475 0.7071067811865476 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o tail2 +v 0.03125 0.27144660940672627 0.8535533905932737 +v 0.03125 0.20894660940672627 0.8535533905932737 +v 0.03125 0.2714466094067264 1.3535533905932737 +v 0.03125 0.20894660940672638 1.3535533905932737 +v -0.03125 0.20894660940672627 0.8535533905932737 +v -0.03125 0.27144660940672627 0.8535533905932737 +v -0.03125 0.20894660940672638 1.3535533905932737 +v -0.03125 0.2714466094067264 1.3535533905932737 +vt 0.078125 0.5 +vt 0.09375 0.5 +vt 0.09375 0.25 +vt 0.078125 0.25 +vt 0.0625 0.5 +vt 0.078125 0.5 +vt 0.078125 0.25 +vt 0.0625 0.25 +vt 0.109375 0.5 +vt 0.125 0.5 +vt 0.125 0.25 +vt 0.109375 0.25 +vt 0.09375 0.5 +vt 0.109375 0.5 +vt 0.109375 0.25 +vt 0.09375 0.25 +vt 0.09375 0.5 +vt 0.078125 0.5 +vt 0.078125 0.53125 +vt 0.09375 0.53125 +vt 0.109375 0.53125 +vt 0.09375 0.53125 +vt 0.09375 0.5 +vt 0.109375 0.5 +vn 0 -1 2.220446049250313e-16 +vn 1 0 0 +vn 0 1 -2.220446049250313e-16 +vn -1 0 0 +vn 0 -2.220446049250313e-16 -1 +vn 0 2.220446049250313e-16 1 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o backLegL +v -0.006249999999999978 0.375 0.5 +v -0.006249999999999978 0.375 0.375 +v -0.006249999999999978 0 0.5 +v -0.006249999999999978 0 0.375 +v -0.13124999999999998 0.375 0.375 +v -0.13124999999999998 0.375 0.5 +v -0.13124999999999998 0 0.375 +v -0.13124999999999998 0 0.5 +vt 0.15625 0.53125 +vt 0.1875 0.53125 +vt 0.1875 0.34375 +vt 0.15625 0.34375 +vt 0.125 0.53125 +vt 0.15625 0.53125 +vt 0.15625 0.34375 +vt 0.125 0.34375 +vt 0.21875 0.53125 +vt 0.25 0.53125 +vt 0.25 0.34375 +vt 0.21875 0.34375 +vt 0.1875 0.53125 +vt 0.21875 0.53125 +vt 0.21875 0.34375 +vt 0.1875 0.34375 +vt 0.1875 0.53125 +vt 0.15625 0.53125 +vt 0.15625 0.59375 +vt 0.1875 0.59375 +vt 0.21875 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.53125 +vt 0.21875 0.53125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o backLegR +v 0.13124999999999998 0.375 0.5 +v 0.13124999999999998 0.375 0.375 +v 0.13124999999999998 0 0.5 +v 0.13124999999999998 0 0.375 +v 0.006249999999999978 0.375 0.375 +v 0.006249999999999978 0.375 0.5 +v 0.006249999999999978 0 0.375 +v 0.006249999999999978 0 0.5 +vt 0.15625 0.53125 +vt 0.1875 0.53125 +vt 0.1875 0.34375 +vt 0.15625 0.34375 +vt 0.125 0.53125 +vt 0.15625 0.53125 +vt 0.15625 0.34375 +vt 0.125 0.34375 +vt 0.21875 0.53125 +vt 0.25 0.53125 +vt 0.25 0.34375 +vt 0.21875 0.34375 +vt 0.1875 0.53125 +vt 0.21875 0.53125 +vt 0.21875 0.34375 +vt 0.1875 0.34375 +vt 0.1875 0.53125 +vt 0.15625 0.53125 +vt 0.15625 0.59375 +vt 0.1875 0.59375 +vt 0.21875 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.53125 +vt 0.21875 0.53125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o frontLegL +v -0.012500000000000011 0.6375 -0.1875 +v -0.012500000000000011 0.6375 -0.3125 +v -0.012500000000000011 0.012499999999999956 -0.1875 +v -0.012500000000000011 0.012499999999999956 -0.3125 +v -0.1375 0.6375 -0.3125 +v -0.1375 0.6375 -0.1875 +v -0.1375 0.012499999999999956 -0.3125 +v -0.1375 0.012499999999999956 -0.1875 +vt 0.65625 0.9375 +vt 0.6875 0.9375 +vt 0.6875 0.625 +vt 0.65625 0.625 +vt 0.625 0.9375 +vt 0.65625 0.9375 +vt 0.65625 0.625 +vt 0.625 0.625 +vt 0.71875 0.9375 +vt 0.75 0.9375 +vt 0.75 0.625 +vt 0.71875 0.625 +vt 0.6875 0.9375 +vt 0.71875 0.9375 +vt 0.71875 0.625 +vt 0.6875 0.625 +vt 0.6875 0.9375 +vt 0.65625 0.9375 +vt 0.65625 1 +vt 0.6875 1 +vt 0.71875 1 +vt 0.6875 1 +vt 0.6875 0.9375 +vt 0.71875 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o frontLegR +v 0.13749999999999996 0.6375 -0.1875 +v 0.13749999999999996 0.6375 -0.3125 +v 0.13749999999999996 0.012499999999999956 -0.1875 +v 0.13749999999999996 0.012499999999999956 -0.3125 +v 0.012499999999999956 0.6375 -0.3125 +v 0.012499999999999956 0.6375 -0.1875 +v 0.012499999999999956 0.012499999999999956 -0.3125 +v 0.012499999999999956 0.012499999999999956 -0.1875 +vt 0.65625 0.9375 +vt 0.6875 0.9375 +vt 0.6875 0.625 +vt 0.65625 0.625 +vt 0.625 0.9375 +vt 0.65625 0.9375 +vt 0.65625 0.625 +vt 0.625 0.625 +vt 0.71875 0.9375 +vt 0.75 0.9375 +vt 0.75 0.625 +vt 0.71875 0.625 +vt 0.6875 0.9375 +vt 0.71875 0.9375 +vt 0.71875 0.625 +vt 0.6875 0.625 +vt 0.6875 0.9375 +vt 0.65625 0.9375 +vt 0.65625 1 +vt 0.6875 1 +vt 0.71875 1 +vt 0.6875 1 +vt 0.6875 0.9375 +vt 0.71875 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_2ee28436-932f-1f18-8613-04d9bd8d8ca4 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/chicken.obj b/renderer/viewer/three/entity/models/chicken.obj new file mode 100644 index 00000000..ec873ffd --- /dev/null +++ b/renderer/viewer/three/entity/models/chicken.obj @@ -0,0 +1,371 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.1875 0.6875 -0.25 +v 0.1875 0.3125 -0.25 +v 0.1875 0.6875 0.25 +v 0.1875 0.3125 0.25 +v -0.1875 0.3125 -0.25 +v -0.1875 0.6875 -0.25 +v -0.1875 0.3125 0.25 +v -0.1875 0.6875 0.25 +vt 0.09375 0.53125 +vt 0.1875 0.53125 +vt 0.1875 0.28125 +vt 0.09375 0.28125 +vt 0 0.53125 +vt 0.09375 0.53125 +vt 0.09375 0.28125 +vt 0 0.28125 +vt 0.28125 0.53125 +vt 0.375 0.53125 +vt 0.375 0.28125 +vt 0.28125 0.28125 +vt 0.1875 0.53125 +vt 0.28125 0.53125 +vt 0.28125 0.28125 +vt 0.1875 0.28125 +vt 0.1875 0.53125 +vt 0.09375 0.53125 +vt 0.09375 0.71875 +vt 0.1875 0.71875 +vt 0.28125 0.71875 +vt 0.1875 0.71875 +vt 0.1875 0.53125 +vt 0.28125 0.53125 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.125 0.9375 -0.1875 +v 0.125 0.9375 -0.375 +v 0.125 0.5625 -0.1875 +v 0.125 0.5625 -0.375 +v -0.125 0.9375 -0.375 +v -0.125 0.9375 -0.1875 +v -0.125 0.5625 -0.375 +v -0.125 0.5625 -0.1875 +vt 0.046875 0.90625 +vt 0.109375 0.90625 +vt 0.109375 0.71875 +vt 0.046875 0.71875 +vt 0 0.90625 +vt 0.046875 0.90625 +vt 0.046875 0.71875 +vt 0 0.71875 +vt 0.15625 0.90625 +vt 0.21875 0.90625 +vt 0.21875 0.71875 +vt 0.15625 0.71875 +vt 0.109375 0.90625 +vt 0.15625 0.90625 +vt 0.15625 0.71875 +vt 0.109375 0.71875 +vt 0.109375 0.90625 +vt 0.046875 0.90625 +vt 0.046875 1 +vt 0.109375 1 +vt 0.171875 1 +vt 0.109375 1 +vt 0.109375 0.90625 +vt 0.171875 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o comb +v 0.0625 0.6875 -0.3125 +v 0.0625 0.6875 -0.4375 +v 0.0625 0.5625 -0.3125 +v 0.0625 0.5625 -0.4375 +v -0.0625 0.6875 -0.4375 +v -0.0625 0.6875 -0.3125 +v -0.0625 0.5625 -0.4375 +v -0.0625 0.5625 -0.3125 +vt 0.25 0.8125 +vt 0.28125 0.8125 +vt 0.28125 0.75 +vt 0.25 0.75 +vt 0.21875 0.8125 +vt 0.25 0.8125 +vt 0.25 0.75 +vt 0.21875 0.75 +vt 0.3125 0.8125 +vt 0.34375 0.8125 +vt 0.34375 0.75 +vt 0.3125 0.75 +vt 0.28125 0.8125 +vt 0.3125 0.8125 +vt 0.3125 0.75 +vt 0.28125 0.75 +vt 0.28125 0.8125 +vt 0.25 0.8125 +vt 0.25 0.875 +vt 0.28125 0.875 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.28125 0.8125 +vt 0.3125 0.8125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o beak +v 0.125 0.8224431423663936 -0.35273540157455374 +v 0.125 0.8333376102098509 -0.4772597388360219 +v 0.125 0.6979188051049254 -0.363629869418011 +v 0.125 0.7088132729483827 -0.4881542066794792 +v -0.125 0.8333376102098509 -0.4772597388360219 +v -0.125 0.8224431423663936 -0.35273540157455374 +v -0.125 0.7088132729483827 -0.4881542066794792 +v -0.125 0.6979188051049254 -0.363629869418011 +vt 0.25 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.25 0.875 +vt 0.21875 0.9375 +vt 0.25 0.9375 +vt 0.25 0.875 +vt 0.21875 0.875 +vt 0.34375 0.9375 +vt 0.40625 0.9375 +vt 0.40625 0.875 +vt 0.34375 0.875 +vt 0.3125 0.9375 +vt 0.34375 0.9375 +vt 0.34375 0.875 +vt 0.3125 0.875 +vt 0.3125 0.9375 +vt 0.25 0.9375 +vt 0.25 1 +vt 0.3125 1 +vt 0.375 1 +vt 0.3125 1 +vt 0.3125 0.9375 +vt 0.375 0.9375 +vn 0 0.08715574274765818 -0.9961946980917455 +vn 1 0 0 +vn 0 -0.08715574274765818 0.9961946980917455 +vn -1 0 0 +vn 0 0.9961946980917455 0.08715574274765818 +vn 0 -0.9961946980917455 -0.08715574274765818 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o leg0 +v 0.1875 0.3125 0.0625 +v 0.1875 0.3125 -0.125 +v 0.1875 0 0.0625 +v 0.1875 0 -0.125 +v 0 0.3125 -0.125 +v 0 0.3125 0.0625 +v 0 0 -0.125 +v 0 0 0.0625 +vt 0.453125 0.90625 +vt 0.5 0.90625 +vt 0.5 0.75 +vt 0.453125 0.75 +vt 0.40625 0.90625 +vt 0.453125 0.90625 +vt 0.453125 0.75 +vt 0.40625 0.75 +vt 0.546875 0.90625 +vt 0.59375 0.90625 +vt 0.59375 0.75 +vt 0.546875 0.75 +vt 0.5 0.90625 +vt 0.546875 0.90625 +vt 0.546875 0.75 +vt 0.5 0.75 +vt 0.5 0.90625 +vt 0.453125 0.90625 +vt 0.453125 1 +vt 0.5 1 +vt 0.546875 1 +vt 0.5 1 +vt 0.5 0.90625 +vt 0.546875 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leg1 +v 0 0.3125 0.0625 +v 0 0.3125 -0.125 +v 0 0 0.0625 +v 0 0 -0.125 +v -0.1875 0.3125 -0.125 +v -0.1875 0.3125 0.0625 +v -0.1875 0 -0.125 +v -0.1875 0 0.0625 +vt 0.453125 0.90625 +vt 0.5 0.90625 +vt 0.5 0.75 +vt 0.453125 0.75 +vt 0.40625 0.90625 +vt 0.453125 0.90625 +vt 0.453125 0.75 +vt 0.40625 0.75 +vt 0.546875 0.90625 +vt 0.59375 0.90625 +vt 0.59375 0.75 +vt 0.546875 0.75 +vt 0.5 0.90625 +vt 0.546875 0.90625 +vt 0.546875 0.75 +vt 0.5 0.75 +vt 0.5 0.90625 +vt 0.453125 0.90625 +vt 0.453125 1 +vt 0.5 1 +vt 0.546875 1 +vt 0.5 1 +vt 0.5 0.90625 +vt 0.546875 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o wing0 +v 0.25 0.6875 0.1875 +v 0.25 0.6875 -0.1875 +v 0.25 0.4375 0.1875 +v 0.25 0.4375 -0.1875 +v 0.1875 0.6875 -0.1875 +v 0.1875 0.6875 0.1875 +v 0.1875 0.4375 -0.1875 +v 0.1875 0.4375 0.1875 +vt 0.46875 0.40625 +vt 0.484375 0.40625 +vt 0.484375 0.28125 +vt 0.46875 0.28125 +vt 0.375 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.28125 +vt 0.375 0.28125 +vt 0.578125 0.40625 +vt 0.59375 0.40625 +vt 0.59375 0.28125 +vt 0.578125 0.28125 +vt 0.484375 0.40625 +vt 0.578125 0.40625 +vt 0.578125 0.28125 +vt 0.484375 0.28125 +vt 0.484375 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.59375 +vt 0.484375 0.59375 +vt 0.5 0.59375 +vt 0.484375 0.59375 +vt 0.484375 0.40625 +vt 0.5 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o wing1 +v -0.1875 0.6875 0.1875 +v -0.1875 0.6875 -0.1875 +v -0.1875 0.4375 0.1875 +v -0.1875 0.4375 -0.1875 +v -0.25 0.6875 -0.1875 +v -0.25 0.6875 0.1875 +v -0.25 0.4375 -0.1875 +v -0.25 0.4375 0.1875 +vt 0.46875 0.40625 +vt 0.484375 0.40625 +vt 0.484375 0.28125 +vt 0.46875 0.28125 +vt 0.375 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.28125 +vt 0.375 0.28125 +vt 0.578125 0.40625 +vt 0.59375 0.40625 +vt 0.59375 0.28125 +vt 0.578125 0.28125 +vt 0.484375 0.40625 +vt 0.578125 0.40625 +vt 0.578125 0.28125 +vt 0.484375 0.28125 +vt 0.484375 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.59375 +vt 0.484375 0.59375 +vt 0.5 0.59375 +vt 0.484375 0.59375 +vt 0.484375 0.40625 +vt 0.5 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a3a58acf-2885-3588-3386-06528bca041b +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/cod.obj b/renderer/viewer/three/entity/models/cod.obj new file mode 100644 index 00000000..2b469b8b --- /dev/null +++ b/renderer/viewer/three/entity/models/cod.obj @@ -0,0 +1,371 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.0625 0.25 0.5 +v 0.0625 0.25 0.0625 +v 0.0625 0 0.5 +v 0.0625 0 0.0625 +v -0.0625 0.25 0.0625 +v -0.0625 0.25 0.5 +v -0.0625 0 0.0625 +v -0.0625 0 0.5 +vt 0.21875 0.78125 +vt 0.28125 0.78125 +vt 0.28125 0.65625 +vt 0.21875 0.65625 +vt 0 0.78125 +vt 0.21875 0.78125 +vt 0.21875 0.65625 +vt 0 0.65625 +vt 0.5 0.78125 +vt 0.5625 0.78125 +vt 0.5625 0.65625 +vt 0.5 0.65625 +vt 0.28125 0.78125 +vt 0.5 0.78125 +vt 0.5 0.65625 +vt 0.28125 0.65625 +vt 0.28125 0.78125 +vt 0.21875 0.78125 +vt 0.21875 1 +vt 0.28125 1 +vt 0.34375 1 +vt 0.28125 1 +vt 0.28125 0.78125 +vt 0.34375 0.78125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0 0.3125 0.375 +v 0 0.3125 0 +v 0 0.25 0.375 +v 0 0.25 0 +v 0 0.3125 0 +v 0 0.3125 0.375 +v 0 0.25 0 +v 0 0.25 0.375 +vt 0.8125 1 +vt 0.8125 1 +vt 0.8125 0.96875 +vt 0.8125 0.96875 +vt 0.625 1 +vt 0.8125 1 +vt 0.8125 0.96875 +vt 0.625 0.96875 +vt 1 1 +vt 1 1 +vt 1 0.96875 +vt 1 0.96875 +vt 0.8125 1 +vt 1 1 +vt 1 0.96875 +vt 0.8125 0.96875 +vt 0.8125 1 +vt 0.8125 1 +vt 0.8125 1.1875 +vt 0.8125 1.1875 +vt 0.8125 1.1875 +vt 0.8125 1.1875 +vt 0.8125 1 +vt 0.8125 1 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o body +v 0 0 0.3125 +v 0 0 0.1875 +v 0 -0.0625 0.3125 +v 0 -0.0625 0.1875 +v 0 0 0.1875 +v 0 0 0.3125 +v 0 -0.0625 0.1875 +v 0 -0.0625 0.3125 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.9375 +vt 0.75 0.9375 +vt 0.6875 0.96875 +vt 0.75 0.96875 +vt 0.75 0.9375 +vt 0.6875 0.9375 +vt 0.8125 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.9375 +vt 0.8125 0.9375 +vt 0.75 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.9375 +vt 0.75 0.9375 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 1.03125 +vt 0.75 1.03125 +vt 0.75 1.03125 +vt 0.75 1.03125 +vt 0.75 0.96875 +vt 0.75 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.062450000000000006 0.25005 -0.125 +v 0.062450000000000006 0.25005 -0.1875 +v 0.062450000000000006 0.06255 -0.125 +v 0.062450000000000006 0.06255 -0.1875 +v -0.06255 0.25005 -0.1875 +v -0.06255 0.25005 -0.125 +v -0.06255 0.06255 -0.1875 +v -0.06255 0.06255 -0.125 +vt 0.03125 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.875 +vt 0.03125 0.875 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.875 +vt 0 0.875 +vt 0.125 0.96875 +vt 0.1875 0.96875 +vt 0.1875 0.875 +vt 0.125 0.875 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.875 +vt 0.09375 0.875 +vt 0.09375 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.09375 1 +vt 0.15625 1 +vt 0.09375 1 +vt 0.09375 0.96875 +vt 0.15625 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head +v 0.0625 0.25 0.0625 +v 0.0625 0.25 -0.125 +v 0.0625 0 0.0625 +v 0.0625 0 -0.125 +v -0.0625 0.25 -0.125 +v -0.0625 0.25 0.0625 +v -0.0625 0 -0.125 +v -0.0625 0 0.0625 +vt 0.4375 0.90625 +vt 0.5 0.90625 +vt 0.5 0.78125 +vt 0.4375 0.78125 +vt 0.34375 0.90625 +vt 0.4375 0.90625 +vt 0.4375 0.78125 +vt 0.34375 0.78125 +vt 0.59375 0.90625 +vt 0.65625 0.90625 +vt 0.65625 0.78125 +vt 0.59375 0.78125 +vt 0.5 0.90625 +vt 0.59375 0.90625 +vt 0.59375 0.78125 +vt 0.5 0.78125 +vt 0.5 0.90625 +vt 0.4375 0.90625 +vt 0.4375 1 +vt 0.5 1 +vt 0.5625 1 +vt 0.5 1 +vt 0.5 0.90625 +vt 0.5625 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leftFin +v -0.0625 0.0625 0.125 +v -0.0625 0.0625 0 +v -0.026651472728059622 0.011302997231938061 0.125 +v -0.026651472728059622 0.011302997231938061 0 +v -0.164894005536124 -0.009197054543880756 0 +v -0.164894005536124 -0.009197054543880756 0.125 +v -0.12904547826418356 -0.06039405731194275 0 +v -0.12904547826418356 -0.06039405731194275 0.125 +vt 0.8125 0.8125 +vt 0.875 0.8125 +vt 0.875 0.78125 +vt 0.8125 0.78125 +vt 0.75 0.8125 +vt 0.8125 0.8125 +vt 0.8125 0.78125 +vt 0.75 0.78125 +vt 0.9375 0.8125 +vt 1 0.8125 +vt 1 0.78125 +vt 0.9375 0.78125 +vt 0.875 0.8125 +vt 0.9375 0.8125 +vt 0.9375 0.78125 +vt 0.875 0.78125 +vt 0.875 0.8125 +vt 0.8125 0.8125 +vt 0.8125 0.875 +vt 0.875 0.875 +vt 0.9375 0.875 +vt 0.875 0.875 +vt 0.875 0.8125 +vt 0.9375 0.8125 +vn 0 0 -1 +vn 0.8191520442889918 0.573576436351046 0 +vn 0 0 1 +vn -0.8191520442889918 -0.573576436351046 0 +vn -0.573576436351046 0.8191520442889918 0 +vn 0.573576436351046 -0.8191520442889918 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o rightFin +v 0.164894005536124 -0.009197054543880756 0.125 +v 0.164894005536124 -0.009197054543880756 0 +v 0.12904547826418356 -0.06039405731194275 0.125 +v 0.12904547826418356 -0.06039405731194275 0 +v 0.0625 0.0625 0 +v 0.0625 0.0625 0.125 +v 0.026651472728059566 0.011302997231938061 0 +v 0.026651472728059566 0.011302997231938061 0.125 +vt 0.8125 0.90625 +vt 0.875 0.90625 +vt 0.875 0.875 +vt 0.8125 0.875 +vt 0.75 0.90625 +vt 0.8125 0.90625 +vt 0.8125 0.875 +vt 0.75 0.875 +vt 0.9375 0.90625 +vt 1 0.90625 +vt 1 0.875 +vt 0.9375 0.875 +vt 0.875 0.90625 +vt 0.9375 0.90625 +vt 0.9375 0.875 +vt 0.875 0.875 +vt 0.875 0.90625 +vt 0.8125 0.90625 +vt 0.8125 0.96875 +vt 0.875 0.96875 +vt 0.9375 0.96875 +vt 0.875 0.96875 +vt 0.875 0.90625 +vt 0.9375 0.90625 +vn 0 0 -1 +vn 0.8191520442889918 -0.573576436351046 0 +vn 0 0 1 +vn -0.8191520442889918 0.573576436351046 0 +vn 0.573576436351046 0.8191520442889918 0 +vn -0.573576436351046 -0.8191520442889918 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o tailfin +v 0 0.25 0.875 +v 0 0.25 0.5 +v 0 0 0.875 +v 0 0 0.5 +v 0 0.25 0.5 +v 0 0.25 0.875 +v 0 0 0.5 +v 0 0 0.875 +vt 0.8125 0.78125 +vt 0.8125 0.78125 +vt 0.8125 0.65625 +vt 0.8125 0.65625 +vt 0.625 0.78125 +vt 0.8125 0.78125 +vt 0.8125 0.65625 +vt 0.625 0.65625 +vt 1 0.78125 +vt 1 0.78125 +vt 1 0.65625 +vt 1 0.65625 +vt 0.8125 0.78125 +vt 1 0.78125 +vt 1 0.65625 +vt 0.8125 0.65625 +vt 0.8125 0.78125 +vt 0.8125 0.78125 +vt 0.8125 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.78125 +vt 0.8125 0.78125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_92191bde-46b9-ba2f-846b-1e3ee628119f +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/creeper.obj b/renderer/viewer/three/entity/models/creeper.obj new file mode 100644 index 00000000..2e68c50e --- /dev/null +++ b/renderer/viewer/three/entity/models/creeper.obj @@ -0,0 +1,279 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o Body +v 0.25 1.125 0.125 +v 0.25 1.125 -0.125 +v 0.25 0.375 0.125 +v 0.25 0.375 -0.125 +v -0.25 1.125 -0.125 +v -0.25 1.125 0.125 +v -0.25 0.375 -0.125 +v -0.25 0.375 0.125 +vt 0.3125 0.375 +vt 0.4375 0.375 +vt 0.4375 0 +vt 0.3125 0 +vt 0.25 0.375 +vt 0.3125 0.375 +vt 0.3125 0 +vt 0.25 0 +vt 0.5 0.375 +vt 0.625 0.375 +vt 0.625 0 +vt 0.5 0 +vt 0.4375 0.375 +vt 0.5 0.375 +vt 0.5 0 +vt 0.4375 0 +vt 0.4375 0.375 +vt 0.3125 0.375 +vt 0.3125 0.5 +vt 0.4375 0.5 +vt 0.5625 0.5 +vt 0.4375 0.5 +vt 0.4375 0.375 +vt 0.5625 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5d56b934-8e1a-577b-049f-fb19876a8641 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o Head +v 0.25 1.625 0.25 +v 0.25 1.625 -0.25 +v 0.25 1.125 0.25 +v 0.25 1.125 -0.25 +v -0.25 1.625 -0.25 +v -0.25 1.625 0.25 +v -0.25 1.125 -0.25 +v -0.25 1.125 0.25 +vt 0.125 0.75 +vt 0.25 0.75 +vt 0.25 0.5 +vt 0.125 0.5 +vt 0 0.75 +vt 0.125 0.75 +vt 0.125 0.5 +vt 0 0.5 +vt 0.375 0.75 +vt 0.5 0.75 +vt 0.5 0.5 +vt 0.375 0.5 +vt 0.25 0.75 +vt 0.375 0.75 +vt 0.375 0.5 +vt 0.25 0.5 +vt 0.25 0.75 +vt 0.125 0.75 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.75 +vt 0.375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5d56b934-8e1a-577b-049f-fb19876a8641 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o leg0 +v 0.25 0.375 0.375 +v 0.25 0.375 0.125 +v 0.25 0 0.375 +v 0.25 0 0.125 +v 0 0.375 0.125 +v 0 0.375 0.375 +v 0 0 0.125 +v 0 0 0.375 +vt 0.0625 0.375 +vt 0.125 0.375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0 0.375 +vt 0.0625 0.375 +vt 0.0625 0.1875 +vt 0 0.1875 +vt 0.1875 0.375 +vt 0.25 0.375 +vt 0.25 0.1875 +vt 0.1875 0.1875 +vt 0.125 0.375 +vt 0.1875 0.375 +vt 0.1875 0.1875 +vt 0.125 0.1875 +vt 0.125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.5 +vt 0.125 0.5 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.375 +vt 0.1875 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5d56b934-8e1a-577b-049f-fb19876a8641 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o leg1 +v 0 0.375 0.375 +v 0 0.375 0.125 +v 0 0 0.375 +v 0 0 0.125 +v -0.25 0.375 0.125 +v -0.25 0.375 0.375 +v -0.25 0 0.125 +v -0.25 0 0.375 +vt 0.0625 0.375 +vt 0.125 0.375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0 0.375 +vt 0.0625 0.375 +vt 0.0625 0.1875 +vt 0 0.1875 +vt 0.1875 0.375 +vt 0.25 0.375 +vt 0.25 0.1875 +vt 0.1875 0.1875 +vt 0.125 0.375 +vt 0.1875 0.375 +vt 0.1875 0.1875 +vt 0.125 0.1875 +vt 0.125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.5 +vt 0.125 0.5 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.375 +vt 0.1875 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5d56b934-8e1a-577b-049f-fb19876a8641 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o leg2 +v 0.25 0.375 -0.125 +v 0.25 0.375 -0.375 +v 0.25 0 -0.125 +v 0.25 0 -0.375 +v 0 0.375 -0.375 +v 0 0.375 -0.125 +v 0 0 -0.375 +v 0 0 -0.125 +vt 0.0625 0.375 +vt 0.125 0.375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0 0.375 +vt 0.0625 0.375 +vt 0.0625 0.1875 +vt 0 0.1875 +vt 0.1875 0.375 +vt 0.25 0.375 +vt 0.25 0.1875 +vt 0.1875 0.1875 +vt 0.125 0.375 +vt 0.1875 0.375 +vt 0.1875 0.1875 +vt 0.125 0.1875 +vt 0.125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.5 +vt 0.125 0.5 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.375 +vt 0.1875 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5d56b934-8e1a-577b-049f-fb19876a8641 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leg3 +v 0 0.375 -0.125 +v 0 0.375 -0.375 +v 0 0 -0.125 +v 0 0 -0.375 +v -0.25 0.375 -0.375 +v -0.25 0.375 -0.125 +v -0.25 0 -0.375 +v -0.25 0 -0.125 +vt 0.0625 0.375 +vt 0.125 0.375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0 0.375 +vt 0.0625 0.375 +vt 0.0625 0.1875 +vt 0 0.1875 +vt 0.1875 0.375 +vt 0.25 0.375 +vt 0.25 0.1875 +vt 0.1875 0.1875 +vt 0.125 0.375 +vt 0.1875 0.375 +vt 0.1875 0.1875 +vt 0.125 0.1875 +vt 0.125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.5 +vt 0.125 0.5 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.375 +vt 0.1875 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5d56b934-8e1a-577b-049f-fb19876a8641 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/dolphin.obj b/renderer/viewer/three/entity/models/dolphin.obj new file mode 100644 index 00000000..478ce015 --- /dev/null +++ b/renderer/viewer/three/entity/models/dolphin.obj @@ -0,0 +1,371 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.25 0.4375 0.625 +v 0.25 0.4375 -0.1875 +v 0.25 0 0.625 +v 0.25 0 -0.1875 +v -0.25 0.4375 -0.1875 +v -0.25 0.4375 0.625 +v -0.25 0 -0.1875 +v -0.25 0 0.625 +vt 0.546875 0.796875 +vt 0.671875 0.796875 +vt 0.671875 0.6875 +vt 0.546875 0.6875 +vt 0.34375 0.796875 +vt 0.546875 0.796875 +vt 0.546875 0.6875 +vt 0.34375 0.6875 +vt 0.875 0.796875 +vt 1 0.796875 +vt 1 0.6875 +vt 0.875 0.6875 +vt 0.671875 0.796875 +vt 0.875 0.796875 +vt 0.875 0.6875 +vt 0.671875 0.6875 +vt 0.671875 0.796875 +vt 0.546875 0.796875 +vt 0.546875 1 +vt 0.671875 1 +vt 0.796875 1 +vt 0.671875 1 +vt 0.671875 0.796875 +vt 0.796875 0.796875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.25 0.4375 -0.1875 +v 0.25 0.4375 -0.5625 +v 0.25 0 -0.1875 +v 0.25 0 -0.5625 +v -0.25 0.4375 -0.5625 +v -0.25 0.4375 -0.1875 +v -0.25 0 -0.5625 +v -0.25 0 -0.1875 +vt 0.09375 0.90625 +vt 0.21875 0.90625 +vt 0.21875 0.796875 +vt 0.09375 0.796875 +vt 0 0.90625 +vt 0.09375 0.90625 +vt 0.09375 0.796875 +vt 0 0.796875 +vt 0.3125 0.90625 +vt 0.4375 0.90625 +vt 0.4375 0.796875 +vt 0.3125 0.796875 +vt 0.21875 0.90625 +vt 0.3125 0.90625 +vt 0.3125 0.796875 +vt 0.21875 0.796875 +vt 0.21875 0.90625 +vt 0.09375 0.90625 +vt 0.09375 1 +vt 0.21875 1 +vt 0.34375 1 +vt 0.21875 1 +vt 0.21875 0.90625 +vt 0.34375 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o nose +v 0.0625 0.125 -0.5625 +v 0.0625 0.125 -0.8125 +v 0.0625 0 -0.5625 +v 0.0625 0 -0.8125 +v -0.0625 0.125 -0.8125 +v -0.0625 0.125 -0.5625 +v -0.0625 0 -0.8125 +v -0.0625 0 -0.5625 +vt 0.0625 0.734375 +vt 0.09375 0.734375 +vt 0.09375 0.703125 +vt 0.0625 0.703125 +vt 0 0.734375 +vt 0.0625 0.734375 +vt 0.0625 0.703125 +vt 0 0.703125 +vt 0.15625 0.734375 +vt 0.1875 0.734375 +vt 0.1875 0.703125 +vt 0.15625 0.703125 +vt 0.09375 0.734375 +vt 0.15625 0.734375 +vt 0.15625 0.703125 +vt 0.09375 0.703125 +vt 0.09375 0.734375 +vt 0.0625 0.734375 +vt 0.0625 0.796875 +vt 0.09375 0.796875 +vt 0.125 0.796875 +vt 0.09375 0.796875 +vt 0.09375 0.734375 +vt 0.125 0.734375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o tail +v 0.125 0.25743308235954887 1.3237397711116625 +v 0.125 0.31735265549856384 0.6388559161735876 +v 0.125 -0.0538777607941216 1.2965036015030194 +v 0.125 0.006041812344893427 0.6116197465649442 +v -0.125 0.31735265549856384 0.6388559161735876 +v -0.125 0.25743308235954887 1.3237397711116625 +v -0.125 0.006041812344893427 0.6116197465649442 +v -0.125 -0.0538777607941216 1.2965036015030194 +vt 0.171875 0.53125 +vt 0.234375 0.53125 +vt 0.234375 0.453125 +vt 0.171875 0.453125 +vt 0 0.53125 +vt 0.171875 0.53125 +vt 0.171875 0.453125 +vt 0 0.453125 +vt 0.40625 0.53125 +vt 0.46875 0.53125 +vt 0.46875 0.453125 +vt 0.40625 0.453125 +vt 0.234375 0.53125 +vt 0.40625 0.53125 +vt 0.40625 0.453125 +vt 0.234375 0.453125 +vt 0.234375 0.53125 +vt 0.171875 0.53125 +vt 0.171875 0.703125 +vt 0.234375 0.703125 +vt 0.296875 0.703125 +vt 0.234375 0.703125 +vt 0.234375 0.53125 +vt 0.296875 0.53125 +vn 0 0.08715574274765818 -0.9961946980917455 +vn 1 0 0 +vn 0 -0.08715574274765818 0.9961946980917455 +vn -1 0 0 +vn 0 0.9961946980917455 0.08715574274765818 +vn 0 -0.9961946980917455 -0.08715574274765818 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o tail_fin +v 0.3125 0.06737675474652305 1.559379883370239 +v 0.3125 0.15173340012547243 1.1939911090757758 +v 0.3125 0.006478625697445839 1.5453204424737472 +v 0.3125 0.09083527107639522 1.1799316681792842 +v -0.3125 0.15173340012547243 1.1939911090757758 +v -0.3125 0.06737675474652305 1.559379883370239 +v -0.3125 0.09083527107639522 1.1799316681792842 +v -0.3125 0.006478625697445839 1.5453204424737472 +vt 0.390625 0.59375 +vt 0.546875 0.59375 +vt 0.546875 0.578125 +vt 0.390625 0.578125 +vt 0.296875 0.59375 +vt 0.390625 0.59375 +vt 0.390625 0.578125 +vt 0.296875 0.578125 +vt 0.640625 0.59375 +vt 0.796875 0.59375 +vt 0.796875 0.578125 +vt 0.640625 0.578125 +vt 0.546875 0.59375 +vt 0.640625 0.59375 +vt 0.640625 0.578125 +vt 0.546875 0.578125 +vt 0.546875 0.59375 +vt 0.390625 0.59375 +vt 0.390625 0.6875 +vt 0.546875 0.6875 +vt 0.703125 0.6875 +vt 0.546875 0.6875 +vt 0.546875 0.59375 +vt 0.703125 0.59375 +vn 0 0.224951054343865 -0.9743700647852354 +vn 1 0 0 +vn 0 -0.224951054343865 0.9743700647852354 +vn -1 0 0 +vn 0 0.9743700647852354 0.224951054343865 +vn 0 -0.9743700647852354 -0.224951054343865 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o back_fin +v 0.03125 0.7045071448143734 0.22503005919760444 +v 0.03125 0.4338742061317362 0.06878005919760444 +v 0.03125 0.5795071448143734 0.44153641014371414 +v 0.03125 0.3088742061317362 0.28528641014371414 +v -0.03125 0.4338742061317362 0.06878005919760444 +v -0.03125 0.7045071448143734 0.22503005919760444 +v -0.03125 0.3088742061317362 0.28528641014371414 +v -0.03125 0.5795071448143734 0.44153641014371414 +vt 0.875 0.921875 +vt 0.890625 0.921875 +vt 0.890625 0.859375 +vt 0.875 0.859375 +vt 0.796875 0.921875 +vt 0.875 0.921875 +vt 0.875 0.859375 +vt 0.796875 0.859375 +vt 0.96875 0.921875 +vt 0.984375 0.921875 +vt 0.984375 0.859375 +vt 0.96875 0.859375 +vt 0.890625 0.921875 +vt 0.96875 0.921875 +vt 0.96875 0.859375 +vt 0.890625 0.859375 +vt 0.890625 0.921875 +vt 0.875 0.921875 +vt 0.875 1 +vt 0.890625 1 +vt 0.90625 1 +vt 0.890625 1 +vt 0.890625 0.921875 +vt 0.90625 0.921875 +vn 0 -0.8660254037844386 -0.5000000000000001 +vn 1 0 0 +vn 0 0.8660254037844386 0.5000000000000001 +vn -1 0 0 +vn 0 0.5000000000000001 -0.8660254037844386 +vn 0 -0.5000000000000001 0.8660254037844386 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o left_fin +v -0.5939081233075358 0.0007485675108518386 0.11737888892342419 +v -0.25118856264048317 0.1055284523428025 -0.13356080198015846 +v -0.456779654809827 0.04267294763203877 0.32216689999567216 +v -0.11406009414277446 0.14745283246398944 0.07122720909208935 +v -0.2329153310953121 0.04575940509511278 -0.13356080198015846 +v -0.5756348917623646 -0.05902047973683788 0.11737888892342419 +v -0.09578686259760338 0.08768378521629971 0.07122720909208935 +v -0.438506423264656 -0.01709609961565095 0.32216689999567216 +vt 0.859375 0.578125 +vt 0.875 0.578125 +vt 0.875 0.515625 +vt 0.859375 0.515625 +vt 0.75 0.578125 +vt 0.859375 0.578125 +vt 0.859375 0.515625 +vt 0.75 0.515625 +vt 0.984375 0.578125 +vt 1 0.578125 +vt 1 0.515625 +vt 0.984375 0.515625 +vt 0.875 0.578125 +vt 0.984375 0.578125 +vt 0.984375 0.515625 +vt 0.875 0.515625 +vt 0.875 0.578125 +vt 0.859375 0.578125 +vt 0.859375 0.6875 +vt 0.875 0.6875 +vt 0.890625 0.6875 +vt 0.875 0.6875 +vt 0.875 0.578125 +vt 0.890625 0.578125 +vn 0.7833589958104059 0.23949687961588728 -0.5735764363510463 +vn -0.2923717047227367 0.9563047559630355 -8.326672684688674e-17 +vn -0.7833589958104059 -0.23949687961588728 0.5735764363510463 +vn 0.2923717047227367 -0.9563047559630355 8.326672684688674e-17 +vn -0.5485138739908348 -0.16769752048474773 -0.8191520442889918 +vn 0.5485138739908348 0.16769752048474773 0.8191520442889918 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o left_fin +v 0.5756348917623646 -0.05902047973683788 0.11737888892342419 +v 0.23291533109531204 0.04575940509511278 -0.13356080198015846 +v 0.43850642326465605 -0.01709609961565095 0.32216689999567216 +v 0.09578686259760338 0.08768378521629971 0.07122720909208935 +v 0.25118856264048317 0.1055284523428025 -0.13356080198015846 +v 0.5939081233075358 0.0007485675108518386 0.11737888892342419 +v 0.1140600941427744 0.14745283246398944 0.07122720909208935 +v 0.4567796548098271 0.04267294763203877 0.32216689999567216 +vt 0.875 0.578125 +vt 0.859375 0.578125 +vt 0.859375 0.515625 +vt 0.875 0.515625 +vt 0.984375 0.578125 +vt 0.875 0.578125 +vt 0.875 0.515625 +vt 0.984375 0.515625 +vt 1 0.578125 +vt 0.984375 0.578125 +vt 0.984375 0.515625 +vt 1 0.515625 +vt 0.859375 0.578125 +vt 0.75 0.578125 +vt 0.75 0.515625 +vt 0.859375 0.515625 +vt 0.859375 0.578125 +vt 0.875 0.578125 +vt 0.875 0.6875 +vt 0.859375 0.6875 +vt 0.875 0.6875 +vt 0.890625 0.6875 +vt 0.890625 0.578125 +vt 0.875 0.578125 +vn -0.7833589958104059 0.23949687961588728 -0.5735764363510463 +vn -0.2923717047227367 -0.9563047559630355 8.326672684688674e-17 +vn 0.7833589958104059 -0.23949687961588728 0.5735764363510463 +vn 0.2923717047227367 0.9563047559630355 -8.326672684688674e-17 +vn 0.5485138739908348 -0.16769752048474773 -0.8191520442889918 +vn -0.5485138739908348 0.16769752048474773 0.8191520442889918 +usemtl m_84be203d-dd47-5073-42d4-8589a81aedb5 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/ender_dragon.obj b/renderer/viewer/three/entity/models/ender_dragon.obj new file mode 100644 index 00000000..84c909a9 --- /dev/null +++ b/renderer/viewer/three/entity/models/ender_dragon.obj @@ -0,0 +1,2993 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o neck +v 0.3125 0.7488108431536706 -0.47276383039135683 +v 0.3125 0.803283182370957 -1.0953855166986979 +v 0.3125 0.1261891568463296 -0.5272361696086432 +v 0.3125 0.18066149606361592 -1.1498578559159842 +v -0.3125 0.803283182370957 -1.0953855166986979 +v -0.3125 0.7488108431536706 -0.47276383039135683 +v -0.3125 0.18066149606361592 -1.1498578559159842 +v -0.3125 0.1261891568463296 -0.5272361696086432 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 0.08715574274765818 -0.9961946980917455 +vn 1 0 0 +vn 0 -0.08715574274765818 0.9961946980917455 +vn -1 0 0 +vn 0 0.9961946980917455 0.08715574274765818 +vn 0 -0.9961946980917455 -0.08715574274765818 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o neck +v 0.0625 1.0087539855200642 -0.5754992319659105 +v 0.0625 1.041437389050436 -0.9490722437503151 +v 0.0625 0.7597053109971279 -0.597288167652825 +v 0.0625 0.7923887145274997 -0.9708611794372296 +v -0.0625 1.041437389050436 -0.9490722437503151 +v -0.0625 1.0087539855200642 -0.5754992319659105 +v -0.0625 0.7923887145274997 -0.9708611794372296 +v -0.0625 0.7597053109971279 -0.597288167652825 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 0.08715574274765818 -0.9961946980917455 +vn 1 0 0 +vn 0 -0.08715574274765818 0.9961946980917455 +vn -1 0 0 +vn 0 0.9961946980917455 0.08715574274765818 +vn 0 -0.9961946980917455 -0.08715574274765818 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o neck +v 0.3125 0.8044723392172863 -1.1226216863073408 +v 0.3125 0.8044723392172863 -1.7476216863073408 +v 0.3125 0.17947233921728634 -1.1226216863073408 +v 0.3125 0.17947233921728634 -1.7476216863073408 +v -0.3125 0.8044723392172863 -1.7476216863073408 +v -0.3125 0.8044723392172863 -1.1226216863073408 +v -0.3125 0.17947233921728634 -1.7476216863073408 +v -0.3125 0.17947233921728634 -1.1226216863073408 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o neck +v 0.0625 1.0544723392172863 -1.2476216863073408 +v 0.0625 1.0544723392172863 -1.6226216863073408 +v 0.0625 0.8044723392172863 -1.2476216863073408 +v 0.0625 0.8044723392172863 -1.6226216863073408 +v -0.0625 1.0544723392172863 -1.6226216863073408 +v -0.0625 1.0544723392172863 -1.2476216863073408 +v -0.0625 0.8044723392172863 -1.6226216863073408 +v -0.0625 0.8044723392172863 -1.2476216863073408 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o neck +v 0.3125 0.803283182370957 -1.774857855915984 +v 0.3125 0.7488108431536706 -2.3974795422233246 +v 0.3125 0.18066149606361592 -1.7203855166986977 +v 0.3125 0.12618915684632948 -2.3430072030060387 +v -0.3125 0.7488108431536706 -2.3974795422233246 +v -0.3125 0.803283182370957 -1.774857855915984 +v -0.3125 0.12618915684632948 -2.3430072030060387 +v -0.3125 0.18066149606361592 -1.7203855166986977 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.08715574274765818 -0.9961946980917455 +vn 1 0 0 +vn 0 0.08715574274765818 0.9961946980917455 +vn -1 0 0 +vn 0 0.9961946980917455 -0.08715574274765818 +vn 0 -0.9961946980917455 0.08715574274765818 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o neck +v 0.0625 1.041437389050436 -1.9211711288643667 +v 0.0625 1.0087539855200642 -2.2947441406487714 +v 0.0625 0.7923887145274997 -1.8993821931774522 +v 0.0625 0.7597053109971279 -2.272955204961857 +v -0.0625 1.0087539855200642 -2.2947441406487714 +v -0.0625 1.041437389050436 -1.9211711288643667 +v -0.0625 0.7597053109971279 -2.272955204961857 +v -0.0625 0.7923887145274997 -1.8993821931774522 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.08715574274765818 -0.9961946980917455 +vn 1 0 0 +vn 0 0.08715574274765818 0.9961946980917455 +vn -1 0 0 +vn 0 0.9961946980917455 -0.08715574274765818 +vn 0 -0.9961946980917455 0.08715574274765818 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o neck +v 0.3125 0.7452524228163151 -2.424508428135597 +v 0.3125 0.6367223117744836 -3.0400132737682273 +v 0.3125 0.1297475771836849 -2.315978317093766 +v 0.3125 0.02121746614185338 -2.931483162726396 +v -0.3125 0.6367223117744836 -3.0400132737682273 +v -0.3125 0.7452524228163151 -2.424508428135597 +v -0.3125 0.02121746614185338 -2.931483162726396 +v -0.3125 0.1297475771836849 -2.315978317093766 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.17364817766693036 -0.9848077530122081 +vn 1 0 0 +vn 0 0.17364817766693036 0.9848077530122081 +vn -1 0 0 +vn 0 0.9848077530122081 -0.17364817766693036 +vn 0 -0.9848077530122081 0.17364817766693036 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o neck +v 0.0625 0.9697483388610009 -2.591021441678856 +v 0.0625 0.9046302722359019 -2.960324349058434 +v 0.0625 0.7235464006079488 -2.5476093972621237 +v 0.0625 0.6584283339828498 -2.9169123046417016 +v -0.0625 0.9046302722359019 -2.960324349058434 +v -0.0625 0.9697483388610009 -2.591021441678856 +v -0.0625 0.6584283339828498 -2.9169123046417016 +v -0.0625 0.7235464006079488 -2.5476093972621237 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.17364817766693036 -0.9848077530122081 +vn 1 0 0 +vn 0 0.17364817766693036 0.9848077530122081 +vn -1 0 0 +vn 0 0.9848077530122081 -0.17364817766693036 +vn 0 -0.9848077530122081 0.17364817766693036 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o neck +v 0.3125 0.6308217096735025 -3.0666291698418497 +v 0.3125 0.469059806484427 -3.670332811272517 +v 0.3125 0.027118068242834803 -2.904867266652774 +v 0.3125 -0.13464383494624066 -3.5085709080834415 +v -0.3125 0.469059806484427 -3.670332811272517 +v -0.3125 0.6308217096735025 -3.0666291698418497 +v -0.3125 -0.13464383494624066 -3.5085709080834415 +v -0.3125 0.027118068242834803 -2.904867266652774 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.25881904510252074 -0.9659258262890683 +vn 1 0 0 +vn 0 0.25881904510252074 0.9659258262890683 +vn -1 0 0 +vn 0 0.9659258262890683 -0.25881904510252074 +vn 0 -0.9659258262890683 0.25881904510252074 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o neck +v 0.0625 0.8399507856079544 -3.252074659403613 +v 0.0625 0.7428936436945093 -3.6142968442620136 +v 0.0625 0.5984693290356873 -3.187369898127983 +v 0.0625 0.5014121871222421 -3.5495920829863836 +v -0.0625 0.7428936436945093 -3.6142968442620136 +v -0.0625 0.8399507856079544 -3.252074659403613 +v -0.0625 0.5014121871222421 -3.5495920829863836 +v -0.0625 0.5984693290356873 -3.187369898127983 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.25881904510252074 -0.9659258262890683 +vn 1 0 0 +vn 0 0.25881904510252074 0.9659258262890683 +vn -1 0 0 +vn 0 0.9659258262890683 -0.25881904510252074 +vn 0 -0.9659258262890683 0.25881904510252074 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o head +v 0.375 -0.07332885084174778 -4.433059161823504 +v 0.375 -0.41534899416741644 -5.372751782609413 +v 0.375 -0.3669827948373441 -4.326177867034232 +v 0.375 -0.7090029381630127 -5.265870487820141 +v -0.375 -0.41534899416741644 -5.372751782609413 +v -0.375 -0.07332885084174778 -4.433059161823504 +v -0.375 -0.7090029381630127 -5.265870487820141 +v -0.375 -0.3669827948373441 -4.326177867034232 +vt 0.75 0.765625 +vt 0.796875 0.765625 +vt 0.796875 0.74609375 +vt 0.75 0.74609375 +vt 0.6875 0.765625 +vt 0.75 0.765625 +vt 0.75 0.74609375 +vt 0.6875 0.74609375 +vt 0.859375 0.765625 +vt 0.90625 0.765625 +vt 0.90625 0.74609375 +vt 0.859375 0.74609375 +vt 0.796875 0.765625 +vt 0.859375 0.765625 +vt 0.859375 0.74609375 +vt 0.796875 0.74609375 +vt 0.796875 0.765625 +vt 0.75 0.765625 +vt 0.75 0.828125 +vt 0.796875 0.828125 +vt 0.84375 0.828125 +vt 0.796875 0.828125 +vt 0.796875 0.765625 +vt 0.84375 0.765625 +vn 0 -0.34202014332566877 -0.9396926207859084 +vn 1 0 0 +vn 0 0.34202014332566877 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 -0.34202014332566877 +vn 0 -0.9396926207859084 0.34202014332566877 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o head +v 0.5 0.6370542961620473 -3.760461931340814 +v 0.5 0.2950341528363787 -4.7001545521267225 +v 0.5 -0.3026383246238611 -3.418441788015145 +v 0.5 -0.6446584679495297 -4.358134408801053 +v -0.5 0.2950341528363787 -4.7001545521267225 +v -0.5 0.6370542961620473 -3.760461931340814 +v -0.5 -0.6446584679495297 -4.358134408801053 +v -0.5 -0.3026383246238611 -3.418441788015145 +vt 0.5 0.8203125 +vt 0.5625 0.8203125 +vt 0.5625 0.7578125 +vt 0.5 0.7578125 +vt 0.4375 0.8203125 +vt 0.5 0.8203125 +vt 0.5 0.7578125 +vt 0.4375 0.7578125 +vt 0.625 0.8203125 +vt 0.6875 0.8203125 +vt 0.6875 0.7578125 +vt 0.625 0.7578125 +vt 0.5625 0.8203125 +vt 0.625 0.8203125 +vt 0.625 0.7578125 +vt 0.5625 0.7578125 +vt 0.5625 0.8203125 +vt 0.5 0.8203125 +vt 0.5 0.8828125 +vt 0.5625 0.8828125 +vt 0.625 0.8828125 +vt 0.5625 0.8828125 +vt 0.5625 0.8203125 +vt 0.625 0.8203125 +vn 0 -0.34202014332566877 -0.9396926207859084 +vn 1 0 0 +vn 0 0.34202014332566877 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 -0.34202014332566877 +vn 0 -0.9396926207859084 0.34202014332566877 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o head +v 0.3125 0.7864724155271074 -4.080890122368709 +v 0.3125 0.6582148617799817 -4.433274855163424 +v 0.3125 0.5515492603306302 -3.9953850865372913 +v 0.3125 0.42329170658350446 -4.347769819332006 +v 0.1875 0.6582148617799817 -4.433274855163424 +v 0.1875 0.7864724155271074 -4.080890122368709 +v 0.1875 0.42329170658350446 -4.347769819332006 +v 0.1875 0.5515492603306302 -3.9953850865372913 +vt 0.03125 0.9765625 +vt 0.0234375 0.9765625 +vt 0.0234375 0.9609375 +vt 0.03125 0.9609375 +vt 0.0546875 0.9765625 +vt 0.03125 0.9765625 +vt 0.03125 0.9609375 +vt 0.0546875 0.9609375 +vt 0.0625 0.9765625 +vt 0.0546875 0.9765625 +vt 0.0546875 0.9609375 +vt 0.0625 0.9609375 +vt 0.0234375 0.9765625 +vt 0 0.9765625 +vt 0 0.9609375 +vt 0.0234375 0.9609375 +vt 0.0234375 0.9765625 +vt 0.03125 0.9765625 +vt 0.03125 1 +vt 0.0234375 1 +vt 0.03125 1 +vt 0.0390625 1 +vt 0.0390625 0.9765625 +vt 0.03125 0.9765625 +vn 0 -0.34202014332566877 -0.9396926207859084 +vn 1 0 0 +vn 0 0.34202014332566877 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 -0.34202014332566877 +vn 0 -0.9396926207859084 0.34202014332566877 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o head +v 0.3125 -0.16962986282205206 -5.063119567730405 +v 0.3125 -0.2551348986534694 -5.298042722926883 +v 0.3125 -0.28709144042029067 -5.020367049814697 +v 0.3125 -0.372596476251708 -5.255290205011175 +v 0.1875 -0.2551348986534694 -5.298042722926883 +v 0.1875 -0.16962986282205206 -5.063119567730405 +v 0.1875 -0.372596476251708 -5.255290205011175 +v 0.1875 -0.28709144042029067 -5.020367049814697 +vt 0.4609375 0.984375 +vt 0.453125 0.984375 +vt 0.453125 0.9765625 +vt 0.4609375 0.9765625 +vt 0.4765625 0.984375 +vt 0.4609375 0.984375 +vt 0.4609375 0.9765625 +vt 0.4765625 0.9765625 +vt 0.484375 0.984375 +vt 0.4765625 0.984375 +vt 0.4765625 0.9765625 +vt 0.484375 0.9765625 +vt 0.453125 0.984375 +vt 0.4375 0.984375 +vt 0.4375 0.9765625 +vt 0.453125 0.9765625 +vt 0.453125 0.984375 +vt 0.4609375 0.984375 +vt 0.4609375 1 +vt 0.453125 1 +vt 0.4609375 1 +vt 0.46875 1 +vt 0.46875 0.984375 +vt 0.4609375 0.984375 +vn 0 -0.34202014332566877 -0.9396926207859084 +vn 1 0 0 +vn 0 0.34202014332566877 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 -0.34202014332566877 +vn 0 -0.9396926207859084 0.34202014332566877 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o head +v -0.1875 0.7864724155271074 -4.080890122368709 +v -0.1875 0.6582148617799817 -4.433274855163424 +v -0.1875 0.5515492603306302 -3.9953850865372913 +v -0.1875 0.42329170658350446 -4.347769819332006 +v -0.3125 0.6582148617799817 -4.433274855163424 +v -0.3125 0.7864724155271074 -4.080890122368709 +v -0.3125 0.42329170658350446 -4.347769819332006 +v -0.3125 0.5515492603306302 -3.9953850865372913 +vt 0.0234375 0.9765625 +vt 0.03125 0.9765625 +vt 0.03125 0.9609375 +vt 0.0234375 0.9609375 +vt 0 0.9765625 +vt 0.0234375 0.9765625 +vt 0.0234375 0.9609375 +vt 0 0.9609375 +vt 0.0546875 0.9765625 +vt 0.0625 0.9765625 +vt 0.0625 0.9609375 +vt 0.0546875 0.9609375 +vt 0.03125 0.9765625 +vt 0.0546875 0.9765625 +vt 0.0546875 0.9609375 +vt 0.03125 0.9609375 +vt 0.03125 0.9765625 +vt 0.0234375 0.9765625 +vt 0.0234375 1 +vt 0.03125 1 +vt 0.0390625 1 +vt 0.03125 1 +vt 0.03125 0.9765625 +vt 0.0390625 0.9765625 +vn 0 -0.34202014332566877 -0.9396926207859084 +vn 1 0 0 +vn 0 0.34202014332566877 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 -0.34202014332566877 +vn 0 -0.9396926207859084 0.34202014332566877 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 +o head +v -0.1875 -0.16962986282205206 -5.063119567730405 +v -0.1875 -0.2551348986534694 -5.298042722926883 +v -0.1875 -0.28709144042029067 -5.020367049814697 +v -0.1875 -0.372596476251708 -5.255290205011175 +v -0.3125 -0.2551348986534694 -5.298042722926883 +v -0.3125 -0.16962986282205206 -5.063119567730405 +v -0.3125 -0.372596476251708 -5.255290205011175 +v -0.3125 -0.28709144042029067 -5.020367049814697 +vt 0.453125 0.984375 +vt 0.4609375 0.984375 +vt 0.4609375 0.9765625 +vt 0.453125 0.9765625 +vt 0.4375 0.984375 +vt 0.453125 0.984375 +vt 0.453125 0.9765625 +vt 0.4375 0.9765625 +vt 0.4765625 0.984375 +vt 0.484375 0.984375 +vt 0.484375 0.9765625 +vt 0.4765625 0.9765625 +vt 0.4609375 0.984375 +vt 0.4765625 0.984375 +vt 0.4765625 0.9765625 +vt 0.4609375 0.9765625 +vt 0.4609375 0.984375 +vt 0.453125 0.984375 +vt 0.453125 1 +vt 0.4609375 1 +vt 0.46875 1 +vt 0.4609375 1 +vt 0.4609375 0.984375 +vt 0.46875 0.984375 +vn 0 -0.34202014332566877 -0.9396926207859084 +vn 1 0 0 +vn 0 0.34202014332566877 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 -0.34202014332566877 +vn 0 -0.9396926207859084 0.34202014332566877 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 124/364/91 127/363/91 125/362/91 122/361/91 +f 123/368/92 124/367/92 122/366/92 121/365/92 +f 128/372/93 123/371/93 121/370/93 126/369/93 +f 127/376/94 128/375/94 126/374/94 125/373/94 +f 126/380/95 121/379/95 122/378/95 125/377/95 +f 127/384/96 124/383/96 123/382/96 128/381/96 +o jaw +v 0.375 -0.3814550631514302 -4.318644081003175 +v 0.375 -0.9550314995024762 -5.137796125292168 +v 0.375 -0.5862430742236779 -4.175249971915413 +v 0.375 -1.159819510574724 -4.994402016204406 +v -0.375 -0.9550314995024762 -5.137796125292168 +v -0.375 -0.3814550631514302 -4.318644081003175 +v -0.375 -1.159819510574724 -4.994402016204406 +v -0.375 -0.5862430742236779 -4.175249971915413 +vt 0.75 0.68359375 +vt 0.796875 0.68359375 +vt 0.796875 0.66796875 +vt 0.75 0.66796875 +vt 0.6875 0.68359375 +vt 0.75 0.68359375 +vt 0.75 0.66796875 +vt 0.6875 0.66796875 +vt 0.859375 0.68359375 +vt 0.90625 0.68359375 +vt 0.90625 0.66796875 +vt 0.859375 0.66796875 +vt 0.796875 0.68359375 +vt 0.859375 0.68359375 +vt 0.859375 0.66796875 +vt 0.796875 0.66796875 +vt 0.796875 0.68359375 +vt 0.75 0.68359375 +vt 0.75 0.74609375 +vt 0.796875 0.74609375 +vt 0.84375 0.74609375 +vt 0.796875 0.74609375 +vt 0.796875 0.68359375 +vt 0.84375 0.68359375 +vn 0 -0.5735764363510462 -0.8191520442889919 +vn 1 0 0 +vn 0 0.5735764363510462 0.8191520442889919 +vn -1 0 0 +vn 0 0.8191520442889919 -0.5735764363510462 +vn 0 -0.8191520442889919 0.5735764363510462 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 132/388/97 135/387/97 133/386/97 130/385/97 +f 131/392/98 132/391/98 130/390/98 129/389/98 +f 136/396/99 131/395/99 129/394/99 134/393/99 +f 135/400/100 136/399/100 134/398/100 133/397/100 +f 134/404/101 129/403/101 130/402/101 133/401/101 +f 135/408/102 132/407/102 131/406/102 136/405/102 +o body +v 0.75 1.25 3.5 +v 0.75 1.25 -0.5 +v 0.75 -0.25 3.5 +v 0.75 -0.25 -0.5 +v -0.75 1.25 -0.5 +v -0.75 1.25 3.5 +v -0.75 -0.25 -0.5 +v -0.75 -0.25 3.5 +vt 0.25 0.75 +vt 0.34375 0.75 +vt 0.34375 0.65625 +vt 0.25 0.65625 +vt 0 0.75 +vt 0.25 0.75 +vt 0.25 0.65625 +vt 0 0.65625 +vt 0.59375 0.75 +vt 0.6875 0.75 +vt 0.6875 0.65625 +vt 0.59375 0.65625 +vt 0.34375 0.75 +vt 0.59375 0.75 +vt 0.59375 0.65625 +vt 0.34375 0.65625 +vt 0.34375 0.75 +vt 0.25 0.75 +vt 0.25 1 +vt 0.34375 1 +vt 0.4375 1 +vt 0.34375 1 +vt 0.34375 0.75 +vt 0.4375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 140/412/103 143/411/103 141/410/103 138/409/103 +f 139/416/104 140/415/104 138/414/104 137/413/104 +f 144/420/105 139/419/105 137/418/105 142/417/105 +f 143/424/106 144/423/106 142/422/106 141/421/106 +f 142/428/107 137/427/107 138/426/107 141/425/107 +f 143/432/108 140/431/108 139/430/108 144/429/108 +o body +v 0.0625 1.625 0.625 +v 0.0625 1.625 -0.125 +v 0.0625 1.25 0.625 +v 0.0625 1.25 -0.125 +v -0.0625 1.625 -0.125 +v -0.0625 1.625 0.625 +v -0.0625 1.25 -0.125 +v -0.0625 1.25 0.625 +vt 0.90625 0.74609375 +vt 0.9140625 0.74609375 +vt 0.9140625 0.72265625 +vt 0.90625 0.72265625 +vt 0.859375 0.74609375 +vt 0.90625 0.74609375 +vt 0.90625 0.72265625 +vt 0.859375 0.72265625 +vt 0.9609375 0.74609375 +vt 0.96875 0.74609375 +vt 0.96875 0.72265625 +vt 0.9609375 0.72265625 +vt 0.9140625 0.74609375 +vt 0.9609375 0.74609375 +vt 0.9609375 0.72265625 +vt 0.9140625 0.72265625 +vt 0.9140625 0.74609375 +vt 0.90625 0.74609375 +vt 0.90625 0.79296875 +vt 0.9140625 0.79296875 +vt 0.921875 0.79296875 +vt 0.9140625 0.79296875 +vt 0.9140625 0.74609375 +vt 0.921875 0.74609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 148/436/109 151/435/109 149/434/109 146/433/109 +f 147/440/110 148/439/110 146/438/110 145/437/110 +f 152/444/111 147/443/111 145/442/111 150/441/111 +f 151/448/112 152/447/112 150/446/112 149/445/112 +f 150/452/113 145/451/113 146/450/113 149/449/113 +f 151/456/114 148/455/114 147/454/114 152/453/114 +o body +v 0.0625 1.625 1.875 +v 0.0625 1.625 1.125 +v 0.0625 1.25 1.875 +v 0.0625 1.25 1.125 +v -0.0625 1.625 1.125 +v -0.0625 1.625 1.875 +v -0.0625 1.25 1.125 +v -0.0625 1.25 1.875 +vt 0.90625 0.74609375 +vt 0.9140625 0.74609375 +vt 0.9140625 0.72265625 +vt 0.90625 0.72265625 +vt 0.859375 0.74609375 +vt 0.90625 0.74609375 +vt 0.90625 0.72265625 +vt 0.859375 0.72265625 +vt 0.9609375 0.74609375 +vt 0.96875 0.74609375 +vt 0.96875 0.72265625 +vt 0.9609375 0.72265625 +vt 0.9140625 0.74609375 +vt 0.9609375 0.74609375 +vt 0.9609375 0.72265625 +vt 0.9140625 0.72265625 +vt 0.9140625 0.74609375 +vt 0.90625 0.74609375 +vt 0.90625 0.79296875 +vt 0.9140625 0.79296875 +vt 0.921875 0.79296875 +vt 0.9140625 0.79296875 +vt 0.9140625 0.74609375 +vt 0.921875 0.74609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 156/460/115 159/459/115 157/458/115 154/457/115 +f 155/464/116 156/463/116 154/462/116 153/461/116 +f 160/468/117 155/467/117 153/466/117 158/465/117 +f 159/472/118 160/471/118 158/470/118 157/469/118 +f 158/476/119 153/475/119 154/474/119 157/473/119 +f 159/480/120 156/479/120 155/478/120 160/477/120 +o body +v 0.0625 1.625 3.125 +v 0.0625 1.625 2.375 +v 0.0625 1.25 3.125 +v 0.0625 1.25 2.375 +v -0.0625 1.625 2.375 +v -0.0625 1.625 3.125 +v -0.0625 1.25 2.375 +v -0.0625 1.25 3.125 +vt 0.90625 0.74609375 +vt 0.9140625 0.74609375 +vt 0.9140625 0.72265625 +vt 0.90625 0.72265625 +vt 0.859375 0.74609375 +vt 0.90625 0.74609375 +vt 0.90625 0.72265625 +vt 0.859375 0.72265625 +vt 0.9609375 0.74609375 +vt 0.96875 0.74609375 +vt 0.96875 0.72265625 +vt 0.9609375 0.72265625 +vt 0.9140625 0.74609375 +vt 0.9609375 0.74609375 +vt 0.9609375 0.72265625 +vt 0.9140625 0.72265625 +vt 0.9140625 0.74609375 +vt 0.90625 0.74609375 +vt 0.90625 0.79296875 +vt 0.9140625 0.79296875 +vt 0.921875 0.79296875 +vt 0.9140625 0.79296875 +vt 0.9140625 0.74609375 +vt 0.921875 0.74609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 164/484/121 167/483/121 165/482/121 162/481/121 +f 163/488/122 164/487/122 162/486/122 161/485/122 +f 168/492/123 163/491/123 161/490/123 166/489/123 +f 167/496/124 168/495/124 166/494/124 165/493/124 +f 166/500/125 161/499/125 162/498/125 165/497/125 +f 167/504/126 164/503/126 163/502/126 168/501/126 +o wing +v 4.0582975240428985 2.024698766671211 0.9789705600873082 +v 4.143802559874316 2.039775611474734 0.48656668358120414 +v 4.1451216128763635 1.5322948901651068 0.9789705600873082 +v 4.230626648707781 1.5473717349686296 0.48656668358120414 +v 0.7493404734989761 1.4412403606548134 -0.12120193825305198 +v 0.6638354376675588 1.4261635158512906 0.37120193825305203 +v 0.8361645623324412 0.9488364841487094 -0.12120193825305198 +v 0.7506595265010241 0.9337596393451866 0.37120193825305203 +vt 0.46875 0.625 +vt 0.6875 0.625 +vt 0.6875 0.59375 +vt 0.46875 0.59375 +vt 0.4375 0.625 +vt 0.46875 0.625 +vt 0.46875 0.59375 +vt 0.4375 0.59375 +vt 0.71875 0.625 +vt 0.9375 0.625 +vt 0.9375 0.59375 +vt 0.71875 0.59375 +vt 0.6875 0.625 +vt 0.71875 0.625 +vt 0.71875 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.625 +vt 0.46875 0.625 +vt 0.46875 0.65625 +vt 0.6875 0.65625 +vt 0.90625 0.65625 +vt 0.6875 0.65625 +vt 0.6875 0.625 +vt 0.90625 0.625 +vn 0.17101007166283433 0.030153689607045803 -0.984807753012208 +vn 0.9698463103929543 0.17101007166283436 0.17364817766693036 +vn -0.17101007166283433 -0.030153689607045803 0.984807753012208 +vn -0.9698463103929543 -0.17101007166283436 -0.17364817766693036 +vn -0.17364817766693033 0.984807753012208 0 +vn 0.17364817766693033 -0.984807753012208 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 172/508/127 175/507/127 173/506/127 170/505/127 +f 171/512/128 172/511/128 170/510/128 169/509/128 +f 176/516/129 171/515/129 169/514/129 174/513/129 +f 175/520/130 176/519/130 174/518/130 173/517/130 +f 174/524/131 169/523/131 170/522/131 173/521/131 +f 175/528/132 172/527/132 171/526/132 176/525/132 +o wing +v 3.5249413191357313 1.677431666078797 4.303420761460185 +v 4.123690332545229 1.7830072718154657 0.8553626162261914 +v 3.525158379357814 1.6762006563875311 4.303420761460185 +v 4.123907392767313 1.7817762621242004 0.8553626162261914 +v 0.7280159382818976 1.184258258405967 0.24737693416985151 +v 0.12926692487239855 1.0786826526692979 3.695435079403845 +v 0.728232998503981 1.1830272487147016 0.24737693416985151 +v 0.12948398509448222 1.0774516429780323 3.695435079403845 +vt 0 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.4375 +vt 0 0.4375 +vt -0.21875 0.4375 +vt 0 0.4375 +vt 0 0.4375 +vt -0.21875 0.4375 +vt 0.4375 0.4375 +vt 0.65625 0.4375 +vt 0.65625 0.4375 +vt 0.4375 0.4375 +vt 0.21875 0.4375 +vt 0.4375 0.4375 +vt 0.4375 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.4375 +vt 0 0.4375 +vt 0 0.65625 +vt 0.21875 0.65625 +vt 0.4375 0.65625 +vt 0.21875 0.65625 +vt 0.21875 0.4375 +vt 0.4375 0.4375 +vn 0.17101007166283433 0.030153689607045803 -0.984807753012208 +vn 0.9698463103929543 0.17101007166283436 0.17364817766693036 +vn -0.17101007166283433 -0.030153689607045803 0.984807753012208 +vn -0.9698463103929543 -0.17101007166283436 -0.17364817766693036 +vn -0.17364817766693033 0.984807753012208 0 +vn 0.17364817766693033 -0.984807753012208 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 180/532/133 183/531/133 181/530/133 178/529/133 +f 179/536/134 180/535/134 178/534/134 177/533/134 +f 184/540/135 179/539/135 177/538/135 182/537/135 +f 183/544/136 184/543/136 182/542/136 181/541/136 +f 182/548/137 177/547/137 178/546/137 181/545/137 +f 183/552/138 180/551/138 179/550/138 184/549/138 +o wingtip +v 7.541772295535269 1.2888090706299558 1.4344091768703897 +v 7.584524813450978 1.2963474930317171 1.1882072386173377 +v 7.499639529805786 1.0428327037253111 1.4195613832169185 +v 7.542392047721496 1.0503711261270725 1.1733594449638665 +v 4.186904728197936 1.912792645473123 0.6170915495344658 +v 4.144152210282227 1.9052542230713616 0.8632934877875178 +v 4.144771962468453 1.6668162785684784 0.6022437558809948 +v 4.102019444552744 1.659277856166717 0.8484456941340466 +vt 0.453125 0.453125 +vt 0.671875 0.453125 +vt 0.671875 0.4375 +vt 0.453125 0.4375 +vt 0.4375 0.453125 +vt 0.453125 0.453125 +vt 0.453125 0.4375 +vt 0.4375 0.4375 +vt 0.6875 0.453125 +vt 0.90625 0.453125 +vt 0.90625 0.4375 +vt 0.6875 0.4375 +vt 0.671875 0.453125 +vt 0.6875 0.453125 +vt 0.6875 0.4375 +vt 0.671875 0.4375 +vt 0.671875 0.453125 +vt 0.453125 0.453125 +vt 0.453125 0.46875 +vt 0.671875 0.46875 +vt 0.890625 0.46875 +vt 0.671875 0.46875 +vt 0.671875 0.453125 +vt 0.890625 0.453125 +vn 0.17101007166283436 0.0301536896070458 -0.9848077530122081 +vn 0.9707485957865836 -0.1761271864118303 0.16317591116653485 +vn -0.17101007166283436 -0.0301536896070458 0.9848077530122081 +vn -0.9707485957865836 0.1761271864118303 -0.16317591116653485 +vn 0.16853106291793427 0.9839054676185789 0.059391174613884684 +vn -0.16853106291793427 -0.9839054676185789 -0.059391174613884684 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 188/556/139 191/555/139 189/554/139 186/553/139 +f 187/560/140 188/559/140 186/558/140 185/557/140 +f 192/564/141 187/563/141 185/562/141 190/561/141 +f 191/568/142 192/567/142 190/566/142 189/565/142 +f 190/572/143 185/571/143 186/570/143 189/569/143 +f 191/576/144 188/575/144 187/574/144 192/573/144 +o wingtip +v 6.922775830342509 1.060768988922723 4.874567024860628 +v 7.521524843752008 1.1663445946593922 1.4265088796266343 +v 6.922565166513862 1.0595391070881996 4.874492785892361 +v 7.521314179923362 1.165114712824869 1.426434640658367 +v 4.122691322754232 1.7830099060838132 0.855189220654804 +v 3.523942309344733 1.677434300347144 4.303247365888798 +v 4.122480658925585 1.7817800242492896 0.8551149816865367 +v 3.5237316455160856 1.6762044185126204 4.30317312692053 +vt 0 0.21875 +vt 0.21875 0.21875 +vt 0.21875 0.21875 +vt 0 0.21875 +vt -0.21875 0.21875 +vt 0 0.21875 +vt 0 0.21875 +vt -0.21875 0.21875 +vt 0.4375 0.21875 +vt 0.65625 0.21875 +vt 0.65625 0.21875 +vt 0.4375 0.21875 +vt 0.21875 0.21875 +vt 0.4375 0.21875 +vt 0.4375 0.21875 +vt 0.21875 0.21875 +vt 0.21875 0.21875 +vt 0 0.21875 +vt 0 0.4375 +vt 0.21875 0.4375 +vt 0.4375 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.21875 +vt 0.4375 0.21875 +vn 0.17101007166283436 0.0301536896070458 -0.9848077530122081 +vn 0.9707485957865836 -0.1761271864118303 0.16317591116653485 +vn -0.17101007166283436 -0.0301536896070458 0.9848077530122081 +vn -0.9707485957865836 0.1761271864118303 -0.16317591116653485 +vn 0.16853106291793427 0.9839054676185789 0.059391174613884684 +vn -0.16853106291793427 -0.9839054676185789 -0.059391174613884684 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 196/580/145 199/579/145 197/578/145 194/577/145 +f 195/584/146 196/583/146 194/582/146 193/581/146 +f 200/588/147 195/587/147 193/586/147 198/585/147 +f 199/592/148 200/591/148 198/590/148 197/589/148 +f 198/596/149 193/595/149 194/594/149 197/593/149 +f 199/600/150 196/599/150 195/598/150 200/597/150 +o wing1 +v -0.6638354376675588 1.4261635158512906 0.37120193825305203 +v -0.749340473498976 1.4412403606548134 -0.12120193825305198 +v -0.7506595265010241 0.9337596393451866 0.37120193825305203 +v -0.8361645623324412 0.9488364841487094 -0.12120193825305198 +v -4.143802559874316 2.039775611474734 0.48656668358120414 +v -4.0582975240428985 2.024698766671211 0.9789705600873082 +v -4.230626648707781 1.5473717349686296 0.48656668358120414 +v -4.1451216128763635 1.5322948901651068 0.9789705600873082 +vt 0.6875 0.625 +vt 0.46875 0.625 +vt 0.46875 0.59375 +vt 0.6875 0.59375 +vt 0.71875 0.625 +vt 0.6875 0.625 +vt 0.6875 0.59375 +vt 0.71875 0.59375 +vt 0.9375 0.625 +vt 0.71875 0.625 +vt 0.71875 0.59375 +vt 0.9375 0.59375 +vt 0.46875 0.625 +vt 0.4375 0.625 +vt 0.4375 0.59375 +vt 0.46875 0.59375 +vt 0.46875 0.625 +vt 0.6875 0.625 +vt 0.6875 0.65625 +vt 0.46875 0.65625 +vt 0.6875 0.65625 +vt 0.90625 0.65625 +vt 0.90625 0.625 +vt 0.6875 0.625 +vn -0.17101007166283433 0.030153689607045803 -0.984807753012208 +vn 0.9698463103929543 -0.17101007166283436 -0.17364817766693036 +vn 0.17101007166283433 -0.030153689607045803 0.984807753012208 +vn -0.9698463103929543 0.17101007166283436 0.17364817766693036 +vn 0.17364817766693033 0.984807753012208 0 +vn -0.17364817766693033 -0.984807753012208 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 204/604/151 207/603/151 205/602/151 202/601/151 +f 203/608/152 204/607/152 202/606/152 201/605/152 +f 208/612/153 203/611/153 201/610/153 206/609/153 +f 207/616/154 208/615/154 206/614/154 205/613/154 +f 206/620/155 201/619/155 202/618/155 205/617/155 +f 207/624/156 204/623/156 203/622/156 208/621/156 +o wing1 +v -0.1292669248723986 1.0786826526692979 3.695435079403845 +v -0.7280159382818975 1.184258258405967 0.24737693416985151 +v -0.12948398509448228 1.0774516429780323 3.695435079403845 +v -0.7282329985039812 1.1830272487147016 0.24737693416985151 +v -4.123690332545229 1.7830072718154657 0.8553626162261914 +v -3.524941319135731 1.677431666078797 4.303420761460185 +v -4.123907392767313 1.7817762621242004 0.8553626162261914 +v -3.5251583793578143 1.6762006563875311 4.303420761460185 +vt 0.21875 0.4375 +vt 0 0.4375 +vt 0 0.4375 +vt 0.21875 0.4375 +vt 0.4375 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.4375 +vt 0.4375 0.4375 +vt 0.65625 0.4375 +vt 0.4375 0.4375 +vt 0.4375 0.4375 +vt 0.65625 0.4375 +vt 0 0.4375 +vt -0.21875 0.4375 +vt -0.21875 0.4375 +vt 0 0.4375 +vt 0 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.65625 +vt 0 0.65625 +vt 0.21875 0.65625 +vt 0.4375 0.65625 +vt 0.4375 0.4375 +vt 0.21875 0.4375 +vn -0.17101007166283433 0.030153689607045803 -0.984807753012208 +vn 0.9698463103929543 -0.17101007166283436 -0.17364817766693036 +vn 0.17101007166283433 -0.030153689607045803 0.984807753012208 +vn -0.9698463103929543 0.17101007166283436 0.17364817766693036 +vn 0.17364817766693033 0.984807753012208 0 +vn -0.17364817766693033 -0.984807753012208 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 212/628/157 215/627/157 213/626/157 210/625/157 +f 211/632/158 212/631/158 210/630/158 209/629/158 +f 216/636/159 211/635/159 209/634/159 214/633/159 +f 215/640/160 216/639/160 214/638/160 213/637/160 +f 214/644/161 209/643/161 210/642/161 213/641/161 +f 215/648/162 212/647/162 211/646/162 216/645/162 +o wingtip1 +v -4.144152210282227 1.9052542230713616 0.8632934877875178 +v -4.186904728197936 1.912792645473123 0.6170915495344658 +v -4.102019444552744 1.659277856166717 0.8484456941340466 +v -4.144771962468453 1.6668162785684784 0.6022437558809948 +v -7.584524813450978 1.2963474930317171 1.1882072386173377 +v -7.541772295535269 1.2888090706299558 1.4344091768703897 +v -7.542392047721495 1.0503711261270725 1.1733594449638665 +v -7.499639529805786 1.0428327037253111 1.4195613832169185 +vt 0.671875 0.453125 +vt 0.453125 0.453125 +vt 0.453125 0.4375 +vt 0.671875 0.4375 +vt 0.6875 0.453125 +vt 0.671875 0.453125 +vt 0.671875 0.4375 +vt 0.6875 0.4375 +vt 0.90625 0.453125 +vt 0.6875 0.453125 +vt 0.6875 0.4375 +vt 0.90625 0.4375 +vt 0.453125 0.453125 +vt 0.4375 0.453125 +vt 0.4375 0.4375 +vt 0.453125 0.4375 +vt 0.453125 0.453125 +vt 0.671875 0.453125 +vt 0.671875 0.46875 +vt 0.453125 0.46875 +vt 0.671875 0.46875 +vt 0.890625 0.46875 +vt 0.890625 0.453125 +vt 0.671875 0.453125 +vn -0.17101007166283436 0.0301536896070458 -0.9848077530122081 +vn 0.9707485957865836 0.1761271864118303 -0.16317591116653485 +vn 0.17101007166283436 -0.0301536896070458 0.9848077530122081 +vn -0.9707485957865836 -0.1761271864118303 0.16317591116653485 +vn -0.16853106291793427 0.9839054676185789 0.059391174613884684 +vn 0.16853106291793427 -0.9839054676185789 -0.059391174613884684 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 220/652/163 223/651/163 221/650/163 218/649/163 +f 219/656/164 220/655/164 218/654/164 217/653/164 +f 224/660/165 219/659/165 217/658/165 222/657/165 +f 223/664/166 224/663/166 222/662/166 221/661/166 +f 222/668/167 217/667/167 218/666/167 221/665/167 +f 223/672/168 220/671/168 219/670/168 224/669/168 +o wingtip1 +v -3.523942309344733 1.677434300347144 4.303247365888798 +v -4.122691322754232 1.7830099060838132 0.855189220654804 +v -3.5237316455160856 1.6762044185126204 4.30317312692053 +v -4.122480658925585 1.7817800242492896 0.8551149816865367 +v -7.521524843752008 1.1663445946593922 1.4265088796266343 +v -6.922775830342509 1.060768988922723 4.874567024860628 +v -7.521314179923361 1.165114712824869 1.426434640658367 +v -6.922565166513862 1.0595391070881996 4.874492785892361 +vt 0.21875 0.21875 +vt 0 0.21875 +vt 0 0.21875 +vt 0.21875 0.21875 +vt 0.4375 0.21875 +vt 0.21875 0.21875 +vt 0.21875 0.21875 +vt 0.4375 0.21875 +vt 0.65625 0.21875 +vt 0.4375 0.21875 +vt 0.4375 0.21875 +vt 0.65625 0.21875 +vt 0 0.21875 +vt -0.21875 0.21875 +vt -0.21875 0.21875 +vt 0 0.21875 +vt 0 0.21875 +vt 0.21875 0.21875 +vt 0.21875 0.4375 +vt 0 0.4375 +vt 0.21875 0.4375 +vt 0.4375 0.4375 +vt 0.4375 0.21875 +vt 0.21875 0.21875 +vn -0.17101007166283436 0.0301536896070458 -0.9848077530122081 +vn 0.9707485957865836 0.1761271864118303 -0.16317591116653485 +vn 0.17101007166283436 -0.0301536896070458 0.9848077530122081 +vn -0.9707485957865836 -0.1761271864118303 0.16317591116653485 +vn -0.16853106291793427 0.9839054676185789 0.059391174613884684 +vn 0.16853106291793427 -0.9839054676185789 -0.059391174613884684 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 228/676/169 231/675/169 229/674/169 226/673/169 +f 227/680/170 228/679/170 226/678/170 225/677/170 +f 232/684/171 227/683/171 225/682/171 230/681/171 +f 231/688/172 232/687/172 230/686/172 229/685/172 +f 230/692/173 225/691/173 226/690/173 229/689/173 +f 231/696/174 228/695/174 227/694/174 232/693/174 +o rearleg +v 1.5 1.0580127018922192 2.6584936490538906 +v 1.5 0.1919872981077808 2.1584936490538906 +v 1.5 0.05801270189221919 4.390544456622768 +v 1.5 -0.8080127018922196 3.8905444566227683 +v 0.5 0.1919872981077808 2.1584936490538906 +v 0.5 1.0580127018922192 2.6584936490538906 +v 0.5 -0.8080127018922196 3.8905444566227683 +v 0.5 0.05801270189221919 4.390544456622768 +vt 0.0625 0.9375 +vt 0.125 0.9375 +vt 0.125 0.8125 +vt 0.0625 0.8125 +vt 0 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.8125 +vt 0 0.8125 +vt 0.1875 0.9375 +vt 0.25 0.9375 +vt 0.25 0.8125 +vt 0.1875 0.8125 +vt 0.125 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.8125 +vt 0.125 0.8125 +vt 0.125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 1 +vt 0.125 1 +vt 0.1875 1 +vt 0.125 1 +vt 0.125 0.9375 +vt 0.1875 0.9375 +vn 0 -0.8660254037844386 -0.5000000000000001 +vn 1 0 0 +vn 0 0.8660254037844386 0.5000000000000001 +vn -1 0 0 +vn 0 0.5000000000000001 -0.8660254037844386 +vn 0 -0.5000000000000001 0.8660254037844386 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 236/700/175 239/699/175 237/698/175 234/697/175 +f 235/704/176 236/703/176 234/702/176 233/701/176 +f 240/708/177 235/707/177 233/706/177 238/705/177 +f 239/712/178 240/711/178 238/710/178 237/709/178 +f 238/716/179 233/715/179 234/714/179 237/713/179 +f 239/720/180 236/719/180 235/718/180 240/717/180 +o rearlegtip +v 1.375 -0.009562569109802244 4.19903062623141 +v 1.375 -0.7567085926786117 4.133663819170667 +v 1.375 -0.18387405460511896 6.191420022414902 +v 1.375 -0.9310200781739284 6.126053215354158 +v 0.625 -0.7567085926786117 4.133663819170667 +v 0.625 -0.009562569109802244 4.19903062623141 +v 0.625 -0.9310200781739284 6.126053215354158 +v 0.625 -0.18387405460511896 6.191420022414902 +vt 0.8125 0.953125 +vt 0.859375 0.953125 +vt 0.859375 0.828125 +vt 0.8125 0.828125 +vt 0.765625 0.953125 +vt 0.8125 0.953125 +vt 0.8125 0.828125 +vt 0.765625 0.828125 +vt 0.90625 0.953125 +vt 0.953125 0.953125 +vt 0.953125 0.828125 +vt 0.90625 0.828125 +vt 0.859375 0.953125 +vt 0.90625 0.953125 +vt 0.90625 0.828125 +vt 0.859375 0.828125 +vt 0.859375 0.953125 +vt 0.8125 0.953125 +vt 0.8125 1 +vt 0.859375 1 +vt 0.90625 1 +vt 0.859375 1 +vt 0.859375 0.953125 +vt 0.90625 0.953125 +vn 0 -0.9961946980917455 -0.0871557427476583 +vn 1 0 0 +vn 0 0.9961946980917455 0.0871557427476583 +vn -1 0 0 +vn 0 0.0871557427476583 -0.9961946980917455 +vn 0 -0.0871557427476583 0.9961946980917455 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 244/724/181 247/723/181 245/722/181 242/721/181 +f 243/728/182 244/727/182 242/726/182 241/725/182 +f 248/732/183 243/731/183 241/730/183 246/729/183 +f 247/736/184 248/735/184 246/734/184 245/733/184 +f 246/740/185 241/739/185 242/738/185 245/737/185 +f 247/744/186 244/743/186 243/742/186 248/741/186 +o rearfoot +v 1.5625 -0.42819812424051307 5.992592482541166 +v 1.5625 -1.5772647889189804 6.956773897070976 +v 1.5625 -0.187152770608062 6.279859148710783 +v 1.5625 -1.3362194352865284 7.244040563240592 +v 0.4375 -1.5772647889189804 6.956773897070976 +v 0.4375 -0.42819812424051307 5.992592482541166 +v 0.4375 -1.3362194352865284 7.244040563240592 +v 0.4375 -0.187152770608062 6.279859148710783 +vt 0.53125 0.90625 +vt 0.6015625 0.90625 +vt 0.6015625 0.8828125 +vt 0.53125 0.8828125 +vt 0.4375 0.90625 +vt 0.53125 0.90625 +vt 0.53125 0.8828125 +vt 0.4375 0.8828125 +vt 0.6953125 0.90625 +vt 0.765625 0.90625 +vt 0.765625 0.8828125 +vt 0.6953125 0.8828125 +vt 0.6015625 0.90625 +vt 0.6953125 0.90625 +vt 0.6953125 0.8828125 +vt 0.6015625 0.8828125 +vt 0.6015625 0.90625 +vt 0.53125 0.90625 +vt 0.53125 1 +vt 0.6015625 1 +vt 0.671875 1 +vt 0.6015625 1 +vt 0.6015625 0.90625 +vt 0.671875 0.90625 +vn 0 -0.766044443118978 0.6427876096865393 +vn 1 0 0 +vn 0 0.766044443118978 -0.6427876096865393 +vn -1 0 0 +vn 0 -0.6427876096865393 -0.766044443118978 +vn 0 0.6427876096865393 0.766044443118978 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 252/748/187 255/747/187 253/746/187 250/745/187 +f 251/752/188 252/751/188 250/750/188 249/749/188 +f 256/756/189 251/755/189 249/754/189 254/753/189 +f 255/760/190 256/759/190 254/758/190 253/757/190 +f 254/764/191 249/763/191 250/762/191 253/761/191 +f 255/768/192 252/767/192 251/766/192 256/765/192 +o rearleg1 +v -0.5 1.0580127018922192 2.6584936490538906 +v -0.5 0.1919872981077808 2.1584936490538906 +v -0.5 0.05801270189221919 4.390544456622768 +v -0.5 -0.8080127018922196 3.8905444566227683 +v -1.5 0.1919872981077808 2.1584936490538906 +v -1.5 1.0580127018922192 2.6584936490538906 +v -1.5 -0.8080127018922196 3.8905444566227683 +v -1.5 0.05801270189221919 4.390544456622768 +vt 0.125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.8125 +vt 0.125 0.8125 +vt 0.1875 0.9375 +vt 0.125 0.9375 +vt 0.125 0.8125 +vt 0.1875 0.8125 +vt 0.25 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.8125 +vt 0.25 0.8125 +vt 0.0625 0.9375 +vt 0 0.9375 +vt 0 0.8125 +vt 0.0625 0.8125 +vt 0.0625 0.9375 +vt 0.125 0.9375 +vt 0.125 1 +vt 0.0625 1 +vt 0.125 1 +vt 0.1875 1 +vt 0.1875 0.9375 +vt 0.125 0.9375 +vn 0 -0.8660254037844386 -0.5000000000000001 +vn 1 0 0 +vn 0 0.8660254037844386 0.5000000000000001 +vn -1 0 0 +vn 0 0.5000000000000001 -0.8660254037844386 +vn 0 -0.5000000000000001 0.8660254037844386 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 260/772/193 263/771/193 261/770/193 258/769/193 +f 259/776/194 260/775/194 258/774/194 257/773/194 +f 264/780/195 259/779/195 257/778/195 262/777/195 +f 263/784/196 264/783/196 262/782/196 261/781/196 +f 262/788/197 257/787/197 258/786/197 261/785/197 +f 263/792/198 260/791/198 259/790/198 264/789/198 +o rearlegtip +v -0.625 -0.009562569109802244 4.19903062623141 +v -0.625 -0.7567085926786117 4.133663819170667 +v -0.625 -0.18387405460511896 6.191420022414902 +v -0.625 -0.9310200781739284 6.126053215354158 +v -1.375 -0.7567085926786117 4.133663819170667 +v -1.375 -0.009562569109802244 4.19903062623141 +v -1.375 -0.9310200781739284 6.126053215354158 +v -1.375 -0.18387405460511896 6.191420022414902 +vt 0.859375 0.953125 +vt 0.8125 0.953125 +vt 0.8125 0.828125 +vt 0.859375 0.828125 +vt 0.90625 0.953125 +vt 0.859375 0.953125 +vt 0.859375 0.828125 +vt 0.90625 0.828125 +vt 0.953125 0.953125 +vt 0.90625 0.953125 +vt 0.90625 0.828125 +vt 0.953125 0.828125 +vt 0.8125 0.953125 +vt 0.765625 0.953125 +vt 0.765625 0.828125 +vt 0.8125 0.828125 +vt 0.8125 0.953125 +vt 0.859375 0.953125 +vt 0.859375 1 +vt 0.8125 1 +vt 0.859375 1 +vt 0.90625 1 +vt 0.90625 0.953125 +vt 0.859375 0.953125 +vn 0 -0.9961946980917455 -0.0871557427476583 +vn 1 0 0 +vn 0 0.9961946980917455 0.0871557427476583 +vn -1 0 0 +vn 0 0.0871557427476583 -0.9961946980917455 +vn 0 -0.0871557427476583 0.9961946980917455 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 268/796/199 271/795/199 269/794/199 266/793/199 +f 267/800/200 268/799/200 266/798/200 265/797/200 +f 272/804/201 267/803/201 265/802/201 270/801/201 +f 271/808/202 272/807/202 270/806/202 269/805/202 +f 270/812/203 265/811/203 266/810/203 269/809/203 +f 271/816/204 268/815/204 267/814/204 272/813/204 +o rearfoot +v -0.4375 -0.42819812424051307 5.992592482541166 +v -0.4375 -1.5772647889189804 6.956773897070976 +v -0.4375 -0.187152770608062 6.279859148710783 +v -0.4375 -1.3362194352865284 7.244040563240592 +v -1.5625 -1.5772647889189804 6.956773897070976 +v -1.5625 -0.42819812424051307 5.992592482541166 +v -1.5625 -1.3362194352865284 7.244040563240592 +v -1.5625 -0.187152770608062 6.279859148710783 +vt 0.6015625 0.90625 +vt 0.53125 0.90625 +vt 0.53125 0.8828125 +vt 0.6015625 0.8828125 +vt 0.6953125 0.90625 +vt 0.6015625 0.90625 +vt 0.6015625 0.8828125 +vt 0.6953125 0.8828125 +vt 0.765625 0.90625 +vt 0.6953125 0.90625 +vt 0.6953125 0.8828125 +vt 0.765625 0.8828125 +vt 0.53125 0.90625 +vt 0.4375 0.90625 +vt 0.4375 0.8828125 +vt 0.53125 0.8828125 +vt 0.53125 0.90625 +vt 0.6015625 0.90625 +vt 0.6015625 1 +vt 0.53125 1 +vt 0.6015625 1 +vt 0.671875 1 +vt 0.671875 0.90625 +vt 0.6015625 0.90625 +vn 0 -0.766044443118978 0.6427876096865393 +vn 1 0 0 +vn 0 0.766044443118978 -0.6427876096865393 +vn -1 0 0 +vn 0 -0.6427876096865393 -0.766044443118978 +vn 0 0.6427876096865393 0.766044443118978 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 276/820/205 279/819/205 277/818/205 274/817/205 +f 275/824/206 276/823/206 274/822/206 273/821/206 +f 280/828/207 275/827/207 273/826/207 278/825/207 +f 279/832/208 280/831/208 278/830/208 277/829/208 +f 278/836/209 273/835/209 274/834/209 277/833/209 +f 279/840/210 276/839/210 275/838/210 280/837/210 +o frontleg +v 1 0.5822315121943373 0.004077618676012307 +v 1 0.1290776186760123 -0.20723151219433733 +v 1 -0.05169588041671158 1.3635392992309874 +v 1 -0.5048497739350366 1.1522301683606377 +v 0.5 0.1290776186760123 -0.20723151219433733 +v 0.5 0.5822315121943373 0.004077618676012307 +v 0.5 -0.5048497739350366 1.1522301683606377 +v 0.5 -0.05169588041671158 1.3635392992309874 +vt 0.46875 0.5625 +vt 0.5 0.5625 +vt 0.5 0.46875 +vt 0.46875 0.46875 +vt 0.4375 0.5625 +vt 0.46875 0.5625 +vt 0.46875 0.46875 +vt 0.4375 0.46875 +vt 0.53125 0.5625 +vt 0.5625 0.5625 +vt 0.5625 0.46875 +vt 0.53125 0.46875 +vt 0.5 0.5625 +vt 0.53125 0.5625 +vt 0.53125 0.46875 +vt 0.5 0.46875 +vt 0.5 0.5625 +vt 0.46875 0.5625 +vt 0.46875 0.59375 +vt 0.5 0.59375 +vt 0.53125 0.59375 +vt 0.5 0.59375 +vt 0.5 0.5625 +vt 0.53125 0.5625 +vn 0 -0.90630778703665 -0.4226182617406993 +vn 1 0 0 +vn 0 0.90630778703665 0.4226182617406993 +vn -1 0 0 +vn 0 0.4226182617406993 -0.90630778703665 +vn 0 -0.4226182617406993 0.90630778703665 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 284/844/211 287/843/211 285/842/211 282/841/211 +f 283/848/212 284/847/212 282/846/212 281/845/212 +f 288/852/213 283/851/213 281/850/213 286/849/213 +f 287/856/214 288/855/214 286/854/214 285/853/214 +f 286/860/215 281/859/215 282/858/215 285/857/215 +f 287/864/216 284/863/216 283/862/216 288/861/216 +o frontlegtip +v 0.9375 -0.10149613187923717 1.346273081444131 +v 0.9375 -0.3666611748241926 1.0811080384991758 +v 0.9375 -1.1621563036590583 2.4069332532239525 +v 0.9375 -1.4273213466040136 2.141768210278997 +v 0.5625 -0.3666611748241926 1.0811080384991758 +v 0.5625 -0.10149613187923717 1.346273081444131 +v 0.5625 -1.4273213466040136 2.141768210278997 +v 0.5625 -1.1621563036590583 2.4069332532239525 +vt 0.90625 0.4375 +vt 0.9296875 0.4375 +vt 0.9296875 0.34375 +vt 0.90625 0.34375 +vt 0.8828125 0.4375 +vt 0.90625 0.4375 +vt 0.90625 0.34375 +vt 0.8828125 0.34375 +vt 0.953125 0.4375 +vt 0.9765625 0.4375 +vt 0.9765625 0.34375 +vt 0.953125 0.34375 +vt 0.9296875 0.4375 +vt 0.953125 0.4375 +vt 0.953125 0.34375 +vt 0.9296875 0.34375 +vt 0.9296875 0.4375 +vt 0.90625 0.4375 +vt 0.90625 0.4609375 +vt 0.9296875 0.4609375 +vt 0.953125 0.4609375 +vt 0.9296875 0.4609375 +vt 0.9296875 0.4375 +vt 0.953125 0.4375 +vn 0 -0.7071067811865477 -0.7071067811865474 +vn 1 0 0 +vn 0 0.7071067811865477 0.7071067811865474 +vn -1 0 0 +vn 0 0.7071067811865474 -0.7071067811865477 +vn 0 -0.7071067811865474 0.7071067811865477 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 292/868/217 295/867/217 293/866/217 290/865/217 +f 291/872/218 292/871/218 290/870/218 289/869/218 +f 296/876/219 291/875/219 289/874/219 294/873/219 +f 295/880/220 296/879/220 294/878/220 293/877/220 +f 294/884/221 289/883/221 290/882/221 293/881/221 +f 295/888/222 292/887/222 291/886/222 296/885/222 +o frontfoot +v 1 -1.0005446513073766 2.2301565579273155 +v 1 -2.0005446513073766 2.2301565579273155 +v 1 -1.0005446513073766 2.4801565579273155 +v 1 -2.0005446513073766 2.4801565579273155 +v 0.5 -2.0005446513073766 2.2301565579273155 +v 0.5 -1.0005446513073766 2.2301565579273155 +v 0.5 -2.0005446513073766 2.4801565579273155 +v 0.5 -1.0005446513073766 2.4801565579273155 +vt 0.625 0.53125 +vt 0.65625 0.53125 +vt 0.65625 0.515625 +vt 0.625 0.515625 +vt 0.5625 0.53125 +vt 0.625 0.53125 +vt 0.625 0.515625 +vt 0.5625 0.515625 +vt 0.71875 0.53125 +vt 0.75 0.53125 +vt 0.75 0.515625 +vt 0.71875 0.515625 +vt 0.65625 0.53125 +vt 0.71875 0.53125 +vt 0.71875 0.515625 +vt 0.65625 0.515625 +vt 0.65625 0.53125 +vt 0.625 0.53125 +vt 0.625 0.59375 +vt 0.65625 0.59375 +vt 0.6875 0.59375 +vt 0.65625 0.59375 +vt 0.65625 0.53125 +vt 0.6875 0.53125 +vn 0 -1 2.7755575615628914e-16 +vn 1 0 0 +vn 0 1 -2.7755575615628914e-16 +vn -1 0 0 +vn 0 -2.7755575615628914e-16 -1 +vn 0 2.7755575615628914e-16 1 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 300/892/223 303/891/223 301/890/223 298/889/223 +f 299/896/224 300/895/224 298/894/224 297/893/224 +f 304/900/225 299/899/225 297/898/225 302/897/225 +f 303/904/226 304/903/226 302/902/226 301/901/226 +f 302/908/227 297/907/227 298/906/227 301/905/227 +f 303/912/228 300/911/228 299/910/228 304/909/228 +o frontleg1 +v -0.5 0.5822315121943373 0.004077618676012307 +v -0.5 0.1290776186760123 -0.20723151219433733 +v -0.5 -0.05169588041671158 1.3635392992309874 +v -0.5 -0.5048497739350366 1.1522301683606377 +v -1 0.1290776186760123 -0.20723151219433733 +v -1 0.5822315121943373 0.004077618676012307 +v -1 -0.5048497739350366 1.1522301683606377 +v -1 -0.05169588041671158 1.3635392992309874 +vt 0.5 0.5625 +vt 0.46875 0.5625 +vt 0.46875 0.46875 +vt 0.5 0.46875 +vt 0.53125 0.5625 +vt 0.5 0.5625 +vt 0.5 0.46875 +vt 0.53125 0.46875 +vt 0.5625 0.5625 +vt 0.53125 0.5625 +vt 0.53125 0.46875 +vt 0.5625 0.46875 +vt 0.46875 0.5625 +vt 0.4375 0.5625 +vt 0.4375 0.46875 +vt 0.46875 0.46875 +vt 0.46875 0.5625 +vt 0.5 0.5625 +vt 0.5 0.59375 +vt 0.46875 0.59375 +vt 0.5 0.59375 +vt 0.53125 0.59375 +vt 0.53125 0.5625 +vt 0.5 0.5625 +vn 0 -0.90630778703665 -0.4226182617406993 +vn 1 0 0 +vn 0 0.90630778703665 0.4226182617406993 +vn -1 0 0 +vn 0 0.4226182617406993 -0.90630778703665 +vn 0 -0.4226182617406993 0.90630778703665 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 308/916/229 311/915/229 309/914/229 306/913/229 +f 307/920/230 308/919/230 306/918/230 305/917/230 +f 312/924/231 307/923/231 305/922/231 310/921/231 +f 311/928/232 312/927/232 310/926/232 309/925/232 +f 310/932/233 305/931/233 306/930/233 309/929/233 +f 311/936/234 308/935/234 307/934/234 312/933/234 +o frontlegtip +v -0.5625 -0.10149613187923717 1.346273081444131 +v -0.5625 -0.3666611748241926 1.0811080384991758 +v -0.5625 -1.1621563036590583 2.4069332532239525 +v -0.5625 -1.4273213466040136 2.141768210278997 +v -0.9375 -0.3666611748241926 1.0811080384991758 +v -0.9375 -0.10149613187923717 1.346273081444131 +v -0.9375 -1.4273213466040136 2.141768210278997 +v -0.9375 -1.1621563036590583 2.4069332532239525 +vt 0.9296875 0.4375 +vt 0.90625 0.4375 +vt 0.90625 0.34375 +vt 0.9296875 0.34375 +vt 0.953125 0.4375 +vt 0.9296875 0.4375 +vt 0.9296875 0.34375 +vt 0.953125 0.34375 +vt 0.9765625 0.4375 +vt 0.953125 0.4375 +vt 0.953125 0.34375 +vt 0.9765625 0.34375 +vt 0.90625 0.4375 +vt 0.8828125 0.4375 +vt 0.8828125 0.34375 +vt 0.90625 0.34375 +vt 0.90625 0.4375 +vt 0.9296875 0.4375 +vt 0.9296875 0.4609375 +vt 0.90625 0.4609375 +vt 0.9296875 0.4609375 +vt 0.953125 0.4609375 +vt 0.953125 0.4375 +vt 0.9296875 0.4375 +vn 0 -0.7071067811865477 -0.7071067811865474 +vn 1 0 0 +vn 0 0.7071067811865477 0.7071067811865474 +vn -1 0 0 +vn 0 0.7071067811865474 -0.7071067811865477 +vn 0 -0.7071067811865474 0.7071067811865477 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 316/940/235 319/939/235 317/938/235 314/937/235 +f 315/944/236 316/943/236 314/942/236 313/941/236 +f 320/948/237 315/947/237 313/946/237 318/945/237 +f 319/952/238 320/951/238 318/950/238 317/949/238 +f 318/956/239 313/955/239 314/954/239 317/953/239 +f 319/960/240 316/959/240 315/958/240 320/957/240 +o frontfoot +v -0.5 -1.0005446513073766 2.2301565579273155 +v -0.5 -2.0005446513073766 2.2301565579273155 +v -0.5 -1.0005446513073766 2.4801565579273155 +v -0.5 -2.0005446513073766 2.4801565579273155 +v -1 -2.0005446513073766 2.2301565579273155 +v -1 -1.0005446513073766 2.2301565579273155 +v -1 -2.0005446513073766 2.4801565579273155 +v -1 -1.0005446513073766 2.4801565579273155 +vt 0.65625 0.53125 +vt 0.625 0.53125 +vt 0.625 0.515625 +vt 0.65625 0.515625 +vt 0.71875 0.53125 +vt 0.65625 0.53125 +vt 0.65625 0.515625 +vt 0.71875 0.515625 +vt 0.75 0.53125 +vt 0.71875 0.53125 +vt 0.71875 0.515625 +vt 0.75 0.515625 +vt 0.625 0.53125 +vt 0.5625 0.53125 +vt 0.5625 0.515625 +vt 0.625 0.515625 +vt 0.625 0.53125 +vt 0.65625 0.53125 +vt 0.65625 0.59375 +vt 0.625 0.59375 +vt 0.65625 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.53125 +vt 0.65625 0.53125 +vn 0 -1 2.7755575615628914e-16 +vn 1 0 0 +vn 0 1 -2.7755575615628914e-16 +vn -1 0 0 +vn 0 -2.7755575615628914e-16 -1 +vn 0 2.7755575615628914e-16 1 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 324/964/241 327/963/241 325/962/241 322/961/241 +f 323/968/242 324/967/242 322/966/242 321/965/242 +f 328/972/243 323/971/243 321/970/243 326/969/243 +f 327/976/244 328/975/244 326/974/244 325/973/244 +f 326/980/245 321/979/245 322/978/245 325/977/245 +f 327/984/246 324/983/246 323/982/246 328/981/246 +o tail +v 0.3125 1.1875 4.125 +v 0.3125 1.1875 3.5 +v 0.3125 0.5625 4.125 +v 0.3125 0.5625 3.5 +v -0.3125 1.1875 3.5 +v -0.3125 1.1875 4.125 +v -0.3125 0.5625 3.5 +v -0.3125 0.5625 4.125 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 332/988/247 335/987/247 333/986/247 330/985/247 +f 331/992/248 332/991/248 330/990/248 329/989/248 +f 336/996/249 331/995/249 329/994/249 334/993/249 +f 335/1000/250 336/999/250 334/998/250 333/997/250 +f 334/1004/251 329/1003/251 330/1002/251 333/1001/251 +f 335/1008/252 332/1007/252 331/1006/252 336/1005/252 +o tail +v 0.0625 1.4375 4 +v 0.0625 1.4375 3.625 +v 0.0625 1.1875 4 +v 0.0625 1.1875 3.625 +v -0.0625 1.4375 3.625 +v -0.0625 1.4375 4 +v -0.0625 1.1875 3.625 +v -0.0625 1.1875 4 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 340/1012/253 343/1011/253 341/1010/253 338/1009/253 +f 339/1016/254 340/1015/254 338/1014/254 337/1013/254 +f 344/1020/255 339/1019/255 337/1018/255 342/1017/255 +f 343/1024/256 344/1023/256 342/1022/256 341/1021/256 +f 342/1028/257 337/1027/257 338/1026/257 341/1025/257 +f 343/1032/258 340/1031/258 339/1030/258 344/1029/258 +o tail +v 0.3125 1.1983601587596744 4.744450932461093 +v 0.3125 1.1874524047363721 4.119546122988348 +v 0.3125 0.5734553492869299 4.755358686484396 +v 0.3125 0.5625475952636276 4.130453877011651 +v -0.3125 1.1874524047363721 4.119546122988348 +v -0.3125 1.1983601587596744 4.744450932461093 +v -0.3125 0.5625475952636276 4.130453877011651 +v -0.3125 0.5734553492869299 4.755358686484396 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.01745240643728351 -0.9998476951563913 +vn 1 0 0 +vn 0 0.01745240643728351 0.9998476951563913 +vn -1 0 0 +vn 0 0.9998476951563913 -0.01745240643728351 +vn 0 -0.9998476951563913 0.01745240643728351 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 348/1036/259 351/1035/259 349/1034/259 346/1033/259 +f 347/1040/260 348/1039/260 346/1038/260 345/1037/260 +f 352/1044/261 347/1043/261 345/1042/261 350/1041/261 +f 351/1048/262 352/1047/262 350/1046/262 349/1045/262 +f 350/1052/263 345/1051/263 346/1050/263 349/1049/263 +f 351/1056/264 348/1055/264 347/1054/264 352/1053/264 +o tail +v 0.0625 1.4461405317441118 4.615106868957223 +v 0.0625 1.4395958793301304 4.240163983273576 +v 0.0625 1.196178607955014 4.619469970566544 +v 0.0625 1.1896339555410327 4.244527084882897 +v -0.0625 1.4395958793301304 4.240163983273576 +v -0.0625 1.4461405317441118 4.615106868957223 +v -0.0625 1.1896339555410327 4.244527084882897 +v -0.0625 1.196178607955014 4.619469970566544 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.01745240643728351 -0.9998476951563913 +vn 1 0 0 +vn 0 0.01745240643728351 0.9998476951563913 +vn -1 0 0 +vn 0 0.9998476951563913 -0.01745240643728351 +vn 0 -0.9998476951563913 0.01745240643728351 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 356/1060/265 359/1059/265 357/1058/265 354/1057/265 +f 355/1064/266 356/1063/266 354/1062/266 353/1061/266 +f 360/1068/267 355/1067/267 353/1066/267 358/1065/267 +f 359/1072/268 360/1071/268 358/1070/268 357/1069/268 +f 358/1076/269 353/1075/269 354/1074/269 357/1073/269 +f 359/1080/270 356/1079/270 355/1078/270 360/1077/270 +o tail +v 0.3125 1.2200295729058328 5.363617983640148 +v 0.3125 1.1982173874667696 4.738998716753213 +v 0.3125 0.595410306018898 5.385430169079211 +v 0.3125 0.5735981205798348 4.760810902192276 +v -0.3125 1.1982173874667696 4.738998716753213 +v -0.3125 1.2200295729058328 5.363617983640148 +v -0.3125 0.5735981205798348 4.760810902192276 +v -0.3125 0.595410306018898 5.385430169079211 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.03489949670250097 -0.9993908270190958 +vn 1 0 0 +vn 0 0.03489949670250097 0.9993908270190958 +vn -1 0 0 +vn 0 0.9993908270190958 -0.03489949670250097 +vn 0 -0.9993908270190958 0.03489949670250097 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 364/1084/271 367/1083/271 365/1082/271 362/1081/271 +f 363/1088/272 364/1087/272 362/1086/272 361/1085/272 +f 368/1092/273 363/1091/273 361/1090/273 366/1089/273 +f 367/1096/274 368/1095/274 366/1094/274 365/1093/274 +f 366/1100/275 361/1099/275 362/1098/275 365/1097/275 +f 367/1104/276 364/1103/276 363/1102/276 368/1101/276 +o tail +v 0.0625 1.465514842572794 5.229969256087136 +v 0.0625 1.4524275313093562 4.855197695954975 +v 0.0625 1.21566713581802 5.238694130262761 +v 0.0625 1.2025798245545822 4.8639225701306 +v -0.0625 1.4524275313093562 4.855197695954975 +v -0.0625 1.465514842572794 5.229969256087136 +v -0.0625 1.2025798245545822 4.8639225701306 +v -0.0625 1.21566713581802 5.238694130262761 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.03489949670250097 -0.9993908270190958 +vn 1 0 0 +vn 0 0.03489949670250097 0.9993908270190958 +vn -1 0 0 +vn 0 0.9993908270190958 -0.03489949670250097 +vn 0 -0.9993908270190958 0.03489949670250097 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 372/1108/277 375/1107/277 373/1106/277 370/1105/277 +f 371/1112/278 372/1111/278 370/1110/278 369/1109/278 +f 376/1116/279 371/1115/279 369/1114/279 374/1113/279 +f 375/1120/280 376/1119/280 374/1118/280 373/1117/280 +f 374/1124/281 369/1123/281 370/1122/281 373/1121/281 +f 375/1128/282 372/1127/282 371/1126/282 376/1125/282 +o tail +v 0.3125 1.2525016417250097 5.982312549255369 +v 0.3125 1.2197916690731698 5.35816909003376 +v 0.3125 0.628358182503401 6.015022521907208 +v 0.3125 0.5956482098515612 5.3908790626856 +v -0.3125 1.2197916690731698 5.35816909003376 +v -0.3125 1.2525016417250097 5.982312549255369 +v -0.3125 0.5956482098515612 5.3908790626856 +v -0.3125 0.628358182503401 6.015022521907208 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.05233595624294383 -0.9986295347545739 +vn 1 0 0 +vn 0 0.05233595624294383 0.9986295347545739 +vn -1 0 0 +vn 0 0.9986295347545739 -0.05233595624294383 +vn 0 -0.9986295347545739 0.05233595624294383 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 380/1132/283 383/1131/283 381/1130/283 378/1129/283 +f 379/1136/284 380/1135/284 378/1134/284 377/1133/284 +f 384/1140/285 379/1139/285 377/1138/285 382/1137/285 +f 383/1144/286 384/1143/286 382/1142/286 381/1141/286 +f 382/1148/287 377/1147/287 378/1146/287 381/1145/287 +f 383/1152/288 380/1151/288 379/1150/288 384/1149/288 +o tail +v 0.0625 1.4956170308832855 5.844399868350311 +v 0.0625 1.4759910472921813 5.469913792817346 +v 0.0625 1.245959647194642 5.857483857411047 +v 0.0625 1.2263336636035378 5.482997781878082 +v -0.0625 1.4759910472921813 5.469913792817346 +v -0.0625 1.4956170308832855 5.844399868350311 +v -0.0625 1.2263336636035378 5.482997781878082 +v -0.0625 1.245959647194642 5.857483857411047 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.05233595624294383 -0.9986295347545739 +vn 1 0 0 +vn 0 0.05233595624294383 0.9986295347545739 +vn -1 0 0 +vn 0 0.9986295347545739 -0.05233595624294383 +vn 0 -0.9986295347545739 0.05233595624294383 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 388/1156/289 391/1155/289 389/1154/289 386/1153/289 +f 387/1160/290 388/1159/290 386/1158/290 385/1157/290 +f 392/1164/291 387/1163/291 385/1162/291 390/1161/291 +f 391/1168/292 392/1167/292 390/1166/292 389/1165/292 +f 390/1172/293 385/1171/293 386/1170/293 389/1169/293 +f 391/1176/294 388/1175/294 387/1174/294 392/1173/294 +o tail +v 0.3125 1.3062130944851622 6.594053052279986 +v 0.3125 1.2517407552678759 5.971431365972645 +v 0.3125 0.6835914081778212 6.648525391497273 +v 0.3125 0.6291190689605348 6.025903705189932 +v -0.3125 1.2517407552678759 5.971431365972645 +v -0.3125 1.3062130944851622 6.594053052279986 +v -0.3125 0.6291190689605348 6.025903705189932 +v -0.3125 0.6835914081778212 6.648525391497273 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.08715574274765817 -0.9961946980917457 +vn 1 0 0 +vn 0 0.08715574274765817 0.9961946980917457 +vn -1 0 0 +vn 0 0.9961946980917457 -0.08715574274765817 +vn 0 -0.9961946980917457 0.08715574274765817 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 396/1180/295 399/1179/295 397/1178/295 394/1177/295 +f 395/1184/296 396/1183/296 394/1182/296 393/1181/296 +f 400/1188/297 395/1187/297 393/1186/297 398/1185/297 +f 399/1192/298 400/1191/298 398/1190/298 397/1189/298 +f 398/1196/299 393/1195/299 394/1194/299 397/1193/299 +f 399/1200/300 396/1199/300 395/1198/300 400/1197/300 +o tail +v 0.0625 1.544367301164641 6.447739779331603 +v 0.0625 1.5116838976342697 6.074166767547199 +v 0.0625 1.295318626641705 6.469528715018518 +v 0.0625 1.2626352231113331 6.095955703234114 +v -0.0625 1.5116838976342697 6.074166767547199 +v -0.0625 1.544367301164641 6.447739779331603 +v -0.0625 1.2626352231113331 6.095955703234114 +v -0.0625 1.295318626641705 6.469528715018518 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.08715574274765817 -0.9961946980917457 +vn 1 0 0 +vn 0 0.08715574274765817 0.9961946980917457 +vn -1 0 0 +vn 0 0.9961946980917457 -0.08715574274765817 +vn 0 -0.9961946980917457 0.08715574274765817 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 404/1204/301 407/1203/301 405/1202/301 402/1201/301 +f 403/1208/302 404/1207/302 402/1206/302 401/1205/302 +f 408/1212/303 403/1211/303 401/1210/303 406/1209/303 +f 407/1216/304 408/1215/304 406/1214/304 405/1213/304 +f 406/1220/305 401/1219/305 402/1218/305 405/1217/305 +f 407/1224/306 404/1223/306 403/1222/306 408/1221/306 +o tail +v 0.3125 1.3913442109132734 7.196715170802091 +v 0.3125 1.304361022813232 6.577797627838609 +v 0.3125 0.772426667949792 7.283698358902132 +v 0.3125 0.6854434798497508 6.66478081593865 +v -0.3125 1.304361022813232 6.577797627838609 +v -0.3125 1.3913442109132734 7.196715170802091 +v -0.3125 0.6854434798497508 6.66478081593865 +v -0.3125 0.772426667949792 7.283698358902132 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.13917310096006544 -0.9902680687415704 +vn 1 0 0 +vn 0 0.13917310096006544 0.9902680687415704 +vn -1 0 0 +vn 0 0.9902680687415704 -0.13917310096006544 +vn 0 -0.9902680687415704 0.13917310096006544 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 412/1228/307 415/1227/307 413/1226/307 410/1225/307 +f 411/1232/308 412/1231/308 410/1230/308 409/1229/308 +f 416/1236/309 411/1235/309 409/1234/309 414/1233/309 +f 415/1240/310 416/1239/310 414/1238/310 413/1237/310 +f 414/1244/311 409/1243/311 410/1242/311 413/1241/311 +f 415/1248/312 412/1247/312 411/1246/312 416/1245/312 +o tail +v 0.0625 1.6215145904786574 7.038138386969378 +v 0.0625 1.5693246776186331 6.666787861191289 +v 0.0625 1.373947573293265 7.0729316622093945 +v 0.0625 1.3217576604332402 6.701581136431305 +v -0.0625 1.5693246776186331 6.666787861191289 +v -0.0625 1.6215145904786574 7.038138386969378 +v -0.0625 1.3217576604332402 6.701581136431305 +v -0.0625 1.373947573293265 7.0729316622093945 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.13917310096006544 -0.9902680687415704 +vn 1 0 0 +vn 0 0.13917310096006544 0.9902680687415704 +vn -1 0 0 +vn 0 0.9902680687415704 -0.13917310096006544 +vn 0 -0.9902680687415704 0.13917310096006544 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 420/1252/313 423/1251/313 421/1250/313 418/1249/313 +f 419/1256/314 420/1255/314 418/1254/314 417/1253/314 +f 424/1260/315 419/1259/315 417/1258/315 422/1257/315 +f 423/1264/316 424/1263/316 422/1262/316 421/1261/316 +f 422/1268/317 417/1267/317 418/1266/317 421/1265/317 +f 423/1272/318 420/1271/318 419/1270/318 424/1269/318 +o tail +v 0.3125 1.507899556369268 7.79409594345173 +v 0.3125 1.3886439342589278 7.180578953796941 +v 0.3125 0.894382566714478 7.9133515655620705 +v 0.3125 0.7751269446041376 7.299834575907282 +v -0.3125 1.3886439342589278 7.180578953796941 +v -0.3125 1.507899556369268 7.79409594345173 +v -0.3125 0.7751269446041376 7.299834575907282 +v -0.3125 0.894382566714478 7.9133515655620705 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.1908089953765448 -0.981627183447664 +vn 1 0 0 +vn 0 0.1908089953765448 0.981627183447664 +vn -1 0 0 +vn 0 0.981627183447664 -0.1908089953765448 +vn 0 -0.981627183447664 0.1908089953765448 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 428/1276/319 431/1275/319 429/1274/319 426/1273/319 +f 427/1280/320 428/1279/320 426/1278/320 425/1277/320 +f 432/1284/321 427/1283/321 425/1282/321 430/1281/321 +f 431/1288/322 432/1287/322 430/1286/322 429/1285/322 +f 430/1292/323 425/1291/323 426/1290/323 429/1289/323 +f 431/1296/324 428/1295/324 427/1294/324 432/1293/324 +o tail +v 0.0625 1.7294552278091162 7.623690296676635 +v 0.0625 1.6579018545429118 7.255580102883762 +v 0.0625 1.4840484319472 7.6713925455207725 +v 0.0625 1.4124950586809957 7.303282351727899 +v -0.0625 1.6579018545429118 7.255580102883762 +v -0.0625 1.7294552278091162 7.623690296676635 +v -0.0625 1.4124950586809957 7.303282351727899 +v -0.0625 1.4840484319472 7.6713925455207725 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.1908089953765448 -0.981627183447664 +vn 1 0 0 +vn 0 0.1908089953765448 0.981627183447664 +vn -1 0 0 +vn 0 0.981627183447664 -0.1908089953765448 +vn 0 -0.981627183447664 0.1908089953765448 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 436/1300/325 439/1299/325 437/1298/325 434/1297/325 +f 435/1304/326 436/1303/326 434/1302/326 433/1301/326 +f 440/1308/327 435/1307/327 433/1306/327 438/1305/327 +f 439/1312/328 440/1311/328 438/1310/328 437/1309/328 +f 438/1316/329 433/1315/329 434/1314/329 437/1313/329 +f 439/1320/330 436/1319/330 435/1318/330 440/1317/330 +o tail +v 0.3125 1.6367569935322868 8.40009360158498 +v 0.3125 1.506812186771187 7.788751351126351 +v 0.3125 1.025414743073658 8.53003840834608 +v 0.3125 0.8954699363125587 7.918696157887451 +v -0.3125 1.506812186771187 7.788751351126351 +v -0.3125 1.6367569935322868 8.40009360158498 +v -0.3125 0.8954699363125587 7.918696157887451 +v -0.3125 1.025414743073658 8.53003840834608 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.20791169081775934 -0.9781476007338057 +vn 1 0 0 +vn 0 0.20791169081775934 0.9781476007338057 +vn -1 0 0 +vn 0 0.9781476007338057 -0.20791169081775934 +vn 0 -0.9781476007338057 0.20791169081775934 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 444/1324/331 447/1323/331 445/1322/331 442/1321/331 +f 443/1328/332 444/1327/332 442/1326/332 441/1325/332 +f 448/1332/333 443/1331/333 441/1330/333 446/1329/333 +f 447/1336/334 448/1335/334 446/1334/334 445/1333/334 +f 446/1340/335 441/1339/335 442/1338/335 445/1337/335 +f 447/1344/336 444/1343/336 443/1342/336 448/1341/336 +o tail +v 0.0625 1.8553049323635182 8.225847228788815 +v 0.0625 1.7773380483068584 7.8590418785136364 +v 0.0625 1.6107680321800668 8.277825151493253 +v 0.0625 1.532801148123407 7.911019801218078 +v -0.0625 1.7773380483068584 7.8590418785136364 +v -0.0625 1.8553049323635182 8.225847228788815 +v -0.0625 1.532801148123407 7.911019801218078 +v -0.0625 1.6107680321800668 8.277825151493253 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.20791169081775934 -0.9781476007338057 +vn 1 0 0 +vn 0 0.20791169081775934 0.9781476007338057 +vn -1 0 0 +vn 0 0.9781476007338057 -0.20791169081775934 +vn 0 -0.9781476007338057 0.20791169081775934 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 452/1348/337 455/1347/337 453/1346/337 450/1345/337 +f 451/1352/338 452/1351/338 450/1350/338 449/1349/338 +f 456/1356/339 451/1355/339 449/1354/339 454/1353/339 +f 455/1360/340 456/1359/340 454/1358/340 453/1357/340 +f 454/1364/341 449/1363/341 450/1362/341 453/1361/341 +f 455/1368/342 452/1367/342 451/1366/342 456/1365/342 +o tail +v 0.3125 1.7570999852407079 9.018955183565149 +v 0.3125 1.6378443631303679 8.405438193910358 +v 0.3125 1.143582995585918 9.13821080567549 +v 0.3125 1.0243273734755776 8.524693816020699 +v -0.3125 1.6378443631303679 8.405438193910358 +v -0.3125 1.7570999852407079 9.018955183565149 +v -0.3125 1.0243273734755776 8.524693816020699 +v -0.3125 1.143582995585918 9.13821080567549 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.1908089953765448 -0.981627183447664 +vn 1 0 0 +vn 0 0.1908089953765448 0.981627183447664 +vn -1 0 0 +vn 0 0.981627183447664 -0.1908089953765448 +vn 0 -0.981627183447664 0.1908089953765448 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 460/1372/343 463/1371/343 461/1370/343 458/1369/343 +f 459/1376/344 460/1375/344 458/1374/344 457/1373/344 +f 464/1380/345 459/1379/345 457/1378/345 462/1377/345 +f 463/1384/346 464/1383/346 462/1382/346 461/1381/346 +f 462/1388/347 457/1387/347 458/1386/347 461/1385/347 +f 463/1392/348 460/1391/348 459/1390/348 464/1389/348 +o tail +v 0.0625 1.978655656680556 8.848549536790054 +v 0.0625 1.9071022834143516 8.480439342997181 +v 0.0625 1.73324886081864 8.89625178563419 +v 0.0625 1.6616954875524357 8.528141591841317 +v -0.0625 1.9071022834143516 8.480439342997181 +v -0.0625 1.978655656680556 8.848549536790054 +v -0.0625 1.6616954875524357 8.528141591841317 +v -0.0625 1.73324886081864 8.89625178563419 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.1908089953765448 -0.981627183447664 +vn 1 0 0 +vn 0 0.1908089953765448 0.981627183447664 +vn -1 0 0 +vn 0 0.981627183447664 -0.1908089953765448 +vn 0 -0.981627183447664 0.1908089953765448 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 468/1396/349 471/1395/349 469/1394/349 466/1393/349 +f 467/1400/350 468/1399/350 466/1398/350 465/1397/350 +f 472/1404/351 467/1403/351 465/1402/351 470/1401/351 +f 471/1408/352 472/1407/352 470/1406/352 469/1405/352 +f 470/1412/353 465/1411/353 466/1410/353 469/1409/353 +f 471/1416/354 468/1415/354 467/1414/354 472/1413/354 +o tail +v 0.3125 1.8567656374994383 9.64700243716721 +v 0.3125 1.7589940968492939 9.029697224295248 +v 0.3125 1.239460424627477 9.744773977817353 +v 0.3125 1.1416888839773327 9.127468764945391 +v -0.3125 1.7589940968492939 9.029697224295248 +v -0.3125 1.8567656374994383 9.64700243716721 +v -0.3125 1.1416888839773327 9.127468764945391 +v -0.3125 1.239460424627477 9.744773977817353 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.15643446504023087 -0.9876883405951378 +vn 1 0 0 +vn 0 0.15643446504023087 0.9876883405951378 +vn -1 0 0 +vn 0 0.9876883405951378 -0.15643446504023087 +vn 0 -0.9876883405951378 0.15643446504023087 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 476/1420/355 479/1419/355 477/1418/355 474/1417/355 +f 475/1424/356 476/1423/356 474/1422/356 473/1421/356 +f 480/1428/357 475/1427/357 473/1426/357 478/1425/357 +f 479/1432/358 480/1431/358 478/1430/358 477/1429/358 +f 478/1436/359 473/1435/359 474/1434/359 477/1433/359 +f 479/1440/360 476/1439/360 475/1438/360 480/1437/360 +o tail +v 0.0625 2.084133414518194 9.484432778332758 +v 0.0625 2.025470490128107 9.114049650609582 +v 0.0625 1.8372113293694095 9.523541394592817 +v 0.0625 1.7785484049793228 9.15315826686964 +v -0.0625 2.025470490128107 9.114049650609582 +v -0.0625 2.084133414518194 9.484432778332758 +v -0.0625 1.7785484049793228 9.15315826686964 +v -0.0625 1.8372113293694095 9.523541394592817 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.15643446504023087 -0.9876883405951378 +vn 1 0 0 +vn 0 0.15643446504023087 0.9876883405951378 +vn -1 0 0 +vn 0 0.9876883405951378 -0.15643446504023087 +vn 0 -0.9876883405951378 0.15643446504023087 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 484/1444/361 487/1443/361 485/1442/361 482/1441/361 +f 483/1448/362 484/1447/362 482/1446/362 481/1445/362 +f 488/1452/363 483/1451/363 481/1450/363 486/1449/363 +f 487/1456/364 488/1455/364 486/1454/364 485/1453/364 +f 486/1460/365 481/1459/365 482/1458/365 485/1457/365 +f 487/1464/366 484/1463/366 483/1462/366 488/1461/366 +o tail +v 0.3125 1.924231412908326 10.284799247326308 +v 0.3125 1.858901123366043 9.663223062721139 +v 0.3125 1.3026552283031554 10.350129536868593 +v 0.3125 1.237324938760872 9.728553352263424 +v -0.3125 1.858901123366043 9.663223062721139 +v -0.3125 1.924231412908326 10.284799247326308 +v -0.3125 1.237324938760872 9.728553352263424 +v -0.3125 1.3026552283031554 10.350129536868593 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.10452846326765347 -0.9945218953682733 +vn 1 0 0 +vn 0 0.10452846326765347 0.9945218953682733 +vn -1 0 0 +vn 0 0.9945218953682733 -0.10452846326765347 +vn 0 -0.9945218953682733 0.10452846326765347 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 492/1468/367 495/1467/367 493/1466/367 490/1465/367 +f 491/1472/368 492/1471/368 490/1470/368 489/1469/368 +f 496/1476/369 491/1475/369 489/1474/369 494/1473/369 +f 495/1480/370 496/1479/370 494/1478/370 493/1477/370 +f 494/1484/371 489/1483/371 490/1482/371 493/1481/371 +f 495/1488/372 492/1487/372 491/1486/372 496/1485/372 +o tail +v 0.0625 2.159795828841938 10.134351894588363 +v 0.0625 2.120597655116568 9.76140618382526 +v 0.0625 1.91116535499987 10.160484010405275 +v 0.0625 1.8719671812745 9.787538299642172 +v -0.0625 2.120597655116568 9.76140618382526 +v -0.0625 2.159795828841938 10.134351894588363 +v -0.0625 1.8719671812745 9.787538299642172 +v -0.0625 1.91116535499987 10.160484010405275 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.10452846326765347 -0.9945218953682733 +vn 1 0 0 +vn 0 0.10452846326765347 0.9945218953682733 +vn -1 0 0 +vn 0 0.9945218953682733 -0.10452846326765347 +vn 0 -0.9945218953682733 0.10452846326765347 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 500/1492/373 503/1491/373 501/1490/373 498/1489/373 +f 499/1496/374 500/1495/374 498/1494/374 497/1493/374 +f 504/1500/375 499/1499/375 497/1498/375 502/1497/375 +f 503/1504/376 504/1503/376 502/1502/376 501/1501/376 +f 502/1508/377 497/1507/377 498/1506/377 501/1505/377 +f 503/1512/378 500/1511/378 499/1510/378 504/1509/378 +o tail +v 0.3125 1.958225022868385 10.92525286499314 +v 0.3125 1.9255150502165446 10.301109405771532 +v 0.3125 1.3340815636467762 10.957962837644981 +v 0.3125 1.3013715909949364 10.333819378423373 +v -0.3125 1.9255150502165446 10.301109405771532 +v -0.3125 1.958225022868385 10.92525286499314 +v -0.3125 1.3013715909949364 10.333819378423373 +v -0.3125 1.3340815636467762 10.957962837644981 +vt 0.7890625 0.5546875 +vt 0.828125 0.5546875 +vt 0.828125 0.515625 +vt 0.7890625 0.515625 +vt 0.75 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.515625 +vt 0.75 0.515625 +vt 0.8671875 0.5546875 +vt 0.90625 0.5546875 +vt 0.90625 0.515625 +vt 0.8671875 0.515625 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vt 0.8671875 0.515625 +vt 0.828125 0.515625 +vt 0.828125 0.5546875 +vt 0.7890625 0.5546875 +vt 0.7890625 0.59375 +vt 0.828125 0.59375 +vt 0.8671875 0.59375 +vt 0.828125 0.59375 +vt 0.828125 0.5546875 +vt 0.8671875 0.5546875 +vn 0 -0.05233595624294385 -0.998629534754574 +vn 1 0 0 +vn 0 0.05233595624294385 0.998629534754574 +vn -1 0 0 +vn 0 0.998629534754574 -0.05233595624294385 +vn 0 -0.998629534754574 0.05233595624294385 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 508/1516/379 511/1515/379 509/1514/379 506/1513/379 +f 507/1520/380 508/1519/380 506/1518/380 505/1517/380 +f 512/1524/381 507/1523/381 505/1522/381 510/1521/381 +f 511/1528/382 512/1527/382 510/1526/382 509/1525/382 +f 510/1532/383 505/1531/383 506/1530/383 509/1529/383 +f 511/1536/384 508/1535/384 507/1534/384 512/1533/384 +o tail +v 0.0625 2.20134041202666 10.787340184088084 +v 0.0625 2.1817144284355567 10.412854108555118 +v 0.0625 1.9516830283380169 10.80042417314882 +v 0.0625 1.9320570447469132 10.425938097615854 +v -0.0625 2.1817144284355567 10.412854108555118 +v -0.0625 2.20134041202666 10.787340184088084 +v -0.0625 1.9320570447469132 10.425938097615854 +v -0.0625 1.9516830283380169 10.80042417314882 +vt 0.2109375 0.9765625 +vt 0.21875 0.9765625 +vt 0.21875 0.9609375 +vt 0.2109375 0.9609375 +vt 0.1875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 0.9609375 +vt 0.1875 0.9609375 +vt 0.2421875 0.9765625 +vt 0.25 0.9765625 +vt 0.25 0.9609375 +vt 0.2421875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2421875 0.9765625 +vt 0.2421875 0.9609375 +vt 0.21875 0.9609375 +vt 0.21875 0.9765625 +vt 0.2109375 0.9765625 +vt 0.2109375 1 +vt 0.21875 1 +vt 0.2265625 1 +vt 0.21875 1 +vt 0.21875 0.9765625 +vt 0.2265625 0.9765625 +vn 0 -0.05233595624294385 -0.998629534754574 +vn 1 0 0 +vn 0 0.05233595624294385 0.998629534754574 +vn -1 0 0 +vn 0 0.998629534754574 -0.05233595624294385 +vn 0 -0.998629534754574 0.05233595624294385 +usemtl m_e48612ca-0475-df4f-6d9d-44f43691817f +f 516/1540/385 519/1539/385 517/1538/385 514/1537/385 +f 515/1544/386 516/1543/386 514/1542/386 513/1541/386 +f 520/1548/387 515/1547/387 513/1546/387 518/1545/387 +f 519/1552/388 520/1551/388 518/1550/388 517/1549/388 +f 518/1556/389 513/1555/389 514/1554/389 517/1553/389 +f 519/1560/390 516/1559/390 515/1558/390 520/1557/390 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/enderman.obj b/renderer/viewer/three/entity/models/enderman.obj new file mode 100644 index 00000000..6b405674 --- /dev/null +++ b/renderer/viewer/three/entity/models/enderman.obj @@ -0,0 +1,325 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o Head +v 0.21875 2.96875 0.21875 +v 0.21875 2.96875 -0.21875 +v 0.21875 2.53125 0.21875 +v 0.21875 2.53125 -0.21875 +v -0.21875 2.96875 -0.21875 +v -0.21875 2.96875 0.21875 +v -0.21875 2.53125 -0.21875 +v -0.21875 2.53125 0.21875 +vt 0.125 0.75 +vt 0.25 0.75 +vt 0.25 0.5 +vt 0.125 0.5 +vt 0 0.75 +vt 0.125 0.75 +vt 0.125 0.5 +vt 0 0.5 +vt 0.375 0.75 +vt 0.5 0.75 +vt 0.5 0.5 +vt 0.375 0.5 +vt 0.25 0.75 +vt 0.375 0.75 +vt 0.375 0.5 +vt 0.25 0.5 +vt 0.25 0.75 +vt 0.125 0.75 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.75 +vt 0.375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o Head layer +v 0.21875 2.84375 0.21875 +v 0.21875 2.84375 -0.21875 +v 0.21875 2.40625 0.21875 +v 0.21875 2.40625 -0.21875 +v -0.21875 2.84375 -0.21875 +v -0.21875 2.84375 0.21875 +v -0.21875 2.40625 -0.21875 +v -0.21875 2.40625 0.21875 +vt 0.125 0.25 +vt 0.25 0.25 +vt 0.25 0 +vt 0.125 0 +vt 0 0.25 +vt 0.125 0.25 +vt 0.125 0 +vt 0 0 +vt 0.375 0.25 +vt 0.5 0.25 +vt 0.5 0 +vt 0.375 0 +vt 0.25 0.25 +vt 0.375 0.25 +vt 0.375 0 +vt 0.25 0 +vt 0.25 0.25 +vt 0.125 0.25 +vt 0.125 0.5 +vt 0.25 0.5 +vt 0.375 0.5 +vt 0.25 0.5 +vt 0.25 0.25 +vt 0.375 0.25 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o Body +v 0.25 2.375 0.125 +v 0.25 2.375 -0.125 +v 0.25 1.625 0.125 +v 0.25 1.625 -0.125 +v -0.25 2.375 -0.125 +v -0.25 2.375 0.125 +v -0.25 1.625 -0.125 +v -0.25 1.625 0.125 +vt 0.5625 0.375 +vt 0.6875 0.375 +vt 0.6875 0 +vt 0.5625 0 +vt 0.5 0.375 +vt 0.5625 0.375 +vt 0.5625 0 +vt 0.5 0 +vt 0.75 0.375 +vt 0.875 0.375 +vt 0.875 0 +vt 0.75 0 +vt 0.6875 0.375 +vt 0.75 0.375 +vt 0.75 0 +vt 0.6875 0 +vt 0.6875 0.375 +vt 0.5625 0.375 +vt 0.5625 0.5 +vt 0.6875 0.5 +vt 0.8125 0.5 +vt 0.6875 0.5 +vt 0.6875 0.375 +vt 0.8125 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o RightArm +v 0.375 2.375 0.0625 +v 0.375 2.375 -0.0625 +v 0.375 0.5 0.0625 +v 0.375 0.5 -0.0625 +v 0.25 2.375 -0.0625 +v 0.25 2.375 0.0625 +v 0.25 0.5 -0.0625 +v 0.25 0.5 0.0625 +vt 0.90625 0.9375 +vt 0.9375 0.9375 +vt 0.9375 0 +vt 0.90625 0 +vt 0.875 0.9375 +vt 0.90625 0.9375 +vt 0.90625 0 +vt 0.875 0 +vt 0.96875 0.9375 +vt 1 0.9375 +vt 1 0 +vt 0.96875 0 +vt 0.9375 0.9375 +vt 0.96875 0.9375 +vt 0.96875 0 +vt 0.9375 0 +vt 0.9375 0.9375 +vt 0.90625 0.9375 +vt 0.90625 1 +vt 0.9375 1 +vt 0.96875 1 +vt 0.9375 1 +vt 0.9375 0.9375 +vt 0.96875 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o LeftArm +v -0.25 2.375 0.0625 +v -0.25 2.375 -0.0625 +v -0.25 0.5 0.0625 +v -0.25 0.5 -0.0625 +v -0.375 2.375 -0.0625 +v -0.375 2.375 0.0625 +v -0.375 0.5 -0.0625 +v -0.375 0.5 0.0625 +vt 0.9375 0.9375 +vt 0.90625 0.9375 +vt 0.90625 0 +vt 0.9375 0 +vt 0.96875 0.9375 +vt 0.9375 0.9375 +vt 0.9375 0 +vt 0.96875 0 +vt 1 0.9375 +vt 0.96875 0.9375 +vt 0.96875 0 +vt 1 0 +vt 0.90625 0.9375 +vt 0.875 0.9375 +vt 0.875 0 +vt 0.90625 0 +vt 0.90625 0.9375 +vt 0.9375 0.9375 +vt 0.9375 1 +vt 0.90625 1 +vt 0.9375 1 +vt 0.96875 1 +vt 0.96875 0.9375 +vt 0.9375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o RightLeg +v 0.1875 1.625 0.0625 +v 0.1875 1.625 -0.0625 +v 0.1875 -0.25 0.0625 +v 0.1875 -0.25 -0.0625 +v 0.0625 1.625 -0.0625 +v 0.0625 1.625 0.0625 +v 0.0625 -0.25 -0.0625 +v 0.0625 -0.25 0.0625 +vt 0.90625 0.9375 +vt 0.9375 0.9375 +vt 0.9375 0 +vt 0.90625 0 +vt 0.875 0.9375 +vt 0.90625 0.9375 +vt 0.90625 0 +vt 0.875 0 +vt 0.96875 0.9375 +vt 1 0.9375 +vt 1 0 +vt 0.96875 0 +vt 0.9375 0.9375 +vt 0.96875 0.9375 +vt 0.96875 0 +vt 0.9375 0 +vt 0.9375 0.9375 +vt 0.90625 0.9375 +vt 0.90625 1 +vt 0.9375 1 +vt 0.96875 1 +vt 0.9375 1 +vt 0.9375 0.9375 +vt 0.96875 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o LeftLeg +v -0.0625 1.625 0.0625 +v -0.0625 1.625 -0.0625 +v -0.0625 -0.25 0.0625 +v -0.0625 -0.25 -0.0625 +v -0.1875 1.625 -0.0625 +v -0.1875 1.625 0.0625 +v -0.1875 -0.25 -0.0625 +v -0.1875 -0.25 0.0625 +vt 0.9375 0.9375 +vt 0.90625 0.9375 +vt 0.90625 0 +vt 0.9375 0 +vt 0.96875 0.9375 +vt 0.9375 0.9375 +vt 0.9375 0 +vt 0.96875 0 +vt 1 0.9375 +vt 0.96875 0.9375 +vt 0.96875 0 +vt 1 0 +vt 0.90625 0.9375 +vt 0.875 0.9375 +vt 0.875 0 +vt 0.90625 0 +vt 0.90625 0.9375 +vt 0.9375 0.9375 +vt 0.9375 1 +vt 0.90625 1 +vt 0.9375 1 +vt 0.96875 1 +vt 0.96875 0.9375 +vt 0.9375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_981d8dac-1361-c897-71b0-9983b6b479fc +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/endermite.obj b/renderer/viewer/three/entity/models/endermite.obj new file mode 100644 index 00000000..595376cd --- /dev/null +++ b/renderer/viewer/three/entity/models/endermite.obj @@ -0,0 +1,187 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o section_2 +v 0.09375 0.1875 0.21875 +v 0.09375 0.1875 0.15625 +v 0.09375 0 0.21875 +v 0.09375 0 0.15625 +v -0.09375 0.1875 0.15625 +v -0.09375 0.1875 0.21875 +v -0.09375 0 0.15625 +v -0.09375 0 0.21875 +vt 0.015625 0.53125 +vt 0.0625 0.53125 +vt 0.0625 0.4375 +vt 0.015625 0.4375 +vt 0 0.53125 +vt 0.015625 0.53125 +vt 0.015625 0.4375 +vt 0 0.4375 +vt 0.078125 0.53125 +vt 0.125 0.53125 +vt 0.125 0.4375 +vt 0.078125 0.4375 +vt 0.0625 0.53125 +vt 0.078125 0.53125 +vt 0.078125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.53125 +vt 0.015625 0.53125 +vt 0.015625 0.5625 +vt 0.0625 0.5625 +vt 0.109375 0.5625 +vt 0.0625 0.5625 +vt 0.0625 0.53125 +vt 0.109375 0.53125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_50f00cc5-283b-b301-14d6-d02da334960b +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o section_0 +v 0.125 0.1875 -0.15000000000000002 +v 0.125 0.1875 -0.275 +v 0.125 0 -0.15000000000000002 +v 0.125 0 -0.275 +v -0.125 0.1875 -0.275 +v -0.125 0.1875 -0.15000000000000002 +v -0.125 0 -0.275 +v -0.125 0 -0.15000000000000002 +vt 0.03125 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.84375 +vt 0.03125 0.84375 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.84375 +vt 0 0.84375 +vt 0.125 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.84375 +vt 0.125 0.84375 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.84375 +vt 0.09375 0.84375 +vt 0.09375 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.09375 1 +vt 0.15625 1 +vt 0.09375 1 +vt 0.09375 0.9375 +vt 0.15625 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_50f00cc5-283b-b301-14d6-d02da334960b +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o section_1 +v 0.1875 0.25 0.16249999999999998 +v 0.1875 0.25 -0.15000000000000002 +v 0.1875 0 0.16249999999999998 +v 0.1875 0 -0.15000000000000002 +v -0.1875 0.25 -0.15000000000000002 +v -0.1875 0.25 0.16249999999999998 +v -0.1875 0 -0.15000000000000002 +v -0.1875 0 0.16249999999999998 +vt 0.078125 0.6875 +vt 0.171875 0.6875 +vt 0.171875 0.5625 +vt 0.078125 0.5625 +vt 0 0.6875 +vt 0.078125 0.6875 +vt 0.078125 0.5625 +vt 0 0.5625 +vt 0.25 0.6875 +vt 0.34375 0.6875 +vt 0.34375 0.5625 +vt 0.25 0.5625 +vt 0.171875 0.6875 +vt 0.25 0.6875 +vt 0.25 0.5625 +vt 0.171875 0.5625 +vt 0.171875 0.6875 +vt 0.078125 0.6875 +vt 0.078125 0.84375 +vt 0.171875 0.84375 +vt 0.265625 0.84375 +vt 0.171875 0.84375 +vt 0.171875 0.6875 +vt 0.265625 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_50f00cc5-283b-b301-14d6-d02da334960b +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o section_3 +v 0.03125 0.125 0.28125 +v 0.03125 0.125 0.21875 +v 0.03125 0 0.28125 +v 0.03125 0 0.21875 +v -0.03125 0.125 0.21875 +v -0.03125 0.125 0.28125 +v -0.03125 0 0.21875 +v -0.03125 0 0.28125 +vt 0.015625 0.40625 +vt 0.03125 0.40625 +vt 0.03125 0.34375 +vt 0.015625 0.34375 +vt 0 0.40625 +vt 0.015625 0.40625 +vt 0.015625 0.34375 +vt 0 0.34375 +vt 0.046875 0.40625 +vt 0.0625 0.40625 +vt 0.0625 0.34375 +vt 0.046875 0.34375 +vt 0.03125 0.40625 +vt 0.046875 0.40625 +vt 0.046875 0.34375 +vt 0.03125 0.34375 +vt 0.03125 0.40625 +vt 0.015625 0.40625 +vt 0.015625 0.4375 +vt 0.03125 0.4375 +vt 0.046875 0.4375 +vt 0.03125 0.4375 +vt 0.03125 0.40625 +vt 0.046875 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_50f00cc5-283b-b301-14d6-d02da334960b +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/fox.obj b/renderer/viewer/three/entity/models/fox.obj new file mode 100644 index 00000000..a0645b37 --- /dev/null +++ b/renderer/viewer/three/entity/models/fox.obj @@ -0,0 +1,463 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.1875 0.6875 -0.1875 +v 0.1875 0.3125 -0.1875 +v 0.1875 0.6875 0.5 +v 0.1875 0.3124999999999999 0.5 +v -0.1875 0.3125 -0.1875 +v -0.1875 0.6875 -0.1875 +v -0.1875 0.3124999999999999 0.5 +v -0.1875 0.6875 0.5 +vt 0.625 0.34375 +vt 0.75 0.34375 +vt 0.75 0 +vt 0.625 0 +vt 0.5 0.34375 +vt 0.625 0.34375 +vt 0.625 0 +vt 0.5 0 +vt 0.875 0.34375 +vt 1 0.34375 +vt 1 0 +vt 0.875 0 +vt 0.75 0.34375 +vt 0.875 0.34375 +vt 0.875 0 +vt 0.75 0 +vt 0.75 0.34375 +vt 0.625 0.34375 +vt 0.625 0.53125 +vt 0.75 0.53125 +vt 0.875 0.53125 +vt 0.75 0.53125 +vt 0.75 0.34375 +vt 0.875 0.34375 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.25 0.625 -0.1875 +v 0.25 0.625 -0.5625 +v 0.25 0.25 -0.1875 +v 0.25 0.25 -0.5625 +v -0.25 0.625 -0.5625 +v -0.25 0.625 -0.1875 +v -0.25 0.25 -0.5625 +v -0.25 0.25 -0.1875 +vt 0.14583333333333334 0.65625 +vt 0.3125 0.65625 +vt 0.3125 0.46875 +vt 0.14583333333333334 0.46875 +vt 0.020833333333333332 0.65625 +vt 0.14583333333333334 0.65625 +vt 0.14583333333333334 0.46875 +vt 0.020833333333333332 0.46875 +vt 0.4375 0.65625 +vt 0.6041666666666666 0.65625 +vt 0.6041666666666666 0.46875 +vt 0.4375 0.46875 +vt 0.3125 0.65625 +vt 0.4375 0.65625 +vt 0.4375 0.46875 +vt 0.3125 0.46875 +vt 0.3125 0.65625 +vt 0.14583333333333334 0.65625 +vt 0.14583333333333334 0.84375 +vt 0.3125 0.84375 +vt 0.4791666666666667 0.84375 +vt 0.3125 0.84375 +vt 0.3125 0.65625 +vt 0.4791666666666667 0.65625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.25 0.75 -0.4375 +v 0.25 0.75 -0.5 +v 0.25 0.625 -0.4375 +v 0.25 0.625 -0.5 +v 0.125 0.75 -0.5 +v 0.125 0.75 -0.4375 +v 0.125 0.625 -0.5 +v 0.125 0.625 -0.4375 +vt 0.1875 0.9375 +vt 0.22916666666666666 0.9375 +vt 0.22916666666666666 0.875 +vt 0.1875 0.875 +vt 0.16666666666666666 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.875 +vt 0.16666666666666666 0.875 +vt 0.25 0.9375 +vt 0.2916666666666667 0.9375 +vt 0.2916666666666667 0.875 +vt 0.25 0.875 +vt 0.22916666666666666 0.9375 +vt 0.25 0.9375 +vt 0.25 0.875 +vt 0.22916666666666666 0.875 +vt 0.22916666666666666 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.96875 +vt 0.22916666666666666 0.96875 +vt 0.2708333333333333 0.96875 +vt 0.22916666666666666 0.96875 +vt 0.22916666666666666 0.9375 +vt 0.2708333333333333 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v -0.125 0.75 -0.4375 +v -0.125 0.75 -0.5 +v -0.125 0.625 -0.4375 +v -0.125 0.625 -0.5 +v -0.25 0.75 -0.5 +v -0.25 0.75 -0.4375 +v -0.25 0.625 -0.5 +v -0.25 0.625 -0.4375 +vt 0.3333333333333333 0.9375 +vt 0.375 0.9375 +vt 0.375 0.875 +vt 0.3333333333333333 0.875 +vt 0.3125 0.9375 +vt 0.3333333333333333 0.9375 +vt 0.3333333333333333 0.875 +vt 0.3125 0.875 +vt 0.3958333333333333 0.9375 +vt 0.4375 0.9375 +vt 0.4375 0.875 +vt 0.3958333333333333 0.875 +vt 0.375 0.9375 +vt 0.3958333333333333 0.9375 +vt 0.3958333333333333 0.875 +vt 0.375 0.875 +vt 0.375 0.9375 +vt 0.3333333333333333 0.9375 +vt 0.3333333333333333 0.96875 +vt 0.375 0.96875 +vt 0.4166666666666667 0.96875 +vt 0.375 0.96875 +vt 0.375 0.9375 +vt 0.4166666666666667 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head +v 0.125 0.375 -0.5625 +v 0.125 0.375 -0.75 +v 0.125 0.25 -0.5625 +v 0.125 0.25 -0.75 +v -0.125 0.375 -0.75 +v -0.125 0.375 -0.5625 +v -0.125 0.25 -0.75 +v -0.125 0.25 -0.5625 +vt 0.1875 0.34375 +vt 0.2708333333333333 0.34375 +vt 0.2708333333333333 0.28125 +vt 0.1875 0.28125 +vt 0.125 0.34375 +vt 0.1875 0.34375 +vt 0.1875 0.28125 +vt 0.125 0.28125 +vt 0.3333333333333333 0.34375 +vt 0.4166666666666667 0.34375 +vt 0.4166666666666667 0.28125 +vt 0.3333333333333333 0.28125 +vt 0.2708333333333333 0.34375 +vt 0.3333333333333333 0.34375 +vt 0.3333333333333333 0.28125 +vt 0.2708333333333333 0.28125 +vt 0.2708333333333333 0.34375 +vt 0.1875 0.34375 +vt 0.1875 0.4375 +vt 0.2708333333333333 0.4375 +vt 0.3541666666666667 0.4375 +vt 0.2708333333333333 0.4375 +vt 0.2708333333333333 0.34375 +vt 0.3541666666666667 0.34375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leg0 +v 0.18781249999999994 0.375 0.4375 +v 0.18781249999999994 0.375 0.3125 +v 0.18781249999999994 0 0.4375 +v 0.18781249999999994 0 0.3125 +v 0.06281249999999994 0.375 0.3125 +v 0.06281249999999994 0.375 0.4375 +v 0.06281249999999994 0 0.3125 +v 0.06281249999999994 0 0.4375 +vt 0.3125 0.1875 +vt 0.3541666666666667 0.1875 +vt 0.3541666666666667 0 +vt 0.3125 0 +vt 0.2708333333333333 0.1875 +vt 0.3125 0.1875 +vt 0.3125 0 +vt 0.2708333333333333 0 +vt 0.3958333333333333 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0 +vt 0.3958333333333333 0 +vt 0.3541666666666667 0.1875 +vt 0.3958333333333333 0.1875 +vt 0.3958333333333333 0 +vt 0.3541666666666667 0 +vt 0.3541666666666667 0.1875 +vt 0.3125 0.1875 +vt 0.3125 0.25 +vt 0.3541666666666667 0.25 +vt 0.3958333333333333 0.25 +vt 0.3541666666666667 0.25 +vt 0.3541666666666667 0.1875 +vt 0.3958333333333333 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o leg1 +v -0.0628125 0.375 0.4375 +v -0.0628125 0.375 0.3125 +v -0.0628125 0 0.4375 +v -0.0628125 0 0.3125 +v -0.1878125 0.375 0.3125 +v -0.1878125 0.375 0.4375 +v -0.1878125 0 0.3125 +v -0.1878125 0 0.4375 +vt 0.125 0.1875 +vt 0.16666666666666666 0.1875 +vt 0.16666666666666666 0 +vt 0.125 0 +vt 0.08333333333333333 0.1875 +vt 0.125 0.1875 +vt 0.125 0 +vt 0.08333333333333333 0 +vt 0.20833333333333334 0.1875 +vt 0.25 0.1875 +vt 0.25 0 +vt 0.20833333333333334 0 +vt 0.16666666666666666 0.1875 +vt 0.20833333333333334 0.1875 +vt 0.20833333333333334 0 +vt 0.16666666666666666 0 +vt 0.16666666666666666 0.1875 +vt 0.125 0.1875 +vt 0.125 0.25 +vt 0.16666666666666666 0.25 +vt 0.20833333333333334 0.25 +vt 0.16666666666666666 0.25 +vt 0.16666666666666666 0.1875 +vt 0.20833333333333334 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o leg2 +v 0.18781249999999994 0.375 0 +v 0.18781249999999994 0.375 -0.125 +v 0.18781249999999994 0 0 +v 0.18781249999999994 0 -0.125 +v 0.06281249999999994 0.375 -0.125 +v 0.06281249999999994 0.375 0 +v 0.06281249999999994 0 -0.125 +v 0.06281249999999994 0 0 +vt 0.3125 0.1875 +vt 0.3541666666666667 0.1875 +vt 0.3541666666666667 0 +vt 0.3125 0 +vt 0.2708333333333333 0.1875 +vt 0.3125 0.1875 +vt 0.3125 0 +vt 0.2708333333333333 0 +vt 0.3958333333333333 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0 +vt 0.3958333333333333 0 +vt 0.3541666666666667 0.1875 +vt 0.3958333333333333 0.1875 +vt 0.3958333333333333 0 +vt 0.3541666666666667 0 +vt 0.3541666666666667 0.1875 +vt 0.3125 0.1875 +vt 0.3125 0.25 +vt 0.3541666666666667 0.25 +vt 0.3958333333333333 0.25 +vt 0.3541666666666667 0.25 +vt 0.3541666666666667 0.1875 +vt 0.3958333333333333 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o leg3 +v -0.0628125 0.375 0 +v -0.0628125 0.375 -0.125 +v -0.0628125 0 0 +v -0.0628125 0 -0.125 +v -0.1878125 0.375 -0.125 +v -0.1878125 0.375 0 +v -0.1878125 0 -0.125 +v -0.1878125 0 0 +vt 0.125 0.1875 +vt 0.16666666666666666 0.1875 +vt 0.16666666666666666 0 +vt 0.125 0 +vt 0.08333333333333333 0.1875 +vt 0.125 0.1875 +vt 0.125 0 +vt 0.08333333333333333 0 +vt 0.20833333333333334 0.1875 +vt 0.25 0.1875 +vt 0.25 0 +vt 0.20833333333333334 0 +vt 0.16666666666666666 0.1875 +vt 0.20833333333333334 0.1875 +vt 0.20833333333333334 0 +vt 0.16666666666666666 0 +vt 0.16666666666666666 0.1875 +vt 0.125 0.1875 +vt 0.125 0.25 +vt 0.16666666666666666 0.25 +vt 0.20833333333333334 0.25 +vt 0.16666666666666666 0.25 +vt 0.16666666666666666 0.1875 +vt 0.20833333333333334 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o tail +v 0.125 0.671875 0.5 +v 0.125 0.359375 0.5 +v 0.125 0.671875 1.0625 +v 0.125 0.3593749999999999 1.0625 +v -0.125 0.359375 0.5 +v -0.125 0.671875 0.5 +v -0.125 0.3593749999999999 1.0625 +v -0.125 0.671875 1.0625 +vt 0.7291666666666666 0.84375 +vt 0.8125 0.84375 +vt 0.8125 0.5625 +vt 0.7291666666666666 0.5625 +vt 0.625 0.84375 +vt 0.7291666666666666 0.84375 +vt 0.7291666666666666 0.5625 +vt 0.625 0.5625 +vt 0.9166666666666666 0.84375 +vt 1 0.84375 +vt 1 0.5625 +vt 0.9166666666666666 0.5625 +vt 0.8125 0.84375 +vt 0.9166666666666666 0.84375 +vt 0.9166666666666666 0.5625 +vt 0.8125 0.5625 +vt 0.8125 0.84375 +vt 0.7291666666666666 0.84375 +vt 0.7291666666666666 1 +vt 0.8125 1 +vt 0.8958333333333334 1 +vt 0.8125 1 +vt 0.8125 0.84375 +vt 0.8958333333333334 0.84375 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_55d42d08-0bdc-6d16-75c7-074abfaa1062 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/frog.obj b/renderer/viewer/three/entity/models/frog.obj new file mode 100644 index 00000000..e76a114e --- /dev/null +++ b/renderer/viewer/three/entity/models/frog.obj @@ -0,0 +1,739 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.21875 0.25 0.3125 +v 0.21875 0.25 -0.25 +v 0.21875 0.0625 0.3125 +v 0.21875 0.0625 -0.25 +v -0.21875 0.25 -0.25 +v -0.21875 0.25 0.3125 +v -0.21875 0.0625 -0.25 +v -0.21875 0.0625 0.3125 +vt 0.25 0.7916666666666666 +vt 0.3958333333333333 0.7916666666666666 +vt 0.3958333333333333 0.7291666666666667 +vt 0.25 0.7291666666666667 +vt 0.0625 0.7916666666666666 +vt 0.25 0.7916666666666666 +vt 0.25 0.7291666666666667 +vt 0.0625 0.7291666666666667 +vt 0.5833333333333334 0.7916666666666666 +vt 0.7291666666666666 0.7916666666666666 +vt 0.7291666666666666 0.7291666666666667 +vt 0.5833333333333334 0.7291666666666667 +vt 0.3958333333333333 0.7916666666666666 +vt 0.5833333333333334 0.7916666666666666 +vt 0.5833333333333334 0.7291666666666667 +vt 0.3958333333333333 0.7291666666666667 +vt 0.3958333333333333 0.7916666666666666 +vt 0.25 0.7916666666666666 +vt 0.25 0.9791666666666666 +vt 0.3958333333333333 0.9791666666666666 +vt 0.5416666666666666 0.9791666666666666 +vt 0.3958333333333333 0.9791666666666666 +vt 0.3958333333333333 0.7916666666666666 +vt 0.5416666666666666 0.7916666666666666 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.21875 0.1875 0.3125 +v 0.21875 0.1875 -0.25 +v 0.21875 0.1875 0.3125 +v 0.21875 0.1875 -0.25 +v -0.21875 0.1875 -0.25 +v -0.21875 0.1875 0.3125 +v -0.21875 0.1875 -0.25 +v -0.21875 0.1875 0.3125 +vt 0.6666666666666666 0.35416666666666663 +vt 0.8125 0.35416666666666663 +vt 0.8125 0.35416666666666663 +vt 0.6666666666666666 0.35416666666666663 +vt 0.4791666666666667 0.35416666666666663 +vt 0.6666666666666666 0.35416666666666663 +vt 0.6666666666666666 0.35416666666666663 +vt 0.4791666666666667 0.35416666666666663 +vt 1 0.35416666666666663 +vt 1.1458333333333333 0.35416666666666663 +vt 1.1458333333333333 0.35416666666666663 +vt 1 0.35416666666666663 +vt 0.8125 0.35416666666666663 +vt 1 0.35416666666666663 +vt 1 0.35416666666666663 +vt 0.8125 0.35416666666666663 +vt 0.8125 0.35416666666666663 +vt 0.6666666666666666 0.35416666666666663 +vt 0.6666666666666666 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 0.9583333333333334 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 0.8125 0.35416666666666663 +vt 0.9583333333333334 0.35416666666666663 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.21875 0.3125 0.3125 +v 0.21875 0.3125 -0.25 +v 0.21875 0.3125 0.3125 +v 0.21875 0.3125 -0.25 +v -0.21875 0.3125 -0.25 +v -0.21875 0.3125 0.3125 +v -0.21875 0.3125 -0.25 +v -0.21875 0.3125 0.3125 +vt 0.6666666666666666 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 0.6666666666666666 0.5416666666666667 +vt 0.4791666666666667 0.5416666666666667 +vt 0.6666666666666666 0.5416666666666667 +vt 0.6666666666666666 0.5416666666666667 +vt 0.4791666666666667 0.5416666666666667 +vt 1 0.5416666666666667 +vt 1.1458333333333333 0.5416666666666667 +vt 1.1458333333333333 0.5416666666666667 +vt 1 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 1 0.5416666666666667 +vt 1 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 0.8125 0.5416666666666667 +vt 0.6666666666666666 0.5416666666666667 +vt 0.6666666666666666 0.7291666666666667 +vt 0.8125 0.7291666666666667 +vt 0.9583333333333334 0.7291666666666667 +vt 0.8125 0.7291666666666667 +vt 0.8125 0.5416666666666667 +vt 0.9583333333333334 0.5416666666666667 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.21875 0.375 0.3125 +v 0.21875 0.375 -0.25 +v 0.21875 0.1875 0.3125 +v 0.21875 0.1875 -0.25 +v -0.21875 0.375 -0.25 +v -0.21875 0.375 0.3125 +v -0.21875 0.1875 -0.25 +v -0.21875 0.1875 0.3125 +vt 0.1875 0.5416666666666667 +vt 0.3333333333333333 0.5416666666666667 +vt 0.3333333333333333 0.47916666666666663 +vt 0.1875 0.47916666666666663 +vt 0 0.5416666666666667 +vt 0.1875 0.5416666666666667 +vt 0.1875 0.47916666666666663 +vt 0 0.47916666666666663 +vt 0.5208333333333334 0.5416666666666667 +vt 0.6666666666666666 0.5416666666666667 +vt 0.6666666666666666 0.47916666666666663 +vt 0.5208333333333334 0.47916666666666663 +vt 0.3333333333333333 0.5416666666666667 +vt 0.5208333333333334 0.5416666666666667 +vt 0.5208333333333334 0.47916666666666663 +vt 0.3333333333333333 0.47916666666666663 +vt 0.3333333333333333 0.5416666666666667 +vt 0.1875 0.5416666666666667 +vt 0.1875 0.7291666666666667 +vt 0.3333333333333333 0.7291666666666667 +vt 0.4791666666666667 0.7291666666666667 +vt 0.3333333333333333 0.7291666666666667 +vt 0.3333333333333333 0.5416666666666667 +vt 0.4791666666666667 0.5416666666666667 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o right_eye +v 0.21875 0.5 0 +v 0.21875 0.5 -0.1875 +v 0.21875 0.375 0 +v 0.21875 0.375 -0.1875 +v 0.03125 0.5 -0.1875 +v 0.03125 0.5 0 +v 0.03125 0.375 -0.1875 +v 0.03125 0.375 0 +vt 0.0625 0.9375 +vt 0.125 0.9375 +vt 0.125 0.8958333333333334 +vt 0.0625 0.8958333333333334 +vt 0 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.8958333333333334 +vt 0 0.8958333333333334 +vt 0.1875 0.9375 +vt 0.25 0.9375 +vt 0.25 0.8958333333333334 +vt 0.1875 0.8958333333333334 +vt 0.125 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.8958333333333334 +vt 0.125 0.8958333333333334 +vt 0.125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 1 +vt 0.125 1 +vt 0.1875 1 +vt 0.125 1 +vt 0.125 0.9375 +vt 0.1875 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o left_eye +v -0.03125 0.5 0 +v -0.03125 0.5 -0.1875 +v -0.03125 0.375 0 +v -0.03125 0.375 -0.1875 +v -0.21875 0.5 -0.1875 +v -0.21875 0.5 0 +v -0.21875 0.375 -0.1875 +v -0.21875 0.375 0 +vt 0.0625 0.8333333333333334 +vt 0.125 0.8333333333333334 +vt 0.125 0.7916666666666666 +vt 0.0625 0.7916666666666666 +vt 0 0.8333333333333334 +vt 0.0625 0.8333333333333334 +vt 0.0625 0.7916666666666666 +vt 0 0.7916666666666666 +vt 0.1875 0.8333333333333334 +vt 0.25 0.8333333333333334 +vt 0.25 0.7916666666666666 +vt 0.1875 0.7916666666666666 +vt 0.125 0.8333333333333334 +vt 0.1875 0.8333333333333334 +vt 0.1875 0.7916666666666666 +vt 0.125 0.7916666666666666 +vt 0.125 0.8333333333333334 +vt 0.0625 0.8333333333333334 +vt 0.0625 0.8958333333333334 +vt 0.125 0.8958333333333334 +vt 0.1875 0.8958333333333334 +vt 0.125 0.8958333333333334 +vt 0.125 0.8333333333333334 +vt 0.1875 0.8333333333333334 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o croaking_body +v 0.21250000000000002 0.1875 -0.0625 +v 0.21250000000000002 0.1875 -0.2375 +v 0.21250000000000002 0.07499999999999996 -0.0625 +v 0.21250000000000002 0.07499999999999996 -0.2375 +v -0.21250000000000002 0.1875 -0.2375 +v -0.21250000000000002 0.1875 -0.0625 +v -0.21250000000000002 0.07499999999999996 -0.2375 +v -0.21250000000000002 0.07499999999999996 -0.0625 +vt 0.6041666666666666 0.8333333333333334 +vt 0.75 0.8333333333333334 +vt 0.75 0.7916666666666666 +vt 0.6041666666666666 0.7916666666666666 +vt 0.5416666666666666 0.8333333333333334 +vt 0.6041666666666666 0.8333333333333334 +vt 0.6041666666666666 0.7916666666666666 +vt 0.5416666666666666 0.7916666666666666 +vt 0.8125 0.8333333333333334 +vt 0.9583333333333334 0.8333333333333334 +vt 0.9583333333333334 0.7916666666666666 +vt 0.8125 0.7916666666666666 +vt 0.75 0.8333333333333334 +vt 0.8125 0.8333333333333334 +vt 0.8125 0.7916666666666666 +vt 0.75 0.7916666666666666 +vt 0.75 0.8333333333333334 +vt 0.6041666666666666 0.8333333333333334 +vt 0.6041666666666666 0.8958333333333334 +vt 0.75 0.8958333333333334 +vt 0.8958333333333334 0.8958333333333334 +vt 0.75 0.8958333333333334 +vt 0.75 0.8333333333333334 +vt 0.8958333333333334 0.8333333333333334 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o tongue +v 0.125 0.19374999999999998 0.30625 +v 0.125 0.19374999999999998 -0.13124999999999998 +v 0.125 0.19374999999999998 0.30625 +v 0.125 0.19374999999999998 -0.13124999999999998 +v -0.125 0.19374999999999998 -0.13124999999999998 +v -0.125 0.19374999999999998 0.30625 +v -0.125 0.19374999999999998 -0.13124999999999998 +v -0.125 0.19374999999999998 0.30625 +vt 0.5 0.5833333333333333 +vt 0.5833333333333334 0.5833333333333333 +vt 0.5833333333333334 0.5833333333333333 +vt 0.5 0.5833333333333333 +vt 0.3541666666666667 0.5833333333333333 +vt 0.5 0.5833333333333333 +vt 0.5 0.5833333333333333 +vt 0.3541666666666667 0.5833333333333333 +vt 0.7291666666666666 0.5833333333333333 +vt 0.8125 0.5833333333333333 +vt 0.8125 0.5833333333333333 +vt 0.7291666666666666 0.5833333333333333 +vt 0.5833333333333334 0.5833333333333333 +vt 0.7291666666666666 0.5833333333333333 +vt 0.7291666666666666 0.5833333333333333 +vt 0.5833333333333334 0.5833333333333333 +vt 0.5833333333333334 0.5833333333333333 +vt 0.5 0.5833333333333333 +vt 0.5 0.7291666666666667 +vt 0.5833333333333334 0.7291666666666667 +vt 0.6666666666666666 0.7291666666666667 +vt 0.5833333333333334 0.7291666666666667 +vt 0.5833333333333334 0.5833333333333333 +vt 0.6666666666666666 0.5833333333333333 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o left_arm +v -0.1875 0.1875 -0.03125 +v -0.1875 0.1875 -0.21875 +v -0.1875 0 -0.03125 +v -0.1875 0 -0.21875 +v -0.3125 0.1875 -0.21875 +v -0.3125 0.1875 -0.03125 +v -0.3125 0 -0.21875 +v -0.3125 0 -0.03125 +vt 0.0625 0.27083333333333337 +vt 0.10416666666666667 0.27083333333333337 +vt 0.10416666666666667 0.20833333333333337 +vt 0.0625 0.20833333333333337 +vt 0 0.27083333333333337 +vt 0.0625 0.27083333333333337 +vt 0.0625 0.20833333333333337 +vt 0 0.20833333333333337 +vt 0.16666666666666666 0.27083333333333337 +vt 0.20833333333333334 0.27083333333333337 +vt 0.20833333333333334 0.20833333333333337 +vt 0.16666666666666666 0.20833333333333337 +vt 0.10416666666666667 0.27083333333333337 +vt 0.16666666666666666 0.27083333333333337 +vt 0.16666666666666666 0.20833333333333337 +vt 0.10416666666666667 0.20833333333333337 +vt 0.10416666666666667 0.27083333333333337 +vt 0.0625 0.27083333333333337 +vt 0.0625 0.33333333333333337 +vt 0.10416666666666667 0.33333333333333337 +vt 0.14583333333333334 0.33333333333333337 +vt 0.10416666666666667 0.33333333333333337 +vt 0.10416666666666667 0.27083333333333337 +vt 0.14583333333333334 0.27083333333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o left_arm +v 0 -0.0006249999999999867 0.03125 +v 0 -0.0006249999999999867 -0.46875 +v 0 -0.0006249999999999867 0.03125 +v 0 -0.0006249999999999867 -0.46875 +v -0.5 -0.0006249999999999867 -0.46875 +v -0.5 -0.0006249999999999867 0.03125 +v -0.5 -0.0006249999999999867 -0.46875 +v -0.5 -0.0006249999999999867 0.03125 +vt 0.5416666666666666 0 +vt 0.7083333333333334 0 +vt 0.7083333333333334 0 +vt 0.5416666666666666 0 +vt 0.375 0 +vt 0.5416666666666666 0 +vt 0.5416666666666666 0 +vt 0.375 0 +vt 0.875 0 +vt 1.0416666666666667 0 +vt 1.0416666666666667 0 +vt 0.875 0 +vt 0.7083333333333334 0 +vt 0.875 0 +vt 0.875 0 +vt 0.7083333333333334 0 +vt 0.7083333333333334 0 +vt 0.5416666666666666 0 +vt 0.5416666666666666 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.875 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.7083333333333334 0 +vt 0.875 0 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o right_arm +v 0.3125 0.1875 -0.03125 +v 0.3125 0.1875 -0.21875 +v 0.3125 0 -0.03125 +v 0.3125 0 -0.21875 +v 0.1875 0.1875 -0.21875 +v 0.1875 0.1875 -0.03125 +v 0.1875 0 -0.21875 +v 0.1875 0 -0.03125 +vt 0.0625 0.14583333333333337 +vt 0.10416666666666667 0.14583333333333337 +vt 0.10416666666666667 0.08333333333333337 +vt 0.0625 0.08333333333333337 +vt 0 0.14583333333333337 +vt 0.0625 0.14583333333333337 +vt 0.0625 0.08333333333333337 +vt 0 0.08333333333333337 +vt 0.16666666666666666 0.14583333333333337 +vt 0.20833333333333334 0.14583333333333337 +vt 0.20833333333333334 0.08333333333333337 +vt 0.16666666666666666 0.08333333333333337 +vt 0.10416666666666667 0.14583333333333337 +vt 0.16666666666666666 0.14583333333333337 +vt 0.16666666666666666 0.08333333333333337 +vt 0.10416666666666667 0.08333333333333337 +vt 0.10416666666666667 0.14583333333333337 +vt 0.0625 0.14583333333333337 +vt 0.0625 0.20833333333333337 +vt 0.10416666666666667 0.20833333333333337 +vt 0.14583333333333334 0.20833333333333337 +vt 0.10416666666666667 0.20833333333333337 +vt 0.10416666666666667 0.14583333333333337 +vt 0.14583333333333334 0.14583333333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o right_arm +v 0.5 -0.0006249999999999867 0.03125 +v 0.5 -0.0006249999999999867 -0.46875 +v 0.5 -0.0006249999999999867 0.03125 +v 0.5 -0.0006249999999999867 -0.46875 +v 0 -0.0006249999999999867 -0.46875 +v 0 -0.0006249999999999867 0.03125 +v 0 -0.0006249999999999867 -0.46875 +v 0 -0.0006249999999999867 0.03125 +vt 0.20833333333333334 0 +vt 0.375 0 +vt 0.375 0 +vt 0.20833333333333334 0 +vt 0.041666666666666664 0 +vt 0.20833333333333334 0 +vt 0.20833333333333334 0 +vt 0.041666666666666664 0 +vt 0.5416666666666666 0 +vt 0.7083333333333334 0 +vt 0.7083333333333334 0 +vt 0.5416666666666666 0 +vt 0.375 0 +vt 0.5416666666666666 0 +vt 0.5416666666666666 0 +vt 0.375 0 +vt 0.375 0 +vt 0.20833333333333334 0 +vt 0.20833333333333334 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.375 0 +vt 0.5416666666666666 0 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o left_leg +v -0.15625 0.1875 0.375 +v -0.15625 0.1875 0.125 +v -0.15625 0 0.375 +v -0.15625 0 0.125 +v -0.34375 0.1875 0.125 +v -0.34375 0.1875 0.375 +v -0.34375 0 0.125 +v -0.34375 0 0.375 +vt 0.375 0.39583333333333337 +vt 0.4375 0.39583333333333337 +vt 0.4375 0.33333333333333337 +vt 0.375 0.33333333333333337 +vt 0.2916666666666667 0.39583333333333337 +vt 0.375 0.39583333333333337 +vt 0.375 0.33333333333333337 +vt 0.2916666666666667 0.33333333333333337 +vt 0.5208333333333334 0.39583333333333337 +vt 0.5833333333333334 0.39583333333333337 +vt 0.5833333333333334 0.33333333333333337 +vt 0.5208333333333334 0.33333333333333337 +vt 0.4375 0.39583333333333337 +vt 0.5208333333333334 0.39583333333333337 +vt 0.5208333333333334 0.33333333333333337 +vt 0.4375 0.33333333333333337 +vt 0.4375 0.39583333333333337 +vt 0.375 0.39583333333333337 +vt 0.375 0.47916666666666663 +vt 0.4375 0.47916666666666663 +vt 0.5 0.47916666666666663 +vt 0.4375 0.47916666666666663 +vt 0.4375 0.39583333333333337 +vt 0.5 0.39583333333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o left_leg +v -0.09375 -0.0006249999999999867 0.5 +v -0.09375 -0.0006249999999999867 0 +v -0.09375 -0.0006249999999999867 0.5 +v -0.09375 -0.0006249999999999867 0 +v -0.59375 -0.0006249999999999867 0 +v -0.59375 -0.0006249999999999867 0.5 +v -0.59375 -0.0006249999999999867 0 +v -0.59375 -0.0006249999999999867 0.5 +vt 0.20833333333333334 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.20833333333333334 0.16666666666666663 +vt 0.041666666666666664 0.16666666666666663 +vt 0.20833333333333334 0.16666666666666663 +vt 0.20833333333333334 0.16666666666666663 +vt 0.041666666666666664 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.20833333333333334 0.16666666666666663 +vt 0.20833333333333334 0.33333333333333337 +vt 0.375 0.33333333333333337 +vt 0.5416666666666666 0.33333333333333337 +vt 0.375 0.33333333333333337 +vt 0.375 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o right_leg +v 0.34375 0.1875 0.375 +v 0.34375 0.1875 0.125 +v 0.34375 0 0.375 +v 0.34375 0 0.125 +v 0.15625 0.1875 0.125 +v 0.15625 0.1875 0.375 +v 0.15625 0 0.125 +v 0.15625 0 0.375 +vt 0.08333333333333333 0.39583333333333337 +vt 0.14583333333333334 0.39583333333333337 +vt 0.14583333333333334 0.33333333333333337 +vt 0.08333333333333333 0.33333333333333337 +vt 0 0.39583333333333337 +vt 0.08333333333333333 0.39583333333333337 +vt 0.08333333333333333 0.33333333333333337 +vt 0 0.33333333333333337 +vt 0.22916666666666666 0.39583333333333337 +vt 0.2916666666666667 0.39583333333333337 +vt 0.2916666666666667 0.33333333333333337 +vt 0.22916666666666666 0.33333333333333337 +vt 0.14583333333333334 0.39583333333333337 +vt 0.22916666666666666 0.39583333333333337 +vt 0.22916666666666666 0.33333333333333337 +vt 0.14583333333333334 0.33333333333333337 +vt 0.14583333333333334 0.39583333333333337 +vt 0.08333333333333333 0.39583333333333337 +vt 0.08333333333333333 0.47916666666666663 +vt 0.14583333333333334 0.47916666666666663 +vt 0.20833333333333334 0.47916666666666663 +vt 0.14583333333333334 0.47916666666666663 +vt 0.14583333333333334 0.39583333333333337 +vt 0.20833333333333334 0.39583333333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 +o right_leg +v 0.59375 -0.0006249999999999867 0.5 +v 0.59375 -0.0006249999999999867 0 +v 0.59375 -0.0006249999999999867 0.5 +v 0.59375 -0.0006249999999999867 0 +v 0.09375 -0.0006249999999999867 0 +v 0.09375 -0.0006249999999999867 0.5 +v 0.09375 -0.0006249999999999867 0 +v 0.09375 -0.0006249999999999867 0.5 +vt 0.5416666666666666 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.375 0.16666666666666663 +vt 0.875 0.16666666666666663 +vt 1.0416666666666667 0.16666666666666663 +vt 1.0416666666666667 0.16666666666666663 +vt 0.875 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.875 0.16666666666666663 +vt 0.875 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.7083333333333334 0.16666666666666663 +vt 0.5416666666666666 0.16666666666666663 +vt 0.5416666666666666 0.33333333333333337 +vt 0.7083333333333334 0.33333333333333337 +vt 0.875 0.33333333333333337 +vt 0.7083333333333334 0.33333333333333337 +vt 0.7083333333333334 0.16666666666666663 +vt 0.875 0.16666666666666663 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_4288dcd0-d35c-c2c4-0fee-38c374635ee1 +f 124/364/91 127/363/91 125/362/91 122/361/91 +f 123/368/92 124/367/92 122/366/92 121/365/92 +f 128/372/93 123/371/93 121/370/93 126/369/93 +f 127/376/94 128/375/94 126/374/94 125/373/94 +f 126/380/95 121/379/95 122/378/95 125/377/95 +f 127/384/96 124/383/96 123/382/96 128/381/96 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/ghast.obj b/renderer/viewer/three/entity/models/ghast.obj new file mode 100644 index 00000000..bed7ccb2 --- /dev/null +++ b/renderer/viewer/three/entity/models/ghast.obj @@ -0,0 +1,463 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.5 1 0.5 +v 0.5 1 -0.5 +v 0.5 0 0.5 +v 0.5 0 -0.5 +v -0.5 1 -0.5 +v -0.5 1 0.5 +v -0.5 0 -0.5 +v -0.5 0 0.5 +vt 0.25 0.5 +vt 0.5 0.5 +vt 0.5 0 +vt 0.25 0 +vt 0 0.5 +vt 0.25 0.5 +vt 0.25 0 +vt 0 0 +vt 0.75 0.5 +vt 1 0.5 +vt 1 0 +vt 0.75 0 +vt 0.5 0.5 +vt 0.75 0.5 +vt 0.75 0 +vt 0.5 0 +vt 0.5 0.5 +vt 0.25 0.5 +vt 0.25 1 +vt 0.5 1 +vt 0.75 1 +vt 0.5 1 +vt 0.5 0.5 +vt 0.75 0.5 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o tentacles_0 +v 0.30000000000000004 0.0625 -0.25 +v 0.30000000000000004 0.0625 -0.375 +v 0.30000000000000004 -0.5 -0.25 +v 0.30000000000000004 -0.5 -0.375 +v 0.17500000000000004 0.0625 -0.375 +v 0.17500000000000004 0.0625 -0.25 +v 0.17500000000000004 -0.5 -0.375 +v 0.17500000000000004 -0.5 -0.25 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.65625 +vt 0.03125 0.65625 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.65625 +vt 0 0.65625 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.65625 +vt 0.09375 0.65625 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.65625 +vt 0.0625 0.65625 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o tentacles_1 +v -0.01874999999999999 0.0625 -0.25 +v -0.01874999999999999 0.0625 -0.375 +v -0.01874999999999999 -0.625 -0.25 +v -0.01874999999999999 -0.625 -0.375 +v -0.14375 0.0625 -0.375 +v -0.14375 0.0625 -0.25 +v -0.14375 -0.625 -0.375 +v -0.14375 -0.625 -0.25 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.59375 +vt 0.03125 0.59375 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.59375 +vt 0 0.59375 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.59375 +vt 0.09375 0.59375 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o tentacles_2 +v -0.33125 0.0625 -0.25 +v -0.33125 0.0625 -0.375 +v -0.33125 -0.4375 -0.25 +v -0.33125 -0.4375 -0.375 +v -0.45625 0.0625 -0.375 +v -0.45625 0.0625 -0.25 +v -0.45625 -0.4375 -0.375 +v -0.45625 -0.4375 -0.25 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.6875 +vt 0.03125 0.6875 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.6875 +vt 0 0.6875 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.6875 +vt 0.09375 0.6875 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o tentacles_3 +v 0.45625000000000004 0.0625 0.0625 +v 0.45625000000000004 0.0625 -0.0625 +v 0.45625000000000004 -0.5 0.0625 +v 0.45625000000000004 -0.5 -0.0625 +v 0.33125000000000004 0.0625 -0.0625 +v 0.33125000000000004 0.0625 0.0625 +v 0.33125000000000004 -0.5 -0.0625 +v 0.33125000000000004 -0.5 0.0625 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.65625 +vt 0.03125 0.65625 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.65625 +vt 0 0.65625 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.65625 +vt 0.09375 0.65625 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.65625 +vt 0.0625 0.65625 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o tentacles_4 +v 0.14375000000000004 0.0625 0.0625 +v 0.14375000000000004 0.0625 -0.0625 +v 0.14375000000000004 -0.75 0.0625 +v 0.14375000000000004 -0.75 -0.0625 +v 0.018750000000000044 0.0625 -0.0625 +v 0.018750000000000044 0.0625 0.0625 +v 0.018750000000000044 -0.75 -0.0625 +v 0.018750000000000044 -0.75 0.0625 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.53125 +vt 0.03125 0.53125 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.53125 +vt 0 0.53125 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.53125 +vt 0.09375 0.53125 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.53125 +vt 0.0625 0.53125 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o tentacles_5 +v -0.175 0.0625 0.0625 +v -0.175 0.0625 -0.0625 +v -0.175 -0.625 0.0625 +v -0.175 -0.625 -0.0625 +v -0.3 0.0625 -0.0625 +v -0.3 0.0625 0.0625 +v -0.3 -0.625 -0.0625 +v -0.3 -0.625 0.0625 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.59375 +vt 0.03125 0.59375 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.59375 +vt 0 0.59375 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.59375 +vt 0.09375 0.59375 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o tentacles_6 +v 0.30000000000000004 0.0625 0.375 +v 0.30000000000000004 0.0625 0.25 +v 0.30000000000000004 -0.6875 0.375 +v 0.30000000000000004 -0.6875 0.25 +v 0.17500000000000004 0.0625 0.25 +v 0.17500000000000004 0.0625 0.375 +v 0.17500000000000004 -0.6875 0.25 +v 0.17500000000000004 -0.6875 0.375 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.5625 +vt 0.03125 0.5625 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.5625 +vt 0 0.5625 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.5625 +vt 0.09375 0.5625 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.5625 +vt 0.0625 0.5625 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o tentacles_7 +v -0.01874999999999999 0.0625 0.375 +v -0.01874999999999999 0.0625 0.25 +v -0.01874999999999999 -0.6875 0.375 +v -0.01874999999999999 -0.6875 0.25 +v -0.14375 0.0625 0.25 +v -0.14375 0.0625 0.375 +v -0.14375 -0.6875 0.25 +v -0.14375 -0.6875 0.375 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.5625 +vt 0.03125 0.5625 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.5625 +vt 0 0.5625 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.5625 +vt 0.09375 0.5625 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.5625 +vt 0.0625 0.5625 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o tentacles_8 +v -0.33125 0.0625 0.375 +v -0.33125 0.0625 0.25 +v -0.33125 -0.75 0.375 +v -0.33125 -0.75 0.25 +v -0.45625 0.0625 0.25 +v -0.45625 0.0625 0.375 +v -0.45625 -0.75 0.25 +v -0.45625 -0.75 0.375 +vt 0.03125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.53125 +vt 0.03125 0.53125 +vt 0 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.53125 +vt 0 0.53125 +vt 0.09375 0.9375 +vt 0.125 0.9375 +vt 0.125 0.53125 +vt 0.09375 0.53125 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vt 0.09375 0.53125 +vt 0.0625 0.53125 +vt 0.0625 0.9375 +vt 0.03125 0.9375 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.9375 +vt 0.09375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_be60a09e-8083-c62f-16c0-359ca5c36498 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/goat.obj b/renderer/viewer/three/entity/models/goat.obj new file mode 100644 index 00000000..adb8910c --- /dev/null +++ b/renderer/viewer/three/entity/models/goat.obj @@ -0,0 +1,601 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o left_back_leg +v -0.0625 0.375 0.4375 +v -0.0625 0.375 0.25 +v -0.0625 0 0.4375 +v -0.0625 0 0.25 +v -0.25 0.375 0.25 +v -0.25 0.375 0.4375 +v -0.25 0 0.25 +v -0.25 0 0.4375 +vt 0.609375 0.5 +vt 0.65625 0.5 +vt 0.65625 0.40625 +vt 0.609375 0.40625 +vt 0.5625 0.5 +vt 0.609375 0.5 +vt 0.609375 0.40625 +vt 0.5625 0.40625 +vt 0.703125 0.5 +vt 0.75 0.5 +vt 0.75 0.40625 +vt 0.703125 0.40625 +vt 0.65625 0.5 +vt 0.703125 0.5 +vt 0.703125 0.40625 +vt 0.65625 0.40625 +vt 0.65625 0.5 +vt 0.609375 0.5 +vt 0.609375 0.546875 +vt 0.65625 0.546875 +vt 0.703125 0.546875 +vt 0.65625 0.546875 +vt 0.65625 0.5 +vt 0.703125 0.5 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o right_back_leg +v 0.1875 0.375 0.4375 +v 0.1875 0.375 0.25 +v 0.1875 0 0.4375 +v 0.1875 0 0.25 +v 0 0.375 0.25 +v 0 0.375 0.4375 +v 0 0 0.25 +v 0 0 0.4375 +vt 0.8125 0.5 +vt 0.859375 0.5 +vt 0.859375 0.40625 +vt 0.8125 0.40625 +vt 0.765625 0.5 +vt 0.8125 0.5 +vt 0.8125 0.40625 +vt 0.765625 0.40625 +vt 0.90625 0.5 +vt 0.953125 0.5 +vt 0.953125 0.40625 +vt 0.90625 0.40625 +vt 0.859375 0.5 +vt 0.90625 0.5 +vt 0.90625 0.40625 +vt 0.859375 0.40625 +vt 0.859375 0.5 +vt 0.8125 0.5 +vt 0.8125 0.546875 +vt 0.859375 0.546875 +vt 0.90625 0.546875 +vt 0.859375 0.546875 +vt 0.859375 0.5 +vt 0.90625 0.5 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o right_front_leg +v 0.1875 0.625 -0.1875 +v 0.1875 0.625 -0.375 +v 0.1875 0 -0.1875 +v 0.1875 0 -0.375 +v 0 0.625 -0.375 +v 0 0.625 -0.1875 +v 0 0 -0.375 +v 0 0 -0.1875 +vt 0.8125 0.921875 +vt 0.859375 0.921875 +vt 0.859375 0.765625 +vt 0.8125 0.765625 +vt 0.765625 0.921875 +vt 0.8125 0.921875 +vt 0.8125 0.765625 +vt 0.765625 0.765625 +vt 0.90625 0.921875 +vt 0.953125 0.921875 +vt 0.953125 0.765625 +vt 0.90625 0.765625 +vt 0.859375 0.921875 +vt 0.90625 0.921875 +vt 0.90625 0.765625 +vt 0.859375 0.765625 +vt 0.859375 0.921875 +vt 0.8125 0.921875 +vt 0.8125 0.96875 +vt 0.859375 0.96875 +vt 0.90625 0.96875 +vt 0.859375 0.96875 +vt 0.859375 0.921875 +vt 0.90625 0.921875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o left_front_leg +v -0.0625 0.625 -0.1875 +v -0.0625 0.625 -0.375 +v -0.0625 0 -0.1875 +v -0.0625 0 -0.375 +v -0.25 0.625 -0.375 +v -0.25 0.625 -0.1875 +v -0.25 0 -0.375 +v -0.25 0 -0.1875 +vt 0.59375 0.921875 +vt 0.640625 0.921875 +vt 0.640625 0.765625 +vt 0.59375 0.765625 +vt 0.546875 0.921875 +vt 0.59375 0.921875 +vt 0.59375 0.765625 +vt 0.546875 0.765625 +vt 0.6875 0.921875 +vt 0.734375 0.921875 +vt 0.734375 0.765625 +vt 0.6875 0.765625 +vt 0.640625 0.921875 +vt 0.6875 0.921875 +vt 0.6875 0.765625 +vt 0.640625 0.765625 +vt 0.640625 0.921875 +vt 0.59375 0.921875 +vt 0.59375 0.96875 +vt 0.640625 0.96875 +vt 0.6875 0.96875 +vt 0.640625 0.96875 +vt 0.640625 0.921875 +vt 0.6875 0.921875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o body +v 0.25 1.0625 0.5625 +v 0.25 1.0625 -0.4375 +v 0.25 0.375 0.5625 +v 0.25 0.375 -0.4375 +v -0.3125 1.0625 -0.4375 +v -0.3125 1.0625 0.5625 +v -0.3125 0.375 -0.4375 +v -0.3125 0.375 0.5625 +vt 0.265625 0.734375 +vt 0.40625 0.734375 +vt 0.40625 0.5625 +vt 0.265625 0.5625 +vt 0.015625 0.734375 +vt 0.265625 0.734375 +vt 0.265625 0.5625 +vt 0.015625 0.5625 +vt 0.65625 0.734375 +vt 0.796875 0.734375 +vt 0.796875 0.5625 +vt 0.65625 0.5625 +vt 0.40625 0.734375 +vt 0.65625 0.734375 +vt 0.65625 0.5625 +vt 0.40625 0.5625 +vt 0.40625 0.734375 +vt 0.265625 0.734375 +vt 0.265625 0.984375 +vt 0.40625 0.984375 +vt 0.546875 0.984375 +vt 0.40625 0.984375 +vt 0.40625 0.734375 +vt 0.546875 0.734375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o body +v 0.3125 1.125 0.1875 +v 0.3125 1.125 -0.5 +v 0.3125 0.25 0.1875 +v 0.3125 0.25 -0.5 +v -0.375 1.125 -0.5 +v -0.375 1.125 0.1875 +v -0.375 0.25 -0.5 +v -0.375 0.25 0.1875 +vt 0.171875 0.390625 +vt 0.34375 0.390625 +vt 0.34375 0.171875 +vt 0.171875 0.171875 +vt 0 0.390625 +vt 0.171875 0.390625 +vt 0.171875 0.171875 +vt 0 0.171875 +vt 0.515625 0.390625 +vt 0.6875 0.390625 +vt 0.6875 0.171875 +vt 0.515625 0.171875 +vt 0.34375 0.390625 +vt 0.515625 0.390625 +vt 0.515625 0.171875 +vt 0.34375 0.171875 +vt 0.34375 0.390625 +vt 0.171875 0.390625 +vt 0.171875 0.5625 +vt 0.34375 0.5625 +vt 0.515625 0.5625 +vt 0.34375 0.5625 +vt 0.34375 0.390625 +vt 0.515625 0.390625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o Head +v 0.125 1.3707881146238856 -0.6330909565283671 +v 0.125 0.8588180869432658 -0.991576229247771 +v 0.125 1.1198484237203028 -0.2747119371519333 +v 0.125 0.607878396039683 -0.6331972098713372 +v -0.1875 0.8588180869432658 -0.991576229247771 +v -0.1875 1.3707881146238856 -0.6330909565283671 +v -0.1875 0.607878396039683 -0.6331972098713372 +v -0.1875 1.1198484237203028 -0.2747119371519333 +vt 0.6875 0.125 +vt 0.765625 0.125 +vt 0.765625 0.015625 +vt 0.6875 0.015625 +vt 0.53125 0.125 +vt 0.6875 0.125 +vt 0.6875 0.015625 +vt 0.53125 0.015625 +vt 0.921875 0.125 +vt 1 0.125 +vt 1 0.015625 +vt 0.921875 0.015625 +vt 0.765625 0.125 +vt 0.921875 0.125 +vt 0.921875 0.015625 +vt 0.765625 0.015625 +vt 0.765625 0.125 +vt 0.6875 0.125 +vt 0.6875 0.28125 +vt 0.765625 0.28125 +vt 0.84375 0.28125 +vt 0.765625 0.28125 +vt 0.765625 0.125 +vt 0.84375 0.125 +vn 0 -0.8191520442889917 -0.5735764363510463 +vn 1 0 0 +vn 0 0.8191520442889917 0.5735764363510463 +vn -1 0 0 +vn 0 0.5735764363510463 -0.8191520442889917 +vn 0 -0.5735764363510463 0.8191520442889917 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o Head +v 0.12437500000000001 1.625 -0.5 +v 0.12437500000000001 1.625 -0.625 +v 0.12437500000000001 1.1875 -0.5 +v 0.12437500000000001 1.1875 -0.625 +v -0.0006249999999999867 1.625 -0.625 +v -0.0006249999999999867 1.625 -0.5 +v -0.0006249999999999867 1.1875 -0.625 +v -0.0006249999999999867 1.1875 -0.5 +vt 0.21875 0.109375 +vt 0.25 0.109375 +vt 0.25 0 +vt 0.21875 0 +vt 0.1875 0.109375 +vt 0.21875 0.109375 +vt 0.21875 0 +vt 0.1875 0 +vt 0.28125 0.109375 +vt 0.3125 0.109375 +vt 0.3125 0 +vt 0.28125 0 +vt 0.25 0.109375 +vt 0.28125 0.109375 +vt 0.28125 0 +vt 0.25 0 +vt 0.25 0.109375 +vt 0.21875 0.109375 +vt 0.21875 0.140625 +vt 0.25 0.140625 +vt 0.28125 0.140625 +vt 0.25 0.140625 +vt 0.25 0.109375 +vt 0.28125 0.109375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o Head +v -0.06187500000000001 1.625 -0.5 +v -0.06187500000000001 1.625 -0.625 +v -0.06187500000000001 1.1875 -0.5 +v -0.06187500000000001 1.1875 -0.625 +v -0.186875 1.625 -0.625 +v -0.186875 1.625 -0.5 +v -0.186875 1.1875 -0.625 +v -0.186875 1.1875 -0.5 +vt 0.21875 0.109375 +vt 0.25 0.109375 +vt 0.25 0 +vt 0.21875 0 +vt 0.1875 0.109375 +vt 0.21875 0.109375 +vt 0.21875 0 +vt 0.1875 0 +vt 0.28125 0.109375 +vt 0.3125 0.109375 +vt 0.3125 0 +vt 0.28125 0 +vt 0.25 0.109375 +vt 0.28125 0.109375 +vt 0.28125 0 +vt 0.25 0 +vt 0.25 0.109375 +vt 0.21875 0.109375 +vt 0.21875 0.140625 +vt 0.25 0.140625 +vt 0.28125 0.140625 +vt 0.25 0.140625 +vt 0.25 0.109375 +vt 0.28125 0.109375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o Head +v -0.1875 1.3125 -0.5625 +v -0.1875 1.3125 -0.625 +v -0.1875 1.1875 -0.5625 +v -0.1875 1.1875 -0.625 +v -0.375 1.3125 -0.625 +v -0.375 1.3125 -0.5625 +v -0.375 1.1875 -0.625 +v -0.375 1.1875 -0.5625 +vt 0.09375 0.03125 +vt 0.046875 0.03125 +vt 0.046875 0 +vt 0.09375 0 +vt 0.109375 0.03125 +vt 0.09375 0.03125 +vt 0.09375 0 +vt 0.109375 0 +vt 0.15625 0.03125 +vt 0.109375 0.03125 +vt 0.109375 0 +vt 0.15625 0 +vt 0.046875 0.03125 +vt 0.03125 0.03125 +vt 0.03125 0 +vt 0.046875 0 +vt 0.046875 0.03125 +vt 0.09375 0.03125 +vt 0.09375 0.046875 +vt 0.046875 0.046875 +vt 0.09375 0.046875 +vt 0.140625 0.046875 +vt 0.140625 0.03125 +vt 0.09375 0.03125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o Head +v 0.3125 1.3125 -0.5625 +v 0.3125 1.3125 -0.625 +v 0.3125 1.1875 -0.5625 +v 0.3125 1.1875 -0.625 +v 0.125 1.3125 -0.625 +v 0.125 1.3125 -0.5625 +v 0.125 1.1875 -0.625 +v 0.125 1.1875 -0.5625 +vt 0.046875 0.03125 +vt 0.09375 0.03125 +vt 0.09375 0 +vt 0.046875 0 +vt 0.03125 0.03125 +vt 0.046875 0.03125 +vt 0.046875 0 +vt 0.03125 0 +vt 0.109375 0.03125 +vt 0.15625 0.03125 +vt 0.15625 0 +vt 0.109375 0 +vt 0.09375 0.03125 +vt 0.109375 0.03125 +vt 0.109375 0 +vt 0.09375 0 +vt 0.09375 0.03125 +vt 0.046875 0.03125 +vt 0.046875 0.046875 +vt 0.09375 0.046875 +vt 0.140625 0.046875 +vt 0.09375 0.046875 +vt 0.09375 0.03125 +vt 0.140625 0.03125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o Head +v -0.03125 0.8125 -0.5625 +v -0.03125 0.8125 -0.875 +v -0.03125 0.375 -0.5625 +v -0.03125 0.375 -0.875 +v -0.03125 0.8125 -0.875 +v -0.03125 0.8125 -0.5625 +v -0.03125 0.375 -0.875 +v -0.03125 0.375 -0.5625 +vt 0.4375 0.109375 +vt 0.4375 0.109375 +vt 0.4375 0 +vt 0.4375 0 +vt 0.359375 0.109375 +vt 0.4375 0.109375 +vt 0.4375 0 +vt 0.359375 0 +vt 0.515625 0.109375 +vt 0.515625 0.109375 +vt 0.515625 0 +vt 0.515625 0 +vt 0.4375 0.109375 +vt 0.515625 0.109375 +vt 0.515625 0 +vt 0.4375 0 +vt 0.4375 0.109375 +vt 0.4375 0.109375 +vt 0.4375 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0.109375 +vt 0.4375 0.109375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o HeadMain +v 0.125 1.3707881146238856 -0.6330909565283671 +v 0.125 0.8588180869432658 -0.991576229247771 +v 0.125 1.1198484237203028 -0.2747119371519333 +v 0.125 0.607878396039683 -0.6331972098713372 +v -0.1875 0.8588180869432658 -0.991576229247771 +v -0.1875 1.3707881146238856 -0.6330909565283671 +v -0.1875 0.607878396039683 -0.6331972098713372 +v -0.1875 1.1198484237203028 -0.2747119371519333 +vt 0.6875 0.125 +vt 0.765625 0.125 +vt 0.765625 0.015625 +vt 0.6875 0.015625 +vt 0.53125 0.125 +vt 0.6875 0.125 +vt 0.6875 0.015625 +vt 0.53125 0.015625 +vt 0.921875 0.125 +vt 1 0.125 +vt 1 0.015625 +vt 0.921875 0.015625 +vt 0.765625 0.125 +vt 0.921875 0.125 +vt 0.921875 0.015625 +vt 0.765625 0.015625 +vt 0.765625 0.125 +vt 0.6875 0.125 +vt 0.6875 0.28125 +vt 0.765625 0.28125 +vt 0.84375 0.28125 +vt 0.765625 0.28125 +vt 0.765625 0.125 +vt 0.84375 0.125 +vn 0 -0.8191520442889917 -0.5735764363510463 +vn 1 0 0 +vn 0 0.8191520442889917 0.5735764363510463 +vn -1 0 0 +vn 0 0.5735764363510463 -0.8191520442889917 +vn 0 -0.5735764363510463 0.8191520442889917 +usemtl m_bd56fb3d-3403-c92f-d539-6df121464423 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/guardian.obj b/renderer/viewer/three/entity/models/guardian.obj new file mode 100644 index 00000000..0addde29 --- /dev/null +++ b/renderer/viewer/three/entity/models/guardian.obj @@ -0,0 +1,1015 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o head +v 0.375 0.875 0.5 +v 0.375 0.875 -0.5 +v 0.375 0.125 0.5 +v 0.375 0.125 -0.5 +v -0.375 0.875 -0.5 +v -0.375 0.875 0.5 +v -0.375 0.125 -0.5 +v -0.375 0.125 0.5 +vt 0.25 0.75 +vt 0.4375 0.75 +vt 0.4375 0.5625 +vt 0.25 0.5625 +vt 0 0.75 +vt 0.25 0.75 +vt 0.25 0.5625 +vt 0 0.5625 +vt 0.6875 0.75 +vt 0.875 0.75 +vt 0.875 0.5625 +vt 0.6875 0.5625 +vt 0.4375 0.75 +vt 0.6875 0.75 +vt 0.6875 0.5625 +vt 0.4375 0.5625 +vt 0.4375 0.75 +vt 0.25 0.75 +vt 0.25 1 +vt 0.4375 1 +vt 0.625 1 +vt 0.4375 1 +vt 0.4375 0.75 +vt 0.625 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.5 0.875 0.375 +v 0.5 0.875 -0.375 +v 0.5 0.125 0.375 +v 0.5 0.125 -0.375 +v 0.375 0.875 -0.375 +v 0.375 0.875 0.375 +v 0.375 0.125 -0.375 +v 0.375 0.125 0.375 +vt 0.1875 0.375 +vt 0.21875 0.375 +vt 0.21875 0.1875 +vt 0.1875 0.1875 +vt 0 0.375 +vt 0.1875 0.375 +vt 0.1875 0.1875 +vt 0 0.1875 +vt 0.40625 0.375 +vt 0.4375 0.375 +vt 0.4375 0.1875 +vt 0.40625 0.1875 +vt 0.21875 0.375 +vt 0.40625 0.375 +vt 0.40625 0.1875 +vt 0.21875 0.1875 +vt 0.21875 0.375 +vt 0.1875 0.375 +vt 0.1875 0.5625 +vt 0.21875 0.5625 +vt 0.25 0.5625 +vt 0.21875 0.5625 +vt 0.21875 0.375 +vt 0.25 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v -0.375 0.875 0.375 +v -0.375 0.875 -0.375 +v -0.375 0.125 0.375 +v -0.375 0.125 -0.375 +v -0.5 0.875 -0.375 +v -0.5 0.875 0.375 +v -0.5 0.125 -0.375 +v -0.5 0.125 0.375 +vt 0.21875 0.375 +vt 0.1875 0.375 +vt 0.1875 0.1875 +vt 0.21875 0.1875 +vt 0.40625 0.375 +vt 0.21875 0.375 +vt 0.21875 0.1875 +vt 0.40625 0.1875 +vt 0.4375 0.375 +vt 0.40625 0.375 +vt 0.40625 0.1875 +vt 0.4375 0.1875 +vt 0.1875 0.375 +vt 0 0.375 +vt 0 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0.375 +vt 0.21875 0.375 +vt 0.21875 0.5625 +vt 0.1875 0.5625 +vt 0.21875 0.5625 +vt 0.25 0.5625 +vt 0.25 0.375 +vt 0.21875 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.375 1 0.375 +v 0.375 1 -0.375 +v 0.375 0.875 0.375 +v 0.375 0.875 -0.375 +v -0.375 1 -0.375 +v -0.375 1 0.375 +v -0.375 0.875 -0.375 +v -0.375 0.875 0.375 +vt 0.625 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0.15625 +vt 0.625 0.15625 +vt 0.8125 0.1875 +vt 0.625 0.1875 +vt 0.625 0.15625 +vt 0.8125 0.15625 +vt 1 0.1875 +vt 0.8125 0.1875 +vt 0.8125 0.15625 +vt 1 0.15625 +vt 0.4375 0.1875 +vt 0.25 0.1875 +vt 0.25 0.15625 +vt 0.4375 0.15625 +vt 0.4375 0.1875 +vt 0.625 0.1875 +vt 0.625 0.375 +vt 0.4375 0.375 +vt 0.625 0.375 +vt 0.8125 0.375 +vt 0.8125 0.1875 +vt 0.625 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head +v 0.375 0.125 0.375 +v 0.375 0.125 -0.375 +v 0.375 0 0.375 +v 0.375 0 -0.375 +v -0.375 0.125 -0.375 +v -0.375 0.125 0.375 +v -0.375 0 -0.375 +v -0.375 0 0.375 +vt 0.625 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0.15625 +vt 0.625 0.15625 +vt 0.8125 0.1875 +vt 0.625 0.1875 +vt 0.625 0.15625 +vt 0.8125 0.15625 +vt 1 0.1875 +vt 0.8125 0.1875 +vt 0.8125 0.15625 +vt 1 0.15625 +vt 0.4375 0.1875 +vt 0.25 0.1875 +vt 0.25 0.15625 +vt 0.4375 0.15625 +vt 0.4375 0.1875 +vt 0.625 0.1875 +vt 0.625 0.375 +vt 0.4375 0.375 +vt 0.625 0.375 +vt 0.8125 0.375 +vt 0.8125 0.1875 +vt 0.625 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o eye +v 0.0625 0.5625 -0.453125 +v 0.0625 0.5625 -0.515625 +v 0.0625 0.4375 -0.453125 +v 0.0625 0.4375 -0.515625 +v -0.0625 0.5625 -0.515625 +v -0.0625 0.5625 -0.453125 +v -0.0625 0.4375 -0.515625 +v -0.0625 0.4375 -0.453125 +vt 0.140625 0.984375 +vt 0.171875 0.984375 +vt 0.171875 0.953125 +vt 0.140625 0.953125 +vt 0.125 0.984375 +vt 0.140625 0.984375 +vt 0.140625 0.953125 +vt 0.125 0.953125 +vt 0.1875 0.984375 +vt 0.21875 0.984375 +vt 0.21875 0.953125 +vt 0.1875 0.953125 +vt 0.171875 0.984375 +vt 0.1875 0.984375 +vt 0.1875 0.953125 +vt 0.171875 0.953125 +vt 0.171875 0.984375 +vt 0.140625 0.984375 +vt 0.140625 1 +vt 0.171875 1 +vt 0.203125 1 +vt 0.171875 1 +vt 0.171875 0.984375 +vt 0.203125 0.984375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o tailpart0 +v 0.125 0.625 1 +v 0.125 0.625 0.5 +v 0.125 0.375 1 +v 0.125 0.375 0.5 +v -0.125 0.625 0.5 +v -0.125 0.625 1 +v -0.125 0.375 0.5 +v -0.125 0.375 1 +vt 0.75 0.875 +vt 0.8125 0.875 +vt 0.8125 0.8125 +vt 0.75 0.8125 +vt 0.625 0.875 +vt 0.75 0.875 +vt 0.75 0.8125 +vt 0.625 0.8125 +vt 0.9375 0.875 +vt 1 0.875 +vt 1 0.8125 +vt 0.9375 0.8125 +vt 0.8125 0.875 +vt 0.9375 0.875 +vt 0.9375 0.8125 +vt 0.8125 0.8125 +vt 0.8125 0.875 +vt 0.75 0.875 +vt 0.75 1 +vt 0.8125 1 +vt 0.875 1 +vt 0.8125 1 +vt 0.8125 0.875 +vt 0.875 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o tailpart1 +v 0.09375 0.625 1.4375 +v 0.09375 0.625 1 +v 0.09375 0.4375 1.4375 +v 0.09375 0.4375 1 +v -0.09375 0.625 1 +v -0.09375 0.625 1.4375 +v -0.09375 0.4375 1 +v -0.09375 0.4375 1.4375 +vt 0.109375 0.046875 +vt 0.15625 0.046875 +vt 0.15625 0 +vt 0.109375 0 +vt 0 0.046875 +vt 0.109375 0.046875 +vt 0.109375 0 +vt 0 0 +vt 0.265625 0.046875 +vt 0.3125 0.046875 +vt 0.3125 0 +vt 0.265625 0 +vt 0.15625 0.046875 +vt 0.265625 0.046875 +vt 0.265625 0 +vt 0.15625 0 +vt 0.15625 0.046875 +vt 0.109375 0.046875 +vt 0.109375 0.15625 +vt 0.15625 0.15625 +vt 0.203125 0.15625 +vt 0.15625 0.15625 +vt 0.15625 0.046875 +vt 0.203125 0.046875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o tailpart2 +v 0.0625 0.625 1.8125 +v 0.0625 0.625 1.4375 +v 0.0625 0.5 1.8125 +v 0.0625 0.5 1.4375 +v -0.0625 0.625 1.4375 +v -0.0625 0.625 1.8125 +v -0.0625 0.5 1.4375 +v -0.0625 0.5 1.8125 +vt 0.734375 0.40625 +vt 0.765625 0.40625 +vt 0.765625 0.375 +vt 0.734375 0.375 +vt 0.640625 0.40625 +vt 0.734375 0.40625 +vt 0.734375 0.375 +vt 0.640625 0.375 +vt 0.859375 0.40625 +vt 0.890625 0.40625 +vt 0.890625 0.375 +vt 0.859375 0.375 +vt 0.765625 0.40625 +vt 0.859375 0.40625 +vt 0.859375 0.375 +vt 0.765625 0.375 +vt 0.765625 0.40625 +vt 0.734375 0.40625 +vt 0.734375 0.5 +vt 0.765625 0.5 +vt 0.796875 0.5 +vt 0.765625 0.5 +vt 0.765625 0.40625 +vt 0.796875 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o tailpart2 +v 0 0.84375 2.1875 +v 0 0.84375 1.625 +v 0 0.28125 2.1875 +v 0 0.28125 1.625 +v -0.0625 0.84375 1.625 +v -0.0625 0.84375 2.1875 +v -0.0625 0.28125 1.625 +v -0.0625 0.28125 2.1875 +vt 0.53125 0.5625 +vt 0.546875 0.5625 +vt 0.546875 0.421875 +vt 0.53125 0.421875 +vt 0.390625 0.5625 +vt 0.53125 0.5625 +vt 0.53125 0.421875 +vt 0.390625 0.421875 +vt 0.6875 0.5625 +vt 0.703125 0.5625 +vt 0.703125 0.421875 +vt 0.6875 0.421875 +vt 0.546875 0.5625 +vt 0.6875 0.5625 +vt 0.6875 0.421875 +vt 0.546875 0.421875 +vt 0.546875 0.5625 +vt 0.53125 0.5625 +vt 0.53125 0.703125 +vt 0.546875 0.703125 +vt 0.5625 0.703125 +vt 0.546875 0.703125 +vt 0.546875 0.5625 +vt 0.5625 0.5625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o spikepart0 +v -0.6518640639063484 1.2458835005110844 0.0625 +v -0.6518640639063484 1.2458835005110844 -0.0625 +v -0.25411649948891535 0.8481359360936516 0.0625 +v -0.25411649948891535 0.8481359360936516 -0.0625 +v -0.7402524115546669 1.157495152862766 -0.0625 +v -0.7402524115546669 1.157495152862766 0.0625 +v -0.34250484713723384 0.7597475884453331 -0.0625 +v -0.34250484713723384 0.7597475884453331 0.0625 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 0 -1 +vn 0.7071067811865475 0.7071067811865476 0 +vn 0 0 1 +vn -0.7071067811865475 -0.7071067811865476 0 +vn -0.7071067811865476 0.7071067811865475 0 +vn 0.7071067811865476 -0.7071067811865475 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o spikepart1 +v 0.7402524115546669 1.157495152862766 0.0625 +v 0.7402524115546669 1.157495152862766 -0.0625 +v 0.34250484713723384 0.7597475884453331 0.0625 +v 0.34250484713723384 0.7597475884453331 -0.0625 +v 0.6518640639063484 1.2458835005110844 -0.0625 +v 0.6518640639063484 1.2458835005110844 0.0625 +v 0.25411649948891535 0.8481359360936516 -0.0625 +v 0.25411649948891535 0.8481359360936516 0.0625 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 0 -1 +vn 0.7071067811865475 -0.7071067811865476 0 +vn 0 0 1 +vn -0.7071067811865475 0.7071067811865476 0 +vn 0.7071067811865476 0.7071067811865475 0 +vn -0.7071067811865476 -0.7071067811865475 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o spikepart2 +v 0.0625 1.2458835005110844 -0.6518640639063484 +v 0.0625 1.157495152862766 -0.7402524115546669 +v 0.0625 0.8481359360936516 -0.25411649948891535 +v 0.0625 0.7597475884453331 -0.34250484713723384 +v -0.0625 1.157495152862766 -0.7402524115546669 +v -0.0625 1.2458835005110844 -0.6518640639063484 +v -0.0625 0.7597475884453331 -0.34250484713723384 +v -0.0625 0.8481359360936516 -0.25411649948891535 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 -0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 -0.7071067811865476 +vn 0 -0.7071067811865475 0.7071067811865476 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o spikepart3 +v 0.0625 1.1464466094067263 0.7513009550107066 +v 0.0625 1.2348349570550448 0.6629126073623883 +v 0.0625 0.7486990449892934 0.3535533905932735 +v 0.0625 0.8370873926376117 0.26516504294495524 +v -0.0625 1.2348349570550448 0.6629126073623883 +v -0.0625 1.1464466094067263 0.7513009550107066 +v -0.0625 0.8370873926376117 0.26516504294495524 +v -0.0625 0.7486990449892934 0.3535533905932735 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o spikepart4 +v -0.7623494984667472 -0.16833006186201072 0.0625 +v -0.7623494984667472 -0.16833006186201072 -0.0625 +v -0.36460193404931407 0.22941750255542193 0.0625 +v -0.36460193404931407 0.22941750255542193 -0.0625 +v -0.6739611508184287 -0.256718409510329 -0.0625 +v -0.6739611508184287 -0.256718409510329 0.0625 +v -0.2762135864009956 0.14102915490710366 -0.0625 +v -0.2762135864009956 0.14102915490710366 0.0625 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 0 -1 +vn -0.7071067811865475 0.7071067811865477 0 +vn 0 0 1 +vn 0.7071067811865475 -0.7071067811865477 0 +vn -0.7071067811865477 -0.7071067811865475 0 +vn 0.7071067811865477 0.7071067811865475 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 +o spikepart5 +v 0.6739611508184287 -0.256718409510329 0.0625 +v 0.6739611508184287 -0.256718409510329 -0.0625 +v 0.2762135864009956 0.14102915490710366 0.0625 +v 0.2762135864009956 0.14102915490710366 -0.0625 +v 0.7623494984667472 -0.16833006186201072 -0.0625 +v 0.7623494984667472 -0.16833006186201072 0.0625 +v 0.36460193404931407 0.22941750255542193 -0.0625 +v 0.36460193404931407 0.22941750255542193 0.0625 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 0 -1 +vn -0.7071067811865475 -0.7071067811865477 0 +vn 0 0 1 +vn 0.7071067811865475 0.7071067811865477 0 +vn 0.7071067811865477 -0.7071067811865475 0 +vn -0.7071067811865477 0.7071067811865475 0 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 124/364/91 127/363/91 125/362/91 122/361/91 +f 123/368/92 124/367/92 122/366/92 121/365/92 +f 128/372/93 123/371/93 121/370/93 126/369/93 +f 127/376/94 128/375/94 126/374/94 125/373/94 +f 126/380/95 121/379/95 122/378/95 125/377/95 +f 127/384/96 124/383/96 123/382/96 128/381/96 +o spikepart6 +v 0.0625 -0.21252423568616985 -0.8065436722909063 +v 0.0625 -0.30091258333448856 -0.7181553246425878 +v 0.0625 0.18522332873126324 -0.4087961078734732 +v 0.0625 0.09683498108294453 -0.3204077602251547 +v -0.0625 -0.30091258333448856 -0.7181553246425878 +v -0.0625 -0.21252423568616985 -0.8065436722909063 +v -0.0625 0.09683498108294453 -0.3204077602251547 +v -0.0625 0.18522332873126324 -0.4087961078734732 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 -0.7071067811865477 0.7071067811865475 +vn 1 0 0 +vn 0 0.7071067811865477 -0.7071067811865475 +vn -1 0 0 +vn 0 -0.7071067811865475 -0.7071067811865477 +vn 0 0.7071067811865475 0.7071067811865477 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 132/388/97 135/387/97 133/386/97 130/385/97 +f 131/392/98 132/391/98 130/390/98 129/389/98 +f 136/396/99 131/395/99 129/394/99 134/393/99 +f 135/400/100 136/399/100 134/398/100 133/397/100 +f 134/404/101 129/403/101 130/402/101 133/401/101 +f 135/408/102 132/407/102 131/406/102 136/405/102 +o spikepart7 +v 0.0625 -0.256718409510329 0.6739611508184287 +v 0.0625 -0.16833006186201072 0.7623494984667472 +v 0.0625 0.14102915490710366 0.2762135864009956 +v 0.0625 0.22941750255542193 0.36460193404931407 +v -0.0625 -0.16833006186201072 0.7623494984667472 +v -0.0625 -0.256718409510329 0.6739611508184287 +v -0.0625 0.22941750255542193 0.36460193404931407 +v -0.0625 0.14102915490710366 0.2762135864009956 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 0 0.7071067811865477 0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865477 -0.7071067811865475 +vn -1 0 0 +vn 0 -0.7071067811865475 0.7071067811865477 +vn 0 0.7071067811865475 -0.7071067811865477 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 140/412/103 143/411/103 141/410/103 138/409/103 +f 139/416/104 140/415/104 138/414/104 137/413/104 +f 144/420/105 139/419/105 137/418/105 142/417/105 +f 143/424/106 144/423/106 142/422/106 141/421/106 +f 142/428/107 137/427/107 138/426/107 141/425/107 +f 143/432/108 140/431/108 139/430/108 144/429/108 +o spikepart8 +v -0.7292038680986273 0.5625000000000004 -0.8175922157469457 +v -0.7292038680986275 0.43750000000000044 -0.8175922157469457 +v -0.33145630368119416 0.5625000000000002 -0.41984465132951265 +v -0.3314563036811944 0.4375000000000002 -0.41984465132951265 +v -0.8175922157469457 0.43750000000000044 -0.7292038680986275 +v -0.8175922157469455 0.5625000000000004 -0.7292038680986275 +v -0.41984465132951265 0.4375000000000002 -0.3314563036811944 +v -0.41984465132951243 0.5625000000000002 -0.3314563036811944 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn -2.355138688025663e-16 -1 -2.3551386880256634e-16 +vn 0.7071067811865476 7.850462293418876e-17 -0.7071067811865476 +vn 2.355138688025663e-16 1 2.3551386880256634e-16 +vn -0.7071067811865476 -7.850462293418876e-17 0.7071067811865476 +vn -0.7071067811865475 2.355138688025663e-16 -0.7071067811865476 +vn 0.7071067811865475 -2.355138688025663e-16 0.7071067811865476 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 148/436/109 151/435/109 149/434/109 146/433/109 +f 147/440/110 148/439/110 146/438/110 145/437/110 +f 152/444/111 147/443/111 145/442/111 150/441/111 +f 151/448/112 152/447/112 150/446/112 149/445/112 +f 150/452/113 145/451/113 146/450/113 149/449/113 +f 151/456/114 148/455/114 147/454/114 152/453/114 +o spikepart8 +v 0.8175922157469455 0.5625000000000004 -0.7292038680986275 +v 0.8175922157469457 0.43750000000000044 -0.7292038680986275 +v 0.41984465132951243 0.5625000000000002 -0.3314563036811944 +v 0.41984465132951265 0.4375000000000002 -0.3314563036811944 +v 0.7292038680986275 0.43750000000000044 -0.8175922157469457 +v 0.7292038680986273 0.5625000000000004 -0.8175922157469457 +v 0.3314563036811944 0.4375000000000002 -0.41984465132951265 +v 0.33145630368119416 0.5625000000000002 -0.41984465132951265 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 2.355138688025663e-16 -1 -2.3551386880256634e-16 +vn 0.7071067811865476 -7.850462293418876e-17 0.7071067811865476 +vn -2.355138688025663e-16 1 2.3551386880256634e-16 +vn -0.7071067811865476 7.850462293418876e-17 -0.7071067811865476 +vn 0.7071067811865475 2.355138688025663e-16 -0.7071067811865476 +vn -0.7071067811865475 -2.355138688025663e-16 0.7071067811865476 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 156/460/115 159/459/115 157/458/115 154/457/115 +f 155/464/116 156/463/116 154/462/116 153/461/116 +f 160/468/117 155/467/117 153/466/117 158/465/117 +f 159/472/118 160/471/118 158/470/118 157/469/118 +f 158/476/119 153/475/119 154/474/119 157/473/119 +f 159/480/120 156/479/120 155/478/120 160/477/120 +o spikepart8 +v -0.8175922157469455 0.5625 0.7292038680986273 +v -0.8175922157469457 0.4375000000000002 0.7292038680986273 +v -0.41984465132951243 0.5625 0.33145630368119416 +v -0.41984465132951265 0.4375 0.33145630368119416 +v -0.7292038680986275 0.4375000000000002 0.8175922157469455 +v -0.7292038680986273 0.5625 0.8175922157469455 +v -0.3314563036811944 0.4375 0.41984465132951243 +v -0.33145630368119416 0.5625 0.41984465132951243 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn -1.5700924586837752e-16 -1 1.570092458683775e-16 +vn -0.7071067811865475 7.850462293418875e-17 -0.7071067811865476 +vn 1.5700924586837752e-16 1 -1.570092458683775e-16 +vn 0.7071067811865475 -7.850462293418875e-17 0.7071067811865476 +vn -0.7071067811865476 7.850462293418876e-17 0.7071067811865475 +vn 0.7071067811865476 -7.850462293418876e-17 -0.7071067811865475 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 164/484/121 167/483/121 165/482/121 162/481/121 +f 163/488/122 164/487/122 162/486/122 161/485/122 +f 168/492/123 163/491/123 161/490/123 166/489/123 +f 167/496/124 168/495/124 166/494/124 165/493/124 +f 166/500/125 161/499/125 162/498/125 165/497/125 +f 167/504/126 164/503/126 163/502/126 168/501/126 +o spikepart8 +v 0.7292038680986273 0.5625 0.8175922157469455 +v 0.7292038680986275 0.4375000000000002 0.8175922157469455 +v 0.33145630368119416 0.5625 0.41984465132951243 +v 0.3314563036811944 0.4375 0.41984465132951243 +v 0.8175922157469457 0.4375000000000002 0.7292038680986273 +v 0.8175922157469455 0.5625 0.7292038680986273 +v 0.41984465132951265 0.4375 0.33145630368119416 +v 0.41984465132951243 0.5625 0.33145630368119416 +vt 0.03125 0.96875 +vt 0.0625 0.96875 +vt 0.0625 0.828125 +vt 0.03125 0.828125 +vt 0 0.96875 +vt 0.03125 0.96875 +vt 0.03125 0.828125 +vt 0 0.828125 +vt 0.09375 0.96875 +vt 0.125 0.96875 +vt 0.125 0.828125 +vt 0.09375 0.828125 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vt 0.09375 0.828125 +vt 0.0625 0.828125 +vt 0.0625 0.96875 +vt 0.03125 0.96875 +vt 0.03125 1 +vt 0.0625 1 +vt 0.09375 1 +vt 0.0625 1 +vt 0.0625 0.96875 +vt 0.09375 0.96875 +vn 1.5700924586837752e-16 -1 1.570092458683775e-16 +vn -0.7071067811865475 -7.850462293418875e-17 0.7071067811865476 +vn -1.5700924586837752e-16 1 -1.570092458683775e-16 +vn 0.7071067811865475 7.850462293418875e-17 -0.7071067811865476 +vn 0.7071067811865476 7.850462293418876e-17 0.7071067811865475 +vn -0.7071067811865476 -7.850462293418876e-17 -0.7071067811865475 +usemtl m_18bfdc93-cc40-fa86-b211-05ac08cf8255 +f 172/508/127 175/507/127 173/506/127 170/505/127 +f 171/512/128 172/511/128 170/510/128 169/509/128 +f 176/516/129 171/515/129 169/514/129 174/513/129 +f 175/520/130 176/519/130 174/518/130 173/517/130 +f 174/524/131 169/523/131 170/522/131 173/521/131 +f 175/528/132 172/527/132 171/526/132 176/525/132 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/horse.obj b/renderer/viewer/three/entity/models/horse.obj new file mode 100644 index 00000000..bb08e913 --- /dev/null +++ b/renderer/viewer/three/entity/models/horse.obj @@ -0,0 +1,1061 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o Body +v 0.3125 1.3125 0.6875 +v 0.3125 1.3125 -0.6875 +v 0.3125 0.6875 0.6875 +v 0.3125 0.6875 -0.6875 +v -0.3125 1.3125 -0.6875 +v -0.3125 1.3125 0.6875 +v -0.3125 0.6875 -0.6875 +v -0.3125 0.6875 0.6875 +vt 0.34375 0.15625 +vt 0.5 0.15625 +vt 0.5 0 +vt 0.34375 0 +vt 0 0.15625 +vt 0.34375 0.15625 +vt 0.34375 0 +vt 0 0 +vt 0.84375 0.15625 +vt 1 0.15625 +vt 1 0 +vt 0.84375 0 +vt 0.5 0.15625 +vt 0.84375 0.15625 +vt 0.84375 0 +vt 0.5 0 +vt 0.5 0.15625 +vt 0.34375 0.15625 +vt 0.34375 0.5 +vt 0.5 0.5 +vt 0.65625 0.5 +vt 0.5 0.5 +vt 0.5 0.15625 +vt 0.65625 0.15625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o TailA +v 0.09375 1.3125 0.7957531754730547 +v 0.09375 1.1875 0.5792468245269451 +v 0.09375 0.5547277716886161 1.2332531754730547 +v 0.09375 0.4297277716886161 1.0167468245269449 +v -0.09375 1.1875 0.5792468245269451 +v -0.09375 1.3125 0.7957531754730547 +v -0.09375 0.4297277716886161 1.0167468245269449 +v -0.09375 0.5547277716886161 1.2332531754730547 +vt 0.71875 0.375 +vt 0.765625 0.375 +vt 0.765625 0.15625 +vt 0.71875 0.15625 +vt 0.65625 0.375 +vt 0.71875 0.375 +vt 0.71875 0.15625 +vt 0.65625 0.15625 +vt 0.828125 0.375 +vt 0.875 0.375 +vt 0.875 0.15625 +vt 0.828125 0.15625 +vt 0.765625 0.375 +vt 0.828125 0.375 +vt 0.828125 0.15625 +vt 0.765625 0.15625 +vt 0.765625 0.375 +vt 0.71875 0.375 +vt 0.71875 0.4375 +vt 0.765625 0.4375 +vt 0.8125 0.4375 +vt 0.765625 0.4375 +vt 0.765625 0.375 +vt 0.8125 0.375 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o Leg1A +v -0.0625 0.6875 0.6875 +v -0.0625 0.6875 0.4375 +v -0.0625 0 0.6875 +v -0.0625 0 0.4375 +v -0.3125 0.6875 0.4375 +v -0.3125 0.6875 0.6875 +v -0.3125 0 0.4375 +v -0.3125 0 0.6875 +vt 0.875 0.609375 +vt 0.8125 0.609375 +vt 0.8125 0.4375 +vt 0.875 0.4375 +vt 0.9375 0.609375 +vt 0.875 0.609375 +vt 0.875 0.4375 +vt 0.9375 0.4375 +vt 1 0.609375 +vt 0.9375 0.609375 +vt 0.9375 0.4375 +vt 1 0.4375 +vt 0.8125 0.609375 +vt 0.75 0.609375 +vt 0.75 0.4375 +vt 0.8125 0.4375 +vt 0.8125 0.609375 +vt 0.875 0.609375 +vt 0.875 0.671875 +vt 0.8125 0.671875 +vt 0.875 0.671875 +vt 0.9375 0.671875 +vt 0.9375 0.609375 +vt 0.875 0.609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o Leg2A +v 0.3125 0.6875 0.6875 +v 0.3125 0.6875 0.4375 +v 0.3125 0 0.6875 +v 0.3125 0 0.4375 +v 0.0625 0.6875 0.4375 +v 0.0625 0.6875 0.6875 +v 0.0625 0 0.4375 +v 0.0625 0 0.6875 +vt 0.8125 0.609375 +vt 0.875 0.609375 +vt 0.875 0.4375 +vt 0.8125 0.4375 +vt 0.75 0.609375 +vt 0.8125 0.609375 +vt 0.8125 0.4375 +vt 0.75 0.4375 +vt 0.9375 0.609375 +vt 1 0.609375 +vt 1 0.4375 +vt 0.9375 0.4375 +vt 0.875 0.609375 +vt 0.9375 0.609375 +vt 0.9375 0.4375 +vt 0.875 0.4375 +vt 0.875 0.609375 +vt 0.8125 0.609375 +vt 0.8125 0.671875 +vt 0.875 0.671875 +vt 0.9375 0.671875 +vt 0.875 0.671875 +vt 0.875 0.609375 +vt 0.9375 0.609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o Leg3A +v -0.0625 0.6875 -0.4375 +v -0.0625 0.6875 -0.6875 +v -0.0625 0 -0.4375 +v -0.0625 0 -0.6875 +v -0.3125 0.6875 -0.6875 +v -0.3125 0.6875 -0.4375 +v -0.3125 0 -0.6875 +v -0.3125 0 -0.4375 +vt 0.875 0.609375 +vt 0.8125 0.609375 +vt 0.8125 0.4375 +vt 0.875 0.4375 +vt 0.9375 0.609375 +vt 0.875 0.609375 +vt 0.875 0.4375 +vt 0.9375 0.4375 +vt 1 0.609375 +vt 0.9375 0.609375 +vt 0.9375 0.4375 +vt 1 0.4375 +vt 0.8125 0.609375 +vt 0.75 0.609375 +vt 0.75 0.4375 +vt 0.8125 0.4375 +vt 0.8125 0.609375 +vt 0.875 0.609375 +vt 0.875 0.671875 +vt 0.8125 0.671875 +vt 0.875 0.671875 +vt 0.9375 0.671875 +vt 0.9375 0.609375 +vt 0.875 0.609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o Leg4A +v 0.3125 0.6875 -0.4375 +v 0.3125 0.6875 -0.6875 +v 0.3125 0 -0.4375 +v 0.3125 0 -0.6875 +v 0.0625 0.6875 -0.6875 +v 0.0625 0.6875 -0.4375 +v 0.0625 0 -0.6875 +v 0.0625 0 -0.4375 +vt 0.8125 0.609375 +vt 0.875 0.609375 +vt 0.875 0.4375 +vt 0.8125 0.4375 +vt 0.75 0.609375 +vt 0.8125 0.609375 +vt 0.8125 0.4375 +vt 0.75 0.4375 +vt 0.9375 0.609375 +vt 1 0.609375 +vt 1 0.4375 +vt 0.9375 0.4375 +vt 0.875 0.609375 +vt 0.9375 0.609375 +vt 0.9375 0.4375 +vt 0.875 0.4375 +vt 0.875 0.609375 +vt 0.8125 0.609375 +vt 0.8125 0.671875 +vt 0.875 0.671875 +vt 0.9375 0.671875 +vt 0.875 0.671875 +vt 0.875 0.609375 +vt 0.9375 0.609375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o Head +v 0.1875 2.051882938682637 -0.7896234122634724 +v 0.1875 1.833132938682637 -1.1685095264191645 +v 0.1875 1.78125 -0.6333734122634727 +v 0.1875 1.5625 -1.0122595264191645 +v -0.1875 1.833132938682637 -1.1685095264191645 +v -0.1875 2.051882938682637 -0.7896234122634724 +v -0.1875 1.5625 -1.0122595264191645 +v -0.1875 1.78125 -0.6333734122634727 +vt 0.109375 0.6875 +vt 0.203125 0.6875 +vt 0.203125 0.609375 +vt 0.109375 0.609375 +vt 0 0.6875 +vt 0.109375 0.6875 +vt 0.109375 0.609375 +vt 0 0.609375 +vt 0.3125 0.6875 +vt 0.40625 0.6875 +vt 0.40625 0.609375 +vt 0.3125 0.609375 +vt 0.203125 0.6875 +vt 0.3125 0.6875 +vt 0.3125 0.609375 +vt 0.203125 0.609375 +vt 0.203125 0.6875 +vt 0.109375 0.6875 +vt 0.109375 0.796875 +vt 0.203125 0.796875 +vt 0.296875 0.796875 +vt 0.203125 0.796875 +vt 0.203125 0.6875 +vt 0.296875 0.6875 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o UMouth +v 0.125 1.833132938682637 -1.1685095264191645 +v 0.125 1.676882938682637 -1.4391424651018014 +v 0.125 1.5625 -1.0122595264191645 +v 0.125 1.40625 -1.2828924651018019 +v -0.125 1.676882938682637 -1.4391424651018014 +v -0.125 1.833132938682637 -1.1685095264191645 +v -0.125 1.40625 -1.2828924651018019 +v -0.125 1.5625 -1.0122595264191645 +vt 0.078125 0.53125 +vt 0.140625 0.53125 +vt 0.140625 0.453125 +vt 0.078125 0.453125 +vt 0 0.53125 +vt 0.078125 0.53125 +vt 0.078125 0.453125 +vt 0 0.453125 +vt 0.21875 0.53125 +vt 0.28125 0.53125 +vt 0.28125 0.453125 +vt 0.21875 0.453125 +vt 0.140625 0.53125 +vt 0.21875 0.53125 +vt 0.21875 0.453125 +vt 0.140625 0.453125 +vt 0.140625 0.53125 +vt 0.078125 0.53125 +vt 0.078125 0.609375 +vt 0.140625 0.609375 +vt 0.203125 0.609375 +vt 0.140625 0.609375 +vt 0.140625 0.53125 +vt 0.203125 0.53125 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o Ear1 +v -0.06465012057680164 2.1600077984998522 -0.8465349149312555 +v -0.06192650361593727 2.1288767141844853 -0.9006615026677829 +v -0.050497791707081574 1.9982459393130734 -0.7527849149312555 +v -0.04777417474621726 1.9671148549977064 -0.8069115026677829 +v -0.1864508408774055 2.117982246341028 -0.9006615026677829 +v -0.1891744578382698 2.149113330656395 -0.8465349149312555 +v -0.17229851200768548 1.9562203871542492 -0.8069115026677829 +v -0.1750221289685498 1.987351471469616 -0.7527849149312555 +vt 0.34375 0.734375 +vt 0.3125 0.734375 +vt 0.3125 0.6875 +vt 0.34375 0.6875 +vt 0.359375 0.734375 +vt 0.34375 0.734375 +vt 0.34375 0.6875 +vt 0.359375 0.6875 +vt 0.390625 0.734375 +vt 0.359375 0.734375 +vt 0.359375 0.6875 +vt 0.390625 0.6875 +vt 0.3125 0.734375 +vt 0.296875 0.734375 +vt 0.296875 0.6875 +vt 0.3125 0.6875 +vt 0.3125 0.734375 +vt 0.34375 0.734375 +vt 0.34375 0.75 +vt 0.3125 0.75 +vt 0.34375 0.75 +vt 0.375 0.75 +vt 0.375 0.734375 +vt 0.34375 0.734375 +vn 0.0435778713738291 -0.4980973490458729 -0.8660254037844388 +vn 0.9961946980917455 0.0871557427476582 -1.3877787807814457e-17 +vn -0.0435778713738291 0.4980973490458729 0.8660254037844388 +vn -0.9961946980917455 -0.0871557427476582 1.3877787807814457e-17 +vn -0.07547908730517335 0.862729915662821 -0.5 +vn 0.07547908730517335 -0.862729915662821 0.5 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o Ear2 +v 0.1891744578382698 2.149113330656395 -0.8465349149312555 +v 0.1864508408774055 2.117982246341028 -0.9006615026677829 +v 0.17502212896854985 1.987351471469616 -0.7527849149312555 +v 0.17229851200768542 1.9562203871542492 -0.8069115026677829 +v 0.06192650361593732 2.1288767141844853 -0.9006615026677829 +v 0.06465012057680164 2.1600077984998522 -0.8465349149312555 +v 0.04777417474621726 1.9671148549977064 -0.8069115026677829 +v 0.050497791707081574 1.9982459393130734 -0.7527849149312555 +vt 0.3125 0.734375 +vt 0.34375 0.734375 +vt 0.34375 0.6875 +vt 0.3125 0.6875 +vt 0.296875 0.734375 +vt 0.3125 0.734375 +vt 0.3125 0.6875 +vt 0.296875 0.6875 +vt 0.359375 0.734375 +vt 0.390625 0.734375 +vt 0.390625 0.6875 +vt 0.359375 0.6875 +vt 0.34375 0.734375 +vt 0.359375 0.734375 +vt 0.359375 0.6875 +vt 0.34375 0.6875 +vt 0.34375 0.734375 +vt 0.3125 0.734375 +vt 0.3125 0.75 +vt 0.34375 0.75 +vt 0.375 0.75 +vt 0.34375 0.75 +vt 0.34375 0.734375 +vt 0.375 0.734375 +vn -0.0435778713738291 -0.4980973490458729 -0.8660254037844388 +vn 0.9961946980917455 -0.0871557427476582 1.3877787807814457e-17 +vn 0.0435778713738291 0.4980973490458729 0.8660254037844388 +vn -0.9961946980917455 0.0871557427476582 -1.3877787807814457e-17 +vn 0.07547908730517335 0.862729915662821 -0.5 +vn -0.07547908730517335 -0.862729915662821 0.5 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o MuleEarL +v -0.15935822581478865 2.3816773650616265 -0.9715349149312555 +v -0.15127013065533484 2.351492182990093 -1.0256615026677829 +v -0.061295283546407786 2.0157014821763357 -0.7527849149312555 +v -0.05320718838695404 1.9855163001048024 -0.8069115026677829 +v -0.27201085894146837 2.3191398023522782 -1.0256615026677829 +v -0.28009895410092217 2.3493249844238115 -0.9715349149312555 +v -0.1739479166730875 1.9531639194669874 -0.8069115026677829 +v -0.1820360118325413 1.9833491015385207 -0.7527849149312555 +vt 0.046875 0.796875 +vt 0.015625 0.796875 +vt 0.015625 0.6875 +vt 0.046875 0.6875 +vt 0.0625 0.796875 +vt 0.046875 0.796875 +vt 0.046875 0.6875 +vt 0.0625 0.6875 +vt 0.09375 0.796875 +vt 0.0625 0.796875 +vt 0.0625 0.6875 +vt 0.09375 0.6875 +vt 0.015625 0.796875 +vt 0 0.796875 +vt 0 0.6875 +vt 0.015625 0.6875 +vt 0.015625 0.796875 +vt 0.046875 0.796875 +vt 0.046875 0.8125 +vt 0.015625 0.8125 +vt 0.046875 0.8125 +vt 0.078125 0.8125 +vt 0.078125 0.796875 +vt 0.046875 0.796875 +vn 0.12940952255126037 -0.48296291314453416 -0.8660254037844387 +vn 0.9659258262890683 0.25881904510252074 1.3877787807814457e-17 +vn -0.12940952255126037 0.48296291314453416 0.8660254037844387 +vn -0.9659258262890683 -0.25881904510252074 -1.3877787807814457e-17 +vn -0.2241438680420134 0.8365163037378079 -0.5 +vn 0.2241438680420134 -0.8365163037378079 0.5 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o MuleEarR +v 0.2800989541009222 2.3493249844238115 -0.9715349149312555 +v 0.27201085894146837 2.3191398023522782 -1.0256615026677829 +v 0.18203601183254126 1.9833491015385207 -0.7527849149312555 +v 0.1739479166730875 1.9531639194669874 -0.8069115026677829 +v 0.1512701306553348 2.351492182990093 -1.0256615026677829 +v 0.15935822581478865 2.3816773650616265 -0.9715349149312555 +v 0.05320718838695404 1.9855163001048024 -0.8069115026677829 +v 0.061295283546407786 2.0157014821763357 -0.7527849149312555 +vt 0.015625 0.796875 +vt 0.046875 0.796875 +vt 0.046875 0.6875 +vt 0.015625 0.6875 +vt 0 0.796875 +vt 0.015625 0.796875 +vt 0.015625 0.6875 +vt 0 0.6875 +vt 0.0625 0.796875 +vt 0.09375 0.796875 +vt 0.09375 0.6875 +vt 0.0625 0.6875 +vt 0.046875 0.796875 +vt 0.0625 0.796875 +vt 0.0625 0.6875 +vt 0.046875 0.6875 +vt 0.046875 0.796875 +vt 0.015625 0.796875 +vt 0.015625 0.8125 +vt 0.046875 0.8125 +vt 0.078125 0.8125 +vt 0.046875 0.8125 +vt 0.046875 0.796875 +vt 0.078125 0.796875 +vn -0.12940952255126037 -0.48296291314453416 -0.8660254037844387 +vn 0.9659258262890683 -0.25881904510252074 -1.3877787807814457e-17 +vn 0.12940952255126037 0.48296291314453416 0.8660254037844387 +vn -0.9659258262890683 0.25881904510252074 1.3877787807814457e-17 +vn 0.2241438680420134 0.8365163037378079 -0.5 +vn -0.2241438680420134 -0.8365163037378079 0.5 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o Neck +v 0.125 1.7828924651018019 -0.6272436490538904 +v 0.125 1.5641424651018019 -1.0061297632095823 +v 0.125 1.1333734122634724 -0.2522436490538904 +v 0.125 0.9146234122634727 -0.6311297632095825 +v -0.125 1.5641424651018019 -1.0061297632095823 +v -0.125 1.7828924651018019 -0.6272436490538904 +v -0.125 0.9146234122634727 -0.6311297632095825 +v -0.125 1.1333734122634724 -0.2522436490538904 +vt 0.109375 0.34375 +vt 0.171875 0.34375 +vt 0.171875 0.15625 +vt 0.109375 0.15625 +vt 0 0.34375 +vt 0.109375 0.34375 +vt 0.109375 0.15625 +vt 0 0.15625 +vt 0.28125 0.34375 +vt 0.34375 0.34375 +vt 0.34375 0.15625 +vt 0.28125 0.15625 +vt 0.171875 0.34375 +vt 0.28125 0.34375 +vt 0.28125 0.15625 +vt 0.171875 0.15625 +vt 0.171875 0.34375 +vt 0.109375 0.34375 +vt 0.109375 0.453125 +vt 0.171875 0.453125 +vt 0.234375 0.453125 +vt 0.171875 0.453125 +vt 0.171875 0.34375 +vt 0.234375 0.34375 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o Mane +v 0.0625 2.1160254037844384 -0.6752404735808355 +v 0.0625 2.0535254037844384 -0.7834936490538902 +v 0.0625 1.25 -0.1752404735808355 +v 0.0625 1.1875 -0.2834936490538904 +v -0.0625 2.0535254037844384 -0.7834936490538902 +v -0.0625 2.1160254037844384 -0.6752404735808355 +v -0.0625 1.1875 -0.2834936490538904 +v -0.0625 1.25 -0.1752404735808355 +vt 0.90625 0.40625 +vt 0.9375 0.40625 +vt 0.9375 0.15625 +vt 0.90625 0.15625 +vt 0.875 0.40625 +vt 0.90625 0.40625 +vt 0.90625 0.15625 +vt 0.875 0.15625 +vt 0.96875 0.40625 +vt 1 0.40625 +vt 1 0.15625 +vt 0.96875 0.15625 +vt 0.9375 0.40625 +vt 0.96875 0.40625 +vt 0.96875 0.15625 +vt 0.9375 0.15625 +vt 0.9375 0.40625 +vt 0.90625 0.40625 +vt 0.90625 0.4375 +vt 0.9375 0.4375 +vt 0.96875 0.4375 +vt 0.9375 0.4375 +vt 0.9375 0.40625 +vt 0.96875 0.40625 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o Bag1 +v 0.5 1.3125 0.125 +v 0.3125000000000001 1.3125 0.12499999999999989 +v 0.5 0.8125 0.125 +v 0.3125000000000001 0.8125 0.12499999999999989 +v 0.3125 1.3125 0.625 +v 0.5 1.3125 0.625 +v 0.3125 0.8125 0.625 +v 0.5 0.8125 0.625 +vt 0.453125 0.625 +vt 0.578125 0.625 +vt 0.578125 0.5 +vt 0.453125 0.5 +vt 0.40625 0.625 +vt 0.453125 0.625 +vt 0.453125 0.5 +vt 0.40625 0.5 +vt 0.625 0.625 +vt 0.75 0.625 +vt 0.75 0.5 +vt 0.625 0.5 +vt 0.578125 0.625 +vt 0.625 0.625 +vt 0.625 0.5 +vt 0.578125 0.5 +vt 0.578125 0.625 +vt 0.453125 0.625 +vt 0.453125 0.671875 +vt 0.578125 0.671875 +vt 0.703125 0.671875 +vt 0.578125 0.671875 +vt 0.578125 0.625 +vt 0.703125 0.625 +vn -1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 -1 +vn 1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 +o Bag2 +v -0.5 1.3125 0.625 +v -0.3125 1.3125 0.625 +v -0.5 0.8125 0.625 +v -0.3125 0.8125 0.625 +v -0.3125000000000001 1.3125 0.12499999999999989 +v -0.5000000000000001 1.3125 0.125 +v -0.3125000000000001 0.8125 0.12499999999999989 +v -0.5000000000000001 0.8125 0.125 +vt 0.578125 0.625 +vt 0.453125 0.625 +vt 0.453125 0.5 +vt 0.578125 0.5 +vt 0.625 0.625 +vt 0.578125 0.625 +vt 0.578125 0.5 +vt 0.625 0.5 +vt 0.75 0.625 +vt 0.625 0.625 +vt 0.625 0.5 +vt 0.75 0.5 +vt 0.453125 0.625 +vt 0.40625 0.625 +vt 0.40625 0.5 +vt 0.453125 0.5 +vt 0.453125 0.625 +vt 0.578125 0.625 +vt 0.578125 0.671875 +vt 0.453125 0.671875 +vt 0.578125 0.671875 +vt 0.703125 0.671875 +vt 0.703125 0.625 +vt 0.578125 0.625 +vn 1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 1 +vn -1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 -1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 124/364/91 127/363/91 125/362/91 122/361/91 +f 123/368/92 124/367/92 122/366/92 121/365/92 +f 128/372/93 123/371/93 121/370/93 126/369/93 +f 127/376/94 128/375/94 126/374/94 125/373/94 +f 126/380/95 121/379/95 122/378/95 125/377/95 +f 127/384/96 124/383/96 123/382/96 128/381/96 +o Saddle +v 0.34375 1.34375 0.375 +v 0.34375 1.34375 -0.25 +v 0.34375 0.71875 0.375 +v 0.34375 0.71875 -0.25 +v -0.34375 1.34375 -0.25 +v -0.34375 1.34375 0.375 +v -0.34375 0.71875 -0.25 +v -0.34375 0.71875 0.375 +vt 0.546875 0.859375 +vt 0.703125 0.859375 +vt 0.703125 0.71875 +vt 0.546875 0.71875 +vt 0.40625 0.859375 +vt 0.546875 0.859375 +vt 0.546875 0.71875 +vt 0.40625 0.71875 +vt 0.84375 0.859375 +vt 1 0.859375 +vt 1 0.71875 +vt 0.84375 0.71875 +vt 0.703125 0.859375 +vt 0.84375 0.859375 +vt 0.84375 0.71875 +vt 0.703125 0.71875 +vt 0.703125 0.859375 +vt 0.546875 0.859375 +vt 0.546875 1 +vt 0.703125 1 +vt 0.859375 1 +vt 0.703125 1 +vt 0.703125 0.859375 +vt 0.859375 0.859375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 132/388/97 135/387/97 133/386/97 130/385/97 +f 131/392/98 132/391/98 130/390/98 129/389/98 +f 136/396/99 131/395/99 129/394/99 134/393/99 +f 135/400/100 136/399/100 134/398/100 133/397/100 +f 134/404/101 129/403/101 130/402/101 133/401/101 +f 135/408/102 132/407/102 131/406/102 136/405/102 +o SaddleMouthL +v -0.125 1.6952722283113841 -1.1540063509461098 +v -0.125 1.6327722283113841 -1.2622595264191645 +v -0.125 1.587019052838329 -1.0915063509461098 +v -0.125 1.524519052838329 -1.1997595264191645 +v -0.1875 1.6327722283113841 -1.2622595264191645 +v -0.1875 1.6952722283113841 -1.1540063509461098 +v -0.1875 1.524519052838329 -1.1997595264191645 +v -0.1875 1.587019052838329 -1.0915063509461098 +vt 0.484375 0.890625 +vt 0.5 0.890625 +vt 0.5 0.859375 +vt 0.484375 0.859375 +vt 0.453125 0.890625 +vt 0.484375 0.890625 +vt 0.484375 0.859375 +vt 0.453125 0.859375 +vt 0.53125 0.890625 +vt 0.546875 0.890625 +vt 0.546875 0.859375 +vt 0.53125 0.859375 +vt 0.5 0.890625 +vt 0.53125 0.890625 +vt 0.53125 0.859375 +vt 0.5 0.859375 +vt 0.5 0.890625 +vt 0.484375 0.890625 +vt 0.484375 0.921875 +vt 0.5 0.921875 +vt 0.515625 0.921875 +vt 0.5 0.921875 +vt 0.5 0.890625 +vt 0.515625 0.890625 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 140/412/103 143/411/103 141/410/103 138/409/103 +f 139/416/104 140/415/104 138/414/104 137/413/104 +f 144/420/105 139/419/105 137/418/105 142/417/105 +f 143/424/106 144/423/106 142/422/106 141/421/106 +f 142/428/107 137/427/107 138/426/107 141/425/107 +f 143/432/108 140/431/108 139/430/108 144/429/108 +o SaddleMouthR +v 0.1875 1.6952722283113841 -1.1540063509461098 +v 0.1875 1.6327722283113841 -1.2622595264191645 +v 0.1875 1.587019052838329 -1.0915063509461098 +v 0.1875 1.524519052838329 -1.1997595264191645 +v 0.125 1.6327722283113841 -1.2622595264191645 +v 0.125 1.6952722283113841 -1.1540063509461098 +v 0.125 1.524519052838329 -1.1997595264191645 +v 0.125 1.587019052838329 -1.0915063509461098 +vt 0.484375 0.890625 +vt 0.5 0.890625 +vt 0.5 0.859375 +vt 0.484375 0.859375 +vt 0.453125 0.890625 +vt 0.484375 0.890625 +vt 0.484375 0.859375 +vt 0.453125 0.859375 +vt 0.53125 0.890625 +vt 0.546875 0.890625 +vt 0.546875 0.859375 +vt 0.53125 0.859375 +vt 0.5 0.890625 +vt 0.53125 0.890625 +vt 0.53125 0.859375 +vt 0.5 0.859375 +vt 0.5 0.890625 +vt 0.484375 0.890625 +vt 0.484375 0.921875 +vt 0.5 0.921875 +vt 0.515625 0.921875 +vt 0.5 0.921875 +vt 0.5 0.890625 +vt 0.515625 0.890625 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 148/436/109 151/435/109 149/434/109 146/433/109 +f 147/440/110 148/439/110 146/438/110 145/437/110 +f 152/444/111 147/443/111 145/442/111 150/441/111 +f 151/448/112 152/447/112 150/446/112 149/445/112 +f 150/452/113 145/451/113 146/450/113 149/449/113 +f 151/456/114 148/455/114 147/454/114 152/453/114 +o SaddleMouthLine +v -0.19374999999999998 1.6875 -0.21875 +v -0.19374999999999998 1.6875 -1.21875 +v -0.19374999999999998 1.5 -0.21875 +v -0.19374999999999998 1.5 -1.21875 +v -0.19374999999999998 1.6875 -1.21875 +v -0.19374999999999998 1.6875 -0.21875 +v -0.19374999999999998 1.5 -1.21875 +v -0.19374999999999998 1.5 -0.21875 +vt 0.75 0.71875 +vt 0.75 0.71875 +vt 0.75 0.671875 +vt 0.75 0.671875 +vt 0.5 0.71875 +vt 0.75 0.71875 +vt 0.75 0.671875 +vt 0.5 0.671875 +vt 1 0.71875 +vt 1 0.71875 +vt 1 0.671875 +vt 1 0.671875 +vt 0.75 0.71875 +vt 1 0.71875 +vt 1 0.671875 +vt 0.75 0.671875 +vt 0.75 0.71875 +vt 0.75 0.71875 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.71875 +vt 0.75 0.71875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 156/460/115 159/459/115 157/458/115 154/457/115 +f 155/464/116 156/463/116 154/462/116 153/461/116 +f 160/468/117 155/467/117 153/466/117 158/465/117 +f 159/472/118 160/471/118 158/470/118 157/469/118 +f 158/476/119 153/475/119 154/474/119 157/473/119 +f 159/480/120 156/479/120 155/478/120 160/477/120 +o SaddleMouthLineR +v 0.19374999999999998 1.6875 -0.21875 +v 0.19374999999999998 1.6875 -1.21875 +v 0.19374999999999998 1.5 -0.21875 +v 0.19374999999999998 1.5 -1.21875 +v 0.19374999999999998 1.6875 -1.21875 +v 0.19374999999999998 1.6875 -0.21875 +v 0.19374999999999998 1.5 -1.21875 +v 0.19374999999999998 1.5 -0.21875 +vt 0.75 0.71875 +vt 0.75 0.71875 +vt 0.75 0.671875 +vt 0.75 0.671875 +vt 0.5 0.71875 +vt 0.75 0.71875 +vt 0.75 0.671875 +vt 0.5 0.671875 +vt 1 0.71875 +vt 1 0.71875 +vt 1 0.671875 +vt 1 0.671875 +vt 0.75 0.71875 +vt 1 0.71875 +vt 1 0.671875 +vt 0.75 0.671875 +vt 0.75 0.71875 +vt 0.75 0.71875 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.96875 +vt 0.75 0.71875 +vt 0.75 0.71875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 164/484/121 167/483/121 165/482/121 162/481/121 +f 163/488/122 164/487/122 162/486/122 161/485/122 +f 168/492/123 163/491/123 161/490/123 166/489/123 +f 167/496/124 168/495/124 166/494/124 165/493/124 +f 166/500/125 161/499/125 162/498/125 165/497/125 +f 167/504/126 164/503/126 163/502/126 168/501/126 +o HeadSaddle +v 0.140625 1.8561195507185708 -1.1566606162754502 +v 0.140625 1.7779945507185708 -1.291977085616769 +v 0.140625 1.5584233181676694 -0.9847856162754505 +v 0.140625 1.4802983181676697 -1.120102085616769 +v -0.140625 1.7779945507185708 -1.291977085616769 +v -0.140625 1.8561195507185708 -1.1566606162754502 +v -0.140625 1.4802983181676697 -1.120102085616769 +v -0.140625 1.5584233181676694 -0.9847856162754505 +vt 0.328125 0.96875 +vt 0.390625 0.96875 +vt 0.390625 0.890625 +vt 0.328125 0.890625 +vt 0.296875 0.96875 +vt 0.328125 0.96875 +vt 0.328125 0.890625 +vt 0.296875 0.890625 +vt 0.421875 0.96875 +vt 0.484375 0.96875 +vt 0.484375 0.890625 +vt 0.421875 0.890625 +vt 0.390625 0.96875 +vt 0.421875 0.96875 +vt 0.421875 0.890625 +vt 0.390625 0.890625 +vt 0.390625 0.96875 +vt 0.328125 0.96875 +vt 0.328125 1 +vt 0.390625 1 +vt 0.453125 1 +vt 0.390625 1 +vt 0.390625 0.96875 +vt 0.453125 0.96875 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 172/508/127 175/507/127 173/506/127 170/505/127 +f 171/512/128 172/511/128 170/510/128 169/509/128 +f 176/516/129 171/515/129 169/514/129 174/513/129 +f 175/520/130 176/519/130 174/518/130 173/517/130 +f 174/524/131 169/523/131 170/522/131 173/521/131 +f 175/528/132 172/527/132 171/526/132 176/525/132 +o HeadSaddle +v 0.203125 2.074869550718571 -0.7777745021197584 +v 0.203125 1.8404945507185708 -1.183723910143714 +v 0.203125 1.7771733181676694 -0.6058995021197586 +v 0.203125 1.5427983181676694 -1.0118489101437143 +v -0.203125 1.8404945507185708 -1.183723910143714 +v -0.203125 2.074869550718571 -0.7777745021197584 +v -0.203125 1.5427983181676694 -1.0118489101437143 +v -0.203125 1.7771733181676694 -0.6058995021197586 +vt 0.109375 0.890625 +vt 0.203125 0.890625 +vt 0.203125 0.8125 +vt 0.109375 0.8125 +vt 0 0.890625 +vt 0.109375 0.890625 +vt 0.109375 0.8125 +vt 0 0.8125 +vt 0.3125 0.890625 +vt 0.40625 0.890625 +vt 0.40625 0.8125 +vt 0.3125 0.8125 +vt 0.203125 0.890625 +vt 0.3125 0.890625 +vt 0.3125 0.8125 +vt 0.203125 0.8125 +vt 0.203125 0.890625 +vt 0.109375 0.890625 +vt 0.109375 1 +vt 0.203125 1 +vt 0.296875 1 +vt 0.203125 1 +vt 0.203125 0.890625 +vt 0.296875 0.890625 +vn 0 -0.49999999999999994 -0.8660254037844387 +vn 1 0 0 +vn 0 0.49999999999999994 0.8660254037844387 +vn -1 0 0 +vn 0 0.8660254037844387 -0.49999999999999994 +vn 0 -0.8660254037844387 0.49999999999999994 +usemtl m_1e01f934-e3cf-9457-589b-e2903555a409 +f 180/532/133 183/531/133 181/530/133 178/529/133 +f 179/536/134 180/535/134 178/534/134 177/533/134 +f 184/540/135 179/539/135 177/538/135 182/537/135 +f 183/544/136 184/543/136 182/542/136 181/541/136 +f 182/548/137 177/547/137 178/546/137 181/545/137 +f 183/552/138 180/551/138 179/550/138 184/549/138 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/llama.obj b/renderer/viewer/three/entity/models/llama.obj new file mode 100644 index 00000000..1ef48ac5 --- /dev/null +++ b/renderer/viewer/three/entity/models/llama.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o head +v 0.125 1.9375 -0.4375 +v 0.125 1.9375 -1 +v 0.125 1.6875 -0.4375 +v 0.125 1.6875 -1 +v -0.125 1.9375 -1 +v -0.125 1.9375 -0.4375 +v -0.125 1.6875 -1 +v -0.125 1.6875 -0.4375 +vt 0.0703125 0.859375 +vt 0.1015625 0.859375 +vt 0.1015625 0.796875 +vt 0.0703125 0.796875 +vt 0 0.859375 +vt 0.0703125 0.859375 +vt 0.0703125 0.796875 +vt 0 0.796875 +vt 0.171875 0.859375 +vt 0.203125 0.859375 +vt 0.203125 0.796875 +vt 0.171875 0.796875 +vt 0.1015625 0.859375 +vt 0.171875 0.859375 +vt 0.171875 0.796875 +vt 0.1015625 0.796875 +vt 0.1015625 0.859375 +vt 0.0703125 0.859375 +vt 0.0703125 1 +vt 0.1015625 1 +vt 0.1328125 1 +vt 0.1015625 1 +vt 0.1015625 0.859375 +vt 0.1328125 0.859375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.25 2.0625 -0.375 +v 0.25 2.0625 -0.75 +v 0.25 0.9375 -0.375 +v 0.25 0.9375 -0.75 +v -0.25 2.0625 -0.75 +v -0.25 2.0625 -0.375 +v -0.25 0.9375 -0.75 +v -0.25 0.9375 -0.375 +vt 0.046875 0.6875 +vt 0.109375 0.6875 +vt 0.109375 0.40625 +vt 0.046875 0.40625 +vt 0 0.6875 +vt 0.046875 0.6875 +vt 0.046875 0.40625 +vt 0 0.40625 +vt 0.15625 0.6875 +vt 0.21875 0.6875 +vt 0.21875 0.40625 +vt 0.15625 0.40625 +vt 0.109375 0.6875 +vt 0.15625 0.6875 +vt 0.15625 0.40625 +vt 0.109375 0.40625 +vt 0.109375 0.6875 +vt 0.046875 0.6875 +vt 0.046875 0.78125 +vt 0.109375 0.78125 +vt 0.171875 0.78125 +vt 0.109375 0.78125 +vt 0.109375 0.6875 +vt 0.171875 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.25 2.25 -0.5 +v 0.25 2.25 -0.625 +v 0.25 2.0625 -0.5 +v 0.25 2.0625 -0.625 +v 0.0625 2.25 -0.625 +v 0.0625 2.25 -0.5 +v 0.0625 2.0625 -0.625 +v 0.0625 2.0625 -0.5 +vt 0.1484375 0.96875 +vt 0.171875 0.96875 +vt 0.171875 0.921875 +vt 0.1484375 0.921875 +vt 0.1328125 0.96875 +vt 0.1484375 0.96875 +vt 0.1484375 0.921875 +vt 0.1328125 0.921875 +vt 0.1875 0.96875 +vt 0.2109375 0.96875 +vt 0.2109375 0.921875 +vt 0.1875 0.921875 +vt 0.171875 0.96875 +vt 0.1875 0.96875 +vt 0.1875 0.921875 +vt 0.171875 0.921875 +vt 0.171875 0.96875 +vt 0.1484375 0.96875 +vt 0.1484375 1 +vt 0.171875 1 +vt 0.1953125 1 +vt 0.171875 1 +vt 0.171875 0.96875 +vt 0.1953125 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v -0.0625 2.25 -0.5 +v -0.0625 2.25 -0.625 +v -0.0625 2.0625 -0.5 +v -0.0625 2.0625 -0.625 +v -0.25 2.25 -0.625 +v -0.25 2.25 -0.5 +v -0.25 2.0625 -0.625 +v -0.25 2.0625 -0.5 +vt 0.1484375 0.96875 +vt 0.171875 0.96875 +vt 0.171875 0.921875 +vt 0.1484375 0.921875 +vt 0.1328125 0.96875 +vt 0.1484375 0.96875 +vt 0.1484375 0.921875 +vt 0.1328125 0.921875 +vt 0.1875 0.96875 +vt 0.2109375 0.96875 +vt 0.2109375 0.921875 +vt 0.1875 0.921875 +vt 0.171875 0.96875 +vt 0.1875 0.96875 +vt 0.1875 0.921875 +vt 0.171875 0.921875 +vt 0.171875 0.96875 +vt 0.1484375 0.96875 +vt 0.1484375 1 +vt 0.171875 1 +vt 0.1953125 1 +vt 0.171875 1 +vt 0.171875 0.96875 +vt 0.1953125 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o chest1 +v 0.34375 1.3125 0.3750000000000001 +v 0.53125 1.3125 0.375 +v 0.34375 0.8125 0.3750000000000001 +v 0.53125 0.8125 0.375 +v 0.53125 1.3125 -0.12499999999999994 +v 0.34375 1.3125 -0.12499999999999989 +v 0.53125 0.8125 -0.12499999999999994 +v 0.34375 0.8125 -0.12499999999999989 +vt 0.375 0.515625 +vt 0.4375 0.515625 +vt 0.4375 0.390625 +vt 0.375 0.390625 +vt 0.3515625 0.515625 +vt 0.375 0.515625 +vt 0.375 0.390625 +vt 0.3515625 0.390625 +vt 0.4609375 0.515625 +vt 0.5234375 0.515625 +vt 0.5234375 0.390625 +vt 0.4609375 0.390625 +vt 0.4375 0.515625 +vt 0.4609375 0.515625 +vt 0.4609375 0.390625 +vt 0.4375 0.390625 +vt 0.4375 0.515625 +vt 0.375 0.515625 +vt 0.375 0.5625 +vt 0.4375 0.5625 +vt 0.5 0.5625 +vt 0.4375 0.5625 +vt 0.4375 0.515625 +vt 0.5 0.515625 +vn 1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 1 +vn -1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 -1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o chest2 +v -0.53125 1.3125 0.3750000000000001 +v -0.34374999999999994 1.3125 0.375 +v -0.53125 0.8125 0.3750000000000001 +v -0.34374999999999994 0.8125 0.375 +v -0.34375000000000006 1.3125 -0.125 +v -0.5312500000000001 1.3125 -0.12499999999999989 +v -0.34375000000000006 0.8125 -0.125 +v -0.5312500000000001 0.8125 -0.12499999999999989 +vt 0.375 0.3125 +vt 0.4375 0.3125 +vt 0.4375 0.1875 +vt 0.375 0.1875 +vt 0.3515625 0.3125 +vt 0.375 0.3125 +vt 0.375 0.1875 +vt 0.3515625 0.1875 +vt 0.4609375 0.3125 +vt 0.5234375 0.3125 +vt 0.5234375 0.1875 +vt 0.4609375 0.1875 +vt 0.4375 0.3125 +vt 0.4609375 0.3125 +vt 0.4609375 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0.3125 +vt 0.375 0.3125 +vt 0.375 0.359375 +vt 0.4375 0.359375 +vt 0.5 0.359375 +vt 0.4375 0.359375 +vt 0.4375 0.3125 +vt 0.5 0.3125 +vn 1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 1 +vn -1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 -1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o body +v 0.375 1.3750000000000002 -0.5 +v 0.375 0.7500000000000002 -0.5 +v 0.375 1.375 0.625 +v 0.375 0.75 0.625 +v -0.375 0.7500000000000002 -0.5 +v -0.375 1.3750000000000002 -0.5 +v -0.375 0.75 0.625 +v -0.375 1.375 0.625 +vt 0.3046875 0.84375 +vt 0.3984375 0.84375 +vt 0.3984375 0.5625 +vt 0.3046875 0.5625 +vt 0.2265625 0.84375 +vt 0.3046875 0.84375 +vt 0.3046875 0.5625 +vt 0.2265625 0.5625 +vt 0.4765625 0.84375 +vt 0.5703125 0.84375 +vt 0.5703125 0.5625 +vt 0.4765625 0.5625 +vt 0.3984375 0.84375 +vt 0.4765625 0.84375 +vt 0.4765625 0.5625 +vt 0.3984375 0.5625 +vt 0.3984375 0.84375 +vt 0.3046875 0.84375 +vt 0.3046875 1 +vt 0.3984375 1 +vt 0.4921875 1 +vt 0.3984375 1 +vt 0.3984375 0.84375 +vt 0.4921875 0.84375 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o leg0 +v 0.34375 0.875 0.5 +v 0.34375 0.875 0.25 +v 0.34375 0 0.5 +v 0.34375 0 0.25 +v 0.09375 0.875 0.25 +v 0.09375 0.875 0.5 +v 0.09375 0 0.25 +v 0.09375 0 0.5 +vt 0.2578125 0.484375 +vt 0.2890625 0.484375 +vt 0.2890625 0.265625 +vt 0.2578125 0.265625 +vt 0.2265625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.265625 +vt 0.2265625 0.265625 +vt 0.3203125 0.484375 +vt 0.3515625 0.484375 +vt 0.3515625 0.265625 +vt 0.3203125 0.265625 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vt 0.3203125 0.265625 +vt 0.2890625 0.265625 +vt 0.2890625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.546875 +vt 0.2890625 0.546875 +vt 0.3203125 0.546875 +vt 0.2890625 0.546875 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o leg1 +v -0.09375 0.875 0.5 +v -0.09375 0.875 0.25 +v -0.09375 0 0.5 +v -0.09375 0 0.25 +v -0.34375 0.875 0.25 +v -0.34375 0.875 0.5 +v -0.34375 0 0.25 +v -0.34375 0 0.5 +vt 0.2578125 0.484375 +vt 0.2890625 0.484375 +vt 0.2890625 0.265625 +vt 0.2578125 0.265625 +vt 0.2265625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.265625 +vt 0.2265625 0.265625 +vt 0.3203125 0.484375 +vt 0.3515625 0.484375 +vt 0.3515625 0.265625 +vt 0.3203125 0.265625 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vt 0.3203125 0.265625 +vt 0.2890625 0.265625 +vt 0.2890625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.546875 +vt 0.2890625 0.546875 +vt 0.3203125 0.546875 +vt 0.2890625 0.546875 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o leg2 +v 0.34375 0.875 -0.1875 +v 0.34375 0.875 -0.4375 +v 0.34375 0 -0.1875 +v 0.34375 0 -0.4375 +v 0.09375 0.875 -0.4375 +v 0.09375 0.875 -0.1875 +v 0.09375 0 -0.4375 +v 0.09375 0 -0.1875 +vt 0.2578125 0.484375 +vt 0.2890625 0.484375 +vt 0.2890625 0.265625 +vt 0.2578125 0.265625 +vt 0.2265625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.265625 +vt 0.2265625 0.265625 +vt 0.3203125 0.484375 +vt 0.3515625 0.484375 +vt 0.3515625 0.265625 +vt 0.3203125 0.265625 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vt 0.3203125 0.265625 +vt 0.2890625 0.265625 +vt 0.2890625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.546875 +vt 0.2890625 0.546875 +vt 0.3203125 0.546875 +vt 0.2890625 0.546875 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o leg3 +v -0.09375 0.875 -0.1875 +v -0.09375 0.875 -0.4375 +v -0.09375 0 -0.1875 +v -0.09375 0 -0.4375 +v -0.34375 0.875 -0.4375 +v -0.34375 0.875 -0.1875 +v -0.34375 0 -0.4375 +v -0.34375 0 -0.1875 +vt 0.2578125 0.484375 +vt 0.2890625 0.484375 +vt 0.2890625 0.265625 +vt 0.2578125 0.265625 +vt 0.2265625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.265625 +vt 0.2265625 0.265625 +vt 0.3203125 0.484375 +vt 0.3515625 0.484375 +vt 0.3515625 0.265625 +vt 0.3203125 0.265625 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vt 0.3203125 0.265625 +vt 0.2890625 0.265625 +vt 0.2890625 0.484375 +vt 0.2578125 0.484375 +vt 0.2578125 0.546875 +vt 0.2890625 0.546875 +vt 0.3203125 0.546875 +vt 0.2890625 0.546875 +vt 0.2890625 0.484375 +vt 0.3203125 0.484375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c2c1ddb5-5693-7a64-a950-f7fec476ea98 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/minecart.obj b/renderer/viewer/three/entity/models/minecart.obj new file mode 100644 index 00000000..0dd2f4c8 --- /dev/null +++ b/renderer/viewer/three/entity/models/minecart.obj @@ -0,0 +1,233 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o bottom +v 0.625 0.12500000000000022 -0.5000000000000002 +v 0.625 2.220446049250313e-16 -0.5000000000000002 +v 0.625 0.125 0.4999999999999998 +v 0.625 0 0.4999999999999998 +v -0.625 2.220446049250313e-16 -0.5000000000000002 +v -0.625 0.12500000000000022 -0.5000000000000002 +v -0.625 0 0.4999999999999998 +v -0.625 0.125 0.4999999999999998 +vt 0.34375 0.625 +vt 0.03125 0.625 +vt 0.03125 0.125 +vt 0.34375 0.125 +vt 0.375 0.625 +vt 0.34375 0.625 +vt 0.34375 0.125 +vt 0.375 0.125 +vt 0.6875 0.625 +vt 0.375 0.625 +vt 0.375 0.125 +vt 0.6875 0.125 +vt 0.03125 0.625 +vt 0 0.625 +vt 0 0.125 +vt 0.03125 0.125 +vt 0.03125 0.625 +vt 0.34375 0.625 +vt 0.34375 0.6875 +vt 0.03125 0.6875 +vt 0.34375 0.6875 +vt 0.65625 0.6875 +vt 0.65625 0.625 +vt 0.34375 0.625 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_51fd1571-03b7-0fcd-9442-72fc6c7ce0a3 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o front +v 0.625 0.625 -0.5 +v 0.5 0.625 -0.5 +v 0.625 0.125 -0.5 +v 0.5 0.125 -0.5 +v 0.4999999999999999 0.625 0.5 +v 0.625 0.625 0.5 +v 0.4999999999999999 0.125 0.5 +v 0.625 0.125 0.5 +vt 0.28125 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.6875 +vt 0.28125 0.6875 +vt 0.3125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 0.6875 +vt 0.3125 0.6875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.6875 +vt 0.5625 0.6875 +vt 0.03125 0.9375 +vt 0 0.9375 +vt 0 0.6875 +vt 0.03125 0.6875 +vt 0.03125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 1 +vt 0.03125 1 +vt 0.28125 1 +vt 0.53125 1 +vt 0.53125 0.9375 +vt 0.28125 0.9375 +vn -1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 -1 +vn 1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_51fd1571-03b7-0fcd-9442-72fc6c7ce0a3 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o back +v -0.6249999999999999 0.625 0.5 +v -0.4999999999999999 0.625 0.5 +v -0.6249999999999999 0.125 0.5 +v -0.4999999999999999 0.125 0.5 +v -0.5000000000000001 0.625 -0.5 +v -0.6250000000000001 0.625 -0.5 +v -0.5000000000000001 0.125 -0.5 +v -0.6250000000000001 0.125 -0.5 +vt 0.28125 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.6875 +vt 0.28125 0.6875 +vt 0.3125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 0.6875 +vt 0.3125 0.6875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.6875 +vt 0.5625 0.6875 +vt 0.03125 0.9375 +vt 0 0.9375 +vt 0 0.6875 +vt 0.03125 0.6875 +vt 0.03125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 1 +vt 0.03125 1 +vt 0.28125 1 +vt 0.53125 1 +vt 0.53125 0.9375 +vt 0.28125 0.9375 +vn 1 0 -2.220446049250313e-16 +vn 2.220446049250313e-16 0 1 +vn -1 0 2.220446049250313e-16 +vn -2.220446049250313e-16 0 -1 +vn 0 1 0 +vn 0 -1 0 +usemtl m_51fd1571-03b7-0fcd-9442-72fc6c7ce0a3 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o right +v -0.49999999999999994 0.625 -0.5 +v -0.5000000000000001 0.625 -0.37500000000000006 +v -0.49999999999999994 0.125 -0.5 +v -0.5000000000000001 0.125 -0.37500000000000006 +v 0.5 0.625 -0.3749999999999999 +v 0.5 0.625 -0.49999999999999994 +v 0.5 0.125 -0.3749999999999999 +v 0.5 0.125 -0.49999999999999994 +vt 0.28125 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.6875 +vt 0.28125 0.6875 +vt 0.3125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 0.6875 +vt 0.3125 0.6875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.6875 +vt 0.5625 0.6875 +vt 0.03125 0.9375 +vt 0 0.9375 +vt 0 0.6875 +vt 0.03125 0.6875 +vt 0.03125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 1 +vt 0.03125 1 +vt 0.28125 1 +vt 0.53125 1 +vt 0.53125 0.9375 +vt 0.28125 0.9375 +vn -1.2246467991473532e-16 0 1 +vn -1 0 -1.2246467991473532e-16 +vn 1.2246467991473532e-16 0 -1 +vn 1 0 1.2246467991473532e-16 +vn 0 1 0 +vn 0 -1 0 +usemtl m_51fd1571-03b7-0fcd-9442-72fc6c7ce0a3 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o left +v 0.5 0.625 0.5 +v 0.5 0.625 0.375 +v 0.5 0.125 0.5 +v 0.5 0.125 0.375 +v -0.5 0.625 0.375 +v -0.5 0.625 0.5 +v -0.5 0.125 0.375 +v -0.5 0.125 0.5 +vt 0.28125 0.9375 +vt 0.03125 0.9375 +vt 0.03125 0.6875 +vt 0.28125 0.6875 +vt 0.3125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 0.6875 +vt 0.3125 0.6875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.6875 +vt 0.5625 0.6875 +vt 0.03125 0.9375 +vt 0 0.9375 +vt 0 0.6875 +vt 0.03125 0.6875 +vt 0.03125 0.9375 +vt 0.28125 0.9375 +vt 0.28125 1 +vt 0.03125 1 +vt 0.28125 1 +vt 0.53125 1 +vt 0.53125 0.9375 +vt 0.28125 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_51fd1571-03b7-0fcd-9442-72fc6c7ce0a3 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/parrot.obj b/renderer/viewer/three/entity/models/parrot.obj new file mode 100644 index 00000000..35d14a5d --- /dev/null +++ b/renderer/viewer/three/entity/models/parrot.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.09375 0.5083704620381906 -0.10253364496531403 +v 0.09375 0.4291295379618094 -0.2724663550346859 +v 0.09375 0.16850504189944682 0.05594820318744831 +v 0.09375 0.08926411782306565 -0.11398450688192363 +v -0.09375 0.4291295379618094 -0.2724663550346859 +v -0.09375 0.5083704620381906 -0.10253364496531403 +v -0.09375 0.08926411782306565 -0.11398450688192363 +v -0.09375 0.16850504189944682 0.05594820318744831 +vt 0.15625 0.65625 +vt 0.25 0.65625 +vt 0.25 0.46875 +vt 0.15625 0.46875 +vt 0.0625 0.65625 +vt 0.15625 0.65625 +vt 0.15625 0.46875 +vt 0.0625 0.46875 +vt 0.34375 0.65625 +vt 0.4375 0.65625 +vt 0.4375 0.46875 +vt 0.34375 0.46875 +vt 0.25 0.65625 +vt 0.34375 0.65625 +vt 0.34375 0.46875 +vt 0.25 0.46875 +vt 0.25 0.65625 +vt 0.15625 0.65625 +vt 0.15625 0.75 +vt 0.25 0.75 +vt 0.34375 0.75 +vt 0.25 0.75 +vt 0.25 0.65625 +vt 0.34375 0.65625 +vn 0 -0.42261826174069944 -0.9063077870366499 +vn 1 0 0 +vn 0 0.42261826174069944 0.9063077870366499 +vn -1 0 0 +vn 0 0.9063077870366499 -0.42261826174069944 +vn 0 -0.9063077870366499 0.42261826174069944 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o wing0 +v -0.0625 0.5051478245037531 -0.08881019196643142 +v -0.0625 0.3976022426879319 -0.24240120027061735 +v -0.0625 0.24916281066344315 0.09043244439327047 +v -0.0625 0.14161722884762196 -0.06315856391091551 +v -0.125 0.3976022426879319 -0.24240120027061735 +v -0.125 0.5051478245037531 -0.08881019196643142 +v -0.125 0.14161722884762196 -0.06315856391091551 +v -0.125 0.24916281066344315 0.09043244439327047 +vt 0.6875 0.65625 +vt 0.71875 0.65625 +vt 0.71875 0.5 +vt 0.6875 0.5 +vt 0.59375 0.65625 +vt 0.6875 0.65625 +vt 0.6875 0.5 +vt 0.59375 0.5 +vt 0.8125 0.65625 +vt 0.84375 0.65625 +vt 0.84375 0.5 +vt 0.8125 0.5 +vt 0.71875 0.65625 +vt 0.8125 0.65625 +vt 0.8125 0.5 +vt 0.71875 0.5 +vt 0.71875 0.65625 +vt 0.6875 0.65625 +vt 0.6875 0.75 +vt 0.71875 0.75 +vt 0.75 0.75 +vt 0.71875 0.75 +vt 0.71875 0.65625 +vt 0.75 0.65625 +vn 0 -0.5735764363510462 -0.8191520442889919 +vn 1 0 0 +vn 0 0.5735764363510462 0.8191520442889919 +vn -1 0 0 +vn 0 0.8191520442889919 -0.5735764363510462 +vn 0 -0.8191520442889919 0.5735764363510462 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o wing1 +v 0.125 0.5051478245037531 -0.08881019196643142 +v 0.125 0.3976022426879319 -0.24240120027061735 +v 0.125 0.24916281066344315 0.09043244439327047 +v 0.125 0.14161722884762196 -0.06315856391091551 +v 0.0625 0.3976022426879319 -0.24240120027061735 +v 0.0625 0.5051478245037531 -0.08881019196643142 +v 0.0625 0.14161722884762196 -0.06315856391091551 +v 0.0625 0.24916281066344315 0.09043244439327047 +vt 0.6875 0.65625 +vt 0.71875 0.65625 +vt 0.71875 0.5 +vt 0.6875 0.5 +vt 0.59375 0.65625 +vt 0.6875 0.65625 +vt 0.6875 0.5 +vt 0.59375 0.5 +vt 0.8125 0.65625 +vt 0.84375 0.65625 +vt 0.84375 0.5 +vt 0.8125 0.5 +vt 0.71875 0.65625 +vt 0.8125 0.65625 +vt 0.8125 0.5 +vt 0.71875 0.5 +vt 0.71875 0.65625 +vt 0.6875 0.65625 +vt 0.6875 0.75 +vt 0.71875 0.75 +vt 0.75 0.75 +vt 0.71875 0.75 +vt 0.71875 0.65625 +vt 0.75 0.65625 +vn 0 -0.5735764363510462 -0.8191520442889919 +vn 1 0 0 +vn 0 0.5735764363510462 0.8191520442889919 +vn -1 0 0 +vn 0 0.8191520442889919 -0.5735764363510462 +vn 0 -0.8191520442889919 0.5735764363510462 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.0625 0.6125 -0.11249999999999999 +v 0.0625 0.6125 -0.2375 +v 0.0625 0.42500000000000004 -0.11249999999999999 +v 0.0625 0.42500000000000004 -0.2375 +v -0.0625 0.6125 -0.2375 +v -0.0625 0.6125 -0.11249999999999999 +v -0.0625 0.42500000000000004 -0.2375 +v -0.0625 0.42500000000000004 -0.11249999999999999 +vt 0.125 0.875 +vt 0.1875 0.875 +vt 0.1875 0.78125 +vt 0.125 0.78125 +vt 0.0625 0.875 +vt 0.125 0.875 +vt 0.125 0.78125 +vt 0.0625 0.78125 +vt 0.25 0.875 +vt 0.3125 0.875 +vt 0.3125 0.78125 +vt 0.25 0.78125 +vt 0.1875 0.875 +vt 0.25 0.875 +vt 0.25 0.78125 +vt 0.1875 0.78125 +vt 0.1875 0.875 +vt 0.125 0.875 +vt 0.125 0.9375 +vt 0.1875 0.9375 +vt 0.25 0.9375 +vt 0.1875 0.9375 +vt 0.1875 0.875 +vt 0.25 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head2 +v 0.0625 0.675 -0.11249999999999999 +v 0.0625 0.675 -0.3625 +v 0.0625 0.6125 -0.11249999999999999 +v 0.0625 0.6125 -0.3625 +v -0.0625 0.675 -0.3625 +v -0.0625 0.675 -0.11249999999999999 +v -0.0625 0.6125 -0.3625 +v -0.0625 0.6125 -0.11249999999999999 +vt 0.4375 0.875 +vt 0.5 0.875 +vt 0.5 0.84375 +vt 0.4375 0.84375 +vt 0.3125 0.875 +vt 0.4375 0.875 +vt 0.4375 0.84375 +vt 0.3125 0.84375 +vt 0.625 0.875 +vt 0.6875 0.875 +vt 0.6875 0.84375 +vt 0.625 0.84375 +vt 0.5 0.875 +vt 0.625 0.875 +vt 0.625 0.84375 +vt 0.5 0.84375 +vt 0.5 0.875 +vt 0.4375 0.875 +vt 0.4375 1 +vt 0.5 1 +vt 0.5625 1 +vt 0.5 1 +vt 0.5 0.875 +vt 0.5625 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o beak1 +v 0.03125 0.6125 -0.23125 +v 0.03125 0.6125 -0.29375 +v 0.03125 0.48750000000000004 -0.23125 +v 0.03125 0.48750000000000004 -0.29375 +v -0.03125 0.6125 -0.29375 +v -0.03125 0.6125 -0.23125 +v -0.03125 0.48750000000000004 -0.29375 +v -0.03125 0.48750000000000004 -0.23125 +vt 0.375 0.75 +vt 0.40625 0.75 +vt 0.40625 0.6875 +vt 0.375 0.6875 +vt 0.34375 0.75 +vt 0.375 0.75 +vt 0.375 0.6875 +vt 0.34375 0.6875 +vt 0.4375 0.75 +vt 0.46875 0.75 +vt 0.46875 0.6875 +vt 0.4375 0.6875 +vt 0.40625 0.75 +vt 0.4375 0.75 +vt 0.4375 0.6875 +vt 0.40625 0.6875 +vt 0.40625 0.75 +vt 0.375 0.75 +vt 0.375 0.78125 +vt 0.40625 0.78125 +vt 0.4375 0.78125 +vt 0.40625 0.78125 +vt 0.40625 0.75 +vt 0.4375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o beak2 +v 0.03125 0.6124999999999998 -0.29375 +v 0.03125 0.6124999999999998 -0.35625 +v 0.03125 0.5062500000000001 -0.29375 +v 0.03125 0.5062500000000001 -0.35625 +v -0.03125 0.6124999999999998 -0.35625 +v -0.03125 0.6124999999999998 -0.29375 +v -0.03125 0.5062500000000001 -0.35625 +v -0.03125 0.5062500000000001 -0.29375 +vt 0.53125 0.75 +vt 0.5625 0.75 +vt 0.5625 0.71875 +vt 0.53125 0.71875 +vt 0.5 0.75 +vt 0.53125 0.75 +vt 0.53125 0.71875 +vt 0.5 0.71875 +vt 0.59375 0.75 +vt 0.625 0.75 +vt 0.625 0.71875 +vt 0.59375 0.71875 +vt 0.5625 0.75 +vt 0.59375 0.75 +vt 0.59375 0.71875 +vt 0.5625 0.71875 +vt 0.5625 0.75 +vt 0.53125 0.75 +vt 0.53125 0.78125 +vt 0.5625 0.78125 +vt 0.59375 0.78125 +vt 0.5625 0.78125 +vt 0.5625 0.75 +vt 0.59375 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o feather +v 0 0.8812500000000001 -0.05625000000000002 +v 0 0.8812500000000001 -0.30625 +v 0 0.5687500000000001 -0.05625000000000002 +v 0 0.5687500000000001 -0.30625 +v 0 0.8812500000000001 -0.30625 +v 0 0.8812500000000001 -0.05625000000000002 +v 0 0.5687500000000001 -0.30625 +v 0 0.5687500000000001 -0.05625000000000002 +vt 0.1875 0.3125 +vt 0.1875 0.3125 +vt 0.1875 0.15625 +vt 0.1875 0.15625 +vt 0.0625 0.3125 +vt 0.1875 0.3125 +vt 0.1875 0.15625 +vt 0.0625 0.15625 +vt 0.3125 0.3125 +vt 0.3125 0.3125 +vt 0.3125 0.15625 +vt 0.3125 0.15625 +vt 0.1875 0.3125 +vt 0.3125 0.3125 +vt 0.3125 0.15625 +vt 0.1875 0.15625 +vt 0.1875 0.3125 +vt 0.1875 0.3125 +vt 0.1875 0.4375 +vt 0.1875 0.4375 +vt 0.1875 0.4375 +vt 0.1875 0.4375 +vt 0.1875 0.3125 +vt 0.1875 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o tail +v 0.09375 0.22142422560540864 0.027122222305063892 +v 0.09375 0.17354644791047258 -0.013052003300344839 +v 0.09375 0.06072732318377394 0.21863333308480837 +v 0.09375 0.012849545488837766 0.17845910747939964 +v -0.09375 0.17354644791047258 -0.013052003300344839 +v -0.09375 0.22142422560540864 0.027122222305063892 +v -0.09375 0.012849545488837766 0.17845910747939964 +v -0.09375 0.06072732318377394 0.21863333308480837 +vt 0.71875 0.9375 +vt 0.8125 0.9375 +vt 0.8125 0.8125 +vt 0.71875 0.8125 +vt 0.6875 0.9375 +vt 0.71875 0.9375 +vt 0.71875 0.8125 +vt 0.6875 0.8125 +vt 0.84375 0.9375 +vt 0.9375 0.9375 +vt 0.9375 0.8125 +vt 0.84375 0.8125 +vt 0.8125 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.8125 +vt 0.8125 0.8125 +vt 0.8125 0.9375 +vt 0.71875 0.9375 +vt 0.71875 0.96875 +vt 0.8125 0.96875 +vt 0.90625 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.9375 +vt 0.90625 0.9375 +vn 0 -0.766044443118978 -0.6427876096865393 +vn 1 0 0 +vn 0 0.766044443118978 0.6427876096865393 +vn -1 0 0 +vn 0 0.6427876096865393 -0.766044443118978 +vn 0 -0.6427876096865393 0.766044443118978 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o leg0 +v -0.03125 0.09375 -0.03125 +v -0.03125 0.09375 -0.09375 +v -0.03125 -0.03125 -0.03125 +v -0.03125 -0.03125 -0.09375 +v -0.09375 0.09375 -0.09375 +v -0.09375 0.09375 -0.03125 +v -0.09375 -0.03125 -0.09375 +v -0.09375 -0.03125 -0.03125 +vt 0.46875 0.40625 +vt 0.5 0.40625 +vt 0.5 0.34375 +vt 0.46875 0.34375 +vt 0.4375 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.34375 +vt 0.4375 0.34375 +vt 0.53125 0.40625 +vt 0.5625 0.40625 +vt 0.5625 0.34375 +vt 0.53125 0.34375 +vt 0.5 0.40625 +vt 0.53125 0.40625 +vt 0.53125 0.34375 +vt 0.5 0.34375 +vt 0.5 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.4375 +vt 0.5 0.4375 +vt 0.53125 0.4375 +vt 0.5 0.4375 +vt 0.5 0.40625 +vt 0.53125 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o leg1 +v 0.09375 0.09375 -0.03125 +v 0.09375 0.09375 -0.09375 +v 0.09375 -0.03125 -0.03125 +v 0.09375 -0.03125 -0.09375 +v 0.03125 0.09375 -0.09375 +v 0.03125 0.09375 -0.03125 +v 0.03125 -0.03125 -0.09375 +v 0.03125 -0.03125 -0.03125 +vt 0.46875 0.40625 +vt 0.5 0.40625 +vt 0.5 0.34375 +vt 0.46875 0.34375 +vt 0.4375 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.34375 +vt 0.4375 0.34375 +vt 0.53125 0.40625 +vt 0.5625 0.40625 +vt 0.5625 0.34375 +vt 0.53125 0.34375 +vt 0.5 0.40625 +vt 0.53125 0.40625 +vt 0.53125 0.34375 +vt 0.5 0.34375 +vt 0.5 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.4375 +vt 0.5 0.4375 +vt 0.53125 0.4375 +vt 0.5 0.4375 +vt 0.5 0.40625 +vt 0.53125 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_c341d2fe-028b-bd76-6b34-40c8c9b3653b +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/piglin.obj b/renderer/viewer/three/entity/models/piglin.obj new file mode 100644 index 00000000..2f68f4c1 --- /dev/null +++ b/renderer/viewer/three/entity/models/piglin.obj @@ -0,0 +1,739 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o Body +v 0.25 1.5 0.125 +v 0.25 1.5 -0.125 +v 0.25 0.75 0.125 +v 0.25 0.75 -0.125 +v -0.25 1.5 -0.125 +v -0.25 1.5 0.125 +v -0.25 0.75 -0.125 +v -0.25 0.75 0.125 +vt 0.3125 0.6875 +vt 0.4375 0.6875 +vt 0.4375 0.5 +vt 0.3125 0.5 +vt 0.25 0.6875 +vt 0.3125 0.6875 +vt 0.3125 0.5 +vt 0.25 0.5 +vt 0.5 0.6875 +vt 0.625 0.6875 +vt 0.625 0.5 +vt 0.5 0.5 +vt 0.4375 0.6875 +vt 0.5 0.6875 +vt 0.5 0.5 +vt 0.4375 0.5 +vt 0.4375 0.6875 +vt 0.3125 0.6875 +vt 0.3125 0.75 +vt 0.4375 0.75 +vt 0.5625 0.75 +vt 0.4375 0.75 +vt 0.4375 0.6875 +vt 0.5625 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o Body +v 0.265625 1.515625 0.140625 +v 0.265625 1.515625 -0.140625 +v 0.265625 0.734375 0.140625 +v 0.265625 0.734375 -0.140625 +v -0.265625 1.515625 -0.140625 +v -0.265625 1.515625 0.140625 +v -0.265625 0.734375 -0.140625 +v -0.265625 0.734375 0.140625 +vt 0.3125 0.4375 +vt 0.4375 0.4375 +vt 0.4375 0.25 +vt 0.3125 0.25 +vt 0.25 0.4375 +vt 0.3125 0.4375 +vt 0.3125 0.25 +vt 0.25 0.25 +vt 0.5 0.4375 +vt 0.625 0.4375 +vt 0.625 0.25 +vt 0.5 0.25 +vt 0.4375 0.4375 +vt 0.5 0.4375 +vt 0.5 0.25 +vt 0.4375 0.25 +vt 0.4375 0.4375 +vt 0.3125 0.4375 +vt 0.3125 0.5 +vt 0.4375 0.5 +vt 0.5625 0.5 +vt 0.4375 0.5 +vt 0.4375 0.4375 +vt 0.5625 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.31125 1.9987500000000002 0.24875000000000003 +v 0.31125 1.9987500000000002 -0.24875000000000003 +v 0.31125 1.5012499999999998 0.24875000000000003 +v 0.31125 1.5012499999999998 -0.24875000000000003 +v -0.31125 1.9987500000000002 -0.24875000000000003 +v -0.31125 1.9987500000000002 0.24875000000000003 +v -0.31125 1.5012499999999998 -0.24875000000000003 +v -0.31125 1.5012499999999998 0.24875000000000003 +vt 0.125 0.875 +vt 0.28125 0.875 +vt 0.28125 0.75 +vt 0.125 0.75 +vt 0 0.875 +vt 0.125 0.875 +vt 0.125 0.75 +vt 0 0.75 +vt 0.40625 0.875 +vt 0.5625 0.875 +vt 0.5625 0.75 +vt 0.40625 0.75 +vt 0.28125 0.875 +vt 0.40625 0.875 +vt 0.40625 0.75 +vt 0.28125 0.75 +vt 0.28125 0.875 +vt 0.125 0.875 +vt 0.125 1 +vt 0.28125 1 +vt 0.4375 1 +vt 0.28125 1 +vt 0.28125 0.875 +vt 0.4375 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.125 1.75 -0.25 +v 0.125 1.75 -0.3125 +v 0.125 1.5 -0.25 +v 0.125 1.5 -0.3125 +v -0.125 1.75 -0.3125 +v -0.125 1.75 -0.25 +v -0.125 1.5 -0.3125 +v -0.125 1.5 -0.25 +vt 0.5 0.96875 +vt 0.5625 0.96875 +vt 0.5625 0.90625 +vt 0.5 0.90625 +vt 0.484375 0.96875 +vt 0.5 0.96875 +vt 0.5 0.90625 +vt 0.484375 0.90625 +vt 0.578125 0.96875 +vt 0.640625 0.96875 +vt 0.640625 0.90625 +vt 0.578125 0.90625 +vt 0.5625 0.96875 +vt 0.578125 0.96875 +vt 0.578125 0.90625 +vt 0.5625 0.90625 +vt 0.5625 0.96875 +vt 0.5 0.96875 +vt 0.5 0.984375 +vt 0.5625 0.984375 +vt 0.625 0.984375 +vt 0.5625 0.984375 +vt 0.5625 0.96875 +vt 0.625 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head +v -0.125 1.625 -0.25 +v -0.125 1.625 -0.3125 +v -0.125 1.5 -0.25 +v -0.125 1.5 -0.3125 +v -0.1875 1.625 -0.3125 +v -0.1875 1.625 -0.25 +v -0.1875 1.5 -0.3125 +v -0.1875 1.5 -0.25 +vt 0.046875 0.921875 +vt 0.0625 0.921875 +vt 0.0625 0.890625 +vt 0.046875 0.890625 +vt 0.03125 0.921875 +vt 0.046875 0.921875 +vt 0.046875 0.890625 +vt 0.03125 0.890625 +vt 0.078125 0.921875 +vt 0.09375 0.921875 +vt 0.09375 0.890625 +vt 0.078125 0.890625 +vt 0.0625 0.921875 +vt 0.078125 0.921875 +vt 0.078125 0.890625 +vt 0.0625 0.890625 +vt 0.0625 0.921875 +vt 0.046875 0.921875 +vt 0.046875 0.9375 +vt 0.0625 0.9375 +vt 0.078125 0.9375 +vt 0.0625 0.9375 +vt 0.0625 0.921875 +vt 0.078125 0.921875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o head +v 0.1875 1.625 -0.25 +v 0.1875 1.625 -0.3125 +v 0.1875 1.5 -0.25 +v 0.1875 1.5 -0.3125 +v 0.125 1.625 -0.3125 +v 0.125 1.625 -0.25 +v 0.125 1.5 -0.3125 +v 0.125 1.5 -0.25 +vt 0.046875 0.984375 +vt 0.0625 0.984375 +vt 0.0625 0.953125 +vt 0.046875 0.953125 +vt 0.03125 0.984375 +vt 0.046875 0.984375 +vt 0.046875 0.953125 +vt 0.03125 0.953125 +vt 0.078125 0.984375 +vt 0.09375 0.984375 +vt 0.09375 0.953125 +vt 0.078125 0.953125 +vt 0.0625 0.984375 +vt 0.078125 0.984375 +vt 0.078125 0.953125 +vt 0.0625 0.953125 +vt 0.0625 0.984375 +vt 0.046875 0.984375 +vt 0.046875 1 +vt 0.0625 1 +vt 0.078125 1 +vt 0.0625 1 +vt 0.0625 0.984375 +vt 0.078125 0.984375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o leftear +v -0.25837341226347266 1.84375 0.125 +v -0.25837341226347266 1.84375 -0.125 +v -0.41462341226347266 1.573117061317363 0.125 +v -0.41462341226347266 1.573117061317363 -0.125 +v -0.3125 1.875 -0.125 +v -0.3125 1.875 0.125 +v -0.46875 1.604367061317363 -0.125 +v -0.46875 1.604367061317363 0.125 +vt 0.859375 0.84375 +vt 0.875 0.84375 +vt 0.875 0.765625 +vt 0.859375 0.765625 +vt 0.796875 0.84375 +vt 0.859375 0.84375 +vt 0.859375 0.765625 +vt 0.796875 0.765625 +vt 0.9375 0.84375 +vt 0.953125 0.84375 +vt 0.953125 0.765625 +vt 0.9375 0.765625 +vt 0.875 0.84375 +vt 0.9375 0.84375 +vt 0.9375 0.765625 +vt 0.875 0.765625 +vt 0.875 0.84375 +vt 0.859375 0.84375 +vt 0.859375 0.90625 +vt 0.875 0.90625 +vt 0.890625 0.90625 +vt 0.875 0.90625 +vt 0.875 0.84375 +vt 0.890625 0.84375 +vn 0 0 -1 +vn 0.8660254037844387 -0.49999999999999994 0 +vn 0 0 1 +vn -0.8660254037844387 0.49999999999999994 0 +vn 0.49999999999999994 0.8660254037844387 0 +vn -0.49999999999999994 -0.8660254037844387 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o rightear +v 0.3125 1.875 0.125 +v 0.3125 1.875 -0.125 +v 0.46875 1.604367061317363 0.125 +v 0.46875 1.604367061317363 -0.125 +v 0.25837341226347266 1.84375 -0.125 +v 0.25837341226347266 1.84375 0.125 +v 0.41462341226347266 1.573117061317363 -0.125 +v 0.41462341226347266 1.573117061317363 0.125 +vt 0.671875 0.84375 +vt 0.6875 0.84375 +vt 0.6875 0.765625 +vt 0.671875 0.765625 +vt 0.609375 0.84375 +vt 0.671875 0.84375 +vt 0.671875 0.765625 +vt 0.609375 0.765625 +vt 0.75 0.84375 +vt 0.765625 0.84375 +vt 0.765625 0.765625 +vt 0.75 0.765625 +vt 0.6875 0.84375 +vt 0.75 0.84375 +vt 0.75 0.765625 +vt 0.6875 0.765625 +vt 0.6875 0.84375 +vt 0.671875 0.84375 +vt 0.671875 0.90625 +vt 0.6875 0.90625 +vt 0.703125 0.90625 +vt 0.6875 0.90625 +vt 0.6875 0.84375 +vt 0.703125 0.84375 +vn 0 0 -1 +vn 0.8660254037844387 0.49999999999999994 0 +vn 0 0 1 +vn -0.8660254037844387 -0.49999999999999994 0 +vn -0.49999999999999994 0.8660254037844387 0 +vn 0.49999999999999994 -0.8660254037844387 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o RightArm +v 0.5 1.5 0.125 +v 0.5 1.5 -0.125 +v 0.5 0.75 0.125 +v 0.5 0.75 -0.125 +v 0.25 1.5 -0.125 +v 0.25 1.5 0.125 +v 0.25 0.75 -0.125 +v 0.25 0.75 0.125 +vt 0.6875 0.6875 +vt 0.75 0.6875 +vt 0.75 0.5 +vt 0.6875 0.5 +vt 0.625 0.6875 +vt 0.6875 0.6875 +vt 0.6875 0.5 +vt 0.625 0.5 +vt 0.8125 0.6875 +vt 0.875 0.6875 +vt 0.875 0.5 +vt 0.8125 0.5 +vt 0.75 0.6875 +vt 0.8125 0.6875 +vt 0.8125 0.5 +vt 0.75 0.5 +vt 0.75 0.6875 +vt 0.6875 0.6875 +vt 0.6875 0.75 +vt 0.75 0.75 +vt 0.8125 0.75 +vt 0.75 0.75 +vt 0.75 0.6875 +vt 0.8125 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o RightArm +v 0.515625 1.515625 0.140625 +v 0.515625 1.515625 -0.140625 +v 0.515625 0.734375 0.140625 +v 0.515625 0.734375 -0.140625 +v 0.234375 1.515625 -0.140625 +v 0.234375 1.515625 0.140625 +v 0.234375 0.734375 -0.140625 +v 0.234375 0.734375 0.140625 +vt 0.6875 0.4375 +vt 0.75 0.4375 +vt 0.75 0.25 +vt 0.6875 0.25 +vt 0.625 0.4375 +vt 0.6875 0.4375 +vt 0.6875 0.25 +vt 0.625 0.25 +vt 0.8125 0.4375 +vt 0.875 0.4375 +vt 0.875 0.25 +vt 0.8125 0.25 +vt 0.75 0.4375 +vt 0.8125 0.4375 +vt 0.8125 0.25 +vt 0.75 0.25 +vt 0.75 0.4375 +vt 0.6875 0.4375 +vt 0.6875 0.5 +vt 0.75 0.5 +vt 0.8125 0.5 +vt 0.75 0.5 +vt 0.75 0.4375 +vt 0.8125 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o LeftArm +v -0.25 1.5 0.125 +v -0.25 1.5 -0.125 +v -0.25 0.75 0.125 +v -0.25 0.75 -0.125 +v -0.5 1.5 -0.125 +v -0.5 1.5 0.125 +v -0.5 0.75 -0.125 +v -0.5 0.75 0.125 +vt 0.5625 0.1875 +vt 0.625 0.1875 +vt 0.625 0 +vt 0.5625 0 +vt 0.5 0.1875 +vt 0.5625 0.1875 +vt 0.5625 0 +vt 0.5 0 +vt 0.6875 0.1875 +vt 0.75 0.1875 +vt 0.75 0 +vt 0.6875 0 +vt 0.625 0.1875 +vt 0.6875 0.1875 +vt 0.6875 0 +vt 0.625 0 +vt 0.625 0.1875 +vt 0.5625 0.1875 +vt 0.5625 0.25 +vt 0.625 0.25 +vt 0.6875 0.25 +vt 0.625 0.25 +vt 0.625 0.1875 +vt 0.6875 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o LeftArm +v -0.234375 1.515625 0.140625 +v -0.234375 1.515625 -0.140625 +v -0.234375 0.734375 0.140625 +v -0.234375 0.734375 -0.140625 +v -0.515625 1.515625 -0.140625 +v -0.515625 1.515625 0.140625 +v -0.515625 0.734375 -0.140625 +v -0.515625 0.734375 0.140625 +vt 0.8125 0.1875 +vt 0.875 0.1875 +vt 0.875 0 +vt 0.8125 0 +vt 0.75 0.1875 +vt 0.8125 0.1875 +vt 0.8125 0 +vt 0.75 0 +vt 0.9375 0.1875 +vt 1 0.1875 +vt 1 0 +vt 0.9375 0 +vt 0.875 0.1875 +vt 0.9375 0.1875 +vt 0.9375 0 +vt 0.875 0 +vt 0.875 0.1875 +vt 0.8125 0.1875 +vt 0.8125 0.25 +vt 0.875 0.25 +vt 0.9375 0.25 +vt 0.875 0.25 +vt 0.875 0.1875 +vt 0.9375 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o RightLeg +v 0.25 0.75 0.125 +v 0.25 0.75 -0.125 +v 0.25 0 0.125 +v 0.25 0 -0.125 +v 0 0.75 -0.125 +v 0 0.75 0.125 +v 0 0 -0.125 +v 0 0 0.125 +vt 0.0625 0.6875 +vt 0.125 0.6875 +vt 0.125 0.5 +vt 0.0625 0.5 +vt 0 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.1875 0.6875 +vt 0.25 0.6875 +vt 0.25 0.5 +vt 0.1875 0.5 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.75 +vt 0.125 0.75 +vt 0.1875 0.75 +vt 0.125 0.75 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o RightLeg +v 0.265625 0.765625 0.140625 +v 0.265625 0.765625 -0.140625 +v 0.265625 -0.015625 0.140625 +v 0.265625 -0.015625 -0.140625 +v -0.015625 0.765625 -0.140625 +v -0.015625 0.765625 0.140625 +v -0.015625 -0.015625 -0.140625 +v -0.015625 -0.015625 0.140625 +vt 0.0625 0.4375 +vt 0.125 0.4375 +vt 0.125 0.25 +vt 0.0625 0.25 +vt 0 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.25 +vt 0 0.25 +vt 0.1875 0.4375 +vt 0.25 0.4375 +vt 0.25 0.25 +vt 0.1875 0.25 +vt 0.125 0.4375 +vt 0.1875 0.4375 +vt 0.1875 0.25 +vt 0.125 0.25 +vt 0.125 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.5 +vt 0.125 0.5 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.4375 +vt 0.1875 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o LeftLeg +v 0 0.75 0.125 +v 0 0.75 -0.125 +v 0 0 0.125 +v 0 0 -0.125 +v -0.25 0.75 -0.125 +v -0.25 0.75 0.125 +v -0.25 0 -0.125 +v -0.25 0 0.125 +vt 0.3125 0.1875 +vt 0.375 0.1875 +vt 0.375 0 +vt 0.3125 0 +vt 0.25 0.1875 +vt 0.3125 0.1875 +vt 0.3125 0 +vt 0.25 0 +vt 0.4375 0.1875 +vt 0.5 0.1875 +vt 0.5 0 +vt 0.4375 0 +vt 0.375 0.1875 +vt 0.4375 0.1875 +vt 0.4375 0 +vt 0.375 0 +vt 0.375 0.1875 +vt 0.3125 0.1875 +vt 0.3125 0.25 +vt 0.375 0.25 +vt 0.4375 0.25 +vt 0.375 0.25 +vt 0.375 0.1875 +vt 0.4375 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 +o LeftLeg +v 0.015625 0.765625 0.140625 +v 0.015625 0.765625 -0.140625 +v 0.015625 -0.015625 0.140625 +v 0.015625 -0.015625 -0.140625 +v -0.265625 0.765625 -0.140625 +v -0.265625 0.765625 0.140625 +v -0.265625 -0.015625 -0.140625 +v -0.265625 -0.015625 0.140625 +vt 0.0625 0.1875 +vt 0.125 0.1875 +vt 0.125 0 +vt 0.0625 0 +vt 0 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0 +vt 0 0 +vt 0.1875 0.1875 +vt 0.25 0.1875 +vt 0.25 0 +vt 0.1875 0 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0 +vt 0.125 0 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.25 +vt 0.125 0.25 +vt 0.1875 0.25 +vt 0.125 0.25 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_bd9b2175-46cc-d3be-0d85-ed94b50a7825 +f 124/364/91 127/363/91 125/362/91 122/361/91 +f 123/368/92 124/367/92 122/366/92 121/365/92 +f 128/372/93 123/371/93 121/370/93 126/369/93 +f 127/376/94 128/375/94 126/374/94 125/373/94 +f 126/380/95 121/379/95 122/378/95 125/377/95 +f 127/384/96 124/383/96 123/382/96 128/381/96 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/pillager.obj b/renderer/viewer/three/entity/models/pillager.obj new file mode 100644 index 00000000..213a3b94 --- /dev/null +++ b/renderer/viewer/three/entity/models/pillager.obj @@ -0,0 +1,371 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o Body +v 0.25 1.5 0.1875 +v 0.25 1.5 -0.1875 +v 0.25 0.75 0.1875 +v 0.25 0.75 -0.1875 +v -0.25 1.5 -0.1875 +v -0.25 1.5 0.1875 +v -0.25 0.75 -0.1875 +v -0.25 0.75 0.1875 +vt 0.34375 0.59375 +vt 0.46875 0.59375 +vt 0.46875 0.40625 +vt 0.34375 0.40625 +vt 0.25 0.59375 +vt 0.34375 0.59375 +vt 0.34375 0.40625 +vt 0.25 0.40625 +vt 0.5625 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.40625 +vt 0.5625 0.40625 +vt 0.46875 0.59375 +vt 0.5625 0.59375 +vt 0.5625 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.59375 +vt 0.34375 0.59375 +vt 0.34375 0.6875 +vt 0.46875 0.6875 +vt 0.59375 0.6875 +vt 0.46875 0.6875 +vt 0.46875 0.59375 +vt 0.59375 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o Body +v 0.28125 1.53125 0.21875 +v 0.28125 1.53125 -0.21875 +v 0.28125 0.34375 0.21875 +v 0.28125 0.34375 -0.21875 +v -0.28125 1.53125 -0.21875 +v -0.28125 1.53125 0.21875 +v -0.28125 0.34375 -0.21875 +v -0.28125 0.34375 0.21875 +vt 0.09375 0.3125 +vt 0.21875 0.3125 +vt 0.21875 0.03125 +vt 0.09375 0.03125 +vt 0 0.3125 +vt 0.09375 0.3125 +vt 0.09375 0.03125 +vt 0 0.03125 +vt 0.3125 0.3125 +vt 0.4375 0.3125 +vt 0.4375 0.03125 +vt 0.3125 0.03125 +vt 0.21875 0.3125 +vt 0.3125 0.3125 +vt 0.3125 0.03125 +vt 0.21875 0.03125 +vt 0.21875 0.3125 +vt 0.09375 0.3125 +vt 0.09375 0.40625 +vt 0.21875 0.40625 +vt 0.34375 0.40625 +vt 0.21875 0.40625 +vt 0.21875 0.3125 +vt 0.34375 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.25 2.125 0.25 +v 0.25 2.125 -0.25 +v 0.25 1.5 0.25 +v 0.25 1.5 -0.25 +v -0.25 2.125 -0.25 +v -0.25 2.125 0.25 +v -0.25 1.5 -0.25 +v -0.25 1.5 0.25 +vt 0.125 0.875 +vt 0.25 0.875 +vt 0.25 0.71875 +vt 0.125 0.71875 +vt 0 0.875 +vt 0.125 0.875 +vt 0.125 0.71875 +vt 0 0.71875 +vt 0.375 0.875 +vt 0.5 0.875 +vt 0.5 0.71875 +vt 0.375 0.71875 +vt 0.25 0.875 +vt 0.375 0.875 +vt 0.375 0.71875 +vt 0.25 0.71875 +vt 0.25 0.875 +vt 0.125 0.875 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.875 +vt 0.375 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o nose +v 0.0625 1.6875 -0.25 +v 0.0625 1.6875 -0.375 +v 0.0625 1.4375 -0.25 +v 0.0625 1.4375 -0.375 +v -0.0625 1.6875 -0.375 +v -0.0625 1.6875 -0.25 +v -0.0625 1.4375 -0.375 +v -0.0625 1.4375 -0.25 +vt 0.40625 0.96875 +vt 0.4375 0.96875 +vt 0.4375 0.90625 +vt 0.40625 0.90625 +vt 0.375 0.96875 +vt 0.40625 0.96875 +vt 0.40625 0.90625 +vt 0.375 0.90625 +vt 0.46875 0.96875 +vt 0.5 0.96875 +vt 0.5 0.90625 +vt 0.46875 0.90625 +vt 0.4375 0.96875 +vt 0.46875 0.96875 +vt 0.46875 0.90625 +vt 0.4375 0.90625 +vt 0.4375 0.96875 +vt 0.40625 0.96875 +vt 0.40625 1 +vt 0.4375 1 +vt 0.46875 1 +vt 0.4375 1 +vt 0.4375 0.96875 +vt 0.46875 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o LeftLeg +v 0 0.75 0.125 +v 0 0.75 -0.125 +v 0 0 0.125 +v 0 0 -0.125 +v -0.25 0.75 -0.125 +v -0.25 0.75 0.125 +v -0.25 0 -0.125 +v -0.25 0 0.125 +vt 0.0625 0.59375 +vt 0.125 0.59375 +vt 0.125 0.40625 +vt 0.0625 0.40625 +vt 0 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.40625 +vt 0 0.40625 +vt 0.1875 0.59375 +vt 0.25 0.59375 +vt 0.25 0.40625 +vt 0.1875 0.40625 +vt 0.125 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.40625 +vt 0.125 0.40625 +vt 0.125 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.65625 +vt 0.125 0.65625 +vt 0.1875 0.65625 +vt 0.125 0.65625 +vt 0.125 0.59375 +vt 0.1875 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o RightLeg +v 0.25 0.75 0.125 +v 0.25 0.75 -0.125 +v 0.25 0 0.125 +v 0.25 0 -0.125 +v 0 0.75 -0.125 +v 0 0.75 0.125 +v 0 0 -0.125 +v 0 0 0.125 +vt 0.125 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.40625 +vt 0.125 0.40625 +vt 0.1875 0.59375 +vt 0.125 0.59375 +vt 0.125 0.40625 +vt 0.1875 0.40625 +vt 0.25 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.40625 +vt 0.25 0.40625 +vt 0.0625 0.59375 +vt 0 0.59375 +vt 0 0.40625 +vt 0.0625 0.40625 +vt 0.0625 0.59375 +vt 0.125 0.59375 +vt 0.125 0.65625 +vt 0.0625 0.65625 +vt 0.125 0.65625 +vt 0.1875 0.65625 +vt 0.1875 0.59375 +vt 0.125 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o RightArm +v 0.5 1.5 0.125 +v 0.5 1.5 -0.125 +v 0.5 0.75 0.125 +v 0.5 0.75 -0.125 +v 0.25 1.5 -0.125 +v 0.25 1.5 0.125 +v 0.25 0.75 -0.125 +v 0.25 0.75 0.125 +vt 0.6875 0.21875 +vt 0.75 0.21875 +vt 0.75 0.03125 +vt 0.6875 0.03125 +vt 0.625 0.21875 +vt 0.6875 0.21875 +vt 0.6875 0.03125 +vt 0.625 0.03125 +vt 0.8125 0.21875 +vt 0.875 0.21875 +vt 0.875 0.03125 +vt 0.8125 0.03125 +vt 0.75 0.21875 +vt 0.8125 0.21875 +vt 0.8125 0.03125 +vt 0.75 0.03125 +vt 0.75 0.21875 +vt 0.6875 0.21875 +vt 0.6875 0.28125 +vt 0.75 0.28125 +vt 0.8125 0.28125 +vt 0.75 0.28125 +vt 0.75 0.21875 +vt 0.8125 0.21875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o LeftArm +v -0.25 1.5 0.125 +v -0.25 1.5 -0.125 +v -0.25 0.75 0.125 +v -0.25 0.75 -0.125 +v -0.5 1.5 -0.125 +v -0.5 1.5 0.125 +v -0.5 0.75 -0.125 +v -0.5 0.75 0.125 +vt 0.75 0.21875 +vt 0.6875 0.21875 +vt 0.6875 0.03125 +vt 0.75 0.03125 +vt 0.8125 0.21875 +vt 0.75 0.21875 +vt 0.75 0.03125 +vt 0.8125 0.03125 +vt 0.875 0.21875 +vt 0.8125 0.21875 +vt 0.8125 0.03125 +vt 0.875 0.03125 +vt 0.6875 0.21875 +vt 0.625 0.21875 +vt 0.625 0.03125 +vt 0.6875 0.03125 +vt 0.6875 0.21875 +vt 0.75 0.21875 +vt 0.75 0.28125 +vt 0.6875 0.28125 +vt 0.75 0.28125 +vt 0.8125 0.28125 +vt 0.8125 0.21875 +vt 0.75 0.21875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_0e7f3cc8-7af8-21a7-d2f3-04bee5fb4aae +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/rabbit.obj b/renderer/viewer/three/entity/models/rabbit.obj new file mode 100644 index 00000000..545c4b5d --- /dev/null +++ b/renderer/viewer/three/entity/models/rabbit.obj @@ -0,0 +1,555 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o rearFootLeft +v -0.125 0.0625 0.4375 +v -0.125 0.0625 0 +v -0.125 0 0.4375 +v -0.125 0 0 +v -0.25 0.0625 0 +v -0.25 0.0625 0.4375 +v -0.25 0 0 +v -0.25 0 0.4375 +vt 0.265625 0.03125 +vt 0.234375 0.03125 +vt 0.234375 0 +vt 0.265625 0 +vt 0.375 0.03125 +vt 0.265625 0.03125 +vt 0.265625 0 +vt 0.375 0 +vt 0.40625 0.03125 +vt 0.375 0.03125 +vt 0.375 0 +vt 0.40625 0 +vt 0.234375 0.03125 +vt 0.125 0.03125 +vt 0.125 0 +vt 0.234375 0 +vt 0.234375 0.03125 +vt 0.265625 0.03125 +vt 0.265625 0.25 +vt 0.234375 0.25 +vt 0.265625 0.25 +vt 0.296875 0.25 +vt 0.296875 0.03125 +vt 0.265625 0.03125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o rearFootRight +v 0.25 0.0625 0.4375 +v 0.25 0.0625 0 +v 0.25 0 0.4375 +v 0.25 0 0 +v 0.125 0.0625 0 +v 0.125 0.0625 0.4375 +v 0.125 0 0 +v 0.125 0 0.4375 +vt 0.546875 0.03125 +vt 0.515625 0.03125 +vt 0.515625 0 +vt 0.546875 0 +vt 0.65625 0.03125 +vt 0.546875 0.03125 +vt 0.546875 0 +vt 0.65625 0 +vt 0.6875 0.03125 +vt 0.65625 0.03125 +vt 0.65625 0 +vt 0.6875 0 +vt 0.515625 0.03125 +vt 0.40625 0.03125 +vt 0.40625 0 +vt 0.515625 0 +vt 0.515625 0.03125 +vt 0.546875 0.03125 +vt 0.546875 0.25 +vt 0.515625 0.25 +vt 0.546875 0.25 +vt 0.578125 0.25 +vt 0.578125 0.03125 +vt 0.546875 0.03125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o haunchLeft +v -0.125 0.29936870521072856 0.5249039439955965 +v -0.125 0.40625 0.23124999999999996 +v -0.125 0.06444555001425145 0.43939890816417915 +v -0.125 0.1713268448035229 0.14574496416858285 +v -0.25 0.40625 0.23124999999999996 +v -0.25 0.29936870521072856 0.5249039439955965 +v -0.25 0.1713268448035229 0.14574496416858285 +v -0.25 0.06444555001425145 0.43939890816417915 +vt 0.359375 0.375 +vt 0.328125 0.375 +vt 0.328125 0.25 +vt 0.359375 0.25 +vt 0.4375 0.375 +vt 0.359375 0.375 +vt 0.359375 0.25 +vt 0.4375 0.25 +vt 0.46875 0.375 +vt 0.4375 0.375 +vt 0.4375 0.25 +vt 0.46875 0.25 +vt 0.328125 0.375 +vt 0.25 0.375 +vt 0.25 0.25 +vt 0.328125 0.25 +vt 0.328125 0.375 +vt 0.359375 0.375 +vt 0.359375 0.53125 +vt 0.328125 0.53125 +vt 0.359375 0.53125 +vt 0.390625 0.53125 +vt 0.390625 0.375 +vt 0.359375 0.375 +vn 0 0.34202014332566866 -0.9396926207859084 +vn 1 0 0 +vn 0 -0.34202014332566866 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 0.34202014332566866 +vn 0 -0.9396926207859084 -0.34202014332566866 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o haunchRight +v 0.25 0.29936870521072856 0.5249039439955965 +v 0.25 0.40625 0.23124999999999996 +v 0.25 0.06444555001425145 0.43939890816417915 +v 0.25 0.1713268448035229 0.14574496416858285 +v 0.125 0.40625 0.23124999999999996 +v 0.125 0.29936870521072856 0.5249039439955965 +v 0.125 0.1713268448035229 0.14574496416858285 +v 0.125 0.06444555001425145 0.43939890816417915 +vt 0.578125 0.375 +vt 0.546875 0.375 +vt 0.546875 0.25 +vt 0.578125 0.25 +vt 0.65625 0.375 +vt 0.578125 0.375 +vt 0.578125 0.25 +vt 0.65625 0.25 +vt 0.6875 0.375 +vt 0.65625 0.375 +vt 0.65625 0.25 +vt 0.6875 0.25 +vt 0.546875 0.375 +vt 0.46875 0.375 +vt 0.46875 0.25 +vt 0.546875 0.25 +vt 0.546875 0.375 +vt 0.578125 0.375 +vt 0.578125 0.53125 +vt 0.546875 0.53125 +vt 0.578125 0.53125 +vt 0.609375 0.53125 +vt 0.609375 0.375 +vt 0.578125 0.375 +vn 0 0.34202014332566866 -0.9396926207859084 +vn 1 0 0 +vn 0 -0.34202014332566866 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 0.34202014332566866 +vn 0 -0.9396926207859084 -0.34202014332566866 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o body +v 0.1875 0.4299615775982385 0.5427525179157087 +v 0.1875 0.6437241671767815 -0.04455537007548416 +v 0.1875 0.1363076336026421 0.4358712231264371 +v 0.1875 0.3500702231811851 -0.1514366648647556 +v -0.1875 0.6437241671767815 -0.04455537007548416 +v -0.1875 0.4299615775982385 0.5427525179157087 +v -0.1875 0.3500702231811851 -0.1514366648647556 +v -0.1875 0.1363076336026421 0.4358712231264371 +vt 0.25 0.6875 +vt 0.15625 0.6875 +vt 0.15625 0.53125 +vt 0.25 0.53125 +vt 0.40625 0.6875 +vt 0.25 0.6875 +vt 0.25 0.53125 +vt 0.40625 0.53125 +vt 0.5 0.6875 +vt 0.40625 0.6875 +vt 0.40625 0.53125 +vt 0.5 0.53125 +vt 0.15625 0.6875 +vt 0 0.6875 +vt 0 0.53125 +vt 0.15625 0.53125 +vt 0.15625 0.6875 +vt 0.25 0.6875 +vt 0.25 1 +vt 0.15625 1 +vt 0.25 1 +vt 0.34375 1 +vt 0.34375 0.6875 +vt 0.25 0.6875 +vn 0 0.34202014332566866 -0.9396926207859084 +vn 1 0 0 +vn 0 -0.34202014332566866 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 0.34202014332566866 +vn 0 -0.9396926207859084 -0.34202014332566866 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o frontLegLeft +v -0.125 0.42664698889581687 -0.0009495154367369918 +v -0.125 0.44835301110418313 -0.12405048456326301 +v -0.125 -0.004206403047024132 -0.07692059316601901 +v -0.125 0.017499619161342128 -0.20002156229254503 +v -0.25 0.44835301110418313 -0.12405048456326301 +v -0.25 0.42664698889581687 -0.0009495154367369918 +v -0.25 0.017499619161342128 -0.20002156229254503 +v -0.25 -0.004206403047024132 -0.07692059316601901 +vt 0.1875 0.46875 +vt 0.15625 0.46875 +vt 0.15625 0.25 +vt 0.1875 0.25 +vt 0.21875 0.46875 +vt 0.1875 0.46875 +vt 0.1875 0.25 +vt 0.21875 0.25 +vt 0.25 0.46875 +vt 0.21875 0.46875 +vt 0.21875 0.25 +vt 0.25 0.25 +vt 0.15625 0.46875 +vt 0.125 0.46875 +vt 0.125 0.25 +vt 0.15625 0.25 +vt 0.15625 0.46875 +vt 0.1875 0.46875 +vt 0.1875 0.53125 +vt 0.15625 0.53125 +vt 0.1875 0.53125 +vt 0.21875 0.53125 +vt 0.21875 0.46875 +vt 0.1875 0.46875 +vn 0 0.17364817766693033 -0.984807753012208 +vn 1 0 0 +vn 0 -0.17364817766693033 0.984807753012208 +vn -1 0 0 +vn 0 0.984807753012208 0.17364817766693033 +vn 0 -0.984807753012208 -0.17364817766693033 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o frontLegRight +v 0.25 0.42664698889581687 -0.0009495154367369918 +v 0.25 0.44835301110418313 -0.12405048456326301 +v 0.25 -0.004206403047024132 -0.07692059316601901 +v 0.25 0.017499619161342128 -0.20002156229254503 +v 0.125 0.44835301110418313 -0.12405048456326301 +v 0.125 0.42664698889581687 -0.0009495154367369918 +v 0.125 0.017499619161342128 -0.20002156229254503 +v 0.125 -0.004206403047024132 -0.07692059316601901 +vt 0.0625 0.46875 +vt 0.03125 0.46875 +vt 0.03125 0.25 +vt 0.0625 0.25 +vt 0.09375 0.46875 +vt 0.0625 0.46875 +vt 0.0625 0.25 +vt 0.09375 0.25 +vt 0.125 0.46875 +vt 0.09375 0.46875 +vt 0.09375 0.25 +vt 0.125 0.25 +vt 0.03125 0.46875 +vt 0 0.46875 +vt 0 0.25 +vt 0.03125 0.25 +vt 0.03125 0.46875 +vt 0.0625 0.46875 +vt 0.0625 0.53125 +vt 0.03125 0.53125 +vt 0.0625 0.53125 +vt 0.09375 0.53125 +vt 0.09375 0.46875 +vt 0.0625 0.46875 +vn 0 0.17364817766693033 -0.984807753012208 +vn 1 0 0 +vn 0 -0.17364817766693033 0.984807753012208 +vn -1 0 0 +vn 0 0.984807753012208 0.17364817766693033 +vn 0 -0.984807753012208 -0.17364817766693033 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o head +v 0.15625 0.75 -0.0625 +v 0.15625 0.75 -0.375 +v 0.15625 0.5 -0.0625 +v 0.15625 0.5 -0.375 +v -0.15625 0.75 -0.375 +v -0.15625 0.75 -0.0625 +v -0.15625 0.5 -0.375 +v -0.15625 0.5 -0.0625 +vt 0.65625 0.84375 +vt 0.578125 0.84375 +vt 0.578125 0.71875 +vt 0.65625 0.71875 +vt 0.734375 0.84375 +vt 0.65625 0.84375 +vt 0.65625 0.71875 +vt 0.734375 0.71875 +vt 0.8125 0.84375 +vt 0.734375 0.84375 +vt 0.734375 0.71875 +vt 0.8125 0.71875 +vt 0.578125 0.84375 +vt 0.5 0.84375 +vt 0.5 0.71875 +vt 0.578125 0.71875 +vt 0.578125 0.84375 +vt 0.65625 0.84375 +vt 0.65625 1 +vt 0.578125 1 +vt 0.65625 1 +vt 0.734375 1 +vt 0.734375 0.84375 +vt 0.65625 0.84375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o earRight +v 0.1509259103576669 1.0625 -0.10294047579726884 +v 0.1347497200387594 1.0625 -0.16331083994033568 +v 0.1509259103576669 0.75 -0.10294047579726884 +v 0.1347497200387594 0.75 -0.16331083994033568 +v 0.014008991752625821 1.0625 -0.1309584593025206 +v 0.030185182071533423 1.0625 -0.07058809515945375 +v 0.014008991752625821 0.75 -0.1309584593025206 +v 0.030185182071533423 0.75 -0.07058809515945375 +vt 0.953125 0.96875 +vt 0.921875 0.96875 +vt 0.921875 0.8125 +vt 0.953125 0.8125 +vt 0.96875 0.96875 +vt 0.953125 0.96875 +vt 0.953125 0.8125 +vt 0.96875 0.8125 +vt 1 0.96875 +vt 0.96875 0.96875 +vt 0.96875 0.8125 +vt 1 0.8125 +vt 0.921875 0.96875 +vt 0.90625 0.96875 +vt 0.90625 0.8125 +vt 0.921875 0.8125 +vt 0.921875 0.96875 +vt 0.953125 0.96875 +vt 0.953125 1 +vt 0.921875 1 +vt 0.953125 1 +vt 0.984375 1 +vt 0.984375 0.96875 +vt 0.953125 0.96875 +vn -0.25881904510252074 0 -0.9659258262890683 +vn 0.9659258262890683 0 -0.25881904510252074 +vn 0.25881904510252074 0 0.9659258262890683 +vn -0.9659258262890683 0 0.25881904510252074 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o earLeft +v -0.030185182071533367 1.0625 -0.07058809515945375 +v -0.014008991752625821 1.0625 -0.1309584593025206 +v -0.030185182071533367 0.75 -0.07058809515945375 +v -0.014008991752625821 0.75 -0.1309584593025206 +v -0.1347497200387594 1.0625 -0.16331083994033568 +v -0.1509259103576669 1.0625 -0.10294047579726884 +v -0.1347497200387594 0.75 -0.16331083994033568 +v -0.1509259103576669 0.75 -0.10294047579726884 +vt 0.859375 0.96875 +vt 0.828125 0.96875 +vt 0.828125 0.8125 +vt 0.859375 0.8125 +vt 0.875 0.96875 +vt 0.859375 0.96875 +vt 0.859375 0.8125 +vt 0.875 0.8125 +vt 0.90625 0.96875 +vt 0.875 0.96875 +vt 0.875 0.8125 +vt 0.90625 0.8125 +vt 0.828125 0.96875 +vt 0.8125 0.96875 +vt 0.8125 0.8125 +vt 0.828125 0.8125 +vt 0.828125 0.96875 +vt 0.859375 0.96875 +vt 0.859375 1 +vt 0.828125 1 +vt 0.859375 1 +vt 0.890625 1 +vt 0.890625 0.96875 +vt 0.859375 0.96875 +vn 0.25881904510252074 0 -0.9659258262890683 +vn 0.9659258262890683 0 0.25881904510252074 +vn -0.25881904510252074 0 0.9659258262890683 +vn -0.9659258262890683 0 -0.25881904510252074 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o tail +v 0.09375 0.2953436652829704 0.5870259660350201 +v 0.09375 0.33809618319867885 0.4695643884367815 +v 0.09375 0.11915129888561249 0.5228971891614571 +v 0.09375 0.16190381680132115 0.4054356115632185 +v -0.09375 0.33809618319867885 0.4695643884367815 +v -0.09375 0.2953436652829704 0.5870259660350201 +v -0.09375 0.16190381680132115 0.4054356115632185 +v -0.09375 0.11915129888561249 0.5228971891614571 +vt 0.890625 0.75 +vt 0.84375 0.75 +vt 0.84375 0.65625 +vt 0.890625 0.65625 +vt 0.921875 0.75 +vt 0.890625 0.75 +vt 0.890625 0.65625 +vt 0.921875 0.65625 +vt 0.96875 0.75 +vt 0.921875 0.75 +vt 0.921875 0.65625 +vt 0.96875 0.65625 +vt 0.84375 0.75 +vt 0.8125 0.75 +vt 0.8125 0.65625 +vt 0.84375 0.65625 +vt 0.84375 0.75 +vt 0.890625 0.75 +vt 0.890625 0.8125 +vt 0.84375 0.8125 +vt 0.890625 0.8125 +vt 0.9375 0.8125 +vt 0.9375 0.75 +vt 0.890625 0.75 +vn 0 0.34202014332566866 -0.9396926207859084 +vn 1 0 0 +vn 0 -0.34202014332566866 0.9396926207859084 +vn -1 0 0 +vn 0 0.9396926207859084 0.34202014332566866 +vn 0 -0.9396926207859084 -0.34202014332566866 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o nose +v 0.03125 0.65625 -0.34375 +v 0.03125 0.65625 -0.40625 +v 0.03125 0.59375 -0.34375 +v 0.03125 0.59375 -0.40625 +v -0.03125 0.65625 -0.40625 +v -0.03125 0.65625 -0.34375 +v -0.03125 0.59375 -0.40625 +v -0.03125 0.59375 -0.34375 +vt 0.53125 0.6875 +vt 0.515625 0.6875 +vt 0.515625 0.65625 +vt 0.53125 0.65625 +vt 0.546875 0.6875 +vt 0.53125 0.6875 +vt 0.53125 0.65625 +vt 0.546875 0.65625 +vt 0.5625 0.6875 +vt 0.546875 0.6875 +vt 0.546875 0.65625 +vt 0.5625 0.65625 +vt 0.515625 0.6875 +vt 0.5 0.6875 +vt 0.5 0.65625 +vt 0.515625 0.65625 +vt 0.515625 0.6875 +vt 0.53125 0.6875 +vt 0.53125 0.71875 +vt 0.515625 0.71875 +vt 0.53125 0.71875 +vt 0.546875 0.71875 +vt 0.546875 0.6875 +vt 0.53125 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_a200089f-c989-c3f8-c313-2c45240e78d7 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/sheep.obj b/renderer/viewer/three/entity/models/sheep.obj new file mode 100644 index 00000000..66a2175f --- /dev/null +++ b/renderer/viewer/three/entity/models/sheep.obj @@ -0,0 +1,555 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.25 1.1250000000000002 -0.5 +v 0.25 0.7500000000000002 -0.5 +v 0.25 1.125 0.5 +v 0.25 0.75 0.4999999999999999 +v -0.25 0.7500000000000002 -0.5 +v -0.25 1.1250000000000002 -0.5 +v -0.25 0.75 0.4999999999999999 +v -0.25 1.125 0.5 +vt 0.53125 0.78125 +vt 0.65625 0.78125 +vt 0.65625 0.53125 +vt 0.53125 0.53125 +vt 0.4375 0.78125 +vt 0.53125 0.78125 +vt 0.53125 0.53125 +vt 0.4375 0.53125 +vt 0.75 0.78125 +vt 0.875 0.78125 +vt 0.875 0.53125 +vt 0.75 0.53125 +vt 0.65625 0.78125 +vt 0.75 0.78125 +vt 0.75 0.53125 +vt 0.65625 0.53125 +vt 0.65625 0.78125 +vt 0.53125 0.78125 +vt 0.53125 0.875 +vt 0.65625 0.875 +vt 0.78125 0.875 +vt 0.65625 0.875 +vt 0.65625 0.78125 +vt 0.78125 0.78125 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.359375 1.2343750000000002 -0.609375 +v 0.359375 0.6406250000000002 -0.609375 +v 0.359375 1.234375 0.609375 +v 0.359375 0.640625 0.609375 +v -0.359375 0.6406250000000002 -0.609375 +v -0.359375 1.2343750000000002 -0.609375 +v -0.359375 0.640625 0.609375 +v -0.359375 1.234375 0.609375 +vt 0.53125 0.28125 +vt 0.65625 0.28125 +vt 0.65625 0.03125 +vt 0.53125 0.03125 +vt 0.4375 0.28125 +vt 0.53125 0.28125 +vt 0.53125 0.03125 +vt 0.4375 0.03125 +vt 0.75 0.28125 +vt 0.875 0.28125 +vt 0.875 0.03125 +vt 0.75 0.03125 +vt 0.65625 0.28125 +vt 0.75 0.28125 +vt 0.75 0.03125 +vt 0.65625 0.03125 +vt 0.65625 0.28125 +vt 0.53125 0.28125 +vt 0.53125 0.375 +vt 0.65625 0.375 +vt 0.78125 0.375 +vt 0.65625 0.375 +vt 0.65625 0.28125 +vt 0.78125 0.28125 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.1875 1.375 -0.375 +v 0.1875 1.375 -0.875 +v 0.1875 1 -0.375 +v 0.1875 1 -0.875 +v -0.1875 1.375 -0.875 +v -0.1875 1.375 -0.375 +v -0.1875 1 -0.875 +v -0.1875 1 -0.375 +vt 0.125 0.875 +vt 0.21875 0.875 +vt 0.21875 0.78125 +vt 0.125 0.78125 +vt 0 0.875 +vt 0.125 0.875 +vt 0.125 0.78125 +vt 0 0.78125 +vt 0.34375 0.875 +vt 0.4375 0.875 +vt 0.4375 0.78125 +vt 0.34375 0.78125 +vt 0.21875 0.875 +vt 0.34375 0.875 +vt 0.34375 0.78125 +vt 0.21875 0.78125 +vt 0.21875 0.875 +vt 0.125 0.875 +vt 0.125 1 +vt 0.21875 1 +vt 0.3125 1 +vt 0.21875 1 +vt 0.21875 0.875 +vt 0.3125 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.22499999999999998 1.4125 -0.3375 +v 0.22499999999999998 1.4125 -0.7875 +v 0.22499999999999998 0.9624999999999999 -0.3375 +v 0.22499999999999998 0.9624999999999999 -0.7875 +v -0.22499999999999998 1.4125 -0.7875 +v -0.22499999999999998 1.4125 -0.3375 +v -0.22499999999999998 0.9624999999999999 -0.7875 +v -0.22499999999999998 0.9624999999999999 -0.3375 +vt 0.09375 0.40625 +vt 0.1875 0.40625 +vt 0.1875 0.3125 +vt 0.09375 0.3125 +vt 0 0.40625 +vt 0.09375 0.40625 +vt 0.09375 0.3125 +vt 0 0.3125 +vt 0.28125 0.40625 +vt 0.375 0.40625 +vt 0.375 0.3125 +vt 0.28125 0.3125 +vt 0.1875 0.40625 +vt 0.28125 0.40625 +vt 0.28125 0.3125 +vt 0.1875 0.3125 +vt 0.1875 0.40625 +vt 0.09375 0.40625 +vt 0.09375 0.5 +vt 0.1875 0.5 +vt 0.28125 0.5 +vt 0.1875 0.5 +vt 0.1875 0.40625 +vt 0.28125 0.40625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o leg0 +v 0.3125 0.75 0.5625 +v 0.3125 0.75 0.3125 +v 0.3125 0 0.5625 +v 0.3125 0 0.3125 +v 0.0625 0.75 0.3125 +v 0.0625 0.75 0.5625 +v 0.0625 0 0.3125 +v 0.0625 0 0.5625 +vt 0.0625 0.6875 +vt 0.125 0.6875 +vt 0.125 0.5 +vt 0.0625 0.5 +vt 0 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.1875 0.6875 +vt 0.25 0.6875 +vt 0.25 0.5 +vt 0.1875 0.5 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.75 +vt 0.125 0.75 +vt 0.1875 0.75 +vt 0.125 0.75 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leg0 +v 0.34375 0.78125 0.59375 +v 0.34375 0.78125 0.28125 +v 0.34375 0.34375 0.59375 +v 0.34375 0.34375 0.28125 +v 0.03125 0.78125 0.28125 +v 0.03125 0.78125 0.59375 +v 0.03125 0.34375 0.28125 +v 0.03125 0.34375 0.59375 +vt 0.0625 0.1875 +vt 0.125 0.1875 +vt 0.125 0.09375 +vt 0.0625 0.09375 +vt 0 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.09375 +vt 0 0.09375 +vt 0.1875 0.1875 +vt 0.25 0.1875 +vt 0.25 0.09375 +vt 0.1875 0.09375 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0.09375 +vt 0.125 0.09375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.25 +vt 0.125 0.25 +vt 0.1875 0.25 +vt 0.125 0.25 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o leg1 +v -0.0625 0.75 0.5625 +v -0.0625 0.75 0.3125 +v -0.0625 0 0.5625 +v -0.0625 0 0.3125 +v -0.3125 0.75 0.3125 +v -0.3125 0.75 0.5625 +v -0.3125 0 0.3125 +v -0.3125 0 0.5625 +vt 0.0625 0.6875 +vt 0.125 0.6875 +vt 0.125 0.5 +vt 0.0625 0.5 +vt 0 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.1875 0.6875 +vt 0.25 0.6875 +vt 0.25 0.5 +vt 0.1875 0.5 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.75 +vt 0.125 0.75 +vt 0.1875 0.75 +vt 0.125 0.75 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o leg1 +v -0.03125 0.78125 0.59375 +v -0.03125 0.78125 0.28125 +v -0.03125 0.34375 0.59375 +v -0.03125 0.34375 0.28125 +v -0.34375 0.78125 0.28125 +v -0.34375 0.78125 0.59375 +v -0.34375 0.34375 0.28125 +v -0.34375 0.34375 0.59375 +vt 0.0625 0.1875 +vt 0.125 0.1875 +vt 0.125 0.09375 +vt 0.0625 0.09375 +vt 0 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.09375 +vt 0 0.09375 +vt 0.1875 0.1875 +vt 0.25 0.1875 +vt 0.25 0.09375 +vt 0.1875 0.09375 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0.09375 +vt 0.125 0.09375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.25 +vt 0.125 0.25 +vt 0.1875 0.25 +vt 0.125 0.25 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o leg2 +v 0.3125 0.75 -0.1875 +v 0.3125 0.75 -0.4375 +v 0.3125 0 -0.1875 +v 0.3125 0 -0.4375 +v 0.0625 0.75 -0.4375 +v 0.0625 0.75 -0.1875 +v 0.0625 0 -0.4375 +v 0.0625 0 -0.1875 +vt 0.0625 0.6875 +vt 0.125 0.6875 +vt 0.125 0.5 +vt 0.0625 0.5 +vt 0 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.1875 0.6875 +vt 0.25 0.6875 +vt 0.25 0.5 +vt 0.1875 0.5 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.75 +vt 0.125 0.75 +vt 0.1875 0.75 +vt 0.125 0.75 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o leg2 +v 0.34375 0.78125 -0.15625 +v 0.34375 0.78125 -0.46875 +v 0.34375 0.34375 -0.15625 +v 0.34375 0.34375 -0.46875 +v 0.03125 0.78125 -0.46875 +v 0.03125 0.78125 -0.15625 +v 0.03125 0.34375 -0.46875 +v 0.03125 0.34375 -0.15625 +vt 0.0625 0.1875 +vt 0.125 0.1875 +vt 0.125 0.09375 +vt 0.0625 0.09375 +vt 0 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.09375 +vt 0 0.09375 +vt 0.1875 0.1875 +vt 0.25 0.1875 +vt 0.25 0.09375 +vt 0.1875 0.09375 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0.09375 +vt 0.125 0.09375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.25 +vt 0.125 0.25 +vt 0.1875 0.25 +vt 0.125 0.25 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o leg3 +v -0.0625 0.75 -0.1875 +v -0.0625 0.75 -0.4375 +v -0.0625 0 -0.1875 +v -0.0625 0 -0.4375 +v -0.3125 0.75 -0.4375 +v -0.3125 0.75 -0.1875 +v -0.3125 0 -0.4375 +v -0.3125 0 -0.1875 +vt 0.0625 0.6875 +vt 0.125 0.6875 +vt 0.125 0.5 +vt 0.0625 0.5 +vt 0 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.1875 0.6875 +vt 0.25 0.6875 +vt 0.25 0.5 +vt 0.1875 0.5 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vt 0.1875 0.5 +vt 0.125 0.5 +vt 0.125 0.6875 +vt 0.0625 0.6875 +vt 0.0625 0.75 +vt 0.125 0.75 +vt 0.1875 0.75 +vt 0.125 0.75 +vt 0.125 0.6875 +vt 0.1875 0.6875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o leg3 +v -0.03125 0.78125 -0.15625 +v -0.03125 0.78125 -0.46875 +v -0.03125 0.34375 -0.15625 +v -0.03125 0.34375 -0.46875 +v -0.34375 0.78125 -0.46875 +v -0.34375 0.78125 -0.15625 +v -0.34375 0.34375 -0.46875 +v -0.34375 0.34375 -0.15625 +vt 0.0625 0.1875 +vt 0.125 0.1875 +vt 0.125 0.09375 +vt 0.0625 0.09375 +vt 0 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.09375 +vt 0 0.09375 +vt 0.1875 0.1875 +vt 0.25 0.1875 +vt 0.25 0.09375 +vt 0.1875 0.09375 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0.09375 +vt 0.125 0.09375 +vt 0.125 0.1875 +vt 0.0625 0.1875 +vt 0.0625 0.25 +vt 0.125 0.25 +vt 0.1875 0.25 +vt 0.125 0.25 +vt 0.125 0.1875 +vt 0.1875 0.1875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_801e22d5-c057-fde3-2edb-c11e082736eb +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/shulker.obj b/renderer/viewer/three/entity/models/shulker.obj new file mode 100644 index 00000000..0287fddb --- /dev/null +++ b/renderer/viewer/three/entity/models/shulker.obj @@ -0,0 +1,141 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o base +v 0.5 0.5 0.5 +v 0.5 0.5 -0.5 +v 0.5 0 0.5 +v 0.5 0 -0.5 +v -0.5 0.5 -0.5 +v -0.5 0.5 0.5 +v -0.5 0 -0.5 +v -0.5 0 0.5 +vt 0.25 0.3125 +vt 0.5 0.3125 +vt 0.5 0.1875 +vt 0.25 0.1875 +vt 0 0.3125 +vt 0.25 0.3125 +vt 0.25 0.1875 +vt 0 0.1875 +vt 0.75 0.3125 +vt 1 0.3125 +vt 1 0.1875 +vt 0.75 0.1875 +vt 0.5 0.3125 +vt 0.75 0.3125 +vt 0.75 0.1875 +vt 0.5 0.1875 +vt 0.5 0.3125 +vt 0.25 0.3125 +vt 0.25 0.5625 +vt 0.5 0.5625 +vt 0.75 0.5625 +vt 0.5 0.5625 +vt 0.5 0.3125 +vt 0.75 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1c25eeea-87e5-6114-747e-b8cdb1a56a09 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o lid +v 0.5 1.5625 0.5 +v 0.5 1.5625 -0.5 +v 0.5 0.8125 0.5 +v 0.5 0.8125 -0.5 +v -0.5 1.5625 -0.5 +v -0.5 1.5625 0.5 +v -0.5 0.8125 -0.5 +v -0.5 0.8125 0.5 +vt 0.25 0.75 +vt 0.5 0.75 +vt 0.5 0.5625 +vt 0.25 0.5625 +vt 0 0.75 +vt 0.25 0.75 +vt 0.25 0.5625 +vt 0 0.5625 +vt 0.75 0.75 +vt 1 0.75 +vt 1 0.5625 +vt 0.75 0.5625 +vt 0.5 0.75 +vt 0.75 0.75 +vt 0.75 0.5625 +vt 0.5 0.5625 +vt 0.5 0.75 +vt 0.25 0.75 +vt 0.25 1 +vt 0.5 1 +vt 0.75 1 +vt 0.5 1 +vt 0.5 0.75 +vt 0.75 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1c25eeea-87e5-6114-747e-b8cdb1a56a09 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.1875 0.75 0.1875 +v 0.1875 0.75 -0.1875 +v 0.1875 0.375 0.1875 +v 0.1875 0.375 -0.1875 +v -0.1875 0.75 -0.1875 +v -0.1875 0.75 0.1875 +v -0.1875 0.375 -0.1875 +v -0.1875 0.375 0.1875 +vt 0.09375 0.09375 +vt 0.1875 0.09375 +vt 0.1875 0 +vt 0.09375 0 +vt 0 0.09375 +vt 0.09375 0.09375 +vt 0.09375 0 +vt 0 0 +vt 0.28125 0.09375 +vt 0.375 0.09375 +vt 0.375 0 +vt 0.28125 0 +vt 0.1875 0.09375 +vt 0.28125 0.09375 +vt 0.28125 0 +vt 0.1875 0 +vt 0.1875 0.09375 +vt 0.09375 0.09375 +vt 0.09375 0.1875 +vt 0.1875 0.1875 +vt 0.28125 0.1875 +vt 0.1875 0.1875 +vt 0.1875 0.09375 +vt 0.28125 0.09375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_1c25eeea-87e5-6114-747e-b8cdb1a56a09 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/sniffer.obj b/renderer/viewer/three/entity/models/sniffer.obj new file mode 100644 index 00000000..6fe8ed15 --- /dev/null +++ b/renderer/viewer/three/entity/models/sniffer.obj @@ -0,0 +1,693 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.8125 2.09375 1.28125 +v 0.8125 2.09375 -1.28125 +v 0.8125 0.53125 1.28125 +v 0.8125 0.53125 -1.28125 +v -0.8125 2.09375 -1.28125 +v -0.8125 2.09375 1.28125 +v -0.8125 0.53125 -1.28125 +v -0.8125 0.53125 1.28125 +vt 0.53125 0.7916666666666666 +vt 0.6614583333333334 0.7916666666666666 +vt 0.6614583333333334 0.6666666666666667 +vt 0.53125 0.6666666666666667 +vt 0.3229166666666667 0.7916666666666666 +vt 0.53125 0.7916666666666666 +vt 0.53125 0.6666666666666667 +vt 0.3229166666666667 0.6666666666666667 +vt 0.8697916666666666 0.7916666666666666 +vt 1 0.7916666666666666 +vt 1 0.6666666666666667 +vt 0.8697916666666666 0.6666666666666667 +vt 0.6614583333333334 0.7916666666666666 +vt 0.8697916666666666 0.7916666666666666 +vt 0.8697916666666666 0.6666666666666667 +vt 0.6614583333333334 0.6666666666666667 +vt 0.6614583333333334 0.7916666666666666 +vt 0.53125 0.7916666666666666 +vt 0.53125 1 +vt 0.6614583333333334 1 +vt 0.7916666666666666 1 +vt 0.6614583333333334 1 +vt 0.6614583333333334 0.7916666666666666 +vt 0.7916666666666666 0.7916666666666666 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.78125 2.0625 1.25 +v 0.78125 2.0625 -1.25 +v 0.78125 0.25 1.25 +v 0.78125 0.25 -1.25 +v -0.78125 2.0625 -1.25 +v -0.78125 2.0625 1.25 +v -0.78125 0.25 -1.25 +v -0.78125 0.25 1.25 +vt 0.53125 0.4375 +vt 0.6614583333333334 0.4375 +vt 0.6614583333333334 0.28645833333333337 +vt 0.53125 0.28645833333333337 +vt 0.3229166666666667 0.4375 +vt 0.53125 0.4375 +vt 0.53125 0.28645833333333337 +vt 0.3229166666666667 0.28645833333333337 +vt 0.8697916666666666 0.4375 +vt 1 0.4375 +vt 1 0.28645833333333337 +vt 0.8697916666666666 0.28645833333333337 +vt 0.6614583333333334 0.4375 +vt 0.8697916666666666 0.4375 +vt 0.8697916666666666 0.28645833333333337 +vt 0.6614583333333334 0.28645833333333337 +vt 0.6614583333333334 0.4375 +vt 0.53125 0.4375 +vt 0.53125 0.6458333333333333 +vt 0.6614583333333334 0.6458333333333333 +vt 0.7916666666666666 0.6458333333333333 +vt 0.6614583333333334 0.6458333333333333 +vt 0.6614583333333334 0.4375 +vt 0.7916666666666666 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o body +v 0.78125 0.5 1.25 +v 0.78125 0.5 -1.25 +v 0.78125 0.5 1.25 +v 0.78125 0.5 -1.25 +v -0.78125 0.5 -1.25 +v -0.78125 0.5 1.25 +v -0.78125 0.5 -1.25 +v -0.78125 0.5 1.25 +vt 0.6614583333333334 0.4375 +vt 0.7916666666666666 0.4375 +vt 0.7916666666666666 0.4375 +vt 0.6614583333333334 0.4375 +vt 0.453125 0.4375 +vt 0.6614583333333334 0.4375 +vt 0.6614583333333334 0.4375 +vt 0.453125 0.4375 +vt 1 0.4375 +vt 1.1302083333333333 0.4375 +vt 1.1302083333333333 0.4375 +vt 1 0.4375 +vt 0.7916666666666666 0.4375 +vt 1 0.4375 +vt 1 0.4375 +vt 0.7916666666666666 0.4375 +vt 0.7916666666666666 0.4375 +vt 0.6614583333333334 0.4375 +vt 0.6614583333333334 0.6458333333333333 +vt 0.7916666666666666 0.6458333333333333 +vt 0.921875 0.6458333333333333 +vt 0.7916666666666666 0.6458333333333333 +vt 0.7916666666666666 0.4375 +vt 0.921875 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.40625 1.3125 -1.24375 +v 0.40625 1.3125 -1.93125 +v 0.40625 0.1875 -1.24375 +v 0.40625 0.1875 -1.93125 +v -0.40625 1.3125 -1.93125 +v -0.40625 1.3125 -1.24375 +v -0.40625 0.1875 -1.93125 +v -0.40625 0.1875 -1.24375 +vt 0.09895833333333333 0.8645833333333334 +vt 0.16666666666666666 0.8645833333333334 +vt 0.16666666666666666 0.7708333333333334 +vt 0.09895833333333333 0.7708333333333334 +vt 0.041666666666666664 0.8645833333333334 +vt 0.09895833333333333 0.8645833333333334 +vt 0.09895833333333333 0.7708333333333334 +vt 0.041666666666666664 0.7708333333333334 +vt 0.22395833333333334 0.8645833333333334 +vt 0.2916666666666667 0.8645833333333334 +vt 0.2916666666666667 0.7708333333333334 +vt 0.22395833333333334 0.7708333333333334 +vt 0.16666666666666666 0.8645833333333334 +vt 0.22395833333333334 0.8645833333333334 +vt 0.22395833333333334 0.7708333333333334 +vt 0.16666666666666666 0.7708333333333334 +vt 0.16666666666666666 0.8645833333333334 +vt 0.09895833333333333 0.8645833333333334 +vt 0.09895833333333333 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.234375 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.16666666666666666 0.8645833333333334 +vt 0.234375 0.8645833333333334 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o head +v 0.40625 0.375 -1.24375 +v 0.40625 0.375 -1.93125 +v 0.40625 0.375 -1.24375 +v 0.40625 0.375 -1.93125 +v -0.40625 0.375 -1.93125 +v -0.40625 0.375 -1.24375 +v -0.40625 0.375 -1.93125 +v -0.40625 0.375 -1.24375 +vt 0.09895833333333333 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.09895833333333333 0.921875 +vt 0.041666666666666664 0.921875 +vt 0.09895833333333333 0.921875 +vt 0.09895833333333333 0.921875 +vt 0.041666666666666664 0.921875 +vt 0.22395833333333334 0.921875 +vt 0.2916666666666667 0.921875 +vt 0.2916666666666667 0.921875 +vt 0.22395833333333334 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.22395833333333334 0.921875 +vt 0.22395833333333334 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.16666666666666666 0.921875 +vt 0.09895833333333333 0.921875 +vt 0.09895833333333333 0.9791666666666666 +vt 0.16666666666666666 0.9791666666666666 +vt 0.234375 0.9791666666666666 +vt 0.16666666666666666 0.9791666666666666 +vt 0.16666666666666666 0.921875 +vt 0.234375 0.921875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o left_ear +v -0.4 1.3125 -1.24375 +v -0.4 1.3125 -1.68125 +v -0.4 0.125 -1.24375 +v -0.4 0.125 -1.68125 +v -0.4625 1.3125 -1.68125 +v -0.4625 1.3125 -1.24375 +v -0.4625 0.125 -1.68125 +v -0.4625 0.125 -1.24375 +vt 0.046875 0.9635416666666666 +vt 0.052083333333333336 0.9635416666666666 +vt 0.052083333333333336 0.8645833333333334 +vt 0.046875 0.8645833333333334 +vt 0.010416666666666666 0.9635416666666666 +vt 0.046875 0.9635416666666666 +vt 0.046875 0.8645833333333334 +vt 0.010416666666666666 0.8645833333333334 +vt 0.08854166666666667 0.9635416666666666 +vt 0.09375 0.9635416666666666 +vt 0.09375 0.8645833333333334 +vt 0.08854166666666667 0.8645833333333334 +vt 0.052083333333333336 0.9635416666666666 +vt 0.08854166666666667 0.9635416666666666 +vt 0.08854166666666667 0.8645833333333334 +vt 0.052083333333333336 0.8645833333333334 +vt 0.052083333333333336 0.9635416666666666 +vt 0.046875 0.9635416666666666 +vt 0.046875 1 +vt 0.052083333333333336 1 +vt 0.057291666666666664 1 +vt 0.052083333333333336 1 +vt 0.052083333333333336 0.9635416666666666 +vt 0.057291666666666664 0.9635416666666666 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o right_ear +v 0.4625 1.3125 -1.24375 +v 0.4625 1.3125 -1.68125 +v 0.4625 0.125 -1.24375 +v 0.4625 0.125 -1.68125 +v 0.4 1.3125 -1.68125 +v 0.4 1.3125 -1.24375 +v 0.4 0.125 -1.68125 +v 0.4 0.125 -1.24375 +vt 0.2864583333333333 0.9635416666666666 +vt 0.2916666666666667 0.9635416666666666 +vt 0.2916666666666667 0.8645833333333334 +vt 0.2864583333333333 0.8645833333333334 +vt 0.25 0.9635416666666666 +vt 0.2864583333333333 0.9635416666666666 +vt 0.2864583333333333 0.8645833333333334 +vt 0.25 0.8645833333333334 +vt 0.328125 0.9635416666666666 +vt 0.3333333333333333 0.9635416666666666 +vt 0.3333333333333333 0.8645833333333334 +vt 0.328125 0.8645833333333334 +vt 0.2916666666666667 0.9635416666666666 +vt 0.328125 0.9635416666666666 +vt 0.328125 0.8645833333333334 +vt 0.2916666666666667 0.8645833333333334 +vt 0.2916666666666667 0.9635416666666666 +vt 0.2864583333333333 0.9635416666666666 +vt 0.2864583333333333 1 +vt 0.2916666666666667 1 +vt 0.296875 1 +vt 0.2916666666666667 1 +vt 0.2916666666666667 0.9635416666666666 +vt 0.296875 0.9635416666666666 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o nose +v 0.40625 1.25 -1.93125 +v 0.40625 1.25 -2.49375 +v 0.40625 1.125 -1.93125 +v 0.40625 1.125 -2.49375 +v -0.40625 1.25 -2.49375 +v -0.40625 1.25 -1.93125 +v -0.40625 1.125 -2.49375 +v -0.40625 1.125 -1.93125 +vt 0.09895833333333333 0.71875 +vt 0.16666666666666666 0.71875 +vt 0.16666666666666666 0.7083333333333333 +vt 0.09895833333333333 0.7083333333333333 +vt 0.052083333333333336 0.71875 +vt 0.09895833333333333 0.71875 +vt 0.09895833333333333 0.7083333333333333 +vt 0.052083333333333336 0.7083333333333333 +vt 0.21354166666666666 0.71875 +vt 0.28125 0.71875 +vt 0.28125 0.7083333333333333 +vt 0.21354166666666666 0.7083333333333333 +vt 0.16666666666666666 0.71875 +vt 0.21354166666666666 0.71875 +vt 0.21354166666666666 0.7083333333333333 +vt 0.16666666666666666 0.7083333333333333 +vt 0.16666666666666666 0.71875 +vt 0.09895833333333333 0.71875 +vt 0.09895833333333333 0.765625 +vt 0.16666666666666666 0.765625 +vt 0.234375 0.765625 +vt 0.16666666666666666 0.765625 +vt 0.16666666666666666 0.71875 +vt 0.234375 0.71875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o lower_beak +v 0.40625 1.125 -1.93125 +v 0.40625 1.125 -2.49375 +v 0.40625 0.375 -1.93125 +v 0.40625 0.375 -2.49375 +v -0.40625 1.125 -2.49375 +v -0.40625 1.125 -1.93125 +v -0.40625 0.375 -2.49375 +v -0.40625 0.375 -1.93125 +vt 0.09895833333333333 0.65625 +vt 0.16666666666666666 0.65625 +vt 0.16666666666666666 0.59375 +vt 0.09895833333333333 0.59375 +vt 0.052083333333333336 0.65625 +vt 0.09895833333333333 0.65625 +vt 0.09895833333333333 0.59375 +vt 0.052083333333333336 0.59375 +vt 0.21354166666666666 0.65625 +vt 0.28125 0.65625 +vt 0.28125 0.59375 +vt 0.21354166666666666 0.59375 +vt 0.16666666666666666 0.65625 +vt 0.21354166666666666 0.65625 +vt 0.21354166666666666 0.59375 +vt 0.16666666666666666 0.59375 +vt 0.16666666666666666 0.65625 +vt 0.09895833333333333 0.65625 +vt 0.09895833333333333 0.703125 +vt 0.16666666666666666 0.703125 +vt 0.234375 0.703125 +vt 0.16666666666666666 0.703125 +vt 0.16666666666666666 0.65625 +vt 0.234375 0.65625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o right_front_leg +v 0.6875 0.625 -0.6875 +v 0.6875 0.625 -1.1875 +v 0.6875 0 -0.6875 +v 0.6875 0 -1.1875 +v 0.25 0.625 -1.1875 +v 0.25 0.625 -0.6875 +v 0.25 0 -1.1875 +v 0.25 0 -0.6875 +vt 0.20833333333333334 0.5052083333333333 +vt 0.24479166666666666 0.5052083333333333 +vt 0.24479166666666666 0.453125 +vt 0.20833333333333334 0.453125 +vt 0.16666666666666666 0.5052083333333333 +vt 0.20833333333333334 0.5052083333333333 +vt 0.20833333333333334 0.453125 +vt 0.16666666666666666 0.453125 +vt 0.2864583333333333 0.5052083333333333 +vt 0.3229166666666667 0.5052083333333333 +vt 0.3229166666666667 0.453125 +vt 0.2864583333333333 0.453125 +vt 0.24479166666666666 0.5052083333333333 +vt 0.2864583333333333 0.5052083333333333 +vt 0.2864583333333333 0.453125 +vt 0.24479166666666666 0.453125 +vt 0.24479166666666666 0.5052083333333333 +vt 0.20833333333333334 0.5052083333333333 +vt 0.20833333333333334 0.546875 +vt 0.24479166666666666 0.546875 +vt 0.28125 0.546875 +vt 0.24479166666666666 0.546875 +vt 0.24479166666666666 0.5052083333333333 +vt 0.28125 0.5052083333333333 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o right_mid_leg +v 0.6875 0.625 0.25 +v 0.6875 0.625 -0.25 +v 0.6875 0 0.25 +v 0.6875 0 -0.25 +v 0.25 0.625 -0.25 +v 0.25 0.625 0.25 +v 0.25 0 -0.25 +v 0.25 0 0.25 +vt 0.20833333333333334 0.41145833333333337 +vt 0.24479166666666666 0.41145833333333337 +vt 0.24479166666666666 0.359375 +vt 0.20833333333333334 0.359375 +vt 0.16666666666666666 0.41145833333333337 +vt 0.20833333333333334 0.41145833333333337 +vt 0.20833333333333334 0.359375 +vt 0.16666666666666666 0.359375 +vt 0.2864583333333333 0.41145833333333337 +vt 0.3229166666666667 0.41145833333333337 +vt 0.3229166666666667 0.359375 +vt 0.2864583333333333 0.359375 +vt 0.24479166666666666 0.41145833333333337 +vt 0.2864583333333333 0.41145833333333337 +vt 0.2864583333333333 0.359375 +vt 0.24479166666666666 0.359375 +vt 0.24479166666666666 0.41145833333333337 +vt 0.20833333333333334 0.41145833333333337 +vt 0.20833333333333334 0.453125 +vt 0.24479166666666666 0.453125 +vt 0.28125 0.453125 +vt 0.24479166666666666 0.453125 +vt 0.24479166666666666 0.41145833333333337 +vt 0.28125 0.41145833333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o right_hind_leg +v 0.6875 0.625 1.1875 +v 0.6875 0.625 0.6875 +v 0.6875 0 1.1875 +v 0.6875 0 0.6875 +v 0.25 0.625 0.6875 +v 0.25 0.625 1.1875 +v 0.25 0 0.6875 +v 0.25 0 1.1875 +vt 0.20833333333333334 0.31770833333333337 +vt 0.24479166666666666 0.31770833333333337 +vt 0.24479166666666666 0.265625 +vt 0.20833333333333334 0.265625 +vt 0.16666666666666666 0.31770833333333337 +vt 0.20833333333333334 0.31770833333333337 +vt 0.20833333333333334 0.265625 +vt 0.16666666666666666 0.265625 +vt 0.2864583333333333 0.31770833333333337 +vt 0.3229166666666667 0.31770833333333337 +vt 0.3229166666666667 0.265625 +vt 0.2864583333333333 0.265625 +vt 0.24479166666666666 0.31770833333333337 +vt 0.2864583333333333 0.31770833333333337 +vt 0.2864583333333333 0.265625 +vt 0.24479166666666666 0.265625 +vt 0.24479166666666666 0.31770833333333337 +vt 0.20833333333333334 0.31770833333333337 +vt 0.20833333333333334 0.359375 +vt 0.24479166666666666 0.359375 +vt 0.28125 0.359375 +vt 0.24479166666666666 0.359375 +vt 0.24479166666666666 0.31770833333333337 +vt 0.28125 0.31770833333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o left_front_leg +v -0.25 0.625 -0.6875 +v -0.25 0.625 -1.1875 +v -0.25 0 -0.6875 +v -0.25 0 -1.1875 +v -0.6875 0.625 -1.1875 +v -0.6875 0.625 -0.6875 +v -0.6875 0 -1.1875 +v -0.6875 0 -0.6875 +vt 0.041666666666666664 0.5052083333333333 +vt 0.078125 0.5052083333333333 +vt 0.078125 0.453125 +vt 0.041666666666666664 0.453125 +vt 0 0.5052083333333333 +vt 0.041666666666666664 0.5052083333333333 +vt 0.041666666666666664 0.453125 +vt 0 0.453125 +vt 0.11979166666666667 0.5052083333333333 +vt 0.15625 0.5052083333333333 +vt 0.15625 0.453125 +vt 0.11979166666666667 0.453125 +vt 0.078125 0.5052083333333333 +vt 0.11979166666666667 0.5052083333333333 +vt 0.11979166666666667 0.453125 +vt 0.078125 0.453125 +vt 0.078125 0.5052083333333333 +vt 0.041666666666666664 0.5052083333333333 +vt 0.041666666666666664 0.546875 +vt 0.078125 0.546875 +vt 0.11458333333333333 0.546875 +vt 0.078125 0.546875 +vt 0.078125 0.5052083333333333 +vt 0.11458333333333333 0.5052083333333333 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o left_mid_leg +v -0.25 0.625 0.25 +v -0.25 0.625 -0.25 +v -0.25 0 0.25 +v -0.25 0 -0.25 +v -0.6875 0.625 -0.25 +v -0.6875 0.625 0.25 +v -0.6875 0 -0.25 +v -0.6875 0 0.25 +vt 0.041666666666666664 0.41145833333333337 +vt 0.078125 0.41145833333333337 +vt 0.078125 0.359375 +vt 0.041666666666666664 0.359375 +vt 0 0.41145833333333337 +vt 0.041666666666666664 0.41145833333333337 +vt 0.041666666666666664 0.359375 +vt 0 0.359375 +vt 0.11979166666666667 0.41145833333333337 +vt 0.15625 0.41145833333333337 +vt 0.15625 0.359375 +vt 0.11979166666666667 0.359375 +vt 0.078125 0.41145833333333337 +vt 0.11979166666666667 0.41145833333333337 +vt 0.11979166666666667 0.359375 +vt 0.078125 0.359375 +vt 0.078125 0.41145833333333337 +vt 0.041666666666666664 0.41145833333333337 +vt 0.041666666666666664 0.453125 +vt 0.078125 0.453125 +vt 0.11458333333333333 0.453125 +vt 0.078125 0.453125 +vt 0.078125 0.41145833333333337 +vt 0.11458333333333333 0.41145833333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 +o left_hind_leg +v -0.25 0.625 1.1875 +v -0.25 0.625 0.6875 +v -0.25 0 1.1875 +v -0.25 0 0.6875 +v -0.6875 0.625 0.6875 +v -0.6875 0.625 1.1875 +v -0.6875 0 0.6875 +v -0.6875 0 1.1875 +vt 0.041666666666666664 0.31770833333333337 +vt 0.078125 0.31770833333333337 +vt 0.078125 0.265625 +vt 0.041666666666666664 0.265625 +vt 0 0.31770833333333337 +vt 0.041666666666666664 0.31770833333333337 +vt 0.041666666666666664 0.265625 +vt 0 0.265625 +vt 0.11979166666666667 0.31770833333333337 +vt 0.15625 0.31770833333333337 +vt 0.15625 0.265625 +vt 0.11979166666666667 0.265625 +vt 0.078125 0.31770833333333337 +vt 0.11979166666666667 0.31770833333333337 +vt 0.11979166666666667 0.265625 +vt 0.078125 0.265625 +vt 0.078125 0.31770833333333337 +vt 0.041666666666666664 0.31770833333333337 +vt 0.041666666666666664 0.359375 +vt 0.078125 0.359375 +vt 0.11458333333333333 0.359375 +vt 0.078125 0.359375 +vt 0.078125 0.31770833333333337 +vt 0.11458333333333333 0.31770833333333337 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_583c9f26-fb84-68dc-a891-67c1661cee33 +f 116/340/85 119/339/85 117/338/85 114/337/85 +f 115/344/86 116/343/86 114/342/86 113/341/86 +f 120/348/87 115/347/87 113/346/87 118/345/87 +f 119/352/88 120/351/88 118/350/88 117/349/88 +f 118/356/89 113/355/89 114/354/89 117/353/89 +f 119/360/90 116/359/90 115/358/90 120/357/90 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/spider.obj b/renderer/viewer/three/entity/models/spider.obj new file mode 100644 index 00000000..eb636839 --- /dev/null +++ b/renderer/viewer/three/entity/models/spider.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o head +v 0.25 0.8125 -0.1875 +v 0.25 0.8125 -0.6875 +v 0.25 0.3125 -0.1875 +v 0.25 0.3125 -0.6875 +v -0.25 0.8125 -0.6875 +v -0.25 0.8125 -0.1875 +v -0.25 0.3125 -0.6875 +v -0.25 0.3125 -0.1875 +vt 0.625 0.625 +vt 0.75 0.625 +vt 0.75 0.375 +vt 0.625 0.375 +vt 0.5 0.625 +vt 0.625 0.625 +vt 0.625 0.375 +vt 0.5 0.375 +vt 0.875 0.625 +vt 1 0.625 +vt 1 0.375 +vt 0.875 0.375 +vt 0.75 0.625 +vt 0.875 0.625 +vt 0.875 0.375 +vt 0.75 0.375 +vt 0.75 0.625 +vt 0.625 0.625 +vt 0.625 0.875 +vt 0.75 0.875 +vt 0.875 0.875 +vt 0.75 0.875 +vt 0.75 0.625 +vt 0.875 0.625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body0 +v 0.1875 0.75 0.1875 +v 0.1875 0.75 -0.1875 +v 0.1875 0.375 0.1875 +v 0.1875 0.375 -0.1875 +v -0.1875 0.75 -0.1875 +v -0.1875 0.75 0.1875 +v -0.1875 0.375 -0.1875 +v -0.1875 0.375 0.1875 +vt 0.09375 0.8125 +vt 0.1875 0.8125 +vt 0.1875 0.625 +vt 0.09375 0.625 +vt 0 0.8125 +vt 0.09375 0.8125 +vt 0.09375 0.625 +vt 0 0.625 +vt 0.28125 0.8125 +vt 0.375 0.8125 +vt 0.375 0.625 +vt 0.28125 0.625 +vt 0.1875 0.8125 +vt 0.28125 0.8125 +vt 0.28125 0.625 +vt 0.1875 0.625 +vt 0.1875 0.8125 +vt 0.09375 0.8125 +vt 0.09375 1 +vt 0.1875 1 +vt 0.28125 1 +vt 0.1875 1 +vt 0.1875 0.8125 +vt 0.28125 0.8125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o body1 +v 0.3125 0.8125 0.9375 +v 0.3125 0.8125 0.1875 +v 0.3125 0.3125 0.9375 +v 0.3125 0.3125 0.1875 +v -0.3125 0.8125 0.1875 +v -0.3125 0.8125 0.9375 +v -0.3125 0.3125 0.1875 +v -0.3125 0.3125 0.9375 +vt 0.1875 0.25 +vt 0.34375 0.25 +vt 0.34375 0 +vt 0.1875 0 +vt 0 0.25 +vt 0.1875 0.25 +vt 0.1875 0 +vt 0 0 +vt 0.53125 0.25 +vt 0.6875 0.25 +vt 0.6875 0 +vt 0.53125 0 +vt 0.34375 0.25 +vt 0.53125 0.25 +vt 0.53125 0 +vt 0.34375 0 +vt 0.34375 0.25 +vt 0.1875 0.25 +vt 0.1875 0.625 +vt 0.34375 0.625 +vt 0.5 0.625 +vt 0.34375 0.625 +vt 0.34375 0.25 +vt 0.5 0.25 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o leg0 +v 0.7316941738241591 0.16919417382415936 0.8321067811865475 +v 0.7941941738241591 0.10669417382415936 0.743718433538229 +v 0.6433058261758406 0.08080582617584087 0.8321067811865475 +v 0.7058058261758406 0.018305826175840867 0.743718433538229 +v 0.29419417382415913 0.6066941738241591 0.03661165235168151 +v 0.23169417382415913 0.6691941738241591 0.125 +v 0.20580582617584076 0.5183058261758409 0.03661165235168151 +v 0.14330582617584076 0.5808058261758409 0.125 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn 0.5 -0.5 -0.7071067811865476 +vn 0.5 -0.5 0.7071067811865477 +vn -0.5 0.5 0.7071067811865476 +vn -0.5 0.5 -0.7071067811865477 +vn 0.7071067811865477 0.7071067811865476 0 +vn -0.7071067811865477 -0.7071067811865476 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o leg1 +v -0.23169417382415913 0.6691941738241591 0.125 +v -0.29419417382415913 0.6066941738241591 0.03661165235168151 +v -0.14330582617584076 0.5808058261758409 0.125 +v -0.20580582617584076 0.5183058261758409 0.03661165235168151 +v -0.794194173824159 0.10669417382415936 0.743718433538229 +v -0.731694173824159 0.16919417382415936 0.8321067811865475 +v -0.7058058261758406 0.018305826175840867 0.743718433538229 +v -0.6433058261758406 0.08080582617584087 0.8321067811865475 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn -0.5 -0.5 -0.7071067811865476 +vn 0.5 0.5 -0.7071067811865477 +vn 0.5 0.5 0.7071067811865476 +vn -0.5 -0.5 0.7071067811865477 +vn -0.7071067811865477 0.7071067811865476 0 +vn 0.7071067811865477 -0.7071067811865476 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leg2 +v 1.0143853759373593 0.10357000946898909 0.36551321892668 +v 1.0408868946744412 0.08501344627527851 0.2447724906405464 +v 0.9426883213934785 0.0011760039328650995 0.36551321892668 +v 0.9691898401305603 -0.017380559260845474 0.2447724906405464 +v 0.24964677943821734 0.6390457394976019 -0.014046554461974337 +v 0.2231452607011355 0.6576023026913125 0.10669417382415924 +v 0.17794972489433658 0.5366517339614778 -0.014046554461974337 +v 0.15144820615725474 0.5552082971551884 0.10669417382415924 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn 0.21201214989665462 -0.1484525055496845 -0.9659258262890683 +vn 0.7912401152362238 -0.5540322932223234 0.25881904510252074 +vn -0.21201214989665462 0.1484525055496845 0.9659258262890683 +vn -0.7912401152362238 0.5540322932223234 -0.25881904510252074 +vn 0.5735764363510462 0.8191520442889919 1.387778780781446e-17 +vn -0.5735764363510462 -0.8191520442889919 -1.387778780781446e-17 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o leg3 +v -0.2231452607011355 0.6576023026913125 0.10669417382415924 +v -0.24964677943821734 0.6390457394976019 -0.014046554461974337 +v -0.15144820615725474 0.5552082971551884 0.10669417382415924 +v -0.17794972489433658 0.5366517339614778 -0.014046554461974337 +v -1.0408868946744412 0.08501344627527851 0.2447724906405464 +v -1.0143853759373593 0.10357000946898909 0.36551321892668 +v -0.9691898401305603 -0.017380559260845474 0.2447724906405464 +v -0.9426883213934785 0.0011760039328650995 0.36551321892668 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn -0.21201214989665462 -0.1484525055496845 -0.9659258262890683 +vn 0.7912401152362238 0.5540322932223234 -0.25881904510252074 +vn 0.21201214989665462 0.1484525055496845 0.9659258262890683 +vn -0.7912401152362238 -0.5540322932223234 0.25881904510252074 +vn -0.5735764363510462 0.8191520442889919 1.387778780781446e-17 +vn 0.5735764363510462 -0.8191520442889919 -1.387778780781446e-17 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o leg4 +v 1.0408868946744412 0.08501344627527851 -0.1822724906405464 +v 1.0143853759373593 0.10357000946898909 -0.30301321892668 +v 0.9691898401305603 -0.017380559260845474 -0.1822724906405464 +v 0.9426883213934785 0.0011760039328650995 -0.30301321892668 +v 0.2231452607011355 0.6576023026913125 -0.044194173824159244 +v 0.24964677943821734 0.6390457394976019 0.07654655446197434 +v 0.15144820615725474 0.5552082971551884 -0.044194173824159244 +v 0.17794972489433658 0.5366517339614778 0.07654655446197434 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn -0.21201214989665462 0.1484525055496845 -0.9659258262890683 +vn 0.7912401152362238 -0.5540322932223234 -0.25881904510252074 +vn 0.21201214989665462 -0.1484525055496845 0.9659258262890683 +vn -0.7912401152362238 0.5540322932223234 0.25881904510252074 +vn 0.5735764363510462 0.8191520442889919 -1.387778780781446e-17 +vn -0.5735764363510462 -0.8191520442889919 1.387778780781446e-17 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o leg5 +v -0.24964677943821734 0.6390457394976019 0.07654655446197434 +v -0.2231452607011355 0.6576023026913125 -0.044194173824159244 +v -0.17794972489433658 0.5366517339614778 0.07654655446197434 +v -0.15144820615725474 0.5552082971551884 -0.044194173824159244 +v -1.0143853759373593 0.10357000946898909 -0.30301321892668 +v -1.0408868946744412 0.08501344627527851 -0.1822724906405464 +v -0.9426883213934785 0.0011760039328650995 -0.30301321892668 +v -0.9691898401305603 -0.017380559260845474 -0.1822724906405464 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn 0.21201214989665462 0.1484525055496845 -0.9659258262890683 +vn 0.7912401152362238 0.5540322932223234 0.25881904510252074 +vn -0.21201214989665462 -0.1484525055496845 0.9659258262890683 +vn -0.7912401152362238 -0.5540322932223234 -0.25881904510252074 +vn -0.5735764363510462 0.8191520442889919 -1.387778780781446e-17 +vn 0.5735764363510462 -0.8191520442889919 1.387778780781446e-17 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o leg6 +v 0.7941941738241591 0.10669417382415936 -0.681218433538229 +v 0.7316941738241591 0.16919417382415936 -0.7696067811865475 +v 0.7058058261758406 0.018305826175840867 -0.681218433538229 +v 0.6433058261758406 0.08080582617584087 -0.7696067811865475 +v 0.23169417382415913 0.6691941738241591 -0.0625 +v 0.29419417382415913 0.6066941738241591 0.025888347648318488 +v 0.14330582617584076 0.5808058261758409 -0.0625 +v 0.20580582617584076 0.5183058261758409 0.025888347648318488 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn -0.5 0.5 -0.7071067811865476 +vn 0.5 -0.5 -0.7071067811865477 +vn 0.5 -0.5 0.7071067811865476 +vn -0.5 0.5 0.7071067811865477 +vn 0.7071067811865477 0.7071067811865476 0 +vn -0.7071067811865477 -0.7071067811865476 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o leg7 +v -0.29419417382415913 0.6066941738241591 0.025888347648318488 +v -0.23169417382415913 0.6691941738241591 -0.0625 +v -0.20580582617584076 0.5183058261758409 0.025888347648318488 +v -0.14330582617584076 0.5808058261758409 -0.0625 +v -0.731694173824159 0.16919417382415936 -0.7696067811865475 +v -0.794194173824159 0.10669417382415936 -0.681218433538229 +v -0.6433058261758406 0.08080582617584087 -0.7696067811865475 +v -0.7058058261758406 0.018305826175840867 -0.681218433538229 +vt 0.3125 0.9375 +vt 0.5625 0.9375 +vt 0.5625 0.875 +vt 0.3125 0.875 +vt 0.28125 0.9375 +vt 0.3125 0.9375 +vt 0.3125 0.875 +vt 0.28125 0.875 +vt 0.59375 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.875 +vt 0.59375 0.875 +vt 0.5625 0.9375 +vt 0.59375 0.9375 +vt 0.59375 0.875 +vt 0.5625 0.875 +vt 0.5625 0.9375 +vt 0.3125 0.9375 +vt 0.3125 1 +vt 0.5625 1 +vt 0.8125 1 +vt 0.5625 1 +vt 0.5625 0.9375 +vt 0.8125 0.9375 +vn 0.5 0.5 -0.7071067811865476 +vn 0.5 0.5 0.7071067811865477 +vn -0.5 -0.5 0.7071067811865476 +vn -0.5 -0.5 -0.7071067811865477 +vn -0.7071067811865477 0.7071067811865476 0 +vn 0.7071067811865477 -0.7071067811865476 0 +usemtl m_9855f90d-c537-3611-ecbb-abc7e6cc7b17 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/tadpole.obj b/renderer/viewer/three/entity/models/tadpole.obj new file mode 100644 index 00000000..b38c8a26 --- /dev/null +++ b/renderer/viewer/three/entity/models/tadpole.obj @@ -0,0 +1,95 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.09375 0.1875 0.03125 +v 0.09375 0.1875 -0.15625 +v 0.09375 0.0625 0.03125 +v 0.09375 0.0625 -0.15625 +v -0.09375 0.1875 -0.15625 +v -0.09375 0.1875 0.03125 +v -0.09375 0.0625 -0.15625 +v -0.09375 0.0625 0.03125 +vt 0.1875 0.8125 +vt 0.375 0.8125 +vt 0.375 0.6875 +vt 0.1875 0.6875 +vt 0 0.8125 +vt 0.1875 0.8125 +vt 0.1875 0.6875 +vt 0 0.6875 +vt 0.5625 0.8125 +vt 0.75 0.8125 +vt 0.75 0.6875 +vt 0.5625 0.6875 +vt 0.375 0.8125 +vt 0.5625 0.8125 +vt 0.5625 0.6875 +vt 0.375 0.6875 +vt 0.375 0.8125 +vt 0.1875 0.8125 +vt 0.1875 1 +vt 0.375 1 +vt 0.5625 1 +vt 0.375 1 +vt 0.375 0.8125 +vt 0.5625 0.8125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_71a3df64-a731-05a3-e848-ff24e21209ec +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o tail +v 0 0.1875 0.40625 +v 0 0.1875 -0.03125 +v 0 0.0625 0.40625 +v 0 0.0625 -0.03125 +v 0 0.1875 -0.03125 +v 0 0.1875 0.40625 +v 0 0.0625 -0.03125 +v 0 0.0625 0.40625 +vt 0.4375 0.5625 +vt 0.4375 0.5625 +vt 0.4375 0.4375 +vt 0.4375 0.4375 +vt 0 0.5625 +vt 0.4375 0.5625 +vt 0.4375 0.4375 +vt 0 0.4375 +vt 0.875 0.5625 +vt 0.875 0.5625 +vt 0.875 0.4375 +vt 0.875 0.4375 +vt 0.4375 0.5625 +vt 0.875 0.5625 +vt 0.875 0.4375 +vt 0.4375 0.4375 +vt 0.4375 0.5625 +vt 0.4375 0.5625 +vt 0.4375 1 +vt 0.4375 1 +vt 0.4375 1 +vt 0.4375 1 +vt 0.4375 0.5625 +vt 0.4375 0.5625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_71a3df64-a731-05a3-e848-ff24e21209ec +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/turtle.obj b/renderer/viewer/three/entity/models/turtle.obj new file mode 100644 index 00000000..ac0d41b9 --- /dev/null +++ b/renderer/viewer/three/entity/models/turtle.obj @@ -0,0 +1,371 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.59375 0.5625 -0.4375000000000001 +v 0.59375 0.1875 -0.4375000000000001 +v 0.59375 0.5624999999999996 0.8125 +v 0.59375 0.18749999999999956 0.8125 +v -0.59375 0.1875 -0.4375000000000001 +v -0.59375 0.5625 -0.4375000000000001 +v -0.59375 0.18749999999999956 0.8125 +v -0.59375 0.5624999999999996 0.8125 +vt 0.1015625 0.328125 +vt 0.25 0.328125 +vt 0.25 0.015625 +vt 0.1015625 0.015625 +vt 0.0546875 0.328125 +vt 0.1015625 0.328125 +vt 0.1015625 0.015625 +vt 0.0546875 0.015625 +vt 0.296875 0.328125 +vt 0.4453125 0.328125 +vt 0.4453125 0.015625 +vt 0.296875 0.015625 +vt 0.25 0.328125 +vt 0.296875 0.328125 +vt 0.296875 0.015625 +vt 0.25 0.015625 +vt 0.25 0.328125 +vt 0.1015625 0.328125 +vt 0.1015625 0.421875 +vt 0.25 0.421875 +vt 0.3984375 0.421875 +vt 0.25 0.421875 +vt 0.25 0.328125 +vt 0.3984375 0.328125 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.34375 0.1875 -0.4375000000000001 +v 0.34375 0 -0.4375000000000002 +v 0.34375 0.18749999999999978 0.6874999999999998 +v 0.34375 -2.220446049250313e-16 0.6874999999999998 +v -0.34375 0 -0.4375000000000002 +v -0.34375 0.1875 -0.4375000000000001 +v -0.34375 -2.220446049250313e-16 0.6874999999999998 +v -0.34375 0.18749999999999978 0.6874999999999998 +vt 0.265625 0.9375 +vt 0.3515625 0.9375 +vt 0.3515625 0.65625 +vt 0.265625 0.65625 +vt 0.2421875 0.9375 +vt 0.265625 0.9375 +vt 0.265625 0.65625 +vt 0.2421875 0.65625 +vt 0.375 0.9375 +vt 0.4609375 0.9375 +vt 0.4609375 0.65625 +vt 0.375 0.65625 +vt 0.3515625 0.9375 +vt 0.375 0.9375 +vt 0.375 0.65625 +vt 0.3515625 0.65625 +vt 0.3515625 0.9375 +vt 0.265625 0.9375 +vt 0.265625 0.984375 +vt 0.3515625 0.984375 +vt 0.4375 0.984375 +vt 0.3515625 0.984375 +vt 0.3515625 0.9375 +vt 0.4375 0.9375 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o eggbelly +v 0.28125 0 -0.4375000000000002 +v 0.28125 -0.0625 -0.4375000000000002 +v 0.28125 -2.220446049250313e-16 0.6874999999999998 +v 0.28125 -0.06250000000000022 0.6874999999999998 +v -0.28125 -0.0625 -0.4375000000000002 +v -0.28125 0 -0.4375000000000002 +v -0.28125 -0.06250000000000022 0.6874999999999998 +v -0.28125 -2.220446049250313e-16 0.6874999999999998 +vt 0.5546875 0.46875 +vt 0.625 0.46875 +vt 0.625 0.1875 +vt 0.5546875 0.1875 +vt 0.546875 0.46875 +vt 0.5546875 0.46875 +vt 0.5546875 0.1875 +vt 0.546875 0.1875 +vt 0.6328125 0.46875 +vt 0.703125 0.46875 +vt 0.703125 0.1875 +vt 0.6328125 0.1875 +vt 0.625 0.46875 +vt 0.6328125 0.46875 +vt 0.6328125 0.1875 +vt 0.625 0.1875 +vt 0.625 0.46875 +vt 0.5546875 0.46875 +vt 0.5546875 0.484375 +vt 0.625 0.484375 +vt 0.6953125 0.484375 +vt 0.625 0.484375 +vt 0.625 0.46875 +vt 0.6953125 0.46875 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.1875 0.375 -0.4375 +v 0.1875 0.375 -0.8125 +v 0.1875 0.0625 -0.4375 +v 0.1875 0.0625 -0.8125 +v -0.1875 0.375 -0.8125 +v -0.1875 0.375 -0.4375 +v -0.1875 0.0625 -0.8125 +v -0.1875 0.0625 -0.4375 +vt 0.0703125 0.90625 +vt 0.1171875 0.90625 +vt 0.1171875 0.828125 +vt 0.0703125 0.828125 +vt 0.0234375 0.90625 +vt 0.0703125 0.90625 +vt 0.0703125 0.828125 +vt 0.0234375 0.828125 +vt 0.1640625 0.90625 +vt 0.2109375 0.90625 +vt 0.2109375 0.828125 +vt 0.1640625 0.828125 +vt 0.1171875 0.90625 +vt 0.1640625 0.90625 +vt 0.1640625 0.828125 +vt 0.1171875 0.828125 +vt 0.1171875 0.90625 +vt 0.0703125 0.90625 +vt 0.0703125 1 +vt 0.1171875 1 +vt 0.1640625 1 +vt 0.1171875 1 +vt 0.1171875 0.90625 +vt 0.1640625 0.90625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o leg0 +v 0.34375 0.125 1.3125 +v 0.34375 0.125 0.6875 +v 0.34375 0.0625 1.3125 +v 0.34375 0.0625 0.6875 +v 0.09375 0.125 0.6875 +v 0.09375 0.125 1.3125 +v 0.09375 0.0625 0.6875 +v 0.09375 0.0625 1.3125 +vt 0.0859375 0.484375 +vt 0.1171875 0.484375 +vt 0.1171875 0.46875 +vt 0.0859375 0.46875 +vt 0.0078125 0.484375 +vt 0.0859375 0.484375 +vt 0.0859375 0.46875 +vt 0.0078125 0.46875 +vt 0.1953125 0.484375 +vt 0.2265625 0.484375 +vt 0.2265625 0.46875 +vt 0.1953125 0.46875 +vt 0.1171875 0.484375 +vt 0.1953125 0.484375 +vt 0.1953125 0.46875 +vt 0.1171875 0.46875 +vt 0.1171875 0.484375 +vt 0.0859375 0.484375 +vt 0.0859375 0.640625 +vt 0.1171875 0.640625 +vt 0.1484375 0.640625 +vt 0.1171875 0.640625 +vt 0.1171875 0.484375 +vt 0.1484375 0.484375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leg1 +v -0.09375 0.125 1.3125 +v -0.09375 0.125 0.6875 +v -0.09375 0.0625 1.3125 +v -0.09375 0.0625 0.6875 +v -0.34375 0.125 0.6875 +v -0.34375 0.125 1.3125 +v -0.34375 0.0625 0.6875 +v -0.34375 0.0625 1.3125 +vt 0.0859375 0.65625 +vt 0.1171875 0.65625 +vt 0.1171875 0.640625 +vt 0.0859375 0.640625 +vt 0.0078125 0.65625 +vt 0.0859375 0.65625 +vt 0.0859375 0.640625 +vt 0.0078125 0.640625 +vt 0.1953125 0.65625 +vt 0.2265625 0.65625 +vt 0.2265625 0.640625 +vt 0.1953125 0.640625 +vt 0.1171875 0.65625 +vt 0.1953125 0.65625 +vt 0.1953125 0.640625 +vt 0.1171875 0.640625 +vt 0.1171875 0.65625 +vt 0.0859375 0.65625 +vt 0.0859375 0.8125 +vt 0.1171875 0.8125 +vt 0.1484375 0.8125 +vt 0.1171875 0.8125 +vt 0.1171875 0.65625 +vt 0.1484375 0.65625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o leg2 +v 1.0800972660098695 0.1875 0.07574059804416988 +v 1.1343623215307852 0.1875 -0.2320118247721451 +v 1.0800972660098695 0.125 0.07574059804416988 +v 1.1343623215307852 0.125 -0.2320118247721451 +v 0.33420602220836626 0.1875 -0.373100969126526 +v 0.2799409666874506 0.1875 -0.06534854631021103 +v 0.33420602220836626 0.125 -0.373100969126526 +v 0.2799409666874506 0.125 -0.06534854631021103 +vt 0.25 0.453125 +vt 0.3515625 0.453125 +vt 0.3515625 0.4375 +vt 0.25 0.4375 +vt 0.2109375 0.453125 +vt 0.25 0.453125 +vt 0.25 0.4375 +vt 0.2109375 0.4375 +vt 0.390625 0.453125 +vt 0.4921875 0.453125 +vt 0.4921875 0.4375 +vt 0.390625 0.4375 +vt 0.3515625 0.453125 +vt 0.390625 0.453125 +vt 0.390625 0.4375 +vt 0.3515625 0.4375 +vt 0.3515625 0.453125 +vt 0.25 0.453125 +vt 0.25 0.53125 +vt 0.3515625 0.53125 +vt 0.453125 0.53125 +vt 0.3515625 0.53125 +vt 0.3515625 0.453125 +vt 0.453125 0.453125 +vn 0.17364817766693033 0 -0.984807753012208 +vn 0.984807753012208 0 0.17364817766693033 +vn -0.17364817766693033 0 0.984807753012208 +vn -0.984807753012208 0 -0.17364817766693033 +vn 0 1 0 +vn 0 -1 0 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o leg3 +v -0.27994096668745055 0.1875 -0.06534854631021103 +v -0.3342060222083663 0.1875 -0.373100969126526 +v -0.27994096668745055 0.125 -0.06534854631021103 +v -0.3342060222083663 0.125 -0.373100969126526 +v -1.1343623215307852 0.1875 -0.2320118247721451 +v -1.0800972660098695 0.1875 0.07574059804416988 +v -1.1343623215307852 0.125 -0.2320118247721451 +v -1.0800972660098695 0.125 0.07574059804416988 +vt 0.25 0.546875 +vt 0.3515625 0.546875 +vt 0.3515625 0.53125 +vt 0.25 0.53125 +vt 0.2109375 0.546875 +vt 0.25 0.546875 +vt 0.25 0.53125 +vt 0.2109375 0.53125 +vt 0.390625 0.546875 +vt 0.4921875 0.546875 +vt 0.4921875 0.53125 +vt 0.390625 0.53125 +vt 0.3515625 0.546875 +vt 0.390625 0.546875 +vt 0.390625 0.53125 +vt 0.3515625 0.53125 +vt 0.3515625 0.546875 +vt 0.25 0.546875 +vt 0.25 0.625 +vt 0.3515625 0.625 +vt 0.453125 0.625 +vt 0.3515625 0.625 +vt 0.3515625 0.546875 +vt 0.453125 0.546875 +vn -0.17364817766693033 0 -0.984807753012208 +vn 0.984807753012208 0 -0.17364817766693033 +vn 0.17364817766693033 0 0.984807753012208 +vn -0.984807753012208 0 0.17364817766693033 +vn 0 1 0 +vn 0 -1 0 +usemtl m_d5e023f1-fe9e-4631-8a6b-282685c9cca4 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/vex.obj b/renderer/viewer/three/entity/models/vex.obj new file mode 100644 index 00000000..63d99f88 --- /dev/null +++ b/renderer/viewer/three/entity/models/vex.obj @@ -0,0 +1,325 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.09375 0.25 0.0625 +v 0.09375 0.25 -0.0625 +v 0.09375 0 0.0625 +v 0.09375 0 -0.0625 +v -0.09375 0.25 -0.0625 +v -0.09375 0.25 0.0625 +v -0.09375 0 -0.0625 +v -0.09375 0 0.0625 +vt 0.0625 0.625 +vt 0.15625 0.625 +vt 0.15625 0.5 +vt 0.0625 0.5 +vt 0 0.625 +vt 0.0625 0.625 +vt 0.0625 0.5 +vt 0 0.5 +vt 0.21875 0.625 +vt 0.3125 0.625 +vt 0.3125 0.5 +vt 0.21875 0.5 +vt 0.15625 0.625 +vt 0.21875 0.625 +vt 0.21875 0.5 +vt 0.15625 0.5 +vt 0.15625 0.625 +vt 0.0625 0.625 +vt 0.0625 0.6875 +vt 0.15625 0.6875 +vt 0.25 0.6875 +vt 0.15625 0.6875 +vt 0.15625 0.625 +vt 0.25 0.625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.08125000000000004 0.17500000000000004 0.050000000000000044 +v 0.08125000000000004 0.17500000000000004 -0.04999999999999999 +v 0.08125000000000004 -0.11249999999999999 0.050000000000000044 +v 0.08125000000000004 -0.11249999999999999 -0.04999999999999999 +v -0.08124999999999999 0.17500000000000004 -0.04999999999999999 +v -0.08124999999999999 0.17500000000000004 0.050000000000000044 +v -0.08124999999999999 -0.11249999999999999 -0.04999999999999999 +v -0.08124999999999999 -0.11249999999999999 0.050000000000000044 +vt 0.0625 0.4375 +vt 0.15625 0.4375 +vt 0.15625 0.28125 +vt 0.0625 0.28125 +vt 0 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.28125 +vt 0 0.28125 +vt 0.21875 0.4375 +vt 0.3125 0.4375 +vt 0.3125 0.28125 +vt 0.21875 0.28125 +vt 0.15625 0.4375 +vt 0.21875 0.4375 +vt 0.21875 0.28125 +vt 0.15625 0.28125 +vt 0.15625 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.5 +vt 0.15625 0.5 +vt 0.25 0.5 +vt 0.15625 0.5 +vt 0.15625 0.4375 +vt 0.25 0.4375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.15625 0.5625 0.15625 +v 0.15625 0.5625 -0.15625 +v 0.15625 0.25 0.15625 +v 0.15625 0.25 -0.15625 +v -0.15625 0.5625 -0.15625 +v -0.15625 0.5625 0.15625 +v -0.15625 0.25 -0.15625 +v -0.15625 0.25 0.15625 +vt 0.15625 0.84375 +vt 0.3125 0.84375 +vt 0.3125 0.6875 +vt 0.15625 0.6875 +vt 0 0.84375 +vt 0.15625 0.84375 +vt 0.15625 0.6875 +vt 0 0.6875 +vt 0.46875 0.84375 +vt 0.625 0.84375 +vt 0.625 0.6875 +vt 0.46875 0.6875 +vt 0.3125 0.84375 +vt 0.46875 0.84375 +vt 0.46875 0.6875 +vt 0.3125 0.6875 +vt 0.3125 0.84375 +vt 0.15625 0.84375 +vt 0.15625 1 +vt 0.3125 1 +vt 0.46875 1 +vt 0.3125 1 +vt 0.3125 0.84375 +vt 0.46875 0.84375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o rightArm +v 0.18125000000000002 0.259375 0.05625000000000002 +v 0.18125000000000002 0.259375 -0.05625000000000002 +v 0.18125000000000002 0.021874999999999978 0.05625000000000002 +v 0.18125000000000002 0.021874999999999978 -0.05625000000000002 +v 0.06874999999999998 0.259375 -0.05625000000000002 +v 0.06874999999999998 0.259375 0.05625000000000002 +v 0.06874999999999998 0.021874999999999978 -0.05625000000000002 +v 0.06874999999999998 0.021874999999999978 0.05625000000000002 +vt 0.78125 0.9375 +vt 0.84375 0.9375 +vt 0.84375 0.8125 +vt 0.78125 0.8125 +vt 0.71875 0.9375 +vt 0.78125 0.9375 +vt 0.78125 0.8125 +vt 0.71875 0.8125 +vt 0.90625 0.9375 +vt 0.96875 0.9375 +vt 0.96875 0.8125 +vt 0.90625 0.8125 +vt 0.84375 0.9375 +vt 0.90625 0.9375 +vt 0.90625 0.8125 +vt 0.84375 0.8125 +vt 0.84375 0.9375 +vt 0.78125 0.9375 +vt 0.78125 1 +vt 0.84375 1 +vt 0.90625 1 +vt 0.84375 1 +vt 0.84375 0.9375 +vt 0.90625 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o leftArm +v -0.06874999999999998 0.259375 0.05625000000000002 +v -0.06874999999999998 0.259375 -0.05625000000000002 +v -0.06874999999999998 0.021874999999999978 0.05625000000000002 +v -0.06874999999999998 0.021874999999999978 -0.05625000000000002 +v -0.18125000000000002 0.259375 -0.05625000000000002 +v -0.18125000000000002 0.259375 0.05625000000000002 +v -0.18125000000000002 0.021874999999999978 -0.05625000000000002 +v -0.18125000000000002 0.021874999999999978 0.05625000000000002 +vt 0.78125 0.75 +vt 0.84375 0.75 +vt 0.84375 0.625 +vt 0.78125 0.625 +vt 0.71875 0.75 +vt 0.78125 0.75 +vt 0.78125 0.625 +vt 0.71875 0.625 +vt 0.90625 0.75 +vt 0.96875 0.75 +vt 0.96875 0.625 +vt 0.90625 0.625 +vt 0.84375 0.75 +vt 0.90625 0.75 +vt 0.90625 0.625 +vt 0.84375 0.625 +vt 0.84375 0.75 +vt 0.78125 0.75 +vt 0.78125 0.8125 +vt 0.84375 0.8125 +vt 0.90625 0.8125 +vt 0.84375 0.8125 +vt 0.84375 0.75 +vt 0.90625 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o leftWing +v -0.03125 0.1875 0.0625 +v -0.03125 0.1875 0.0625 +v -0.03125 -0.125 0.0625 +v -0.03125 -0.125 0.0625 +v -0.53125 0.1875 0.0625 +v -0.53125 0.1875 0.0625 +v -0.53125 -0.125 0.0625 +v -0.53125 -0.125 0.0625 +vt 0.75 0.3125 +vt 0.5 0.3125 +vt 0.5 0.15625 +vt 0.75 0.15625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 0.75 0.15625 +vt 1 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 1 0.15625 +vt 0.5 0.3125 +vt 0.5 0.3125 +vt 0.5 0.15625 +vt 0.5 0.15625 +vt 0.5 0.3125 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.5 0.3125 +vt 0.75 0.3125 +vt 1 0.3125 +vt 1 0.3125 +vt 0.75 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o rightWing +v 0.53125 0.1875 0.0625 +v 0.53125 0.1875 0.0625 +v 0.53125 -0.125 0.0625 +v 0.53125 -0.125 0.0625 +v 0.03125 0.1875 0.0625 +v 0.03125 0.1875 0.0625 +v 0.03125 -0.125 0.0625 +v 0.03125 -0.125 0.0625 +vt 0.5 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 0.5 0.15625 +vt 0.5 0.3125 +vt 0.5 0.3125 +vt 0.5 0.15625 +vt 0.5 0.15625 +vt 0.75 0.3125 +vt 1 0.3125 +vt 1 0.15625 +vt 0.75 0.15625 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 0.75 0.15625 +vt 0.75 0.15625 +vt 0.75 0.3125 +vt 0.5 0.3125 +vt 0.5 0.3125 +vt 0.75 0.3125 +vt 1 0.3125 +vt 0.75 0.3125 +vt 0.75 0.3125 +vt 1 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_f88b8145-cdbb-8011-03d2-81deb45b4e43 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/villager.obj b/renderer/viewer/three/entity/models/villager.obj new file mode 100644 index 00000000..73462472 --- /dev/null +++ b/renderer/viewer/three/entity/models/villager.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.25 1.5 0.1875 +v 0.25 1.5 -0.1875 +v 0.25 0.75 0.1875 +v 0.25 0.75 -0.1875 +v -0.25 1.5 -0.1875 +v -0.25 1.5 0.1875 +v -0.25 0.75 -0.1875 +v -0.25 0.75 0.1875 +vt 0.34375 0.59375 +vt 0.46875 0.59375 +vt 0.46875 0.40625 +vt 0.34375 0.40625 +vt 0.25 0.59375 +vt 0.34375 0.59375 +vt 0.34375 0.40625 +vt 0.25 0.40625 +vt 0.5625 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.40625 +vt 0.5625 0.40625 +vt 0.46875 0.59375 +vt 0.5625 0.59375 +vt 0.5625 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.59375 +vt 0.34375 0.59375 +vt 0.34375 0.6875 +vt 0.46875 0.6875 +vt 0.59375 0.6875 +vt 0.46875 0.6875 +vt 0.46875 0.59375 +vt 0.59375 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o body +v 0.28125 1.53125 0.21875 +v 0.28125 1.53125 -0.21875 +v 0.28125 0.34375 0.21875 +v 0.28125 0.34375 -0.21875 +v -0.28125 1.53125 -0.21875 +v -0.28125 1.53125 0.21875 +v -0.28125 0.34375 -0.21875 +v -0.28125 0.34375 0.21875 +vt 0.09375 0.3125 +vt 0.21875 0.3125 +vt 0.21875 0.03125 +vt 0.09375 0.03125 +vt 0 0.3125 +vt 0.09375 0.3125 +vt 0.09375 0.03125 +vt 0 0.03125 +vt 0.3125 0.3125 +vt 0.4375 0.3125 +vt 0.4375 0.03125 +vt 0.3125 0.03125 +vt 0.21875 0.3125 +vt 0.3125 0.3125 +vt 0.3125 0.03125 +vt 0.21875 0.03125 +vt 0.21875 0.3125 +vt 0.09375 0.3125 +vt 0.09375 0.40625 +vt 0.21875 0.40625 +vt 0.34375 0.40625 +vt 0.21875 0.40625 +vt 0.21875 0.3125 +vt 0.34375 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0.25 2.125 0.25 +v 0.25 2.125 -0.25 +v 0.25 1.5 0.25 +v 0.25 1.5 -0.25 +v -0.25 2.125 -0.25 +v -0.25 2.125 0.25 +v -0.25 1.5 -0.25 +v -0.25 1.5 0.25 +vt 0.125 0.875 +vt 0.25 0.875 +vt 0.25 0.71875 +vt 0.125 0.71875 +vt 0 0.875 +vt 0.125 0.875 +vt 0.125 0.71875 +vt 0 0.71875 +vt 0.375 0.875 +vt 0.5 0.875 +vt 0.5 0.71875 +vt 0.375 0.71875 +vt 0.25 0.875 +vt 0.375 0.875 +vt 0.375 0.71875 +vt 0.25 0.71875 +vt 0.25 0.875 +vt 0.125 0.875 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.875 +vt 0.375 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o helmet +v 0.28125 2.15625 0.28125 +v 0.28125 2.15625 -0.28125 +v 0.28125 1.46875 0.28125 +v 0.28125 1.46875 -0.28125 +v -0.28125 2.15625 -0.28125 +v -0.28125 2.15625 0.28125 +v -0.28125 1.46875 -0.28125 +v -0.28125 1.46875 0.28125 +vt 0.625 0.875 +vt 0.75 0.875 +vt 0.75 0.71875 +vt 0.625 0.71875 +vt 0.5 0.875 +vt 0.625 0.875 +vt 0.625 0.71875 +vt 0.5 0.71875 +vt 0.875 0.875 +vt 1 0.875 +vt 1 0.71875 +vt 0.875 0.71875 +vt 0.75 0.875 +vt 0.875 0.875 +vt 0.875 0.71875 +vt 0.75 0.71875 +vt 0.75 0.875 +vt 0.625 0.875 +vt 0.625 1 +vt 0.75 1 +vt 0.875 1 +vt 0.75 1 +vt 0.75 0.875 +vt 0.875 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o brim +v 0.5062500000000001 1.80625 0.5062500000000001 +v 0.5062500000000001 1.88125 0.5062500000000001 +v 0.5062500000000001 1.80625 -0.5062500000000001 +v 0.5062500000000001 1.8812499999999996 -0.5062500000000001 +v -0.50625 1.88125 0.5062500000000001 +v -0.50625 1.80625 0.5062500000000001 +v -0.50625 1.8812499999999996 -0.5062500000000001 +v -0.50625 1.80625 -0.5062500000000001 +vt 0.484375 0.25 +vt 0.734375 0.25 +vt 0.734375 0 +vt 0.484375 0 +vt 0.46875 0.25 +vt 0.484375 0.25 +vt 0.484375 0 +vt 0.46875 0 +vt 0.75 0.25 +vt 1 0.25 +vt 1 0 +vt 0.75 0 +vt 0.734375 0.25 +vt 0.75 0.25 +vt 0.75 0 +vt 0.734375 0 +vt 0.734375 0.25 +vt 0.484375 0.25 +vt 0.484375 0.265625 +vt 0.734375 0.265625 +vt 0.984375 0.265625 +vt 0.734375 0.265625 +vt 0.734375 0.25 +vt 0.984375 0.25 +vn 0 1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 -1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 1 +vn 0 -2.220446049250313e-16 -1 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o nose +v 0.0625 1.6875 -0.25 +v 0.0625 1.6875 -0.375 +v 0.0625 1.4375 -0.25 +v 0.0625 1.4375 -0.375 +v -0.0625 1.6875 -0.375 +v -0.0625 1.6875 -0.25 +v -0.0625 1.4375 -0.375 +v -0.0625 1.4375 -0.25 +vt 0.40625 0.96875 +vt 0.4375 0.96875 +vt 0.4375 0.90625 +vt 0.40625 0.90625 +vt 0.375 0.96875 +vt 0.40625 0.96875 +vt 0.40625 0.90625 +vt 0.375 0.90625 +vt 0.46875 0.96875 +vt 0.5 0.96875 +vt 0.5 0.90625 +vt 0.46875 0.90625 +vt 0.4375 0.96875 +vt 0.46875 0.96875 +vt 0.46875 0.90625 +vt 0.4375 0.90625 +vt 0.4375 0.96875 +vt 0.40625 0.96875 +vt 0.40625 1 +vt 0.4375 1 +vt 0.46875 1 +vt 0.4375 1 +vt 0.4375 0.96875 +vt 0.46875 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o arms +v 0.25 1.198223304703363 0 +v 0.25 1.375 -0.17677669529663698 +v 0.25 1.0214466094067263 -0.17677669529663698 +v 0.25 1.1982233047033632 -0.35355339059327373 +v -0.25 1.375 -0.17677669529663698 +v -0.25 1.198223304703363 0 +v -0.25 1.1982233047033632 -0.35355339059327373 +v -0.25 1.0214466094067263 -0.17677669529663698 +vt 0.6875 0.34375 +vt 0.8125 0.34375 +vt 0.8125 0.28125 +vt 0.6875 0.28125 +vt 0.625 0.34375 +vt 0.6875 0.34375 +vt 0.6875 0.28125 +vt 0.625 0.28125 +vt 0.875 0.34375 +vt 1 0.34375 +vt 1 0.28125 +vt 0.875 0.28125 +vt 0.8125 0.34375 +vt 0.875 0.34375 +vt 0.875 0.28125 +vt 0.8125 0.28125 +vt 0.8125 0.34375 +vt 0.6875 0.34375 +vt 0.6875 0.40625 +vt 0.8125 0.40625 +vt 0.9375 0.40625 +vt 0.8125 0.40625 +vt 0.8125 0.34375 +vt 0.9375 0.34375 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o arms +v 0.5 1.375 0.17677669529663698 +v 0.5 1.551776695296637 0 +v 0.5 1.0214466094067263 -0.17677669529663698 +v 0.5 1.1982233047033632 -0.35355339059327373 +v 0.25 1.551776695296637 0 +v 0.25 1.375 0.17677669529663698 +v 0.25 1.1982233047033632 -0.35355339059327373 +v 0.25 1.0214466094067263 -0.17677669529663698 +vt 0.75 0.59375 +vt 0.8125 0.59375 +vt 0.8125 0.46875 +vt 0.75 0.46875 +vt 0.6875 0.59375 +vt 0.75 0.59375 +vt 0.75 0.46875 +vt 0.6875 0.46875 +vt 0.875 0.59375 +vt 0.9375 0.59375 +vt 0.9375 0.46875 +vt 0.875 0.46875 +vt 0.8125 0.59375 +vt 0.875 0.59375 +vt 0.875 0.46875 +vt 0.8125 0.46875 +vt 0.8125 0.59375 +vt 0.75 0.59375 +vt 0.75 0.65625 +vt 0.8125 0.65625 +vt 0.875 0.65625 +vt 0.8125 0.65625 +vt 0.8125 0.59375 +vt 0.875 0.59375 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o arms +v -0.25 1.375 0.17677669529663698 +v -0.25 1.551776695296637 0 +v -0.25 1.0214466094067263 -0.17677669529663698 +v -0.25 1.1982233047033632 -0.35355339059327373 +v -0.5 1.551776695296637 0 +v -0.5 1.375 0.17677669529663698 +v -0.5 1.1982233047033632 -0.35355339059327373 +v -0.5 1.0214466094067263 -0.17677669529663698 +vt 0.8125 0.59375 +vt 0.75 0.59375 +vt 0.75 0.46875 +vt 0.8125 0.46875 +vt 0.875 0.59375 +vt 0.8125 0.59375 +vt 0.8125 0.46875 +vt 0.875 0.46875 +vt 0.9375 0.59375 +vt 0.875 0.59375 +vt 0.875 0.46875 +vt 0.9375 0.46875 +vt 0.75 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.46875 +vt 0.75 0.46875 +vt 0.75 0.59375 +vt 0.8125 0.59375 +vt 0.8125 0.65625 +vt 0.75 0.65625 +vt 0.8125 0.65625 +vt 0.875 0.65625 +vt 0.875 0.59375 +vt 0.8125 0.59375 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o leg0 +v 0.25 0.75 0.125 +v 0.25 0.75 -0.125 +v 0.25 0 0.125 +v 0.25 0 -0.125 +v 0 0.75 -0.125 +v 0 0.75 0.125 +v 0 0 -0.125 +v 0 0 0.125 +vt 0.0625 0.59375 +vt 0.125 0.59375 +vt 0.125 0.40625 +vt 0.0625 0.40625 +vt 0 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.40625 +vt 0 0.40625 +vt 0.1875 0.59375 +vt 0.25 0.59375 +vt 0.25 0.40625 +vt 0.1875 0.40625 +vt 0.125 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.40625 +vt 0.125 0.40625 +vt 0.125 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.65625 +vt 0.125 0.65625 +vt 0.1875 0.65625 +vt 0.125 0.65625 +vt 0.125 0.59375 +vt 0.1875 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o leg1 +v 0 0.75 0.125 +v 0 0.75 -0.125 +v 0 0 0.125 +v 0 0 -0.125 +v -0.25 0.75 -0.125 +v -0.25 0.75 0.125 +v -0.25 0 -0.125 +v -0.25 0 0.125 +vt 0.125 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.40625 +vt 0.125 0.40625 +vt 0.1875 0.59375 +vt 0.125 0.59375 +vt 0.125 0.40625 +vt 0.1875 0.40625 +vt 0.25 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.40625 +vt 0.25 0.40625 +vt 0.0625 0.59375 +vt 0 0.59375 +vt 0 0.40625 +vt 0.0625 0.40625 +vt 0.0625 0.59375 +vt 0.125 0.59375 +vt 0.125 0.65625 +vt 0.0625 0.65625 +vt 0.125 0.65625 +vt 0.1875 0.65625 +vt 0.1875 0.59375 +vt 0.125 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b4465c9b-59e0-9882-a652-f0428980cb6e +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/warden.obj b/renderer/viewer/three/entity/models/warden.obj new file mode 100644 index 00000000..da0b2656 --- /dev/null +++ b/renderer/viewer/three/entity/models/warden.obj @@ -0,0 +1,463 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o body +v 0.5625 2.125 0.4375 +v 0.5625 2.125 -0.25 +v 0.5625 0.8125 0.4375 +v 0.5625 0.8125 -0.25 +v -0.5625 2.125 -0.25 +v -0.5625 2.125 0.4375 +v -0.5625 0.8125 -0.25 +v -0.5625 0.8125 0.4375 +vt 0.0859375 0.9140625 +vt 0.2265625 0.9140625 +vt 0.2265625 0.75 +vt 0.0859375 0.75 +vt 0 0.9140625 +vt 0.0859375 0.9140625 +vt 0.0859375 0.75 +vt 0 0.75 +vt 0.3125 0.9140625 +vt 0.453125 0.9140625 +vt 0.453125 0.75 +vt 0.3125 0.75 +vt 0.2265625 0.9140625 +vt 0.3125 0.9140625 +vt 0.3125 0.75 +vt 0.2265625 0.75 +vt 0.2265625 0.9140625 +vt 0.0859375 0.9140625 +vt 0.0859375 1 +vt 0.2265625 1 +vt 0.3671875 1 +vt 0.2265625 1 +vt 0.2265625 0.9140625 +vt 0.3671875 0.9140625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o right_ribcage +v 0.5625 2.125 -0.25625 +v 0.5625 2.125 -0.25625 +v 0.5625 0.8125 -0.25625 +v 0.5625 0.8125 -0.25625 +v 0 2.125 -0.25625 +v 0 2.125 -0.25625 +v 0 0.8125 -0.25625 +v 0 0.8125 -0.25625 +vt 0.703125 0.9140625 +vt 0.7734375 0.9140625 +vt 0.7734375 0.75 +vt 0.703125 0.75 +vt 0.703125 0.9140625 +vt 0.703125 0.9140625 +vt 0.703125 0.75 +vt 0.703125 0.75 +vt 0.7734375 0.9140625 +vt 0.84375 0.9140625 +vt 0.84375 0.75 +vt 0.7734375 0.75 +vt 0.7734375 0.9140625 +vt 0.7734375 0.9140625 +vt 0.7734375 0.75 +vt 0.7734375 0.75 +vt 0.7734375 0.9140625 +vt 0.703125 0.9140625 +vt 0.703125 0.9140625 +vt 0.7734375 0.9140625 +vt 0.84375 0.9140625 +vt 0.7734375 0.9140625 +vt 0.7734375 0.9140625 +vt 0.84375 0.9140625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o left_ribcage +v 0 2.125 -0.25625 +v 0 2.125 -0.25625 +v 0 0.8125 -0.25625 +v 0 0.8125 -0.25625 +v -0.5625 2.125 -0.25625 +v -0.5625 2.125 -0.25625 +v -0.5625 0.8125 -0.25625 +v -0.5625 0.8125 -0.25625 +vt 0.7734375 0.9140625 +vt 0.703125 0.9140625 +vt 0.703125 0.75 +vt 0.7734375 0.75 +vt 0.7734375 0.9140625 +vt 0.7734375 0.9140625 +vt 0.7734375 0.75 +vt 0.7734375 0.75 +vt 0.84375 0.9140625 +vt 0.7734375 0.9140625 +vt 0.7734375 0.75 +vt 0.84375 0.75 +vt 0.703125 0.9140625 +vt 0.703125 0.9140625 +vt 0.703125 0.75 +vt 0.703125 0.75 +vt 0.703125 0.9140625 +vt 0.7734375 0.9140625 +vt 0.7734375 0.9140625 +vt 0.703125 0.9140625 +vt 0.7734375 0.9140625 +vt 0.84375 0.9140625 +vt 0.84375 0.9140625 +vt 0.7734375 0.9140625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.5 3.125 0.3125 +v 0.5 3.125 -0.3125 +v 0.5 2.125 0.3125 +v 0.5 2.125 -0.3125 +v -0.5 3.125 -0.3125 +v -0.5 3.125 0.3125 +v -0.5 2.125 -0.3125 +v -0.5 2.125 0.3125 +vt 0.078125 0.671875 +vt 0.203125 0.671875 +vt 0.203125 0.546875 +vt 0.078125 0.546875 +vt 0 0.671875 +vt 0.078125 0.671875 +vt 0.078125 0.546875 +vt 0 0.546875 +vt 0.28125 0.671875 +vt 0.40625 0.671875 +vt 0.40625 0.546875 +vt 0.28125 0.546875 +vt 0.203125 0.671875 +vt 0.28125 0.671875 +vt 0.28125 0.546875 +vt 0.203125 0.546875 +vt 0.203125 0.671875 +vt 0.078125 0.671875 +vt 0.078125 0.75 +vt 0.203125 0.75 +vt 0.328125 0.75 +vt 0.203125 0.75 +vt 0.203125 0.671875 +vt 0.328125 0.671875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o right_tendril +v 1.5 3.6875 0 +v 1.5 3.6875 0 +v 1.5 2.6875 0 +v 1.5 2.6875 0 +v 0.5 3.6875 0 +v 0.5 3.6875 0 +v 0.5 2.6875 0 +v 0.5 2.6875 0 +vt 0.40625 0.75 +vt 0.53125 0.75 +vt 0.53125 0.625 +vt 0.40625 0.625 +vt 0.40625 0.75 +vt 0.40625 0.75 +vt 0.40625 0.625 +vt 0.40625 0.625 +vt 0.53125 0.75 +vt 0.65625 0.75 +vt 0.65625 0.625 +vt 0.53125 0.625 +vt 0.53125 0.75 +vt 0.53125 0.75 +vt 0.53125 0.625 +vt 0.53125 0.625 +vt 0.53125 0.75 +vt 0.40625 0.75 +vt 0.40625 0.75 +vt 0.53125 0.75 +vt 0.65625 0.75 +vt 0.53125 0.75 +vt 0.53125 0.75 +vt 0.65625 0.75 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o left_tendril +v -0.5 3.6875 0 +v -0.5 3.6875 0 +v -0.5 2.6875 0 +v -0.5 2.6875 0 +v -1.5 3.6875 0 +v -1.5 3.6875 0 +v -1.5 2.6875 0 +v -1.5 2.6875 0 +vt 0.453125 1 +vt 0.578125 1 +vt 0.578125 0.875 +vt 0.453125 0.875 +vt 0.453125 1 +vt 0.453125 1 +vt 0.453125 0.875 +vt 0.453125 0.875 +vt 0.578125 1 +vt 0.703125 1 +vt 0.703125 0.875 +vt 0.578125 0.875 +vt 0.578125 1 +vt 0.578125 1 +vt 0.578125 0.875 +vt 0.578125 0.875 +vt 0.578125 1 +vt 0.453125 1 +vt 0.453125 1 +vt 0.578125 1 +vt 0.703125 1 +vt 0.578125 1 +vt 0.578125 1 +vt 0.703125 1 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o right_arm +v 1.0625 2.125 0.3125 +v 1.0625 2.125 -0.1875 +v 1.0625 0.375 0.3125 +v 1.0625 0.375 -0.1875 +v 0.5625 2.125 -0.1875 +v 0.5625 2.125 0.3125 +v 0.5625 0.375 -0.1875 +v 0.5625 0.375 0.3125 +vt 0.40625 0.546875 +vt 0.46875 0.546875 +vt 0.46875 0.328125 +vt 0.40625 0.328125 +vt 0.34375 0.546875 +vt 0.40625 0.546875 +vt 0.40625 0.328125 +vt 0.34375 0.328125 +vt 0.53125 0.546875 +vt 0.59375 0.546875 +vt 0.59375 0.328125 +vt 0.53125 0.328125 +vt 0.46875 0.546875 +vt 0.53125 0.546875 +vt 0.53125 0.328125 +vt 0.46875 0.328125 +vt 0.46875 0.546875 +vt 0.40625 0.546875 +vt 0.40625 0.609375 +vt 0.46875 0.609375 +vt 0.53125 0.609375 +vt 0.46875 0.609375 +vt 0.46875 0.546875 +vt 0.53125 0.546875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o left_arm +v -0.5625 2.125 0.3125 +v -0.5625 2.125 -0.1875 +v -0.5625 0.375 0.3125 +v -0.5625 0.375 -0.1875 +v -1.0625 2.125 -0.1875 +v -1.0625 2.125 0.3125 +v -1.0625 0.375 -0.1875 +v -1.0625 0.375 0.3125 +vt 0.0625 0.484375 +vt 0.125 0.484375 +vt 0.125 0.265625 +vt 0.0625 0.265625 +vt 0 0.484375 +vt 0.0625 0.484375 +vt 0.0625 0.265625 +vt 0 0.265625 +vt 0.1875 0.484375 +vt 0.25 0.484375 +vt 0.25 0.265625 +vt 0.1875 0.265625 +vt 0.125 0.484375 +vt 0.1875 0.484375 +vt 0.1875 0.265625 +vt 0.125 0.265625 +vt 0.125 0.484375 +vt 0.0625 0.484375 +vt 0.0625 0.546875 +vt 0.125 0.546875 +vt 0.1875 0.546875 +vt 0.125 0.546875 +vt 0.125 0.484375 +vt 0.1875 0.484375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o right_leg +v 0.5625 0.8125 0.1875 +v 0.5625 0.8125 -0.1875 +v 0.5625 0 0.1875 +v 0.5625 0 -0.1875 +v 0.1875 0.8125 -0.1875 +v 0.1875 0.8125 0.1875 +v 0.1875 0 -0.1875 +v 0.1875 0 0.1875 +vt 0.640625 0.578125 +vt 0.6875 0.578125 +vt 0.6875 0.4765625 +vt 0.640625 0.4765625 +vt 0.59375 0.578125 +vt 0.640625 0.578125 +vt 0.640625 0.4765625 +vt 0.59375 0.4765625 +vt 0.734375 0.578125 +vt 0.78125 0.578125 +vt 0.78125 0.4765625 +vt 0.734375 0.4765625 +vt 0.6875 0.578125 +vt 0.734375 0.578125 +vt 0.734375 0.4765625 +vt 0.6875 0.4765625 +vt 0.6875 0.578125 +vt 0.640625 0.578125 +vt 0.640625 0.625 +vt 0.6875 0.625 +vt 0.734375 0.625 +vt 0.6875 0.625 +vt 0.6875 0.578125 +vt 0.734375 0.578125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o left_leg +v -0.1875 0.8125 0.1875 +v -0.1875 0.8125 -0.1875 +v -0.1875 0 0.1875 +v -0.1875 0 -0.1875 +v -0.5625 0.8125 -0.1875 +v -0.5625 0.8125 0.1875 +v -0.5625 0 -0.1875 +v -0.5625 0 0.1875 +vt 0.640625 0.359375 +vt 0.6875 0.359375 +vt 0.6875 0.2578125 +vt 0.640625 0.2578125 +vt 0.59375 0.359375 +vt 0.640625 0.359375 +vt 0.640625 0.2578125 +vt 0.59375 0.2578125 +vt 0.734375 0.359375 +vt 0.78125 0.359375 +vt 0.78125 0.2578125 +vt 0.734375 0.2578125 +vt 0.6875 0.359375 +vt 0.734375 0.359375 +vt 0.734375 0.2578125 +vt 0.6875 0.2578125 +vt 0.6875 0.359375 +vt 0.640625 0.359375 +vt 0.640625 0.40625 +vt 0.6875 0.40625 +vt 0.734375 0.40625 +vt 0.6875 0.40625 +vt 0.6875 0.359375 +vt 0.734375 0.359375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_b8aa60f9-5a7d-baf1-aee9-852f731e91b8 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/witch.obj b/renderer/viewer/three/entity/models/witch.obj new file mode 100644 index 00000000..275da643 --- /dev/null +++ b/renderer/viewer/three/entity/models/witch.obj @@ -0,0 +1,647 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o head +v 0.25 2.125 0.25 +v 0.25 2.125 -0.25 +v 0.25 1.5 0.25 +v 0.25 1.5 -0.25 +v -0.25 2.125 -0.25 +v -0.25 2.125 0.25 +v -0.25 1.5 -0.25 +v -0.25 1.5 0.25 +vt 0.125 0.9375 +vt 0.25 0.9375 +vt 0.25 0.859375 +vt 0.125 0.859375 +vt 0 0.9375 +vt 0.125 0.9375 +vt 0.125 0.859375 +vt 0 0.859375 +vt 0.375 0.9375 +vt 0.5 0.9375 +vt 0.5 0.859375 +vt 0.375 0.859375 +vt 0.25 0.9375 +vt 0.375 0.9375 +vt 0.375 0.859375 +vt 0.25 0.859375 +vt 0.25 0.9375 +vt 0.125 0.9375 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.9375 +vt 0.375 0.9375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o nose +v 0.0625 1.6875 -0.25 +v 0.0625 1.6875 -0.375 +v 0.0625 1.4375 -0.25 +v 0.0625 1.4375 -0.375 +v -0.0625 1.6875 -0.375 +v -0.0625 1.6875 -0.25 +v -0.0625 1.4375 -0.375 +v -0.0625 1.4375 -0.25 +vt 0.40625 0.984375 +vt 0.4375 0.984375 +vt 0.4375 0.953125 +vt 0.40625 0.953125 +vt 0.375 0.984375 +vt 0.40625 0.984375 +vt 0.40625 0.953125 +vt 0.375 0.953125 +vt 0.46875 0.984375 +vt 0.5 0.984375 +vt 0.5 0.953125 +vt 0.46875 0.953125 +vt 0.4375 0.984375 +vt 0.46875 0.984375 +vt 0.46875 0.953125 +vt 0.4375 0.953125 +vt 0.4375 0.984375 +vt 0.40625 0.984375 +vt 0.40625 1 +vt 0.4375 1 +vt 0.46875 1 +vt 0.4375 1 +vt 0.4375 0.984375 +vt 0.46875 0.984375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o nose +v -0.015625 1.609375 -0.375 +v -0.015625 1.609375 -0.40625 +v -0.015625 1.578125 -0.375 +v -0.015625 1.578125 -0.40625 +v -0.046875 1.609375 -0.40625 +v -0.046875 1.609375 -0.375 +v -0.046875 1.578125 -0.40625 +v -0.046875 1.578125 -0.375 +vt 0.015625 0.9921875 +vt 0.03125 0.9921875 +vt 0.03125 0.984375 +vt 0.015625 0.984375 +vt 0 0.9921875 +vt 0.015625 0.9921875 +vt 0.015625 0.984375 +vt 0 0.984375 +vt 0.046875 0.9921875 +vt 0.0625 0.9921875 +vt 0.0625 0.984375 +vt 0.046875 0.984375 +vt 0.03125 0.9921875 +vt 0.046875 0.9921875 +vt 0.046875 0.984375 +vt 0.03125 0.984375 +vt 0.03125 0.9921875 +vt 0.015625 0.9921875 +vt 0.015625 1 +vt 0.03125 1 +vt 0.046875 1 +vt 0.03125 1 +vt 0.03125 0.9921875 +vt 0.046875 0.9921875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o hat +v 0.3125 2.128125 0.3125 +v 0.3125 2.128125 -0.3125 +v 0.3125 2.003125 0.3125 +v 0.3125 2.003125 -0.3125 +v -0.3125 2.128125 -0.3125 +v -0.3125 2.128125 0.3125 +v -0.3125 2.003125 -0.3125 +v -0.3125 2.003125 0.3125 +vt 0.15625 0.421875 +vt 0.3125 0.421875 +vt 0.3125 0.40625 +vt 0.15625 0.40625 +vt 0 0.421875 +vt 0.15625 0.421875 +vt 0.15625 0.40625 +vt 0 0.40625 +vt 0.46875 0.421875 +vt 0.625 0.421875 +vt 0.625 0.40625 +vt 0.46875 0.40625 +vt 0.3125 0.421875 +vt 0.46875 0.421875 +vt 0.46875 0.40625 +vt 0.3125 0.40625 +vt 0.3125 0.421875 +vt 0.15625 0.421875 +vt 0.15625 0.5 +vt 0.3125 0.5 +vt 0.46875 0.5 +vt 0.3125 0.5 +vt 0.3125 0.421875 +vt 0.46875 0.421875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o hat2 +v 0.19420316941970583 2.3448018130595334 0.26781917680283374 +v 0.19360379633542446 2.367690947692347 -0.16908124465229235 +v 0.20073843784720224 2.0952299807209473 0.2547351877420978 +v 0.20013906476292087 2.118119115353761 -0.18216523371302834 +v -0.24374628334138193 2.3562385328076525 -0.16908124465229235 +v -0.24314691025710045 2.333349398174839 0.26781917680283374 +v -0.2372110149138854 2.1066667004690665 -0.18216523371302834 +v -0.23661164182960404 2.083777565836253 0.2547351877420978 +vt 0.109375 0.3515625 +vt 0.21875 0.3515625 +vt 0.21875 0.3203125 +vt 0.109375 0.3203125 +vt 0 0.3515625 +vt 0.109375 0.3515625 +vt 0.109375 0.3203125 +vt 0 0.3203125 +vt 0.328125 0.3515625 +vt 0.4375 0.3515625 +vt 0.4375 0.3203125 +vt 0.328125 0.3203125 +vt 0.21875 0.3515625 +vt 0.328125 0.3515625 +vt 0.328125 0.3203125 +vt 0.21875 0.3203125 +vt 0.21875 0.3515625 +vt 0.109375 0.3515625 +vt 0.109375 0.40625 +vt 0.21875 0.40625 +vt 0.328125 0.40625 +vt 0.21875 0.40625 +vt 0.21875 0.3515625 +vt 0.328125 0.3515625 +vn -0.0013699956212146517 0.052318022017859046 -0.998629534754574 +vn 0.9996573249755573 0.026176948307873146 -2.168404344971009e-19 +vn 0.0013699956212146517 -0.052318022017859046 0.998629534754574 +vn -0.9996573249755573 -0.026176948307873146 2.168404344971009e-19 +vn -0.02614107370998589 0.9982873293543426 0.05233595624294382 +vn 0.02614107370998589 -0.9982873293543426 -0.05233595624294382 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o hat3 +v 0.06205916642018039 2.5318108508382715 0.25085017875105275 +v 0.059669177777630145 2.5708345128476795 0.003926219286475963 +v 0.08152178038959057 2.2856541582304613 0.21175939541454591 +v 0.07913179174704033 2.3246778202398692 -0.03516456405003088 +v -0.1895606250384982 2.5512376639234398 0.0032414562075101516 +v -0.187170636395948 2.5122140019140318 0.25016541567208694 +v -0.17009801106908795 2.3050809713156295 -0.03584932712899669 +v -0.1677080224265377 2.2660573093062215 0.2110746323355801 +vt 0.0625 0.2890625 +vt 0.125 0.2890625 +vt 0.125 0.2578125 +vt 0.0625 0.2578125 +vt 0 0.2890625 +vt 0.0625 0.2890625 +vt 0.0625 0.2578125 +vt 0 0.2578125 +vt 0.1875 0.2890625 +vt 0.25 0.2890625 +vt 0.25 0.2578125 +vt 0.1875 0.2578125 +vt 0.125 0.2890625 +vt 0.1875 0.2890625 +vt 0.1875 0.2578125 +vt 0.125 0.2578125 +vt 0.125 0.2890625 +vt 0.0625 0.2890625 +vt 0.0625 0.3203125 +vt 0.125 0.3203125 +vt 0.1875 0.3203125 +vt 0.125 0.3203125 +vt 0.125 0.2890625 +vt 0.1875 0.2890625 +vn -0.009559954570200835 0.156094648037632 -0.9876958378583074 +vn 0.9969192112645133 0.07838739569696003 0.002739052315863333 +vn 0.009559954570200835 -0.156094648037632 0.9876958378583074 +vn -0.9969192112645133 -0.07838739569696003 -0.002739052315863333 +vn -0.07785045587764094 0.9846267704312412 0.1563631333460272 +vn 0.07785045587764094 -0.9846267704312412 -0.1563631333460272 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o hat4 +v -0.06724991963009147 2.6297151428282426 0.2756843667494786 +v -0.07166686964939262 2.6629564553960905 0.18813684713130974 +v -0.03980089941132947 2.486376747492985 0.21987470619018534 +v -0.04421784943063062 2.519618060060833 0.1323271865720166 +v -0.16385315668047867 2.645999001844819 0.18634918036183534 +v -0.15943620666117758 2.6127576892769713 0.2738966999800041 +v -0.13640413646171662 2.502660606509562 0.1305395198025422 +v -0.13198718644241553 2.469419293941714 0.21808703942071106 +vt 0.015625 0.25 +vt 0.03125 0.25 +vt 0.03125 0.234375 +vt 0.015625 0.234375 +vt 0 0.25 +vt 0.015625 0.25 +vt 0.015625 0.234375 +vt 0 0.234375 +vt 0.046875 0.25 +vt 0.0625 0.25 +vt 0.0625 0.234375 +vt 0.046875 0.234375 +vt 0.03125 0.25 +vt 0.046875 0.25 +vt 0.046875 0.234375 +vt 0.03125 0.234375 +vt 0.03125 0.25 +vt 0.015625 0.25 +vt 0.015625 0.2578125 +vt 0.03125 0.2578125 +vt 0.046875 0.2578125 +vt 0.03125 0.2578125 +vt 0.03125 0.25 +vt 0.046875 0.25 +vn -0.04711413353921201 0.3545740007237079 -0.9338402092604674 +vn 0.9833203949982517 0.18087950454689375 0.019068445541060658 +vn 0.04711413353921201 -0.3545740007237079 0.9338402092604674 +vn -0.9833203949982517 -0.18087950454689375 -0.019068445541060658 +vn -0.17567372940007692 0.9173657301456462 0.3571818275794757 +vn 0.17567372940007692 -0.9173657301456462 -0.3571818275794757 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o body +v 0.25 1.5 0.1875 +v 0.25 1.5 -0.1875 +v 0.25 0.75 0.1875 +v 0.25 0.75 -0.1875 +v -0.25 1.5 -0.1875 +v -0.25 1.5 0.1875 +v -0.25 0.75 -0.1875 +v -0.25 0.75 0.1875 +vt 0.34375 0.796875 +vt 0.46875 0.796875 +vt 0.46875 0.703125 +vt 0.34375 0.703125 +vt 0.25 0.796875 +vt 0.34375 0.796875 +vt 0.34375 0.703125 +vt 0.25 0.703125 +vt 0.5625 0.796875 +vt 0.6875 0.796875 +vt 0.6875 0.703125 +vt 0.5625 0.703125 +vt 0.46875 0.796875 +vt 0.5625 0.796875 +vt 0.5625 0.703125 +vt 0.46875 0.703125 +vt 0.46875 0.796875 +vt 0.34375 0.796875 +vt 0.34375 0.84375 +vt 0.46875 0.84375 +vt 0.59375 0.84375 +vt 0.46875 0.84375 +vt 0.46875 0.796875 +vt 0.59375 0.796875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o body +v 0.28125 1.53125 0.21875 +v 0.28125 1.53125 -0.21875 +v 0.28125 0.34375 0.21875 +v 0.28125 0.34375 -0.21875 +v -0.28125 1.53125 -0.21875 +v -0.28125 1.53125 0.21875 +v -0.28125 0.34375 -0.21875 +v -0.28125 0.34375 0.21875 +vt 0.09375 0.65625 +vt 0.21875 0.65625 +vt 0.21875 0.515625 +vt 0.09375 0.515625 +vt 0 0.65625 +vt 0.09375 0.65625 +vt 0.09375 0.515625 +vt 0 0.515625 +vt 0.3125 0.65625 +vt 0.4375 0.65625 +vt 0.4375 0.515625 +vt 0.3125 0.515625 +vt 0.21875 0.65625 +vt 0.3125 0.65625 +vt 0.3125 0.515625 +vt 0.21875 0.515625 +vt 0.21875 0.65625 +vt 0.09375 0.65625 +vt 0.09375 0.703125 +vt 0.21875 0.703125 +vt 0.34375 0.703125 +vt 0.21875 0.703125 +vt 0.21875 0.65625 +vt 0.34375 0.65625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o arms +v 0.25 1.198223304703363 0 +v 0.25 1.375 -0.17677669529663698 +v 0.25 1.0214466094067263 -0.17677669529663698 +v 0.25 1.1982233047033632 -0.35355339059327373 +v -0.25 1.375 -0.17677669529663698 +v -0.25 1.198223304703363 0 +v -0.25 1.1982233047033632 -0.35355339059327373 +v -0.25 1.0214466094067263 -0.17677669529663698 +vt 0.6875 0.671875 +vt 0.8125 0.671875 +vt 0.8125 0.640625 +vt 0.6875 0.640625 +vt 0.625 0.671875 +vt 0.6875 0.671875 +vt 0.6875 0.640625 +vt 0.625 0.640625 +vt 0.875 0.671875 +vt 1 0.671875 +vt 1 0.640625 +vt 0.875 0.640625 +vt 0.8125 0.671875 +vt 0.875 0.671875 +vt 0.875 0.640625 +vt 0.8125 0.640625 +vt 0.8125 0.671875 +vt 0.6875 0.671875 +vt 0.6875 0.703125 +vt 0.8125 0.703125 +vt 0.9375 0.703125 +vt 0.8125 0.703125 +vt 0.8125 0.671875 +vt 0.9375 0.671875 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o arms +v 0.5 1.375 0.17677669529663698 +v 0.5 1.551776695296637 0 +v 0.5 1.0214466094067263 -0.17677669529663698 +v 0.5 1.1982233047033632 -0.35355339059327373 +v 0.25 1.551776695296637 0 +v 0.25 1.375 0.17677669529663698 +v 0.25 1.1982233047033632 -0.35355339059327373 +v 0.25 1.0214466094067263 -0.17677669529663698 +vt 0.75 0.796875 +vt 0.8125 0.796875 +vt 0.8125 0.734375 +vt 0.75 0.734375 +vt 0.6875 0.796875 +vt 0.75 0.796875 +vt 0.75 0.734375 +vt 0.6875 0.734375 +vt 0.875 0.796875 +vt 0.9375 0.796875 +vt 0.9375 0.734375 +vt 0.875 0.734375 +vt 0.8125 0.796875 +vt 0.875 0.796875 +vt 0.875 0.734375 +vt 0.8125 0.734375 +vt 0.8125 0.796875 +vt 0.75 0.796875 +vt 0.75 0.828125 +vt 0.8125 0.828125 +vt 0.875 0.828125 +vt 0.8125 0.828125 +vt 0.8125 0.796875 +vt 0.875 0.796875 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 +o arms +v -0.25 1.375 0.17677669529663698 +v -0.25 1.551776695296637 0 +v -0.25 1.0214466094067263 -0.17677669529663698 +v -0.25 1.1982233047033632 -0.35355339059327373 +v -0.5 1.551776695296637 0 +v -0.5 1.375 0.17677669529663698 +v -0.5 1.1982233047033632 -0.35355339059327373 +v -0.5 1.0214466094067263 -0.17677669529663698 +vt 0.75 0.796875 +vt 0.8125 0.796875 +vt 0.8125 0.734375 +vt 0.75 0.734375 +vt 0.6875 0.796875 +vt 0.75 0.796875 +vt 0.75 0.734375 +vt 0.6875 0.734375 +vt 0.875 0.796875 +vt 0.9375 0.796875 +vt 0.9375 0.734375 +vt 0.875 0.734375 +vt 0.8125 0.796875 +vt 0.875 0.796875 +vt 0.875 0.734375 +vt 0.8125 0.734375 +vt 0.8125 0.796875 +vt 0.75 0.796875 +vt 0.75 0.828125 +vt 0.8125 0.828125 +vt 0.875 0.828125 +vt 0.8125 0.828125 +vt 0.8125 0.796875 +vt 0.875 0.796875 +vn 0 0.7071067811865476 -0.7071067811865475 +vn 1 0 0 +vn 0 -0.7071067811865476 0.7071067811865475 +vn -1 0 0 +vn 0 0.7071067811865475 0.7071067811865476 +vn 0 -0.7071067811865475 -0.7071067811865476 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 92/268/67 95/267/67 93/266/67 90/265/67 +f 91/272/68 92/271/68 90/270/68 89/269/68 +f 96/276/69 91/275/69 89/274/69 94/273/69 +f 95/280/70 96/279/70 94/278/70 93/277/70 +f 94/284/71 89/283/71 90/282/71 93/281/71 +f 95/288/72 92/287/72 91/286/72 96/285/72 +o leg0 +v 0.25 0.75 0.125 +v 0.25 0.75 -0.125 +v 0.25 0 0.125 +v 0.25 0 -0.125 +v 0 0.75 -0.125 +v 0 0.75 0.125 +v 0 0 -0.125 +v 0 0 0.125 +vt 0.0625 0.796875 +vt 0.125 0.796875 +vt 0.125 0.703125 +vt 0.0625 0.703125 +vt 0 0.796875 +vt 0.0625 0.796875 +vt 0.0625 0.703125 +vt 0 0.703125 +vt 0.1875 0.796875 +vt 0.25 0.796875 +vt 0.25 0.703125 +vt 0.1875 0.703125 +vt 0.125 0.796875 +vt 0.1875 0.796875 +vt 0.1875 0.703125 +vt 0.125 0.703125 +vt 0.125 0.796875 +vt 0.0625 0.796875 +vt 0.0625 0.828125 +vt 0.125 0.828125 +vt 0.1875 0.828125 +vt 0.125 0.828125 +vt 0.125 0.796875 +vt 0.1875 0.796875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 100/292/73 103/291/73 101/290/73 98/289/73 +f 99/296/74 100/295/74 98/294/74 97/293/74 +f 104/300/75 99/299/75 97/298/75 102/297/75 +f 103/304/76 104/303/76 102/302/76 101/301/76 +f 102/308/77 97/307/77 98/306/77 101/305/77 +f 103/312/78 100/311/78 99/310/78 104/309/78 +o leg1 +v 0 0.75 0.125 +v 0 0.75 -0.125 +v 0 0 0.125 +v 0 0 -0.125 +v -0.25 0.75 -0.125 +v -0.25 0.75 0.125 +v -0.25 0 -0.125 +v -0.25 0 0.125 +vt 0.0625 0.796875 +vt 0.125 0.796875 +vt 0.125 0.703125 +vt 0.0625 0.703125 +vt 0 0.796875 +vt 0.0625 0.796875 +vt 0.0625 0.703125 +vt 0 0.703125 +vt 0.1875 0.796875 +vt 0.25 0.796875 +vt 0.25 0.703125 +vt 0.1875 0.703125 +vt 0.125 0.796875 +vt 0.1875 0.796875 +vt 0.1875 0.703125 +vt 0.125 0.703125 +vt 0.125 0.796875 +vt 0.0625 0.796875 +vt 0.0625 0.828125 +vt 0.125 0.828125 +vt 0.1875 0.828125 +vt 0.125 0.828125 +vt 0.125 0.796875 +vt 0.1875 0.796875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_5cc249a3-cb80-69af-8be3-aeafeadc66a1 +f 108/316/79 111/315/79 109/314/79 106/313/79 +f 107/320/80 108/319/80 106/318/80 105/317/80 +f 112/324/81 107/323/81 105/322/81 110/321/81 +f 111/328/82 112/327/82 110/326/82 109/325/82 +f 110/332/83 105/331/83 106/330/83 109/329/83 +f 111/336/84 108/335/84 107/334/84 112/333/84 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/wolf.obj b/renderer/viewer/three/entity/models/wolf.obj new file mode 100644 index 00000000..086cda39 --- /dev/null +++ b/renderer/viewer/three/entity/models/wolf.obj @@ -0,0 +1,509 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o head +v 0.25 0.84375 -0.3125 +v 0.25 0.84375 -0.5625 +v 0.25 0.46875 -0.3125 +v 0.25 0.46875 -0.5625 +v -0.125 0.84375 -0.5625 +v -0.125 0.84375 -0.3125 +v -0.125 0.46875 -0.5625 +v -0.125 0.46875 -0.3125 +vt 0.0625 0.875 +vt 0.15625 0.875 +vt 0.15625 0.6875 +vt 0.0625 0.6875 +vt 0 0.875 +vt 0.0625 0.875 +vt 0.0625 0.6875 +vt 0 0.6875 +vt 0.21875 0.875 +vt 0.3125 0.875 +vt 0.3125 0.6875 +vt 0.21875 0.6875 +vt 0.15625 0.875 +vt 0.21875 0.875 +vt 0.21875 0.6875 +vt 0.15625 0.6875 +vt 0.15625 0.875 +vt 0.0625 0.875 +vt 0.0625 1 +vt 0.15625 1 +vt 0.25 1 +vt 0.15625 1 +vt 0.15625 0.875 +vt 0.25 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o head +v 0.25 0.96875 -0.375 +v 0.25 0.96875 -0.4375 +v 0.25 0.84375 -0.375 +v 0.25 0.84375 -0.4375 +v 0.125 0.96875 -0.4375 +v 0.125 0.96875 -0.375 +v 0.125 0.84375 -0.4375 +v 0.125 0.84375 -0.375 +vt 0.265625 0.53125 +vt 0.296875 0.53125 +vt 0.296875 0.46875 +vt 0.265625 0.46875 +vt 0.25 0.53125 +vt 0.265625 0.53125 +vt 0.265625 0.46875 +vt 0.25 0.46875 +vt 0.3125 0.53125 +vt 0.34375 0.53125 +vt 0.34375 0.46875 +vt 0.3125 0.46875 +vt 0.296875 0.53125 +vt 0.3125 0.53125 +vt 0.3125 0.46875 +vt 0.296875 0.46875 +vt 0.296875 0.53125 +vt 0.265625 0.53125 +vt 0.265625 0.5625 +vt 0.296875 0.5625 +vt 0.328125 0.5625 +vt 0.296875 0.5625 +vt 0.296875 0.53125 +vt 0.328125 0.53125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o head +v 0 0.96875 -0.375 +v 0 0.96875 -0.4375 +v 0 0.84375 -0.375 +v 0 0.84375 -0.4375 +v -0.125 0.96875 -0.4375 +v -0.125 0.96875 -0.375 +v -0.125 0.84375 -0.4375 +v -0.125 0.84375 -0.375 +vt 0.265625 0.53125 +vt 0.296875 0.53125 +vt 0.296875 0.46875 +vt 0.265625 0.46875 +vt 0.25 0.53125 +vt 0.265625 0.53125 +vt 0.265625 0.46875 +vt 0.25 0.46875 +vt 0.3125 0.53125 +vt 0.34375 0.53125 +vt 0.34375 0.46875 +vt 0.3125 0.46875 +vt 0.296875 0.53125 +vt 0.3125 0.53125 +vt 0.3125 0.46875 +vt 0.296875 0.46875 +vt 0.296875 0.53125 +vt 0.265625 0.53125 +vt 0.265625 0.5625 +vt 0.296875 0.5625 +vt 0.328125 0.5625 +vt 0.296875 0.5625 +vt 0.296875 0.53125 +vt 0.328125 0.53125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o head +v 0.15625 0.6572268750000001 -0.5 +v 0.15625 0.6572268750000001 -0.75 +v 0.15625 0.469726875 -0.5 +v 0.15625 0.469726875 -0.75 +v -0.03125 0.6572268750000001 -0.75 +v -0.03125 0.6572268750000001 -0.5 +v -0.03125 0.469726875 -0.75 +v -0.03125 0.469726875 -0.5 +vt 0.0625 0.5625 +vt 0.109375 0.5625 +vt 0.109375 0.46875 +vt 0.0625 0.46875 +vt 0 0.5625 +vt 0.0625 0.5625 +vt 0.0625 0.46875 +vt 0 0.46875 +vt 0.171875 0.5625 +vt 0.21875 0.5625 +vt 0.21875 0.46875 +vt 0.171875 0.46875 +vt 0.109375 0.5625 +vt 0.171875 0.5625 +vt 0.171875 0.46875 +vt 0.109375 0.46875 +vt 0.109375 0.5625 +vt 0.0625 0.5625 +vt 0.0625 0.6875 +vt 0.109375 0.6875 +vt 0.15625 0.6875 +vt 0.109375 0.6875 +vt 0.109375 0.5625 +vt 0.15625 0.5625 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o body +v 0.25 0.8125 1.1102230246251565e-16 +v 0.25 0.4375 0 +v 0.25 0.8125 0.5625 +v 0.25 0.4375 0.5625 +v -0.125 0.4375 0 +v -0.125 0.8125 1.1102230246251565e-16 +v -0.125 0.4375 0.5625 +v -0.125 0.8125 0.5625 +vt 0.375 0.375 +vt 0.46875 0.375 +vt 0.46875 0.09375 +vt 0.375 0.09375 +vt 0.28125 0.375 +vt 0.375 0.375 +vt 0.375 0.09375 +vt 0.28125 0.09375 +vt 0.5625 0.375 +vt 0.65625 0.375 +vt 0.65625 0.09375 +vt 0.5625 0.09375 +vt 0.46875 0.375 +vt 0.5625 0.375 +vt 0.5625 0.09375 +vt 0.46875 0.09375 +vt 0.46875 0.375 +vt 0.375 0.375 +vt 0.375 0.5625 +vt 0.46875 0.5625 +vt 0.5625 0.5625 +vt 0.46875 0.5625 +vt 0.46875 0.375 +vt 0.5625 0.375 +vn 0 -1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 -1 +vn 0 -2.220446049250313e-16 1 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o upperBody +v 0.3125 0.4375 1.1102230246251565e-16 +v 0.3125 0.875 0 +v 0.3125 0.4374999999999999 -0.37499999999999994 +v 0.3125 0.875 -0.375 +v -0.1875 0.875 0 +v -0.1875 0.4375 1.1102230246251565e-16 +v -0.1875 0.875 -0.375 +v -0.1875 0.4374999999999999 -0.37499999999999994 +vt 0.4375 0.78125 +vt 0.5625 0.78125 +vt 0.5625 0.59375 +vt 0.4375 0.59375 +vt 0.328125 0.78125 +vt 0.4375 0.78125 +vt 0.4375 0.59375 +vt 0.328125 0.59375 +vt 0.671875 0.78125 +vt 0.796875 0.78125 +vt 0.796875 0.59375 +vt 0.671875 0.59375 +vt 0.5625 0.78125 +vt 0.671875 0.78125 +vt 0.671875 0.59375 +vt 0.5625 0.59375 +vt 0.5625 0.78125 +vt 0.4375 0.78125 +vt 0.4375 1 +vt 0.5625 1 +vt 0.6875 1 +vt 0.5625 1 +vt 0.5625 0.78125 +vt 0.6875 0.78125 +vn 0 1 -2.220446049250313e-16 +vn 1 0 0 +vn 0 -1 2.220446049250313e-16 +vn -1 0 0 +vn 0 2.220446049250313e-16 1 +vn 0 -2.220446049250313e-16 -1 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o leg0 +v 0.21875 0.5 0.5 +v 0.21875 0.5 0.375 +v 0.21875 0 0.5 +v 0.21875 0 0.375 +v 0.09375 0.5 0.375 +v 0.09375 0.5 0.5 +v 0.09375 0 0.375 +v 0.09375 0 0.5 +vt 0.03125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.125 +vt 0.03125 0.125 +vt 0 0.375 +vt 0.03125 0.375 +vt 0.03125 0.125 +vt 0 0.125 +vt 0.09375 0.375 +vt 0.125 0.375 +vt 0.125 0.125 +vt 0.09375 0.125 +vt 0.0625 0.375 +vt 0.09375 0.375 +vt 0.09375 0.125 +vt 0.0625 0.125 +vt 0.0625 0.375 +vt 0.03125 0.375 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.375 +vt 0.09375 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o leg1 +v 0.03125 0.5 0.5 +v 0.03125 0.5 0.375 +v 0.03125 0 0.5 +v 0.03125 0 0.375 +v -0.09375 0.5 0.375 +v -0.09375 0.5 0.5 +v -0.09375 0 0.375 +v -0.09375 0 0.5 +vt 0.03125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.125 +vt 0.03125 0.125 +vt 0 0.375 +vt 0.03125 0.375 +vt 0.03125 0.125 +vt 0 0.125 +vt 0.09375 0.375 +vt 0.125 0.375 +vt 0.125 0.125 +vt 0.09375 0.125 +vt 0.0625 0.375 +vt 0.09375 0.375 +vt 0.09375 0.125 +vt 0.0625 0.125 +vt 0.0625 0.375 +vt 0.03125 0.375 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.375 +vt 0.09375 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o leg2 +v 0.21875 0.5 -0.1875 +v 0.21875 0.5 -0.3125 +v 0.21875 0 -0.1875 +v 0.21875 0 -0.3125 +v 0.09375 0.5 -0.3125 +v 0.09375 0.5 -0.1875 +v 0.09375 0 -0.3125 +v 0.09375 0 -0.1875 +vt 0.03125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.125 +vt 0.03125 0.125 +vt 0 0.375 +vt 0.03125 0.375 +vt 0.03125 0.125 +vt 0 0.125 +vt 0.09375 0.375 +vt 0.125 0.375 +vt 0.125 0.125 +vt 0.09375 0.125 +vt 0.0625 0.375 +vt 0.09375 0.375 +vt 0.09375 0.125 +vt 0.0625 0.125 +vt 0.0625 0.375 +vt 0.03125 0.375 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.375 +vt 0.09375 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o leg3 +v 0.03125 0.5 -0.1875 +v 0.03125 0.5 -0.3125 +v 0.03125 0 -0.1875 +v 0.03125 0 -0.3125 +v -0.09375 0.5 -0.3125 +v -0.09375 0.5 -0.1875 +v -0.09375 0 -0.3125 +v -0.09375 0 -0.1875 +vt 0.03125 0.375 +vt 0.0625 0.375 +vt 0.0625 0.125 +vt 0.03125 0.125 +vt 0 0.375 +vt 0.03125 0.375 +vt 0.03125 0.125 +vt 0 0.125 +vt 0.09375 0.375 +vt 0.125 0.375 +vt 0.125 0.125 +vt 0.09375 0.125 +vt 0.0625 0.375 +vt 0.09375 0.375 +vt 0.09375 0.125 +vt 0.0625 0.125 +vt 0.0625 0.375 +vt 0.03125 0.375 +vt 0.03125 0.4375 +vt 0.0625 0.4375 +vt 0.09375 0.4375 +vt 0.0625 0.4375 +vt 0.0625 0.375 +vt 0.09375 0.375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 +o tail +v 0.125 0.8011970027680619 0.5358485272719404 +v 0.125 0.6988029972319381 0.46415147272805957 +v 0.125 0.5144087845925389 0.9454245494164362 +v 0.125 0.4120147790564149 0.8737274948725555 +v 0 0.6988029972319381 0.46415147272805957 +v 0 0.8011970027680619 0.5358485272719404 +v 0 0.4120147790564149 0.8737274948725555 +v 0 0.5144087845925389 0.9454245494164362 +vt 0.171875 0.375 +vt 0.203125 0.375 +vt 0.203125 0.125 +vt 0.171875 0.125 +vt 0.140625 0.375 +vt 0.171875 0.375 +vt 0.171875 0.125 +vt 0.140625 0.125 +vt 0.234375 0.375 +vt 0.265625 0.375 +vt 0.265625 0.125 +vt 0.234375 0.125 +vt 0.203125 0.375 +vt 0.234375 0.375 +vt 0.234375 0.125 +vt 0.203125 0.125 +vt 0.203125 0.375 +vt 0.171875 0.375 +vt 0.171875 0.4375 +vt 0.203125 0.4375 +vt 0.234375 0.4375 +vt 0.203125 0.4375 +vt 0.203125 0.375 +vt 0.234375 0.375 +vn 0 -0.8191520442889917 -0.5735764363510463 +vn 1 0 0 +vn 0 0.8191520442889917 0.5735764363510463 +vn -1 0 0 +vn 0 0.5735764363510463 -0.8191520442889917 +vn 0 -0.5735764363510463 0.8191520442889917 +usemtl m_94c40dbc-0261-544f-e827-6e5c4aaaa4dd +f 84/244/61 87/243/61 85/242/61 82/241/61 +f 83/248/62 84/247/62 82/246/62 81/245/62 +f 88/252/63 83/251/63 81/250/63 86/249/63 +f 87/256/64 88/255/64 86/254/64 85/253/64 +f 86/260/65 81/259/65 82/258/65 85/257/65 +f 87/264/66 84/263/66 83/262/66 88/261/66 \ No newline at end of file diff --git a/renderer/viewer/three/entity/models/zombie_villager.obj b/renderer/viewer/three/entity/models/zombie_villager.obj new file mode 100644 index 00000000..22282802 --- /dev/null +++ b/renderer/viewer/three/entity/models/zombie_villager.obj @@ -0,0 +1,463 @@ +# Made in Blockbench 4.9.4 +mtllib materials.mtl + +o Body +v 0.25 1.5 0.1875 +v 0.25 1.5 -0.1875 +v 0.25 0.75 0.1875 +v 0.25 0.75 -0.1875 +v -0.25 1.5 -0.1875 +v -0.25 1.5 0.1875 +v -0.25 0.75 -0.1875 +v -0.25 0.75 0.1875 +vt 0.34375 0.59375 +vt 0.46875 0.59375 +vt 0.46875 0.40625 +vt 0.34375 0.40625 +vt 0.25 0.59375 +vt 0.34375 0.59375 +vt 0.34375 0.40625 +vt 0.25 0.40625 +vt 0.5625 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.40625 +vt 0.5625 0.40625 +vt 0.46875 0.59375 +vt 0.5625 0.59375 +vt 0.5625 0.40625 +vt 0.46875 0.40625 +vt 0.46875 0.59375 +vt 0.34375 0.59375 +vt 0.34375 0.6875 +vt 0.46875 0.6875 +vt 0.59375 0.6875 +vt 0.46875 0.6875 +vt 0.46875 0.59375 +vt 0.59375 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 4/4/1 7/3/1 5/2/1 2/1/1 +f 3/8/2 4/7/2 2/6/2 1/5/2 +f 8/12/3 3/11/3 1/10/3 6/9/3 +f 7/16/4 8/15/4 6/14/4 5/13/4 +f 6/20/5 1/19/5 2/18/5 5/17/5 +f 7/24/6 4/23/6 3/22/6 8/21/6 +o Body +v 0.28125 1.53125 0.21875 +v 0.28125 1.53125 -0.21875 +v 0.28125 0.34375 0.21875 +v 0.28125 0.34375 -0.21875 +v -0.28125 1.53125 -0.21875 +v -0.28125 1.53125 0.21875 +v -0.28125 0.34375 -0.21875 +v -0.28125 0.34375 0.21875 +vt 0.09375 0.3125 +vt 0.21875 0.3125 +vt 0.21875 0.03125 +vt 0.09375 0.03125 +vt 0 0.3125 +vt 0.09375 0.3125 +vt 0.09375 0.03125 +vt 0 0.03125 +vt 0.3125 0.3125 +vt 0.4375 0.3125 +vt 0.4375 0.03125 +vt 0.3125 0.03125 +vt 0.21875 0.3125 +vt 0.3125 0.3125 +vt 0.3125 0.03125 +vt 0.21875 0.03125 +vt 0.21875 0.3125 +vt 0.09375 0.3125 +vt 0.09375 0.40625 +vt 0.21875 0.40625 +vt 0.34375 0.40625 +vt 0.21875 0.40625 +vt 0.21875 0.3125 +vt 0.34375 0.3125 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 12/28/7 15/27/7 13/26/7 10/25/7 +f 11/32/8 12/31/8 10/30/8 9/29/8 +f 16/36/9 11/35/9 9/34/9 14/33/9 +f 15/40/10 16/39/10 14/38/10 13/37/10 +f 14/44/11 9/43/11 10/42/11 13/41/11 +f 15/48/12 12/47/12 11/46/12 16/45/12 +o Head +v 0.265625 2.140625 0.265625 +v 0.265625 2.140625 -0.265625 +v 0.265625 1.484375 0.265625 +v 0.265625 1.484375 -0.265625 +v -0.265625 2.140625 -0.265625 +v -0.265625 2.140625 0.265625 +v -0.265625 1.484375 -0.265625 +v -0.265625 1.484375 0.265625 +vt 0.125 0.875 +vt 0.25 0.875 +vt 0.25 0.71875 +vt 0.125 0.71875 +vt 0 0.875 +vt 0.125 0.875 +vt 0.125 0.71875 +vt 0 0.71875 +vt 0.375 0.875 +vt 0.5 0.875 +vt 0.5 0.71875 +vt 0.375 0.71875 +vt 0.25 0.875 +vt 0.375 0.875 +vt 0.375 0.71875 +vt 0.25 0.71875 +vt 0.25 0.875 +vt 0.125 0.875 +vt 0.125 1 +vt 0.25 1 +vt 0.375 1 +vt 0.25 1 +vt 0.25 0.875 +vt 0.375 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 20/52/13 23/51/13 21/50/13 18/49/13 +f 19/56/14 20/55/14 18/54/14 17/53/14 +f 24/60/15 19/59/15 17/58/15 22/57/15 +f 23/64/16 24/63/16 22/62/16 21/61/16 +f 22/68/17 17/67/17 18/66/17 21/65/17 +f 23/72/18 20/71/18 19/70/18 24/69/18 +o Head +v 0.078125 1.703125 -0.234375 +v 0.078125 1.703125 -0.390625 +v 0.078125 1.421875 -0.234375 +v 0.078125 1.421875 -0.390625 +v -0.078125 1.703125 -0.390625 +v -0.078125 1.703125 -0.234375 +v -0.078125 1.421875 -0.390625 +v -0.078125 1.421875 -0.234375 +vt 0.40625 0.96875 +vt 0.4375 0.96875 +vt 0.4375 0.90625 +vt 0.40625 0.90625 +vt 0.375 0.96875 +vt 0.40625 0.96875 +vt 0.40625 0.90625 +vt 0.375 0.90625 +vt 0.46875 0.96875 +vt 0.5 0.96875 +vt 0.5 0.90625 +vt 0.46875 0.90625 +vt 0.4375 0.96875 +vt 0.46875 0.96875 +vt 0.46875 0.90625 +vt 0.4375 0.90625 +vt 0.4375 0.96875 +vt 0.40625 0.96875 +vt 0.40625 1 +vt 0.4375 1 +vt 0.46875 1 +vt 0.4375 1 +vt 0.4375 0.96875 +vt 0.46875 0.96875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 28/76/19 31/75/19 29/74/19 26/73/19 +f 27/80/20 28/79/20 26/78/20 25/77/20 +f 32/84/21 27/83/21 25/82/21 30/81/21 +f 31/88/22 32/87/22 30/86/22 29/85/22 +f 30/92/23 25/91/23 26/90/23 29/89/23 +f 31/96/24 28/95/24 27/94/24 32/93/24 +o Head Layer +v 0.28125 2.15625 0.28125 +v 0.28125 2.15625 -0.28125 +v 0.28125 1.46875 0.28125 +v 0.28125 1.46875 -0.28125 +v -0.28125 2.15625 -0.28125 +v -0.28125 2.15625 0.28125 +v -0.28125 1.46875 -0.28125 +v -0.28125 1.46875 0.28125 +vt 0.625 0.875 +vt 0.75 0.875 +vt 0.75 0.71875 +vt 0.625 0.71875 +vt 0.5 0.875 +vt 0.625 0.875 +vt 0.625 0.71875 +vt 0.5 0.71875 +vt 0.875 0.875 +vt 1 0.875 +vt 1 0.71875 +vt 0.875 0.71875 +vt 0.75 0.875 +vt 0.875 0.875 +vt 0.875 0.71875 +vt 0.75 0.71875 +vt 0.75 0.875 +vt 0.625 0.875 +vt 0.625 1 +vt 0.75 1 +vt 0.875 1 +vt 0.75 1 +vt 0.75 0.875 +vt 0.875 0.875 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 36/100/25 39/99/25 37/98/25 34/97/25 +f 35/104/26 36/103/26 34/102/26 33/101/26 +f 40/108/27 35/107/27 33/106/27 38/105/27 +f 39/112/28 40/111/28 38/110/28 37/109/28 +f 38/116/29 33/115/29 34/114/29 37/113/29 +f 39/120/30 36/119/30 35/118/30 40/117/30 +o brim +v 0.5062500000000001 2.00625 -0.30625 +v 0.5062500000000001 2.00625 -0.38125 +v 0.5062500000000001 0.9937499999999999 -0.30625 +v 0.5062500000000001 0.9937499999999999 -0.38125 +v -0.50625 2.00625 -0.38125 +v -0.50625 2.00625 -0.30625 +v -0.50625 0.9937499999999999 -0.38125 +v -0.50625 0.9937499999999999 -0.30625 +vt 0.484375 0.25 +vt 0.734375 0.25 +vt 0.734375 0 +vt 0.484375 0 +vt 0.46875 0.25 +vt 0.484375 0.25 +vt 0.484375 0 +vt 0.46875 0 +vt 0.75 0.25 +vt 1 0.25 +vt 1 0 +vt 0.75 0 +vt 0.734375 0.25 +vt 0.75 0.25 +vt 0.75 0 +vt 0.734375 0 +vt 0.734375 0.25 +vt 0.484375 0.25 +vt 0.484375 0.265625 +vt 0.734375 0.265625 +vt 0.984375 0.265625 +vt 0.734375 0.265625 +vt 0.734375 0.25 +vt 0.984375 0.25 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 44/124/31 47/123/31 45/122/31 42/121/31 +f 43/128/32 44/127/32 42/126/32 41/125/32 +f 48/132/33 43/131/33 41/130/33 46/129/33 +f 47/136/34 48/135/34 46/134/34 45/133/34 +f 46/140/35 41/139/35 42/138/35 45/137/35 +f 47/144/36 44/143/36 43/142/36 48/141/36 +o RightArm +v 0.5 1.5 0.125 +v 0.5 1.5 -0.125 +v 0.5 0.75 0.125 +v 0.5 0.75 -0.125 +v 0.25 1.5 -0.125 +v 0.25 1.5 0.125 +v 0.25 0.75 -0.125 +v 0.25 0.75 0.125 +vt 0.75 0.59375 +vt 0.8125 0.59375 +vt 0.8125 0.40625 +vt 0.75 0.40625 +vt 0.6875 0.59375 +vt 0.75 0.59375 +vt 0.75 0.40625 +vt 0.6875 0.40625 +vt 0.875 0.59375 +vt 0.9375 0.59375 +vt 0.9375 0.40625 +vt 0.875 0.40625 +vt 0.8125 0.59375 +vt 0.875 0.59375 +vt 0.875 0.40625 +vt 0.8125 0.40625 +vt 0.8125 0.59375 +vt 0.75 0.59375 +vt 0.75 0.65625 +vt 0.8125 0.65625 +vt 0.875 0.65625 +vt 0.8125 0.65625 +vt 0.8125 0.59375 +vt 0.875 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 52/148/37 55/147/37 53/146/37 50/145/37 +f 51/152/38 52/151/38 50/150/38 49/149/38 +f 56/156/39 51/155/39 49/154/39 54/153/39 +f 55/160/40 56/159/40 54/158/40 53/157/40 +f 54/164/41 49/163/41 50/162/41 53/161/41 +f 55/168/42 52/167/42 51/166/42 56/165/42 +o LeftArm +v -0.25 1.5 0.125 +v -0.25 1.5 -0.125 +v -0.25 0.75 0.125 +v -0.25 0.75 -0.125 +v -0.5 1.5 -0.125 +v -0.5 1.5 0.125 +v -0.5 0.75 -0.125 +v -0.5 0.75 0.125 +vt 0.8125 0.59375 +vt 0.75 0.59375 +vt 0.75 0.40625 +vt 0.8125 0.40625 +vt 0.875 0.59375 +vt 0.8125 0.59375 +vt 0.8125 0.40625 +vt 0.875 0.40625 +vt 0.9375 0.59375 +vt 0.875 0.59375 +vt 0.875 0.40625 +vt 0.9375 0.40625 +vt 0.75 0.59375 +vt 0.6875 0.59375 +vt 0.6875 0.40625 +vt 0.75 0.40625 +vt 0.75 0.59375 +vt 0.8125 0.59375 +vt 0.8125 0.65625 +vt 0.75 0.65625 +vt 0.8125 0.65625 +vt 0.875 0.65625 +vt 0.875 0.59375 +vt 0.8125 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 60/172/43 63/171/43 61/170/43 58/169/43 +f 59/176/44 60/175/44 58/174/44 57/173/44 +f 64/180/45 59/179/45 57/178/45 62/177/45 +f 63/184/46 64/183/46 62/182/46 61/181/46 +f 62/188/47 57/187/47 58/186/47 61/185/47 +f 63/192/48 60/191/48 59/190/48 64/189/48 +o RightLeg +v 0.25 0.75 0.125 +v 0.25 0.75 -0.125 +v 0.25 0 0.125 +v 0.25 0 -0.125 +v 0 0.75 -0.125 +v 0 0.75 0.125 +v 0 0 -0.125 +v 0 0 0.125 +vt 0.0625 0.59375 +vt 0.125 0.59375 +vt 0.125 0.40625 +vt 0.0625 0.40625 +vt 0 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.40625 +vt 0 0.40625 +vt 0.1875 0.59375 +vt 0.25 0.59375 +vt 0.25 0.40625 +vt 0.1875 0.40625 +vt 0.125 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.40625 +vt 0.125 0.40625 +vt 0.125 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.65625 +vt 0.125 0.65625 +vt 0.1875 0.65625 +vt 0.125 0.65625 +vt 0.125 0.59375 +vt 0.1875 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 68/196/49 71/195/49 69/194/49 66/193/49 +f 67/200/50 68/199/50 66/198/50 65/197/50 +f 72/204/51 67/203/51 65/202/51 70/201/51 +f 71/208/52 72/207/52 70/206/52 69/205/52 +f 70/212/53 65/211/53 66/210/53 69/209/53 +f 71/216/54 68/215/54 67/214/54 72/213/54 +o LeftLeg +v 0 0.75 0.125 +v 0 0.75 -0.125 +v 0 0 0.125 +v 0 0 -0.125 +v -0.25 0.75 -0.125 +v -0.25 0.75 0.125 +v -0.25 0 -0.125 +v -0.25 0 0.125 +vt 0.125 0.59375 +vt 0.0625 0.59375 +vt 0.0625 0.40625 +vt 0.125 0.40625 +vt 0.1875 0.59375 +vt 0.125 0.59375 +vt 0.125 0.40625 +vt 0.1875 0.40625 +vt 0.25 0.59375 +vt 0.1875 0.59375 +vt 0.1875 0.40625 +vt 0.25 0.40625 +vt 0.0625 0.59375 +vt 0 0.59375 +vt 0 0.40625 +vt 0.0625 0.40625 +vt 0.0625 0.59375 +vt 0.125 0.59375 +vt 0.125 0.65625 +vt 0.0625 0.65625 +vt 0.125 0.65625 +vt 0.1875 0.65625 +vt 0.1875 0.59375 +vt 0.125 0.59375 +vn 0 0 -1 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +usemtl m_dabf2b9e-3dd1-eda4-ab17-56ae6660288d +f 76/220/55 79/219/55 77/218/55 74/217/55 +f 75/224/56 76/223/56 74/222/56 73/221/56 +f 80/228/57 75/227/57 73/226/57 78/225/57 +f 79/232/58 80/231/58 78/230/58 77/229/58 +f 78/236/59 73/235/59 74/234/59 77/233/59 +f 79/240/60 76/239/60 75/238/60 80/237/60 \ No newline at end of file diff --git a/renderer/viewer/three/entity/objModels.js b/renderer/viewer/three/entity/objModels.js new file mode 100644 index 00000000..edff440b --- /dev/null +++ b/renderer/viewer/three/entity/objModels.js @@ -0,0 +1 @@ +export * as externalModels from './exportedModels' diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts new file mode 100644 index 00000000..04cb00ca --- /dev/null +++ b/renderer/viewer/three/graphicsBackend.ts @@ -0,0 +1,166 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { GraphicsBackendLoader, GraphicsBackend, GraphicsInitOptions, DisplayWorldOptions } from '../../../src/appViewer' +import { ProgressReporter } from '../../../src/core/progressReporter' +import { showNotification } from '../../../src/react/NotificationProvider' +import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug' +import supportedVersions from '../../../src/supportedVersions.mjs' +import { ResourcesManager } from '../../../src/resourcesManager' +import { WorldRendererThree } from './worldrendererThree' +import { DocumentRenderer } from './documentRenderer' +import { PanoramaRenderer } from './panorama' +import { initVR } from './world/vr' + +// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791 +THREE.ColorManagement.enabled = false +globalThis.THREE = THREE + +const getBackendMethods = (worldRenderer: WorldRendererThree) => { + return { + updateMap: worldRenderer.entities.updateMap.bind(worldRenderer.entities), + updateCustomBlock: worldRenderer.updateCustomBlock.bind(worldRenderer), + getBlockInfo: worldRenderer.getBlockInfo.bind(worldRenderer), + playEntityAnimation: worldRenderer.entities.playAnimation.bind(worldRenderer.entities), + damageEntity: worldRenderer.entities.handleDamageEvent.bind(worldRenderer.entities), + updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities), + changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer), + getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer), + reloadWorld: worldRenderer.reloadWorld.bind(worldRenderer), + + addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media), + destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media), + setVideoPlaying: worldRenderer.media.setVideoPlaying.bind(worldRenderer.media), + setVideoSeeking: worldRenderer.media.setVideoSeeking.bind(worldRenderer.media), + setVideoVolume: worldRenderer.media.setVideoVolume.bind(worldRenderer.media), + setVideoSpeed: worldRenderer.media.setVideoSpeed.bind(worldRenderer.media), + + addSectionAnimation (id: string, animation: typeof worldRenderer.sectionsOffsetsAnimations[string]) { + worldRenderer.sectionsOffsetsAnimations[id] = animation + }, + removeSectionAnimation (id: string) { + delete worldRenderer.sectionsOffsetsAnimations[id] + }, + + shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake), + onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media), + downloadMesherLog: worldRenderer.downloadMesherLog.bind(worldRenderer), + + addWaypoint: worldRenderer.waypoints.addWaypoint.bind(worldRenderer.waypoints), + removeWaypoint: worldRenderer.waypoints.removeWaypoint.bind(worldRenderer.waypoints), + + // New method for updating skybox + setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer) + } +} + +export type ThreeJsBackendMethods = ReturnType + +const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitOptions) => { + // Private state + const documentRenderer = new DocumentRenderer(initOptions) + globalThis.renderer = documentRenderer.renderer + + let panoramaRenderer: PanoramaRenderer | null = null + let worldRenderer: WorldRendererThree | null = null + + const startPanorama = async () => { + if (!documentRenderer) throw new Error('Document renderer not initialized') + if (worldRenderer) return + const qs = new URLSearchParams(location.search) + if (qs.get('debugEntities')) { + const fullResourceManager = initOptions.resourcesManager as ResourcesManager + fullResourceManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true } + await fullResourceManager.updateAssetsData({ }) + + displayEntitiesDebugList(fullResourceManager.currentConfig.version) + return + } + + if (!panoramaRenderer) { + panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE) + globalThis.panoramaRenderer = panoramaRenderer + callModsMethod('panoramaCreated', panoramaRenderer) + await panoramaRenderer.start() + callModsMethod('panoramaReady', panoramaRenderer) + } + } + + const startWorld = async (displayOptions: DisplayWorldOptions) => { + if (panoramaRenderer) { + panoramaRenderer.dispose() + panoramaRenderer = null + } + worldRenderer = new WorldRendererThree(documentRenderer.renderer, initOptions, displayOptions) + void initVR(worldRenderer, documentRenderer) + await worldRenderer.worldReadyPromise + documentRenderer.render = (sizeChanged: boolean) => { + worldRenderer?.render(sizeChanged) + } + documentRenderer.inWorldRenderingConfig = displayOptions.inWorldRenderingConfig + window.world = worldRenderer + callModsMethod('worldReady', worldRenderer) + } + + const disconnect = () => { + if (panoramaRenderer) { + panoramaRenderer.dispose() + panoramaRenderer = null + } + if (documentRenderer) { + documentRenderer.dispose() + } + if (worldRenderer) { + worldRenderer.destroy() + worldRenderer = null + } + } + + // Public interface + const backend: GraphicsBackend = { + id: 'threejs', + displayName: `three.js ${THREE.REVISION}`, + startPanorama, + startWorld, + disconnect, + setRendering (rendering) { + documentRenderer.setPaused(!rendering) + if (worldRenderer) worldRenderer.renderingActive = rendering + }, + getDebugOverlay: () => ({ + get entitiesString () { + return worldRenderer?.entities.getDebugString() + }, + }), + updateCamera (pos: Vec3 | null, yaw: number, pitch: number) { + worldRenderer?.setFirstPersonCamera(pos, yaw, pitch) + }, + get soundSystem () { + return worldRenderer?.soundSystem + }, + get backendMethods () { + if (!worldRenderer) return undefined + return getBackendMethods(worldRenderer) + } + } + + globalThis.threeJsBackend = backend + globalThis.resourcesManager = initOptions.resourcesManager + callModsMethod('default', backend) + + return backend +} + +const callModsMethod = (method: string, ...args: any[]) => { + for (const mod of Object.values((window.loadedMods ?? {}) as Record)) { + try { + mod.threeJsBackendModule?.[method]?.(...args) + } catch (err) { + const errorMessage = `[mod three.js] Error calling ${method} on ${mod.name}: ${err}` + showNotification(errorMessage, 'error') + throw new Error(errorMessage) + } + } +} + +createGraphicsBackend.id = 'threejs' +export default createGraphicsBackend diff --git a/renderer/viewer/three/hand.ts b/renderer/viewer/three/hand.ts new file mode 100644 index 00000000..2bd3832b --- /dev/null +++ b/renderer/viewer/three/hand.ts @@ -0,0 +1,89 @@ +import * as THREE from 'three' +import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins' +import { steveTexture } from './entities' + + +export const getMyHand = async (image?: string, userName?: string) => { + let newMap: THREE.Texture + if (!image && !userName) { + newMap = await steveTexture + } else { + if (!image) { + image = await loadSkinFromUsername(userName!, 'skin') + } + if (!image) { + return + } + const { canvas } = await loadSkinImage(image) + newMap = new THREE.CanvasTexture(canvas) + } + + newMap.magFilter = THREE.NearestFilter + newMap.minFilter = THREE.NearestFilter + // right arm + const box = new THREE.BoxGeometry() + const material = new THREE.MeshStandardMaterial() + const slim = false + const mesh = new THREE.Mesh(box, material) + mesh.scale.x = slim ? 3 : 4 + mesh.scale.y = 12 + mesh.scale.z = 4 + setSkinUVs(box, 40, 16, slim ? 3 : 4, 12, 4) + material.map = newMap + material.needsUpdate = true + const group = new THREE.Group() + group.add(mesh) + group.scale.set(0.1, 0.1, 0.1) + mesh.rotation.z = Math.PI + return group +} + +function setUVs ( + box: THREE.BoxGeometry, + u: number, + v: number, + width: number, + height: number, + depth: number, + textureWidth: number, + textureHeight: number +): void { + const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [ + new THREE.Vector2(x1 / textureWidth, 1 - y2 / textureHeight), + new THREE.Vector2(x2 / textureWidth, 1 - y2 / textureHeight), + new THREE.Vector2(x2 / textureWidth, 1 - y1 / textureHeight), + new THREE.Vector2(x1 / textureWidth, 1 - y1 / textureHeight), + ] + + const top = toFaceVertices(u + depth, v, u + width + depth, v + depth) + const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth) + const left = toFaceVertices(u, v + depth, u + depth, v + depth + height) + const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height) + const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth) + const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth) + + const uvAttr = box.attributes.uv as THREE.BufferAttribute + const uvRight = [right[3], right[2], right[0], right[1]] + const uvLeft = [left[3], left[2], left[0], left[1]] + const uvTop = [top[3], top[2], top[0], top[1]] + const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]] + const uvFront = [front[3], front[2], front[0], front[1]] + const uvBack = [back[3], back[2], back[0], back[1]] + + // Create a new array to hold the modified UV data + const newUVData = [] as number[] + + // Iterate over the arrays and copy the data to uvData + for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) { + for (const uv of uvArray) { + newUVData.push(uv.x, uv.y) + } + } + + uvAttr.set(new Float32Array(newUVData)) + uvAttr.needsUpdate = true +} + +function setSkinUVs (box: THREE.BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void { + setUVs(box, u, v, width, height, depth, 64, 64) +} diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts new file mode 100644 index 00000000..f9d00f0e --- /dev/null +++ b/renderer/viewer/three/holdingBlock.ts @@ -0,0 +1,933 @@ +import * as THREE from 'three' +import * as tweenJs from '@tweenjs/tween.js' +import PrismarineItem from 'prismarine-item' +import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' +import { BlockModel } from 'mc-assets' +import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer' +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 { getMyHand } from './hand' +import { WorldRendererThree } from './worldrendererThree' +import { disposeObject } from './threeJsUtils' + +export type HandItemBlock = { + name? + properties? + fullItem? + type: 'block' | 'item' | 'hand' + id?: number +} + +const rotationPositionData = { + itemRight: { + 'rotation': [ + 0, + -90, + 25 + ], + 'translation': [ + 1.13, + 3.2, + 1.13 + ], + 'scale': [ + 0.68, + 0.68, + 0.68 + ] + }, + itemLeft: { + 'rotation': [ + 0, + 90, + -25 + ], + 'translation': [ + 1.13, + 3.2, + 1.13 + ], + 'scale': [ + 0.68, + 0.68, + 0.68 + ] + }, + blockRight: { + 'rotation': [ + 0, + 45, + 0 + ], + 'translation': [ + 0, + 0, + 0 + ], + 'scale': [ + 0.4, + 0.4, + 0.4 + ] + }, + blockLeft: { + 'rotation': [ + 0, + 225, + 0 + ], + 'translation': [ + 0, + 0, + 0 + ], + 'scale': [ + 0.4, + 0.4, + 0.4 + ] + } +} + +export default class HoldingBlock { + // TODO refactor with the tree builder for better visual understanding + holdingBlock: THREE.Object3D | undefined = undefined + blockSwapAnimation: { + switcher: SmoothSwitcher + // hidden: boolean + } | undefined = undefined + cameraGroup = new THREE.Mesh() + objectOuterGroup = new THREE.Group() // 3 + objectInnerGroup = new THREE.Group() // 4 + holdingBlockInnerGroup = new THREE.Group() // 5 + camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100) + stopUpdate = false + lastHeldItem: HandItemBlock | undefined + isSwinging = false + nextIterStopCallbacks: Array<() => void> | undefined + idleAnimator: HandIdleAnimator | undefined + ready = false + lastUpdate = 0 + playerHand: THREE.Object3D | undefined + offHandDisplay = false + offHandModeLegacy = false + + swingAnimator: HandSwingAnimator | undefined + config: WorldRendererConfig + + constructor (public worldRenderer: WorldRendererThree, 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.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) + } + ) + }) + } + } + + updateItem () { + if (!this.ready) return + const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain + if (item) { + void this.setNewItem(item) + } else if (this.offHand) { + void this.setNewItem() + } else { + void this.setNewItem({ + type: 'hand', + }) + } + } + + initCameraGroup () { + this.cameraGroup = new THREE.Mesh() + } + + startSwing () { + this.swingAnimator?.startSwing() + } + + stopSwing () { + this.swingAnimator?.stopSwing() + } + + render (originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) { + if (!this.lastHeldItem) return + const now = performance.now() + if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick + void this.replaceItemModel(this.lastHeldItem) + } + + // Only update idle animation if not swinging + if (this.swingAnimator?.isCurrentlySwinging() || this.swingAnimator?.debugParams.animationStage) { + this.swingAnimator?.update() + } else { + this.idleAnimator?.update() + } + + this.blockSwapAnimation?.switcher.update() + + const scene = new THREE.Scene() + scene.add(this.cameraGroup) + // if (this.camera.aspect !== originalCamera.aspect) { + // this.camera.aspect = originalCamera.aspect + // this.camera.updateProjectionMatrix() + // } + this.updateCameraGroup() + scene.add(ambientLight.clone()) + scene.add(directionalLight.clone()) + + const viewerSize = renderer.getSize(new THREE.Vector2()) + const minSize = Math.min(viewerSize.width, viewerSize.height) + const x = viewerSize.width - minSize + + // Mirror the scene for offhand by scaling + const { offHandDisplay } = this + if (offHandDisplay) { + this.cameraGroup.scale.x = -1 + } + + renderer.autoClear = false + renderer.clearDepth() + if (this.offHandDisplay) { + renderer.setViewport(0, 0, minSize, minSize) + } else { + const x = viewerSize.width - minSize + // if (x) x -= x / 4 + renderer.setViewport(x, 0, minSize, minSize) + } + renderer.render(scene, this.camera) + renderer.setViewport(0, 0, viewerSize.width, viewerSize.height) + + // Reset the mirroring after rendering + if (offHandDisplay) { + this.cameraGroup.scale.x = 1 + } + } + + // worldTest () { + // const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 })) + // mesh.position.set(0.5, 0.5, 0.5) + // const group = new THREE.Group() + // group.add(mesh) + // group.position.set(-0.5, -0.5, -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) + // this.scene.add(outerGroup) + + // new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start() + // } + + async playBlockSwapAnimation (forceState: 'appeared' | 'disappeared') { + this.blockSwapAnimation ??= { + switcher: new SmoothSwitcher( + () => ({ + y: this.objectInnerGroup.position.y + }), + (property, value) => { + if (property === 'y') this.objectInnerGroup.position.y = value + }, + { + y: 16 // units per second + } + ) + } + + const newState = forceState + // if (forceState && newState !== forceState) { + // throw new Error(`forceState does not match current state ${forceState} !== ${newState}`) + // } + + const targetY = this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (newState === 'appeared' ? 1 : -1)) + + // if (newState === this.blockSwapAnimation.switcher.transitioningToStateName) { + // return false + // } + + let cancelled = false + return new Promise((resolve) => { + this.blockSwapAnimation!.switcher.transitionTo( + { y: targetY }, + newState, + () => { + if (!cancelled) { + resolve(true) + } + }, + () => { + cancelled = true + resolve(false) + } + ) + }) + } + + isDifferentItem (block: HandItemBlock | undefined) { + const Item = PrismarineItem(this.worldRenderer.version) + if (!this.lastHeldItem) { + return true + } + if (this.lastHeldItem.name !== block?.name) { + 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)) { + return true + } + + return false + } + + updateCameraGroup () { + if (this.stopUpdate) return + const { camera } = this + this.cameraGroup.position.copy(camera.position) + this.cameraGroup.rotation.copy(camera.rotation) + + // const viewerSize = viewer.renderer.getSize(new THREE.Vector2()) + // const aspect = viewerSize.width / viewerSize.height + const aspect = 1 + + + // Adjust the position based on the aspect ratio + const { position, scale: scaleData } = this.getHandHeld3d() + const distance = -position.z + const side = this.offHandModeLegacy ? -1 : 1 + this.objectOuterGroup.position.set( + distance * position.x * aspect * side, + distance * position.y, + -distance + ) + + // const scale = Math.min(0.8, Math.max(1, 1 * aspect)) + const scale = scaleData * 2.22 * 0.2 + this.objectOuterGroup.scale.set(scale, scale, scale) + } + + lastItemModelName: string | undefined + private async createItemModel (handItem: HandItemBlock): Promise<{ model: THREE.Object3D; type: 'hand' | 'block' | 'item' } | undefined> { + this.lastUpdate = performance.now() + if (!handItem || (handItem.type === 'hand' && !this.playerHand)) return undefined + + let blockInner: THREE.Object3D | undefined + if (handItem.type === 'item' || handItem.type === 'block') { + const result = this.worldRenderer.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) + if (result) { + const { mesh: itemMesh, isBlock, modelName } = result + if (isBlock) { + blockInner = itemMesh + handItem.type = 'block' + } else { + itemMesh.position.set(0.5, 0.5, 0.5) + blockInner = itemMesh + handItem.type = 'item' + } + this.lastItemModelName = modelName + } + } else { + blockInner = this.playerHand! + } + if (!blockInner) return + blockInner.name = 'holdingBlock' + + const rotationDeg = this.getHandHeld3d().rotation + blockInner.rotation.x = THREE.MathUtils.degToRad(rotationDeg.x) + blockInner.rotation.y = THREE.MathUtils.degToRad(rotationDeg.y) + blockInner.rotation.z = THREE.MathUtils.degToRad(rotationDeg.z) + + return { model: blockInner, type: handItem.type } + } + + async replaceItemModel (handItem?: HandItemBlock): Promise { + // if switch animation is in progress, do not replace the item + if (this.blockSwapAnimation?.switcher.isTransitioning) return + + if (!handItem) { + this.holdingBlock?.removeFromParent() + this.holdingBlock = undefined + this.swingAnimator?.stopSwing() + this.swingAnimator = undefined + this.idleAnimator = undefined + return + } + + const result = await this.createItemModel(handItem) + if (!result) return + + // Update the model without changing the group structure + this.holdingBlock?.removeFromParent() + this.holdingBlock = result.model + this.holdingBlockInnerGroup.add(result.model) + + + } + + testUnknownBlockSwitch () { + void this.setNewItem({ + type: 'item', + name: 'minecraft:some-unknown-block', + id: 0, + fullItem: {} + }) + } + + switchRequest = 0 + async setNewItem (handItem?: HandItemBlock) { + if (!this.isDifferentItem(handItem)) return + this.lastItemModelName = undefined + const switchRequest = ++this.switchRequest + this.lastHeldItem = handItem + let playAppearAnimation = false + if (this.holdingBlock) { + // play disappear animation + playAppearAnimation = true + const result = await this.playBlockSwapAnimation('disappeared') + if (!result) return + this.holdingBlock?.removeFromParent() + this.holdingBlock = undefined + } + + if (!handItem) { + this.swingAnimator?.stopSwing() + this.swingAnimator = undefined + this.idleAnimator = undefined + this.blockSwapAnimation = undefined + return + } + + if (switchRequest !== this.switchRequest) return + const result = await this.createItemModel(handItem) + if (!result || switchRequest !== this.switchRequest) return + + const blockOuterGroup = new THREE.Group() + this.holdingBlockInnerGroup.removeFromParent() + this.holdingBlockInnerGroup = new THREE.Group() + this.holdingBlockInnerGroup.add(result.model) + blockOuterGroup.add(this.holdingBlockInnerGroup) + this.holdingBlock = result.model + this.objectInnerGroup = new THREE.Group() + this.objectInnerGroup.add(blockOuterGroup) + this.objectInnerGroup.position.set(-0.5, -0.5, -0.5) + if (playAppearAnimation) { + this.objectInnerGroup.position.y -= this.objectInnerGroup.scale.y * 1.5 + } + Object.assign(blockOuterGroup.position, { x: 0.5, y: 0.5, z: 0.5 }) + + this.objectOuterGroup = new THREE.Group() + this.objectOuterGroup.add(this.objectInnerGroup) + + this.cameraGroup.add(this.objectOuterGroup) + const rotationDeg = this.getHandHeld3d().rotation + this.objectOuterGroup.rotation.y = THREE.MathUtils.degToRad(rotationDeg.yOuter) + + if (playAppearAnimation) { + await this.playBlockSwapAnimation('appeared') + } + + this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup) + this.swingAnimator.type = result.type + if (this.config.viewBobbing) { + this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive) + } + } + + getHandHeld3d () { + const type = this.lastHeldItem?.type ?? 'hand' + const side = this.offHandModeLegacy ? 'Left' : 'Right' + + let scale = 0.8 * 1.15 // default scale for hand + let position = { + x: 0.4, + y: -0.7, + z: -0.45 + } + let rotation = { + x: -32.4, + y: 42.8, + z: -41.3, + yOuter: 0 + } + + if (type === 'item') { + const itemData = rotationPositionData[`item${side}`] + position = { + x: -0.05, + y: -0.7, + z: -0.45 + } + rotation = { + x: itemData.rotation[0], + y: itemData.rotation[1], + z: itemData.rotation[2], + yOuter: 0 + } + scale = itemData.scale[0] * 1.15 + } else if (type === 'block') { + const blockData = rotationPositionData[`block${side}`] + position = { + x: 0.4, + y: -0.7, + z: -0.45 + } + rotation = { + x: blockData.rotation[0], + y: blockData.rotation[1], + z: blockData.rotation[2], + yOuter: 0 + } + scale = blockData.scale[0] * 1.15 + } + + return { + rotation, + position, + scale + } + } +} + +class HandIdleAnimator { + globalTime = 0 + lastTime = 0 + currentState: MovementState + targetState: MovementState + defaultPosition: { x: number; y: number; z: number; rotationX: number; rotationY: number; rotationZ: number } + private readonly idleOffset = { y: 0, rotationZ: 0 } + private readonly tween = new tweenJs.Group() + private idleTween: tweenJs.Tween<{ y: number; rotationZ: number }> | null = null + private readonly stateSwitcher: SmoothSwitcher + + // Debug parameters + private readonly debugParams = { + // Transition durations for different state changes + walkingSpeed: 8, + sprintingSpeed: 16, + walkingAmplitude: { x: 1 / 30, y: 1 / 10, rotationZ: 0.25 }, + sprintingAmplitude: { x: 1 / 30, y: 1 / 10, rotationZ: 0.4 } + } + + private readonly debugGui: DebugGui + + constructor (public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) { + this.handMesh = handMesh + this.globalTime = 0 + this.currentState = 'NOT_MOVING' + this.targetState = 'NOT_MOVING' + + this.defaultPosition = { + x: handMesh.position.x, + y: handMesh.position.y, + z: handMesh.position.z, + rotationX: handMesh.rotation.x, + rotationY: handMesh.rotation.y, + rotationZ: handMesh.rotation.z + } + + // Initialize state switcher with appropriate speeds + this.stateSwitcher = new SmoothSwitcher( + () => { + return { + x: this.handMesh.position.x, + y: this.handMesh.position.y, + z: this.handMesh.position.z, + rotationX: this.handMesh.rotation.x, + rotationY: this.handMesh.rotation.y, + rotationZ: this.handMesh.rotation.z + } + }, + (property, value) => { + switch (property) { + case 'x': this.handMesh.position.x = value; break + case 'y': this.handMesh.position.y = value; break + case 'z': this.handMesh.position.z = value; break + case 'rotationX': this.handMesh.rotation.x = value; break + case 'rotationY': this.handMesh.rotation.y = value; break + case 'rotationZ': this.handMesh.rotation.z = value; break + } + }, + { + x: 2, // units per second + y: 2, + z: 2, + rotation: Math.PI // radians per second + } + ) + + // Initialize debug GUI + this.debugGui = new DebugGui('idle_animator', this.debugParams) + // this.debugGui.activate() + } + + private startIdleAnimation () { + if (this.idleTween) { + this.idleTween.stop() + } + + // Start from current position for smooth transition + this.idleOffset.y = this.handMesh.position.y - this.defaultPosition.y + this.idleOffset.rotationZ = this.handMesh.rotation.z - this.defaultPosition.rotationZ + + this.idleTween = new tweenJs.Tween(this.idleOffset, this.tween) + .to({ + y: 0.05, + rotationZ: 0.05 + }, 3000) + .easing(tweenJs.Easing.Sinusoidal.InOut) + .yoyo(true) + .repeat(Infinity) + .start() + } + + private stopIdleAnimation () { + if (this.idleTween) { + this.idleTween.stop() + this.idleOffset.y = 0 + this.idleOffset.rotationZ = 0 + } + } + + private getStateTransform (state: MovementState, time: number) { + switch (state) { + case 'NOT_MOVING': + case 'SNEAKING': + return { + x: this.defaultPosition.x, + y: this.defaultPosition.y, + z: this.defaultPosition.z, + rotationX: this.defaultPosition.rotationX, + rotationY: this.defaultPosition.rotationY, + rotationZ: this.defaultPosition.rotationZ + } + case 'WALKING': + case 'SPRINTING': { + const speed = state === 'SPRINTING' ? this.debugParams.sprintingSpeed : this.debugParams.walkingSpeed + const amplitude = state === 'SPRINTING' ? this.debugParams.sprintingAmplitude : this.debugParams.walkingAmplitude + + return { + x: this.defaultPosition.x + Math.sin(time * speed) * amplitude.x, + y: this.defaultPosition.y - Math.abs(Math.cos(time * speed)) * amplitude.y, + z: this.defaultPosition.z, + rotationX: this.defaultPosition.rotationX, + rotationY: this.defaultPosition.rotationY, + // rotationZ: this.defaultPosition.rotationZ + Math.sin(time * speed) * amplitude.rotationZ + rotationZ: this.defaultPosition.rotationZ + } + } + } + } + + setState (newState: MovementState) { + if (newState === this.targetState) return + + this.targetState = newState + const noTransition = false + if (this.currentState !== newState) { + // Stop idle animation during state transitions + this.stopIdleAnimation() + + // Calculate new state transform + if (!noTransition) { + // this.globalTime = 0 + const stateTransform = this.getStateTransform(newState, this.globalTime) + + // Start transition to new state + this.stateSwitcher.transitionTo(stateTransform, newState) + // this.updated = false + } + this.currentState = newState + } + } + + updated = false + update () { + this.stateSwitcher.update() + + const now = performance.now() + const deltaTime = (now - this.lastTime) / 1000 + this.lastTime = now + + // Update global time based on current state + if (!this.stateSwitcher.isTransitioning) { + switch (this.currentState) { + case 'NOT_MOVING': + case 'SNEAKING': + this.globalTime = Math.PI / 4 + break + case 'SPRINTING': + case 'WALKING': + this.globalTime += deltaTime + break + } + } + + // Check for state changes from player state + if (this.playerState) { + const newState = this.playerState.movementState + if (newState !== this.targetState) { + this.setState(newState) + } + } + + // If we're not transitioning between states and in a stable state that should have idle animation + if (!this.stateSwitcher.isTransitioning && + (this.currentState === 'NOT_MOVING' || this.currentState === 'SNEAKING')) { + // Start idle animation if not already running + if (!this.idleTween?.isPlaying()) { + this.startIdleAnimation() + } + // Update idle animation + this.tween.update() + + // Apply idle offsets + this.handMesh.position.y = this.defaultPosition.y + this.idleOffset.y + this.handMesh.rotation.z = this.defaultPosition.rotationZ + this.idleOffset.rotationZ + } + + // If we're in a movement state and not transitioning, update the movement animation + if (!this.stateSwitcher.isTransitioning && + (this.currentState === 'WALKING' || this.currentState === 'SPRINTING')) { + const stateTransform = this.getStateTransform(this.currentState, this.globalTime) + Object.assign(this.handMesh.position, stateTransform) + Object.assign(this.handMesh.rotation, { + x: stateTransform.rotationX, + y: stateTransform.rotationY, + z: stateTransform.rotationZ + }) + // this.stateSwitcher.transitionTo(stateTransform, this.currentState) + } + } + + getCurrentState () { + return this.currentState + } + + destroy () { + this.stopIdleAnimation() + this.stateSwitcher.forceFinish() + } +} + +class HandSwingAnimator { + private readonly PI = Math.PI + private animationTimer = 0 + private lastTime = 0 + private isAnimating = false + private stopRequested = false + private readonly originalRotation: THREE.Euler + private readonly originalPosition: THREE.Vector3 + private readonly originalScale: THREE.Vector3 + + readonly debugParams = { + // Animation timing + animationTime: 250, + animationStage: 0, + useClassicSwing: true, + + // Item/Block animation parameters + itemSwingXPosScale: -0.8, + itemSwingYPosScale: 0.2, + itemSwingZPosScale: -0.2, + itemHeightScale: -0.6, + itemPreswingRotY: 45, + itemSwingXRotAmount: -30, + itemSwingYRotAmount: -35, + itemSwingZRotAmount: -5, + + // Hand/Arm animation parameters + armSwingXPosScale: -0.3, + armSwingYPosScale: 0.4, + armSwingZPosScale: -0.4, + armSwingYRotAmount: 70, + armSwingZRotAmount: -20, + armHeightScale: -0.6 + } + + private readonly debugGui: DebugGui + + public type: 'hand' | 'block' | 'item' = 'hand' + + constructor (public handMesh: THREE.Object3D) { + this.handMesh = handMesh + // Store initial transforms + this.originalRotation = handMesh.rotation.clone() + this.originalPosition = handMesh.position.clone() + this.originalScale = handMesh.scale.clone() + + // Initialize debug GUI + this.debugGui = new DebugGui('hand_animator', this.debugParams, undefined, { + animationStage: { + min: 0, + max: 1, + step: 0.01 + }, + // Add ranges for all animation parameters + itemSwingXPosScale: { min: -2, max: 2, step: 0.1 }, + itemSwingYPosScale: { min: -2, max: 2, step: 0.1 }, + itemSwingZPosScale: { min: -2, max: 2, step: 0.1 }, + itemHeightScale: { min: -2, max: 2, step: 0.1 }, + itemPreswingRotY: { min: -180, max: 180, step: 5 }, + itemSwingXRotAmount: { min: -180, max: 180, step: 5 }, + itemSwingYRotAmount: { min: -180, max: 180, step: 5 }, + itemSwingZRotAmount: { min: -180, max: 180, step: 5 }, + armSwingXPosScale: { min: -2, max: 2, step: 0.1 }, + armSwingYPosScale: { min: -2, max: 2, step: 0.1 }, + armSwingZPosScale: { min: -2, max: 2, step: 0.1 }, + armSwingYRotAmount: { min: -180, max: 180, step: 5 }, + armSwingZRotAmount: { min: -180, max: 180, step: 5 }, + armHeightScale: { min: -2, max: 2, step: 0.1 } + }) + // this.debugGui.activate() + } + + update () { + if (!this.isAnimating && !this.debugParams.animationStage) { + // If not animating, ensure we're at original position + this.handMesh.rotation.copy(this.originalRotation) + this.handMesh.position.copy(this.originalPosition) + this.handMesh.scale.copy(this.originalScale) + return + } + + const now = performance.now() + const deltaTime = (now - this.lastTime) / 1000 + this.lastTime = now + + // Update animation progress + this.animationTimer += deltaTime * 1000 // Convert to ms + + // Calculate animation stage (0 to 1) + const stage = this.debugParams.animationStage || Math.min(this.animationTimer / this.debugParams.animationTime, 1) + + if (stage >= 1) { + // Animation complete + if (this.stopRequested) { + // If stop was requested, actually stop now that we've completed a swing + this.isAnimating = false + this.stopRequested = false + this.animationTimer = 0 + this.handMesh.rotation.copy(this.originalRotation) + this.handMesh.position.copy(this.originalPosition) + this.handMesh.scale.copy(this.originalScale) + return + } + // Otherwise reset timer and continue + this.animationTimer = 0 + return + } + + // Start from original transforms + this.handMesh.rotation.copy(this.originalRotation) + this.handMesh.position.copy(this.originalPosition) + this.handMesh.scale.copy(this.originalScale) + + // Calculate swing progress + const swingProgress = stage + const sqrtProgress = Math.sqrt(swingProgress) + const sinProgress = Math.sin(swingProgress * this.PI) + const sinSqrtProgress = Math.sin(sqrtProgress * this.PI) + + if (this.type === 'hand') { + // Hand animation + const xOffset = this.debugParams.armSwingXPosScale * sinSqrtProgress + const yOffset = this.debugParams.armSwingYPosScale * Math.sin(sqrtProgress * this.PI * 2) + const zOffset = this.debugParams.armSwingZPosScale * sinProgress + + this.handMesh.position.x += xOffset + this.handMesh.position.y += yOffset + this.debugParams.armHeightScale * swingProgress + this.handMesh.position.z += zOffset + + // Rotations + this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.armSwingYRotAmount * sinSqrtProgress) + this.handMesh.rotation.z += THREE.MathUtils.degToRad(this.debugParams.armSwingZRotAmount * sinProgress) + } else { + // Item/Block animation + const xOffset = this.debugParams.itemSwingXPosScale * sinSqrtProgress + const yOffset = this.debugParams.itemSwingYPosScale * Math.sin(sqrtProgress * this.PI * 2) + const zOffset = this.debugParams.itemSwingZPosScale * sinProgress + + this.handMesh.position.x += xOffset + this.handMesh.position.y += yOffset + this.debugParams.itemHeightScale * swingProgress + this.handMesh.position.z += zOffset + + // Pre-swing rotation + this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.itemPreswingRotY) + + // Swing rotations + this.handMesh.rotation.x += THREE.MathUtils.degToRad(this.debugParams.itemSwingXRotAmount * sinProgress) + this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.itemSwingYRotAmount * sinSqrtProgress) + this.handMesh.rotation.z += THREE.MathUtils.degToRad(this.debugParams.itemSwingZRotAmount * sinProgress) + } + } + + startSwing () { + this.stopRequested = false + if (this.isAnimating) return + + this.isAnimating = true + this.animationTimer = 0 + this.lastTime = performance.now() + } + + stopSwing () { + if (!this.isAnimating) return + this.stopRequested = true + } + + isCurrentlySwinging () { + return this.isAnimating + } +} + +export const getBlockMeshFromModel = (material: THREE.Material, model: BlockModel, name: string, blockProvider: WorldBlockProvider) => { + const worldRenderModel = blockProvider.transformModel(model, { + name, + properties: {} + }) as any + return getThreeBlockModelGroup(material, [[worldRenderModel]], undefined, 'plains', loadedData) +} diff --git a/renderer/viewer/three/itemMesh.ts b/renderer/viewer/three/itemMesh.ts new file mode 100644 index 00000000..3fa069b9 --- /dev/null +++ b/renderer/viewer/three/itemMesh.ts @@ -0,0 +1,427 @@ +import * as THREE from 'three' + +export interface Create3DItemMeshOptions { + depth: number + pixelSize?: number +} + +export interface Create3DItemMeshResult { + geometry: THREE.BufferGeometry + totalVertices: number + totalTriangles: number +} + +/** + * Creates a 3D item geometry with front/back faces and connecting edges + * from a canvas containing the item texture + */ +export function create3DItemMesh ( + canvas: HTMLCanvasElement, + options: Create3DItemMeshOptions +): Create3DItemMeshResult { + const { depth, pixelSize } = options + + // Validate canvas dimensions + if (canvas.width <= 0 || canvas.height <= 0) { + throw new Error(`Invalid canvas dimensions: ${canvas.width}x${canvas.height}`) + } + + const ctx = canvas.getContext('2d')! + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) + const { data } = imageData + + const w = canvas.width + const h = canvas.height + const halfDepth = depth / 2 + const actualPixelSize = pixelSize ?? (1 / Math.max(w, h)) + + // Find opaque pixels + const isOpaque = (x: number, y: number) => { + if (x < 0 || y < 0 || x >= w || y >= h) return false + const i = (y * w + x) * 4 + return data[i + 3] > 128 // alpha > 128 + } + + const vertices: number[] = [] + const indices: number[] = [] + const uvs: number[] = [] + const normals: number[] = [] + + let vertexIndex = 0 + + // Helper to add a vertex + const addVertex = (x: number, y: number, z: number, u: number, v: number, nx: number, ny: number, nz: number) => { + vertices.push(x, y, z) + uvs.push(u, v) + normals.push(nx, ny, nz) + return vertexIndex++ + } + + // Helper to add a quad (two triangles) + const addQuad = (v0: number, v1: number, v2: number, v3: number) => { + indices.push(v0, v1, v2, v0, v2, v3) + } + + // Convert pixel coordinates to world coordinates + const pixelToWorld = (px: number, py: number) => { + const x = (px / w - 0.5) * actualPixelSize * w + const y = -(py / h - 0.5) * actualPixelSize * h + return { x, y } + } + + // Create a grid of vertices for front and back faces + const frontVertices: Array> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null)) + const backVertices: Array> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null)) + + // Create vertices at pixel corners + for (let py = 0; py <= h; py++) { + for (let px = 0; px <= w; px++) { + const { x, y } = pixelToWorld(px - 0.5, py - 0.5) + + // UV coordinates should map to the texture space of the extracted tile + const u = px / w + const v = py / h + + // Check if this vertex is needed for any face or edge + let needVertex = false + + // Check all 4 adjacent pixels to see if any are opaque + const adjacentPixels = [ + [px - 1, py - 1], // top-left pixel + [px, py - 1], // top-right pixel + [px - 1, py], // bottom-left pixel + [px, py] // bottom-right pixel + ] + + for (const [adjX, adjY] of adjacentPixels) { + if (isOpaque(adjX, adjY)) { + needVertex = true + break + } + } + + if (needVertex) { + frontVertices[py][px] = addVertex(x, y, halfDepth, u, v, 0, 0, 1) + backVertices[py][px] = addVertex(x, y, -halfDepth, u, v, 0, 0, -1) + } + } + } + + // Create front and back faces + for (let py = 0; py < h; py++) { + for (let px = 0; px < w; px++) { + if (!isOpaque(px, py)) continue + + const v00 = frontVertices[py][px] + const v10 = frontVertices[py][px + 1] + const v11 = frontVertices[py + 1][px + 1] + const v01 = frontVertices[py + 1][px] + + const b00 = backVertices[py][px] + const b10 = backVertices[py][px + 1] + const b11 = backVertices[py + 1][px + 1] + const b01 = backVertices[py + 1][px] + + if (v00 !== null && v10 !== null && v11 !== null && v01 !== null) { + // Front face + addQuad(v00, v10, v11, v01) + } + + if (b00 !== null && b10 !== null && b11 !== null && b01 !== null) { + // Back face (reversed winding) + addQuad(b10, b00, b01, b11) + } + } + } + + // Create edge faces for each side of the pixel with proper UVs + for (let py = 0; py < h; py++) { + for (let px = 0; px < w; px++) { + if (!isOpaque(px, py)) continue + + const pixelU = (px + 0.5) / w // Center of current pixel + const pixelV = (py + 0.5) / h + + // Left edge (x = px) + if (!isOpaque(px - 1, py)) { + const f0 = frontVertices[py][px] + const f1 = frontVertices[py + 1][px] + const b0 = backVertices[py][px] + const b1 = backVertices[py + 1][px] + + if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) { + // Create new vertices for edge with current pixel's UV + const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, -1, 0, 0) + const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, -1, 0, 0) + const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, -1, 0, 0) + const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, -1, 0, 0) + addQuad(ef0, ef1, eb1, eb0) + } + } + + // Right edge (x = px + 1) + if (!isOpaque(px + 1, py)) { + const f0 = frontVertices[py + 1][px + 1] + const f1 = frontVertices[py][px + 1] + const b0 = backVertices[py + 1][px + 1] + const b1 = backVertices[py][px + 1] + + if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) { + const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 1, 0, 0) + const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 1, 0, 0) + const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 1, 0, 0) + const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 1, 0, 0) + addQuad(ef0, ef1, eb1, eb0) + } + } + + // Top edge (y = py) + if (!isOpaque(px, py - 1)) { + const f0 = frontVertices[py][px] + const f1 = frontVertices[py][px + 1] + const b0 = backVertices[py][px] + const b1 = backVertices[py][px + 1] + + if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) { + const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, -1, 0) + const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, -1, 0) + const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, -1, 0) + const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, -1, 0) + addQuad(ef0, ef1, eb1, eb0) + } + } + + // Bottom edge (y = py + 1) + if (!isOpaque(px, py + 1)) { + const f0 = frontVertices[py + 1][px + 1] + const f1 = frontVertices[py + 1][px] + const b0 = backVertices[py + 1][px + 1] + const b1 = backVertices[py + 1][px] + + if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) { + const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, 1, 0) + const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, 1, 0) + const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, 1, 0) + const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, 1, 0) + addQuad(ef0, ef1, eb1, eb0) + } + } + } + } + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)) + geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)) + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)) + geometry.setIndex(indices) + + // Compute normals properly + geometry.computeVertexNormals() + + return { + geometry, + totalVertices: vertexIndex, + totalTriangles: indices.length / 3 + } +} + +export interface ItemTextureInfo { + u: number + v: number + sizeX: number + sizeY: number +} + +export interface ItemMeshResult { + mesh: THREE.Object3D + itemsTexture?: THREE.Texture + itemsTextureFlipped?: THREE.Texture + cleanup?: () => void +} + +/** + * Extracts item texture region to a canvas + */ +export function extractItemTextureToCanvas ( + sourceTexture: THREE.Texture, + textureInfo: ItemTextureInfo +): HTMLCanvasElement { + const { u, v, sizeX, sizeY } = textureInfo + + // Calculate canvas size - fix the calculation + const canvasWidth = Math.max(1, Math.floor(sizeX * sourceTexture.image.width)) + const canvasHeight = Math.max(1, Math.floor(sizeY * sourceTexture.image.height)) + + const canvas = document.createElement('canvas') + canvas.width = canvasWidth + canvas.height = canvasHeight + + const ctx = canvas.getContext('2d')! + ctx.imageSmoothingEnabled = false + + // Draw the item texture region to canvas + ctx.drawImage( + sourceTexture.image, + u * sourceTexture.image.width, + v * sourceTexture.image.height, + sizeX * sourceTexture.image.width, + sizeY * sourceTexture.image.height, + 0, + 0, + canvas.width, + canvas.height + ) + + return canvas +} + +/** + * Creates either a 2D or 3D item mesh based on parameters + */ +export function createItemMesh ( + sourceTexture: THREE.Texture, + textureInfo: ItemTextureInfo, + options: { + faceCamera?: boolean + use3D?: boolean + depth?: number + } = {} +): ItemMeshResult { + const { faceCamera = false, use3D = true, depth = 0.04 } = options + const { u, v, sizeX, sizeY } = textureInfo + + if (faceCamera) { + // Create sprite for camera-facing items + const itemsTexture = sourceTexture.clone() + itemsTexture.flipY = true + itemsTexture.offset.set(u, 1 - v - sizeY) + itemsTexture.repeat.set(sizeX, sizeY) + itemsTexture.needsUpdate = true + itemsTexture.magFilter = THREE.NearestFilter + itemsTexture.minFilter = THREE.NearestFilter + + const spriteMat = new THREE.SpriteMaterial({ + map: itemsTexture, + transparent: true, + alphaTest: 0.1, + }) + const mesh = new THREE.Sprite(spriteMat) + + return { + mesh, + itemsTexture, + cleanup () { + itemsTexture.dispose() + } + } + } + + if (use3D) { + // Try to create 3D mesh + try { + const canvas = extractItemTextureToCanvas(sourceTexture, textureInfo) + const { geometry } = create3DItemMesh(canvas, { depth }) + + // Create texture from canvas for the 3D mesh + const itemsTexture = new THREE.CanvasTexture(canvas) + itemsTexture.magFilter = THREE.NearestFilter + itemsTexture.minFilter = THREE.NearestFilter + itemsTexture.wrapS = itemsTexture.wrapT = THREE.ClampToEdgeWrapping + itemsTexture.flipY = false + itemsTexture.needsUpdate = true + + const material = new THREE.MeshStandardMaterial({ + map: itemsTexture, + side: THREE.DoubleSide, + transparent: true, + alphaTest: 0.1, + }) + + const mesh = new THREE.Mesh(geometry, material) + + return { + mesh, + itemsTexture, + cleanup () { + itemsTexture.dispose() + geometry.dispose() + if (material.map) material.map.dispose() + material.dispose() + } + } + } catch (error) { + console.warn('Failed to create 3D item mesh, falling back to 2D:', error) + // Fall through to 2D rendering + } + } + + // Fallback to 2D flat rendering + const itemsTexture = sourceTexture.clone() + itemsTexture.flipY = true + itemsTexture.offset.set(u, 1 - v - sizeY) + itemsTexture.repeat.set(sizeX, sizeY) + itemsTexture.needsUpdate = true + itemsTexture.magFilter = THREE.NearestFilter + itemsTexture.minFilter = THREE.NearestFilter + + const itemsTextureFlipped = itemsTexture.clone() + itemsTextureFlipped.repeat.x *= -1 + itemsTextureFlipped.needsUpdate = true + itemsTextureFlipped.offset.set(u + sizeX, 1 - v - sizeY) + + const material = new THREE.MeshStandardMaterial({ + map: itemsTexture, + transparent: true, + alphaTest: 0.1, + }) + const materialFlipped = new THREE.MeshStandardMaterial({ + map: itemsTextureFlipped, + transparent: true, + alphaTest: 0.1, + }) + const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [ + new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), + new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), + material, materialFlipped, + ]) + + return { + mesh, + itemsTexture, + itemsTextureFlipped, + cleanup () { + itemsTexture.dispose() + itemsTextureFlipped.dispose() + material.dispose() + materialFlipped.dispose() + } + } +} + +/** + * Creates a complete 3D item mesh from a canvas texture + */ +export function createItemMeshFromCanvas ( + canvas: HTMLCanvasElement, + options: Create3DItemMeshOptions +): THREE.Mesh { + const { geometry } = create3DItemMesh(canvas, options) + + // Base color texture for the item + const colorTexture = new THREE.CanvasTexture(canvas) + colorTexture.magFilter = THREE.NearestFilter + colorTexture.minFilter = THREE.NearestFilter + colorTexture.wrapS = colorTexture.wrapT = THREE.ClampToEdgeWrapping + colorTexture.flipY = false // Important for canvas textures + colorTexture.needsUpdate = true + + // Material - no transparency, no alpha test needed for edges + const material = new THREE.MeshBasicMaterial({ + map: colorTexture, + side: THREE.DoubleSide, + transparent: true, + alphaTest: 0.1 + }) + + return new THREE.Mesh(geometry, material) +} diff --git a/renderer/viewer/three/panorama.ts b/renderer/viewer/three/panorama.ts new file mode 100644 index 00000000..254b980c --- /dev/null +++ b/renderer/viewer/three/panorama.ts @@ -0,0 +1,302 @@ +import { join } from 'path' +import * as THREE from 'three' +import { getSyncWorld } from 'renderer/playground/shared' +import { Vec3 } from 'vec3' +import * as tweenJs from '@tweenjs/tween.js' +import type { GraphicsInitOptions } from '../../../src/appViewer' +import { WorldDataEmitter } from '../lib/worldDataEmitter' +import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon' +import { getDefaultRendererState } from '../baseGraphicsBackend' +import { ResourcesManager } from '../../../src/resourcesManager' +import { getInitialPlayerStateRenderer } from '../lib/basePlayerState' +import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './threeJsUtils' +import { WorldRendererThree } from './worldrendererThree' +import { EntityMesh } from './entity/EntityMesh' +import { DocumentRenderer } from './documentRenderer' +import { PANORAMA_VERSION } from './panoramaShared' + +const panoramaFiles = [ + 'panorama_3.png', // right (+x) + 'panorama_1.png', // left (-x) + 'panorama_4.png', // top (+y) + 'panorama_5.png', // bottom (-y) + 'panorama_0.png', // front (+z) + 'panorama_2.png', // back (-z) +] + +export class PanoramaRenderer { + private readonly camera: THREE.PerspectiveCamera + private scene: THREE.Scene + private readonly ambientLight: THREE.AmbientLight + private readonly directionalLight: THREE.DirectionalLight + private panoramaGroup: THREE.Object3D | null = null + private time = 0 + private readonly abortController = new AbortController() + private worldRenderer: WorldRendererCommon | WorldRendererThree | undefined + public WorldRendererClass = WorldRendererThree + public startTimes = new Map() + + constructor (private readonly documentRenderer: DocumentRenderer, private readonly options: GraphicsInitOptions, private readonly doWorldBlocksPanorama = false) { + this.scene = new THREE.Scene() + // #324568 + this.scene.background = new THREE.Color(0x32_45_68) + + // Add ambient light + this.ambientLight = new THREE.AmbientLight(0xcc_cc_cc) + this.scene.add(this.ambientLight) + + // Add directional light + this.directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5) + this.directionalLight.position.set(1, 1, 0.5).normalize() + this.directionalLight.castShadow = true + this.scene.add(this.directionalLight) + + this.camera = new THREE.PerspectiveCamera(85, this.documentRenderer.canvas.width / this.documentRenderer.canvas.height, 0.05, 1000) + this.camera.position.set(0, 0, 0) + this.camera.rotation.set(0, 0, 0) + } + + async start () { + if (this.doWorldBlocksPanorama) { + await this.worldBlocksPanorama() + } else { + this.addClassicPanorama() + } + + + this.documentRenderer.render = (sizeChanged = false) => { + if (sizeChanged) { + this.camera.aspect = this.documentRenderer.canvas.width / this.documentRenderer.canvas.height + this.camera.updateProjectionMatrix() + } + this.documentRenderer.renderer.render(this.scene, this.camera) + } + } + + async debugImageInFrontOfCamera () { + const image = await loadThreeJsTextureFromUrl(join('background', 'panorama_0.png')) + const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), new THREE.MeshBasicMaterial({ map: image })) + mesh.position.set(0, 0, -500) + mesh.rotation.set(0, 0, 0) + this.scene.add(mesh) + } + + addClassicPanorama () { + const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000) + const panorMaterials = [] as THREE.MeshBasicMaterial[] + const fadeInDuration = 200 + + // void this.debugImageInFrontOfCamera() + + for (const file of panoramaFiles) { + const load = async () => { + const { texture } = loadThreeJsTextureFromUrlSync(join('background', file)) + + // Instead of using repeat/offset to flip, we'll use the texture matrix + texture.matrixAutoUpdate = false + texture.matrix.set( + -1, 0, 1, 0, 1, 0, 0, 0, 1 + ) + + texture.wrapS = THREE.ClampToEdgeWrapping + texture.wrapT = THREE.ClampToEdgeWrapping + texture.minFilter = THREE.LinearFilter + texture.magFilter = THREE.LinearFilter + + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + opacity: 0 // Start with 0 opacity + }) + + // Start fade-in when texture is loaded + this.startTimes.set(material, Date.now()) + panorMaterials.push(material) + } + + void load() + } + + const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials) + panoramaBox.onBeforeRender = () => { + this.time += 0.01 + panoramaBox.rotation.y = Math.PI + this.time * 0.01 + panoramaBox.rotation.z = Math.sin(-this.time * 0.001) * 0.001 + + // Time-based fade in animation for each material + for (const material of panorMaterials) { + const startTime = this.startTimes.get(material) + if (startTime) { + const elapsed = Date.now() - startTime + const progress = Math.min(1, elapsed / fadeInDuration) + material.opacity = progress + } + } + } + + const group = new THREE.Object3D() + group.add(panoramaBox) + + // Add squids + for (let i = 0; i < 20; i++) { + const m = new EntityMesh('1.16.4', 'squid').mesh + m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17) + m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX') + const v = Math.random() * 0.01 + m.children[0].onBeforeRender = () => { + m.rotation.y += v + m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2 + } + group.add(m) + } + + this.scene.add(group) + this.panoramaGroup = group + } + + async worldBlocksPanorama () { + const version = PANORAMA_VERSION + const fullResourceManager = this.options.resourcesManager as ResourcesManager + fullResourceManager.currentConfig = { version, noInventoryGui: true, } + await fullResourceManager.updateAssetsData({ }) + if (this.abortController.signal.aborted) return + console.time('load panorama scene') + const world = getSyncWorld(version) + const PrismarineBlock = require('prismarine-block') + const Block = PrismarineBlock(version) + const fullBlocks = loadedData.blocksArray.filter(block => { + // if (block.name.includes('leaves')) return false + if (/* !block.name.includes('wool') && */!block.name.includes('stained_glass')/* && !block.name.includes('terracotta') */) return false + const b = Block.fromStateId(block.defaultState, 0) + if (b.shapes?.length !== 1) return false + const shape = b.shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + }) + const Z = -15 + const sizeX = 100 + const sizeY = 100 + for (let x = -sizeX; x < sizeX; x++) { + for (let y = -sizeY; y < sizeY; y++) { + const block = fullBlocks[Math.floor(Math.random() * fullBlocks.length)] + world.setBlockStateId(new Vec3(x, y, Z), block.defaultState) + } + } + this.camera.updateProjectionMatrix() + this.camera.position.set(0.5, sizeY / 2 + 0.5, 0.5) + this.camera.rotation.set(0, 0, 0) + const initPos = new Vec3(...this.camera.position.toArray()) + const worldView = new WorldDataEmitter(world, 2, initPos) + // worldView.addWaitTime = 0 + if (this.abortController.signal.aborted) return + + this.worldRenderer = new this.WorldRendererClass( + this.documentRenderer.renderer, + this.options, + { + version, + worldView, + inWorldRenderingConfig: defaultWorldRendererConfig, + playerStateReactive: getInitialPlayerStateRenderer().reactive, + rendererState: getDefaultRendererState().reactive, + nonReactiveState: getDefaultRendererState().nonReactive + } + ) + if (this.worldRenderer instanceof WorldRendererThree) { + this.scene = this.worldRenderer.scene + } + void worldView.init(initPos) + + await this.worldRenderer.waitForChunksToRender() + if (this.abortController.signal.aborted) return + // add small camera rotation to side on mouse move depending on absolute position of the cursor + const { camera } = this + const initX = camera.position.x + const initY = camera.position.y + let prevTwin: tweenJs.Tween | undefined + document.body.addEventListener('pointermove', (e) => { + if (e.pointerType !== 'mouse') return + const pos = new THREE.Vector2(e.clientX, e.clientY) + const SCALE = 0.2 + /* -0.5 - 0.5 */ + const xRel = pos.x / window.innerWidth - 0.5 + const yRel = -(pos.y / window.innerHeight - 0.5) + prevTwin?.stop() + const to = { + x: initX + (xRel * SCALE), + y: initY + (yRel * SCALE) + } + prevTwin = new tweenJs.Tween(camera.position).to(to, 0) // todo use the number depending on diff // todo use the number depending on diff + // prevTwin.easing(tweenJs.Easing.Exponential.InOut) + prevTwin.start() + camera.updateProjectionMatrix() + }, { + signal: this.abortController.signal + }) + + console.timeEnd('load panorama scene') + } + + dispose () { + this.scene.clear() + this.worldRenderer?.destroy() + this.abortController.abort() + } +} + +// export class ClassicPanoramaRenderer { +// panoramaGroup: THREE.Object3D + +// constructor (private readonly backgroundFiles: string[], onRender: Array<(sizeChanged: boolean) => void>, addSquids = true) { +// const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000) +// const loader = new THREE.TextureLoader() +// const panorMaterials = [] as THREE.MeshBasicMaterial[] + +// for (const file of this.backgroundFiles) { +// const texture = loader.load(file) + +// // Instead of using repeat/offset to flip, we'll use the texture matrix +// texture.matrixAutoUpdate = false +// texture.matrix.set( +// -1, 0, 1, 0, 1, 0, 0, 0, 1 +// ) + +// texture.wrapS = THREE.ClampToEdgeWrapping // Changed from RepeatWrapping +// texture.wrapT = THREE.ClampToEdgeWrapping // Changed from RepeatWrapping +// texture.minFilter = THREE.LinearFilter +// texture.magFilter = THREE.LinearFilter + +// panorMaterials.push(new THREE.MeshBasicMaterial({ +// map: texture, +// transparent: true, +// side: THREE.DoubleSide, +// depthWrite: false, +// })) +// } + +// const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials) +// panoramaBox.onBeforeRender = () => { +// } + +// const group = new THREE.Object3D() +// group.add(panoramaBox) + +// if (addSquids) { +// // Add squids +// for (let i = 0; i < 20; i++) { +// const m = new EntityMesh('1.16.4', 'squid').mesh +// m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17) +// m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX') +// const v = Math.random() * 0.01 +// onRender.push(() => { +// m.rotation.y += v +// m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2 +// }) +// group.add(m) +// } +// } + +// this.panoramaGroup = group +// } +// } diff --git a/renderer/viewer/three/panoramaShared.ts b/renderer/viewer/three/panoramaShared.ts new file mode 100644 index 00000000..ad80367f --- /dev/null +++ b/renderer/viewer/three/panoramaShared.ts @@ -0,0 +1 @@ +export const PANORAMA_VERSION = '1.21.4' diff --git a/prismarine-viewer/viewer/lib/primitives.js b/renderer/viewer/three/primitives.js similarity index 93% rename from prismarine-viewer/viewer/lib/primitives.js rename to renderer/viewer/three/primitives.js index f206ee87..08c1c49e 100644 --- a/prismarine-viewer/viewer/lib/primitives.js +++ b/renderer/viewer/three/primitives.js @@ -1,8 +1,9 @@ +/* eslint-disable unicorn/no-abusive-eslint-disable */ +/* eslint-disable */ const THREE = require('three') const { MeshLine, MeshLineMaterial } = require('three.meshline') -const { dispose3 } = require('./dispose') -function getMesh (primitive, camera) { +function getMesh(primitive, camera) { if (primitive.type === 'line') { const color = primitive.color ? primitive.color : 0xff0000 const resolution = new THREE.Vector2(window.innerWidth / camera.zoom, window.innerHeight / camera.zoom) @@ -48,24 +49,24 @@ function getMesh (primitive, camera) { } class Primitives { - constructor (scene, camera) { + constructor(scene, camera) { this.scene = scene this.camera = camera this.primitives = {} } - clear () { + clear() { for (const mesh of Object.values(this.primitives)) { this.scene.remove(mesh) - dispose3(mesh) + disposeObject(mesh) } this.primitives = {} } - update (primitive) { + update(primitive) { if (this.primitives[primitive.id]) { this.scene.remove(this.primitives[primitive.id]) - dispose3(this.primitives[primitive.id]) + disposeObject(this.primitives[primitive.id]) delete this.primitives[primitive.id] } @@ -76,7 +77,7 @@ class Primitives { } } -function GridBoxGeometry (geometry, independent) { +function GridBoxGeometry(geometry, independent) { if (!(geometry instanceof THREE.BoxBufferGeometry)) { console.log("GridBoxGeometry: the parameter 'geometry' has to be of the type THREE.BoxBufferGeometry") return geometry @@ -114,7 +115,7 @@ function GridBoxGeometry (geometry, independent) { newGeometry.setIndex(fullIndices) - function indexSide (x, y, shift) { + function indexSide(x, y, shift) { const indices = [] for (let i = 0; i < y + 1; i++) { let index11 = 0 diff --git a/renderer/viewer/three/renderSlot.ts b/renderer/viewer/three/renderSlot.ts new file mode 100644 index 00000000..321633eb --- /dev/null +++ b/renderer/viewer/three/renderSlot.ts @@ -0,0 +1,82 @@ +import { getRenamedData } from 'flying-squid/dist/blockRenames' +import { BlockModel } from 'mc-assets' +import { versionToNumber } from 'mc-assets/dist/utils' +import type { ResourcesManagerCommon } from '../../../src/resourcesManager' + +export type ResolvedItemModelRender = { + modelName: string, + originalItemName?: string +} + +export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): { + texture: string, + blockData: Record & { resolvedModel: BlockModel } | null, + scale: number | null, + slice: number[] | null, + modelName: string | null, +} => { + let itemModelName = model.modelName + const isItem = loadedData.itemsByName[itemModelName] + + // #region normalize item name + if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string + // #endregion + + + let itemTexture + + if (!fullBlockModelSupport) { + const atlas = resourcesManager.currentResources?.guiAtlas?.json + // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) + const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')] + const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName) + if (item) { + const x = item.u * atlas.width + const y = item.v * atlas.height + return { + texture: 'gui', + slice: [x, y, atlas.tileSize, atlas.tileSize], + scale: 0.25, + blockData: null, + modelName: null + } + } + } + + const blockToTopTexture = (r) => r.top ?? r + + try { + if (!appViewer.resourcesManager.currentResources?.itemsRenderer) throw new Error('Items renderer is not available') + itemTexture = + appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) + ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined) + ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')! + } catch (err) { + // get resourcepack from resource manager + reportError?.(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: TODO!): ${err.stack}`) + itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!) + } + + itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!) + + + if ('type' in itemTexture) { + // is item + return { + texture: itemTexture.type, + slice: itemTexture.slice, + modelName: itemModelName, + blockData: null, + scale: null + } + } else { + // is block + return { + texture: 'blocks', + blockData: itemTexture, + modelName: itemModelName, + slice: null, + scale: null + } + } +} diff --git a/renderer/viewer/three/skyboxRenderer.ts b/renderer/viewer/three/skyboxRenderer.ts new file mode 100644 index 00000000..fb9edae6 --- /dev/null +++ b/renderer/viewer/three/skyboxRenderer.ts @@ -0,0 +1,406 @@ +import * as THREE from 'three' +import { DebugGui } from '../lib/DebugGui' + +export const DEFAULT_TEMPERATURE = 0.75 + +export class SkyboxRenderer { + private texture: THREE.Texture | null = null + private mesh: THREE.Mesh | null = null + private skyMesh: THREE.Mesh | null = null + private voidMesh: THREE.Mesh | null = null + + // World state + private worldTime = 0 + private partialTicks = 0 + private viewDistance = 4 + private temperature = DEFAULT_TEMPERATURE + private inWater = false + private waterBreathing = false + private fogBrightness = 0 + private prevFogBrightness = 0 + private readonly fogOrangeness = 0 // Debug property to control sky color orangeness + private readonly distanceFactor = 2.7 + + private readonly brightnessAtPosition = 1 + debugGui: DebugGui + + constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) { + this.debugGui = new DebugGui('skybox_renderer', this, [ + 'temperature', + 'worldTime', + 'inWater', + 'waterBreathing', + 'fogOrangeness', + 'brightnessAtPosition', + 'distanceFactor' + ], { + brightnessAtPosition: { min: 0, max: 1, step: 0.01 }, + temperature: { min: 0, max: 1, step: 0.01 }, + worldTime: { min: 0, max: 24_000, step: 1 }, + fogOrangeness: { min: -1, max: 1, step: 0.01 }, + distanceFactor: { min: 0, max: 5, step: 0.01 }, + }) + + if (!initialImage) { + this.createGradientSky() + } + // this.debugGui.activate() + } + + async init () { + if (this.initialImage) { + await this.setSkyboxImage(this.initialImage) + } + } + + async setSkyboxImage (imageUrl: string) { + // Dispose old textures if they exist + if (this.texture) { + this.texture.dispose() + } + + // Load the equirectangular texture + const textureLoader = new THREE.TextureLoader() + this.texture = await new Promise((resolve) => { + textureLoader.load( + imageUrl, + (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping + texture.encoding = THREE.sRGBEncoding + // Keep pixelated look + texture.minFilter = THREE.NearestFilter + texture.magFilter = THREE.NearestFilter + texture.needsUpdate = true + resolve(texture) + } + ) + }) + + // Create or update the skybox + if (this.mesh) { + // Just update the texture on the existing material + this.mesh.material.map = this.texture + this.mesh.material.needsUpdate = true + } else { + // Create a large sphere geometry for the skybox + const geometry = new THREE.SphereGeometry(500, 60, 40) + // Flip the geometry inside out + geometry.scale(-1, 1, 1) + + // Create material using the loaded texture + const material = new THREE.MeshBasicMaterial({ + map: this.texture, + side: THREE.FrontSide // Changed to FrontSide since we're flipping the geometry + }) + + // Create and add the skybox mesh + this.mesh = new THREE.Mesh(geometry, material) + this.scene.add(this.mesh) + } + } + + update (cameraPosition: THREE.Vector3, newViewDistance: number) { + if (newViewDistance !== this.viewDistance) { + this.viewDistance = newViewDistance + this.updateSkyColors() + } + + if (this.mesh) { + // Update skybox position + this.mesh.position.copy(cameraPosition) + } else if (this.skyMesh) { + // Update gradient sky position + this.skyMesh.position.copy(cameraPosition) + this.voidMesh?.position.copy(cameraPosition) + this.updateSkyColors() // Update colors based on time of day + } + } + + // Update world time + updateTime (timeOfDay: number, partialTicks = 0) { + if (this.debugGui.visible) return + this.worldTime = timeOfDay + this.partialTicks = partialTicks + this.updateSkyColors() + } + + // Update view distance + updateViewDistance (viewDistance: number) { + this.viewDistance = viewDistance + this.updateSkyColors() + } + + // Update temperature (for biome support) + updateTemperature (temperature: number) { + if (this.debugGui.visible) return + this.temperature = temperature + this.updateSkyColors() + } + + // Update water state + updateWaterState (inWater: boolean, waterBreathing: boolean) { + if (this.debugGui.visible) return + this.inWater = inWater + this.waterBreathing = waterBreathing + this.updateSkyColors() + } + + // Update default skybox setting + updateDefaultSkybox (defaultSkybox: boolean) { + if (this.debugGui.visible) return + this.defaultSkybox = defaultSkybox + this.updateSkyColors() + } + + private createGradientSky () { + const size = 64 + const scale = 256 / size + 2 + + { + const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2) + geometry.rotateX(-Math.PI / 2) + geometry.translate(0, 16, 0) + + const material = new THREE.MeshBasicMaterial({ + color: 0xff_ff_ff, + side: THREE.DoubleSide, + depthTest: false + }) + + this.skyMesh = new THREE.Mesh(geometry, material) + this.scene.add(this.skyMesh) + } + + { + const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2) + geometry.rotateX(-Math.PI / 2) + geometry.translate(0, -16, 0) + + const material = new THREE.MeshBasicMaterial({ + color: 0xff_ff_ff, + side: THREE.DoubleSide, + depthTest: false + }) + + this.voidMesh = new THREE.Mesh(geometry, material) + this.scene.add(this.voidMesh) + } + + this.updateSkyColors() + } + + private getFogColor (partialTicks = 0): THREE.Vector3 { + const angle = this.getCelestialAngle(partialTicks) + let rotation = Math.cos(angle * Math.PI * 2) * 2 + 0.5 + rotation = Math.max(0, Math.min(1, rotation)) + + let x = 0.752_941_2 + let y = 0.847_058_83 + let z = 1 + + x *= (rotation * 0.94 + 0.06) + y *= (rotation * 0.94 + 0.06) + z *= (rotation * 0.91 + 0.09) + + return new THREE.Vector3(x, y, z) + } + + private getSkyColor (x = 0, z = 0, partialTicks = 0): THREE.Vector3 { + const angle = this.getCelestialAngle(partialTicks) + let brightness = Math.cos(angle * 3.141_593 * 2) * 2 + 0.5 + + if (brightness < 0) brightness = 0 + if (brightness > 1) brightness = 1 + + const temperature = this.getTemperature(x, z) + const rgb = this.getSkyColorByTemp(temperature) + + const red = ((rgb >> 16) & 0xff) / 255 + const green = ((rgb >> 8) & 0xff) / 255 + const blue = (rgb & 0xff) / 255 + + return new THREE.Vector3( + red * brightness, + green * brightness, + blue * brightness + ) + } + + private calculateCelestialAngle (time: number, partialTicks: number): number { + const modTime = (time % 24_000) + let angle = (modTime + partialTicks) / 24_000 - 0.25 + + if (angle < 0) { + angle++ + } + if (angle > 1) { + angle-- + } + + angle = 1 - ((Math.cos(angle * Math.PI) + 1) / 2) + angle += (angle - angle) / 3 + + return angle + } + + private getCelestialAngle (partialTicks: number): number { + return this.calculateCelestialAngle(this.worldTime, partialTicks) + } + + private getTemperature (x: number, z: number): number { + return this.temperature + } + + private getSkyColorByTemp (temperature: number): number { + temperature /= 3 + if (temperature < -1) temperature = -1 + if (temperature > 1) temperature = 1 + + // Apply debug fog orangeness to hue - positive values make it more orange, negative make it less orange + const baseHue = 0.622_222_2 - temperature * 0.05 + // Orange is around hue 0.08-0.15, so we need to shift from blue-purple (0.62) toward orange + // Use a more dramatic shift and also increase saturation for more noticeable effect + const orangeHue = 0.12 // Orange hue value + const hue = this.fogOrangeness > 0 + ? baseHue + (orangeHue - baseHue) * this.fogOrangeness * 0.8 // Blend toward orange + : baseHue + this.fogOrangeness * 0.1 // Subtle shift for negative values + const saturation = 0.5 + temperature * 0.1 + Math.abs(this.fogOrangeness) * 0.3 // Increase saturation with orangeness + const brightness = 1 + + return this.hsbToRgb(hue, saturation, brightness) + } + + private hsbToRgb (hue: number, saturation: number, brightness: number): number { + let r = 0; let g = 0; let b = 0 + if (saturation === 0) { + r = g = b = Math.floor(brightness * 255 + 0.5) + } else { + const h = (hue - Math.floor(hue)) * 6 + const f = h - Math.floor(h) + const p = brightness * (1 - saturation) + const q = brightness * (1 - saturation * f) + const t = brightness * (1 - (saturation * (1 - f))) + switch (Math.floor(h)) { + case 0: + r = Math.floor(brightness * 255 + 0.5) + g = Math.floor(t * 255 + 0.5) + b = Math.floor(p * 255 + 0.5) + break + case 1: + r = Math.floor(q * 255 + 0.5) + g = Math.floor(brightness * 255 + 0.5) + b = Math.floor(p * 255 + 0.5) + break + case 2: + r = Math.floor(p * 255 + 0.5) + g = Math.floor(brightness * 255 + 0.5) + b = Math.floor(t * 255 + 0.5) + break + case 3: + r = Math.floor(p * 255 + 0.5) + g = Math.floor(q * 255 + 0.5) + b = Math.floor(brightness * 255 + 0.5) + break + case 4: + r = Math.floor(t * 255 + 0.5) + g = Math.floor(p * 255 + 0.5) + b = Math.floor(brightness * 255 + 0.5) + break + case 5: + r = Math.floor(brightness * 255 + 0.5) + g = Math.floor(p * 255 + 0.5) + b = Math.floor(q * 255 + 0.5) + break + } + } + return 0xff_00_00_00 | (r << 16) | (g << 8) | (Math.trunc(b)) + } + + private updateSkyColors () { + if (!this.skyMesh || !this.voidMesh) return + + // If default skybox is disabled, hide the skybox meshes + if (!this.defaultSkybox) { + this.skyMesh.visible = false + this.voidMesh.visible = false + if (this.mesh) { + this.mesh.visible = false + } + return + } + + // Show skybox meshes when default skybox is enabled + this.skyMesh.visible = true + this.voidMesh.visible = true + if (this.mesh) { + this.mesh.visible = true + } + + // Update fog brightness with smooth transition + this.prevFogBrightness = this.fogBrightness + const renderDistance = this.viewDistance / 32 + const targetBrightness = this.brightnessAtPosition * (1 - renderDistance) + renderDistance + this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1 + + // Handle water fog + if (this.inWater) { + const waterViewDistance = this.waterBreathing ? 100 : 5 + this.scene.fog = new THREE.Fog(new THREE.Color(0, 0, 1), 0.0025, waterViewDistance) + this.scene.background = new THREE.Color(0, 0, 1) + + // Update sky and void colors for underwater effect + ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 1)) + ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 0.6)) + return + } + + // Normal sky colors + const viewDistance = this.viewDistance * 16 + const viewFactor = 1 - (0.25 + 0.75 * this.viewDistance / 32) ** 0.25 + + const angle = this.getCelestialAngle(this.partialTicks) + const skyColor = this.getSkyColor(0, 0, this.partialTicks) + const fogColor = this.getFogColor(this.partialTicks) + + const brightness = Math.cos(angle * Math.PI * 2) * 2 + 0.5 + const clampedBrightness = Math.max(0, Math.min(1, brightness)) + + // Interpolate fog brightness + const interpolatedBrightness = this.prevFogBrightness + (this.fogBrightness - this.prevFogBrightness) * this.partialTicks + + const red = (fogColor.x + (skyColor.x - fogColor.x) * viewFactor) * clampedBrightness * interpolatedBrightness + const green = (fogColor.y + (skyColor.y - fogColor.y) * viewFactor) * clampedBrightness * interpolatedBrightness + const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness + + this.scene.background = new THREE.Color(red, green, blue) + this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * this.distanceFactor) + + ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z)) + ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color( + skyColor.x * 0.2 + 0.04, + skyColor.y * 0.2 + 0.04, + skyColor.z * 0.6 + 0.1 + )) + } + + dispose () { + if (this.texture) { + this.texture.dispose() + } + if (this.mesh) { + this.mesh.geometry.dispose() + ;(this.mesh.material as THREE.Material).dispose() + this.scene.remove(this.mesh) + } + if (this.skyMesh) { + this.skyMesh.geometry.dispose() + ;(this.skyMesh.material as THREE.Material).dispose() + this.scene.remove(this.skyMesh) + } + if (this.voidMesh) { + this.voidMesh.geometry.dispose() + ;(this.voidMesh.material as THREE.Material).dispose() + this.scene.remove(this.voidMesh) + } + } +} diff --git a/renderer/viewer/three/threeJsMedia.ts b/renderer/viewer/three/threeJsMedia.ts new file mode 100644 index 00000000..582273d1 --- /dev/null +++ b/renderer/viewer/three/threeJsMedia.ts @@ -0,0 +1,599 @@ +import * as THREE from 'three' +import { sendVideoPlay, sendVideoStop } from '../../../src/customChannels' +import { WorldRendererThree } from './worldrendererThree' +import { ThreeJsSound } from './threeJsSound' + +interface MediaProperties { + position: { x: number, y: number, z: number } + size: { width: number, height: number } + src: string + rotation?: 0 | 1 | 2 | 3 // 0-3 for 0°, 90°, 180°, 270° + doubleSide?: boolean + background?: number // Hexadecimal color (e.g., 0x000000 for black) + opacity?: number // 0-1 value for transparency + uvMapping?: { startU: number, endU: number, startV: number, endV: number } + allowOrigins?: string[] | boolean + loop?: boolean + volume?: number + autoPlay?: boolean + + allowLighting?: boolean +} + +export class ThreeJsMedia { + customMedia = new Map void + positionalAudio?: THREE.PositionalAudio + hadAutoPlayError?: boolean + }>() + + constructor (private readonly worldRenderer: WorldRendererThree) { + this.worldRenderer.onWorldSwitched.push(() => { + this.onWorldGone() + }) + + this.worldRenderer.onRender.push(() => { + this.render() + }) + } + + onWorldGone () { + for (const [id, videoData] of this.customMedia.entries()) { + this.destroyMedia(id) + } + } + + onWorldStop () { + for (const [id, videoData] of this.customMedia.entries()) { + this.setVideoPlaying(id, false) + } + } + + private createErrorTexture (width: number, height: number, background = 0x00_00_00, error = 'Failed to load'): THREE.CanvasTexture { + const canvas = document.createElement('canvas') + const MAX_DIMENSION = 100 + + canvas.width = MAX_DIMENSION + canvas.height = MAX_DIMENSION + + const ctx = canvas.getContext('2d') + if (!ctx) return new THREE.CanvasTexture(canvas) + + // Clear with transparent background + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Add background color + ctx.fillStyle = `rgba(${background >> 16 & 255}, ${background >> 8 & 255}, ${background & 255}, 0.5)` + ctx.fillRect(0, 0, canvas.width, canvas.height) + + // Add red text with size relative to canvas dimensions + ctx.fillStyle = '#ff0000' + ctx.font = 'bold 10px sans-serif' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(error, canvas.width / 2, canvas.height / 2, canvas.width) + + const texture = new THREE.CanvasTexture(canvas) + texture.minFilter = THREE.LinearFilter + texture.magFilter = THREE.LinearFilter + return texture + } + + private createBackgroundTexture (width: number, height: number, color = 0x00_00_00, opacity = 1): THREE.CanvasTexture { + const canvas = document.createElement('canvas') + canvas.width = 1 + canvas.height = 1 + + const ctx = canvas.getContext('2d') + if (!ctx) return new THREE.CanvasTexture(canvas) + + // Convert hex color to rgba + const r = (color >> 16) & 255 + const g = (color >> 8) & 255 + const b = color & 255 + + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})` + ctx.fillRect(0, 0, 1, 1) + + const texture = new THREE.CanvasTexture(canvas) + texture.minFilter = THREE.NearestFilter + texture.magFilter = THREE.NearestFilter + return texture + } + + validateOrigin (src: string, allowOrigins: string[] | boolean) { + if (allowOrigins === true) return true + if (allowOrigins === false) return false + const url = new URL(src) + return allowOrigins.some(origin => url.origin.endsWith(origin)) + } + + onPageInteraction () { + for (const [id, videoData] of this.customMedia.entries()) { + if (videoData.hadAutoPlayError) { + videoData.hadAutoPlayError = false + void videoData.video?.play() + .catch(err => { + if (err.name === 'AbortError') return + console.error('Failed to play video:', err) + videoData.hadAutoPlayError = true + return true + }) + .then((fromCatch) => { + if (fromCatch) return + if (videoData.positionalAudio) { + // workaround: audio has to be recreated + this.addMedia(id, videoData.props) + } + }) + } + } + } + + addMedia (id: string, props: MediaProperties) { + const originalProps = structuredClone(props) + this.destroyMedia(id) + + const { scene } = this.worldRenderer + + const originSecurityError = props.allowOrigins !== undefined && !this.validateOrigin(props.src, props.allowOrigins) + if (originSecurityError) { + console.warn('Remote resource blocked due to security policy', props.src, 'allowed origins:', props.allowOrigins, 'you can control it with `remoteContentNotSameOrigin` option') + props.src = '' + } + + const isImage = props.src.endsWith('.png') || props.src.endsWith('.jpg') || props.src.endsWith('.jpeg') + + let video: HTMLVideoElement | undefined + let positionalAudio: THREE.PositionalAudio | undefined + if (!isImage) { + video = document.createElement('video') + video.src = props.src.endsWith('.gif') ? props.src.replace('.gif', '.mp4') : props.src + video.loop = props.loop ?? true + video.volume = props.volume ?? 1 + video.playsInline = true + video.crossOrigin = 'anonymous' + + // Create positional audio + const soundSystem = this.worldRenderer.soundSystem as ThreeJsSound + soundSystem.initAudioListener() + if (!soundSystem.audioListener) throw new Error('Audio listener not initialized') + positionalAudio = new THREE.PositionalAudio(soundSystem.audioListener) + positionalAudio.setRefDistance(6) + positionalAudio.setVolume(props.volume ?? 1) + scene.add(positionalAudio) + positionalAudio.position.set(props.position.x, props.position.y, props.position.z) + + // Connect video to positional audio + positionalAudio.setMediaElementSource(video) + positionalAudio.connect() + + video.addEventListener('pause', () => { + positionalAudio?.pause() + sendVideoStop(id, 'paused', video!.currentTime) + }) + video.addEventListener('play', () => { + positionalAudio?.play() + sendVideoPlay(id) + }) + video.addEventListener('seeked', () => { + if (positionalAudio && video) { + positionalAudio.offset = video.currentTime + } + }) + video.addEventListener('stalled', () => { + sendVideoStop(id, 'stalled', video!.currentTime) + }) + video.addEventListener('waiting', () => { + sendVideoStop(id, 'waiting', video!.currentTime) + }) + video.addEventListener('error', ({ error }) => { + sendVideoStop(id, `error: ${error}`, video!.currentTime) + }) + video.addEventListener('ended', () => { + sendVideoStop(id, 'ended', video!.currentTime) + }) + } + + + // Create background texture first + const backgroundTexture = this.createBackgroundTexture( + props.size.width, + props.size.height, + props.background, + // props.opacity ?? 1 + ) + + const handleError = (text?: string) => { + const errorTexture = this.createErrorTexture(props.size.width, props.size.height, props.background, text) + material.map = errorTexture + material.needsUpdate = true + } + + // Create a plane geometry with configurable UV mapping + const geometry = new THREE.PlaneGeometry(1, 1) + + // Create material with initial properties using background texture + const MaterialClass = props.allowLighting ? THREE.MeshLambertMaterial : THREE.MeshBasicMaterial + const material = new MaterialClass({ + map: backgroundTexture, + transparent: true, + side: props.doubleSide ? THREE.DoubleSide : THREE.FrontSide, + alphaTest: 0.1 + }) + + const texture = video + ? new THREE.VideoTexture(video) + : new THREE.TextureLoader().load(props.src, () => { + if (this.customMedia.get(id)?.texture === texture) { + material.map = texture + material.needsUpdate = true + } + }, undefined, () => handleError()) // todo cache + texture.minFilter = THREE.NearestFilter + texture.magFilter = THREE.NearestFilter + // texture.format = THREE.RGBAFormat + // texture.colorSpace = THREE.SRGBColorSpace + texture.generateMipmaps = false + + // Create inner mesh for offsets + const mesh = new THREE.Mesh(geometry, material) + + const { mesh: panel } = this.positionMeshExact(mesh, THREE.MathUtils.degToRad((props.rotation ?? 0) * 90), props.position, props.size.width, props.size.height) + + scene.add(panel) + + if (video) { + // Start playing the video + video.play().catch(err => { + if (err.name === 'AbortError') return + console.error('Failed to play video:', err) + videoData.hadAutoPlayError = true + handleError(err.name === 'NotAllowedError' ? 'Waiting for user interaction' : 'Failed to auto play') + }) + + // Update texture in animation loop + mesh.onBeforeRender = () => { + if (video.readyState === video.HAVE_ENOUGH_DATA && (!video.paused || !videoData?.hadAutoPlayError)) { + if (material.map !== texture) { + material.map = texture + material.needsUpdate = true + } + texture.needsUpdate = true + + // Sync audio position with video position + if (positionalAudio) { + positionalAudio.position.copy(panel.position) + positionalAudio.rotation.copy(panel.rotation) + } + } + } + } + + // UV mapping configuration + const updateUVMapping = (config: { startU: number, endU: number, startV: number, endV: number }) => { + const uvs = geometry.attributes.uv.array as Float32Array + uvs[0] = config.startU + uvs[1] = config.startV + uvs[2] = config.endU + uvs[3] = config.startV + uvs[4] = config.endU + uvs[5] = config.endV + uvs[6] = config.startU + uvs[7] = config.endV + geometry.attributes.uv.needsUpdate = true + } + + // Apply initial UV mapping if provided + if (props.uvMapping) { + updateUVMapping(props.uvMapping) + } + + const videoData = { + mesh: panel, + video, + texture, + updateUVMapping, + positionalAudio, + props: originalProps, + hadAutoPlayError: false + } + // Store video data + this.customMedia.set(id, videoData) + + return id + } + + render () { + for (const [id, videoData] of this.customMedia.entries()) { + const chunkX = Math.floor(videoData.props.position.x / 16) * 16 + const chunkZ = Math.floor(videoData.props.position.z / 16) * 16 + const sectionY = Math.floor(videoData.props.position.y / 16) * 16 + + const chunkKey = `${chunkX},${chunkZ}` + const sectionKey = `${chunkX},${sectionY},${chunkZ}` + videoData.mesh.visible = !!this.worldRenderer.sectionObjects[sectionKey] || !!this.worldRenderer.finishedChunks[chunkKey] + } + } + + setVideoPlaying (id: string, playing: boolean) { + const videoData = this.customMedia.get(id) + if (videoData?.video) { + if (playing) { + videoData.video.play().catch(console.error) + } else { + videoData.video.pause() + } + } + } + + setVideoSeeking (id: string, seconds: number) { + const videoData = this.customMedia.get(id) + if (videoData?.video) { + videoData.video.currentTime = seconds + } + } + + setVideoVolume (id: string, volume: number) { + const videoData = this.customMedia.get(id) + if (videoData?.video) { + videoData.video.volume = volume + } + } + + setVideoSpeed (id: string, speed: number) { + const videoData = this.customMedia.get(id) + if (videoData?.video) { + videoData.video.playbackRate = speed + } + } + + destroyMedia (id: string) { + const { scene } = this.worldRenderer + const mediaData = this.customMedia.get(id) + if (mediaData) { + if (mediaData.video) { + mediaData.video.pause() + mediaData.video.src = '' + mediaData.video.remove() + } + if (mediaData.positionalAudio) { + // mediaData.positionalAudio.stop() + // mediaData.positionalAudio.disconnect() + scene.remove(mediaData.positionalAudio) + } + scene.remove(mediaData.mesh) + mediaData.texture.dispose() + + // Get the inner mesh from the group + const mesh = mediaData.mesh.children[0] as THREE.Mesh + if (mesh) { + mesh.geometry.dispose() + if (mesh.material instanceof THREE.Material) { + mesh.material.dispose() + } + } + + this.customMedia.delete(id) + } + } + + /** + * Positions a mesh exactly at startPosition and extends it along the rotation direction + * with the specified width and height + * + * @param mesh The mesh to position + * @param rotation Rotation in radians (applied to Y axis) + * @param startPosition The exact starting position (corner) of the mesh + * @param width Width of the mesh + * @param height Height of the mesh + * @param depth Depth of the mesh (default: 1) + * @returns The positioned mesh for chaining + */ + positionMeshExact ( + mesh: THREE.Mesh, + rotation: number, + startPosition: { x: number, y: number, z: number }, + width: number, + height: number, + depth = 1 + ) { + // avoid z-fighting with the ground plane + if (rotation === 0) { + startPosition.z += 0.001 + } + if (rotation === Math.PI / 2) { + startPosition.x -= 0.001 + } + if (rotation === Math.PI) { + startPosition.z -= 0.001 + } + if (rotation === 3 * Math.PI / 2) { + startPosition.x += 0.001 + } + + // rotation normalize coordinates + if (rotation === 0) { + startPosition.z += 1 + } + if (rotation === Math.PI) { + startPosition.x += 1 + } + if (rotation === 3 * Math.PI / 2) { + startPosition.z += 1 + startPosition.x += 1 + } + + + // First, clean up any previous transformations + mesh.matrix.identity() + mesh.position.set(0, 0, 0) + mesh.rotation.set(0, 0, 0) + mesh.scale.set(1, 1, 1) + + // By default, PlaneGeometry creates a plane in the XY plane (facing +Z) + // We need to set up the proper orientation for our use case + // Rotate the plane to face the correct direction based on the rotation parameter + mesh.rotateY(rotation) + if (rotation === Math.PI / 2 || rotation === 3 * Math.PI / 2) { + mesh.rotateZ(-Math.PI) + mesh.rotateX(-Math.PI) + } + + // Scale it to the desired size + mesh.scale.set(width, height, depth) + + // For a PlaneGeometry, if we want the corner at the origin, we need to offset + // by half the dimensions after scaling + mesh.geometry.translate(0.5, 0.5, 0) + mesh.geometry.attributes.position.needsUpdate = true + + // Now place the mesh at the start position + mesh.position.set(startPosition.x, startPosition.y, startPosition.z) + + // Create a group to hold our mesh and markers + const debugGroup = new THREE.Group() + debugGroup.add(mesh) + + // Add a marker at the starting position (should be exactly at pos) + const startMarker = new THREE.Mesh( + new THREE.BoxGeometry(0.1, 0.1, 0.1), + new THREE.MeshBasicMaterial({ color: 0xff_00_00 }) + ) + startMarker.position.copy(new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z)) + debugGroup.add(startMarker) + + // Add a marker at the end position (width units away in the rotated direction) + const endX = startPosition.x + Math.cos(rotation) * width + const endZ = startPosition.z + Math.sin(rotation) * width + const endYMarker = new THREE.Mesh( + new THREE.BoxGeometry(0.1, 0.1, 0.1), + new THREE.MeshBasicMaterial({ color: 0x00_00_ff }) + ) + endYMarker.position.set(startPosition.x, startPosition.y + height, startPosition.z) + debugGroup.add(endYMarker) + + // Add a marker at the width endpoint + const endWidthMarker = new THREE.Mesh( + new THREE.BoxGeometry(0.1, 0.1, 0.1), + new THREE.MeshBasicMaterial({ color: 0xff_ff_00 }) + ) + endWidthMarker.position.set(endX, startPosition.y, endZ) + debugGroup.add(endWidthMarker) + + // Add a marker at the corner diagonal endpoint (both width and height) + const endCornerMarker = new THREE.Mesh( + new THREE.BoxGeometry(0.1, 0.1, 0.1), + new THREE.MeshBasicMaterial({ color: 0xff_00_ff }) + ) + endCornerMarker.position.set(endX, startPosition.y + height, endZ) + debugGroup.add(endCornerMarker) + + // Also add a visual helper to show the rotation direction + const directionHelper = new THREE.ArrowHelper( + new THREE.Vector3(Math.cos(rotation), 0, Math.sin(rotation)), + new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z), + 1, + 0xff_00_00 + ) + debugGroup.add(directionHelper) + + return { + mesh, + debugGroup + } + } + + createTestCanvasTexture () { + const canvas = document.createElement('canvas') + canvas.width = 100 + canvas.height = 100 + const ctx = canvas.getContext('2d') + if (!ctx) return null + ctx.font = '10px Arial' + ctx.fillStyle = 'red' + ctx.fillText('Hello World', 0, 10) // at + return new THREE.CanvasTexture(canvas) + } + + /** + * Creates a test mesh that demonstrates the exact positioning + */ + addTestMeshExact (rotationNum: number) { + const pos = window.cursorBlockRel().position + console.log('Creating exact positioned test mesh at:', pos) + + // Create a plane mesh with a wireframe to visualize boundaries + const plane = new THREE.Mesh( + new THREE.PlaneGeometry(1, 1), + new THREE.MeshBasicMaterial({ + // side: THREE.DoubleSide, + map: this.createTestCanvasTexture() + }) + ) + + const width = 2 + const height = 1 + const rotation = THREE.MathUtils.degToRad(rotationNum * 90) // 90 degrees in radians + + // Position the mesh exactly where we want it + const { debugGroup } = this.positionMeshExact(plane, rotation, pos, width, height) + + this.worldRenderer.scene.add(debugGroup) + console.log('Exact test mesh added with dimensions:', width, height, 'and rotation:', rotation) + } + + lastCheck = 0 + THROTTLE_TIME = 100 + tryIntersectMedia () { + // hack: need to optimize this by pulling only in distance of interaction instead and throttle + if (this.customMedia.size === 0) return + if (Date.now() - this.lastCheck < this.THROTTLE_TIME) return + this.lastCheck = Date.now() + + const { camera, scene } = this.worldRenderer + const raycaster = new THREE.Raycaster() + + // Get mouse position at center of screen + const mouse = new THREE.Vector2(0, 0) + + // Update the raycaster + raycaster.setFromCamera(mouse, camera) + + // Check intersection with all objects in scene + const intersects = raycaster.intersectObjects(scene.children, true) + if (intersects.length > 0) { + const intersection = intersects[0] + const intersectedObject = intersection.object + + // Find if this object belongs to any media + for (const [id, videoData] of this.customMedia.entries()) { + // Check if the intersected object is part of our media mesh + if (intersectedObject === videoData.mesh || + videoData.mesh.children.includes(intersectedObject)) { + const { uv } = intersection + if (uv) { + const result = { + id, + x: uv.x, + y: uv.y + } + this.worldRenderer.reactiveState.world.intersectMedia = result + this.worldRenderer['debugVideo'] = videoData + this.worldRenderer.cursorBlock.cursorLinesHidden = true + return + } + } + } + } + + // No media intersection found + this.worldRenderer.reactiveState.world.intersectMedia = null + this.worldRenderer['debugVideo'] = null + this.worldRenderer.cursorBlock.cursorLinesHidden = false + } +} diff --git a/renderer/viewer/three/threeJsMethods.ts b/renderer/viewer/three/threeJsMethods.ts new file mode 100644 index 00000000..629909c9 --- /dev/null +++ b/renderer/viewer/three/threeJsMethods.ts @@ -0,0 +1,15 @@ +import type { GraphicsBackend } from '../../../src/appViewer' +import type { ThreeJsBackendMethods } from './graphicsBackend' + +export function getThreeJsRendererMethods (): ThreeJsBackendMethods | undefined { + const renderer = appViewer.backend + if (renderer?.id !== 'threejs' || !renderer.backendMethods) return + return new Proxy(renderer.backendMethods, { + get (target, prop) { + return async (...args) => { + const result = await (target[prop as any] as any)(...args) + return result + } + } + }) as ThreeJsBackendMethods +} diff --git a/renderer/viewer/three/threeJsParticles.ts b/renderer/viewer/three/threeJsParticles.ts new file mode 100644 index 00000000..993f2b62 --- /dev/null +++ b/renderer/viewer/three/threeJsParticles.ts @@ -0,0 +1,160 @@ +import * as THREE from 'three' + +interface ParticleMesh extends THREE.Mesh { + velocity: THREE.Vector3; +} + +interface ParticleConfig { + fountainHeight: number; + resetHeight: number; + xVelocityRange: number; + zVelocityRange: number; + particleCount: number; + particleRadiusRange: { min: number; max: number }; + yVelocityRange: { min: number; max: number }; +} + +export interface FountainOptions { + position?: { x: number, y: number, z: number } + particleConfig?: Partial; +} + +export class Fountain { + private readonly particles: ParticleMesh[] = [] + private readonly config: { particleConfig: ParticleConfig } + private readonly position: THREE.Vector3 + container: THREE.Object3D | undefined + + constructor (public sectionId: string, options: FountainOptions = {}) { + this.position = options.position ? new THREE.Vector3(options.position.x, options.position.y, options.position.z) : new THREE.Vector3(0, 0, 0) + this.config = this.createConfig(options.particleConfig) + } + + private createConfig ( + particleConfigOverride?: Partial + ): { particleConfig: ParticleConfig } { + const particleConfig: ParticleConfig = { + fountainHeight: 10, + resetHeight: 0, + xVelocityRange: 0.4, + zVelocityRange: 0.4, + particleCount: 400, + particleRadiusRange: { min: 0.1, max: 0.6 }, + yVelocityRange: { min: 0.1, max: 2 }, + ...particleConfigOverride + } + + return { particleConfig } + } + + + createParticles (container: THREE.Object3D): void { + this.container = container + const colorStart = new THREE.Color(0xff_ff_00) + const colorEnd = new THREE.Color(0xff_a5_00) + + for (let i = 0; i < this.config.particleConfig.particleCount; i++) { + const radius = Math.random() * + (this.config.particleConfig.particleRadiusRange.max - this.config.particleConfig.particleRadiusRange.min) + + this.config.particleConfig.particleRadiusRange.min + const geometry = new THREE.SphereGeometry(radius) + const material = new THREE.MeshBasicMaterial({ + color: colorStart.clone().lerp(colorEnd, Math.random()) + }) + const mesh = new THREE.Mesh(geometry, material) + const particle = mesh as unknown as ParticleMesh + + particle.position.set( + this.position.x + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange * 2, + this.position.y + this.config.particleConfig.fountainHeight, + this.position.z + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange * 2 + ) + + particle.velocity = new THREE.Vector3( + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange, + -Math.random() * this.config.particleConfig.yVelocityRange.max, + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange + ) + + this.particles.push(particle) + this.container.add(particle) + + // this.container.onBeforeRender = () => { + // this.render() + // } + } + } + + render (): void { + for (const particle of this.particles) { + particle.velocity.y -= 0.01 + Math.random() * 0.1 + particle.position.add(particle.velocity) + + if (particle.position.y < this.position.y + this.config.particleConfig.resetHeight) { + particle.position.set( + this.position.x + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange * 2, + this.position.y + this.config.particleConfig.fountainHeight, + this.position.z + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange * 2 + ) + particle.velocity.set( + (Math.random() - 0.5) * this.config.particleConfig.xVelocityRange, + -Math.random() * this.config.particleConfig.yVelocityRange.max, + (Math.random() - 0.5) * this.config.particleConfig.zVelocityRange + ) + } + } + } + + private updateParticleCount (newCount: number): void { + if (newCount !== this.config.particleConfig.particleCount) { + this.config.particleConfig.particleCount = newCount + const currentCount = this.particles.length + + if (newCount > currentCount) { + this.addParticles(newCount - currentCount) + } else if (newCount < currentCount) { + this.removeParticles(currentCount - newCount) + } + } + } + + private addParticles (count: number): void { + const geometry = new THREE.SphereGeometry(0.1) + const material = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 }) + + for (let i = 0; i < count; i++) { + const mesh = new THREE.Mesh(geometry, material) + const particle = mesh as unknown as ParticleMesh + particle.position.copy(this.position) + particle.velocity = new THREE.Vector3( + Math.random() * this.config.particleConfig.xVelocityRange - + this.config.particleConfig.xVelocityRange / 2, + Math.random() * 2, + Math.random() * this.config.particleConfig.zVelocityRange - + this.config.particleConfig.zVelocityRange / 2 + ) + this.particles.push(particle) + this.container!.add(particle) + } + } + + private removeParticles (count: number): void { + for (let i = 0; i < count; i++) { + const particle = this.particles.pop() + if (particle) { + this.container!.remove(particle) + } + } + } + + public dispose (): void { + for (const particle of this.particles) { + particle.geometry.dispose() + if (Array.isArray(particle.material)) { + for (const material of particle.material) material.dispose() + } else { + particle.material.dispose() + } + } + } +} diff --git a/renderer/viewer/three/threeJsSound.ts b/renderer/viewer/three/threeJsSound.ts new file mode 100644 index 00000000..699bb2cc --- /dev/null +++ b/renderer/viewer/three/threeJsSound.ts @@ -0,0 +1,99 @@ +import * as THREE from 'three' +import { WorldRendererThree } from './worldrendererThree' + +export interface SoundSystem { + playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number, timeout?: number) => void + destroy: () => void +} + +export class ThreeJsSound implements SoundSystem { + audioListener: THREE.AudioListener | undefined + private readonly activeSounds = new Set() + private readonly audioContext: AudioContext | undefined + private readonly soundVolumes = new Map() + baseVolume = 1 + + constructor (public worldRenderer: WorldRendererThree) { + worldRenderer.onWorldSwitched.push(() => { + this.stopAll() + }) + + worldRenderer.onReactiveConfigUpdated('volume', (volume) => { + this.changeVolume(volume) + }) + } + + initAudioListener () { + if (this.audioListener) return + this.audioListener = new THREE.AudioListener() + this.worldRenderer.camera.add(this.audioListener) + } + + playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1, timeout = 500) { + this.initAudioListener() + + const sound = new THREE.PositionalAudio(this.audioListener!) + this.activeSounds.add(sound) + this.soundVolumes.set(sound, volume) + + const audioLoader = new THREE.AudioLoader() + const start = Date.now() + void audioLoader.loadAsync(path).then((buffer) => { + if (Date.now() - start > timeout) { + console.warn('Ignored playing sound', path, 'due to timeout:', timeout, 'ms <', Date.now() - start, 'ms') + return + } + // play + sound.setBuffer(buffer) + sound.setRefDistance(20) + sound.setVolume(volume * this.baseVolume) + sound.setPlaybackRate(pitch) // set the pitch + this.worldRenderer.scene.add(sound) + // set sound position + sound.position.set(position.x, position.y, position.z) + sound.onEnded = () => { + this.worldRenderer.scene.remove(sound) + if (sound.source) { + sound.disconnect() + } + this.activeSounds.delete(sound) + this.soundVolumes.delete(sound) + audioLoader.manager.itemEnd(path) + } + sound.play() + }) + } + + stopAll () { + for (const sound of this.activeSounds) { + if (!sound) continue + sound.stop() + if (sound.source) { + sound.disconnect() + } + this.worldRenderer.scene.remove(sound) + } + this.activeSounds.clear() + this.soundVolumes.clear() + } + + changeVolume (volume: number) { + this.baseVolume = volume + for (const [sound, individualVolume] of this.soundVolumes) { + sound.setVolume(individualVolume * this.baseVolume) + } + } + + destroy () { + this.stopAll() + // Remove and cleanup audio listener + if (this.audioListener) { + this.audioListener.removeFromParent() + this.audioListener = undefined + } + } + + playTestSound () { + this.playSound(this.worldRenderer.camera.position, '/sound.mp3') + } +} diff --git a/renderer/viewer/three/threeJsUtils.ts b/renderer/viewer/three/threeJsUtils.ts new file mode 100644 index 00000000..cbef9065 --- /dev/null +++ b/renderer/viewer/three/threeJsUtils.ts @@ -0,0 +1,73 @@ +import * as THREE from 'three' +import { getLoadedImage } from 'mc-assets/dist/utils' +import { createCanvas } from '../lib/utils' + +export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => { + // not cleaning texture there as it might be used by other objects, but would be good to also do that + if (obj instanceof THREE.Mesh) { + obj.geometry?.dispose?.() + obj.material?.dispose?.() + } + if (obj.children) { + // eslint-disable-next-line unicorn/no-array-for-each + obj.children.forEach(child => disposeObject(child, cleanTextures)) + } + if (cleanTextures) { + if (obj instanceof THREE.Mesh) { + obj.material?.map?.dispose?.() + } + } +} + +let textureCache: Record = {} +let imagesPromises: Record> = {} + +export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => { + const texture = new THREE.Texture() + const promise = getLoadedImage(imageUrl).then(image => { + texture.image = image + texture.needsUpdate = true + return texture + }) + return { + texture, + promise + } +} + +export const loadThreeJsTextureFromUrl = async (imageUrl: string) => { + const loaded = new THREE.TextureLoader().loadAsync(imageUrl) + return loaded +} + +export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => { + const canvas = createCanvas(image.width, image.height) + const ctx = canvas.getContext('2d')! + ctx.drawImage(image, 0, 0) + const texture = new THREE.Texture(canvas) + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + return texture +} + +export async function loadTexture (texture: string, cb: (texture: THREE.Texture) => void, onLoad?: () => void): Promise { + const cached = textureCache[texture] + if (!cached) { + const { promise, resolve } = Promise.withResolvers() + const t = loadThreeJsTextureFromUrlSync(texture) + textureCache[texture] = t.texture + void t.promise.then(resolve) + imagesPromises[texture] = promise + } + + cb(textureCache[texture]) + void imagesPromises[texture].then(() => { + onLoad?.() + }) +} + +export const clearTextureCache = () => { + textureCache = {} + imagesPromises = {} +} + diff --git a/renderer/viewer/three/waypointSprite.ts b/renderer/viewer/three/waypointSprite.ts new file mode 100644 index 00000000..6a30e6db --- /dev/null +++ b/renderer/viewer/three/waypointSprite.ts @@ -0,0 +1,418 @@ +import * as THREE from 'three' + +// Centralized visual configuration (in screen pixels) +export const WAYPOINT_CONFIG = { + // Target size in screen pixels (this controls the final sprite size) + TARGET_SCREEN_PX: 150, + // Canvas size for internal rendering (keep power of 2 for textures) + CANVAS_SIZE: 256, + // Relative positions in canvas (0-1) + LAYOUT: { + DOT_Y: 0.3, + NAME_Y: 0.45, + DISTANCE_Y: 0.55, + }, + // Multiplier for canvas internal resolution to keep text crisp + CANVAS_SCALE: 2, + ARROW: { + enabledDefault: false, + pixelSize: 50, + paddingPx: 50, + }, +} + +export type WaypointSprite = { + group: THREE.Group + sprite: THREE.Sprite + // Offscreen arrow controls + enableOffscreenArrow: (enabled: boolean) => void + setArrowParent: (parent: THREE.Object3D | null) => void + // Convenience combined updater + updateForCamera: ( + cameraPosition: THREE.Vector3, + camera: THREE.PerspectiveCamera, + viewportWidthPx: number, + viewportHeightPx: number + ) => boolean + // Utilities + setColor: (color: number) => void + setLabel: (label?: string) => void + updateDistanceText: (label: string, distanceText: string) => void + setVisible: (visible: boolean) => void + setPosition: (x: number, y: number, z: number) => void + dispose: () => void +} + +export function createWaypointSprite (options: { + position: THREE.Vector3 | { x: number, y: number, z: number }, + color?: number, + label?: string, + depthTest?: boolean, + // Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this) + labelYOffset?: number, + metadata?: any, +}): WaypointSprite { + const color = options.color ?? 0xFF_00_00 + const depthTest = options.depthTest ?? false + const labelYOffset = options.labelYOffset ?? 1.5 + + // Build combined sprite + const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest) + sprite.renderOrder = 10 + let currentLabel = options.label ?? '' + + // Offscreen arrow (detached by default) + let arrowSprite: THREE.Sprite | undefined + let arrowParent: THREE.Object3D | null = null + let arrowEnabled = WAYPOINT_CONFIG.ARROW.enabledDefault + + // Group for easy add/remove + const group = new THREE.Group() + group.add(sprite) + + // Initial position + const { x, y, z } = options.position + group.position.set(x, y, z) + + function setColor (newColor: number) { + const canvas = drawCombinedCanvas(newColor, currentLabel, '0m') + const texture = new THREE.CanvasTexture(canvas) + const mat = sprite.material + mat.map?.dispose() + mat.map = texture + mat.needsUpdate = true + } + + function setLabel (newLabel?: string) { + currentLabel = newLabel ?? '' + const canvas = drawCombinedCanvas(color, currentLabel, '0m') + const texture = new THREE.CanvasTexture(canvas) + const mat = sprite.material + mat.map?.dispose() + mat.map = texture + mat.needsUpdate = true + } + + function updateDistanceText (label: string, distanceText: string) { + const canvas = drawCombinedCanvas(color, label, distanceText) + const texture = new THREE.CanvasTexture(canvas) + const mat = sprite.material + mat.map?.dispose() + mat.map = texture + mat.needsUpdate = true + } + + function setVisible (visible: boolean) { + sprite.visible = visible + } + + function setPosition (nx: number, ny: number, nz: number) { + group.position.set(nx, ny, nz) + } + + // Keep constant pixel size on screen using global config + function updateScaleScreenPixels ( + cameraPosition: THREE.Vector3, + cameraFov: number, + distance: number, + viewportHeightPx: number + ) { + const vFovRad = cameraFov * Math.PI / 180 + const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance + // Use configured target screen size + const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX / viewportHeightPx) + sprite.scale.set(scale, scale, 1) + } + + function ensureArrow () { + if (arrowSprite) return + const size = 128 + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d')! + ctx.clearRect(0, 0, size, size) + + // Draw arrow shape + ctx.beginPath() + ctx.moveTo(size * 0.15, size * 0.5) + ctx.lineTo(size * 0.85, size * 0.5) + ctx.lineTo(size * 0.5, size * 0.15) + ctx.closePath() + + // Use waypoint color for arrow + const colorHex = `#${color.toString(16).padStart(6, '0')}` + ctx.lineWidth = 6 + ctx.strokeStyle = 'black' + ctx.stroke() + ctx.fillStyle = colorHex + ctx.fill() + + const texture = new THREE.CanvasTexture(canvas) + const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false }) + arrowSprite = new THREE.Sprite(material) + arrowSprite.renderOrder = 12 + arrowSprite.visible = false + if (arrowParent) arrowParent.add(arrowSprite) + } + + function enableOffscreenArrow (enabled: boolean) { + arrowEnabled = enabled + if (!enabled && arrowSprite) arrowSprite.visible = false + } + + function setArrowParent (parent: THREE.Object3D | null) { + if (arrowSprite?.parent) arrowSprite.parent.remove(arrowSprite) + arrowParent = parent + if (arrowSprite && parent) parent.add(arrowSprite) + } + + function updateOffscreenArrow ( + camera: THREE.PerspectiveCamera, + viewportWidthPx: number, + viewportHeightPx: number + ): boolean { + if (!arrowEnabled) return true + ensureArrow() + if (!arrowSprite) return true + + // Check if onlyLeftRight is enabled in metadata + const onlyLeftRight = options.metadata?.onlyLeftRight === true + + // Build camera basis using camera.up to respect custom orientations + const forward = new THREE.Vector3() + camera.getWorldDirection(forward) // camera look direction + const upWorld = camera.up.clone().normalize() + const right = new THREE.Vector3().copy(forward).cross(upWorld).normalize() + const upCam = new THREE.Vector3().copy(right).cross(forward).normalize() + + // Vector from camera to waypoint + const camPos = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld) + const toWp = new THREE.Vector3(group.position.x, group.position.y, group.position.z).sub(camPos) + + // Components in camera basis + const z = toWp.dot(forward) + const x = toWp.dot(right) + const y = toWp.dot(upCam) + + const aspect = viewportWidthPx / viewportHeightPx + const vFovRad = camera.fov * Math.PI / 180 + const hFovRad = 2 * Math.atan(Math.tan(vFovRad / 2) * aspect) + + // Determine if waypoint is inside view frustum using angular checks + const thetaX = Math.atan2(x, z) + const thetaY = Math.atan2(y, z) + const visible = z > 0 && Math.abs(thetaX) <= hFovRad / 2 && Math.abs(thetaY) <= vFovRad / 2 + if (visible) { + arrowSprite.visible = false + return true + } + + // Direction on screen in normalized frustum units + let rx = thetaX / (hFovRad / 2) + let ry = thetaY / (vFovRad / 2) + + // If behind the camera, snap to dominant axis to avoid confusing directions + if (z <= 0) { + if (Math.abs(rx) > Math.abs(ry)) { + rx = Math.sign(rx) + ry = 0 + } else { + rx = 0 + ry = Math.sign(ry) + } + } + + // Apply onlyLeftRight logic - restrict arrows to left/right edges only + if (onlyLeftRight) { + // Force the arrow to appear only on left or right edges + if (Math.abs(rx) > Math.abs(ry)) { + // Horizontal direction is dominant, keep it + ry = 0 + } else { + // Vertical direction is dominant, but we want only left/right + // So choose left or right based on the sign of rx + rx = rx >= 0 ? 1 : -1 + ry = 0 + } + } + + // Place on the rectangle border [-1,1]x[-1,1] + const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1 + let ndcX = rx / s + let ndcY = ry / s + + // Apply padding in pixel space by clamping + const padding = WAYPOINT_CONFIG.ARROW.paddingPx + const pxX = ((ndcX + 1) * 0.5) * viewportWidthPx + const pxY = ((1 - ndcY) * 0.5) * viewportHeightPx + const clampedPxX = Math.min(Math.max(pxX, padding), viewportWidthPx - padding) + const clampedPxY = Math.min(Math.max(pxY, padding), viewportHeightPx - padding) + ndcX = (clampedPxX / viewportWidthPx) * 2 - 1 + ndcY = -(clampedPxY / viewportHeightPx) * 2 + 1 + + // Compute world position at a fixed distance in front of the camera using camera basis + const placeDist = Math.max(2, camera.near * 4) + const halfPlaneHeight = Math.tan(vFovRad / 2) * placeDist + const halfPlaneWidth = halfPlaneHeight * aspect + const pos = camPos.clone() + .add(forward.clone().multiplyScalar(placeDist)) + .add(right.clone().multiplyScalar(ndcX * halfPlaneWidth)) + .add(upCam.clone().multiplyScalar(ndcY * halfPlaneHeight)) + + // Update arrow sprite + arrowSprite.visible = true + arrowSprite.position.copy(pos) + + // Angle for rotation relative to screen right/up (derived from camera up vector) + const angle = Math.atan2(ry, rx) + arrowSprite.material.rotation = angle - Math.PI / 2 + + // Constant pixel size for arrow (use fixed placement distance) + const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * placeDist + const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize / viewportHeightPx) + arrowSprite.scale.set(sPx, sPx, 1) + return false + } + + function computeDistance (cameraPosition: THREE.Vector3): number { + return cameraPosition.distanceTo(group.position) + } + + function updateForCamera ( + cameraPosition: THREE.Vector3, + camera: THREE.PerspectiveCamera, + viewportWidthPx: number, + viewportHeightPx: number + ): boolean { + const distance = computeDistance(cameraPosition) + // Keep constant pixel size + updateScaleScreenPixels(cameraPosition, camera.fov, distance, viewportHeightPx) + // Update text + updateDistanceText(currentLabel, `${Math.round(distance)}m`) + // Update arrow and visibility + const onScreen = updateOffscreenArrow(camera, viewportWidthPx, viewportHeightPx) + setVisible(onScreen) + return onScreen + } + + function dispose () { + const mat = sprite.material + mat.map?.dispose() + mat.dispose() + if (arrowSprite) { + const am = arrowSprite.material + am.map?.dispose() + am.dispose() + } + } + + return { + group, + sprite, + enableOffscreenArrow, + setArrowParent, + updateForCamera, + setColor, + setLabel, + updateDistanceText, + setVisible, + setPosition, + dispose, + } +} + +// Internal helpers +function drawCombinedCanvas (color: number, id: string, distance: string): HTMLCanvasElement { + const scale = WAYPOINT_CONFIG.CANVAS_SCALE * (globalThis.devicePixelRatio || 1) + const size = WAYPOINT_CONFIG.CANVAS_SIZE * scale + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d')! + + // Clear canvas + ctx.clearRect(0, 0, size, size) + + // Draw dot + const centerX = size / 2 + const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y) + const radius = Math.round(size * 0.05) // Dot takes up ~12% of canvas height + const borderWidth = Math.max(2, Math.round(4 * scale)) + + // Outer border (black) + ctx.beginPath() + ctx.arc(centerX, dotY, radius + borderWidth, 0, Math.PI * 2) + ctx.fillStyle = 'black' + ctx.fill() + + // Inner circle (colored) + ctx.beginPath() + ctx.arc(centerX, dotY, radius, 0, Math.PI * 2) + ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}` + ctx.fill() + + // Text properties + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + // Title + const nameFontPx = Math.round(size * 0.08) // ~8% of canvas height + const distanceFontPx = Math.round(size * 0.06) // ~6% of canvas height + ctx.font = `bold ${nameFontPx}px mojangles` + ctx.lineWidth = Math.max(2, Math.round(3 * scale)) + const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y) + + ctx.strokeStyle = 'black' + ctx.strokeText(id, centerX, nameY) + ctx.fillStyle = 'white' + ctx.fillText(id, centerX, nameY) + + // Distance + ctx.font = `bold ${distanceFontPx}px mojangles` + ctx.lineWidth = Math.max(2, Math.round(2 * scale)) + const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y) + + ctx.strokeStyle = 'black' + ctx.strokeText(distance, centerX, distanceY) + ctx.fillStyle = '#CCCCCC' + ctx.fillText(distance, centerX, distanceY) + + return canvas +} + +function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean): THREE.Sprite { + const canvas = drawCombinedCanvas(color, id, distance) + const texture = new THREE.CanvasTexture(canvas) + texture.anisotropy = 1 + texture.magFilter = THREE.LinearFilter + texture.minFilter = THREE.LinearFilter + const material = new THREE.SpriteMaterial({ + map: texture, + transparent: true, + opacity: 1, + depthTest, + depthWrite: false, + }) + const sprite = new THREE.Sprite(material) + sprite.position.set(0, 0, 0) + return sprite +} + +export const WaypointHelpers = { + // World-scale constant size helper + computeWorldScale (distance: number, fixedReference = 10) { + return Math.max(0.0001, distance / fixedReference) + }, + // Screen-pixel constant size helper + computeScreenPixelScale ( + camera: THREE.PerspectiveCamera, + distance: number, + pixelSize: number, + viewportHeightPx: number + ) { + const vFovRad = camera.fov * Math.PI / 180 + const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance + return worldUnitsPerScreenHeightAtDist * (pixelSize / viewportHeightPx) + } +} diff --git a/renderer/viewer/three/waypoints.ts b/renderer/viewer/three/waypoints.ts new file mode 100644 index 00000000..256ca6df --- /dev/null +++ b/renderer/viewer/three/waypoints.ts @@ -0,0 +1,140 @@ +import * as THREE from 'three' +import { WorldRendererThree } from './worldrendererThree' +import { createWaypointSprite, type WaypointSprite } from './waypointSprite' + +interface Waypoint { + id: string + x: number + y: number + z: number + minDistance: number + color: number + label?: string + sprite: WaypointSprite +} + +interface WaypointOptions { + color?: number + label?: string + minDistance?: number + metadata?: any +} + +export class WaypointsRenderer { + private readonly waypoints = new Map() + private readonly waypointScene = new THREE.Scene() + + constructor ( + private readonly worldRenderer: WorldRendererThree + ) { + } + + private updateWaypoints () { + const playerPos = this.worldRenderer.cameraObject.position + const sizeVec = this.worldRenderer.renderer.getSize(new THREE.Vector2()) + + for (const waypoint of this.waypoints.values()) { + const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z) + const distance = playerPos.distanceTo(waypointPos) + const visible = !waypoint.minDistance || distance >= waypoint.minDistance + + waypoint.sprite.setVisible(visible) + + if (visible) { + // Update position + waypoint.sprite.setPosition(waypoint.x, waypoint.y, waypoint.z) + // Ensure camera-based update each frame + waypoint.sprite.updateForCamera(this.worldRenderer.getCameraPosition(), this.worldRenderer.camera, sizeVec.width, sizeVec.height) + } + } + } + + render () { + if (this.waypoints.size === 0) return + + // Update waypoint scaling + this.updateWaypoints() + + // Render waypoints scene with the world camera + this.worldRenderer.renderer.render(this.waypointScene, this.worldRenderer.camera) + } + + // Removed sprite/label texture creation. Use utils/waypointSprite.ts + + addWaypoint ( + id: string, + x: number, + y: number, + z: number, + options: WaypointOptions = {} + ) { + // Remove existing waypoint if it exists + this.removeWaypoint(id) + + const color = options.color ?? 0xFF_00_00 + const { label, metadata } = options + const minDistance = options.minDistance ?? 0 + + const sprite = createWaypointSprite({ + position: new THREE.Vector3(x, y, z), + color, + label: (label || id), + metadata, + }) + sprite.enableOffscreenArrow(true) + sprite.setArrowParent(this.waypointScene) + + this.waypointScene.add(sprite.group) + + this.waypoints.set(id, { + id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance, + color, label, + sprite, + }) + } + + removeWaypoint (id: string) { + const waypoint = this.waypoints.get(id) + if (waypoint) { + this.waypointScene.remove(waypoint.sprite.group) + waypoint.sprite.dispose() + this.waypoints.delete(id) + } + } + + clear () { + for (const id of this.waypoints.keys()) { + this.removeWaypoint(id) + } + } + + testWaypoint () { + this.addWaypoint('Test Point', 0, 70, 0, { color: 0x00_FF_00, label: 'Test Point' }) + this.addWaypoint('Spawn', 0, 64, 0, { color: 0xFF_FF_00, label: 'Spawn' }) + this.addWaypoint('Far Point', 100, 70, 100, { color: 0x00_00_FF, label: 'Far Point' }) + } + + getWaypoint (id: string): Waypoint | undefined { + return this.waypoints.get(id) + } + + getAllWaypoints (): Waypoint[] { + return [...this.waypoints.values()] + } + + setWaypointColor (id: string, color: number) { + const waypoint = this.waypoints.get(id) + if (waypoint) { + waypoint.sprite.setColor(color) + waypoint.color = color + } + } + + setWaypointLabel (id: string, label?: string) { + const waypoint = this.waypoints.get(id) + if (waypoint) { + waypoint.label = label + waypoint.sprite.setLabel(label) + } + } +} diff --git a/renderer/viewer/three/world/cursorBlock.ts b/renderer/viewer/three/world/cursorBlock.ts new file mode 100644 index 00000000..a03a6999 --- /dev/null +++ b/renderer/viewer/three/world/cursorBlock.ts @@ -0,0 +1,162 @@ +import * as THREE from 'three' +import { LineMaterial, LineSegmentsGeometry, Wireframe } from 'three-stdlib' +import { Vec3 } from 'vec3' +import { BlockShape, BlocksShapes } from 'renderer/viewer/lib/basePlayerState' +import { WorldRendererThree } from '../worldrendererThree' +import { loadThreeJsTextureFromUrl } from '../threeJsUtils' +import destroyStage0 from '../../../../assets/destroy_stage_0.png' +import destroyStage1 from '../../../../assets/destroy_stage_1.png' +import destroyStage2 from '../../../../assets/destroy_stage_2.png' +import destroyStage3 from '../../../../assets/destroy_stage_3.png' +import destroyStage4 from '../../../../assets/destroy_stage_4.png' +import destroyStage5 from '../../../../assets/destroy_stage_5.png' +import destroyStage6 from '../../../../assets/destroy_stage_6.png' +import destroyStage7 from '../../../../assets/destroy_stage_7.png' +import destroyStage8 from '../../../../assets/destroy_stage_8.png' +import destroyStage9 from '../../../../assets/destroy_stage_9.png' + +export class CursorBlock { + _cursorLinesHidden = false + get cursorLinesHidden () { + return this._cursorLinesHidden + } + set cursorLinesHidden (value: boolean) { + if (this.interactionLines) { + this.interactionLines.mesh.visible = !value + } + this._cursorLinesHidden = value + } + + cursorLineMaterial: LineMaterial + interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null + prevColor: string | undefined + blockBreakMesh: THREE.Mesh + breakTextures: THREE.Texture[] = [] + + constructor (public readonly worldRenderer: WorldRendererThree) { + // Initialize break mesh and textures + const destroyStagesImages = [ + destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4, + destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9 + ] + + for (let i = 0; i < 10; i++) { + void loadThreeJsTextureFromUrl(destroyStagesImages[i]).then((texture) => { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + this.breakTextures.push(texture) + }) + } + + const breakMaterial = new THREE.MeshBasicMaterial({ + transparent: true, + blending: THREE.MultiplyBlending, + alphaTest: 0.5, + }) + this.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial) + this.blockBreakMesh.visible = false + this.blockBreakMesh.renderOrder = 999 + this.blockBreakMesh.name = 'blockBreakMesh' + this.worldRenderer.scene.add(this.blockBreakMesh) + + this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => { + this.updateLineMaterial() + }) + // todo figure out why otherwise fog from skybox breaks it + setTimeout(() => { + this.updateLineMaterial() + if (this.interactionLines) { + this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true) + } + }) + } + + // Update functions + updateLineMaterial () { + const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative' + const pixelRatio = this.worldRenderer.renderer.getPixelRatio() + + if (this.cursorLineMaterial) { + this.cursorLineMaterial.dispose() + } + this.cursorLineMaterial = new LineMaterial({ + color: (() => { + switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) { + case 'blue': + return 0x40_80_ff + case 'classic': + return 0x00_00_00 + default: + return inCreative ? 0x40_80_ff : 0x00_00_00 + } + })(), + linewidth: Math.max(pixelRatio * 0.7, 1) * 2, + // dashed: true, + // dashSize: 5, + }) + this.prevColor = this.worldRenderer.worldRendererConfig.highlightBlockColor + } + + updateBreakAnimation (blockPosition: { x: number, y: number, z: number } | undefined, stage: number | null, mergedShape?: BlockShape) { + this.hideBreakAnimation() + if (stage === null || !blockPosition || !mergedShape) return + + const { position, width, height, depth } = mergedShape + this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001) + position.add(blockPosition) + this.blockBreakMesh.position.set(position.x, position.y, position.z) + this.blockBreakMesh.visible = true; + + (this.blockBreakMesh.material as THREE.MeshBasicMaterial).map = this.breakTextures[stage] ?? this.breakTextures.at(-1); + (this.blockBreakMesh.material as THREE.MeshBasicMaterial).needsUpdate = true + } + + hideBreakAnimation () { + if (this.blockBreakMesh) { + this.blockBreakMesh.visible = false + } + } + + updateDisplay () { + if (this.cursorLineMaterial) { + const { renderer } = this.worldRenderer + this.cursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height) + this.cursorLineMaterial.dashOffset = performance.now() / 750 + } + } + + setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void { + if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && !force) { + return + } + if (this.interactionLines !== null) { + this.worldRenderer.scene.remove(this.interactionLines.mesh) + this.interactionLines = null + } + if (blockPos === null) { + return + } + + const group = new THREE.Group() + for (const { position, width, height, depth } of shapePositions ?? []) { + const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const + const geometry = new THREE.BoxGeometry(...scale) + const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry)) + const wireframe = new Wireframe(lines, this.cursorLineMaterial) + const pos = blockPos.plus(position) + wireframe.position.set(pos.x, pos.y, pos.z) + wireframe.computeLineDistances() + group.add(wireframe) + } + this.worldRenderer.scene.add(group) + group.visible = !this.cursorLinesHidden + this.interactionLines = { blockPos, mesh: group, shapePositions } + } + + render () { + if (this.prevColor !== this.worldRenderer.worldRendererConfig.highlightBlockColor) { + this.updateLineMaterial() + } + this.updateDisplay() + } +} diff --git a/src/vr.js b/renderer/viewer/three/world/vr.ts similarity index 51% rename from src/vr.js rename to renderer/viewer/three/world/vr.ts index 45c25b3f..ecf1b299 100644 --- a/src/vr.js +++ b/renderer/viewer/three/world/vr.ts @@ -1,23 +1,109 @@ -const { VRButton } = require('three/examples/jsm/webxr/VRButton.js') -const { GLTFLoader } = require('three/examples/jsm/loaders/GLTFLoader.js') -const { XRControllerModelFactory } = require('three/examples/jsm/webxr/XRControllerModelFactory.js') -const { buttonMap: standardButtonsMap } = require('contro-max/build/gamepad') -const { activeModalStack, hideModal } = require('./globalState') +import { VRButton } from 'three/examples/jsm/webxr/VRButton.js' +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' +import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js' +import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad' +import * as THREE from 'three' +import { WorldRendererThree } from '../worldrendererThree' +import { DocumentRenderer } from '../documentRenderer' -async function initVR () { - const { renderer } = viewer - if (!('xr' in navigator)) return - const isSupported = await navigator.xr.isSessionSupported('immersive-vr') && !!XRSession.prototype.updateRenderState // e.g. android webview doesn't support updateRenderState +export async function initVR (worldRenderer: WorldRendererThree, documentRenderer: DocumentRenderer) { + if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return + const { renderer } = worldRenderer + + const isSupported = await checkVRSupport() if (!isSupported) return - // VR - document.body.appendChild(VRButton.createButton(renderer)) - renderer.xr.enabled = true + enableVr() + + const vrButtonContainer = createVrButtonContainer(renderer) + const updateVrButtons = () => { + const newHidden = !worldRenderer.worldRendererConfig.vrSupport || !worldRenderer.worldRendererConfig.foreground + if (vrButtonContainer.hidden !== newHidden) { + vrButtonContainer.hidden = newHidden + } + } + + worldRenderer.onRender.push(updateVrButtons) + + function enableVr () { + renderer.xr.enabled = true + // renderer.xr.setReferenceSpaceType('local-floor') + worldRenderer.reactiveState.preventEscapeMenu = true + } + + function disableVr () { + renderer.xr.enabled = false + worldRenderer.cameraGroupVr = undefined + worldRenderer.reactiveState.preventEscapeMenu = false + worldRenderer.scene.remove(user) + vrButtonContainer.hidden = true + } + + function createVrButtonContainer (renderer) { + const container = document.createElement('div') + const vrButton = VRButton.createButton(renderer) + styleContainer(container) + + const closeButton = createCloseButton(container) + + container.appendChild(vrButton) + container.appendChild(closeButton) + document.body.appendChild(container) + + return container + } + + function styleContainer (container: HTMLElement) { + typedAssign(container.style, { + position: 'absolute', + bottom: '80px', + left: '0', + right: '0', + display: 'flex', + justifyContent: 'center', + zIndex: '8', + gap: '8px', + }) + } + + function createCloseButton (container: HTMLElement) { + const closeButton = document.createElement('button') + closeButton.textContent = 'X' + typedAssign(closeButton.style, { + padding: '0 12px', + color: 'white', + fontSize: '14px', + lineHeight: '20px', + cursor: 'pointer', + background: 'transparent', + border: '1px solid rgb(255, 255, 255)', + borderRadius: '4px', + opacity: '0.7', + }) + + closeButton.addEventListener('click', () => { + container.hidden = true + worldRenderer.worldRendererConfig.vrSupport = false + }) + + return closeButton + } + + + async function checkVRSupport () { + try { + const supported = await navigator.xr?.isSessionSupported('immersive-vr') + return supported && !!XRSession.prototype.updateRenderState + } catch (err) { + console.error('Error checking if VR is supported', err) + return false + } + } // hack for vr camera const user = new THREE.Group() - user.add(viewer.camera) - viewer.scene.add(user) + user.name = 'vr-camera-container' + worldRenderer.scene.add(user) const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader()) const controller1 = renderer.xr.getControllerGrip(0) const controller2 = renderer.xr.getControllerGrip(1) @@ -25,22 +111,23 @@ async function initVR () { // todo the logic written here can be hard to understand as it was designed to work in gamepad api emulation mode, will be refactored once there is a contro-max rewrite is done const virtualGamepadIndex = 4 let connectedVirtualGamepad + //@ts-expect-error const manageXrInputSource = ({ gamepad, handedness = defaultHandedness }, defaultHandedness, removeAction = false) => { if (handedness === 'right') { - const event = new Event(removeAction ? 'gamepaddisconnected' : 'gamepadconnected') // todo need to expose and use external gamepads api in contro-max instead + const event: any = new Event(removeAction ? 'gamepaddisconnected' : 'gamepadconnected') // todo need to expose and use external gamepads api in contro-max instead event.gamepad = removeAction ? connectedVirtualGamepad : { ...gamepad, mapping: 'standard', index: virtualGamepadIndex } connectedVirtualGamepad = event.gamepad window.dispatchEvent(event) } } - let hand1 = controllerModelFactory.createControllerModel(controller1) + let hand1: any = controllerModelFactory.createControllerModel(controller1) controller1.addEventListener('connected', (event) => { hand1.xrInputSource = event.data manageXrInputSource(event.data, 'left') user.add(controller1) }) controller1.add(hand1) - let hand2 = controllerModelFactory.createControllerModel(controller2) + let hand2: any = controllerModelFactory.createControllerModel(controller2) controller2.addEventListener('connected', (event) => { hand2.xrInputSource = event.data manageXrInputSource(event.data, 'right') @@ -50,15 +137,17 @@ async function initVR () { controller1.addEventListener('disconnected', () => { // don't handle removal of gamepads for now as is don't affect contro-max - hand1.xrInputSource = undefined manageXrInputSource(hand1.xrInputSource, 'left', true) + hand1.xrInputSource = undefined }) controller2.addEventListener('disconnected', () => { - hand2.xrInputSource = undefined manageXrInputSource(hand1.xrInputSource, 'right', true) + hand2.xrInputSource = undefined }) const originalGetGamepads = navigator.getGamepads.bind(navigator) + // is it okay to patch this? + //@ts-expect-error navigator.getGamepads = () => { const originalGamepads = originalGetGamepads() if (!hand1.xrInputSource || !hand2.xrInputSource) return originalGamepads @@ -101,34 +190,28 @@ async function initVR () { rotSnapReset = true } - // viewer.setFirstPersonCamera(null, yawOffset, 0) - viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch) + // appViewer.backend?.updateCamera(null, yawOffset, 0) + // worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch) // todo restore this logic (need to preserve ability to move camera) - // const xrCamera = renderer.xr.getCamera(viewer.camera) - // const d = xrCamera.getWorldDirection() // todo target + // const xrCamera = renderer.xr.getCamera() + // const d = xrCamera.getWorldDirection(new THREE.Vector3()) // bot.entity.yaw = Math.atan2(-d.x, -d.z) // bot.entity.pitch = Math.asin(d.y) - // todo ? - // bot.physics.stepHeight = 1 - - viewer.update() - viewer.render() + documentRenderer.frameRender(false) }) renderer.xr.addEventListener('sessionstart', () => { - viewer.cameraObjectOverride = user - // close all modals to be in game - for (const _modal of activeModalStack) { - hideModal(undefined, {}, { force: true }) - } + user.add(worldRenderer.camera) + worldRenderer.cameraGroupVr = user }) renderer.xr.addEventListener('sessionend', () => { - viewer.cameraObjectOverride = undefined + worldRenderer.cameraGroupVr = undefined + user.remove(worldRenderer.camera) }) -} -module.exports.initVR = initVR + worldRenderer.abortController.signal.addEventListener('abort', disableVr) +} const xrStandardRightButtonsMap = [ [0 /* trigger */, 'Right Trigger'], @@ -146,9 +229,9 @@ const xrStandardLeftButtonsMap = [ [4 /* A */, 'X'], [5 /* B */, 'Y'], ] -const remapButtons = (rightButtons, leftButtons) => { +const remapButtons = (rightButtons: any[], leftButtons: any[]) => { // return remapped buttons - const remapped = [] + const remapped = [] as string[] const remapWithMap = (buttons, map) => { for (const [index, standardName] of map) { const standardMappingIndex = standardButtonsMap.findIndex((aliases) => aliases.find(alias => standardName === alias)) @@ -168,3 +251,7 @@ const remapAxes = (axesRight, axesLeft) => { axesRight[3] ] } + +function typedAssign> (target: T, source: Partial) { + Object.assign(target, source) +} diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts new file mode 100644 index 00000000..1b4e6152 --- /dev/null +++ b/renderer/viewer/three/worldrendererThree.ts @@ -0,0 +1,1161 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import nbt from 'prismarine-nbt' +import PrismarineChatLoader from 'prismarine-chat' +import * as tweenJs from '@tweenjs/tween.js' +import { Biome } from 'minecraft-data' +import { renderSign } from '../sign-renderer' +import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer' +import { chunkPos, sectionPos } from '../lib/simpleUtils' +import { WorldRendererCommon } from '../lib/worldrendererCommon' +import { addNewStat } from '../lib/ui/newStats' +import { MesherGeometryOutput } from '../lib/mesher/shared' +import { ItemSpecificContextProperties } from '../lib/basePlayerState' +import { setBlockPosition } from '../lib/mesher/standaloneRenderer' +import { getMyHand } from './hand' +import HoldingBlock from './holdingBlock' +import { getMesh } from './entity/EntityMesh' +import { armorModel } from './entity/armorModels' +import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils' +import { CursorBlock } from './world/cursorBlock' +import { getItemUv } from './appShared' +import { Entities } from './entities' +import { ThreeJsSound } from './threeJsSound' +import { CameraShake } from './cameraShake' +import { ThreeJsMedia } from './threeJsMedia' +import { Fountain } from './threeJsParticles' +import { WaypointsRenderer } from './waypoints' +import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer' + +type SectionKey = string + +export class WorldRendererThree extends WorldRendererCommon { + outputFormat = 'threeJs' as const + sectionObjects: Record = {} + chunkTextures = new Map() + signsCache = new Map() + starField: StarField + cameraSectionPos: Vec3 = new Vec3(0, 0, 0) + holdingBlock: HoldingBlock + holdingBlockLeft: HoldingBlock + scene = new THREE.Scene() + ambientLight = new THREE.AmbientLight(0xcc_cc_cc) + directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5) + entities = new Entities(this) + cameraGroupVr?: THREE.Object3D + material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) + itemsTexture: THREE.Texture + cursorBlock: CursorBlock + onRender: Array<() => void> = [] + cameraShake: CameraShake + cameraContainer: THREE.Object3D + media: ThreeJsMedia + waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } + waypoints: WaypointsRenderer + camera: THREE.PerspectiveCamera + renderTimeAvg = 0 + sectionsOffsetsAnimations = {} as { + [chunkKey: string]: { + time: number, + // also specifies direction + speedX: number, + speedY: number, + speedZ: number, + + currentOffsetX: number, + currentOffsetY: number, + currentOffsetZ: number, + + limitX?: number, + limitY?: number, + limitZ?: number, + } + } + fountains: Fountain[] = [] + DEBUG_RAYCAST = false + skyboxRenderer: SkyboxRenderer + + private currentPosTween?: tweenJs.Tween + private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }> + + get tilesRendered () { + return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0) + } + + get blocksRendered () { + return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0) + } + + constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) { + if (!initOptions.resourcesManager) throw new Error('resourcesManager is required') + super(initOptions.resourcesManager, displayOptions, initOptions) + + this.renderer = renderer + displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...' + this.starField = new StarField(this) + this.cursorBlock = new CursorBlock(this) + this.holdingBlock = new HoldingBlock(this) + this.holdingBlockLeft = new HoldingBlock(this, true) + + // Initialize skybox renderer + this.skyboxRenderer = new SkyboxRenderer(this.scene, this.worldRendererConfig.defaultSkybox, null) + void this.skyboxRenderer.init() + + this.addDebugOverlay() + this.resetScene() + void this.init() + + this.soundSystem = new ThreeJsSound(this) + this.cameraShake = new CameraShake(this, this.onRender) + this.media = new ThreeJsMedia(this) + this.waypoints = new WaypointsRenderer(this) + + // this.fountain = new Fountain(this.scene, this.scene, { + // position: new THREE.Vector3(0, 10, 0), + // }) + + this.renderUpdateEmitter.on('chunkFinished', (chunkKey: string) => { + this.finishChunk(chunkKey) + }) + this.worldSwitchActions() + } + + get cameraObject () { + return this.cameraGroupVr ?? this.cameraContainer + } + + worldSwitchActions () { + this.onWorldSwitched.push(() => { + // clear custom blocks + this.protocolCustomBlocks.clear() + // Reset section animations + this.sectionsOffsetsAnimations = {} + // Clear waypoints + this.waypoints.clear() + }) + } + + updateEntity (e, isPosUpdate = false) { + const overrides = { + rotation: { + head: { + x: e.headPitch ?? e.pitch, + y: e.headYaw, + z: 0 + } + } + } + if (isPosUpdate) { + this.entities.updateEntityPosition(e, false, overrides) + } else { + this.entities.update(e, overrides) + } + } + + updatePlayerEntity (e: any) { + this.entities.handlePlayerEntity(e) + } + + resetScene () { + this.scene.matrixAutoUpdate = false // for perf + this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground) + this.scene.add(this.ambientLight) + this.directionalLight.position.set(1, 1, 0.5).normalize() + this.directionalLight.castShadow = true + this.scene.add(this.directionalLight) + + const size = this.renderer.getSize(new THREE.Vector2()) + this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000) + this.cameraContainer = new THREE.Object3D() + this.cameraContainer.add(this.camera) + this.scene.add(this.cameraContainer) + } + + override watchReactivePlayerState () { + super.watchReactivePlayerState() + this.onReactivePlayerStateUpdated('inWater', (value) => { + this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing) + }) + this.onReactivePlayerStateUpdated('waterBreathing', (value) => { + this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value) + }) + this.onReactivePlayerStateUpdated('ambientLight', (value) => { + if (!value) return + this.ambientLight.intensity = value + }) + this.onReactivePlayerStateUpdated('directionalLight', (value) => { + if (!value) return + this.directionalLight.intensity = value + }) + this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => { + this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes) + }) + this.onReactivePlayerStateUpdated('diggingBlock', (value) => { + this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape) + }) + this.onReactivePlayerStateUpdated('perspective', (value) => { + // Update camera perspective when it changes + const vecPos = new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z) + this.updateCamera(vecPos, this.cameraShake.getBaseRotation().yaw, this.cameraShake.getBaseRotation().pitch) + // todo also update camera when block within camera was changed + }) + } + + override watchReactiveConfig () { + super.watchReactiveConfig() + this.onReactiveConfigUpdated('showChunkBorders', (value) => { + this.updateShowChunksBorder(value) + }) + this.onReactiveConfigUpdated('defaultSkybox', (value) => { + this.skyboxRenderer.updateDefaultSkybox(value) + }) + } + + changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) { + const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock + if (isAnimationPlaying) { + holdingBlock.startSwing() + } else { + holdingBlock.stopSwing() + } + } + + async updateAssetsData (): Promise { + const resources = this.resourcesManager.currentResources + + const oldTexture = this.material.map + const oldItemsTexture = this.itemsTexture + + const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage) + texture.needsUpdate = true + texture.flipY = false + this.material.map = texture + + const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage) + itemsTexture.needsUpdate = true + itemsTexture.flipY = false + this.itemsTexture = itemsTexture + + if (oldTexture) { + oldTexture.dispose() + } + if (oldItemsTexture) { + oldItemsTexture.dispose() + } + + await super.updateAssetsData() + this.onAllTexturesLoaded() + if (Object.keys(this.loadedChunks).length > 0) { + console.log('rerendering chunks because of texture update') + this.rerenderAllChunks() + } + } + + onAllTexturesLoaded () { + this.holdingBlock.ready = true + this.holdingBlock.updateItem() + this.holdingBlockLeft.ready = true + this.holdingBlockLeft.updateItem() + } + + changeBackgroundColor (color: [number, number, number]): void { + this.scene.background = new THREE.Color(color[0], color[1], color[2]) + } + + timeUpdated (newTime: number): void { + const nightTime = 13_500 + const morningStart = 23_000 + const displayStars = newTime > nightTime && newTime < morningStart + if (displayStars) { + this.starField.addToScene() + } else { + this.starField.remove() + } + + this.skyboxRenderer.updateTime(newTime) + } + + biomeUpdated (biome: Biome): void { + if (biome?.temperature !== undefined) { + this.skyboxRenderer.updateTemperature(biome.temperature) + } + } + + biomeReset (): void { + // Reset to default temperature when biome is unknown + this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE) + } + + getItemRenderData (item: Record, specificProps: ItemSpecificContextProperties) { + return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive) + } + + async demoModel () { + //@ts-expect-error + const pos = cursorBlockRel(0, 1, 0).position + + const mesh = (await getMyHand())! + // mesh.rotation.y = THREE.MathUtils.degToRad(90) + setBlockPosition(mesh, pos) + const helper = new THREE.BoxHelper(mesh, 0xff_ff_00) + mesh.add(helper) + this.scene.add(mesh) + } + + demoItem () { + //@ts-expect-error + const pos = cursorBlockRel(0, 1, 0).position + const { mesh } = this.entities.getItemMesh({ + itemId: 541, + }, {})! + mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5) + // mesh.scale.set(0.5, 0.5, 0.5) + const helper = new THREE.BoxHelper(mesh, 0xff_ff_00) + mesh.add(helper) + this.scene.add(mesh) + } + + debugOverlayAdded = false + addDebugOverlay () { + if (this.debugOverlayAdded) return + this.debugOverlayAdded = true + const pane = addNewStat('debug-overlay') + setInterval(() => { + pane.setVisibility(this.displayAdvancedStats) + if (this.displayAdvancedStats) { + const formatBigNumber = (num: number) => { + return new Intl.NumberFormat('en-US', {}).format(num) + } + let text = '' + text += `C: ${formatBigNumber(this.renderer.info.render.calls)} ` + text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} ` + text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} ` + text += `F: ${formatBigNumber(this.tilesRendered)} ` + text += `B: ${formatBigNumber(this.blocksRendered)}` + pane.updateText(text) + this.backendInfoReport = text + } + }, 200) + } + + /** + * Optionally update data that are depedendent on the viewer position + */ + updatePosDataChunk (key: string) { + const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16)) + // sum of distances: x + y + z + const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z) + const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')! + section.renderOrder = 500 - chunkDistance + } + + override updateViewerPosition (pos: Vec3): void { + this.viewerChunkPosition = pos + } + + cameraSectionPositionUpdate () { + // eslint-disable-next-line guard-for-in + for (const key in this.sectionObjects) { + const value = this.sectionObjects[key] + if (!value) continue + this.updatePosDataChunk(key) + } + } + + getDir (current: number, origin: number) { + if (current === origin) return 0 + return current < origin ? 1 : -1 + } + + finishChunk (chunkKey: string) { + for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) { + this.sectionObjects[sectionKey].visible = true + } + delete this.waitingChunksToDisplay[chunkKey] + } + + // debugRecomputedDeletedObjects = 0 + handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void { + if (data.type !== 'geometry') return + let object: THREE.Object3D = this.sectionObjects[data.key] + if (object) { + this.scene.remove(object) + disposeObject(object) + delete this.sectionObjects[data.key] + } + + const chunkCoords = data.key.split(',') + if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return + + // if (object) { + // this.debugRecomputedDeletedObjects++ + // } + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3)) + geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3)) + geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3)) + geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2)) + geometry.index = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1) + + const mesh = new THREE.Mesh(geometry, this.material) + mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz) + mesh.name = 'mesh' + object = new THREE.Group() + object.add(mesh) + // mesh with static dimensions: 16x16x16 + const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })) + staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz) + const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00) + boxHelper.name = 'helper' + object.add(boxHelper) + object.name = 'chunk'; + (object as any).tilesCount = data.geometry.positions.length / 3 / 4; + (object as any).blocksCount = data.geometry.blocksCount + if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) { + boxHelper.visible = false + } + // should not compute it once + if (Object.keys(data.geometry.signs).length) { + for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) { + const signBlockEntity = this.blockEntities[posKey] + if (!signBlockEntity) continue + const [x, y, z] = posKey.split(',') + const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity)) + if (!sign) continue + object.add(sign) + } + } + if (Object.keys(data.geometry.heads).length) { + for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) { + const headBlockEntity = this.blockEntities[posKey] + if (!headBlockEntity) continue + const [x, y, z] = posKey.split(',') + const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity)) + if (!head) continue + object.add(head) + } + } + this.sectionObjects[data.key] = object + if (this.displayOptions.inWorldRenderingConfig._renderByChunks) { + object.visible = false + const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}` + this.waitingChunksToDisplay[chunkKey] ??= [] + this.waitingChunksToDisplay[chunkKey].push(data.key) + if (this.finishedChunks[chunkKey]) { + // todo it might happen even when it was not an update + this.finishChunk(chunkKey) + } + } + + this.updatePosDataChunk(data.key) + object.matrixAutoUpdate = false + mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => { + // mesh.matrixAutoUpdate = false + } + + this.scene.add(object) + } + + getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) { + const chunk = chunkPos(position) + let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`) + if (!textures) { + textures = {} + this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures) + } + const texturekey = `${position.x},${position.y},${position.z}` + // todo investigate bug and remove this so don't need to clean in section dirty + if (textures[texturekey]) return textures[texturekey] + + const PrismarineChat = PrismarineChatLoader(this.version) + const canvas = renderSign(blockEntity, isHanging, PrismarineChat) + if (!canvas) return + const tex = new THREE.Texture(canvas) + tex.magFilter = THREE.NearestFilter + tex.minFilter = THREE.NearestFilter + tex.needsUpdate = true + textures[texturekey] = tex + return tex + } + + getCameraPosition () { + const worldPos = new THREE.Vector3() + this.camera.getWorldPosition(worldPos) + return worldPos + } + + getSectionCameraPosition () { + const pos = this.getCameraPosition() + return new Vec3( + Math.floor(pos.x / 16), + Math.floor(pos.y / 16), + Math.floor(pos.z / 16) + ) + } + + updateCameraSectionPos () { + const newSectionPos = this.getSectionCameraPosition() + if (!this.cameraSectionPos.equals(newSectionPos)) { + this.cameraSectionPos = newSectionPos + this.cameraSectionPositionUpdate() + } + } + + setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { + const yOffset = this.playerStateReactive.eyeHeight + + this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) + this.media.tryIntersectMedia() + this.updateCameraSectionPos() + } + + getThirdPersonCamera (pos: THREE.Vector3 | null, yaw: number, pitch: number) { + pos ??= this.cameraObject.position + + // Calculate camera offset based on perspective + const isBack = this.playerStateReactive.perspective === 'third_person_back' + const distance = 4 // Default third person distance + + // Calculate direction vector using proper world orientation + // We need to get the camera's current look direction and use that for positioning + + // Create a direction vector that represents where the camera is looking + // This matches the Three.js camera coordinate system + const direction = new THREE.Vector3(0, 0, -1) // Forward direction in camera space + + // Apply the same rotation that's applied to the camera container + const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitch) + const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw) + const finalQuat = new THREE.Quaternion().multiplyQuaternions(yawQuat, pitchQuat) + + // Transform the direction vector by the camera's rotation + direction.applyQuaternion(finalQuat) + + // For back view, we want the camera behind the player (opposite to view direction) + // For front view, we want the camera in front of the player (same as view direction) + if (isBack) { + direction.multiplyScalar(-1) + } + + // Create debug visualization if advanced stats are enabled + if (this.DEBUG_RAYCAST) { + this.debugRaycast(pos, direction, distance) + } + + // Perform raycast to avoid camera going through blocks + const raycaster = new THREE.Raycaster() + raycaster.set(pos, direction) + raycaster.far = distance // Limit raycast distance + + // Filter to only nearby chunks for performance + const nearbyChunks = Object.values(this.sectionObjects) + .filter(obj => obj.name === 'chunk' && obj.visible) + .filter(obj => { + // Get the mesh child which has the actual geometry + const mesh = obj.children.find(child => child.name === 'mesh') + if (!mesh) return false + + // Check distance from player position to chunk + const chunkWorldPos = new THREE.Vector3() + mesh.getWorldPosition(chunkWorldPos) + const distance = pos.distanceTo(chunkWorldPos) + return distance < 80 // Only check chunks within 80 blocks + }) + + // Get all mesh children for raycasting + const meshes: THREE.Object3D[] = [] + for (const chunk of nearbyChunks) { + const mesh = chunk.children.find(child => child.name === 'mesh') + if (mesh) meshes.push(mesh) + } + + const intersects = raycaster.intersectObjects(meshes, false) + + let finalDistance = distance + if (intersects.length > 0) { + // Use intersection distance minus a small offset to prevent clipping + finalDistance = Math.max(0.5, intersects[0].distance - 0.2) + } + + const finalPos = new Vec3( + pos.x + direction.x * finalDistance, + pos.y + direction.y * finalDistance, + pos.z + direction.z * finalDistance + ) + + return finalPos + } + + private debugRaycastHelper?: THREE.ArrowHelper + private debugHitPoint?: THREE.Mesh + + private debugRaycast (pos: THREE.Vector3, direction: THREE.Vector3, distance: number) { + // Remove existing debug objects + if (this.debugRaycastHelper) { + this.scene.remove(this.debugRaycastHelper) + this.debugRaycastHelper = undefined + } + if (this.debugHitPoint) { + this.scene.remove(this.debugHitPoint) + this.debugHitPoint = undefined + } + + // Create raycast arrow + this.debugRaycastHelper = new THREE.ArrowHelper( + direction.clone().normalize(), + pos, + distance, + 0xff_00_00, // Red color + distance * 0.1, + distance * 0.05 + ) + this.scene.add(this.debugRaycastHelper) + + // Create hit point indicator + const hitGeometry = new THREE.SphereGeometry(0.2, 8, 8) + const hitMaterial = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 }) + this.debugHitPoint = new THREE.Mesh(hitGeometry, hitMaterial) + this.debugHitPoint.position.copy(pos).add(direction.clone().multiplyScalar(distance)) + this.scene.add(this.debugHitPoint) + } + + prevFramePerspective = null as string | null + + updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void { + // if (this.freeFlyMode) { + // pos = this.freeFlyState.position + // pitch = this.freeFlyState.pitch + // yaw = this.freeFlyState.yaw + // } + + if (pos) { + if (this.renderer.xr.isPresenting) { + pos.y -= this.camera.position.y // Fix Y position of camera in world + } + + this.currentPosTween?.stop() + this.currentPosTween = new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, this.playerStateUtils.isSpectatingEntity() ? 150 : 50).start() + // this.freeFlyState.position = pos + } + + if (this.playerStateUtils.isSpectatingEntity()) { + const rotation = this.cameraShake.getBaseRotation() + // wrap in the correct direction + let yawOffset = 0 + const halfPi = Math.PI / 2 + if (rotation.yaw < halfPi && yaw > Math.PI + halfPi) { + yawOffset = -Math.PI * 2 + } else if (yaw < halfPi && rotation.yaw > Math.PI + halfPi) { + yawOffset = Math.PI * 2 + } + this.currentRotTween?.stop() + this.currentRotTween = new tweenJs.Tween(rotation).to({ pitch, yaw: yaw + yawOffset }, 100) + .onUpdate(params => this.cameraShake.setBaseRotation(params.pitch, params.yaw - yawOffset)).start() + } else { + this.currentRotTween?.stop() + this.cameraShake.setBaseRotation(pitch, yaw) + + const { perspective } = this.playerStateReactive + if (perspective === 'third_person_back' || perspective === 'third_person_front') { + // Use getThirdPersonCamera for proper raycasting with max distance of 4 + const currentCameraPos = this.cameraObject.position + const thirdPersonPos = this.getThirdPersonCamera( + new THREE.Vector3(currentCameraPos.x, currentCameraPos.y, currentCameraPos.z), + yaw, + pitch + ) + + const distance = currentCameraPos.distanceTo(new THREE.Vector3(thirdPersonPos.x, thirdPersonPos.y, thirdPersonPos.z)) + // Apply Z offset based on perspective and calculated distance + const zOffset = perspective === 'third_person_back' ? distance : -distance + this.camera.position.set(0, 0, zOffset) + + if (perspective === 'third_person_front') { + // Flip camera view 180 degrees around Y axis for front view + this.camera.rotation.set(0, Math.PI, 0) + } else { + this.camera.rotation.set(0, 0, 0) + } + } else { + this.camera.position.set(0, 0, 0) + this.camera.rotation.set(0, 0, 0) + + // remove any debug raycasting + if (this.debugRaycastHelper) { + this.scene.remove(this.debugRaycastHelper) + this.debugRaycastHelper = undefined + } + if (this.debugHitPoint) { + this.scene.remove(this.debugHitPoint) + this.debugHitPoint = undefined + } + } + } + + this.updateCameraSectionPos() + } + + debugChunksVisibilityOverride () { + const { chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled } = this.reactiveDebugParams + + const baseY = this.cameraSectionPos.y * 16 + + if ( + this.displayOptions.inWorldRenderingConfig.enableDebugOverlay && + chunksRenderAboveOverride !== undefined || + chunksRenderBelowOverride !== undefined || + chunksRenderDistanceOverride !== undefined + ) { + for (const [key, object] of Object.entries(this.sectionObjects)) { + const [x, y, z] = key.split(',').map(Number) + const isVisible = + // eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean + (chunksRenderAboveEnabled && chunksRenderAboveOverride !== undefined) ? y <= (baseY + chunksRenderAboveOverride) : true && + // eslint-disable-next-line @stylistic/indent-binary-ops, no-constant-binary-expression, sonarjs/no-redundant-boolean + (chunksRenderBelowEnabled && chunksRenderBelowOverride !== undefined) ? y >= (baseY - chunksRenderBelowOverride) : true && + // eslint-disable-next-line @stylistic/indent-binary-ops + (chunksRenderDistanceEnabled && chunksRenderDistanceOverride !== undefined) ? Math.abs(y - baseY) <= chunksRenderDistanceOverride : true + + object.visible = isVisible + } + } else { + for (const object of Object.values(this.sectionObjects)) { + object.visible = true + } + } + } + + render (sizeChanged = false) { + if (this.reactiveDebugParams.stopRendering) return + this.debugChunksVisibilityOverride() + const start = performance.now() + this.lastRendered = performance.now() + this.cursorBlock.render() + this.updateSectionOffsets() + + // Update skybox position to follow camera + const cameraPos = this.getCameraPosition() + this.skyboxRenderer.update(cameraPos, this.viewDistance) + + const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov + if (sizeOrFovChanged) { + const size = this.renderer.getSize(new THREE.Vector2()) + this.camera.aspect = size.width / size.height + this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov + this.camera.updateProjectionMatrix() + } + + if (!this.reactiveDebugParams.disableEntities) { + this.entities.render() + } + + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera + this.renderer.render(this.scene, cam) + + if ( + this.displayOptions.inWorldRenderingConfig.showHand && + this.playerStateReactive.gameMode !== 'spectator' && + this.playerStateReactive.perspective === 'first_person' && + // !this.freeFlyMode && + !this.renderer.xr.isPresenting + ) { + this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) + this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) + } + + for (const fountain of this.fountains) { + if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) { + fountain.createParticles(this.sectionObjects[fountain.sectionId]) + this.sectionObjects[fountain.sectionId].foutain = true + } + fountain.render() + } + + this.waypoints.render() + + for (const onRender of this.onRender) { + onRender() + } + const end = performance.now() + const totalTime = end - start + this.renderTimeAvgCount++ + this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount + this.renderTimeMax = Math.max(this.renderTimeMax, totalTime) + this.currentRenderedFrames++ + } + + renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) { + let textureData: string + if (blockEntity.SkullOwner) { + textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value + } else { + textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value + } + if (!textureData) return + + try { + const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString()) + let skinUrl = decodedData.textures?.SKIN?.url + const { skinTexturesProxy } = this.worldRendererConfig + if (skinTexturesProxy) { + skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy) + .replace('https://textures.minecraft.net/', skinTexturesProxy) + } + + const mesh = getMesh(this, skinUrl, armorModel.head) + const group = new THREE.Group() + if (isWall) { + mesh.position.set(0, 0.3125, 0.3125) + } + // move head model down as armor have a different offset than blocks + mesh.position.y -= 23 / 16 + group.add(mesh) + group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5) + group.rotation.set( + 0, + -THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)), + 0 + ) + group.scale.set(0.8, 0.8, 0.8) + return group + } catch (err) { + console.error('Error decoding player texture:', err) + } + } + + renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) { + const tex = this.getSignTexture(position, blockEntity, isHanging) + + if (!tex) return + + // todo implement + // const key = JSON.stringify({ position, rotation, isWall }) + // if (this.signsCache.has(key)) { + // console.log('cached', key) + // } else { + // this.signsCache.set(key, tex) + // } + + const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true })) + mesh.renderOrder = 999 + + const lineHeight = 7 / 16 + const scaleFactor = isHanging ? 1.3 : 1 + mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor) + + const thickness = (isHanging ? 2 : 1.5) / 16 + const wallSpacing = 0.25 / 16 + if (isWall && !isHanging) { + mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001) + } else { + mesh.position.set(0, 0, thickness / 2 + 0.0001) + } + + const group = new THREE.Group() + group.rotation.set( + 0, + -THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)), + 0 + ) + group.add(mesh) + const height = (isHanging ? 10 : 8) / 16 + const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16 + const textPosition = height / 2 + heightOffset + group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5) + return group + } + + lightUpdate (chunkX: number, chunkZ: number) { + // set all sections in the chunk dirty + for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) { + this.setSectionDirty(new Vec3(chunkX, y, chunkZ)) + } + } + + rerenderAllChunks () { // todo not clear what to do with loading chunks + for (const key of Object.keys(this.sectionObjects)) { + const [x, y, z] = key.split(',').map(Number) + this.setSectionDirty(new Vec3(x, y, z)) + } + } + + updateShowChunksBorder (value: boolean) { + for (const object of Object.values(this.sectionObjects)) { + for (const child of object.children) { + if (child.name === 'helper') { + child.visible = value + } + } + } + } + + resetWorld () { + super.resetWorld() + + for (const mesh of Object.values(this.sectionObjects)) { + this.scene.remove(mesh) + } + + // Clean up debug objects + if (this.debugRaycastHelper) { + this.scene.remove(this.debugRaycastHelper) + this.debugRaycastHelper = undefined + } + if (this.debugHitPoint) { + this.scene.remove(this.debugHitPoint) + this.debugHitPoint = undefined + } + } + + getLoadedChunksRelative (pos: Vec3, includeY = false) { + const [currentX, currentY, currentZ] = sectionPos(pos) + return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => { + const [xRaw, yRaw, zRaw] = key.split(',').map(Number) + const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw }) + const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}` + return [setKey, o] + })) + } + + cleanChunkTextures (x, z) { + const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {} + for (const key of Object.keys(textures)) { + textures[key].dispose() + delete textures[key] + } + } + + readdChunks () { + for (const key of Object.keys(this.sectionObjects)) { + this.scene.remove(this.sectionObjects[key]) + } + setTimeout(() => { + for (const key of Object.keys(this.sectionObjects)) { + this.scene.add(this.sectionObjects[key]) + } + }, 500) + } + + disableUpdates (children = this.scene.children) { + for (const child of children) { + child.matrixWorldNeedsUpdate = false + this.disableUpdates(child.children ?? []) + } + } + + removeColumn (x, z) { + super.removeColumn(x, z) + + this.cleanChunkTextures(x, z) + for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) { + this.setSectionDirty(new Vec3(x, y, z), false) + const key = `${x},${y},${z}` + const mesh = this.sectionObjects[key] + if (mesh) { + this.scene.remove(mesh) + disposeObject(mesh) + } + delete this.sectionObjects[key] + } + } + + setSectionDirty (...args: Parameters) { + const [pos] = args + this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! + super.setSectionDirty(...args) + } + + static getRendererInfo (renderer: THREE.WebGLRenderer) { + try { + const gl = renderer.getContext() + return `${gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)}` + } catch (err) { + console.warn('Failed to get renderer info', err) + } + } + + worldStop () { + this.media.onWorldStop() + } + + destroy (): void { + super.destroy() + this.skyboxRenderer.dispose() + } + + shouldObjectVisible (object: THREE.Object3D) { + // Get chunk coordinates + const chunkX = Math.floor(object.position.x / 16) * 16 + const chunkZ = Math.floor(object.position.z / 16) * 16 + const sectionY = Math.floor(object.position.y / 16) * 16 + + const chunkKey = `${chunkX},${chunkZ}` + const sectionKey = `${chunkX},${sectionY},${chunkZ}` + + return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey] + } + + updateSectionOffsets () { + const currentTime = performance.now() + for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) { + const timeDelta = (currentTime - anim.time) / 1000 // Convert to seconds + anim.time = currentTime + + // Update offsets based on speed and time delta + anim.currentOffsetX += anim.speedX * timeDelta + anim.currentOffsetY += anim.speedY * timeDelta + anim.currentOffsetZ += anim.speedZ * timeDelta + + // Apply limits if they exist + if (anim.limitX !== undefined) { + if (anim.speedX > 0) { + anim.currentOffsetX = Math.min(anim.currentOffsetX, anim.limitX) + } else { + anim.currentOffsetX = Math.max(anim.currentOffsetX, anim.limitX) + } + } + if (anim.limitY !== undefined) { + if (anim.speedY > 0) { + anim.currentOffsetY = Math.min(anim.currentOffsetY, anim.limitY) + } else { + anim.currentOffsetY = Math.max(anim.currentOffsetY, anim.limitY) + } + } + if (anim.limitZ !== undefined) { + if (anim.speedZ > 0) { + anim.currentOffsetZ = Math.min(anim.currentOffsetZ, anim.limitZ) + } else { + anim.currentOffsetZ = Math.max(anim.currentOffsetZ, anim.limitZ) + } + } + + // Apply the offset to the section object + const section = this.sectionObjects[key] + if (section) { + section.position.set( + anim.currentOffsetX, + anim.currentOffsetY, + anim.currentOffsetZ + ) + section.updateMatrix() + } + } + } + + reloadWorld () { + this.entities.reloadEntities() + } +} + +class StarField { + points?: THREE.Points + private _enabled = true + get enabled () { + return this._enabled + } + + set enabled (value) { + this._enabled = value + if (this.points) { + this.points.visible = value + } + } + + constructor ( + private readonly worldRenderer: WorldRendererThree + ) { + const clock = new THREE.Clock() + const speed = 0.2 + this.worldRenderer.onRender.push(() => { + if (!this.points) return + this.points.position.copy(this.worldRenderer.getCameraPosition()); + (this.points.material as StarfieldMaterial).uniforms.time.value = clock.getElapsedTime() * speed + }) + } + + addToScene () { + if (this.points || !this.enabled) return + + const radius = 80 + const depth = 50 + const count = 7000 + const factor = 7 + const saturation = 10 + + const geometry = new THREE.BufferGeometry() + + const genStar = r => new THREE.Vector3().setFromSpherical(new THREE.Spherical(r, Math.acos(1 - Math.random() * 2), Math.random() * 2 * Math.PI)) + + const positions = [] as number[] + const colors = [] as number[] + const sizes = Array.from({ length: count }, () => (0.5 + 0.5 * Math.random()) * factor) + const color = new THREE.Color() + let r = radius + depth + const increment = depth / count + for (let i = 0; i < count; i++) { + r -= increment * Math.random() + positions.push(...genStar(r).toArray()) + color.setHSL(i / count, saturation, 0.9) + colors.push(color.r, color.g, color.b) + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)) + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)) + geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1)) + + // Create a material + const material = new StarfieldMaterial() + material.blending = THREE.AdditiveBlending + material.depthTest = false + material.transparent = true + + // Create points and add them to the scene + this.points = new THREE.Points(geometry, material) + this.worldRenderer.scene.add(this.points) + + this.points.renderOrder = -1 + } + + remove () { + if (this.points) { + this.points.geometry.dispose(); + (this.points.material as THREE.Material).dispose() + this.worldRenderer.scene.remove(this.points) + + this.points = undefined + } + } +} + +const version = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10) +class StarfieldMaterial extends THREE.ShaderMaterial { + constructor () { + super({ + uniforms: { time: { value: 0 }, fade: { value: 1 } }, + vertexShader: /* glsl */ ` + uniform float time; + attribute float size; + varying vec3 vColor; + attribute vec3 color; + void main() { + vColor = color; + vec4 mvPosition = modelViewMatrix * vec4(position, 0.5); + gl_PointSize = 0.7 * size * (30.0 / -mvPosition.z) * (3.0 + sin(time + 100.0)); + gl_Position = projectionMatrix * mvPosition; + }`, + fragmentShader: /* glsl */ ` + uniform sampler2D pointTexture; + uniform float fade; + varying vec3 vColor; + void main() { + float opacity = 1.0; + gl_FragColor = vec4(vColor, 1.0); + + #include + #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> + }`, + }) + } +} diff --git a/rsbuild.config.ts b/rsbuild.config.ts new file mode 100644 index 00000000..6cd6b2ed --- /dev/null +++ b/rsbuild.config.ts @@ -0,0 +1,298 @@ +/// +import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' +import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules' +import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill' +import { pluginTypeCheck } from '@rsbuild/plugin-type-check' +import path from 'path' +import childProcess from 'child_process' +import fs from 'fs' +import fsExtra from 'fs-extra' +import { promisify } from 'util' +import { generateSW } from 'workbox-build' +import { getSwAdditionalEntries } from './scripts/build' +import { appAndRendererSharedConfig } from './renderer/rsbuildSharedConfig' +import { genLargeDataAliases } from './scripts/genLargeDataAliases' +import sharp from 'sharp' +import supportedVersions from './src/supportedVersions.mjs' +import { startWsServer } from './scripts/wsServer' + +const SINGLE_FILE_BUILD = process.env.SINGLE_FILE_BUILD === 'true' + +if (SINGLE_FILE_BUILD) { + const patchCssFile = 'node_modules/pixelarticons/fonts/pixelart-icons-font.css' + const text = fs.readFileSync(patchCssFile, 'utf8') + fs.writeFileSync(patchCssFile, text.replaceAll("url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'),", ""), 'utf8') +} + +//@ts-ignore +try { require('./localSettings.js') } catch { } + +const execAsync = promisify(childProcess.exec) + +const buildingVersion = new Date().toISOString().split(':')[0] + +const dev = process.env.NODE_ENV === 'development' +const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true' + +let releaseTag +let releaseLink +let releaseChangelog +let githubRepositoryFallback + +if (fs.existsSync('./assets/release.json')) { + const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8')) + releaseTag = releaseJson.latestTag + releaseLink = releaseJson.isCommit ? `/commit/${releaseJson.latestTag}` : `/releases/${releaseJson.latestTag}` + releaseChangelog = releaseJson.changelog?.replace(//, '') + githubRepositoryFallback = releaseJson.repository +} + +const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8')) +try { + Object.assign(configJson, JSON.parse(fs.readFileSync(process.env.LOCAL_CONFIG_FILE || './config.local.json', 'utf8'))) +} catch (err) {} +if (dev) { + configJson.defaultProxy = ':8080' +} + +const configSource = (SINGLE_FILE_BUILD ? 'BUNDLED' : (process.env.CONFIG_JSON_SOURCE || 'REMOTE')) as 'BUNDLED' | 'REMOTE' + +const faviconPath = 'favicon.png' + +const enableMetrics = process.env.ENABLE_METRICS === 'true' + +// base options are in ./renderer/rsbuildSharedConfig.ts +const appConfig = defineConfig({ + html: { + template: './index.html', + inject: 'body', + tags: [ + ...SINGLE_FILE_BUILD ? [] : [ + { + tag: 'link', + attrs: { + rel: 'manifest', + crossorigin: 'anonymous', + href: 'manifest.json' + }, + } + ], + // + // + // + { + tag: 'link', + attrs: { + rel: 'favicon', + href: faviconPath + } + }, + ...SINGLE_FILE_BUILD ? [] : [ + { + tag: 'link', + attrs: { + rel: 'icon', + type: 'image/png', + href: faviconPath + } + }, + { + tag: 'meta', + attrs: { + property: 'og:image', + content: faviconPath + } + } + ] + ] + }, + output: { + externals: [ + 'sharp' + ], + sourceMap: { + js: 'source-map', + css: true, + }, + minify: { + // js: false, + jsOptions: { + minimizerOptions: { + mangle: { + safari10: true, + keep_classnames: true, + keep_fnames: true, + keep_private_props: true, + }, + compress: { + unused: true, + }, + }, + }, + }, + distPath: SINGLE_FILE_BUILD ? { + html: './single', + } : undefined, + inlineScripts: SINGLE_FILE_BUILD, + inlineStyles: SINGLE_FILE_BUILD, + // 50kb limit for data uri + dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024 + }, + performance: { + // prefetch: { + // include(filename) { + // return filename.includes('mc-data') || filename.includes('mc-assets') + // }, + // }, + }, + source: { + entry: { + index: './src/index.ts', + }, + // exclude: [ + // /.woff$/ + // ], + define: { + 'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'), + 'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS), + 'process.env.SINGLE_FILE_BUILD': JSON.stringify(process.env.SINGLE_FILE_BUILD), + 'process.env.SINGLE_FILE_BUILD_MODE': JSON.stringify(process.env.SINGLE_FILE_BUILD), + 'process.platform': '"browser"', + 'process.env.GITHUB_URL': + JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`), + 'process.env.ALWAYS_MINIMAL_SERVER_UI': JSON.stringify(process.env.ALWAYS_MINIMAL_SERVER_UI), + 'process.env.RELEASE_TAG': JSON.stringify(releaseTag), + 'process.env.RELEASE_LINK': JSON.stringify(releaseLink), + 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), + 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), + 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), + 'process.env.ENABLE_COOKIE_STORAGE': JSON.stringify(process.env.ENABLE_COOKIE_STORAGE || true), + 'process.env.COOKIE_STORAGE_PREFIX': JSON.stringify(process.env.COOKIE_STORAGE_PREFIX || ''), + 'process.env.WS_PORT': JSON.stringify(enableMetrics ? 8081 : false), + }, + }, + server: { + // strictPort: true, + // publicDir: { + // name: 'assets', + // }, + proxy: { + '/api': 'http://localhost:8080', + }, + }, + plugins: [ + pluginTypedCSSModules(), + { + name: 'test', + setup(build: RsbuildPluginAPI) { + const prep = async () => { + console.time('total-prep') + fs.mkdirSync('./generated', { recursive: true }) + if (!fs.existsSync('./generated/minecraft-data-optimized.json') || !fs.existsSync('./generated/mc-assets-compressed.js') || require('./generated/minecraft-data-optimized.json').versionKey !== require('minecraft-data/package.json').version) { + childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit' }) + } + childProcess.execSync('tsx ./scripts/genShims.ts', { stdio: 'inherit' }) + if (!fs.existsSync('./generated/latestBlockCollisionsShapes.json') || require('./generated/latestBlockCollisionsShapes.json').versionKey !== require('minecraft-data/package.json').version) { + childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' }) + } + // childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' }) + genLargeDataAliases(SINGLE_FILE_BUILD || process.env.ALWAYS_COMPRESS_LARGE_DATA === 'true') + fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity') + fsExtra.copySync('./assets/background', './dist/background') + fs.copyFileSync('./assets/favicon.png', './dist/favicon.png') + fs.copyFileSync('./assets/playground.html', './dist/playground.html') + fs.copyFileSync('./assets/manifest.json', './dist/manifest.json') + fs.copyFileSync('./assets/config.html', './dist/config.html') + fs.copyFileSync('./assets/debug-inputs.html', './dist/debug-inputs.html') + fs.copyFileSync('./assets/loading-bg.jpg', './dist/loading-bg.jpg') + if (fs.existsSync('./assets/release.json')) { + fs.copyFileSync('./assets/release.json', './dist/release.json') + } + + if (configSource === 'REMOTE') { + fs.writeFileSync('./dist/config.json', JSON.stringify(configJson, undefined, 2), 'utf8') + } + if (fs.existsSync('./generated/sounds.js')) { + fs.copyFileSync('./generated/sounds.js', './dist/sounds.js') + } + // childProcess.execSync('./scripts/prepareSounds.mjs', { stdio: 'inherit' }) + // childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' }) + // childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' }) + if (fs.existsSync('./renderer/dist/mesher.js') && dev) { + // copy mesher + fs.copyFileSync('./renderer/dist/mesher.js', './dist/mesher.js') + fs.copyFileSync('./renderer/dist/mesher.js.map', './dist/mesher.js.map') + } else if (!dev) { + await execAsync('pnpm run build-mesher') + } + fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8') + + // Start WebSocket server in development + if (dev && enableMetrics) { + await startWsServer(8081, false) + } + + console.timeEnd('total-prep') + } + if (!dev) { + build.onBeforeBuild(async () => { + prep() + }) + build.onAfterBuild(async () => { + if (fs.readdirSync('./assets/customTextures').length > 0) { + childProcess.execSync('tsx ./scripts/patchAssets.ts', { stdio: 'inherit' }) + } + + if (SINGLE_FILE_BUILD) { + // check that only index.html is in the dist/single folder + const singleBuildFiles = fs.readdirSync('./dist/single') + if (singleBuildFiles.length !== 1 || singleBuildFiles[0] !== 'index.html') { + throw new Error('Single file build must only have index.html in the dist/single folder. Ensure workers are imported & built correctly.') + } + + // process index.html + const singleBuildHtml = './dist/single/index.html' + let html = fs.readFileSync(singleBuildHtml, 'utf8') + const verToMajor = (ver: string) => ver.split('.').slice(0, 2).join('.') + const supportedMajorVersions = [...new Set(supportedVersions.map(a => verToMajor(a)))].join(', ') + html = `\n\n\n${html}` + + const resizedImage = (await (sharp('./assets/favicon.png') as any).resize(64).toBuffer()).toString('base64') + html = html.replace('favicon.png', `data:image/png;base64,${resizedImage}`) + html = html.replace('src="./loading-bg.jpg"', `src="data:image/png;base64,${fs.readFileSync('./assets/loading-bg.jpg', 'base64')}"`) + html += '' + fs.writeFileSync(singleBuildHtml, html, 'utf8') + // write output file size + console.log('single file size', (fs.statSync(singleBuildHtml).size / 1024 / 1024).toFixed(2), 'mb') + } else { + if (!disableServiceWorker) { + const { count, size, warnings } = await generateSW({ + // dontCacheBustURLsMatching: [new RegExp('...')], + globDirectory: 'dist', + skipWaiting: true, + clientsClaim: true, + additionalManifestEntries: getSwAdditionalEntries(), + globPatterns: [], + swDest: './dist/service-worker.js', + }) + } + } + }) + } + build.onBeforeStartDevServer(() => prep()) + }, + }, + ], + // performance: { + // bundleAnalyze: { + // analyzerMode: 'json', + // reportFilename: 'report.json', + // }, + // }, +}) + +export default mergeRsbuildConfig( + appAndRendererSharedConfig(), + appConfig +) diff --git a/scripts/build.js b/scripts/build.js index 9840e5d3..2d5e0a2e 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -5,18 +5,15 @@ const glob = require('glob') const fs = require('fs') const crypto = require('crypto') const path = require('path') -const McAssets = require('minecraft-assets') -const prismarineViewerBase = "./node_modules/prismarine-viewer" -const entityMcAssets = McAssets('1.16.4') +const prismarineViewerBase = "./node_modules/renderer" // these files could be copied at build time eg with copy plugin, but copy plugin slows down the config so we copy them there, alternative we could inline it in esbuild config const filesToCopy = [ - { from: `${prismarineViewerBase}/public/blocksStates/`, to: 'dist/blocksStates/' }, - { from: `${prismarineViewerBase}/public/worker.js`, to: 'dist/worker.js' }, + { from: `${prismarineViewerBase}/public/mesher.js`, to: 'dist/mesher.js' }, { from: './assets/', to: './dist/' }, { from: './config.json', to: 'dist/config.json' }, - { from: path.join(entityMcAssets.directory, 'entity'), to: 'dist/textures/1.16.4/entity' }, + // { from: path.join(entityMcAssets.directory, 'entity'), to: 'dist/textures/1.16.4/entity' }, ] exports.filesToCopy = filesToCopy exports.copyFiles = (dev = false) => { @@ -46,29 +43,26 @@ exports.copyFilesDev = () => { exports.getSwAdditionalEntries = () => { // need to be careful with this - const singlePlayerVersion = defaultLocalServerOptions.version const filesToCachePatterns = [ 'index.html', - 'index.js', - 'index.css', - 'favicon.ico', - `mc-data/${defaultLocalServerOptions.versionMajor}.js`, - `blocksStates/${singlePlayerVersion}.json`, - 'extra-textures/**', + 'background/**', // todo-low copy from assets '*.mp3', '*.ttf', '*.png', '*.woff', - 'worker.js', - // todo-low preload entity atlas? - `textures/${singlePlayerVersion}.png`, - `textures/1.16.4/entity/squid.png`, + 'mesher.js', + 'manifest.json', + 'worldSaveWorker.js', + `textures/entity/squid/squid.png`, + 'sounds.js', + // everything but not .map + 'static/**/!(*.map)', ] const filesNeedsCacheKey = [ - 'index.js', - 'index.css', - 'worker.js', + 'index.html', + 'mesher.js', + 'worldSaveWorker.js', ] const output = [] console.log('Generating sw additional entries...') @@ -87,15 +81,28 @@ exports.getSwAdditionalEntries = () => { output.push({ url, revision }) } } + if (output.length > 40) { + throw new Error(`SW: Ios has a limit of 40 urls to cache (now ${output.length})`) + } console.log(`Got ${output.length} additional sw entries to cache`) return output } exports.moveStorybookFiles = () => { - fsExtra.moveSync('storybook-static', 'dist/storybook', {overwrite: true,}) + fsExtra.moveSync('storybook-static', 'dist/storybook', { overwrite: true, }) fsExtra.copySync('dist/storybook', '.vercel/output/static/storybook') } +exports.getSwFilesSize = () => { + const files = exports.getSwAdditionalEntries() + let size = 0 + for (const { url } of files) { + const file = path.join(__dirname, '../dist', url) + size += fs.statSync(file).size + } + console.log('mb', size / 1024 / 1024) +} + const fn = require.main === module && exports[process.argv[2]] if (fn) { diff --git a/scripts/buildNpmReact.ts b/scripts/buildNpmReact.ts new file mode 100644 index 00000000..f4f00e4d --- /dev/null +++ b/scripts/buildNpmReact.ts @@ -0,0 +1,160 @@ +import fs from 'fs' +import path from 'path' +import { build, transform } from 'esbuild' +import { execSync } from 'child_process' +// import { copy } from 'fs-extra' +import { glob } from 'glob' + +const isAbsolute = (path: string) => path.startsWith('/') || /^[A-Z]:/i.test(path) + +fs.promises.readdir(path.resolve(__dirname, '../src/react')).then(async (files) => { + const components = files + .filter((file) => { + if (file.startsWith('Concept')) return false + return file.endsWith('.stories.tsx') + }) + .map((file) => { + return file.replace('.stories.tsx', '') + }) + + const content = components.map((component) => { + return `export { default as ${component} } from './${component}'` + }).join('\n') + + await fs.promises.writeFile( + path.resolve(__dirname, '../src/react/npmReactComponents.ts'), + content + ) + + execSync('pnpm tsc -p tsconfig.npm.json', { + cwd: path.resolve(__dirname, '../'), + stdio: 'inherit', + }) + + const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.npm.json'), 'utf-8')) + const packageJsonRoot = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8')) + const external = Object.keys(packageJson.peerDependencies) + const dependencies = new Set() + let version = process.argv[2] || packageJsonRoot.version + version = version.replace(/^v/, '') + packageJson.version = version + + const externalize = ['renderer', 'mc-assets'] + const { metafile } = await build({ + entryPoints: [path.resolve(__dirname, '../src/react/npmReactComponents.ts')], + bundle: true, + outfile: path.resolve(__dirname, '../dist-npm/bundle.esm.js'), + format: 'esm', + platform: 'browser', + target: 'es2020', + external: external, + metafile: true, + minify: true, + write: false, // todo + loader: { + '.png': 'dataurl', + '.jpg': 'dataurl', + '.jpeg': 'dataurl', + '.webp': 'dataurl', + '.css': 'text', + }, + plugins: [ + // on external module resolve + { + name: 'collect-imports', + setup (build) { + build.onResolve({ filter: /.*/ }, (args) => { + if (args.importer.includes('node_modules') || external.some(x => args.path.startsWith(x)) || isAbsolute(args.path)) { + return undefined + } + if (args.path.startsWith('./') || args.path.startsWith('../')) { + if (args.path.endsWith('.png') || args.path.endsWith('.css') || args.path.endsWith('.jpg') || args.path.endsWith('.jpeg')) { + const absoluteImporting = path.join(path.dirname(args.importer), args.path) + const absoluteRoot = path.resolve(__dirname, '../src') + const relativeToRoot = path.relative(absoluteRoot, absoluteImporting) + fs.copyFileSync(absoluteImporting, path.resolve(__dirname, '../dist-npm/dist-pre', relativeToRoot)) + } + // default behavior + return undefined + } + const dep = args.path.startsWith('@') ? args.path.split('/').slice(0, 2).join('/') : args.path.split('/')[0] + if (!dependencies.has(dep)) { + dependencies.add(dep) + console.log('Adding dependency:', dep, 'from', args.importer) + } + // return { external: true } + }) + }, + }, + ], + }) + for (const dependency of dependencies) { + if (externalize.includes(dependency)) continue + if (!packageJsonRoot.dependencies[dependency]) throw new Error(`Dependency ${dependency} not found in package.json`) + packageJson.dependencies[dependency] = packageJsonRoot.dependencies[dependency] + } + fs.writeFileSync(path.resolve(__dirname, '../dist-npm/package.json'), JSON.stringify(packageJson, null, 2)) + // fs.promises.writeFile('./dist-npm/metafile.json', JSON.stringify(metafile, null, 2)) + + await build({ + entryPoints: ['dist-npm/dist-pre/**/*.js'], + outdir: 'dist-npm/dist', + // allowOverwrite: true, + jsx: 'preserve', + bundle: true, + target: 'esnext', + platform: 'browser', + format: 'esm', + loader: { + '.css': 'copy', + '.module.css': 'copy', + '.png': 'copy', + }, + minifyWhitespace: false, + logOverride: { + // 'ignored-bare-import': "info" + }, + plugins: [ + { + name: 'all-external', + setup (build) { + build.onResolve({ filter: /.*/ }, (args) => { + // todo use workspace deps + if (externalize.some(x => args.path.startsWith(x))) { + return undefined // bundle + } + if (args.path.endsWith('.css') || args.path.endsWith('.png') || args.path.endsWith('.jpg') || args.path.endsWith('.jpeg')) { + return undefined // loader action + } + return { + path: args.path, + external: true, + } + }) + }, + } + ], + }) + + const paths = await glob('dist-npm/dist-pre/**/*.d.ts') + // copy to dist + for (const p of paths) { + const relative = path.relative('dist-npm/dist-pre', p) + const target = path.resolve('dist-npm/dist', relative) + fs.copyFileSync(p, target) + } + // rm dist-pre + fs.rmSync('dist-npm/dist-pre', { recursive: true }) + fs.copyFileSync(path.resolve(__dirname, '../README.NPM.MD'), path.resolve(__dirname, '../dist-npm/README.md')) + + if (version !== '0.0.0-dev') { + execSync('npm publish', { + cwd: path.resolve(__dirname, '../dist-npm'), + env: { + ...process.env, + NPM_TOKEN: process.env.NPM_TOKEN, + NODE_AUTH_TOKEN: process.env.NPM_TOKEN + } + }) + } +}) diff --git a/scripts/dockerPrepare.mjs b/scripts/dockerPrepare.mjs new file mode 100644 index 00000000..62a4f5e4 --- /dev/null +++ b/scripts/dockerPrepare.mjs @@ -0,0 +1,35 @@ +//@ts-check +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import { execSync } from 'child_process' + +// Get repository from git config +const getGitRepository = () => { + try { + const gitConfig = fs.readFileSync('.git/config', 'utf8') + const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m) + if (originUrlMatch) { + return originUrlMatch[1] + } + } catch (err) { + console.warn('Failed to read git repository from config:', err) + } + return null +} + +// write release tag and repository info +const commitShort = execSync('git rev-parse --short HEAD').toString().trim() +const repository = getGitRepository() +fs.writeFileSync('./assets/release.json', JSON.stringify({ + latestTag: `${commitShort} (docker)`, + repository +}), 'utf8') + +const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')) +delete packageJson.optionalDependencies +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2), 'utf8') + +const packageJsonViewer = JSON.parse(fs.readFileSync('./renderer/package.json', 'utf8')) +delete packageJsonViewer.optionalDependencies +fs.writeFileSync('./renderer/package.json', JSON.stringify(packageJsonViewer, null, 2), 'utf8') diff --git a/scripts/downloadSoundsMap.mjs b/scripts/downloadSoundsMap.mjs index 066a3df7..f5791768 100644 --- a/scripts/downloadSoundsMap.mjs +++ b/scripts/downloadSoundsMap.mjs @@ -1,9 +1,12 @@ import fs from 'fs' -const url = 'https://github.com/zardoy/prismarine-web-client/raw/sounds-generated/sounds.js' -const savePath = 'dist/sounds.js' +const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds-v2.js' fetch(url).then(res => res.text()).then(data => { - fs.writeFileSync(savePath, data, 'utf8') + if (fs.existsSync('./dist')) { + fs.writeFileSync('./dist/sounds.js', data, 'utf8') + } + fs.mkdirSync('./generated', { recursive: true }) + fs.writeFileSync('./generated/sounds.js', data, 'utf8') if (fs.existsSync('.vercel/output/static/')) { fs.writeFileSync('.vercel/output/static/sounds.js', data, 'utf8') } diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index 978157ff..431c16a4 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -1,311 +1,27 @@ //@ts-check -import { polyfillNode } from 'esbuild-plugin-polyfill-node' -import { join, dirname, basename } from 'path' +import { join, dirname } from 'path' import * as fs from 'fs' -import { filesize } from 'filesize' -import MCProtocol from 'minecraft-protocol' -import MCData from 'minecraft-data' +import { fileURLToPath } from 'url' -const { supportedVersions } = MCProtocol - -const prod = process.argv.includes('--prod') -let connectedClients = [] - -let singleFileBuild = false -export const setSingleFileBuild = (v) => singleFileBuild = v +const __dirname = dirname(fileURLToPath(new URL(import.meta.url))) /** @type {import('esbuild').Plugin[]} */ -const plugins = [ +const mesherSharedPlugins = [ { - name: 'strict-aliases', + name: 'minecraft-data', setup (build) { - build.onResolve({ - filter: /^minecraft-protocol$/, - }, async ({ kind, resolveDir }) => { - return { - path: (await build.resolve('minecraft-protocol/src/index.js', { kind, resolveDir })).path, - } - }) build.onLoad({ - filter: /minecraft-data[\/\\]data.js$/, - }, (args) => { - const version = supportedVersions.at(-1); - const data = MCData(version) - const defaultVersionsObj = { - // default protocol data, needed for auto-version - [version]: { - version: data.version, - // protocol: JSON.parse(fs.readFileSync(join(args.path, '..', 'minecraft-data/data/pc/1.20/protocol.json'), 'utf8')), - protocol: data.protocol, - } - } + filter: /data[\/\\]pc[\/\\]common[\/\\]legacy.json$/, + }, async (args) => { + const data = fs.readFileSync(join(__dirname, '../src/preflatMap.json'), 'utf8') return { - contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`, - loader: 'js', - } - }) - build.onResolve({ - filter: /^minecraft-assets$/, - }, () => { - throw new Error('hit banned package') - }) - } - }, - { - name: 'data-assets', - setup (build) { - build.onResolve({ - filter: /.*/, - }, async ({ path, ...rest }) => { - if (['.woff', '.woff2', '.ttf'].some(ext => path.endsWith(ext)) || path.startsWith('extra-textures/')) { - return { - path, - namespace: 'assets', - external: true, - } - } - }) - - build.onEnd(async ({ metafile, outputFiles = [], errors }) => { - if (errors.length) return - // write outputFiles - if (singleFileBuild) { - const jsFile = outputFiles.find(outputFile => outputFile.path.endsWith('.js'))?.contents - const cssFile = outputFiles.find(outputFile => outputFile.path.endsWith('.css'))?.contents - let html = fs.readFileSync('./index.html', 'utf8') - html = html.replace('', ``) - html = html.replace('', ``) - - const favicon = fs.readFileSync('./assets/favicon.png', 'base64') - html = html.replace(``, ``) - html = html.replace('', '') - html = html.replace('', '') - fs.writeFileSync('./dist/minecraft.html', html) - outputFiles - } else { - for (const file of outputFiles) { - await fs.promises.writeFile(file.path, file.contents) - } - } - if (!prod) return - // const deps = Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).map(([x, { bytes }]) => [x, filesize(bytes)]).slice(0, 5) - //@ts-ignore - const sizeByExt = {} - //@ts-ignore - Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).forEach(([x, { bytes }]) => { - const ext = x.slice(x.lastIndexOf('.')) - sizeByExt[ext] ??= 0 - sizeByExt[ext] += bytes - }) - console.log('Input size by ext:') - console.log(Object.fromEntries(Object.entries(sizeByExt).map(x => [x[0], filesize(x[1])]))) - }) - }, - }, - { - name: 'prevent-incorrect-linking', - setup (build) { - build.onResolve({ - filter: /.+/, - }, async ({ resolveDir, path, importer, kind, pluginData }) => { - if (pluginData?.__internal) return - // not ideal as packages can have different version, on the other hand we should not have multiple versions of the same package of developing deps - const packageName = path.startsWith('@') ? path.split('/', 2).join('/') : path.split('/', 1)[0] - const localPackageToReuse = join('node_modules', packageName) - if (!resolveDir.startsWith(process.cwd()) && ['./', '../'].every(x => !path.startsWith(x)) && fs.existsSync(localPackageToReuse)) { - const redirected = await build.resolve(path, { kind: 'import-statement', resolveDir: process.cwd(), pluginData: { __internal: true }, }) - return redirected - } - // disallow imports from outside the root directory to ensure modules are resolved from node_modules of this workspace - // if ([resolveDir, path].some(x => x.includes('node_modules')) && !resolveDir.startsWith(process.cwd())) { - // // why? ensure workspace dependency versions are used (we have overrides and need to dedupe so it doesn't grow in size) - // throw new Error(`Restricted package import from outside the root directory: ${resolveDir}`) - // } - return undefined - }) - } - }, - { - name: 'watch-notify', - setup (build) { - let count = 0 - let time - let prevHash - build.onStart(() => { - time = Date.now() - }) - build.onEnd(({ errors, outputFiles: _outputFiles, metafile, warnings }) => { - /** @type {import('esbuild').OutputFile[]} */ - // @ts-ignore - const outputFiles = _outputFiles - const elapsed = Date.now() - time - outputFiles.find(outputFile => outputFile.path) - - if (errors.length) { - connectedClients.forEach((res) => { - res.write(`data: ${JSON.stringify({ errors: errors.map(error => error.text) })}\n\n`) - res.flush() - }) - return - } - - // write metafile to disk if needed to analyze - fs.writeFileSync('dist/meta.json', JSON.stringify(metafile, null, 2)) - - /** @type {import('esbuild').OutputFile} */ - //@ts-ignore - const outputFile = outputFiles.find(x => x.path.endsWith('.js')) - if (outputFile.hash === prevHash) { - console.log('Ignoring reload as contents the same') - return - } - prevHash = outputFile.hash - let outputText = outputFile.text - //@ts-ignore - if (['inline', 'both'].includes(build.initialOptions.sourcemap)) { - outputText = outputText.slice(0, outputText.indexOf('//# sourceMappingURL=data:application/json;base64,')) - } - console.log(`Done in ${elapsed}ms. Size: ${filesize(outputText.length)} (${build.initialOptions.minify ? 'minified' : 'without minify'})`) - - if (count++ === 0) { - return - } - - connectedClients.forEach((res) => { - res.write(`data: ${JSON.stringify({ update: { time: elapsed } })}\n\n`) - res.flush() - }) - connectedClients.length = 0 - }) - } - }, - { - name: 'esbuild-readdir', - setup (build) { - build.onResolve({ - filter: /^esbuild-readdir:.+$/, - }, ({ resolveDir, path }) => { - return { - namespace: 'esbuild-readdir', - path, - pluginData: { - resolveDir: join(resolveDir, path.replace(/^esbuild-readdir:/, '')) - }, - } - }) - build.onLoad({ - filter: /.+/, - namespace: 'esbuild-readdir', - }, async ({ pluginData }) => { - const { resolveDir } = pluginData - const files = await fs.promises.readdir(resolveDir) - return { - contents: `module.exports = ${JSON.stringify(files)}`, - resolveDir, + contents: `module.exports = ${data}`, loader: 'js', } }) } - }, - { - name: 'esbuild-import-glob', - setup (build) { - build.onResolve({ - filter: /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/, - }, ({ resolveDir, path }) => { - return { - namespace: 'esbuild-import-glob', - path, - pluginData: { - resolveDir - }, - } - }) - build.onLoad({ - filter: /.+/, - namespace: 'esbuild-import-glob', - }, async ({ pluginData, path }) => { - const { resolveDir } = pluginData - //@ts-ignore - const [, userPath, skipFiles] = /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/g.exec(path) - const files = (await fs.promises.readdir(join(resolveDir, userPath))).filter(f => !skipFiles.includes(f)) - return { - contents: `module.exports = { ${files.map(f => `'${f}': require('./${join(userPath, f)}')`).join(',')} }`, - resolveDir, - loader: 'js', - } - }) - } - }, - { - name: 'fix-dynamic-require', - setup (build) { - build.onResolve({ - filter: /1\.14\/chunk/, - }, async ({ resolveDir, path }) => { - if (!resolveDir.includes('prismarine-provider-anvil')) return - return { - namespace: 'fix-dynamic-require', - path, - pluginData: { - resolvedPath: `${join(resolveDir, path)}.js`, - resolveDir - }, - } - }) - build.onLoad({ - filter: /.+/, - namespace: 'fix-dynamic-require', - }, async ({ pluginData: { resolvedPath, resolveDir } }) => { - const resolvedFile = await fs.promises.readFile(resolvedPath, 'utf8') - return { - contents: resolvedFile.replace("require(`prismarine-chunk/src/pc/common/BitArray${noSpan ? 'NoSpan' : ''}`)", "noSpan ? require(`prismarine-chunk/src/pc/common/BitArray`) : require(`prismarine-chunk/src/pc/common/BitArrayNoSpan`)"), - resolveDir, - loader: 'js', - } - }) - } - }, - { - name: 'react-displayname', - setup (build) { - build.onLoad({ - filter: /.tsx$/, - }, async ({ path }) => { - let file = await fs.promises.readFile(path, 'utf8') - const fileName = basename(path, '.tsx') - let replaced = false - const varName = `__${fileName}_COMPONENT` - file = file.replace(/export default /, () => { - replaced = true - return `const ${varName} = ` - }) - if (replaced) { - file += `;${varName}.displayName = '${fileName}';export default ${varName};` - } - - return { - contents: file, - loader: 'tsx', - } - }) - } - }, - polyfillNode({ - polyfills: { - fs: false, - dns: false, - crypto: false, - events: false, - http: false, - stream: false, - buffer: false, - perf_hooks: false, - net: false, - assert: false, - }, - }) + } ] -export { plugins, connectedClients as clients } +export { mesherSharedPlugins } diff --git a/scripts/gen-texturepack-files.mjs b/scripts/gen-texturepack-files.mjs deleted file mode 100644 index d996236e..00000000 --- a/scripts/gen-texturepack-files.mjs +++ /dev/null @@ -1,52 +0,0 @@ -//@ts-check -import fs from 'fs' -import minecraftAssets from 'minecraft-assets' - -// why store another data? -// 1. want to make it compatible (at least for now) -// 2. don't want to read generated blockStates as it might change in future, and the current way was faster to implement - -const blockNames = [] -const indexesPerVersion = {} -/** @type {Map} */ -const allBlocksMap = new Map() -const getBlockIndex = (block) => { - if (allBlocksMap.has(block)) { - return allBlocksMap.get(block) - } - - const index = blockNames.length - allBlocksMap.set(block, index) - blockNames.push(block) - return index -} - -// const blocksFull = [] -// const allBlocks = [] -// // we can even optimize it even futher by doing prev-step resolving -// const blocksDiff = {} - -for (const [i, version] of minecraftAssets.versions.reverse().entries()) { - const assets = minecraftAssets(version) - const blocksDir = assets.directory + '/blocks' - const blocks = fs.readdirSync(blocksDir) - indexesPerVersion[version] = blocks.map(block => { - if (!block.endsWith('.png')) return undefined - return getBlockIndex(block) - }).filter(i => i !== undefined) - - // if (!blocksFull.length) { - // // first iter - // blocksFull.push(...blocks) - // } else { - // const missing = blocksFull.map((b, i) => !blocks.includes(b) ? i : -1).filter(i => i !== -1) - // const added = blocks.filter(b => !blocksFull.includes(b)) - // blocksDiff[version] = { - // missing, - // added - // } - // } -} - -fs.mkdirSync('./generated', { recursive: true, }) -fs.writeFileSync('./generated/blocks.json', JSON.stringify({ blockNames: blockNames, indexes: indexesPerVersion })) diff --git a/scripts/genLargeDataAliases.ts b/scripts/genLargeDataAliases.ts new file mode 100644 index 00000000..2372dbfd --- /dev/null +++ b/scripts/genLargeDataAliases.ts @@ -0,0 +1,62 @@ +import * as fs from 'fs' + +export const genLargeDataAliases = async (isCompressed: boolean) => { + const modules = { + mcData: { + raw: '../generated/minecraft-data-optimized.json', + compressed: '../generated/mc-data-compressed.js', + }, + blockStatesModels: { + raw: 'mc-assets/dist/blockStatesModels.json', + compressed: '../generated/mc-assets-compressed.js', + } + } + + const OUT_FILE = './generated/large-data-aliases.ts' + + let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n` + for (const [module, { compressed, raw }] of Object.entries(modules)) { + const chunkName = module === 'mcData' ? 'mc-data' : 'mc-assets'; + let importCode = `(await import(/* webpackChunkName: "${chunkName}" */ '${isCompressed ? compressed : raw}')).default`; + if (isCompressed) { + importCode = `JSON.parse(decompressFromBase64(${importCode}))` + } + str += ` if (mod === '${module}') return ${importCode}\n` + } + str += `}\n` + + fs.writeFileSync(OUT_FILE, str, 'utf8') +} + +const decoderCode = /* ts */ ` +import pako from 'pako'; + +globalThis.pako = { inflate: pako.inflate.bind(pako) } + +function decompressFromBase64(input) { + console.time('decompressFromBase64') + // Decode the Base64 string + const binaryString = atob(input); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + // Convert the binary string to a byte array + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Decompress the byte array + const decompressedData = pako.inflate(bytes, { to: 'string' }); + + console.timeEnd('decompressFromBase64') + return decompressedData; +} +` + +// execute if run directly +if (require.main === module) { + console.log('running...') + const isCompressed = process.argv.includes('--compressed') + genLargeDataAliases(isCompressed) + console.log('done generating large data aliases') +} diff --git a/scripts/genMcDataTypes.ts b/scripts/genMcDataTypes.ts new file mode 100644 index 00000000..82b5b878 --- /dev/null +++ b/scripts/genMcDataTypes.ts @@ -0,0 +1,51 @@ +import minecraftData from 'minecraft-data' +import fs from 'fs' +import supportedVersions from '../src/supportedVersions.mjs' + +const data = minecraftData('1.20.1') + +let types = '' +types += `\nexport type BlockNames = ${Object.keys(data.blocksByName).map(blockName => `'${blockName}'`).join(' | ')};` +types += `\nexport type ItemNames = ${Object.keys(data.itemsByName).map(blockName => `'${blockName}'`).join(' | ')};` +types += `\nexport type EntityNames = ${Object.keys(data.entitiesByName).map(blockName => `'${blockName}'`).join(' | ')};` +types += `\nexport type BiomesNames = ${Object.keys(data.biomesByName).map(blockName => `'${blockName}'`).join(' | ')};` +types += `\nexport type EnchantmentNames = ${Object.keys(data.enchantmentsByName).map(blockName => `'${blockName}'`).join(' | ')};` + +type Version = string +const allVersionsEntitiesMetadata = {} as Record> +for (const version of supportedVersions) { + const data = minecraftData(version) + for (const { name, metadataKeys } of data.entitiesArray) { + allVersionsEntitiesMetadata[name] ??= {} + if (!metadataKeys) { + // console.warn('Entity has no metadata', name, version) + } + for (const [i, key] of (metadataKeys ?? []).entries()) { + allVersionsEntitiesMetadata[name][key] ??= { + version: version, + firstKey: i, + } + } + } +} + +types += '\n\nexport type EntityMetadataVersions = {\n' +for (const [name, versions] of Object.entries(allVersionsEntitiesMetadata)) { + types += `'${name}': {` + for (const [key, v] of Object.entries(versions)) { + types += `\n/** ${v.version}+ (${v.firstKey}) */\n` + types += `'${key}': string;` + } + types += '},' +} +types += '\n}' + +const minify = false +if (minify) { + types = types.replaceAll(/[\t]/g, '') +} + +fs.writeFileSync('./src/mcDataTypes.ts', types, 'utf8') diff --git a/scripts/genPixelartTypes.ts b/scripts/genPixelartTypes.ts new file mode 100644 index 00000000..e7c9649a --- /dev/null +++ b/scripts/genPixelartTypes.ts @@ -0,0 +1,16 @@ +import fs from 'fs' + +const icons = fs.readdirSync('node_modules/pixelarticons/svg') + +const addIconPath = '../../node_modules/pixelarticons/svg/' + +let str = 'export type PixelartIconsGenerated = {\n' +for (const icon of icons) { + const name = icon.replace('.svg', '') + // jsdoc + const jsdocImage = '![image](' + addIconPath + icon + ')' + str += ` /** ${jsdocImage} */\n` + str += ` '${name}': string;\n` +} +str += '}\n' +fs.writeFileSync('./src/react/pixelartIcons.generated.ts', str, 'utf8') diff --git a/scripts/genShims.ts b/scripts/genShims.ts new file mode 100644 index 00000000..2916044a --- /dev/null +++ b/scripts/genShims.ts @@ -0,0 +1,37 @@ +import fs from 'fs' +import { appReplacableResources } from '../src/resourcesSource' + +fs.mkdirSync('./generated', { recursive: true }) + +// app resources + +let headerImports = '' +let resourcesContent = 'export const appReplacableResources: { [key in Keys]: { content: any, resourcePackPath: string, cssVar?: string, cssVarRepeat?: number } } = {\n' +let resourcesContentOriginal = 'export const resourcesContentOriginal = {\n' +const keys = [] as string[] + +for (const resource of appReplacableResources) { + const { path, ...rest } = resource + const name = path.split('/').slice(-4).join('_').replace('.png', '').replaceAll('-', '_').replaceAll('.', '_') + keys.push(name) + headerImports += `import ${name} from '${path.replace('../node_modules/', '')}'\n` + + resourcesContent += ` + '${name}': { + content: ${name}, + resourcePackPath: 'minecraft/textures/${path.slice(path.indexOf('other-textures/') + 'other-textures/'.length).split('/').slice(1).join('/')}', + ...${JSON.stringify(rest)} + }, +` + resourcesContentOriginal += ` + '${name}': ${name}, +` +} + +resourcesContent += '}\n' +resourcesContent += `type Keys = ${keys.map(k => `'${k}'`).join(' | ')}\n` +resourcesContentOriginal += '}\n' +resourcesContent += resourcesContentOriginal + +fs.mkdirSync('./src/generated', { recursive: true }) +fs.writeFileSync('./src/generated/resources.ts', headerImports + '\n' + resourcesContent, 'utf8') diff --git a/scripts/getMissingRecipes.mjs b/scripts/getMissingRecipes.mjs new file mode 100644 index 00000000..59e78672 --- /dev/null +++ b/scripts/getMissingRecipes.mjs @@ -0,0 +1,41 @@ +//@ts-check +// tsx ./scripts/getMissingRecipes.mjs +import MinecraftData from 'minecraft-data' +import supportedVersions from '../src/supportedVersions.mjs' +import fs from 'fs' + +console.time('import-data') +const { descriptionGenerators } = await import('../src/itemsDescriptions') +console.timeEnd('import-data') + +const data = MinecraftData(supportedVersions.at(-1)) + +const hasDescription = name => { + for (const [key, value] of descriptionGenerators) { + if (Array.isArray(key) && key.includes(name)) { + return true + } + if (key instanceof RegExp && key.test(name)) { + return true + } + } + return false +} + +const result = [] +for (const item of data.itemsArray) { + const recipes = data.recipes[item.id] + if (!recipes) { + if (item.name.endsWith('_slab') || item.name.endsWith('_stairs') || item.name.endsWith('_wall')) { + console.warn('Must have recipe!', item.name) + continue + } + if (hasDescription(item.name)) { + continue + } + + result.push(item.name) + } +} + +fs.writeFileSync('./generated/noRecipies.json', JSON.stringify(result, null, 2)) diff --git a/scripts/getMoreCollisionShapes.mjs b/scripts/getMoreCollisionShapes.mjs deleted file mode 100644 index 3f772af1..00000000 --- a/scripts/getMoreCollisionShapes.mjs +++ /dev/null @@ -1,206 +0,0 @@ -import minecraftData from 'minecraft-data' -import minecraftAssets from 'minecraft-assets' -import fs from 'fs' - -const latestData = minecraftData('1.20.2') - -// dont touch, these are the ones that are already full box -const fullBoxInteractionShapes = [ - 'dead_bush', - 'cave_vines_plant', - 'grass', - 'tall_seagrass', - 'spruce_sapling', - 'oak_sapling', - 'dark_oak_sapling', - 'birch_sapling', - 'seagrass', - 'nether_portal', - 'tall_grass', - 'lilac', - 'cobweb' -] - -const ignoreStates = [ - 'mangrove_propagule', - 'moving_piston' -] - -// const - -// to fix -const fullBoxInteractionShapesTemp = [ - 'moving_piston', - 'lime_wall_banner', - 'gray_wall_banner', - 'weeping_vines_plant', - 'pumpkin_stem', - 'red_wall_banner', - 'crimson_wall_sign', - 'magenta_wall_banner', - 'melon_stem', - 'gray_banner', - 'spruce_sign', - 'pink_wall_banner', - 'purple_banner', - 'bamboo_sapling', - 'mangrove_sign', - 'cyan_banner', - 'blue_banner', - 'green_wall_banner', - 'yellow_banner', - 'black_wall_banner', - 'green_banner', - 'oak_sign', - 'jungle_sign', - 'yellow_wall_banner', - 'lime_banner', - 'tube_coral', - 'red_banner', - 'magenta_banner', - 'brown_wall_banner', - 'white_wall_banner', -] - -const shapes = latestData.blockCollisionShapes; -const fullShape = shapes.shapes[1] -const outputJson = {} - -let interestedBlocksNoStates = [] -let interestedBlocksStates = [] - -const stateIgnoreStates = ['waterlogged'] - -const isNonInteractive = block => block.name.includes('air') || block.name.includes('water') || block.name.includes('lava') || block.name.includes('void') -const interestedBlocks = latestData.blocksArray.filter(block => { - const shapeId = shapes.blocks[block.name] - // console.log('shapeId', shapeId, block.name) - if (!shapeId) return true - const shape = typeof shapeId === 'number' ? shapes.shapes[shapeId] : shapeId - if (shape.length === 0) return true - // console.log(shape) -}).filter(b => !isNonInteractive(b)).filter(b => { - if (fullBoxInteractionShapes.includes(b.name)) { - outputJson[b.name] = fullShape - return false - } - - if (!b.states?.length || ignoreStates.includes(b.name) || b.states.every(s => stateIgnoreStates.every(state => s.name === state))) { - interestedBlocksNoStates.push(b.name) - return false - } else { - interestedBlocksStates.push(b.name) - return false - } -}).map(d => d.name) - -const { blocksStates, blocksModels } = minecraftAssets(latestData.version.minecraftVersion) - -const getShapeFromModel = (block,) => { - const blockStates = JSON.parse(fs.readFileSync('./prismarine-viewer/public/blocksStates/1.19.1.json')) - const blockState = blockStates[block]; - const perVariant = {} - for (const [key, variant] of Object.entries(blockState.variants)) { - // const shapes = (Array.isArray(variant) ? variant : [variant]).flatMap((v) => v.model?.elements).filter(Boolean).map(({ from, to }) => [...from, ...to]).reduce((acc, cur) => { - // return [ - // Math.min(acc[0], cur[0]), - // Math.min(acc[1], cur[1]), - // Math.min(acc[2], cur[2]), - // Math.max(acc[3], cur[3]), - // Math.max(acc[4], cur[4]), - // Math.max(acc[5], cur[5]) - // ] - // }) - console.log(variant) - const shapes = (Array.isArray(variant) ? variant : [variant]).flatMap((v) => v.model?.elements).filter(Boolean).map(({ from, to }) => [...from, ...to]) - perVariant[key] = shapes - break - } - return perVariant -} - -// console.log(getShapeFromModel('oak_button')) - -// const addShapeIf = { -// redstone: [ -// ['east', 'up', shape] -// ] -// } - -const needBlocksStated = {} - -const groupedBlocksRules = { - // button: block => block.includes('button'), - // pressure_plate: block => block.includes('pressure_plate'), - // sign: block => block.includes('_sign'), - // sapling: block => block.includes('_sapling'), -} -const groupedBlocksOutput = {} - -outer: for (const interestedBlock of [...interestedBlocksNoStates, ...interestedBlocksStates]) { - for (const [block, func] of Object.entries(groupedBlocksRules)) { - if (func(interestedBlock)) { - groupedBlocksOutput[block] ??= [] - groupedBlocksOutput[block].push(interestedBlock) - continue outer - } - } - - const hasStates = interestedBlocksStates.includes(interestedBlock); - if (hasStates) { - const states = blocksStates[interestedBlock] - if (!states) { - console.log('no states', interestedBlock) - continue - } - if (!states.variants) { - if (!states.multipart) { - console.log('no variants', interestedBlock) - continue - } - let outputStates = {} - for (const {when} of states.multipart) { - if (when) { - for (const [key, value] of Object.entries(when)) { - if (key === 'OR') { - for (const or of value) { - for (const [key, value] of Object.entries(or)) { - const str = `${key}=${value}` - outputStates[str] = true - } - } - continue - } - const str = `${key}=${value}` - outputStates[str] = true - } - } - } - needBlocksStated[interestedBlock] = outputStates - continue - } - if (Object.keys(states.variants).length === 1 && states.variants['']) { - needBlocksStated[interestedBlock] = false - } else { - needBlocksStated[interestedBlock] = Object.fromEntries(Object.entries(states.variants).map(([key, value]) => [key, true])) - } - } else { - needBlocksStated[interestedBlock] = false - } - // let vars = [] - // Object.keys(variants).forEach(variant => { - // if (variant !== '') vars.push(variant) - // }) - // needBlocksVariants.push({ - // block: interestedBlock, - // variants: vars - // }) -} - -fs.writeFileSync('scripts/needBlocks.json', JSON.stringify(needBlocksStated)) - -// console.log(interestedBlocks.includes('lever')) - -// read latest block states - -// read block model elements & combine diff --git a/scripts/githubActions.mjs b/scripts/githubActions.mjs new file mode 100644 index 00000000..3e8eb0f6 --- /dev/null +++ b/scripts/githubActions.mjs @@ -0,0 +1,43 @@ +//@ts-check +import fs from 'fs' +import os from 'os' + +const fns = { + async getAlias () { + const aliasesRaw = process.env.ALIASES + if (!aliasesRaw) throw new Error('No aliases found') + const aliases = aliasesRaw.split('\n').map((x) => x.trim().split('=')) + const githubActionsPull = process.env.PULL_URL?.split('/').at(-1) + if (!githubActionsPull) throw new Error(`Not a pull request, got ${process.env.PULL_URL}`) + const prNumber = githubActionsPull + const alias = aliases.find((x) => x[0] === prNumber) + if (alias) { + // set github output + setOutput('alias', alias[1]) + } + }, + getReleasingAlias() { + const final = (ver) => `${ver}.mcraft.fun,${ver}.pcm.gg` + const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8')) + const tag = releaseJson.latestTag + const [major, minor, patch] = tag.replace('v', '').split('.') + if (major === '0' && minor === '1') { + setOutput('alias', final(`v${patch}`)) + } else { + setOutput('alias', final(tag)) + } + } +} + +function setOutput (key, value) { + // Temporary hack until core actions library catches up with github new recommendations + const output = process.env['GITHUB_OUTPUT'] + fs.appendFileSync(output, `${key}=${value}${os.EOL}`) +} + +const fn = fns[process.argv[2]] +if (fn) { + fn() +} else { + console.error('Function not found') +} diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs new file mode 100644 index 00000000..a572d067 --- /dev/null +++ b/scripts/makeOptimizedMcData.mjs @@ -0,0 +1,382 @@ +//@ts-check +import { build } from 'esbuild' +import { existsSync } from 'node:fs' +import Module from "node:module" +import { dirname } from 'node:path' +import supportedVersions from '../src/supportedVersions.mjs' +import { gzipSizeFromFileSync } from 'gzip-size' +import fs from 'fs' +import { default as _JsonOptimizer } from '../src/optimizeJson' +import { gzipSync } from 'zlib' +import MinecraftData from 'minecraft-data' +import MCProtocol from 'minecraft-protocol' + +/** @type {typeof _JsonOptimizer} */ +//@ts-ignore +const JsonOptimizer = _JsonOptimizer.default + +// console.log(a.diff_main(JSON.stringify({ a: 1 }), JSON.stringify({ a: 1, b: 2 }))) + +const require = Module.createRequire(import.meta.url) + +const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json') + +function toMajor(version) { + const [a, b] = (version + '').split('.') + return `${a}.${b}` +} + +let versions = {} +const dataTypes = new Set() + +for (const [version, dataSet] of Object.entries(dataPaths.pc)) { + if (!supportedVersions.includes(version)) continue + for (const type of Object.keys(dataSet)) { + dataTypes.add(type) + } + versions[version] = dataSet +} + +const versionToNumber = (ver) => { + const [x, y = '0', z = '0'] = ver.split('.') + return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` +} + +// Version clipping support +const minVersion = process.env.MIN_MC_VERSION +const maxVersion = process.env.MAX_MC_VERSION + +// Filter versions based on MIN_VERSION and MAX_VERSION if provided +if (minVersion || maxVersion) { + const filteredVersions = {} + const minVersionNum = minVersion ? versionToNumber(minVersion) : 0 + const maxVersionNum = maxVersion ? versionToNumber(maxVersion) : Infinity + + for (const [version, dataSet] of Object.entries(versions)) { + const versionNum = versionToNumber(version) + if (versionNum >= minVersionNum && versionNum <= maxVersionNum) { + filteredVersions[version] = dataSet + } + } + + versions = filteredVersions + + console.log(`Version clipping applied: ${minVersion || 'none'} to ${maxVersion || 'none'}`) + console.log(`Processing ${Object.keys(versions).length} versions:`, Object.keys(versions).sort((a, b) => versionToNumber(a) - versionToNumber(b))) +} + +console.log('Bundling version range:', Object.keys(versions)[0], 'to', Object.keys(versions).at(-1)) + +// if not included here (even as {}) will not be bundled & accessible! +// const compressedOutput = !!process.env.SINGLE_FILE_BUILD +const compressedOutput = true +const dataTypeBundling2 = { + blocks: { + arrKey: 'name', + }, + items: { + arrKey: 'name', + }, + recipes: { + processData: processRecipes + } +} +const dataTypeBundling = { + language: process.env.SKIP_MC_DATA_LANGUAGE === 'true' ? { + raw: {} + } : { + ignoreRemoved: true, + ignoreChanges: true + }, + blocks: { + arrKey: 'name', + processData(current, prev, _, version) { + for (const block of current) { + const prevBlock = prev?.find(x => x.name === block.name) + if (block.transparent) { + const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name) + + if (forceOpaque || (prevBlock && !prevBlock.transparent)) { + block.transparent = false + } + } + if (block.hardness === 0 && prevBlock && prevBlock.hardness > 0) { + block.hardness = prevBlock.hardness + } + } + } + // ignoreRemoved: true, + // genChanges (source, diff) { + // const diffs = {} + // const newItems = {} + // for (const [key, val] of Object.entries(diff)) { + // const src = source[key] + // if (!src) { + // newItems[key] = val + // continue + // } + // const { minStateId, defaultState, maxStateId } = val + // if (defaultState === undefined || minStateId === src.minStateId || maxStateId === src.maxStateId || defaultState === src.defaultState) continue + // diffs[key] = [minStateId, defaultState, maxStateId] + // } + // return { + // stateChanges: diffs + // } + // }, + // ignoreChanges: true + }, + items: { + arrKey: 'name' + }, + attributes: { + arrKey: 'name' + }, + particles: { + arrKey: 'name' + }, + effects: { + arrKey: 'name' + }, + enchantments: { + arrKey: 'name' + }, + instruments: { + arrKey: 'name' + }, + foods: { + arrKey: 'name' + }, + entities: { + arrKey: 'id+type' + }, + materials: {}, + windows: { + arrKey: 'name' + }, + version: { + raw: true + }, + tints: {}, + biomes: { + arrKey: 'name' + }, + entityLoot: { + arrKey: 'entity' + }, + blockLoot: { + arrKey: 'block' + }, + recipes: process.env.SKIP_MC_DATA_RECIPES === 'true' ? { + raw: {} + } : { + raw: true + // processData: processRecipes + }, + blockCollisionShapes: {}, + loginPacket: {}, + protocol: { + raw: true + }, + // sounds: { + // arrKey: 'name' + // } +} + +function processRecipes(current, prev, getData, version) { + // can require the same multiple times per different versions + if (current._proccessed) return + const items = getData('items') + const blocks = getData('blocks') + const itemsIdsMap = Object.fromEntries(items.map((b) => [b.id, b.name])) + const blocksIdsMap = Object.fromEntries(blocks.map((b) => [b.id, b.name])) + for (const key of Object.keys(current)) { + const mapId = (id) => { + if (typeof id !== 'string' && typeof id !== 'number') throw new Error('Incorrect type') + const mapped = itemsIdsMap[id] ?? blocksIdsMap[id] + if (!mapped) { + throw new Error(`No item/block name with id ${id}`) + } + return mapped + } + const processRecipe = (obj) => { + // if (!obj) return + // if (Array.isArray(obj)) { + // obj.forEach((id, i) => { + // obj[i] = mapId(obj[id]) + // }) + // } else if (obj && typeof obj === 'object') { + // if (!'count metadata id'.split(' ').every(x => x in obj)) { + // throw new Error(`process error: Unknown deep object pattern: ${JSON.stringify(obj)}`) + // } + // obj.id = mapId(obj.id) + // } else { + // throw new Error('unknown type') + // } + const parseRecipeItem = (item) => { + if (typeof item === 'number') return mapId(item) + if (Array.isArray(item)) return [mapId(item), ...item.slice(1)] + if (!item) { + return item + } + if ('id' in item) { + item.id = mapId(item.id) + return item + } + throw new Error('unhandled') + } + const maybeProccessShape = (shape) => { + if (!shape) return + for (const shapeRow of shape) { + for (const [i, item] of shapeRow.entries()) { + shapeRow[i] = parseRecipeItem(item) + } + } + } + if (obj.result) obj.result = parseRecipeItem(obj.result) + maybeProccessShape(obj.inShape) + maybeProccessShape(obj.outShape) + if (obj.ingredients) { + for (const [i, ingredient] of obj.ingredients.entries()) { + obj.ingredients[i] = parseRecipeItem(ingredient) + } + } + } + try { + const name = mapId(key) + for (const [i, recipe] of current[key].entries()) { + try { + processRecipe(recipe) + } catch (err) { + console.warn(`${version} [warn] Removing incorrect recipe: ${err}`) + delete current[i] + } + } + current[name] = current[key] + } catch (err) { + console.warn(`${version} [warn] Removing incorrect recipe: ${err}`) + } + delete current[key] + } + current._proccessed = true +} + +const notBundling = [...dataTypes.keys()].filter(x => !Object.keys(dataTypeBundling).includes(x)) +console.log("Not bundling minecraft-data data:", notBundling) + +let previousData = {} +// /** @type {Record} */ +const diffSources = {} +const versionsArr = Object.entries(versions) +const sizePerDataType = {} +const rawDataVersions = {} +// const versionsArr = Object.entries(versions).slice(-1) +for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) { + for (const [dataType, dataPath] of Object.entries(dataSet)) { + const config = dataTypeBundling[dataType] + if (!config) continue + const ignoreCollisionShapes = dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13') + + let injectCode = '' + const getRealData = (type) => { + const loc = `minecraft-data/data/${dataSet[type]}/` + const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`) + // const data = fs.readFileSync(dataPathAbsolute, 'utf8') + const dataRaw = require(dataPathAbsolute) + return dataRaw + } + const dataRaw = getRealData(dataType) + let rawData = dataRaw + if (config.raw) { + rawDataVersions[dataType] ??= {} + rawDataVersions[dataType][version] = rawData + if (config.raw === true) { + rawData = dataRaw + } else { + rawData = config.raw + } + + if (ignoreCollisionShapes && dataType === 'blockCollisionShapes') { + rawData = { + blocks: {}, + shapes: {} + } + } + } else { + if (!diffSources[dataType]) { + diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved) + } + try { + config.processData?.(dataRaw, previousData[dataType], getRealData, version) + diffSources[dataType].recordDiff(version, dataRaw) + injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})` + } catch (err) { + const error = new Error(`Failed to diff ${dataType} for ${version}: ${err.message}`) + error.stack = err.stack + throw error + } + } + sizePerDataType[dataType] ??= 0 + sizePerDataType[dataType] += Buffer.byteLength(JSON.stringify(injectCode || rawData), 'utf8') + if (config.genChanges && previousData[dataType]) { + const changes = config.genChanges(previousData[dataType], dataRaw) + // Object.assign(data, changes) + } + previousData[dataType] = dataRaw + } +} +const sources = Object.fromEntries(Object.entries(diffSources).map(x => { + const data = x[1].export() + // const data = {} + sizePerDataType[x[0]] += Buffer.byteLength(JSON.stringify(data), 'utf8') + return [x[0], data] +})) +Object.assign(sources, rawDataVersions) +sources.versionKey = require('minecraft-data/package.json').version + +const totalSize = Object.values(sizePerDataType).reduce((acc, val) => acc + val, 0) +console.log('total size (mb)', totalSize / 1024 / 1024) +console.log( + 'size per data type (mb, %)', + Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => { + return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]] + }).sort((a, b) => { + //@ts-ignore + return b[1][1] - a[1][1] + })) +) + +function compressToBase64(input) { + const buffer = gzipSync(input) + return buffer.toString('base64') +} + +const filePath = './generated/minecraft-data-optimized.json' +fs.writeFileSync(filePath, JSON.stringify(sources), 'utf8') +if (compressedOutput) { + const minizedCompressed = compressToBase64(fs.readFileSync(filePath)) + console.log('size of compressed', Buffer.byteLength(minizedCompressed, 'utf8') / 1000 / 1000) + const compressedFilePath = './generated/mc-data-compressed.js' + fs.writeFileSync(compressedFilePath, `export default ${JSON.stringify(minizedCompressed)}`, 'utf8') + + const mcAssets = JSON.stringify(require('mc-assets/dist/blockStatesModels.json')) + fs.writeFileSync('./generated/mc-assets-compressed.js', `export default ${JSON.stringify(compressToBase64(mcAssets))}`, 'utf8') + + // const modelsObj = fs.readFileSync('./prismarine-renderer/viewer/lib/entity/exportedModels.js') + // const models = +} + +console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileSync(filePath) / 1000 / 1000) + +// always bundled + +const { defaultVersion } = MCProtocol +const data = MinecraftData(defaultVersion) +console.log('defaultVersion', defaultVersion, !!data) +const initialMcData = { + [defaultVersion]: { + version: data.version, + protocol: data.protocol, + } +} + +// fs.writeFileSync('./generated/minecraft-initial-data.json', JSON.stringify(initialMcData), 'utf8') diff --git a/scripts/optimizeBlockCollisions.ts b/scripts/optimizeBlockCollisions.ts index 8a87b358..251a564a 100644 --- a/scripts/optimizeBlockCollisions.ts +++ b/scripts/optimizeBlockCollisions.ts @@ -48,7 +48,9 @@ for (const version of [...supportedVersions].reverse()) { console.log('using blockCollisionShapes of version', version) const data = JSON.parse(fs.readFileSync(dataPath, 'utf8')) data.version = version + data.versionKey = require('minecraft-data/package.json').version processData(data) + fs.mkdirSync('./generated', { recursive: true }) fs.writeFileSync('./generated/latestBlockCollisionsShapes.json', JSON.stringify(data), 'utf8') break } diff --git a/scripts/outdatedGitPackages.mjs b/scripts/outdatedGitPackages.mjs new file mode 100644 index 00000000..c6a3b2d5 --- /dev/null +++ b/scripts/outdatedGitPackages.mjs @@ -0,0 +1,52 @@ +// pnpm bug workaround +import fs from 'fs' +import { parse } from 'yaml' +import _ from 'lodash' + +const lockfile = parse(fs.readFileSync('./pnpm-lock.yaml', 'utf8')) +const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')) +const depsKeys = ['dependencies', 'devDependencies'] + +const githubToken = process.env.GITHUB_TOKEN +const ignoreDeps = packageJson.pnpm?.updateConfig?.ignoreDependencies ?? [] + +const outdatedDeps = [] + +const allDepsObj = {} + +for (const [key, val] of Object.entries(lockfile.importers)) { + // Object.assign(allDepsObj, val) + _.merge(allDepsObj, val) +} + +for (const [depsKey, deps] of Object.entries(allDepsObj)) { + for (const [depName, { specifier, version }] of Object.entries(deps)) { + if (ignoreDeps.includes(depName)) continue + if (!specifier.startsWith('github:')) continue + // console.log('checking github:', depName, version, specifier) + + let possiblyBranch = specifier.match(/#(.*)$/)?.[1] ?? '' + if (possiblyBranch) possiblyBranch = `/${possiblyBranch}` + const sha = version.split('/').slice(3).join('/').replace(/\(.+/, '') + const repo = version.split('/').slice(1, 3).join('/') + + const lastCommitJson = await fetch(`https://api.github.com/repos/${repo}/commits${possiblyBranch}?per_page=1`, { + headers: { + Authorization: githubToken ? `token ${githubToken}` : undefined, + }, + }).then(res => res.json()) + + const lastCommitActual = lastCommitJson ?? lastCommitJson[0] + const lastCommitActualSha = Array.isArray(lastCommitActual) ? lastCommitActual[0]?.sha : lastCommitActual?.sha + if (lastCommitActualSha === undefined) debugger + if (sha !== lastCommitActualSha) { + // console.log(`Outdated ${depName} github.com/${repo} : ${sha} -> ${lastCommitActualSha} (${lastCommitActual.commit.message})`) + outdatedDeps.push({ depName, repo, sha, lastCommitActualSha }) + } + } + +} + +if (outdatedDeps.length) { + throw new Error(`Outdated dependencies found: \n${outdatedDeps.map(({ depName, repo, sha, lastCommitActualSha }) => `${depName} github.com/${repo} : ${sha} -> ${lastCommitActualSha}`).join('\n')}`) +} diff --git a/scripts/patchAssets.ts b/scripts/patchAssets.ts new file mode 100644 index 00000000..99994f5f --- /dev/null +++ b/scripts/patchAssets.ts @@ -0,0 +1,137 @@ +import blocksAtlas from 'mc-assets/dist/blocksAtlases.json' +import itemsAtlas from 'mc-assets/dist/itemsAtlases.json' +import * as fs from 'fs' +import * as path from 'path' +import sharp from 'sharp' + +interface AtlasFile { + latest: { + suSv: number + tileSize: number + width: number + height: number + textures: { + [key: string]: { + u: number + v: number + su: number + sv: number + tileIndex: number + } + } + } +} + +async function patchTextureAtlas( + atlasType: 'blocks' | 'items', + atlasData: AtlasFile, + customTexturesDir: string, + distDir: string +) { + // Check if custom textures directory exists and has files + if (!fs.existsSync(customTexturesDir) || fs.readdirSync(customTexturesDir).length === 0) { + return + } + + // Find the latest atlas file + const atlasFiles = fs.readdirSync(distDir) + .filter(file => file.startsWith(`${atlasType}AtlasLatest`) && file.endsWith('.png')) + .sort() + + if (atlasFiles.length === 0) { + console.log(`No ${atlasType}AtlasLatest.png found in ${distDir}`) + return + } + + const latestAtlasFile = atlasFiles[atlasFiles.length - 1] + const atlasPath = path.join(distDir, latestAtlasFile) + console.log(`Patching ${atlasPath}`) + + // Get atlas dimensions + const atlasMetadata = await sharp(atlasPath).metadata() + if (!atlasMetadata.width || !atlasMetadata.height) { + throw new Error(`Failed to get atlas dimensions for ${atlasPath}`) + } + + // Process each custom texture + const customTextureFiles = fs.readdirSync(customTexturesDir) + .filter(file => file.endsWith('.png')) + + if (customTextureFiles.length === 0) return + + // Prepare composite operations + const composites: sharp.OverlayOptions[] = [] + + for (const textureFile of customTextureFiles) { + const textureName = path.basename(textureFile, '.png') + + if (atlasData.latest.textures[textureName]) { + const textureData = atlasData.latest.textures[textureName] + const customTexturePath = path.join(customTexturesDir, textureFile) + + try { + // Convert UV coordinates to pixel coordinates + const x = Math.round(textureData.u * atlasMetadata.width) + const y = Math.round(textureData.v * atlasMetadata.height) + const width = Math.round((textureData.su ?? atlasData.latest.suSv) * atlasMetadata.width) + const height = Math.round((textureData.sv ?? atlasData.latest.suSv) * atlasMetadata.height) + + // Resize custom texture to match atlas dimensions and add to composite operations + const resizedTextureBuffer = await sharp(customTexturePath) + .resize(width, height, { + fit: 'fill', + kernel: 'nearest' // Preserve pixel art quality + }) + .png() + .toBuffer() + + composites.push({ + input: resizedTextureBuffer, + left: x, + top: y, + blend: 'over' + }) + + console.log(`Prepared ${textureName} at (${x}, ${y}) with size (${width}, ${height})`) + } catch (error) { + console.error(`Failed to prepare ${textureName}:`, error) + } + } else { + console.warn(`Texture ${textureName} not found in ${atlasType} atlas`) + } + } + + if (composites.length > 0) { + // Apply all patches at once using Sharp's composite + await sharp(atlasPath) + .composite(composites) + .png() + .toFile(atlasPath + '.tmp') + + // Replace original with patched version + fs.renameSync(atlasPath + '.tmp', atlasPath) + console.log(`Saved patched ${atlasType} atlas to ${atlasPath}`) + } +} + +async function main() { + const customBlocksDir = './assets/customTextures/blocks' + const customItemsDir = './assets/customTextures/items' + const distDir = './dist/static/image' + + try { + // Patch blocks atlas + await patchTextureAtlas('blocks', blocksAtlas as unknown as AtlasFile, customBlocksDir, distDir) + + // Patch items atlas + await patchTextureAtlas('items', itemsAtlas as unknown as AtlasFile, customItemsDir, distDir) + + console.log('Texture atlas patching completed!') + } catch (error) { + console.error('Failed to patch texture atlases:', error) + process.exit(1) + } +} + +// Run the script +main() diff --git a/scripts/prepareData.mjs b/scripts/prepareData.mjs deleted file mode 100644 index ab92499e..00000000 --- a/scripts/prepareData.mjs +++ /dev/null @@ -1,72 +0,0 @@ -//@ts-check -import { build } from 'esbuild' -import { existsSync } from 'node:fs' -import Module from "node:module" -import { dirname } from 'node:path' -import supportedVersions from '../src/supportedVersions.mjs' - -if (existsSync('dist/mc-data') && !process.argv.includes('-f')) { - console.log('using cached prepared data') - process.exit(0) -} - -const require = Module.createRequire(import.meta.url) - -const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json') - -function toMajor (version) { - const [a, b] = (version + '').split('.') - return `${a}.${b}` -} - -const grouped = {} - -for (const [version, data] of Object.entries(dataPaths.pc)) { - if (!supportedVersions.includes(version)) continue - const major = toMajor(version) - grouped[major] ??= {} - grouped[major][version] = data -} - -const versionToNumber = (ver) => { - const [x, y = '0', z = '0'] = ver.split('.') - return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` -} - -console.log('preparing data') -console.time('data prepared') -let builds = [] -for (const [major, versions] of Object.entries(grouped)) { - // if (major !== '1.19') continue - let contents = 'Object.assign(window.mcData, {\n' - for (const [version, dataSet] of Object.entries(versions)) { - contents += ` '${version}': {\n` - for (const [dataType, dataPath] of Object.entries(dataSet)) { - if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) { - contents += ` get ${dataType} () { return window.globalGetCollisionShapes?.("${version}") },\n` - continue - } - const loc = `minecraft-data/data/${dataPath}/` - contents += ` get ${dataType} () { return require("./${loc}${dataType}.json") },\n` - } - contents += ' },\n' - } - contents += '})' - - const promise = build({ - bundle: true, - outfile: `dist/mc-data/${major}.js`, - stdin: { - contents, - - resolveDir: dirname(require.resolve('minecraft-data')), - sourcefile: `mcData${major}.js`, - loader: 'js', - }, - metafile: true, - }) - // require('fs').writeFileSync('dist/mc-data/metafile.json', JSON.stringify(promise.metafile), 'utf8') - builds.push(promise) -} -await Promise.all(builds) -console.timeEnd('data prepared') diff --git a/scripts/prepareSounds.mjs b/scripts/prepareSounds.mjs index 4ed119cb..02026a04 100644 --- a/scripts/prepareSounds.mjs +++ b/scripts/prepareSounds.mjs @@ -7,29 +7,40 @@ import { fileURLToPath } from 'url' import { exec } from 'child_process' import { promisify } from 'util' import { build } from 'esbuild' +import supportedVersions from '../src/supportedVersions.mjs' const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url))) -const targetedVersions = ['1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9'] +export const versionToNumber = (ver) => { + const [x, y = '0', z = '0'] = ver.split('.') + return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` +} + +const targetedVersions = [...supportedVersions].sort((a, b) => versionToNumber(b) - versionToNumber(a)) /** @type {{name, size, hash}[]} */ let prevSounds = null const burgerDataUrl = (version) => `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/${version}.json` const burgerDataPath = './generated/burger.json' +const EXISTING_CACHE_PATH = './generated/existing-sounds-cache.json' // const perVersionData: Record { +const downloadAllSoundsAndCreateMap = async () => { + let existingSoundsCache = {} + try { + existingSoundsCache = JSON.parse(await fs.promises.readFile(EXISTING_CACHE_PATH, 'utf8')) + } catch (err) {} const { versions } = await getVersionList() const lastVersion = versions.filter(version => !version.id.includes('w'))[0] // if (lastVersion.id !== targetedVersions[0]) throw new Error('last version is not the same as targetedVersions[0], update') - for (const targetedVersion of targetedVersions) { - const versionData = versions.find(x => x.id === targetedVersion) - if (!versionData) throw new Error('no version data for ' + targetedVersion) - console.log('Getting assets for version', targetedVersion) + for (const version of targetedVersions) { + const versionData = versions.find(x => x.id === version) + if (!versionData) throw new Error('no version data for ' + version) + console.log('Getting assets for version', version) const { assetIndex } = await fetch(versionData.url).then((r) => r.json()) /** @type {{objects: {[a: string]: { size, hash }}}} */ const index = await fetch(assetIndex.url).then((r) => r.json()) @@ -45,26 +56,30 @@ const downloadAllSounds = async () => { const changedSize = soundAssets.filter(x => prevSoundNames.has(x.name) && prevSounds.find(y => y.name === x.name).size !== x.size) console.log('changed size', changedSize.map(x => ({ name: x.name, prev: prevSounds.find(y => y.name === x.name).size, curr: x.size }))) if (addedSounds.length || changedSize.length) { - soundsPathVersionsRemap[targetedVersion] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', '')) + soundsPathVersionsRemap[version] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', '')) } if (addedSounds.length) { - console.log('downloading new sounds for version', targetedVersion) - downloadSounds(addedSounds, targetedVersion + '/') + console.log('downloading new sounds for version', version) + downloadSounds(version, addedSounds, version + '/') } if (changedSize.length) { - console.log('downloading changed sounds for version', targetedVersion) - downloadSounds(changedSize, targetedVersion + '/') + console.log('downloading changed sounds for version', version) + downloadSounds(version, changedSize, version + '/') } } else { - console.log('downloading sounds for version', targetedVersion) - downloadSounds(soundAssets) + console.log('downloading sounds for version', version) + downloadSounds(version, soundAssets) } prevSounds = soundAssets } - async function downloadSound ({ name, hash, size }, namePath, log) { + async function downloadSound({ name, hash, size }, namePath, log) { + const cached = + !!namePath.replace('.ogg', '.mp3').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds) || + !!namePath.replace('.ogg', '.ogg').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds) const savePath = path.resolve(`generated/sounds/${namePath}`) - if (fs.existsSync(savePath)) { + if (cached || fs.existsSync(savePath)) { // console.log('skipped', name) + existingSoundsCache.sounds[namePath] = true return } log() @@ -86,7 +101,12 @@ const downloadAllSounds = async () => { } writer.close() } - async function downloadSounds (assets, addPath = '') { + async function downloadSounds(version, assets, addPath = '') { + if (addPath && existingSoundsCache.sounds[version]) { + console.log('using existing sounds for version', version) + return + } + console.log(version, 'have to download', assets.length, 'sounds') for (let i = 0; i < assets.length; i += 5) { await Promise.all(assets.slice(i, i + 5).map((asset, j) => downloadSound(asset, `${addPath}${asset.name}`, () => { console.log('downloading', addPath, asset.name, i + j, '/', assets.length) @@ -95,6 +115,7 @@ const downloadAllSounds = async () => { } fs.writeFileSync('./generated/soundsPathVersionsRemap.json', JSON.stringify(soundsPathVersionsRemap), 'utf8') + fs.writeFileSync(EXISTING_CACHE_PATH, JSON.stringify(existingSoundsCache), 'utf8') } const lightpackOverrideSounds = { @@ -106,7 +127,8 @@ const lightpackOverrideSounds = { // this is not done yet, will be used to select only sounds for bundle (most important ones) const isSoundWhitelisted = (name) => name.startsWith('random/') || name.startsWith('note/') || name.endsWith('/say1') || name.endsWith('/death') || (name.startsWith('mob/') && name.endsWith('/step1')) || name.endsWith('/swoop1') || /* name.endsWith('/break1') || */ name.endsWith('dig/stone1') -const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // will be ffmpeg-static +// const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // can be ffmpeg-static +const ffmpegExec = 'ffmpeg' const maintainBitrate = true const scanFilesDeep = async (root, onOggFile) => { @@ -127,15 +149,16 @@ const convertSounds = async () => { }) const convertSound = async (i) => { - const proc = promisify(exec)(`${ffmpeg} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`) + const proc = promisify(exec)(`${ffmpegExec} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`) // pipe stdout to the console + //@ts-ignore proc.child.stdout.pipe(process.stdout) await proc console.log('converted to mp3', i, '/', toConvert.length, toConvert[i]) } const CONCURRENCY = 5 - for(let i = 0; i < toConvert.length; i += CONCURRENCY) { + for (let i = 0; i < toConvert.length; i += CONCURRENCY) { await Promise.all(toConvert.slice(i, i + CONCURRENCY).map((oggPath, j) => convertSound(i + j))) } } @@ -147,17 +170,44 @@ const getSoundsMap = (burgerData) => { } const writeSoundsMap = async () => { - // const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json()) - // fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8') + const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json()) + fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8') const allSoundsMapOutput = {} let prevMap // todo REMAP ONLY IDS. Do diffs, as mostly only ids are changed between versions // const localTargetedVersions = targetedVersions.slice(0, 2) + let lastMappingsJson const localTargetedVersions = targetedVersions - for (const targetedVersion of localTargetedVersions) { - const burgerData = await fetch(burgerDataUrl(targetedVersion)).then((r) => r.json()) + for (const targetedVersion of [...localTargetedVersions].reverse()) { + console.log('Processing version', targetedVersion) + + const burgerData = await fetch(burgerDataUrl(targetedVersion)).then((r) => r.json()).catch((err) => { + // console.error('error fetching burger data', targetedVersion, err) + return null + }) + /** @type {{sounds: string[]}} */ + const mappingJson = await fetch(`https://raw.githubusercontent.com/ViaVersion/Mappings/7a45c1f9dbc1f1fdadacfecdb205ba84e55766fc/mappings/mapping-${targetedVersion}.json`).then(async (r) => { + return r.json() + // lastMappingsJson = r.status === 404 ? lastMappingsJson : (await r.json()) + // if (r.status === 404) { + // console.warn('using prev mappings json for ' + targetedVersion) + // } + // return lastMappingsJson + }).catch((err) => { + // console.error('error fetching mapping json', targetedVersion, err) + return null + }) + // if (!mappingJson) throw new Error('no initial mapping json for ' + targetedVersion) + if (burgerData && !mappingJson) { + console.warn('has burger but no mapping json for ' + targetedVersion) + continue + } + if (!mappingJson || !burgerData) { + console.warn('no mapping json or burger data for ' + targetedVersion) + continue + } const allSoundsMap = getSoundsMap(burgerData) // console.log(Object.keys(sounds).length, 'ids') const outputIdMap = {} @@ -168,22 +218,33 @@ const writeSoundsMap = async () => { new: 0, same: 0 } - for (const { id, subtitle, sounds, name } of Object.values(allSoundsMap)) { + for (const { _id, subtitle, sounds, name } of Object.values(allSoundsMap)) { if (!sounds?.length /* && !subtitle */) continue const firstName = sounds[0].name // const includeSound = isSoundWhitelisted(firstName) // if (!includeSound) continue const mostUsedSound = sounds.sort((a, b) => b.weight - a.weight)[0] - const targetSound = sounds[0] // outputMap[id] = { subtitle, sounds: mostUsedSound } // outputMap[id] = { subtitle, sounds } - const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3` + // const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3` // if (!fs.existsSync(soundFilePath)) { // console.warn('no sound file', targetSound.name) // continue // } + let outputUseSoundLine = [] + const minWeight = sounds.reduce((acc, cur) => cur.weight ? Math.min(acc, cur.weight) : acc, sounds[0].weight ?? 1) + if (isNaN(minWeight)) debugger + for (const sound of sounds) { + if (sound.weight && isNaN(sound.weight)) debugger + outputUseSoundLine.push(`${sound.volume ?? 1};${sound.name};${sound.weight ?? minWeight}`) + } + const id = mappingJson.sounds.findIndex(x => x === name) + if (id === -1) { + console.warn('no id for sound', name, targetedVersion) + continue + } const key = `${id};${name}` - outputIdMap[key] = `${targetSound.volume ?? 1};${targetSound.name}` + outputIdMap[key] = outputUseSoundLine.join(',') if (prevMap && prevMap[key]) { keysStats.same++ } else { @@ -218,10 +279,11 @@ const writeSoundsMap = async () => { const makeSoundsBundle = async () => { const allSoundsMap = JSON.parse(fs.readFileSync('./generated/sounds.json', 'utf8')) const allSoundsVersionedMap = JSON.parse(fs.readFileSync('./generated/soundsPathVersionsRemap.json', 'utf8')) + if (!process.env.REPO_SLUG) throw new Error('REPO_SLUG is not set') const allSoundsMeta = { format: 'mp3', - baseUrl: 'https://raw.githubusercontent.com/zardoy/prismarine-web-client/sounds-generated/sounds/' + baseUrl: `https://raw.githubusercontent.com/${process.env.REPO_SLUG}/sounds/sounds/` } await build({ @@ -235,9 +297,25 @@ const makeSoundsBundle = async () => { }, metafile: true, }) + // copy also to generated/sounds.js + fs.copyFileSync('./dist/sounds.js', './generated/sounds.js') } -// downloadAllSounds() -// convertSounds() -// writeSoundsMap() -// makeSoundsBundle() +const action = process.argv[2] +if (action) { + const execFn = { + download: downloadAllSoundsAndCreateMap, + convert: convertSounds, + write: writeSoundsMap, + bundle: makeSoundsBundle, + }[action] + + if (execFn) { + execFn() + } +} else { + // downloadAllSoundsAndCreateMap() + // convertSounds() + writeSoundsMap() + // makeSoundsBundle() +} diff --git a/scripts/replaceFavicon.mjs b/scripts/replaceFavicon.mjs new file mode 100644 index 00000000..0c60d26d --- /dev/null +++ b/scripts/replaceFavicon.mjs @@ -0,0 +1,8 @@ +import fs from 'fs' + +const faviconUrl = process.argv[2] + +// save to assets/favicon.png +fetch(faviconUrl).then(res => res.arrayBuffer()).then(buffer => { + fs.writeFileSync('assets/favicon.png', Buffer.from(buffer)) +}) diff --git a/scripts/requestData.ts b/scripts/requestData.ts new file mode 100644 index 00000000..dc866a1b --- /dev/null +++ b/scripts/requestData.ts @@ -0,0 +1,42 @@ +import WebSocket from 'ws' + +function formatBytes(bytes: number) { + return `${(bytes).toFixed(2)} MB` +} + +function formatTime(ms: number) { + return `${(ms / 1000).toFixed(2)}s` +} + +const ws = new WebSocket('ws://localhost:8081') + +ws.on('open', () => { + console.log('Connected to metrics server, waiting for metrics...') +}) + +ws.on('message', (data) => { + try { + const metrics = JSON.parse(data.toString()) + console.log('\nPerformance Metrics:') + console.log('------------------') + console.log(`Load Time: ${formatTime(metrics.loadTime)}`) + console.log(`Memory Usage: ${formatBytes(metrics.memoryUsage)}`) + console.log(`Timestamp: ${new Date(metrics.timestamp).toLocaleString()}`) + if (!process.argv.includes('-f')) { // follow mode + process.exit(0) + } + } catch (error) { + console.error('Error parsing metrics:', error) + } +}) + +ws.on('error', (error) => { + console.error('WebSocket error:', error) + process.exit(1) +}) + +// Exit if no metrics received after 5 seconds +setTimeout(() => { + console.error('Timeout waiting for metrics') + process.exit(1) +}, 5000) diff --git a/scripts/test-texturepack-files.mjs b/scripts/test-texturepack-files.mjs deleted file mode 100644 index 0446a2fe..00000000 --- a/scripts/test-texturepack-files.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import fs from 'fs' -import minecraftAssets from 'minecraft-assets' - -const gen = JSON.parse(fs.readFileSync('./blocks.json', 'utf8')) - -const version = '1.8.8' -const { blockNames, indexes } = gen - -const blocksActual = indexes[version].map((i) => blockNames[i]) - -const blocksExpected = fs.readdirSync(minecraftAssets(version).directory + '/blocks') -for (const [i, item] of blocksActual.entries()) { - if (item !== blocksExpected[i]) { - console.log('not equal at', i) - } -} diff --git a/scripts/testOptimizedMcdata.ts b/scripts/testOptimizedMcdata.ts new file mode 100644 index 00000000..7c94fac2 --- /dev/null +++ b/scripts/testOptimizedMcdata.ts @@ -0,0 +1,116 @@ +import assert from 'assert' +import JsonOptimizer, { restoreMinecraftData } from '../src/optimizeJson'; +import fs from 'fs' +import minecraftData from 'minecraft-data' + +const json = JSON.parse(fs.readFileSync('./generated/minecraft-data-optimized.json', 'utf8')) + +const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json') + +const validateData = (ver, type) => { + const target = restoreMinecraftData(structuredClone(json), type, ver) + const arrKey = json[type].arrKey + const originalPath = dataPaths.pc[ver][type] + const original = require(`minecraft-data/minecraft-data/data/${originalPath}/${type}.json`) + if (arrKey) { + const originalKeys = original.map(a => JsonOptimizer.getByArrKey(a, arrKey)) as string[] + for (const [i, item] of originalKeys.entries()) { + if (originalKeys.indexOf(item) !== i) { + console.warn(`${type} ${ver} Incorrect source, duplicated arrKey (${arrKey}) ${item}. Ignoring!`) // todo should span instead + const index = originalKeys.indexOf(item); + original.splice(index, 1) + originalKeys.splice(index, 1) + } + } + // if (target.length !== originalKeys.length) { + // throw new Error(`wrong arr length: ${target.length} !== ${original.length}`) + // } + checkKeys(originalKeys, target.map(a => JsonOptimizer.getByArrKey(a, arrKey))) + for (const item of target as any[]) { + const keys = Object.entries(item).map(a => a[0]) + const origItem = original.find(a => JsonOptimizer.getByArrKey(a, arrKey) === JsonOptimizer.getByArrKey(item, arrKey)); + const keysSource = Object.entries(origItem).map(a => a[0]) + checkKeys(keysSource, keys, true, 'prop keys', true) + checkObj(origItem, item) + } + } else { + const keysOriginal = Object.keys(original) + const keysTarget = Object.keys(target) + checkKeys(keysOriginal, keysTarget) + for (const key of keysTarget) { + checkObj(original[key], target[key]) + } + } +} + +const sortObj = (obj) => { + const sorted = {} + for (const key of Object.keys(obj).sort()) { + sorted[key] = obj[key] + } + return sorted +} + +const checkObj = (source, diffing) => { + if (!Array.isArray(source)) { + source = sortObj(source) + } + if (!Array.isArray(diffing)) { + diffing = sortObj(diffing) + } + if (JSON.stringify(source) !== JSON.stringify(diffing)) { + throw new Error(`different value: ${JSON.stringify(source)} ${JSON.stringify(diffing)}`) + } + // checkKeys(Object.keys(source), Object.keys(diffing)) + // for (const [key, val] of Object.entries(source)) { + // if (JSON.stringify(val) !== JSON.stringify(diffing[key])) { + // throw new Error(`different value of ${key}: ${val} ${diffing[key]}`) + // } + // } +} + +const checkKeys = (source, diffing, isUniq = true, msg = '', redundantIsOk = false) => { + if (isUniq) { + for (const [i, item] of diffing.entries()) { + if (diffing.indexOf(item) !== i) { + throw new Error(`Duplicate: ${item}: ${i} ${diffing.indexOf(item)} ${msg}`) + } + } + } + for (const key of source) { + if (!diffing.includes(key)) { + throw new Error(`Diffing does not include "${key}" (${msg})`) + } + } + if (!redundantIsOk) { + for (const key of diffing) { + if (!source.includes(key)) { + throw new Error(`Source does not include "${key}" (${msg})`) + } + } + } +} + +// const data = minecraftData('1.20.4') +const oldId = JsonOptimizer.restoreData(json['blocks'], '1.20', undefined).find(x => x.name === 'brown_stained_glass').id; +const newId = JsonOptimizer.restoreData(json['blocks'], '1.20.4', undefined).find(x => x.name === 'brown_stained_glass').id; +assert(oldId !== newId) +// test all types + all versions + +for (const type of Object.keys(json)) { + if (!json[type].__IS_OPTIMIZED__) continue + if (type === 'language') continue // we have loose data for language for size reasons + console.log('validating', type) + const source = json[type] + let checkedVer = 0 + for (const ver of Object.keys(source.diffs)) { + try { + validateData(ver, type) + } catch (err) { + err.message = `Failed to validate ${type} for ${ver}: ${err.message}` + throw err; + } + checkedVer++ + } + console.log('Checked versions:', checkedVer) +} diff --git a/scripts/updateGitDeps.ts b/scripts/updateGitDeps.ts new file mode 100644 index 00000000..797aea8f --- /dev/null +++ b/scripts/updateGitDeps.ts @@ -0,0 +1,160 @@ +import fs from 'fs' +import path from 'path' +import yaml from 'yaml' +import { execSync } from 'child_process' +import { createInterface } from 'readline' + +interface LockfilePackage { + specifier: string + version: string +} + +interface Lockfile { + importers: { + '.': { + dependencies?: Record + devDependencies?: Record + } + } +} + +interface PackageJson { + pnpm?: { + updateConfig?: { + ignoreDependencies?: string[] + } + } +} + +async function prompt(question: string): Promise { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }) + + return new Promise(resolve => { + rl.question(question, answer => { + rl.close() + resolve(answer.toLowerCase().trim()) + }) + }) +} + +async function getLatestCommit(owner: string, repo: string): Promise { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/HEAD`) + if (!response.ok) { + throw new Error(`Failed to fetch latest commit: ${response.statusText}`) + } + const data = await response.json() + return data.sha +} + +function extractGitInfo(specifier: string): { owner: string; repo: string; branch: string } | null { + const match = specifier.match(/github:([^/]+)\/([^#]+)(?:#(.+))?/) + if (!match) return null + return { + owner: match[1], + repo: match[2], + branch: match[3] || 'master' + } +} + +function extractCommitHash(version: string): string | null { + const match = version.match(/https:\/\/codeload\.github\.com\/[^/]+\/[^/]+\/tar\.gz\/([a-f0-9]+)/) + return match ? match[1] : null +} + +function getIgnoredDependencies(): string[] { + try { + const packageJsonPath = path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as PackageJson + return packageJson.pnpm?.updateConfig?.ignoreDependencies || [] + } catch (error) { + console.warn('Failed to read package.json for ignored dependencies:', error) + return [] + } +} + +async function main() { + const lockfilePath = path.join(process.cwd(), 'pnpm-lock.yaml') + const lockfileContent = fs.readFileSync(lockfilePath, 'utf8') + const lockfile = yaml.parse(lockfileContent) as Lockfile + + const ignoredDependencies = new Set(getIgnoredDependencies()) + console.log('Ignoring dependencies:', Array.from(ignoredDependencies).join(', ') || 'none') + + const dependencies = { + ...lockfile.importers['.'].dependencies, + ...lockfile.importers['.'].devDependencies + } + + const updates: Array<{ + name: string + currentHash: string + latestHash: string + gitInfo: ReturnType + }> = [] + + console.log('\nChecking git dependencies...') + for (const [name, pkg] of Object.entries(dependencies)) { + if (ignoredDependencies.has(name)) { + console.log(`Skipping ignored dependency: ${name}`) + continue + } + + if (!pkg.specifier.startsWith('github:')) continue + + const gitInfo = extractGitInfo(pkg.specifier) + if (!gitInfo) continue + + const currentHash = extractCommitHash(pkg.version) + if (!currentHash) continue + + try { + process.stdout.write(`Checking ${name}... `) + const latestHash = await getLatestCommit(gitInfo.owner, gitInfo.repo) + if (currentHash !== latestHash) { + console.log('update available') + updates.push({ name, currentHash, latestHash, gitInfo }) + } else { + console.log('up to date') + } + } catch (error) { + console.log('failed') + console.error(`Error checking ${name}:`, error) + } + } + + if (updates.length === 0) { + console.log('\nAll git dependencies are up to date!') + return + } + + console.log('\nThe following git dependencies can be updated:') + for (const update of updates) { + console.log(`\n${update.name}:`) + console.log(` Current: ${update.currentHash}`) + console.log(` Latest: ${update.latestHash}`) + console.log(` Repo: ${update.gitInfo!.owner}/${update.gitInfo!.repo}`) + } + + const answer = await prompt('\nWould you like to update these dependencies? (y/N): ') + if (answer === 'y' || answer === 'yes') { + let newLockfileContent = lockfileContent + for (const update of updates) { + newLockfileContent = newLockfileContent.replace( + new RegExp(update.currentHash, 'g'), + update.latestHash + ) + } + fs.writeFileSync(lockfilePath, newLockfileContent) + console.log('\nUpdated pnpm-lock.yaml with new commit hashes') + // console.log('Running pnpm install to apply changes...') + // execSync('pnpm install', { stdio: 'inherit' }) + console.log('Done!') + } else { + console.log('\nNo changes were made.') + } +} + +main().catch(console.error) diff --git a/scripts/updateGitPackages.mjs b/scripts/updateGitPackages.mjs deleted file mode 100644 index 14524587..00000000 --- a/scripts/updateGitPackages.mjs +++ /dev/null @@ -1,26 +0,0 @@ -// pnpm bug workaround -import fs from 'fs' -import { parse } from 'yaml' - -const lockfile = parse(fs.readFileSync('./pnpm-lock.yaml', 'utf8')) - -const depsKeys = ['dependencies', 'devDependencies'] - -for (const importer of Object.values(lockfile.importers)) { - for (const depsKey of depsKeys) { - for (const [depName, { specifier, version }] of Object.entries(importer[depsKey])) { - if (!specifier.startsWith('github:')) continue - let branch = specifier.match(/#(.*)$/)?.[1] ?? '' - if (branch) branch = `/${branch}` - const sha = version.split('/').slice(3).join('/').replace(/\(.+/, '') - const repo = version.split('/').slice(1, 3).join('/') - const lastCommitJson = await fetch(`https://api.github.com/repos/${repo}/commits${branch}?per_page=1`).then(res => res.json()) - const lastCommitActual = lastCommitJson ?? lastCommitJson[0] - const lastCommitActualSha = lastCommitActual?.sha - if (lastCommitActualSha === undefined) debugger - if (sha !== lastCommitActualSha) { - console.log(`Outdated ${depName} github.com/${repo} : ${sha} -> ${lastCommitActualSha} (${lastCommitActual.commit.message})`) - } - } - } -} diff --git a/scripts/updateHandledPackets.mjs b/scripts/updateHandledPackets.mjs new file mode 100644 index 00000000..080eaf44 --- /dev/null +++ b/scripts/updateHandledPackets.mjs @@ -0,0 +1,60 @@ +import fs from 'fs' +import path from 'path' +import minecraftData from 'minecraft-data' + +const lastVersion = minecraftData.versions.pc[0] +// console.log('last proto ver', lastVersion.minecraftVersion) +const allPackets = minecraftData(lastVersion.minecraftVersion).protocol +const getPackets = ({ types }) => { + return Object.keys(types).map(type => type.replace('packet_', '')) +} +// todo test against all versions +const allFromServerPackets = getPackets(allPackets.play.toClient) +const allToServerPackets = getPackets(allPackets.play.toServer).filter(x => !['packet'].includes(x)) + +const buildFile = './dist/index.js' + +const file = fs.readFileSync(buildFile, 'utf8') + +const packetsReceiveRegex = /client\.on\("(\w+)"/g +const packetsReceiveSend = /client\.write\("(\w+)"/g + +let allSupportedReceive = [...new Set([...file.matchAll(packetsReceiveRegex)].map(x => x[1]))] +let allSupportedSend = [...new Set([...file.matchAll(packetsReceiveSend)].map(x => x[1]))] + +let md = '# Handled Packets\n' + +md += '\n## Server -> Client\n\n' +let notSupportedRows = [] +let supportedRows = [] +for (const packet of allFromServerPackets) { + const includes = allSupportedReceive.includes(packet); + (includes ? supportedRows : notSupportedRows).push(packet) +} + +for (const row of notSupportedRows) { + md += `❌ ${row}\n` +} +for (const row of supportedRows) { + md += `✅ ${row}\n` +} + +md += '\n' + +notSupportedRows = [] +supportedRows = [] + +md += '## Client -> Server\n\n' +for (const packet of allToServerPackets) { + const includes = allSupportedSend.includes(packet); + (includes ? supportedRows : notSupportedRows).push(packet) +} + +for (const row of notSupportedRows) { + md += `❌ ${row}\n` +} +for (const row of supportedRows) { + md += `✅ ${row}\n` +} + +fs.writeFileSync('./docs-assets/handled-packets.md', md) diff --git a/scripts/uploadSoundFiles.ts b/scripts/uploadSoundFiles.ts new file mode 100644 index 00000000..e8677c87 --- /dev/null +++ b/scripts/uploadSoundFiles.ts @@ -0,0 +1,109 @@ +import fetch from 'node-fetch'; +import * as fs from 'fs'; +import * as path from 'path'; +import { glob } from 'glob'; + +// Git details +const REPO_SLUG = process.env.REPO_SLUG; +const owner = REPO_SLUG.split('/')[0]; +const repo = REPO_SLUG.split('/')[1]; +const branch = "sounds"; + +// GitHub token for authentication +const token = process.env.GITHUB_TOKEN; + +// GitHub API endpoint +const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents`; + +const headers = { + Authorization: `token ${token}`, + 'Content-Type': 'application/json' +}; + +async function getShaForExistingFile(repoFilePath: string): Promise { + const url = `${baseUrl}/${repoFilePath}?ref=${branch}`; + const response = await fetch(url, { headers }); + if (response.status === 404) { + return null; // File does not exist + } + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.statusText}`); + } + const data = await response.json(); + return data.sha; +} + +async function uploadFiles() { + const commitMessage = "Upload multiple files via script"; + const committer = { + name: "GitHub", + email: "noreply@github.com" + }; + + const filesToUpload = glob.sync("generated/sounds/**/*.mp3").map(localPath => { + const repoPath = localPath.replace(/^generated\//, ''); + return { localPath, repoPath }; + }); + + const files = await Promise.all(filesToUpload.map(async file => { + const content = fs.readFileSync(file.localPath, 'base64'); + const sha = await getShaForExistingFile(file.repoPath); + return { + path: file.repoPath, + mode: "100644", + type: "blob", + sha: sha || undefined, + content: content + }; + })); + + const treeResponse = await fetch(`${baseUrl}/git/trees`, { + method: 'POST', + headers: headers, + body: JSON.stringify({ + base_tree: null, + tree: files + }) + }); + + if (!treeResponse.ok) { + throw new Error(`Failed to create tree: ${treeResponse.statusText}`); + } + + const treeData = await treeResponse.json(); + + const commitResponse = await fetch(`${baseUrl}/git/commits`, { + method: 'POST', + headers: headers, + body: JSON.stringify({ + message: commitMessage, + tree: treeData.sha, + parents: [branch], + committer: committer + }) + }); + + if (!commitResponse.ok) { + throw new Error(`Failed to create commit: ${commitResponse.statusText}`); + } + + const commitData = await commitResponse.json(); + + const updateRefResponse = await fetch(`${baseUrl}/git/refs/heads/${branch}`, { + method: 'PATCH', + headers: headers, + body: JSON.stringify({ + sha: commitData.sha + }) + }); + + if (!updateRefResponse.ok) { + throw new Error(`Failed to update ref: ${updateRefResponse.statusText}`); + } + + console.log("Files uploaded successfully"); +} + +uploadFiles().catch(error => { + console.error("Error uploading files:", error); +}); diff --git a/scripts/uploadSounds.ts b/scripts/uploadSounds.ts new file mode 100644 index 00000000..b0e9ecd7 --- /dev/null +++ b/scripts/uploadSounds.ts @@ -0,0 +1,67 @@ +import fs from 'fs' + +// GitHub details +const owner = "zardoy"; +const repo = "minecraft-web-client"; +const branch = "sounds-generated"; +const filePath = "dist/sounds.js"; // Local file path +const repoFilePath = "sounds-v2.js"; // Path in the repo + +// GitHub token for authentication +const token = process.env.GITHUB_TOKEN; + +// GitHub API endpoint +const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${repoFilePath}`; + +const headers = { + Authorization: `token ${token}`, + 'Content-Type': 'application/json' +}; + +async function getShaForExistingFile(): Promise { + const url = `${baseUrl}?ref=${branch}`; + const response = await fetch(url, { headers }); + if (response.status === 404) { + return null; // File does not exist + } + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.statusText}`); + } + const data = await response.json(); + return data.sha; +} + +async function uploadFile() { + const content = fs.readFileSync(filePath, 'utf8'); + const base64Content = Buffer.from(content).toString('base64'); + const sha = await getShaForExistingFile(); + console.log('got sha') + + const body = { + message: "Update sounds.js", + content: base64Content, + branch: branch, + committer: { + name: "GitHub", + email: "noreply@github.com" + }, + sha: sha || undefined + }; + + const response = await fetch(baseUrl, { + method: 'PUT', + headers: headers, + body: JSON.stringify(body) + }); + + if (!response.ok) { + throw new Error(`Failed to upload file: ${response.statusText}`); + } + + const responseData = await response.json(); + console.log("File uploaded successfully:", responseData); +} + +uploadFile().catch(error => { + console.error("Error uploading file:", error); +}); diff --git a/scripts/wsServer.ts b/scripts/wsServer.ts new file mode 100644 index 00000000..43035f52 --- /dev/null +++ b/scripts/wsServer.ts @@ -0,0 +1,45 @@ +import {WebSocketServer} from 'ws' + +export function startWsServer(port: number = 8081, tryOtherPort: boolean = true): Promise { + return new Promise((resolve, reject) => { + const tryPort = (currentPort: number) => { + const wss = new WebSocketServer({ port: currentPort }) + .on('listening', () => { + console.log(`WebSocket server started on port ${currentPort}`) + resolve(currentPort) + }) + .on('error', (err: any) => { + if (err.code === 'EADDRINUSE' && tryOtherPort) { + console.log(`Port ${currentPort} in use, trying ${currentPort + 1}`) + wss.close() + tryPort(currentPort + 1) + } else { + reject(err) + } + }) + + wss.on('connection', (ws) => { + console.log('Client connected') + + ws.on('message', (message) => { + try { + // Simply relay the message to all connected clients except sender + wss.clients.forEach(client => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(message.toString()) + } + }) + } catch (error) { + console.error('Error processing message:', error) + } + }) + + ws.on('close', () => { + console.log('Client disconnected') + }) + }) + } + + tryPort(port) + }) +} diff --git a/server.js b/server.js index da40e163..49699cdb 100644 --- a/server.js +++ b/server.js @@ -15,19 +15,32 @@ try { // Create our app const app = express() -const isProd = process.argv.includes('--prod') +const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production' +const timeoutIndex = process.argv.indexOf('--timeout') +let timeout = timeoutIndex > -1 && timeoutIndex + 1 < process.argv.length + ? parseInt(process.argv[timeoutIndex + 1]) + : process.env.TIMEOUT + ? parseInt(process.env.TIMEOUT) + : 10000 +if (isNaN(timeout) || timeout < 0) { + console.warn('Invalid timeout value provided, using default of 10000ms') + timeout = 10000 +} app.use(compression()) -app.use(netApi({ allowOrigin: '*' })) +app.use(cors()) +app.use(netApi({ + allowOrigin: '*', + log: process.argv.includes('--log') || process.env.LOG === 'true', + timeout +})) if (!isProd) { - app.use('/blocksStates', express.static(path.join(__dirname, './prismarine-viewer/public/blocksStates'))) - app.use('/textures', express.static(path.join(__dirname, './prismarine-viewer/public/textures'))) - app.use('/sounds', express.static(path.join(__dirname, './generated/sounds/'))) } // patch config app.get('/config.json', (req, res, next) => { // read original file config let config = {} + let publicConfig = {} try { config = require('./config.json') } catch { @@ -35,22 +48,38 @@ app.get('/config.json', (req, res, next) => { config = require('./dist/config.json') } catch { } } + try { + publicConfig = require('./public/config.json') + } catch { } res.json({ ...config, 'defaultProxy': '', // use current url (this server) + ...publicConfig, }) }) -app.use(express.static(path.join(__dirname, './dist'))) +if (isProd) { + // add headers to enable shared array buffer + app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin') + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp') + next() + }) -const portArg = process.argv.indexOf('--port') -const port = (require.main === module ? process.argv[2] : portArg !== -1 ? process.argv[portArg + 1] : undefined) || 8080 + // First serve from the override directory (volume mount) + app.use(express.static(path.join(__dirname, './public'))) + + // Then fallback to the original dist directory + app.use(express.static(path.join(__dirname, './dist'))) +} + +const numArg = process.argv.find(x => x.match(/^\d+$/)) +const port = (require.main === module ? numArg : undefined) || 8080 // Start the server -const server = isProd ? - undefined : +const server = app.listen(port, async function () { - console.log('Server listening on port ' + server.address().port) - if (siModule) { + console.log('Proxy server listening on port ' + server.address().port) + if (siModule && isProd) { const _interfaces = await siModule.networkInterfaces() const interfaces = Array.isArray(_interfaces) ? _interfaces : [_interfaces] let netInterface = interfaces.find(int => int.default) diff --git a/src/api/mcStatusApi.ts b/src/api/mcStatusApi.ts new file mode 100644 index 00000000..8ac429dd --- /dev/null +++ b/src/api/mcStatusApi.ts @@ -0,0 +1,64 @@ +globalThis.resolveDnsFallback = async (hostname: string) => { + const response = await fetchServerStatus(hostname) + return response?.raw.srv_record ?? undefined +} + +export const isServerValid = (ip: string) => { + const isInLocalNetwork = ip.startsWith('192.168.') || + ip.startsWith('10.') || + ip.startsWith('172.') || + ip.startsWith('127.') || + ip.startsWith('localhost') || + ip.startsWith(':') + const VALID_IP_OR_DOMAIN = ip.includes('.') + + return !isInLocalNetwork && VALID_IP_OR_DOMAIN +} + +export async function fetchServerStatus (ip: string, signal?: AbortSignal, versionOverride?: string) { + if (!isServerValid(ip)) return + + const response = await fetch(`https://api.mcstatus.io/v2/status/java/${ip}`, { signal }) + const data: ServerResponse = await response.json() + const versionClean = data.version?.name_raw.replace(/^[^\d.]+/, '') + + return { + formattedText: data.motd?.raw ?? '', + textNameRight: data.online ? + `${versionOverride ?? versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}` : + '', + icon: data.icon, + offline: !data.online, + raw: data + } +} + +export type ServerResponse = { + online: boolean + version?: { + name_raw: string + } + // display tooltip + players?: { + online: number + max: number + list: Array<{ + name_raw: string + name_clean: string + }> + } + icon?: string + motd?: { + raw: string + } + // todo circle error icon + mods?: Array<{ name: string, version: string }> + // todo display via hammer icon + software?: string + plugins?: Array<{ name, version }> + // port?: number + srv_record?: { + host: string + port: number + } +} diff --git a/src/appConfig.ts b/src/appConfig.ts new file mode 100644 index 00000000..c29d74e8 --- /dev/null +++ b/src/appConfig.ts @@ -0,0 +1,109 @@ +import { defaultsDeep } from 'lodash' +import { disabledSettings, options, qsOptions } from './optionsStorage' +import { miscUiState } from './globalState' +import { setLoadingScreenStatus } from './appStatus' +import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider' +import { customKeymaps, updateBinds } from './controls' + +export type CustomAction = { + readonly type: string + readonly input: readonly any[] +} + +export type ActionType = string | CustomAction + +export type ActionHoldConfig = { + readonly command: ActionType + readonly longPressAction?: ActionType + readonly duration?: number + readonly threshold?: number +} + +export type MobileButtonConfig = { + readonly label?: string + readonly icon?: string + readonly action?: ActionType + readonly actionHold?: ActionType | ActionHoldConfig + readonly iconStyle?: React.CSSProperties +} + +export type AppConfig = { + // defaultHost?: string + // defaultHostSave?: string + defaultProxy?: string + // defaultProxySave?: string + // defaultVersion?: string + peerJsServer?: string + peerJsServerFallback?: string + promoteServers?: Array<{ ip, description, name?, version?, }> + mapsProvider?: string + + appParams?: Record // query string params + rightSideText?: string + + defaultSettings?: Record + forceSettings?: Record + // hideSettings?: Record + allowAutoConnect?: boolean + splashText?: string + splashTextFallback?: string + pauseLinks?: Array>> + mobileButtons?: MobileButtonConfig[] + keybindings?: Record + defaultLanguage?: string + displayLanguageSelector?: boolean + supportedLanguages?: string[] + showModsButton?: boolean + defaultUsername?: string + skinTexturesProxy?: string + alwaysReconnectButton?: boolean + reportBugButtonWithReconnect?: boolean + disabledCommands?: string[] // Array of command IDs to disable (e.g. ['general.jump', 'general.chat']) +} + +export const loadAppConfig = (appConfig: AppConfig) => { + + if (miscUiState.appConfig) { + Object.assign(miscUiState.appConfig, appConfig) + } else { + miscUiState.appConfig = appConfig + } + + if (appConfig.forceSettings) { + for (const [key, value] of Object.entries(appConfig.forceSettings)) { + if (value) { + disabledSettings.value.add(key) + // since the setting is forced, we need to set it to that value + if (appConfig.defaultSettings && key in appConfig.defaultSettings && !qsOptions[key]) { + options[key] = appConfig.defaultSettings[key] + } + } else { + disabledSettings.value.delete(key) + } + } + } + // todo apply defaultSettings to defaults even if not forced in case of remote config + + if (appConfig.keybindings) { + Object.assign(customKeymaps, defaultsDeep(appConfig.keybindings, customKeymaps)) + updateBinds(customKeymaps) + } + + appViewer?.appConfigUdpate() + + setStorageDataOnAppConfigLoad(appConfig) +} + +export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG + +if (isBundledConfigUsed) { + loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {}) +} else { + void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { + // console.warn('Failed to load optional app config.json', error) + // return {} + setLoadingScreenStatus('Failed to load app config.json', true) + }).then((config: AppConfig) => { + loadAppConfig(config) + }) +} diff --git a/src/appParams.ts b/src/appParams.ts new file mode 100644 index 00000000..4c8ca186 --- /dev/null +++ b/src/appParams.ts @@ -0,0 +1,121 @@ +import type { AppConfig } from './appConfig' +import { miscUiState } from './globalState' + +const qsParams = new URLSearchParams(window.location?.search ?? '') + +export type AppQsParams = { + // AddServerOrConnect.tsx params + ip?: string + name?: string + version?: string + proxy?: string + username?: string + lockConnect?: string + autoConnect?: string + alwaysReconnect?: string + // googledrive.ts params + state?: string + // ServersListProvider.tsx params + serversList?: string + // Map and texture params + texturepack?: string + map?: string + mapDirBaseUrl?: string + mapDirGuess?: string + // Singleplayer params + singleplayer?: string + sp?: string + loadSave?: string + // Server params + reconnect?: string + server?: string + // Peer connection params + connectPeer?: string + peerVersion?: string + // UI params + modal?: string + viewerConnect?: string + // Map version param + mapVersion?: string + // Command params + command?: string + // Misc params + suggest_save?: string + noPacketsValidation?: string + testCrashApp?: string + onlyConnect?: string + connectText?: string + freezeSettings?: string + testIosCrash?: string + addPing?: string + + // Replay params + replayFilter?: string + replaySpeed?: string + replayFileUrl?: string + replayValidateClient?: string + replayStopOnError?: string + replaySkipMissingOnTimeout?: string + replayPacketsSenderDelay?: string + + // Benchmark params + openBenchmark?: string + renderDistance?: string + downloadBenchmark?: string + benchmarkMapZipUrl?: string + benchmarkPosition?: string +} + +export type AppQsParamsArray = { + mapDir?: string[] + setting?: string[] + serverSetting?: string[] + command?: string[] +} + +type AppQsParamsArrayTransformed = { + [k in keyof AppQsParamsArray]: string[] +} + +globalThis.process ??= {} as any +const initialAppConfig = process?.env?.INLINED_APP_CONFIG as AppConfig ?? {} + +export const appQueryParams = new Proxy({} as AppQsParams, { + get (target, property) { + if (typeof property !== 'string') { + return undefined + } + const qsParam = qsParams.get(property) + if (qsParam) return qsParam + return miscUiState.appConfig?.appParams?.[property] + }, +}) + +export const appQueryParamsArray = new Proxy({} as AppQsParamsArrayTransformed, { + get (target, property) { + if (typeof property !== 'string') { + return null + } + const qsParam = qsParams.getAll(property) + if (qsParam.length) return qsParam + return miscUiState.appConfig?.appParams?.[property] ?? [] + }, +}) + +export function updateQsParam (name: keyof AppQsParams, value: string | undefined) { + const url = new URL(window.location.href) + if (value) { + url.searchParams.set(name, value) + } else { + url.searchParams.delete(name) + } + window.history.replaceState({}, '', url.toString()) +} + +// Helper function to check if a specific query parameter exists +export const hasQueryParam = (param: keyof AppQsParams) => qsParams.has(param) + +// Helper function to get all query parameters as a URLSearchParams object +export const getRawQueryParams = () => qsParams; + +(globalThis as any).debugQueryParams = Object.fromEntries(qsParams.entries()) diff --git a/src/appStatus.ts b/src/appStatus.ts new file mode 100644 index 00000000..101714f5 --- /dev/null +++ b/src/appStatus.ts @@ -0,0 +1,41 @@ +import { resetStateAfterDisconnect } from './browserfs' +import { hideModal, activeModalStack, showModal, miscUiState } from './globalState' +import { appStatusState, resetAppStatusState } from './react/AppStatusProvider' + +let ourLastStatus: string | undefined = '' +export const setLoadingScreenStatus = function (status: string | undefined | null, isError = false, hideDots = false, fromFlyingSquid = false, minecraftJsonMessage?: Record) { + if (typeof status === 'string') status = window.translateText?.(status) ?? status + // null can come from flying squid, should restore our last status + if (status === null) { + status = ourLastStatus + } else if (!fromFlyingSquid) { + ourLastStatus = status + } + fromFlyingSquid = false + + if (status === undefined) { + appStatusState.status = '' + + hideModal({ reactType: 'app-status' }, {}, { force: true }) + return + } + + if (!activeModalStack.some(x => x.reactType === 'app-status')) { + // just showing app status + resetAppStatusState() + } + showModal({ reactType: 'app-status' }) + if (appStatusState.isError) { + return + } + appStatusState.hideDots = hideDots + appStatusState.isError = isError + appStatusState.lastStatus = isError ? appStatusState.status : '' + appStatusState.status = status + appStatusState.minecraftJsonMessage = minecraftJsonMessage ?? null + + if (isError && miscUiState.gameLoaded) { + resetStateAfterDisconnect() + } +} +globalThis.setLoadingScreenStatus = setLoadingScreenStatus diff --git a/src/appViewer.ts b/src/appViewer.ts new file mode 100644 index 00000000..628d11b4 --- /dev/null +++ b/src/appViewer.ts @@ -0,0 +1,354 @@ +import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter' +import { getInitialPlayerState, PlayerStateRenderer, PlayerStateReactive } from 'renderer/viewer/lib/basePlayerState' +import { subscribeKey } from 'valtio/utils' +import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon' +import { Vec3 } from 'vec3' +import { SoundSystem } from 'renderer/viewer/three/threeJsSound' +import { proxy, subscribe } from 'valtio' +import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend' +import { getSyncWorld } from 'renderer/playground/shared' +import { MaybePromise } from 'contro-max/build/types/store' +import { PANORAMA_VERSION } from 'renderer/viewer/three/panoramaShared' +import { playerState } from './mineflayer/playerState' +import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter' +import { setLoadingScreenStatus } from './appStatus' +import { activeModalStack, miscUiState } from './globalState' +import { options } from './optionsStorage' +import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager' +import { watchOptionsAfterWorldViewInit } from './watchOptions' +import { loadMinecraftData } from './connect' +import { reloadChunks } from './utils' +import { displayClientChat } from './botUtils' + +export interface RendererReactiveState { + world: { + chunksLoaded: Set + // chunksTotalNumber: number + heightmaps: Map + allChunksLoaded: boolean + mesherWork: boolean + intersectMedia: { id: string, x: number, y: number } | null + } + renderer: string + preventEscapeMenu: boolean +} +export interface NonReactiveState { + world: { + chunksLoaded: Set + chunksTotalNumber: number + } +} + +export interface GraphicsBackendConfig { + fpsLimit?: number + powerPreference?: 'high-performance' | 'low-power' + statsVisible?: number + sceneBackground: string + timeoutRendering?: boolean +} + +const defaultGraphicsBackendConfig: GraphicsBackendConfig = { + fpsLimit: undefined, + powerPreference: undefined, + sceneBackground: 'lightblue', + timeoutRendering: false +} + +export interface GraphicsInitOptions { + resourcesManager: ResourcesManagerTransferred + config: GraphicsBackendConfig + rendererSpecificSettings: S + + callbacks: { + displayCriticalError: (error: Error) => void + setRendererSpecificSettings: (key: string, value: any) => void + + fireCustomEvent: (eventName: string, ...args: any[]) => void + } +} + +export interface DisplayWorldOptions { + version: string + worldView: WorldDataEmitterWorker + inWorldRenderingConfig: WorldRendererConfig + playerStateReactive: PlayerStateReactive + rendererState: RendererReactiveState + nonReactiveState: NonReactiveState +} + +export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => MaybePromise) & { + id: string +} + +// no sync methods +export interface GraphicsBackend { + id: string + displayName?: string + startPanorama: () => void + // prepareResources: (version: string, progressReporter: ProgressReporter) => Promise + startWorld: (options: DisplayWorldOptions) => Promise | void + disconnect: () => void + setRendering: (rendering: boolean) => void + getDebugOverlay?: () => Record + updateCamera: (pos: Vec3 | null, yaw: number, pitch: number) => void + setRoll?: (roll: number) => void + soundSystem: SoundSystem | undefined + + backendMethods: Record | undefined +} + +export class AppViewer { + waitBackendLoadPromises = [] as Array> + + resourcesManager = new ResourcesManager() + worldView: WorldDataEmitter | undefined + readonly config: GraphicsBackendConfig = { + ...defaultGraphicsBackendConfig, + powerPreference: options.gpuPreference === 'default' ? undefined : options.gpuPreference + } + backend?: GraphicsBackend + backendLoader?: GraphicsBackendLoader + private currentState?: { + method: string + args: any[] + } + currentDisplay = null as 'menu' | 'world' | null + inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig) + lastCamUpdate = 0 + playerState = playerState + rendererState = getDefaultRendererState().reactive + nonReactiveState: NonReactiveState = getDefaultRendererState().nonReactive + worldReady: Promise + private resolveWorldReady: () => void + + constructor () { + this.disconnectBackend() + } + + async loadBackend (loader: GraphicsBackendLoader) { + if (this.backend) { + this.disconnectBackend() + } + + await Promise.all(this.waitBackendLoadPromises) + this.waitBackendLoadPromises = [] + + this.backendLoader = loader + const rendererSpecificSettings = {} as Record + const rendererSettingsKey = `renderer.${this.backendLoader?.id}` + for (const key in options) { + if (key.startsWith(rendererSettingsKey)) { + rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key] + } + } + const loaderOptions: GraphicsInitOptions = { // todo! + resourcesManager: this.resourcesManager as ResourcesManagerTransferred, + config: this.config, + callbacks: { + displayCriticalError (error) { + console.error(error) + setLoadingScreenStatus(error.message, true) + }, + setRendererSpecificSettings (key: string, value: any) { + options[`${rendererSettingsKey}.${key}`] = value + }, + fireCustomEvent (eventName, ...args) { + // this.callbacks.fireCustomEvent(eventName, ...args) + } + }, + rendererSpecificSettings, + } + this.backend = await loader(loaderOptions) + + // if (this.resourcesManager.currentResources) { + // void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter()) + // } + + // Execute queued action if exists + if (this.currentState) { + if (this.currentState.method === 'startPanorama') { + this.startPanorama() + } else { + const { method, args } = this.currentState + this.backend[method](...args) + if (method === 'startWorld') { + void this.worldView!.init(bot.entity.position) + // void this.worldView!.init(args[0].playerState.getPosition()) + } + } + } + + // todo + modalStackUpdateChecks() + } + + async startWithBot () { + const renderDistance = miscUiState.singleplayer ? options.renderDistance : options.multiplayerRenderDistance + await this.startWorld(bot.world, renderDistance) + this.worldView!.listenToBot(bot) + } + + appConfigUdpate () { + if (miscUiState.appConfig) { + this.inWorldRenderingConfig.skinTexturesProxy = miscUiState.appConfig.skinTexturesProxy + } + } + + async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState.reactive) { + if (this.currentDisplay === 'world') throw new Error('World already started') + this.currentDisplay = 'world' + const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0) + this.worldView = new WorldDataEmitter(world, renderDistance, startPosition) + this.worldView.panicChunksReload = () => { + if (!options.experimentalClientSelfReload) return + if (process.env.NODE_ENV === 'development') { + displayClientChat(`[client] client panicked due to too long loading time. Soft reloading chunks...`) + } + void reloadChunks() + } + window.worldView = this.worldView + watchOptionsAfterWorldViewInit(this.worldView) + this.appConfigUdpate() + + const displayWorldOptions: DisplayWorldOptions = { + version: this.resourcesManager.currentConfig!.version, + worldView: this.worldView, + inWorldRenderingConfig: this.inWorldRenderingConfig, + playerStateReactive: playerStateSend, + rendererState: this.rendererState, + nonReactiveState: this.nonReactiveState + } + let promise: undefined | Promise + if (this.backend) { + promise = this.backend.startWorld(displayWorldOptions) ?? undefined + // void this.worldView.init(startPosition) + } + this.currentState = { method: 'startWorld', args: [displayWorldOptions] } + + await promise + // Resolve the promise after world is started + this.resolveWorldReady() + return !!promise + } + + resetBackend (cleanState = false) { + this.disconnectBackend(cleanState) + if (this.backendLoader) { + void this.loadBackend(this.backendLoader) + } + } + + startPanorama () { + if (this.currentDisplay === 'menu') return + if (options.disableAssets) return + if (this.backend && !hasAppStatus()) { + this.currentDisplay = 'menu' + if (process.env.SINGLE_FILE_BUILD_MODE) { + void loadMinecraftData(PANORAMA_VERSION).then(() => { + this.backend?.startPanorama() + }) + } else { + this.backend.startPanorama() + } + } + this.currentState = { method: 'startPanorama', args: [] } + } + + // async prepareResources (version: string, progressReporter: ProgressReporter) { + // if (this.backend) { + // await this.backend.prepareResources(version, progressReporter) + // } + // } + + destroyAll () { + this.disconnectBackend() + this.resourcesManager.destroy() + } + + disconnectBackend (cleanState = false) { + if (cleanState) { + this.currentState = undefined + this.currentDisplay = null + this.worldView = undefined + } + if (this.backend) { + this.backend.disconnect() + this.backend = undefined + } + this.currentDisplay = null + const { promise, resolve } = Promise.withResolvers() + this.worldReady = promise + this.resolveWorldReady = resolve + this.rendererState = proxy(getDefaultRendererState().reactive) + this.nonReactiveState = getDefaultRendererState().nonReactive + // this.queuedDisplay = undefined + } + + get utils () { + return { + async waitingForChunks () { + if (this.backend?.worldState.allChunksLoaded) return + return new Promise((resolve) => { + const interval = setInterval(() => { + if (this.backend?.worldState.allChunksLoaded) { + clearInterval(interval) + resolve(true) + } + }, 100) + }) + } + } + } +} + +// do not import this. Use global appViewer instead (without window prefix). +export const appViewer = new AppViewer() +window.appViewer = appViewer + +const initialMenuStart = async () => { + if (appViewer.currentDisplay === 'world') { + appViewer.resetBackend(true) + } + const demo = new URLSearchParams(window.location.search).get('demo') + if (!demo) { + appViewer.startPanorama() + return + } + + // const version = '1.18.2' + const version = '1.21.4' + const { loadMinecraftData } = await import('./connect') + const { getSyncWorld } = await import('../renderer/playground/shared') + await loadMinecraftData(version) + const world = getSyncWorld(version) + world.setBlockStateId(new Vec3(0, 64, 0), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(1, 64, 0), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(1, 64, 1), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(0, 64, 1), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(-1, 64, -1), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(-1, 64, 0), loadedData.blocksByName.water.defaultState) + world.setBlockStateId(new Vec3(0, 64, -1), loadedData.blocksByName.water.defaultState) + appViewer.resourcesManager.currentConfig = { version } + appViewer.playerState.reactive = getInitialPlayerState() + await appViewer.resourcesManager.updateAssetsData({}) + await appViewer.startWorld(world, 3) + appViewer.backend!.updateCamera(new Vec3(0, 65.7, 0), 0, -Math.PI / 2) // Y+1 and pitch = PI/2 to look down + void appViewer.worldView!.init(new Vec3(0, 64, 0)) +} +window.initialMenuStart = initialMenuStart + +const hasAppStatus = () => activeModalStack.some(m => m.reactType === 'app-status') + +const modalStackUpdateChecks = () => { + // maybe start panorama + if (!miscUiState.gameLoaded && !hasAppStatus()) { + void initialMenuStart() + } + + if (appViewer.backend) { + appViewer.backend.setRendering(!hasAppStatus()) + } + + appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0 +} +subscribe(activeModalStack, modalStackUpdateChecks) diff --git a/src/appViewerLoad.ts b/src/appViewerLoad.ts new file mode 100644 index 00000000..53260662 --- /dev/null +++ b/src/appViewerLoad.ts @@ -0,0 +1,51 @@ +import { subscribeKey } from 'valtio/utils' +import createGraphicsBackend from 'renderer/viewer/three/graphicsBackend' +import { options } from './optionsStorage' +import { appViewer } from './appViewer' +import { miscUiState } from './globalState' +import { watchOptionsAfterViewerInit } from './watchOptions' +import { showNotification } from './react/NotificationProvider' + +const backends = [ + createGraphicsBackend, +] +const loadBackend = async () => { + let backend = backends.find(backend => backend.id === options.activeRenderer) + if (!backend) { + showNotification(`No backend found for renderer ${options.activeRenderer}`, `Falling back to ${backends[0].id}`, true) + backend = backends[0] + } + await appViewer.loadBackend(backend) +} +window.loadBackend = loadBackend +if (process.env.SINGLE_FILE_BUILD_MODE) { + const unsub = subscribeKey(miscUiState, 'fsReady', () => { + if (miscUiState.fsReady) { + // don't do it earlier to load fs and display menu faster + void loadBackend() + unsub() + } + }) +} else { + setTimeout(() => { + void loadBackend() + }) +} + +const animLoop = () => { + for (const fn of beforeRenderFrame) fn() + requestAnimationFrame(animLoop) +} +requestAnimationFrame(animLoop) + +watchOptionsAfterViewerInit() + +// reset backend when renderer changes + +subscribeKey(options, 'activeRenderer', async () => { + if (appViewer.currentDisplay === 'world' && bot) { + appViewer.resetBackend(true) + await loadBackend() + void appViewer.startWithBot() + } +}) diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 53c86652..54af0d35 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -1,3 +1,4 @@ +import { subscribeKey } from 'valtio/utils' import { options } from './optionsStorage' import { isCypress } from './standaloneUtils' import { reportWarningOnce } from './utils' @@ -5,38 +6,61 @@ import { reportWarningOnce } from './utils' let audioContext: AudioContext const sounds: Record = {} +// Track currently playing sounds and their gain nodes +const activeSounds: Array<{ + source: AudioBufferSourceNode; + gainNode: GainNode; + volumeMultiplier: number; + isMusic: boolean; +}> = [] +window.activeSounds = activeSounds + // load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded const loadingSounds = [] as string[] const convertedSounds = [] as string[] -export async function loadSound (path: string) { + +export async function loadSound (path: string, contents = path) { if (loadingSounds.includes(path)) return true loadingSounds.push(path) - const res = await window.fetch(path) - if (!res.ok) { - const error = `Failed to load sound ${path}` - if (isCypress()) throw new Error(error) - else console.warn(error) - return - } - const data = await res.arrayBuffer() - sounds[path] = data - loadingSounds.splice(loadingSounds.indexOf(path), 1) + try { + audioContext ??= new window.AudioContext() + + const res = await window.fetch(contents) + if (!res.ok) { + const error = `Failed to load sound ${path}` + if (isCypress()) throw new Error(error) + else console.warn(error) + return + } + const arrayBuffer = await res.arrayBuffer() + + // Decode the audio data immediately + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) + sounds[path] = audioBuffer + convertedSounds.push(path) // Mark as converted immediately + + loadingSounds.splice(loadingSounds.indexOf(path), 1) + } catch (err) { + console.warn(`Failed to load sound ${path}:`, err) + loadingSounds.splice(loadingSounds.indexOf(path), 1) + if (isCypress()) throw err + } } -export const loadOrPlaySound = async (url, soundVolume = 1) => { +export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => { const soundBuffer = sounds[url] if (!soundBuffer) { const start = Date.now() const cancelled = await loadSound(url) - if (cancelled || Date.now() - start > 500) return + if (cancelled || Date.now() - start > loadTimeout) return } - await playSound(url) + return playSound(url, soundVolume, loop, isMusic) } -export async function playSound (url, soundVolume = 1) { - const volume = soundVolume * (options.volume / 100) +export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) { + const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1) if (!volume) return @@ -47,12 +71,6 @@ export async function playSound (url, soundVolume = 1) { return } - for (const [soundName, sound] of Object.entries(sounds)) { - if (convertedSounds.includes(soundName)) continue - sounds[soundName] = await audioContext.decodeAudioData(sound) - convertedSounds.push(soundName) - } - const soundBuffer = sounds[url] if (!soundBuffer) { console.warn(`Sound ${url} not loaded yet`) @@ -62,8 +80,84 @@ export async function playSound (url, soundVolume = 1) { const gainNode = audioContext.createGain() const source = audioContext.createBufferSource() source.buffer = soundBuffer + source.loop = loop source.connect(gainNode) gainNode.connect(audioContext.destination) gainNode.gain.value = volume source.start(0) + + // Add to active sounds + activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic }) + + const callbacks = [] as Array<() => void> + source.onended = () => { + // Remove from active sounds when finished + const index = activeSounds.findIndex(s => s.source === source) + if (index !== -1) activeSounds.splice(index, 1) + + for (const callback of callbacks) { + callback() + } + callbacks.length = 0 + } + + return { + onEnded (callback: () => void) { + callbacks.push(callback) + }, + stop () { + try { + source.stop() + // Remove from active sounds + const index = activeSounds.findIndex(s => s.source === source) + if (index !== -1) activeSounds.splice(index, 1) + } catch (err) { + console.warn('Failed to stop sound:', err) + } + }, + gainNode, + } } + +export function stopAllSounds () { + for (const { source } of activeSounds) { + try { + source.stop() + } catch (err) { + console.warn('Failed to stop sound:', err) + } + } + activeSounds.length = 0 +} + +export function stopSound (url: string) { + const soundIndex = activeSounds.findIndex(s => s.source.buffer === sounds[url]) + if (soundIndex !== -1) { + const { source } = activeSounds[soundIndex] + try { + source.stop() + } catch (err) { + console.warn('Failed to stop sound:', err) + } + activeSounds.splice(soundIndex, 1) + } +} + +export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) { + const normalizedVolume = newVolume / 100 + for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) { + try { + gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1) + } catch (err) { + console.warn('Failed to change sound volume:', err) + } + } +} + +subscribeKey(options, 'volume', () => { + changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume) +}) + +subscribeKey(options, 'musicVolume', () => { + changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume) +}) diff --git a/src/benchmark.ts b/src/benchmark.ts new file mode 100644 index 00000000..42603a10 --- /dev/null +++ b/src/benchmark.ts @@ -0,0 +1,311 @@ +import { Vec3 } from 'vec3' +import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon' +import prettyBytes from 'pretty-bytes' +import { subscribe } from 'valtio' +import { downloadAndOpenMapFromUrl } from './downloadAndOpenFile' +import { activeModalStack, miscUiState } from './globalState' +import { disabledSettings, options } from './optionsStorage' +import { BenchmarkAdapterInfo, getAllInfoLines } from './benchmarkAdapter' +import { appQueryParams } from './appParams' +import { getScreenRefreshRate } from './utils' +import { setLoadingScreenStatus } from './appStatus' + +const DEFAULT_RENDER_DISTANCE = 5 + +interface BenchmarkFixture { + urlZip?: string + urlDir?: string[] + replayFileUrl?: string + spawn?: [number, number, number] +} + +const fixtures: Record = { + default: { + urlZip: 'https://bucket.mcraft.fun/Future CITY 4.4-slim.zip', + spawn: [-133, 87, 309] as [number, number, number], + }, + dir: { + urlDir: ['https://bucket.mcraft.fun/Greenfield%20v0.5.1/map-index.json', 'https://mcraft-proxy.vercel.app/0/bucket.mcraft.fun/Greenfield%20v0.5.1/map-index.json'], + }, + replay: { + replayFileUrl: 'https://raw.githubusercontent.com/zardoy/mcraft-fun-replays/refs/heads/main/hypepixel-tnt-lobby.worldstate.txt', + }, +} + +Error.stackTraceLimit = Error.stackTraceLimit < 30 ? 30 : Error.stackTraceLimit + +const SESSION_STORAGE_BACKUP_KEY = 'benchmark-backup' +export const openBenchmark = async (renderDistance = DEFAULT_RENDER_DISTANCE) => { + let fixtureNameOpen = appQueryParams.openBenchmark + if (!fixtureNameOpen || fixtureNameOpen === '1' || fixtureNameOpen === 'true' || fixtureNameOpen === 'zip') { + fixtureNameOpen = 'default' + } + + + if (sessionStorage.getItem(SESSION_STORAGE_BACKUP_KEY)) { + const backup = JSON.stringify(JSON.parse(sessionStorage.getItem(SESSION_STORAGE_BACKUP_KEY)!), null, 2) + setLoadingScreenStatus('Either other tab with benchmark is open or page crashed. Last data backup is downloaded. Reload page to retry.') + // download file + const a = document.createElement('a') + a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(backup) + a.download = `benchmark-${appViewer.backend?.id}.txt` + a.click() + sessionStorage.removeItem(SESSION_STORAGE_BACKUP_KEY) + return + } + + const fixture: BenchmarkFixture = appQueryParams.benchmarkMapZipUrl ? { + urlZip: appQueryParams.benchmarkMapZipUrl, + spawn: appQueryParams.benchmarkPosition ? appQueryParams.benchmarkPosition.split(',').map(Number) as [number, number, number] : fixtures.default.spawn, + } : fixtures[fixtureNameOpen] + + if (!fixture) { + setLoadingScreenStatus(`Benchmark fixture ${fixtureNameOpen} not found`) + return + } + + let memoryUsageAverage = 0 + let memoryUsageSamples = 0 + let memoryUsageWorst = 0 + setInterval(() => { + const memoryUsage = (window.performance as any)?.memory?.usedJSHeapSize + if (memoryUsage) { + memoryUsageAverage = (memoryUsageAverage * memoryUsageSamples + memoryUsage) / (memoryUsageSamples + 1) + memoryUsageSamples++ + if (memoryUsage > memoryUsageWorst) { + memoryUsageWorst = memoryUsage + } + } + }, 200) + + let mainThreadFpsAverage = 0 + let mainThreadFpsWorst = undefined as number | undefined + let mainThreadFpsSamples = 0 + let currentPassedFrames = 0 + const mainLoop = () => { + currentPassedFrames++ + requestAnimationFrame(mainLoop) + } + requestAnimationFrame(mainLoop) + setInterval(() => { + mainThreadFpsAverage = (mainThreadFpsAverage * mainThreadFpsSamples + currentPassedFrames) / (mainThreadFpsSamples + 1) + mainThreadFpsSamples++ + if (mainThreadFpsWorst === undefined) { + mainThreadFpsWorst = currentPassedFrames + } else { + mainThreadFpsWorst = Math.min(mainThreadFpsWorst, currentPassedFrames) + } + currentPassedFrames = 0 + }, 1000) + + // todo urlDir fix + let fixtureName = `${fixture.urlZip ?? fixture.urlDir?.join('|') ?? fixture.replayFileUrl ?? 'unknown'}` + if (fixture.spawn) { + fixtureName += ` - ${fixture.spawn.join(' ')}` + } + + fixtureName += ` - ${renderDistance}` + if (process.env.NODE_ENV !== 'development') { // do not delay + setLoadingScreenStatus('Benchmark requested... Getting screen refresh rate') + await new Promise(resolve => { + setTimeout(resolve, 1000) + }) + } + let start = 0 + // interval to backup data in sessionStorage in case of page crash + const saveBackupInterval = setInterval(() => { + if (!window.world) return + const backup = JSON.parse(JSON.stringify(window.benchmarkAdapter)) + backup.timePassed = ((Date.now() - start) / 1000).toFixed(2) + sessionStorage.setItem(SESSION_STORAGE_BACKUP_KEY, JSON.stringify(backup)) + }, 500) + + const screenRefreshRate = await getScreenRefreshRate() + const benchmarkAdapter: BenchmarkAdapterInfo = { + get fixture () { + return fixtureName + }, + get worldLoadTimeSeconds () { + return window.worldLoadTime + }, + get mesherWorkersCount () { + return (window.world as WorldRendererCommon).worldRendererConfig.mesherWorkers + }, + get mesherProcessAvgMs () { + return (window.world as WorldRendererCommon).workersProcessAverageTime + }, + get mesherProcessTotalMs () { + return (window.world as WorldRendererCommon).workersProcessAverageTime * (window.world as WorldRendererCommon).workersProcessAverageTimeCount + }, + get mesherProcessWorstMs () { + return (window.world as WorldRendererCommon).maxWorkersProcessTime + }, + get chunksFullInfo () { + return (window.world as WorldRendererCommon).chunksFullInfo + }, + get averageRenderTimeMs () { + return (window.world as WorldRendererCommon).renderTimeAvg + }, + get worstRenderTimeMs () { + return (window.world as WorldRendererCommon).renderTimeMax + }, + get fpsAveragePrediction () { + const avgRenderTime = (window.world as WorldRendererCommon).renderTimeAvg + return 1000 / avgRenderTime + }, + get fpsWorstPrediction () { + const maxRenderTime = (window.world as WorldRendererCommon).renderTimeMax + return 1000 / maxRenderTime + }, + get fpsAverageReal () { + return `${(window.world as WorldRendererCommon).fpsAverage.toFixed(0)} / ${screenRefreshRate}` + }, + get fpsWorstReal () { + return (window.world as WorldRendererCommon).fpsWorst ?? -1 + }, + get backendInfoReport () { + return (window.world as WorldRendererCommon).backendInfoReport + }, + get fpsAverageMainThread () { + return mainThreadFpsAverage + }, + get fpsWorstMainThread () { + return mainThreadFpsWorst ?? -1 + }, + get memoryUsageAverage () { + return prettyBytes(memoryUsageAverage) + }, + get memoryUsageWorst () { + return prettyBytes(memoryUsageWorst) + }, + get gpuInfo () { + return appViewer.rendererState.renderer + }, + get hardwareConcurrency () { + return navigator.hardwareConcurrency + }, + get userAgent () { + return navigator.userAgent + }, + clientVersion: `${process.env.RELEASE_TAG} ${process.env.BUILD_VERSION} ${process.env.RELEASE_LINK ?? ''}`, + } + window.benchmarkAdapter = benchmarkAdapter + + disabledSettings.value.add('renderDistance') + options.renderDistance = renderDistance + disabledSettings.value.add('renderDebug') + options.renderDebug = 'advanced' + disabledSettings.value.add('waitForChunksRender') + options.waitForChunksRender = false + + void downloadAndOpenMapFromUrl(fixture.urlZip, undefined, fixture.urlDir, fixture.replayFileUrl, { + connectEvents: { + serverCreated () { + if (fixture.spawn) { + localServer!.spawnPoint = new Vec3(...fixture.spawn) + localServer!.on('newPlayer', (player) => { + player.on('dataLoaded', () => { + player.position = new Vec3(...fixture.spawn!) + start = Date.now() + }) + }) + } + }, + } + }) + + document.addEventListener('cypress-world-ready', () => { + clearInterval(saveBackupInterval) + sessionStorage.removeItem(SESSION_STORAGE_BACKUP_KEY) + let stats = getAllInfoLines(window.benchmarkAdapter) + const downloadFile = () => { + // const changedSettings = + + const a = document.createElement('a') + a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(stats.join('\n')) + a.download = `benchmark-${appViewer.backend?.id}.txt` + a.click() + } + if (appQueryParams.downloadBenchmark) { + downloadFile() + } + + const panel = document.createElement('div') + panel.style.position = 'fixed' + panel.style.top = '20px' + panel.style.right = '10px' + panel.style.backgroundColor = 'rgba(0,0,0,0.8)' + panel.style.color = 'white' + panel.style.padding = '10px' + panel.style.zIndex = '1000' + panel.style.fontFamily = 'monospace' + panel.style.maxWidth = '80%' + panel.style.maxHeight = '90vh' + panel.style.overflow = 'auto' + panel.id = 'benchmark-panel' + + // Add download button + const downloadButton = document.createElement('button') + downloadButton.textContent = 'Download Results' + downloadButton.style.marginBottom = '10px' + downloadButton.style.padding = '5px 10px' + downloadButton.style.backgroundColor = '#4CAF50' + downloadButton.style.color = 'white' + downloadButton.style.border = 'none' + downloadButton.style.borderRadius = '4px' + downloadButton.style.cursor = 'pointer' + downloadButton.onclick = downloadFile + panel.appendChild(downloadButton) + + const pre = document.createElement('pre') + pre.style.whiteSpace = 'pre-wrap' + pre.style.wordBreak = 'break-word' + panel.appendChild(pre) + + pre.textContent = stats.join('\n') + const updateStats = () => { + stats = getAllInfoLines(window.benchmarkAdapter) + pre.textContent = stats.join('\n') + } + + document.body.appendChild(panel) + // setInterval(updateStats, 100) + }) +} + +// add before unload +window.addEventListener('beforeunload', () => { + // remove sessionStorage backup + sessionStorage.removeItem(SESSION_STORAGE_BACKUP_KEY) +}) + +document.addEventListener('pointerlockchange', (e) => { + const panel = document.querySelector('#benchmark-panel') + if (panel) { + panel.hidden = !!document.pointerLockElement + } +}) + +subscribe(activeModalStack, () => { + const panel = document.querySelector('#benchmark-panel') + if (panel && activeModalStack.length > 1) { + panel.hidden = true + } +}) + +export const registerOpenBenchmarkListener = () => { + if (appQueryParams.openBenchmark) { + void openBenchmark(appQueryParams.renderDistance ? +appQueryParams.renderDistance : undefined) + } + + window.addEventListener('keydown', (e) => { + if (e.code === 'KeyB' && e.shiftKey && !miscUiState.gameLoaded && activeModalStack.length === 0) { + e.preventDefault() + // add ?openBenchmark=true to url without reload + const url = new URL(window.location.href) + url.searchParams.set('openBenchmark', 'true') + window.history.replaceState({}, '', url.toString()) + void openBenchmark() + } + }) +} diff --git a/src/benchmarkAdapter.ts b/src/benchmarkAdapter.ts new file mode 100644 index 00000000..e3da6669 --- /dev/null +++ b/src/benchmarkAdapter.ts @@ -0,0 +1,66 @@ +import { noCase } from 'change-case' + +export interface BenchmarkAdapterInfo { + fixture: string + // general load info + worldLoadTimeSeconds: number + + // mesher + mesherWorkersCount: number + mesherProcessAvgMs: number + mesherProcessWorstMs: number + mesherProcessTotalMs: number + chunksFullInfo: string + + // rendering backend + averageRenderTimeMs: number + worstRenderTimeMs: number + fpsAveragePrediction: number + fpsWorstPrediction: number + fpsAverageReal: string + fpsWorstReal: number + backendInfoReport: string + + // main thread + fpsAverageMainThread: number + fpsWorstMainThread: number + + // memory total + memoryUsageAverage: string + memoryUsageWorst: string + + // context info + gpuInfo: string + hardwareConcurrency: number + userAgent: string + clientVersion: string +} + +export const getAllInfo = (adapter: BenchmarkAdapterInfo) => { + return Object.fromEntries( + Object.entries(adapter).map(([key, value]) => { + if (typeof value === 'function') { + value = (value as () => any)() + } + if (typeof value === 'number') { + value = value.toFixed(2) + } + return [noCase(key), value] + }) + ) +} + +export const getAllInfoLines = (adapter: BenchmarkAdapterInfo, delayed = false) => { + const info = getAllInfo(adapter) + if (delayed) { + for (const key in info) { + if (key !== 'fpsAveragePrediction' && key !== 'fpsAverageReal') { + delete info[key] + } + } + } + + return Object.entries(info).map(([key, value]) => { + return `${key}${delayed ? ' (delayed)' : ''}: ${value}` + }) +} diff --git a/src/botUtils.ts b/src/botUtils.ts index 5dacbf4f..10609322 100644 --- a/src/botUtils.ts +++ b/src/botUtils.ts @@ -1,119 +1,49 @@ -// this should actually be moved to mineflayer / prismarine-viewer +import { versionToNumber } from 'renderer/viewer/common/utils' +import * as nbt from 'prismarine-nbt' -import { fromFormattedString, TextComponent } from '@xmcl/text-component' - -export type MessageFormatPart = Pick & { - text: string - color?: string - bold?: boolean - italic?: boolean - underlined?: boolean - strikethrough?: boolean - obfuscated?: boolean +export const displayClientChat = (text: string) => { + const message = { + text + } + if (versionToNumber(bot.version) >= versionToNumber('1.19')) { + bot._client.emit('systemChat', { + formattedMessage: JSON.stringify(message), + position: 0, + sender: 'minecraft:chat' + }) + return + } + bot._client.emit('chat', { + message: JSON.stringify(message), + position: 0, + sender: 'minecraft:chat' + }) } -type MessageInput = { - text?: string - translate?: string - with?: Array - color?: string - bold?: boolean - italic?: boolean - underlined?: boolean - strikethrough?: boolean - obfuscated?: boolean - extra?: MessageInput[] - json?: any -} - -// todo move to sign-renderer, replace with prismarine-chat -export const formatMessage = (message: MessageInput) => { - let msglist: MessageFormatPart[] = [] - - const readMsg = (msg: MessageInput) => { - const styles = { - color: msg.color, - bold: !!msg.bold, - italic: !!msg.italic, - underlined: !!msg.underlined, - strikethrough: !!msg.strikethrough, - obfuscated: !!msg.obfuscated - } - - if (msg.text) { - msglist.push({ - ...msg, - text: msg.text, - ...styles - }) - } else if (msg.translate) { - const tText = window.loadedData.language[msg.translate] ?? msg.translate - - if (msg.with) { - const splitted = tText.split(/%s|%\d+\$s/g) - - let i = 0 - for (const [j, part] of splitted.entries()) { - msglist.push({ text: part, ...styles }) - - if (j + 1 < splitted.length) { - if (msg.with[i]) { - const msgWith = msg.with[i] - if (typeof msgWith === 'string') { - readMsg({ - ...styles, - text: msgWith - }) - } else { - readMsg({ - ...styles, - ...msgWith - }) - } - } - i++ - } - } - } else { - msglist.push({ - ...msg, - text: tText, - ...styles - }) +export const parseFormattedMessagePacket = (arg) => { + if (typeof arg === 'string') { + try { + arg = JSON.parse(arg) + return { + formatted: arg, + plain: '' } - } - - if (msg.extra) { - for (const ex of msg.extra) { - readMsg({ ...styles, ...ex }) + } catch {} + } + if (typeof arg === 'object') { + try { + return { + formatted: nbt.simplify(arg), + plain: '' + } + } catch (err) { + console.warn('Failed to parse formatted message', arg, err) + return { + plain: JSON.stringify(arg) } } } - - readMsg(message) - - const flat = (msg) => { - return [msg, msg.extra?.flatMap(flat) ?? []] + return { + plain: String(arg) } - - msglist = msglist.map(msg => { - // normalize § - if (!msg.text.includes('§')) return msg - const newMsg = fromFormattedString(msg.text) - return flat(newMsg) - }).flat(Infinity) - - return msglist -} - -const blockToItemRemaps = { - water: 'water_bucket', - lava: 'lava_bucket', - redstone_wire: 'redstone', - tripwire: 'tripwire_hook' -} - -export const getItemFromBlock = (block: import('prismarine-block').Block) => { - const item = loadedData.items[blockToItemRemaps[block.name] ?? block.name] - return item } diff --git a/src/browserfs.ts b/src/browserfs.ts index 13c266ac..006b6db8 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -7,27 +7,56 @@ import * as browserfs from 'browserfs' import { options, resetOptions } from './optionsStorage' import { fsState, loadSave } from './loadSave' -import { installTexturePack, installTexturePackFromHandle, updateTexturePackInstalledState } from './texturePack' +import { installResourcepackPack, installTexturePackFromHandle, updateTexturePackInstalledState } from './resourcePack' import { miscUiState } from './globalState' -import { setLoadingScreenStatus } from './utils' +import { setLoadingScreenStatus } from './appStatus' +import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets' +import { getFixedFilesize } from './downloadAndOpenFile' +import { packetsReplayState } from './react/state/packetsReplayState' +import { createFullScreenProgressReporter } from './core/progressReporter' +import { showNotification } from './react/NotificationProvider' +import { resetAppStorage } from './react/appStorageProvider' +import { ConnectOptions } from './connect' +const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') browserfs.install(window) const defaultMountablePoints = { - '/world': { fs: 'LocalStorage' }, // will be removed in future '/data': { fs: 'IndexedDB' }, + '/resourcepack': { fs: 'InMemory' }, // temporary storage for currently loaded resource pack + '/temp': { fs: 'InMemory' } +} +const fallbackMountablePoints = { + '/resourcepack': { fs: 'InMemory' }, // temporary storage for downloaded server resource pack + '/temp': { fs: 'InMemory' } } browserfs.configure({ fs: 'MountableFileSystem', options: defaultMountablePoints, }, async (e) => { - // todo disable singleplayer button - if (e) throw e + if (e) { + browserfs.configure({ + fs: 'MountableFileSystem', + options: fallbackMountablePoints, + }, async (e2) => { + if (e2) { + showNotification('Unknown FS error, cannot continue', e2.message, true) + throw e2 + } + showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true) + miscUiState.fsReady = true + miscUiState.singleplayerAvailable = false + }) + return + } await updateTexturePackInstalledState() - miscUiState.appLoaded = true + miscUiState.fsReady = true + miscUiState.singleplayerAvailable = true }) export const forceCachedDataPaths = {} +export const forceRedirectPaths = {} +window.fs = fs //@ts-expect-error fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mkdir', 'rmdir', 'unlink', 'rename', /* 'copyFile', */'readdir'].map(key => [key, promisify(fs[key])])), { get (target, p: string, receiver) { @@ -35,14 +64,20 @@ fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mk return (...args) => { // browser fs bug: if path doesn't start with / dirname will return . which would cause infinite loop, so we need to normalize paths if (typeof args[0] === 'string' && !args[0].startsWith('/')) args[0] = '/' + args[0] + const toRemap = Object.entries(forceRedirectPaths).find(([from]) => args[0].startsWith(from)) + if (toRemap) { + args[0] = args[0].replace(toRemap[0], toRemap[1]) + } // Write methods // todo issue one-time warning (in chat I guess) - if (fsState.isReadonly) { + const readonly = fsState.isReadonly && !(args[0].startsWith('/data') && !fsState.inMemorySave) // allow copying worlds from external providers such as zip + if (readonly) { if (oneOf(p, 'readFile', 'writeFile') && forceCachedDataPaths[args[0]]) { if (p === 'readFile') { return Promise.resolve(forceCachedDataPaths[args[0]]) } else if (p === 'writeFile') { forceCachedDataPaths[args[0]] = args[1] + console.debug('Skipped writing to readonly fs', args[0]) return Promise.resolve() } } @@ -51,7 +86,18 @@ fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mk if (p === 'open' && fsState.isReadonly) { args[1] = 'r' // read-only, zipfs throw otherwise } - return target[p](...args) + if (p === 'readFile') { + fsState.openReadOperations++ + } else if (p === 'writeFile') { + fsState.openWriteOperations++ + } + return target[p](...args).finally(() => { + if (p === 'readFile') { + fsState.openReadOperations-- + } else if (p === 'writeFile') { + fsState.openWriteOperations-- + } + }) } } }) @@ -68,7 +114,18 @@ fs.promises.open = async (...args) => { return } + if (x === 'read') { + fsState.openReadOperations++ + } else if (x === 'write' || x === 'close') { + fsState.openWriteOperations++ + } fs[x](fd, ...args, (err, bytesRead, buffer) => { + if (x === 'read') { + fsState.openReadOperations-- + } else if (x === 'write' || x === 'close') { + // todo that's not correct + fsState.openWriteOperations-- + } if (err) throw err // todo if readonly probably there is no need to open at all (return some mocked version - check reload)? if (x === 'write' && !fsState.isReadonly) { @@ -176,7 +233,37 @@ export const mountExportFolder = async () => { return true } -export async function removeFileRecursiveAsync (path) { +let googleDriveFileSystem + +/** Only cached! */ +export const googleDriveGetFileIdFromPath = (path: string) => { + return googleDriveFileSystem._getExistingFileId(path) +} + +export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string) => { + googleDriveFileSystem = new GoogleDriveFileSystem() + googleDriveFileSystem.rootDirId = rootId + googleDriveFileSystem.isReadonly = readonly + await new Promise(resolve => { + browserfs.configure({ + fs: 'MountableFileSystem', + options: { + ...defaultMountablePoints, + '/google': googleDriveFileSystem + }, + }, (e) => { + if (e) throw e + resolve() + }) + }) + fsState.isReadonly = readonly + fsState.syncFs = false + fsState.inMemorySave = false + fsState.remoteBackend = true + return true +} + +export async function removeFileRecursiveAsync (path, removeDirectoryItself = true) { const errors = [] as Array<[string, Error]> try { const files = await fs.promises.readdir(path) @@ -195,7 +282,9 @@ export async function removeFileRecursiveAsync (path) { })) // After removing all files/directories, remove the current directory - await fs.promises.rmdir(path) + if (removeDirectoryItself) { + await fs.promises.rmdir(path) + } } catch (error) { errors.push([path, error]) } @@ -253,6 +342,7 @@ export const openWorldDirectory = async (dragndropHandle?: FileSystemDirectoryHa fsState.isReadonly = !writeAccess fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = false await loadSave() } @@ -292,7 +382,45 @@ export const possiblyCleanHandle = (callback = () => { }) => { } } -export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string) => { +const readdirSafe = async (path: string) => { + try { + return await fs.promises.readdir(path) + } catch (err) { + return null + } +} + +export const collectFilesToCopy = async (basePath: string, safe = false): Promise => { + const result: string[] = [] + const countFiles = async (relPath: string) => { + const resolvedPath = join(basePath, relPath) + const files = relPath === '.' && !safe ? await fs.promises.readdir(resolvedPath) : await readdirSafe(resolvedPath) + if (!files) return null + await Promise.all(files.map(async file => { + const res = await countFiles(join(relPath, file)) + if (res === null) { + // is file + result.push(join(relPath, file)) + } + })) + } + await countFiles('.') + return result +} + +export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true, addMsg = '') => { + const stat = await existsViaStats(pathSrc) + if (!stat) { + if (throwRootNotExist) throw new Error(`Cannot copy. Source directory ${pathSrc} does not exist`) + console.debug('source directory does not exist', pathSrc) + return + } + if (!stat.isDirectory()) { + await fs.promises.writeFile(pathDest, await fs.promises.readFile(pathSrc) as any) + console.debug('copied single file', pathSrc, pathDest) + return + } + try { setLoadingScreenStatus('Copying files') let filesCount = 0 @@ -309,21 +437,48 @@ export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: stri } })) } + console.debug('Counting files', pathSrc) await countFiles(pathSrc) + console.debug('counted', filesCount) let copied = 0 await copyFilesAsync(pathSrc, pathDest, (name) => { copied++ - setLoadingScreenStatus(`Copying files (${copied}/${filesCount}) ${name}...`) + setLoadingScreenStatus(`Copying files${addMsg} (${copied}/${filesCount}): ${name}`) }) } finally { setLoadingScreenStatus(undefined) } } +export const existsViaStats = async (path: string) => { + try { + return await fs.promises.stat(path) + } catch (e) { + return false + } +} + +export const fileExistsAsyncOptimized = async (path: string) => { + try { + await fs.promises.readdir(path) + } catch (err) { + if (err.code === 'ENOTDIR') return true + // eslint-disable-next-line sonarjs/prefer-single-boolean-return + if (err.code === 'ENOENT') return false + // throw err + return false + } + return true +} + export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopied?: (name) => void) => { // query: can't use fs.copy! use fs.promises.writeFile and readFile const files = await fs.promises.readdir(pathSrc) + if (!await existsViaStats(pathDest)) { + await fs.promises.mkdir(pathDest, { recursive: true }) + } + // Use Promise.all to parallelize file/directory copying await Promise.all(files.map(async (file) => { const curPathSrc = join(pathSrc, file) @@ -335,14 +490,78 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi await copyFilesAsync(curPathSrc, curPathDest, fileCopied) } else { // Copy file - await fs.promises.writeFile(curPathDest, await fs.promises.readFile(curPathSrc)) - fileCopied?.(file) + try { + await fs.promises.writeFile(curPathDest, await fs.promises.readFile(curPathSrc) as any) + console.debug('copied file', curPathSrc, curPathDest) + } catch (err) { + console.error('Error copying file', curPathSrc, curPathDest, err) + throw err + } + fileCopied?.(curPathDest) } })) } +export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | undefined */, baseUrlParam) => { + // todo try go guess mode + let index + let baseUrl + for (const url of fileDescriptorUrls) { + let file + try { + setLoadingScreenStatus(`Trying to get world descriptor from ${new URL(url).host}`) + const controller = new AbortController() + setTimeout(() => { + controller.abort() + }, 3000) + // eslint-disable-next-line no-await-in-loop + const response = await fetch(url, { signal: controller.signal }) + // eslint-disable-next-line no-await-in-loop + file = await response.json() + } catch (err) { + console.error('Error fetching file descriptor', url, err) + } + if (!file) continue + if (file.baseUrl) { + baseUrl = new URL(file.baseUrl, baseUrl).toString() + index = file.index + } else { + index = file + baseUrl = baseUrlParam ?? url.split('/').slice(0, -1).join('/') + } + break + } + if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrls.join(', ')}`) + await new Promise(async resolve => { + browserfs.configure({ + fs: 'MountableFileSystem', + options: { + ...defaultMountablePoints, + '/world': { + fs: 'HTTPRequest', + options: { + index, + baseUrl + } + } + }, + }, (e) => { + if (e) throw e + resolve() + }) + }) + + fsState.saveLoaded = false + fsState.isReadonly = true + fsState.syncFs = false + fsState.inMemorySave = false + fsState.remoteBackend = true + + await loadSave() +} + // todo rename method -const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) => { +const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name'], connectOptions?: Partial) => { await new Promise(async resolve => { browserfs.configure({ // todo @@ -367,6 +586,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) fsState.isReadonly = true fsState.syncFs = true fsState.inMemorySave = false + fsState.remoteBackend = false if (fs.existsSync('/world/level.dat')) { await loadSave() @@ -386,7 +606,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) } if (availableWorlds.length === 1) { - await loadSave(`/world/${availableWorlds[0]}`) + await loadSave(`/world/${availableWorlds[0]}`, connectOptions) return } @@ -404,44 +624,46 @@ export const openWorldZip = async (...args: Parameters } } -export const resetLocalStorageWorld = () => { - for (const key of Object.keys(localStorage)) { - if (/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) || key === '/') { - localStorage.removeItem(key) - } - } -} - -export const resetLocalStorageWithoutWorld = () => { - for (const key of Object.keys(localStorage)) { - if (!/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) && key !== '/') { - localStorage.removeItem(key) - } - } +export const resetLocalStorage = () => { resetOptions() + resetAppStorage() } -window.resetLocalStorageWorld = resetLocalStorageWorld +window.resetLocalStorage = resetLocalStorage + export const openFilePicker = (specificCase?: 'resourcepack') => { // create and show input picker let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')! if (!picker) { picker = document.createElement('input') picker.type = 'file' - picker.accept = '.zip' + picker.accept = specificCase ? '.zip' : [...VALID_REPLAY_EXTENSIONS, '.zip'].join(',') picker.addEventListener('change', () => { const file = picker.files?.[0] picker.value = '' if (!file) return - if (!file.name.endsWith('.zip')) { - const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? Only .zip files are supported. Continue?`) - if (!doContinue) return - } if (specificCase === 'resourcepack') { - void installTexturePack(file) + if (!file.name.endsWith('.zip')) { + const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? ONLY .zip files are supported. Continue?`) + if (!doContinue) return + } + void installResourcepackPack(file, createFullScreenProgressReporter()).catch((err) => { + setLoadingScreenStatus(err.message, true) + }) } else { - void openWorldZip(file) + // eslint-disable-next-line no-lonely-if + if (VALID_REPLAY_EXTENSIONS.some(ext => file.name.endsWith(ext)) || file.name.startsWith('packets-replay')) { + void file.text().then(contents => { + openFile({ + contents, + filename: file.name, + filesize: file.size + }) + }) + } else { + void openWorldZip(file) + } } }) picker.hidden = true diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index e68daa03..a292c5cd 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -1,11 +1,14 @@ import fs from 'fs' import { join } from 'path' import JSZip from 'jszip' -import { readLevelDat } from './loadSave' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { fsState, readLevelDat } from './loadSave' import { closeWan, openToWanAndCopyJoinLink } from './localServerMultiplayer' import { copyFilesAsync, uniqueFileNameFromWorldName } from './browserfs' import { saveServer } from './flyingSquidUtils' -import { setLoadingScreenStatus } from './utils' +import { setLoadingScreenStatus } from './appStatus' +import { displayClientChat } from './botUtils' +import { miscUiState } from './globalState' const notImplemented = () => { return 'Not implemented yet' @@ -67,7 +70,7 @@ export const exportWorld = async (path: string, type: 'zip' | 'folder', zipName // todo include in help const exportLoadedWorld = async () => { await saveServer() - let { worldFolder } = localServer!.options + let worldFolder = fsState.inMemorySavePath if (!worldFolder.startsWith('/')) worldFolder = `/${worldFolder}` await exportWorld(worldFolder, 'zip') } @@ -75,14 +78,13 @@ const exportLoadedWorld = async () => { window.exportWorld = exportLoadedWorld const writeText = (text) => { - bot._client.emit('chat', { - message: JSON.stringify({ text }) - }) + displayClientChat(text) } -const commands: Array<{ +export const commands: Array<{ command: string[], - invoke (): Promise | void + alwaysAvailable?: boolean, + invoke (args: string[]): Promise | void //@ts-format-ignore-region }> = [ { @@ -107,19 +109,47 @@ const commands: Array<{ command: ['/save'], async invoke () { await saveServer(false) + writeText('Saved to browser memory') + } + }, + { + command: ['/pos'], + alwaysAvailable: true, + async invoke ([type]) { + let pos: { x: number, y: number, z: number } | undefined + if (type === 'block') { + const blockPos = window.cursorBlockRel()?.position + if (blockPos) { + pos = { x: blockPos.x, y: blockPos.y, z: blockPos.z } + } + } else { + const playerPos = bot.entity.position + pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z } + } + if (!pos) return + const formatted = `${pos.x.toFixed(2)} ${pos.y.toFixed(2)} ${pos.z.toFixed(2)}` + await navigator.clipboard.writeText(formatted) + writeText(`Copied position to clipboard: ${formatted}`) + } + }, + { + command: ['/mesherlog'], + alwaysAvailable: true, + invoke () { + getThreeJsRendererMethods()?.downloadMesherLog() } } ] //@ts-format-ignore-endregion -export const getBuiltinCommandsList = () => commands.flatMap(command => command.command) +export const getBuiltinCommandsList = () => commands.filter(command => command.alwaysAvailable || miscUiState.singleplayer).flatMap(command => command.command) -export const tryHandleBuiltinCommand = (message) => { - if (!localServer) return +export const tryHandleBuiltinCommand = (message: string) => { + const [userCommand, ...args] = message.split(' ') - for (const command of commands) { - if (command.command.includes(message)) { - void command.invoke() // ignoring for now + for (const command of commands.filter(command => command.alwaysAvailable || miscUiState.singleplayer)) { + if (command.command.includes(userCommand)) { + void command.invoke(args) // ignoring for now return true } } diff --git a/src/cameraRotationControls.ts b/src/cameraRotationControls.ts new file mode 100644 index 00000000..679a3a44 --- /dev/null +++ b/src/cameraRotationControls.ts @@ -0,0 +1,83 @@ +import { contro } from './controls' +import { activeModalStack, isGameActive, miscUiState, showModal } from './globalState' +import { options } from './optionsStorage' +import { hideNotification, notificationProxy } from './react/NotificationProvider' +import { pointerLock } from './utils' +import { updateMotion, initMotionTracking } from './react/uiMotion' + +let lastMouseMove: number + +export type CameraMoveEvent = { + movementX: number + movementY: number + type: string + stopPropagation?: () => void +} + +export function onCameraMove (e: MouseEvent | CameraMoveEvent) { + if (!isGameActive(true)) return + if (e.type === 'mousemove' && !document.pointerLockElement) return + e.stopPropagation?.() + if (appViewer.playerState.utils.isSpectatingEntity()) return + const now = performance.now() + // todo: limit camera movement for now to avoid unexpected jumps + if (now - lastMouseMove < 4 && !options.preciseMouseInput) return + lastMouseMove = now + let { mouseSensX, mouseSensY } = options + if (mouseSensY === -1) mouseSensY = mouseSensX + moveCameraRawHandler({ + x: e.movementX * mouseSensX * 0.0001, + y: e.movementY * mouseSensY * 0.0001 + }) + bot.mouse.update() + updateMotion() +} + +export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => { + const maxPitch = 0.5 * Math.PI + const minPitch = -0.5 * Math.PI + + appViewer.lastCamUpdate = Date.now() + + // if (viewer.world.freeFlyMode) { + // // Update freeFlyState directly + // viewer.world.freeFlyState.yaw = (viewer.world.freeFlyState.yaw - x) % (2 * Math.PI) + // viewer.world.freeFlyState.pitch = Math.max(minPitch, Math.min(maxPitch, viewer.world.freeFlyState.pitch - y)) + // return + // } + + if (!bot?.entity) return + const pitch = bot.entity.pitch - y + void bot.look(bot.entity.yaw - x, Math.max(minPitch, Math.min(maxPitch, pitch)), true) + appViewer.backend?.updateCamera(null, bot.entity.yaw, pitch) +} + +window.addEventListener('mousemove', (e: MouseEvent) => { + onCameraMove(e) +}, { capture: true }) + +export const onControInit = () => { + contro.on('stickMovement', ({ stick, vector }) => { + if (!isGameActive(true)) return + if (stick !== 'right') return + let { x, z } = vector + if (Math.abs(x) < 0.18) x = 0 + if (Math.abs(z) < 0.18) z = 0 + onCameraMove({ + movementX: x * 10, + movementY: z * 10, + type: 'stickMovement', + stopPropagation () {} + } as CameraMoveEvent) + miscUiState.usingGamepadInput = true + }) +} + +function pointerLockChangeCallback () { + if (appViewer.rendererState.preventEscapeMenu) return + if (!pointerLock.hasPointerLock && activeModalStack.length === 0 && miscUiState.gameLoaded) { + showModal({ reactType: 'pause-screen' }) + } +} + +document.addEventListener('pointerlockchange', pointerLockChangeCallback, false) diff --git a/src/botUtils.test.ts b/src/chatUtils.test.ts similarity index 63% rename from src/botUtils.test.ts rename to src/chatUtils.test.ts index 91531138..6d683919 100644 --- a/src/botUtils.test.ts +++ b/src/chatUtils.test.ts @@ -1,9 +1,9 @@ import { test, expect } from 'vitest' import mcData from 'minecraft-data' -import { formatMessage } from './botUtils' +import { formatMessage, isAllowedChatCharacter, isStringAllowed } from './chatUtils' -globalThis.window ??= {} as any -globalThis.window.loadedData ??= mcData('1.20.1') +//@ts-expect-error +globalThis.loadedData ??= mcData('1.20.1') const mapIncludeDefined = (props) => { return (x) => { @@ -64,3 +64,21 @@ test('formatMessage', () => { ] `) }) + +test('isAllowedChatCharacter', () => { + expect(isAllowedChatCharacter('a')).toBe(true) + expect(isAllowedChatCharacter('a')).toBe(true) + expect(isAllowedChatCharacter('§')).toBe(false) + expect(isAllowedChatCharacter(' ')).toBe(true) + expect(isStringAllowed('a§b')).toMatchObject({ + valid: false, + clean: 'ab', + invalid: ['§'] + }) + expect(isStringAllowed('aツ')).toMatchObject({ + valid: true, + }) + expect(isStringAllowed('a🟢')).toMatchObject({ + valid: true, + }) +}) diff --git a/src/chatUtils.ts b/src/chatUtils.ts new file mode 100644 index 00000000..849d5847 --- /dev/null +++ b/src/chatUtils.ts @@ -0,0 +1,173 @@ +// this should actually be moved to mineflayer / renderer + +import { fromFormattedString, TextComponent } from '@xmcl/text-component' +import type { IndexedData } from 'minecraft-data' +import { versionToNumber } from 'renderer/viewer/common/utils' + +export interface MessageFormatOptions { + doShadow?: boolean +} + +export type MessageFormatPart = Pick & { + text: string + color?: string + bold?: boolean + italic?: boolean + underlined?: boolean + strikethrough?: boolean + obfuscated?: boolean +} + +type MessageInput = { + text?: string + translate?: string + with?: Array + color?: string + bold?: boolean + italic?: boolean + underlined?: boolean + strikethrough?: boolean + obfuscated?: boolean + extra?: MessageInput[] + json?: any +} + +const global = globalThis as any + +// todo move to sign-renderer, replace with prismarine-chat, fix mcData issue! +export const formatMessage = (message: MessageInput, mcData: IndexedData = global.loadedData) => { + let msglist: MessageFormatPart[] = [] + + const readMsg = (msg: MessageInput) => { + const styles = { + color: msg.color, + bold: !!msg.bold, + italic: !!msg.italic, + underlined: !!msg.underlined, + strikethrough: !!msg.strikethrough, + obfuscated: !!msg.obfuscated + } + + if (!msg.text && typeof msg.json?.[''] === 'string') msg.text = msg.json[''] + if (msg.text) { + msglist.push({ + ...msg, + text: msg.text, + ...styles + }) + } else if (msg.translate) { + const tText = mcData?.language[msg.translate] ?? msg.translate + + if (msg.with) { + const splitted = tText.split(/%s|%\d+\$s/g) + + let i = 0 + for (const [j, part] of splitted.entries()) { + msglist.push({ text: part, ...styles }) + + if (j + 1 < splitted.length) { + if (msg.with[i]) { + const msgWith = msg.with[i] + if (typeof msgWith === 'string') { + readMsg({ + ...styles, + text: msgWith + }) + } else { + readMsg({ + ...styles, + ...msgWith + }) + } + } + i++ + } + } + } else { + msglist.push({ + ...msg, + text: tText, + ...styles + }) + } + } + + if (msg.extra) { + for (let ex of msg.extra) { + if (typeof ex === 'string') { + ex = { text: ex } + } + readMsg({ ...styles, ...ex }) + } + } + } + + readMsg(message) + + const flat = (msg) => { + return [msg, msg.extra?.flatMap(flat) ?? []] + } + + msglist = msglist.map(msg => { + // normalize § + if (!msg.text.includes?.('§')) return msg + const newMsg = fromFormattedString(msg.text) + return flat(newMsg) + }).flat(Infinity) + + return msglist +} + +export const messageToString = (message: MessageInput | string) => { + if (typeof message === 'string') { + return message + } + const msglist = formatMessage(message) + return msglist.map(msg => msg.text).join('') +} + +const blockToItemRemaps = { + water: 'water_bucket', + lava: 'lava_bucket', + redstone_wire: 'redstone', + tripwire: 'tripwire_hook' +} + +export const getItemFromBlock = (block: import('prismarine-block').Block) => { + const item = global.loadedData.itemsByName[blockToItemRemaps[block.name] ?? block.name] + return item +} + +export function isAllowedChatCharacter (char: string): boolean { + // if (char.length !== 1) { + // throw new Error('Input must be a single character') + // } + + const charCode = char.codePointAt(0)! + return charCode !== 167 && charCode >= 32 && charCode !== 127 +} + +export const isStringAllowed = (str: string) => { + const invalidChars = new Set() + for (const [i, char] of [...str].entries()) { + const isSurrogatePair = str.codePointAt(i) !== str['charCodeAt'](i) + if (isSurrogatePair) continue + + if (!isAllowedChatCharacter(char)) { + invalidChars.add(char) + } + } + + const valid = invalidChars.size === 0 + if (valid) { + return { + valid: true + } + } + + return { + valid, + clean: [...str].filter(c => !invalidChars.has(c)).join(''), + invalid: [...invalidChars] + } +} diff --git a/src/clientMods.ts b/src/clientMods.ts new file mode 100644 index 00000000..204a5861 --- /dev/null +++ b/src/clientMods.ts @@ -0,0 +1,637 @@ +/* eslint-disable no-await-in-loop */ +import { openDB } from 'idb' +import * as React from 'react' +import * as valtio from 'valtio' +import * as valtioUtils from 'valtio/utils' +import { gt } from 'semver' +import { proxy } from 'valtio' +import { options } from './optionsStorage' +import { appStorage } from './react/appStorageProvider' +import { showInputsModal, showOptionsModal } from './react/SelectOption' +import { ProgressReporter } from './core/progressReporter' +import { showNotification } from './react/NotificationProvider' + +let sillyProtection = false +const protectRuntime = () => { + if (sillyProtection) return + sillyProtection = true + const sensetiveKeys = new Set(['authenticatedAccounts', 'serversList', 'username']) + const proxy = new Proxy(window.localStorage, { + get (target, prop) { + if (typeof prop === 'string') { + if (sensetiveKeys.has(prop)) { + console.warn(`Access to sensitive key "${prop}" was blocked`) + return null + } + if (prop === 'getItem') { + return (key: string) => { + if (sensetiveKeys.has(key)) { + console.warn(`Access to sensitive key "${key}" via getItem was blocked`) + return null + } + return target.getItem(key) + } + } + if (prop === 'setItem') { + return (key: string, value: string) => { + if (sensetiveKeys.has(key)) { + console.warn(`Attempt to set sensitive key "${key}" via setItem was blocked`) + return + } + target.setItem(key, value) + } + } + if (prop === 'removeItem') { + return (key: string) => { + if (sensetiveKeys.has(key)) { + console.warn(`Attempt to delete sensitive key "${key}" via removeItem was blocked`) + return + } + target.removeItem(key) + } + } + if (prop === 'clear') { + console.warn('Attempt to clear localStorage was blocked') + return () => {} + } + } + return Reflect.get(target, prop) + }, + set (target, prop, value) { + if (typeof prop === 'string' && sensetiveKeys.has(prop)) { + console.warn(`Attempt to set sensitive key "${prop}" was blocked`) + return false + } + return Reflect.set(target, prop, value) + }, + deleteProperty (target, prop) { + if (typeof prop === 'string' && sensetiveKeys.has(prop)) { + console.warn(`Attempt to delete sensitive key "${prop}" was blocked`) + return false + } + return Reflect.deleteProperty(target, prop) + } + }) + Object.defineProperty(window, 'localStorage', { + value: proxy, + writable: false, + configurable: false, + }) +} + +// #region Database +const dbPromise = openDB('mods-db', 1, { + upgrade (db) { + db.createObjectStore('mods', { + keyPath: 'name', + }) + db.createObjectStore('repositories', { + keyPath: 'url', + }) + }, +}) + +export interface ModSetting { + label?: string + type: 'toggle' | 'choice' | 'input' | 'slider' + hidden?: boolean + values?: string[] + inputType?: string + hint?: string + default?: any +} + +export interface ModSettingsDict { + [settingId: string]: ModSetting +} + +export interface ModAction { + method?: string + label?: string + /** @default false */ + gameGlobal?: boolean + /** @default false */ + onlyForeground?: boolean +} + +// mcraft-repo.json +export interface McraftRepoFile { + packages: ClientModDefinition[] + /** @default true */ + prefix?: string | boolean + name?: string // display name + description?: string + mirrorUrls?: string[] + autoUpdateOverride?: boolean + lastUpdated?: number +} +export interface Repository extends McraftRepoFile { + url: string +} + +export interface ClientMod { + name: string; // unique identifier like owner.name + version: string + enabled?: boolean + + scriptMainUnstable?: string; + serverPlugin?: string + // serverPlugins?: string[] + // mesherThread?: string + stylesGlobal?: string + threeJsBackend?: string // three.js + // stylesLocal?: string + + requiresNetwork?: boolean + fullyOffline?: boolean + description?: string + author?: string + section?: string + autoUpdateOverride?: boolean + lastUpdated?: number + wasModifiedLocally?: boolean + // todo depends, hashsum + + settings?: ModSettingsDict + actionsMain?: Record +} + +const cleanupFetchedModData = (mod: ClientModDefinition | Record) => { + delete mod['enabled'] + delete mod['repo'] + delete mod['autoUpdateOverride'] + delete mod['lastUpdated'] + delete mod['wasModifiedLocally'] + return mod +} + +export type ClientModDefinition = Omit & { + scriptMainUnstable?: boolean + stylesGlobal?: boolean + serverPlugin?: boolean + threeJsBackend?: boolean +} + +export async function saveClientModData (data: ClientMod) { + const db = await dbPromise + data.lastUpdated = Date.now() + await db.put('mods', data) + modsReactiveUpdater.counter++ +} + +async function getPlugin (name: string) { + const db = await dbPromise + return db.get('mods', name) as Promise +} + +export async function getAllMods () { + const db = await dbPromise + return db.getAll('mods') as Promise +} + +async function deletePlugin (name) { + const db = await dbPromise + await db.delete('mods', name) + modsReactiveUpdater.counter++ +} + +async function removeAllMods () { + const db = await dbPromise + await db.clear('mods') + modsReactiveUpdater.counter++ +} + +// --- + +async function saveRepository (data: Repository) { + const db = await dbPromise + data.lastUpdated = Date.now() + await db.put('repositories', data) +} + +async function getRepository (url: string) { + const db = await dbPromise + return db.get('repositories', url) as Promise +} + +async function getAllRepositories () { + const db = await dbPromise + return db.getAll('repositories') as Promise +} +window.getAllRepositories = getAllRepositories + +async function deleteRepository (url) { + const db = await dbPromise + await db.delete('repositories', url) +} + +// --- + +// #endregion + +window.mcraft = { + version: process.env.RELEASE_TAG, + build: process.env.BUILD_VERSION, + ui: {}, + React, + valtio: { + ...valtio, + ...valtioUtils, + }, + // openDB +} + +const activateMod = async (mod: ClientMod, reason: string) => { + if (mod.enabled === false) return false + protectRuntime() + console.debug(`Activating mod ${mod.name} (${reason})...`) + window.loadedMods ??= {} + if (window.loadedMods[mod.name]) { + console.warn(`Mod is ${mod.name} already loaded, skipping activation...`) + return false + } + if (mod.stylesGlobal) { + const style = document.createElement('style') + style.textContent = mod.stylesGlobal + style.id = `mod-${mod.name}` + document.head.appendChild(style) + } + if (mod.scriptMainUnstable) { + const blob = new Blob([mod.scriptMainUnstable], { type: 'text/javascript' }) + const url = URL.createObjectURL(blob) + // eslint-disable-next-line no-useless-catch + try { + const module = await import(/* webpackIgnore: true */ url) + module.default?.(structuredClone(mod), { settings: getModSettingsProxy(mod) }) + window.loadedMods[mod.name] ??= {} + window.loadedMods[mod.name].mainUnstableModule = module + } catch (e) { + throw e + } + URL.revokeObjectURL(url) + } + if (mod.threeJsBackend) { + const blob = new Blob([mod.threeJsBackend], { type: 'text/javascript' }) + const url = URL.createObjectURL(blob) + // eslint-disable-next-line no-useless-catch + try { + const module = await import(/* webpackIgnore: true */ url) + // todo + window.loadedMods[mod.name] ??= {} + // for accessing global world var + window.loadedMods[mod.name].threeJsBackendModule = module + } catch (e) { + throw e + } + URL.revokeObjectURL(url) + } + mod.enabled = true + return true +} + +export const appStartup = async () => { + void checkModsUpdates() + + const mods = await getAllMods() + for (const mod of mods) { + await activateMod(mod, 'autostart').catch(e => { + modsErrors[mod.name] ??= [] + modsErrors[mod.name].push(`startup: ${String(e)}`) + console.error(`Error activating mod on startup ${mod.name}:`, e) + }) + } +} + +export const modsUpdateStatus = proxy({} as Record) +export const modsWaitingReloadStatus = proxy({} as Record) +export const modsErrors = proxy({} as Record) + +const normalizeRepoUrl = (url: string) => { + if (url.startsWith('https://')) return url + if (url.startsWith('http://')) return url + if (url.startsWith('//')) return `https:${url}` + return `https://raw.githubusercontent.com/${url}/master` +} + +const installOrUpdateMod = async (repo: Repository, mod: ClientModDefinition, activate = true, progress?: ProgressReporter) => { + // eslint-disable-next-line no-useless-catch + try { + const fetchData = async (urls: string[]) => { + const errored = [] as string[] + // eslint-disable-next-line no-unreachable-loop + for (const urlTemplate of urls) { + const modNameOnly = mod.name.split('.').pop() + const modFolder = repo.prefix === false ? modNameOnly : typeof repo.prefix === 'string' ? `${repo.prefix}/${modNameOnly}` : mod.name + const url = new URL(`${modFolder}/${urlTemplate}`, normalizeRepoUrl(repo.url).replace(/\/$/, '') + '/').href + // eslint-disable-next-line no-useless-catch + try { + const response = await fetch(url) + if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`) + return await response.text() + } catch (e) { + // errored.push(String(e)) + throw e + } + } + console.warn(`[${mod.name}] Error installing component of ${urls[0]}: ${errored.join(', ')}`) + return undefined + } + if (mod.stylesGlobal) { + await progress?.executeWithMessage( + `Downloading ${mod.name} styles`, + async () => { + mod.stylesGlobal = await fetchData(['global.css']) as any + } + ) + } + if (mod.scriptMainUnstable) { + await progress?.executeWithMessage( + `Downloading ${mod.name} script`, + async () => { + mod.scriptMainUnstable = await fetchData(['mainUnstable.js']) as any + } + ) + } + if (mod.threeJsBackend) { + await progress?.executeWithMessage( + `Downloading ${mod.name} three.js backend`, + async () => { + mod.threeJsBackend = await fetchData(['three.js']) as any + } + ) + } + if (mod.serverPlugin) { + if (mod.name.endsWith('.disabled')) throw new Error(`Mod name ${mod.name} can't end with .disabled`) + await progress?.executeWithMessage( + `Downloading ${mod.name} server plugin`, + async () => { + mod.serverPlugin = await fetchData(['serverPlugin.js']) as any + } + ) + } + if (activate) { + // todo try to de-activate mod if it's already loaded + if (window.loadedMods?.[mod.name]) { + modsWaitingReloadStatus[mod.name] = true + } else { + await activateMod(mod as ClientMod, 'install') + } + } + await saveClientModData(mod as ClientMod) + delete modsUpdateStatus[mod.name] + } catch (e) { + // console.error(`Error installing mod ${mod.name}:`, e) + throw e + } +} + +const checkRepositoryUpdates = async (repo: Repository) => { + for (const mod of repo.packages) { + + const modExisting = await getPlugin(mod.name) + if (modExisting?.version && gt(mod.version, modExisting.version)) { + modsUpdateStatus[mod.name] = [modExisting.version, mod.version] + if (options.modsAutoUpdate === 'always' && (!repo.autoUpdateOverride && !modExisting.autoUpdateOverride)) { + void installOrUpdateMod(repo, mod).catch(e => { + console.error(`Error updating mod ${mod.name}:`, e) + }) + } + } + } + +} + +export const fetchRepository = async (urlOriginal: string, url: string, hasMirrors = false) => { + const fetchUrl = normalizeRepoUrl(url).replace(/\/$/, '') + '/mcraft-repo.json' + try { + const response = await fetch(fetchUrl).then(async res => res.json()) + if (!response.packages) throw new Error(`No packages field in the response json of the repository: ${fetchUrl}`) + response.autoUpdateOverride = (await getRepository(urlOriginal))?.autoUpdateOverride + response.url = urlOriginal + void saveRepository(response) + modsReactiveUpdater.counter++ + return true + } catch (e) { + console.warn(`Error fetching repository (trying other mirrors) ${url}:`, e) + return false + } +} + +export const fetchAllRepositories = async () => { + const repositories = await getAllRepositories() + await Promise.all(repositories.map(async (repo) => { + const allUrls = [repo.url, ...(repo.mirrorUrls || [])] + for (const [i, url] of allUrls.entries()) { + const isLast = i === allUrls.length - 1 + + if (await fetchRepository(repo.url, url, !isLast)) break + } + })) + appStorage.modsAutoUpdateLastCheck = Date.now() +} + +const checkModsUpdates = async () => { + await autoRefreshModRepositories() + for (const repo of await getAllRepositories()) { + + await checkRepositoryUpdates(repo) + } +} + +const autoRefreshModRepositories = async () => { + if (options.modsAutoUpdate === 'never') return + const lastCheck = appStorage.modsAutoUpdateLastCheck + if (lastCheck && Date.now() - lastCheck < 1000 * 60 * 60 * options.modsUpdatePeriodCheck) return + await fetchAllRepositories() + // todo think of not updating check timestamp on offline access +} + +export const installModByName = async (repoUrl: string, name: string, progress?: ProgressReporter) => { + progress?.beginStage('main', `Installing ${name}`) + const repo = await getRepository(repoUrl) + if (!repo) throw new Error(`Repository ${repoUrl} not found`) + const mod = repo.packages.find(m => m.name === name) + if (!mod) throw new Error(`Mod ${name} not found in repository ${repoUrl}`) + await installOrUpdateMod(repo, mod, undefined, progress) + progress?.endStage('main') +} + +export const uninstallModAction = async (name: string) => { + const choice = await showOptionsModal(`Uninstall mod ${name}?`, ['Yes']) + if (!choice) return + await deletePlugin(name) + window.loadedMods ??= {} + if (window.loadedMods[name]) { + // window.loadedMods[name].default?.(null) + delete window.loadedMods[name] + modsWaitingReloadStatus[name] = true + } + // Clear any errors associated with the mod + delete modsErrors[name] +} + +export const setEnabledModAction = async (name: string, newEnabled: boolean) => { + const mod = await getPlugin(name) + if (!mod) throw new Error(`Mod ${name} not found`) + if (newEnabled) { + mod.enabled = true + if (!window.loadedMods?.[mod.name]) { + await activateMod(mod, 'manual') + } + } else { + // todo deactivate mod + mod.enabled = false + if (window.loadedMods?.[mod.name]) { + if (window.loadedMods[mod.name]?.threeJsBackendModule) { + window.loadedMods[mod.name].threeJsBackendModule.deactivate() + delete window.loadedMods[mod.name].threeJsBackendModule + } + if (window.loadedMods[mod.name]?.mainUnstableModule) { + window.loadedMods[mod.name].mainUnstableModule.deactivate() + delete window.loadedMods[mod.name].mainUnstableModule + } + + if (Object.keys(window.loadedMods[mod.name]).length === 0) { + delete window.loadedMods[mod.name] + } + } + } + await saveClientModData(mod) +} + +export const modsReactiveUpdater = proxy({ + counter: 0 +}) + +export const getAllModsDisplayList = async () => { + const repos = await getAllRepositories() + const installedMods = await getAllMods() + const modsWithoutRepos = installedMods.filter(mod => !repos.some(repo => repo.packages.some(m => m.name === mod.name))) + const mapMods = (mapMods: ClientMod[]) => mapMods.map(mod => ({ + ...mod, + installed: installedMods.find(m => m.name === mod.name), + activated: !!window.loadedMods?.[mod.name], + installedVersion: installedMods.find(m => m.name === mod.name)?.version, + canBeActivated: mod.scriptMainUnstable || mod.stylesGlobal, + })) + return { + repos: repos.map(repo => ({ + ...repo, + packages: mapMods(repo.packages as ClientMod[]), + })), + modsWithoutRepos: mapMods(modsWithoutRepos), + } +} + +export const removeRepositoryAction = async (url: string) => { + // todo remove mods + const choice = await showOptionsModal('Remove repository? Installed mods wont be automatically removed.', ['Yes']) + if (!choice) return + await deleteRepository(url) + modsReactiveUpdater.counter++ +} + +export const selectAndRemoveRepository = async () => { + const repos = await getAllRepositories() + const choice = await showOptionsModal('Select repository to remove', repos.map(repo => repo.url)) + if (!choice) return + await removeRepositoryAction(choice) +} + +export const addRepositoryAction = async () => { + const { url } = await showInputsModal('Add repository', { + url: { + type: 'text', + label: 'Repository URL or slug', + placeholder: 'github-owner/repo-name', + }, + }) + if (!url) return + await fetchRepository(url, url) +} + +export const getServerPlugin = async (plugin: string) => { + const mod = await getPlugin(plugin) + if (!mod) return null + if (mod.serverPlugin) { + return { + content: mod.serverPlugin, + version: mod.version + } + } + return null +} + +export const getAvailableServerPlugins = async () => { + const mods = await getAllMods() + return mods.filter(mod => mod.serverPlugin) +} + +window.inspectInstalledMods = getAllMods + +type ModifiableField = { + field: string + label: string + language: string + getContent?: () => string +} + +// --- + +export const getAllModsModifiableFields = () => { + const fields: ModifiableField[] = [ + { + field: 'scriptMainUnstable', + label: 'Main Thread Script (unstable)', + language: 'js' + }, + { + field: 'stylesGlobal', + label: 'Global CSS Styles', + language: 'css' + }, + { + field: 'threeJsBackend', + label: 'Three.js Renderer Backend Thread', + language: 'js' + }, + { + field: 'serverPlugin', + label: 'Built-in server plugin', + language: 'js' + } + ] + return fields +} + +export const getModModifiableFields = (mod: ClientMod): ModifiableField[] => { + return getAllModsModifiableFields().filter(field => mod[field.field]) +} + +export const getModSettingsProxy = (mod: ClientMod) => { + if (!mod.settings) return valtio.proxy({}) + + const proxy = valtio.proxy({}) + for (const [key, setting] of Object.entries(mod.settings)) { + proxy[key] = options[`mod-${mod.name}-${key}`] ?? setting.default + } + + valtio.subscribe(proxy, (ops) => { + for (const op of ops) { + const [type, path, value] = op + const key = path[0] as string + options[`mod-${mod.name}-${key}`] = value + } + }) + + return proxy +} + +export const callMethodAction = async (modName: string, type: 'main', method: string) => { + try { + const mod = window.loadedMods?.[modName] + await mod[method]() + } catch (err) { + showNotification(`Failed to execute ${method}`, `Problem in ${type} js script of ${modName}`, true) + } +} diff --git a/src/connect.ts b/src/connect.ts new file mode 100644 index 00000000..cb6b8f65 --- /dev/null +++ b/src/connect.ts @@ -0,0 +1,96 @@ +// import { versionsByMinecraftVersion } from 'minecraft-data' +// import minecraftInitialDataJson from '../generated/minecraft-initial-data.json' +import MinecraftData from 'minecraft-data' +import PrismarineBlock from 'prismarine-block' +import PrismarineItem from 'prismarine-item' +import { miscUiState } from './globalState' +import supportedVersions from './supportedVersions.mjs' +import { options } from './optionsStorage' +import { downloadSoundsIfNeeded } from './sounds/botSoundSystem' +import { AuthenticatedAccount } from './react/serversStorage' + +export type ConnectOptions = { + server?: string + singleplayer?: any + username: string + proxy?: string + botVersion?: string + serverOverrides? + serverOverridesFlat? + peerId?: string + ignoreQs?: boolean + onSuccessfulPlay?: () => void + serverIndex?: string + authenticatedAccount?: AuthenticatedAccount | true + peerOptions?: any + viewerWsConnect?: string + saveServerToHistory?: boolean + + /** Will enable local replay server */ + worldStateFileContents?: string + + connectEvents?: { + serverCreated?: () => void + // connect: () => void; + // disconnect: () => void; + // error: (err: any) => void; + // ready: () => void; + // end: () => void; + } +} + +export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVersionSelect) => { + if (autoVersionSelect === 'auto') { + return '1.19.4' + } + if (autoVersionSelect === 'latest') { + return supportedVersions.at(-1)! + } + return autoVersionSelect +} + +export const loadMinecraftData = async (version: string) => { + await window._LOAD_MC_DATA() + // setLoadingScreenStatus(`Loading data for ${version}`) + // // todo expose cache + // // const initialDataVersion = Object.keys(minecraftInitialDataJson)[0]! + // // if (version === initialDataVersion) { + // // // ignore cache hit + // // versionsByMinecraftVersion.pc[initialDataVersion]!.dataVersion!++ + // // } + + const mcData = MinecraftData(version) + window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!) + window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!) + window.loadedData = mcData + window.mcData = mcData + miscUiState.loadedDataVersion = version +} + +export type AssetDownloadReporter = (asset: string, isDone: boolean) => void + +export const downloadAllMinecraftData = async (reporter?: AssetDownloadReporter) => { + reporter?.('mc-data', false) + await window._LOAD_MC_DATA() + reporter?.('mc-data', true) +} + +const loadFonts = async () => { + const FONT_FAMILY = 'mojangles' + if (!document.fonts.check(`1em ${FONT_FAMILY}`)) { + // todo instead re-render signs on load + await document.fonts.load(`1em ${FONT_FAMILY}`).catch(() => { + console.error('Failed to load font, signs wont be rendered correctly') + }) + } +} + +export const downloadOtherGameData = async (reporter?: AssetDownloadReporter) => { + reporter?.('fonts', false) + reporter?.('sounds', false) + + await Promise.all([ + loadFonts().then(() => reporter?.('fonts', true)), + downloadSoundsIfNeeded().then(() => reporter?.('sounds', true)) + ]) +} diff --git a/src/controls.ts b/src/controls.ts index 068b16b0..db6a6fc6 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -6,45 +6,89 @@ import { proxy, subscribe } from 'valtio' import { ControMax } from 'contro-max/build/controMax' import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types' import { stringStartsWith } from 'contro-max/build/stringUtils' -import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState } from './globalState' -import { goFullscreen, pointerLock, reloadChunks } from './utils' +import { GameMode } from 'mineflayer' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, hideModal, hideAllModals } from './globalState' +import { goFullscreen, isInRealGameSession, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' -import { openPlayerInventory } from './playerWindows' -import { chatInputValueGlobal } from './react/ChatContainer' +import { openPlayerInventory } from './inventoryWindows' +import { chatInputValueGlobal } from './react/Chat' import { fsState } from './loadSave' +import { customCommandsConfig } from './customCommands' +import type { CustomCommand } from './react/KeybindingsCustom' import { showOptionsModal } from './react/SelectOption' import widgets from './react/widgets' -import { getItemFromBlock } from './botUtils' +import { getItemFromBlock } from './chatUtils' +import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor' +import { completeResourcepackPackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack' +import { showNotification } from './react/NotificationProvider' +import { lastConnectOptions } from './react/AppStatusProvider' +import { onCameraMove, onControInit } from './cameraRotationControls' +import { createNotificationProgressReporter } from './core/progressReporter' +import { appStorage } from './react/appStorageProvider' +import { switchGameMode } from './packetsReplay/replayPackets' +import { tabListState } from './react/PlayerListOverlayProvider' +import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig' +import { playerState } from './mineflayer/playerState' -// doesnt seem to work for now -const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) +export const customKeymaps = proxy(appStorage.keybindings) subscribe(customKeymaps, () => { - localStorage.keymap = JSON.parse(customKeymaps) + appStorage.keybindings = customKeymaps }) +const controlOptions = { + preventDefault: true +} + export const contro = new ControMax({ commands: { general: { + // movement jump: ['Space', 'A'], inventory: ['KeyE', 'X'], drop: ['KeyQ', 'B'], - sneak: ['ShiftLeft', 'Right Stick'], + dropStack: [null], + sneak: ['ShiftLeft'], + toggleSneakOrDown: [null, 'Right Stick'], sprint: ['ControlLeft', 'Left Stick'], - nextHotbarSlot: [null, 'Left Bumper'], - prevHotbarSlot: [null, 'Right Bumper'], + // game interactions + nextHotbarSlot: [null, 'Right Bumper'], + prevHotbarSlot: [null, 'Left Bumper'], attackDestroy: [null, 'Right Trigger'], interactPlace: [null, 'Left Trigger'], + swapHands: ['KeyF'], + selectItem: ['KeyH'], + rotateCameraLeft: [null], + rotateCameraRight: [null], + rotateCameraUp: [null], + rotateCameraDown: [null], + // ui? chat: [['KeyT', 'Enter']], command: ['Slash'], - selectItem: ['KeyH'] // default will be removed + playersList: ['Tab'], + debugOverlay: ['F3'], + debugOverlayHelpMenu: [null], + // client side + zoom: ['KeyC'], + viewerConsole: ['Backquote'], + togglePerspective: ['F5'], }, ui: { + toggleFullscreen: ['F11'], back: [null/* 'Escape' */, 'B'], - click: [null, 'A'], + toggleMap: ['KeyJ'], + leftClick: [null, 'A'], + rightClick: [null, 'Y'], + speedupCursor: [null, 'Left Stick'], + pauseMenu: [null, 'Start'] + }, + communication: { + toggleMicrophone: ['KeyM'], }, advanced: { lockUrl: ['KeyY'], - } + }, + custom: {} as Record, // waila: { // showLookingBlockRecipe: ['Numpad3'], // showLookingBlockUsages: ['Numpad4'] @@ -58,9 +102,10 @@ export const contro = new ControMax({ } }, }, { + defaultControlOptions: controlOptions, target: document, captureEvents () { - return bot && isGameActive(false) + return true }, storeProvider: { load: () => customKeymaps, @@ -71,13 +116,63 @@ export const contro = new ControMax({ window.controMax = contro export type Command = CommandEventArgument['command'] +export const isCommandDisabled = (command: Command) => { + return miscUiState.appConfig?.disabledCommands?.includes(command) +} + +onControInit() + +updateBinds(customKeymaps) + +const updateDoPreventDefault = () => { + controlOptions.preventDefault = miscUiState.gameLoaded && !activeModalStack.length +} + +subscribe(miscUiState, updateDoPreventDefault) +subscribe(activeModalStack, updateDoPreventDefault) +updateDoPreventDefault() + const setSprinting = (state: boolean) => { bot.setControlState('sprint', state) gameAdditionalState.isSprinting = state } -contro.on('movementUpdate', ({ vector, gamepadIndex }) => { +const isSpectatingEntity = () => { + return appViewer.playerState.utils.isSpectatingEntity() +} + +contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => { + // Don't allow movement while spectating an entity + if (isSpectatingEntity()) return + + if (gamepadIndex !== undefined && gamepadUiCursorState.display) { + const deadzone = 0.1 // TODO make deadzone configurable + if (Math.abs(soleVector.x) < deadzone && Math.abs(soleVector.z) < deadzone) { + return + } + moveGamepadCursorByPx(soleVector.x, true) + moveGamepadCursorByPx(soleVector.z, false) + emitMousemove() + } miscUiState.usingGamepadInput = gamepadIndex !== undefined + if (!bot || !isGameActive(false)) return + + // if (viewer.world.freeFlyMode) { + // // Create movement vector from input + // const direction = new THREE.Vector3(0, 0, 0) + // if (vector.z !== undefined) direction.z = vector.z + // if (vector.x !== undefined) direction.x = vector.x + + // // Apply camera rotation to movement direction + // direction.applyQuaternion(viewer.camera.quaternion) + + // // Update freeFlyState position with normalized direction + // const moveSpeed = 1 + // direction.multiplyScalar(moveSpeed) + // viewer.world.freeFlyState.position.add(new Vec3(direction.x, direction.y, direction.z)) + // return + // } + // gamepadIndex will be used for splitscreen in future const coordToAction = [ ['z', -1, 'forward'], @@ -107,6 +202,7 @@ contro.on('movementUpdate', ({ vector, gamepadIndex }) => { if (action) { void contro.emit('trigger', { command: 'general.forward' } as any) } else { + void contro.emit('release', { command: 'general.forward' } as any) setSprinting(false) } } @@ -118,6 +214,7 @@ let lastCommandTrigger = null as { command: string, time: number } | null const secondActionActivationTimeout = 300 const secondActionCommands = { 'general.jump' () { + // if (bot.game.gameMode === 'spectator') return toggleFly() }, 'general.forward' () { @@ -135,30 +232,177 @@ subscribe(activeModalStack, () => { } }) -const uiCommand = (command: Command) => { - if (command === 'ui.back') { - hideCurrentModal() - } else if (command === 'ui.click') { - // todo cursor +const emitMousemove = () => { + const { x, y } = gamepadUiCursorState + const xAbs = x / 100 * window.innerWidth + const yAbs = y / 100 * window.innerHeight + const element = document.elementFromPoint(xAbs, yAbs) as HTMLElement | null + if (!element) return + element.dispatchEvent(new MouseEvent('mousemove', { + clientX: xAbs, + clientY: yAbs + })) +} + +let lastClickedEl = null as HTMLElement | null +let lastClickedElTimeout: ReturnType | undefined +const inModalCommand = (command: Command, pressed: boolean) => { + if (pressed && !gamepadUiCursorState.display) return + + if (pressed) { + if (command === 'ui.back') { + hideCurrentModal() + } + if (command === 'ui.pauseMenu') { + // hide all modals + hideAllModals() + } + if (command === 'ui.leftClick' || command === 'ui.rightClick') { + // in percent + const { x, y } = gamepadUiCursorState + const xAbs = x / 100 * window.innerWidth + const yAbs = y / 100 * window.innerHeight + const el = document.elementFromPoint(xAbs, yAbs) as HTMLElement + if (el) { + if (el === lastClickedEl && command === 'ui.leftClick') { + el.dispatchEvent(new MouseEvent('dblclick', { + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + return + } + el.dispatchEvent(new MouseEvent('mousedown', { + button: command === 'ui.leftClick' ? 0 : 2, + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + el.dispatchEvent(new MouseEvent(command === 'ui.leftClick' ? 'click' : 'contextmenu', { + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + el.dispatchEvent(new MouseEvent('mouseup', { + button: command === 'ui.leftClick' ? 0 : 2, + bubbles: true, + clientX: xAbs, + clientY: yAbs + })) + el.focus() + lastClickedEl = el + if (lastClickedElTimeout) clearTimeout(lastClickedElTimeout) + lastClickedElTimeout = setTimeout(() => { + lastClickedEl = null + }, 500) + } + } } + + if (command === 'ui.speedupCursor') { + gamepadUiCursorState.multiply = pressed ? 2 : 1 + } +} + +// Camera rotation controls +const cameraRotationControls = { + activeDirections: new Set<'left' | 'right' | 'up' | 'down'>(), + interval: null as ReturnType | null, + config: { + speed: 1, // movement per interval + interval: 5 // ms between movements + }, + movements: { + left: { movementX: -0.5, movementY: 0 }, + right: { movementX: 0.5, movementY: 0 }, + up: { movementX: 0, movementY: -0.5 }, + down: { movementX: 0, movementY: 0.5 } + }, + updateMovement () { + if (cameraRotationControls.activeDirections.size === 0) { + if (cameraRotationControls.interval) { + clearInterval(cameraRotationControls.interval) + cameraRotationControls.interval = null + } + return + } + + if (!cameraRotationControls.interval) { + cameraRotationControls.interval = setInterval(() => { + // Combine all active movements + const movement = { movementX: 0, movementY: 0 } + for (const direction of cameraRotationControls.activeDirections) { + movement.movementX += cameraRotationControls.movements[direction].movementX + movement.movementY += cameraRotationControls.movements[direction].movementY + } + + onCameraMove({ + ...movement, + type: 'keyboardRotation', + stopPropagation () {} + }) + }, cameraRotationControls.config.interval) + } + }, + start (direction: 'left' | 'right' | 'up' | 'down') { + cameraRotationControls.activeDirections.add(direction) + cameraRotationControls.updateMovement() + }, + stop (direction: 'left' | 'right' | 'up' | 'down') { + cameraRotationControls.activeDirections.delete(direction) + cameraRotationControls.updateMovement() + }, + handleCommand (command: string, pressed: boolean) { + // Don't allow movement while spectating an entity + if (isSpectatingEntity()) return + + const directionMap = { + 'general.rotateCameraLeft': 'left', + 'general.rotateCameraRight': 'right', + 'general.rotateCameraUp': 'up', + 'general.rotateCameraDown': 'down' + } as const + + const direction = directionMap[command] + if (direction) { + if (pressed) cameraRotationControls.start(direction) + else cameraRotationControls.stop(direction) + return true + } + return false + } +} +window.cameraRotationControls = cameraRotationControls + +const setSneaking = (state: boolean) => { + gameAdditionalState.isSneaking = state + bot.setControlState('sneak', state) + } const onTriggerOrReleased = (command: Command, pressed: boolean) => { // always allow release! - if (pressed && !isGameActive(true)) { - uiCommand(command) - return - } + if (!bot || !isGameActive(false)) return if (stringStartsWith(command, 'general')) { // handle general commands // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { case 'general.jump': + if (isSpectatingEntity()) break + // if (viewer.world.freeFlyMode) { + // const moveSpeed = 0.5 + // viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0)) + // } else { bot.setControlState('jump', pressed) + // } break case 'general.sneak': - gameAdditionalState.isSneaking = pressed - bot.setControlState('sneak', pressed) + // if (viewer.world.freeFlyMode) { + // const moveSpeed = 0.5 + // viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? -moveSpeed : 0, 0)) + // } else { + setSneaking(pressed) + // } break case 'general.sprint': // todo add setting to change behavior @@ -166,28 +410,148 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { setSprinting(pressed) } break + case 'general.toggleSneakOrDown': + if (gameAdditionalState.isFlying) { + setSneaking(pressed) + } else if (pressed) { + setSneaking(!gameAdditionalState.isSneaking) + } + break case 'general.attackDestroy': document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 0 })) break case 'general.interactPlace': document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 2 })) break + case 'general.zoom': + gameAdditionalState.isZooming = pressed + break + case 'general.debugOverlay': + if (pressed) { + miscUiState.showDebugHud = !miscUiState.showDebugHud + } + break + case 'general.debugOverlayHelpMenu': + if (pressed) { + void onF3LongPress() + } + break + case 'general.rotateCameraLeft': + case 'general.rotateCameraRight': + case 'general.rotateCameraUp': + case 'general.rotateCameraDown': + cameraRotationControls.handleCommand(command, pressed) + break + case 'general.playersList': + tabListState.isOpen = pressed + break + case 'general.viewerConsole': + if (lastConnectOptions.value?.viewerWsConnect) { + showModal({ reactType: 'console' }) + } + break + case 'general.togglePerspective': + if (pressed) { + const currentPerspective = playerState.reactive.perspective + // eslint-disable-next-line sonarjs/no-nested-switch + switch (currentPerspective) { + case 'first_person': + playerState.reactive.perspective = 'third_person_back' + break + case 'third_person_back': + playerState.reactive.perspective = 'third_person_front' + break + case 'third_person_front': + playerState.reactive.perspective = 'first_person' + break + } + } + break + } + } else if (stringStartsWith(command, 'ui')) { + switch (command) { + case 'ui.pauseMenu': + if (pressed) { + if (activeModalStack.length) { + hideCurrentModal() + } else { + showModal({ reactType: 'pause-screen' }) + } + } + break + case 'ui.back': + case 'ui.toggleFullscreen': + case 'ui.toggleMap': + case 'ui.leftClick': + case 'ui.rightClick': + case 'ui.speedupCursor': + // These are handled elsewhere + break } } } // im still not sure, maybe need to refactor to handle in inventory instead -const alwaysHandledCommand = (command: Command) => { +const alwaysPressedHandledCommand = (command: Command) => { + inModalCommand(command, true) + // triggered even outside of the game if (command === 'general.inventory') { if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) { // todo? hideCurrentModal() } } + if (command === 'advanced.lockUrl') { + lockUrl() + } + if (command === 'communication.toggleMicrophone') { + toggleMicrophoneMuted?.() + } } +export function lockUrl () { + let newQs = '' + if (fsState.saveLoaded && fsState.inMemorySave) { + const worldFolder = fsState.inMemorySavePath + const save = worldFolder.split('/').at(-1) + newQs = `loadSave=${save}` + } else if (process.env.NODE_ENV === 'development') { + newQs = `reconnect=1` + } else if (lastConnectOptions.value?.server) { + const qs = new URLSearchParams() + const { server, botVersion, proxy, username } = lastConnectOptions.value + qs.set('ip', server) + if (botVersion) qs.set('version', botVersion) + if (proxy) qs.set('proxy', proxy) + if (username) qs.set('username', username) + newQs = String(qs.toString()) + } + + if (newQs) { + window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`) + } +} + +function cycleHotbarSlot (dir: 1 | -1) { + const newHotbarSlot = (bot.quickBarSlot + dir + 9) % 9 + bot.setQuickBarSlot(newHotbarSlot) +} + +// custom commands handler +const customCommandsHandler = ({ command }) => { + const [section, name] = command.split('.') + if (!isGameActive(true) || section !== 'custom') return + + if (contro.userConfig?.custom) { + customCommandsConfig[(contro.userConfig.custom[name] as CustomCommand).type].handler((contro.userConfig.custom[name] as CustomCommand).inputs) + } +} +contro.on('trigger', customCommandsHandler) + contro.on('trigger', ({ command }) => { + if (isCommandDisabled(command)) return + const willContinue = !isGameActive(true) - alwaysHandledCommand(command) + alwaysPressedHandledCommand(command) if (willContinue) return const secondActionCommand = secondActionCommands[command] @@ -207,14 +571,46 @@ contro.on('trigger', ({ command }) => { onTriggerOrReleased(command, true) if (stringStartsWith(command, 'general')) { - // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { + case 'general.jump': + case 'general.sneak': + case 'general.toggleSneakOrDown': + case 'general.sprint': + case 'general.attackDestroy': + case 'general.rotateCameraLeft': + case 'general.rotateCameraRight': + case 'general.rotateCameraUp': + case 'general.rotateCameraDown': + case 'general.debugOverlay': + case 'general.debugOverlayHelpMenu': + case 'general.playersList': + case 'general.togglePerspective': + // no-op + break + case 'general.swapHands': { + if (isSpectatingEntity()) break + bot._client.write('block_dig', { + 'status': 6, + 'location': { + 'x': 0, + 'z': 0, + 'y': 0 + }, + 'face': 0, + }) + break + } + case 'general.interactPlace': + // handled in onTriggerOrReleased + break case 'general.inventory': + if (isSpectatingEntity()) break document.exitPointerLock?.() openPlayerInventory() break - case 'general.drop': - // if (bot.heldItem/* && ctrl */) bot.tossStack(bot.heldItem) + case 'general.drop': { + if (isSpectatingEntity()) break + // protocol 1.9+ bot._client.write('block_dig', { 'status': 4, 'location': { @@ -225,7 +621,20 @@ contro.on('trigger', ({ command }) => { 'face': 0, sequence: 0 }) + const slot = bot.inventory.hotbarStart + bot.quickBarSlot + const item = bot.inventory.slots[slot] + if (item) { + item.count-- + bot.inventory.updateSlot(slot, item.count > 0 ? item : null!) + } break + } + case 'general.dropStack': { + if (bot.heldItem) { + void bot.tossStack(bot.heldItem) + } + break + } case 'general.chat': showModal({ reactType: 'chat' }) break @@ -234,37 +643,60 @@ contro.on('trigger', ({ command }) => { showModal({ reactType: 'chat' }) break case 'general.selectItem': + if (isSpectatingEntity()) break void selectItem() break + case 'general.nextHotbarSlot': + if (isSpectatingEntity()) break + cycleHotbarSlot(1) + break + case 'general.prevHotbarSlot': + if (isSpectatingEntity()) break + cycleHotbarSlot(-1) + break + case 'general.zoom': + break + case 'general.viewerConsole': + if (lastConnectOptions.value?.viewerWsConnect) { + showModal({ reactType: 'console' }) + } + break } } - if (command === 'advanced.lockUrl') { - let newQs = '' - if (fsState.saveLoaded) { - const save = localServer!.options.worldFolder.split('/').at(-1) - newQs = `loadSave=${save}` - } else if (process.env.NODE_ENV === 'development') { - newQs = `reconnect=1` - } else { - const qs = new URLSearchParams() - const { server, version } = localStorage - qs.set('server', server) - if (version) qs.set('version', version) - newQs = String(qs.toString()) - } - window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`) - // return + if (command === 'ui.toggleFullscreen') { + void goFullscreen(true) + } +}) + +// show-hide Fullmap +contro.on('trigger', ({ command }) => { + if (command !== 'ui.toggleMap') return + const isActive = isGameActive(true) + if (activeModalStack.at(-1)?.reactType === 'full-map') { + miscUiState.displayFullmap = false + hideModal({ reactType: 'full-map' }) + } else if (isActive && !activeModalStack.length) { + miscUiState.displayFullmap = true + showModal({ reactType: 'full-map' }) } }) contro.on('release', ({ command }) => { + if (isCommandDisabled(command)) return + + inModalCommand(command, false) onTriggerOrReleased(command, false) }) // hard-coded keybindings -export const f3Keybinds = [ +export const f3Keybinds: Array<{ + key?: string, + action: () => void | Promise, + mobileTitle: string + enabled?: () => boolean +}> = [ { key: 'KeyA', action () { @@ -273,17 +705,20 @@ export const f3Keybinds = [ for (const [x, z] of loadedChunks) { worldView!.unloadChunk({ x, z }) } - for (const child of viewer.scene.children) { - if (child.name === 'chunk') { // should not happen - viewer.scene.remove(child) - console.warn('forcefully removed chunk from scene') - } - } + // for (const child of viewer.scene.children) { + // if (child.name === 'chunk') { // should not happen + // viewer.scene.remove(child) + // console.warn('forcefully removed chunk from scene') + // } + // } if (localServer) { //@ts-expect-error not sure why it is private... maybe revisit api? localServer.players[0].world.columns = {} } void reloadChunks() + if (appViewer.backend?.backendMethods && typeof appViewer.backend.backendMethods.reloadWorld === 'function') { + appViewer.backend.backendMethods.reloadWorld() + } }, mobileTitle: 'Reload chunks', }, @@ -291,12 +726,24 @@ export const f3Keybinds = [ key: 'KeyG', action () { options.showChunkBorders = !options.showChunkBorders - viewer.world.updateShowChunksBorder(options.showChunkBorders) }, mobileTitle: 'Toggle chunk borders', }, { - key: 'KeyT', + key: 'KeyH', + action () { + showModal({ reactType: 'chunks-debug' }) + }, + mobileTitle: 'Show Chunks Debug', + }, + { + action () { + showModal({ reactType: 'renderer-debug' }) + }, + mobileTitle: 'Renderer Debug Menu', + }, + { + key: 'KeyY', async action () { // waypoints const widgetNames = widgets.map(widget => widget.name) @@ -305,86 +752,91 @@ export const f3Keybinds = [ showModal({ reactType: `widget-${widget}` }) }, mobileTitle: 'Open Widget' + }, + { + key: 'KeyT', + async action () { + // TODO! + if (resourcePackState.resourcePackInstalled || gameAdditionalState.usingServerResourcePack) { + showNotification('Reloading textures...') + await completeResourcepackPackInstall('default', 'default', gameAdditionalState.usingServerResourcePack, createNotificationProgressReporter()) + } + }, + mobileTitle: 'Reload Textures' + }, + { + key: 'F4', + async action () { + let nextGameMode: GameMode + switch (bot.game.gameMode) { + case 'creative': { + nextGameMode = 'survival' + + break + } + case 'survival': { + nextGameMode = 'adventure' + + break + } + case 'adventure': { + nextGameMode = 'spectator' + + break + } + case 'spectator': { + nextGameMode = 'creative' + + break + } + // No default + } + if (lastConnectOptions.value?.worldStateFileContents) { + switchGameMode(nextGameMode) + } else { + bot.chat(`/gamemode ${nextGameMode}`) + } + }, + mobileTitle: 'Cycle Game Mode' + }, + { + key: 'KeyP', + async action () { + const { uuid, ping: playerPing, username } = bot.player + const proxyPing = await bot['pingProxy']() + void showOptionsModal(`${username}: last known total latency (ping): ${playerPing}. Connected to ${lastConnectOptions.value?.proxy} with current ping ${proxyPing}. Player UUID: ${uuid}`, []) + }, + mobileTitle: 'Show Player & Ping Details', + enabled: () => !lastConnectOptions.value?.singleplayer && !!bot.player + }, + { + action () { + void copyServerResourcePackToRegular() + }, + mobileTitle: 'Copy Server Resource Pack', + enabled: () => !!gameAdditionalState.usingServerResourcePack } ] -const hardcodedPressedKeys = new Set() +export const reloadChunksAction = () => { + const action = f3Keybinds.find(f3Keybind => f3Keybind.key === 'KeyA') + void action!.action() +} + document.addEventListener('keydown', (e) => { if (!isGameActive(false)) return - if (hardcodedPressedKeys.has('F3')) { + if (contro.pressedKeys.has('F3')) { const keybind = f3Keybinds.find((v) => v.key === e.code) - if (keybind) keybind.action() - return - } - - hardcodedPressedKeys.add(e.code) -}) -document.addEventListener('keyup', (e) => { - hardcodedPressedKeys.delete(e.code) -}) -document.addEventListener('visibilitychange', (e) => { - if (document.visibilityState === 'hidden') { - hardcodedPressedKeys.clear() - } -}) - -// #region creative fly -// these controls are more like for gamemode 3 - -const makeInterval = (fn, interval) => { - const intervalId = setInterval(fn, interval) - - const cleanup = () => { - clearInterval(intervalId) - cleanup.active = false - } - cleanup.active = true - return cleanup -} - -const isFlying = () => bot.physics.gravity === 0 -let endFlyLoop: ReturnType | undefined - -const currentFlyVector = new Vec3(0, 0, 0) -window.currentFlyVector = currentFlyVector - -const startFlyLoop = () => { - if (!isFlying()) return - endFlyLoop?.() - - endFlyLoop = makeInterval(() => { - if (!bot) { - endFlyLoop?.() - return + if (keybind && (keybind.enabled?.() ?? true)) { + void keybind.action() + e.stopPropagation() } - - bot.entity.position.add(currentFlyVector.clone().multiply(new Vec3(0, 0.5, 0))) - }, 50) -} - -// todo we will get rid of patching it when refactor controls -let originalSetControlState -const patchedSetControlState = (action, state) => { - if (!isFlying()) { - return originalSetControlState(action, state) } +}, { + capture: true, +}) - const actionPerFlyVector = { - jump: new Vec3(0, 1, 0), - sneak: new Vec3(0, -1, 0), - } - - const changeVec = actionPerFlyVector[action] - if (!changeVec) { - return originalSetControlState(action, state) - } - const toAddVec = changeVec.scaled(state ? 1 : -1) - for (const coord of ['x', 'y', 'z']) { - if (toAddVec[coord] === 0) continue - if (currentFlyVector[coord] === toAddVec[coord]) return - } - currentFlyVector.add(toAddVec) -} +const isFlying = () => (bot.entity as any).flying const startFlying = (sendAbilities = true) => { if (sendAbilities) { @@ -392,46 +844,24 @@ const startFlying = (sendAbilities = true) => { flags: 2, }) } - // window.flyingSpeed will be removed - bot.physics['airborneAcceleration'] = window.flyingSpeed ?? 0.1 // todo use abilities - bot.entity.velocity = new Vec3(0, 0, 0) - bot.creative.startFlying() - startFlyLoop() + (bot.entity as any).flying = true } const endFlying = (sendAbilities = true) => { - if (bot.physics.gravity !== 0) return + if (!isFlying()) return if (sendAbilities) { bot._client.write('abilities', { flags: 0, }) } - bot.physics['airborneAcceleration'] = standardAirborneAcceleration - bot.creative.stopFlying() - endFlyLoop?.() + (bot.entity as any).flying = false } -let allowFlying = false - export const onBotCreate = () => { - bot._client.on('abilities', ({ flags }) => { - if (flags & 2) { // flying - toggleFly(true, false) - } else { - toggleFly(false, false) - } - allowFlying = !!(flags & 4) - }) } -const standardAirborneAcceleration = 0.02 const toggleFly = (newState = !isFlying(), sendAbilities?: boolean) => { - // if (bot.game.gameMode !== 'creative' && bot.game.gameMode !== 'spectator') return - if (!allowFlying) return - if (bot.setControlState !== patchedSetControlState) { - originalSetControlState = bot.setControlState - bot.setControlState = patchedSetControlState - } + if (!bot.entity.canFly) return if (newState) { startFlying(sendAbilities) @@ -440,7 +870,6 @@ const toggleFly = (newState = !isFlying(), sendAbilities?: boolean) => { } gameAdditionalState.isFlying = isFlying() } -// #endregion const selectItem = async () => { const block = bot.blockAtCursor(5) @@ -454,9 +883,16 @@ const selectItem = async () => { } addEventListener('mousedown', async (e) => { + // always prevent default for side buttons (back / forward navigation) + if (e.button === 3 || e.button === 4) { + e.preventDefault() + } + if ((e.target as HTMLElement).matches?.('#VRButton')) return + if (!isInRealGameSession() && !(e.target as HTMLElement).id.includes('ui-root')) return void pointerLock.requestPointerLock() if (!bot) return + getThreeJsRendererMethods()?.onPageInteraction() // wheel click // todo support ctrl+wheel (+nbt) if (e.button === 1) { @@ -466,12 +902,21 @@ addEventListener('mousedown', async (e) => { window.addEventListener('keydown', (e) => { if (e.code !== 'Escape') return + if (!activeModalStack.length) { + getThreeJsRendererMethods()?.onPageInteraction() + } + if (activeModalStack.length) { - hideCurrentModal(undefined, () => { - if (!activeModalStack.length) { - pointerLock.justHitEscape = true - } - }) + const hideAll = e.ctrlKey || e.metaKey + if (hideAll) { + hideAllModals() + } else { + hideCurrentModal() + } + if (activeModalStack.length === 0) { + getThreeJsRendererMethods()?.onPageInteraction() + pointerLock.justHitEscape = true + } } else if (pointerLock.hasPointerLock) { document.exitPointerLock?.() if (options.autoExitFullscreen) { @@ -482,14 +927,125 @@ window.addEventListener('keydown', (e) => { } }) +window.addEventListener('keydown', (e) => { + if (e.code !== 'F2' || e.repeat || !isGameActive(true)) return + e.preventDefault() + const canvas = document.getElementById('viewer-canvas') as HTMLCanvasElement + if (!canvas) return + const link = document.createElement('a') + link.href = canvas.toDataURL('image/png') + const date = new Date() + link.download = `screenshot ${date.toLocaleString().replaceAll('.', '-').replace(',', '')}.png` + link.click() +}) + +window.addEventListener('keydown', (e) => { + if (e.code !== 'F1' || e.repeat || !isGameActive(true)) return + e.preventDefault() + miscUiState.showUI = !miscUiState.showUI +}) + // #region experimental debug things window.addEventListener('keydown', (e) => { - if (e.code === 'F11') { - e.preventDefault() - void goFullscreen(true) - } - if (e.code === 'KeyL' && e.altKey) { + if (e.code === 'KeyL' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { console.clear() } + if (e.code === 'KeyK' && e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey) { + if (sessionStorage.delayLoadUntilFocus) { + sessionStorage.removeItem('delayLoadUntilFocus') + } else { + sessionStorage.setItem('delayLoadUntilFocus', 'true') + } + } + if (e.code === 'KeyK' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { + // eslint-disable-next-line no-debugger + debugger + } }) // #endregion + +export function updateBinds (commands: any) { + contro.inputSchema.commands.custom = Object.fromEntries(Object.entries(commands?.custom ?? {}).map(([key, value]) => { + return [key, { + keys: [], + gamepad: [], + type: '', + inputs: [] + }] + })) + + for (const [group, actions] of Object.entries(commands)) { + contro.userConfig![group] = Object.fromEntries(Object.entries(actions).map(([key, value]) => { + const newValue = { + keys: value?.keys ?? undefined, + gamepad: value?.gamepad ?? undefined, + } + + if (group === 'custom') { + newValue['type'] = (value).type + newValue['inputs'] = (value).inputs + } + + return [key, newValue] + })) + } +} + +export const onF3LongPress = async () => { + const actions = f3Keybinds.filter(f3Keybind => { + return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true) + }) + const actionNames = actions.map(f3Keybind => { + return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` + }) + const select = await showOptionsModal('', actionNames) + if (!select) return + const actionIndex = actionNames.indexOf(select) + const f3Keybind = actions[actionIndex]! + void f3Keybind.action() +} + +export const handleMobileButtonCustomAction = (action: CustomAction) => { + const handler = customCommandsConfig[action.type]?.handler + if (handler) { + handler([...action.input]) + } +} + +export const triggerCommand = (command: Command, isDown: boolean) => { + handleMobileButtonActionCommand(command, isDown) +} + +export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => { + const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command + + // Check if command is disabled before proceeding + if (typeof commandValue === 'string' && isCommandDisabled(commandValue as Command)) return + + if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) { + const event: CommandEventArgument = { + command: commandValue as Command, + schema: { + keys: [], + gamepad: [] + } + } + if (isDown) { + contro.emit('trigger', event) + } else { + contro.emit('release', event) + } + } else if (typeof commandValue === 'object') { + if (isDown) { + handleMobileButtonCustomAction(commandValue) + } + } +} + +export const handleMobileButtonLongPress = (actionHold: ActionHoldConfig) => { + if (typeof actionHold.longPressAction === 'string' && actionHold.longPressAction === 'general.debugOverlayHelpMenu') { + void onF3LongPress() + } else if (actionHold.longPressAction) { + handleMobileButtonActionCommand(actionHold.longPressAction, true) + } +} diff --git a/src/core/ideChannels.ts b/src/core/ideChannels.ts new file mode 100644 index 00000000..a9c517f7 --- /dev/null +++ b/src/core/ideChannels.ts @@ -0,0 +1,106 @@ +import { proxy } from 'valtio' + +export const ideState = proxy({ + id: '', + contents: '', + line: 0, + column: 0, + language: 'typescript', + title: '', +}) +globalThis.ideState = ideState + +export const registerIdeChannels = () => { + registerIdeOpenChannel() + registerIdeSaveChannel() +} + +const registerIdeOpenChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:ide-open' + + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'language', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'contents', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'line', + type: 'i32' + }, + { + name: 'column', + type: 'i32' + }, + { + name: 'title', + type: ['pstring', { countType: 'i16' }] + } + ] + ] + + bot._client.registerChannel(CHANNEL_NAME, packetStructure, true) + + bot._client.on(CHANNEL_NAME as any, (data) => { + const { id, language, contents, line, column, title } = data + + ideState.contents = contents + ideState.line = line + ideState.column = column + ideState.id = id + ideState.language = language || 'typescript' + ideState.title = title + }) + + console.debug(`registered custom channel ${CHANNEL_NAME} channel`) +} +const IDE_SAVE_CHANNEL_NAME = 'minecraft-web-client:ide-save' +const registerIdeSaveChannel = () => { + + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'contents', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'language', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'line', + type: 'i32' + }, + { + name: 'column', + type: 'i32' + }, + ] + ] + bot._client.registerChannel(IDE_SAVE_CHANNEL_NAME, packetStructure, true) +} + +export const saveIde = () => { + bot._client.writeChannel(IDE_SAVE_CHANNEL_NAME, { + id: ideState.id, + contents: ideState.contents, + language: ideState.language, + // todo: reflect updated + line: ideState.line, + column: ideState.column, + }) +} diff --git a/src/core/importExport.ts b/src/core/importExport.ts new file mode 100644 index 00000000..b3e26347 --- /dev/null +++ b/src/core/importExport.ts @@ -0,0 +1,219 @@ +import { appStorage } from '../react/appStorageProvider' +import { getChangedSettings, options } from '../optionsStorage' +import { customKeymaps } from '../controls' +import { showInputsModal } from '../react/SelectOption' + +interface ExportedFile { + _about: string + options?: Record + keybindings?: Record + servers?: any[] + username?: string + proxy?: string + proxies?: string[] + accountTokens?: any[] +} + +export const importData = async () => { + try { + const input = document.createElement('input') + input.type = 'file' + input.accept = '.json' + input.click() + + const file = await new Promise((resolve) => { + input.onchange = () => { + if (!input.files?.[0]) return + resolve(input.files[0]) + } + }) + + const text = await file.text() + const data = JSON.parse(text) + + if (!data._about?.includes('Minecraft Web Client')) { + const doContinue = confirm('This file does not appear to be a Minecraft Web Client profile. Continue anyway?') + if (!doContinue) return + } + + // Build available data types for selection + const availableData: Record, { present: boolean, description: string }> = { + options: { present: !!data.options, description: 'Game settings and preferences' }, + keybindings: { present: !!data.keybindings, description: 'Custom key mappings' }, + servers: { present: !!data.servers, description: 'Saved server list' }, + username: { present: !!data.username, description: 'Username' }, + proxy: { present: !!data.proxy, description: 'Selected proxy server' }, + proxies: { present: !!data.proxies, description: 'Global proxies list' }, + accountTokens: { present: !!data.accountTokens, description: 'Account authentication tokens' }, + } + + // Filter to only present data types + const presentTypes = Object.fromEntries(Object.entries(availableData) + .filter(([_, info]) => info.present) + .map(([key, info]) => [key, info])) + + if (Object.keys(presentTypes).length === 0) { + alert('No compatible data found in the imported file.') + return + } + + const importChoices = await showInputsModal('Select Data to Import', { + mergeData: { + type: 'checkbox', + label: 'Merge with existing data (uncheck to remove old data)', + defaultValue: true, + }, + ...Object.fromEntries(Object.entries(presentTypes).map(([key, info]) => [key, { + type: 'checkbox', + label: info.description, + defaultValue: true, + }])) + }) as { mergeData: boolean } & Record + + if (!importChoices) return + + const importedTypes: string[] = [] + const shouldMerge = importChoices.mergeData + + if (importChoices.options && data.options) { + if (shouldMerge) { + Object.assign(options, data.options) + } else { + for (const key of Object.keys(options)) { + if (key in data.options) { + options[key as any] = data.options[key] + } + } + } + importedTypes.push('settings') + } + + if (importChoices.keybindings && data.keybindings) { + if (shouldMerge) { + Object.assign(customKeymaps, data.keybindings) + } else { + for (const key of Object.keys(customKeymaps)) delete customKeymaps[key] + Object.assign(customKeymaps, data.keybindings) + } + importedTypes.push('keybindings') + } + + if (importChoices.servers && data.servers) { + if (shouldMerge && appStorage.serversList) { + // Merge by IP, update existing entries and add new ones + const existingIps = new Set(appStorage.serversList.map(s => s.ip)) + const newServers = data.servers.filter(s => !existingIps.has(s.ip)) + appStorage.serversList = [...appStorage.serversList, ...newServers] + } else { + appStorage.serversList = data.servers + } + importedTypes.push('servers') + } + + if (importChoices.username && data.username) { + appStorage.username = data.username + importedTypes.push('username') + } + + if ((importChoices.proxy && data.proxy) || (importChoices.proxies && data.proxies)) { + if (!appStorage.proxiesData) { + appStorage.proxiesData = { proxies: [], selected: '' } + } + + if (importChoices.proxies && data.proxies) { + if (shouldMerge) { + // Merge unique proxies + const uniqueProxies = new Set([...appStorage.proxiesData.proxies, ...data.proxies]) + appStorage.proxiesData.proxies = [...uniqueProxies] + } else { + appStorage.proxiesData.proxies = data.proxies + } + importedTypes.push('proxies list') + } + + if (importChoices.proxy && data.proxy) { + appStorage.proxiesData.selected = data.proxy + importedTypes.push('selected proxy') + } + } + + if (importChoices.accountTokens && data.accountTokens) { + if (shouldMerge && appStorage.authenticatedAccounts) { + // Merge by unique identifier (assuming accounts have some unique ID or username) + const existingAccounts = new Set(appStorage.authenticatedAccounts.map(a => a.username)) + const newAccounts = data.accountTokens.filter(a => !existingAccounts.has(a.username)) + appStorage.authenticatedAccounts = [...appStorage.authenticatedAccounts, ...newAccounts] + } else { + appStorage.authenticatedAccounts = data.accountTokens + } + importedTypes.push('account tokens') + } + + alert(`Profile imported successfully! Imported data: ${importedTypes.join(', ')}.\nYou may need to reload the page for some changes to take effect.`) + } catch (err) { + console.error('Failed to import profile:', err) + alert('Failed to import profile: ' + (err.message || err)) + } +} + +export const exportData = async () => { + const data = await showInputsModal('Export Profile', { + profileName: { + type: 'text', + }, + exportSettings: { + type: 'checkbox', + defaultValue: true, + }, + exportKeybindings: { + type: 'checkbox', + defaultValue: true, + }, + exportServers: { + type: 'checkbox', + defaultValue: true, + }, + saveUsernameAndProxy: { + type: 'checkbox', + defaultValue: true, + }, + exportGlobalProxiesList: { + type: 'checkbox', + defaultValue: false, + }, + exportAccountTokens: { + type: 'checkbox', + defaultValue: false, + }, + }) + const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json` + const json: ExportedFile = { + _about: 'Minecraft Web Client (mcraft.fun) Profile', + ...data.exportSettings ? { + options: getChangedSettings(), + } : {}, + ...data.exportKeybindings ? { + keybindings: customKeymaps, + } : {}, + ...data.exportServers ? { + servers: appStorage.serversList, + } : {}, + ...data.saveUsernameAndProxy ? { + username: appStorage.username, + proxy: appStorage.proxiesData?.selected, + } : {}, + ...data.exportGlobalProxiesList ? { + proxies: appStorage.proxiesData?.proxies, + } : {}, + ...data.exportAccountTokens ? { + accountTokens: appStorage.authenticatedAccounts, + } : {}, + } + const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/core/progressReporter.ts b/src/core/progressReporter.ts new file mode 100644 index 00000000..75878fd2 --- /dev/null +++ b/src/core/progressReporter.ts @@ -0,0 +1,234 @@ +import { setLoadingScreenStatus } from '../appStatus' +import { appStatusState } from '../react/AppStatusProvider' +import { hideNotification, showNotification } from '../react/NotificationProvider' +import { pixelartIcons } from '../react/PixelartIcon' + +export interface ProgressReporter { + currentMessage: string | undefined + beginStage (stage: string, title: string): void + endStage (stage: string): void + setSubStage (stage: string, subStageTitle: string): void + reportProgress (stage: string, progress: number): void + executeWithMessage(message: string, fn: () => Promise): Promise + executeWithMessage(message: string, stage: string, fn: () => Promise): Promise + + setMessage (message: string): void + + end(): void + error(message: string): void +} + +interface ReporterDisplayImplementation { + setMessage (message: string): void + end (): void + error(message: string): void +} + +interface StageInfo { + title: string + subStage?: string + progress?: number +} + +const NO_STAGES_ACTION_END = false + +const createProgressReporter = (implementation: ReporterDisplayImplementation): ProgressReporter => { + const stages = new Map() + let currentMessage: string | undefined + let ended = false + + const end = () => { + if (ended) return + ended = true + stages.clear() + implementation.end() + } + + const updateStatus = () => { + if (ended) return + const activeStages = [...stages.entries()] + if (activeStages.length === 0) { + if (NO_STAGES_ACTION_END) { + end() + } else { + implementation.setMessage('Waiting for tasks') + } + return + } + + const [currentStage, info] = activeStages.at(-1)! + let message = info.title + if (info.subStage) { + message += ` - ${info.subStage}` + } + if (info.progress !== undefined) { + const num = Math.round(info.progress * 100) + if (isFinite(num)) { + message += `: ${num}%` + } + } + + currentMessage = message + implementation.setMessage(message) + } + + const reporter = { + beginStage (stage: string, title: string) { + if (stages.has(stage)) { + throw new Error(`Stage ${stage} already is running`) + } + stages.set(stage, { title }) + updateStatus() + }, + + endStage (stage: string) { + stages.delete(stage) + updateStatus() + }, + + setSubStage (stage: string, subStageTitle: string) { + const info = stages.get(stage) + if (info) { + info.subStage = subStageTitle + updateStatus() + } + }, + + reportProgress (stage: string, progress: number) { + const info = stages.get(stage) + if (info) { + info.progress = progress + updateStatus() + } + }, + + async executeWithMessage(...args: any[]): Promise { + const message = args[0] + const stage = typeof args[1] === 'string' ? args[1] : undefined + const fn = typeof args[1] === 'string' ? args[2] : args[1] + + const tempStage = stage ?? 'temp-' + Math.random().toString(36).slice(2) + reporter.beginStage(tempStage, message) + try { + const result = await fn() + return result + } finally { + reporter.endStage(tempStage) + } + }, + + end (): void { + end() + }, + + setMessage (message: string): void { + if (ended) return + implementation.setMessage(message) + }, + + get currentMessage () { + return currentMessage + }, + + error (message: string): void { + if (ended) return + implementation.error(message) + } + } + + return reporter +} + +const fullScreenReporters = [] as ProgressReporter[] +export const createFullScreenProgressReporter = (): ProgressReporter => { + const reporter = createProgressReporter({ + setMessage (message: string) { + if (appStatusState.isError) return + setLoadingScreenStatus(message) + }, + end () { + if (appStatusState.isError) return + fullScreenReporters.splice(fullScreenReporters.indexOf(reporter), 1) + if (fullScreenReporters.length === 0) { + setLoadingScreenStatus(undefined) + } else { + setLoadingScreenStatus(fullScreenReporters.at(-1)!.currentMessage) + } + }, + + error (message: string): void { + if (appStatusState.isError) return + setLoadingScreenStatus(message, true) + } + }) + fullScreenReporters.push(reporter) + return reporter +} + +export const createNotificationProgressReporter = (endMessage?: string): ProgressReporter => { + const id = `progress-reporter-${Math.random().toString(36).slice(2)}` + return createProgressReporter({ + setMessage (message: string) { + showNotification(`${message}...`, '', false, '', undefined, true, id) + }, + end () { + if (endMessage) { + showNotification(endMessage, '', false, pixelartIcons.check, undefined, true) + } else { + hideNotification(id) + } + }, + + error (message: string): void { + showNotification(message, '', true, '', undefined, true) + } + }) +} + +export const createConsoleLogProgressReporter = (group?: string): ProgressReporter => { + return createProgressReporter({ + setMessage (message: string) { + console.log(group ? `[${group}] ${message}` : message) + }, + end () { + console.log(group ? `[${group}] done` : 'done') + }, + + error (message: string): void { + console.error(message) + } + }) +} + +export const createWrappedProgressReporter = (reporter: ProgressReporter, message?: string) => { + const stage = `wrapped-${message}` + if (message) { + reporter.beginStage(stage, message) + } + + return createProgressReporter({ + setMessage (message: string) { + reporter.setMessage(message) + }, + end () { + if (message) { + reporter.endStage(stage) + } + }, + + error (message: string): void { + reporter.error(message) + } + }) +} + +export const createNullProgressReporter = (): ProgressReporter => { + return createProgressReporter({ + setMessage (message: string) { + }, + end () { + }, + error (message: string) { + } + }) +} diff --git a/src/core/timers.ts b/src/core/timers.ts new file mode 100644 index 00000000..570f46c0 --- /dev/null +++ b/src/core/timers.ts @@ -0,0 +1,139 @@ +import { options } from '../optionsStorage' + +interface Timer { + id: number + callback: () => void + targetTime: number + isInterval: boolean + interval?: number + cleanup?: () => void +} + +let nextTimerId = 1 +const timers: Timer[] = [] + +// TODO implementation breaks tps (something is wrong with intervals) +const fixBrowserTimers = () => { + const originalSetTimeout = window.setTimeout + //@ts-expect-error + window.setTimeout = (callback: () => void, delay: number) => { + if (!delay) { + return originalSetTimeout(callback) + } + const id = nextTimerId++ + const targetTime = performance.now() + delay + timers.push({ id, callback, targetTime, isInterval: false }) + originalSetTimeout(() => { + checkTimers() + }, delay) + return id + } + + const originalSetInterval = window.setInterval + //@ts-expect-error + window.setInterval = (callback: () => void, interval: number) => { + if (!interval) { + return originalSetInterval(callback, interval) + } + const id = nextTimerId++ + const targetTime = performance.now() + interval + const originalInterval = originalSetInterval(() => { + checkTimers() + }, interval) + timers.push({ + id, + callback, + targetTime, + isInterval: true, + interval, + cleanup () { + originalClearInterval(originalInterval) + }, + }) + return id + } + + const originalClearTimeout = window.clearTimeout + //@ts-expect-error + window.clearTimeout = (id: number) => { + const index = timers.findIndex(t => t.id === id) + if (index !== -1) { + timers.splice(index, 1) + } + return originalClearTimeout(id) + } + + const originalClearInterval = window.clearInterval + //@ts-expect-error + window.clearInterval = (id: number) => { + const index = timers.findIndex(t => t.id === id) + if (index !== -1) { + const timer = timers[index] + if (timer.cleanup) { + timer.cleanup() + } + timers.splice(index, 1) + } + return originalClearInterval(id) + } +} + +export const checkTimers = () => { + const now = performance.now() + + let triggered = false + for (let i = timers.length - 1; i >= 0; i--) { + const timer = timers[i] + + if (now >= timer.targetTime) { + triggered = true + timer.callback() + + if (timer.isInterval && timer.interval) { + // Reschedule interval + timer.targetTime = now + timer.interval + } else { + // Remove one-time timer + timers.splice(i, 1) + } + } + } + + if (!triggered) { + console.log('No timers triggered!') + } +} + +// workaround for browser timers throttling after 5 minutes of tab inactivity +export const preventThrottlingWithSound = () => { + try { + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + const oscillator = audioContext.createOscillator() + const gainNode = audioContext.createGain() + + // Unfortunatelly cant use 0 + gainNode.gain.value = 0.001 + + // Connect nodes + oscillator.connect(gainNode) + gainNode.connect(audioContext.destination) + + // Use a very low frequency + oscillator.frequency.value = 1 + + // Start playing + oscillator.start() + + return async () => { + try { + oscillator.stop() + await audioContext.close() + } catch (err) { + console.warn('Error stopping silent audio:', err) + } + } + } catch (err) { + console.error('Error creating silent audio:', err) + return () => {} + } +} diff --git a/src/createLocalServer.ts b/src/createLocalServer.ts index 6c5b2fa9..d0beac9a 100644 --- a/src/createLocalServer.ts +++ b/src/createLocalServer.ts @@ -5,7 +5,6 @@ const { createMCServer } = require('flying-squid/dist') export const startLocalServer = (serverOptions) => { const passOptions = { ...serverOptions, Server: LocalServer } const server: NonNullable = createMCServer(passOptions) - //@ts-expect-error browser patch server.formatMessage = (message) => `[server] ${message}` server.options = passOptions //@ts-expect-error todo remove @@ -15,4 +14,4 @@ export const startLocalServer = (serverOptions) => { // features that flying-squid doesn't support at all // todo move & generate in flying-squid -export const unsupportedLocalServerFeatures = ['transactionPacketExists', 'teleportUsesOwnPacket', 'dimensionDataIsAvailable'] +export const unsupportedLocalServerFeatures = ['transactionPacketExists', 'teleportUsesOwnPacket'] diff --git a/src/crypto.js b/src/crypto.js deleted file mode 100644 index 9034a397..00000000 --- a/src/crypto.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from 'crypto-browserify' -export function createPublicKey () { } diff --git a/src/customChannels.ts b/src/customChannels.ts new file mode 100644 index 00000000..506ea776 --- /dev/null +++ b/src/customChannels.ts @@ -0,0 +1,504 @@ +import PItem from 'prismarine-item' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { options } from './optionsStorage' +import { jeiCustomCategories } from './inventoryWindows' +import { registerIdeChannels } from './core/ideChannels' + +export default () => { + customEvents.on('mineflayerBotCreated', async () => { + if (!options.customChannels) return + bot.once('login', () => { + registerBlockModelsChannel() + registerMediaChannels() + registerSectionAnimationChannels() + registeredJeiChannel() + registerBlockInteractionsCustomizationChannel() + registerWaypointChannels() + registerIdeChannels() + }) + }) +} + +const registerChannel = (channelName: string, packetStructure: any[], handler: (data: any) => void, waitForWorld = true) => { + bot._client.registerChannel(channelName, packetStructure, true) + bot._client.on(channelName as any, async (data) => { + if (waitForWorld) { + await appViewer.worldReady + handler(data) + } else { + handler(data) + } + }) + + console.debug(`registered custom channel ${channelName} channel`) +} + +const registerBlockInteractionsCustomizationChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:block-interactions-customization' + const packetStructure = [ + 'container', + [ + { + name: 'newConfiguration', + type: ['pstring', { countType: 'i16' }] + }, + ] + ] + + registerChannel(CHANNEL_NAME, packetStructure, (data) => { + const config = JSON.parse(data.newConfiguration) + bot.mouse.setConfigFromPacket(config) + }, true) +} + +const registerWaypointChannels = () => { + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'x', + type: 'f32' + }, + { + name: 'y', + type: 'f32' + }, + { + name: 'z', + type: 'f32' + }, + { + name: 'minDistance', + type: 'i32' + }, + { + name: 'label', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'color', + type: 'i32' + }, + { + name: 'metadataJson', + type: ['pstring', { countType: 'i16' }] + } + ] + ] + + registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => { + // Parse metadata if provided + let metadata: any = {} + if (data.metadataJson && data.metadataJson.trim() !== '') { + try { + metadata = JSON.parse(data.metadataJson) + } catch (error) { + console.warn('Failed to parse waypoint metadataJson:', error) + } + } + + getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, { + minDistance: data.minDistance, + label: data.label || undefined, + color: data.color || undefined, + metadata + }) + }) + + registerChannel('minecraft-web-client:waypoint-delete', [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + } + ] + ], (data) => { + getThreeJsRendererMethods()?.removeWaypoint(data.id) + }) +} + +const registerBlockModelsChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:blockmodels' + + const packetStructure = [ + 'container', + [ + { + name: 'worldName', // currently not used + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'x', + type: 'i32' + }, + { + name: 'y', + type: 'i32' + }, + { + name: 'z', + type: 'i32' + }, + { + name: 'model', + type: ['pstring', { countType: 'i16' }] + } + ] + ] + + registerChannel(CHANNEL_NAME, packetStructure, (data) => { + const { worldName, x, y, z, model } = data + + const chunkX = Math.floor(x / 16) * 16 + const chunkZ = Math.floor(z / 16) * 16 + const chunkKey = `${chunkX},${chunkZ}` + const blockPosKey = `${x},${y},${z}` + + getThreeJsRendererMethods()?.updateCustomBlock(chunkKey, blockPosKey, model) + }, true) +} + +const registerSectionAnimationChannels = () => { + const ADD_CHANNEL = 'minecraft-web-client:section-animation-add' + const REMOVE_CHANNEL = 'minecraft-web-client:section-animation-remove' + + /** + * Add a section animation + * @param id - Section position for animation like `16,32,16` + * @param offset - Initial offset in blocks + * @param speedX - Movement speed in blocks per second on X axis + * @param speedY - Movement speed in blocks per second on Y axis + * @param speedZ - Movement speed in blocks per second on Z axis + * @param limitX - Maximum offset in blocks on X axis (0 means no limit) + * @param limitY - Maximum offset in blocks on Y axis (0 means no limit) + * @param limitZ - Maximum offset in blocks on Z axis (0 means no limit) + */ + const addPacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] }, + { name: 'offset', type: 'f32' }, + { name: 'speedX', type: 'f32' }, + { name: 'speedY', type: 'f32' }, + { name: 'speedZ', type: 'f32' }, + { name: 'limitX', type: 'f32' }, + { name: 'limitY', type: 'f32' }, + { name: 'limitZ', type: 'f32' } + ] + ] + + /** + * Remove a section animation + * @param id - Identifier of the animation to remove + */ + const removePacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] } + ] + ] + + registerChannel(ADD_CHANNEL, addPacketStructure, (data) => { + const { id, offset, speedX, speedY, speedZ, limitX, limitY, limitZ } = data + getThreeJsRendererMethods()?.addSectionAnimation(id, { + time: performance.now(), + speedX, + speedY, + speedZ, + currentOffsetX: offset, + currentOffsetY: offset, + currentOffsetZ: offset, + limitX: limitX === 0 ? undefined : limitX, + limitY: limitY === 0 ? undefined : limitY, + limitZ: limitZ === 0 ? undefined : limitZ + }) + }, true) + + registerChannel(REMOVE_CHANNEL, removePacketStructure, (data) => { + const { id } = data + getThreeJsRendererMethods()?.removeSectionAnimation(id) + }, true) + + console.debug('Registered section animation channels') +} + +window.testSectionAnimation = (speedY = 1) => { + const pos = bot.entity.position + const id = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}` + getThreeJsRendererMethods()?.addSectionAnimation(id, { + time: performance.now(), + speedX: 0, + speedY, + speedZ: 0, + currentOffsetX: 0, + currentOffsetY: 0, + currentOffsetZ: 0, + // limitX: 10, + // limitY: 10, + }) +} + +const registeredJeiChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:jei' + // id - string, categoryTitle - string, items - string (json array) + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: '_categoryTitle', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'items', + type: ['pstring', { countType: 'i16' }] + }, + ] + ] + + bot._client.registerChannel(CHANNEL_NAME, packetStructure, true) + + bot._client.on(CHANNEL_NAME as any, (data) => { + const { id, categoryTitle, items } = data + if (items === '') { + // remove category + jeiCustomCategories.value = jeiCustomCategories.value.filter(x => x.id !== id) + return + } + const PrismarineItem = PItem(bot.version) + jeiCustomCategories.value.push({ + id, + categoryTitle, + items: JSON.parse(items).map(x => { + const itemString = x.itemName || x.item_name || x.item || x.itemId + const itemId = loadedData.itemsByName[itemString.replace('minecraft:', '')] + if (!itemId) { + console.warn(`Could not add item ${itemString} to JEI category ${categoryTitle} because it was not found`) + return null + } + // const item = new PrismarineItem(itemId.id, x.itemCount || x.item_count || x.count || 1, x.itemDamage || x.item_damage || x.damage || 0, x.itemNbt || x.item_nbt || x.nbt || null) + return PrismarineItem.fromNotch({ + ...x, + itemId: itemId.id, + }) + }) + }) + }) + + console.debug(`registered custom channel ${CHANNEL_NAME} channel`) +} + +const registerMediaChannels = () => { + // Media Add Channel + const ADD_CHANNEL = 'minecraft-web-client:media-add' + const addPacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] }, + { name: 'x', type: 'f32' }, + { name: 'y', type: 'f32' }, + { name: 'z', type: 'f32' }, + { name: 'width', type: 'f32' }, + { name: 'height', type: 'f32' }, + { name: 'rotation', type: 'i16' }, // 0: 0° - towards positive z, 1: 90° - positive x, 2: 180° - negative z, 3: 270° - negative x (3-6 is same but double side) + { name: 'source', type: ['pstring', { countType: 'i16' }] }, + { name: 'loop', type: 'bool' }, + { name: 'volume', type: 'f32' }, // 0 + { name: '_aspectRatioMode', type: 'i16' }, // 0 + { name: '_background', type: 'i16' }, // 0 + { name: '_opacity', type: 'i16' }, // 1 + { name: '_cropXStart', type: 'f32' }, // 0 + { name: '_cropYStart', type: 'f32' }, // 0 + { name: '_cropXEnd', type: 'f32' }, // 0 + { name: '_cropYEnd', type: 'f32' }, // 0 + ] + ] + + // Media Control Channels + const PLAY_CHANNEL = 'minecraft-web-client:media-play' + const PAUSE_CHANNEL = 'minecraft-web-client:media-pause' + const SEEK_CHANNEL = 'minecraft-web-client:media-seek' + const VOLUME_CHANNEL = 'minecraft-web-client:media-volume' + const SPEED_CHANNEL = 'minecraft-web-client:media-speed' + const DESTROY_CHANNEL = 'minecraft-web-client:media-destroy' + + const noDataPacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] } + ] + ] + + const setNumberPacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] }, + { name: 'seconds', type: 'f32' } + ] + ] + + // Register channels + registerChannel(PLAY_CHANNEL, noDataPacketStructure, (data) => { + const { id } = data + getThreeJsRendererMethods()?.setVideoPlaying(id, true) + }, true) + registerChannel(PAUSE_CHANNEL, noDataPacketStructure, (data) => { + const { id } = data + getThreeJsRendererMethods()?.setVideoPlaying(id, false) + }, true) + registerChannel(SEEK_CHANNEL, setNumberPacketStructure, (data) => { + const { id, seconds } = data + getThreeJsRendererMethods()?.setVideoSeeking(id, seconds) + }, true) + registerChannel(VOLUME_CHANNEL, setNumberPacketStructure, (data) => { + const { id, volume } = data + getThreeJsRendererMethods()?.setVideoVolume(id, volume) + }, true) + registerChannel(SPEED_CHANNEL, setNumberPacketStructure, (data) => { + const { id, speed } = data + getThreeJsRendererMethods()?.setVideoSpeed(id, speed) + }, true) + registerChannel(DESTROY_CHANNEL, noDataPacketStructure, (data) => { + const { id } = data + getThreeJsRendererMethods()?.destroyMedia(id) + }, true) + + // Handle media add + registerChannel(ADD_CHANNEL, addPacketStructure, (data) => { + const { id, x, y, z, width, height, rotation, source, loop, volume, background, opacity } = data + + // Add new video + getThreeJsRendererMethods()?.addMedia(id, { + position: { x, y, z }, + size: { width, height }, + // side: 'towards', + src: source, + rotation: rotation as 0 | 1 | 2 | 3, + doubleSide: false, + background, + opacity: opacity / 100, + allowOrigins: options.remoteContentNotSameOrigin === false ? [getCurrentTopDomain()] : options.remoteContentNotSameOrigin, + loop, + volume + }) + }) + + // --- + + // Video interaction channel + const interactionPacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] }, + { name: 'x', type: 'f32' }, + { name: 'y', type: 'f32' }, + { name: 'isRightClick', type: 'bool' } + ] + ] + + bot._client.registerChannel(MEDIA_INTERACTION_CHANNEL, interactionPacketStructure, true) + + // Media play channel + bot._client.registerChannel(MEDIA_PLAY_CHANNEL_CLIENTBOUND, noDataPacketStructure, true) + const mediaStopPacketStructure = [ + 'container', + [ + { name: 'id', type: ['pstring', { countType: 'i16' }] }, + // ended - emitted even when loop is true (will continue playing) + // error: ... + // stalled - connection drops, server stops sending data + // waiting - connection is slow, server is sending data, but not fast enough (buffering) + // control + { name: 'reason', type: ['pstring', { countType: 'i16' }] }, + { name: 'time', type: 'f32' } + ] + ] + bot._client.registerChannel(MEDIA_STOP_CHANNEL_CLIENTBOUND, mediaStopPacketStructure, true) + + console.debug('Registered media channels') +} + +const MEDIA_INTERACTION_CHANNEL = 'minecraft-web-client:media-interaction' +const MEDIA_PLAY_CHANNEL_CLIENTBOUND = 'minecraft-web-client:media-play' +const MEDIA_STOP_CHANNEL_CLIENTBOUND = 'minecraft-web-client:media-stop' + +export const sendVideoInteraction = (id: string, x: number, y: number, isRightClick: boolean) => { + bot._client.writeChannel(MEDIA_INTERACTION_CHANNEL, { id, x, y, isRightClick }) +} + +export const sendVideoPlay = (id: string) => { + bot._client.writeChannel(MEDIA_PLAY_CHANNEL_CLIENTBOUND, { id }) +} + +export const sendVideoStop = (id: string, reason: string, time: number) => { + bot._client.writeChannel(MEDIA_STOP_CHANNEL_CLIENTBOUND, { id, reason, time }) +} + +export const videoCursorInteraction = () => { + const { intersectMedia } = appViewer.rendererState.world + if (!intersectMedia) return null + return intersectMedia +} +window.videoCursorInteraction = videoCursorInteraction + +const addTestVideo = (rotation = 0 as 0 | 1 | 2 | 3, scale = 1, isImage = false) => { + const block = window.cursorBlockRel() + if (!block) return + const { position: startPosition } = block + + // Add video with proper positioning + getThreeJsRendererMethods()?.addMedia('test-video', { + position: { + x: startPosition.x, + y: startPosition.y + 1, + z: startPosition.z + }, + size: { + width: scale, + height: scale + }, + src: isImage ? 'https://bucket.mcraft.fun/test_image.png' : 'https://bucket.mcraft.fun/test_video.mp4', + rotation, + // doubleSide: true, + background: 0x00_00_00, // Black color + // TODO broken + // uvMapping: { + // startU: 0, + // endU: 1, + // startV: 0, + // endV: 1 + // }, + opacity: 1, + allowOrigins: true, + }) +} +window.addTestVideo = addTestVideo + +function getCurrentTopDomain (): string { + const { hostname } = location + // Split hostname into parts + const parts = hostname.split('.') + + // Handle special cases like co.uk, com.br, etc. + if (parts.length > 2) { + // Check for common country codes with additional segments + if (parts.at(-2) === 'co' || + parts.at(-2) === 'com' || + parts.at(-2) === 'org' || + parts.at(-2) === 'gov') { + // Return last 3 parts (e.g., example.co.uk) + return parts.slice(-3).join('.') + } + } + + // Return last 2 parts (e.g., example.com) + return parts.slice(-2).join('.') +} diff --git a/src/customClient.js b/src/customClient.js index e349a837..b1a99904 100644 --- a/src/customClient.js +++ b/src/customClient.js @@ -1,18 +1,19 @@ +//@ts-check +import * as nbt from 'prismarine-nbt' import { options } from './optionsStorage' -//@ts-check const { EventEmitter } = require('events') const debug = require('debug')('minecraft-protocol') const states = require('minecraft-protocol/src/states') window.serverDataChannel ??= {} export const customCommunication = { - sendData (data) { + sendData(data) { setTimeout(() => { window.serverDataChannel[this.isServer ? 'emitClient' : 'emitServer'](data) }) }, - receiverSetup (processData) { + receiverSetup(processData) { window.serverDataChannel[this.isServer ? 'emitServer' : 'emitClient'] = (data) => { processData(data) } @@ -20,18 +21,18 @@ export const customCommunication = { } class CustomChannelClient extends EventEmitter { - constructor (isServer, version) { + constructor(isServer, version) { super() this.version = version this.isServer = !!isServer this.state = states.HANDSHAKING } - get state () { + get state() { return this.protocolState } - setSerializer (state) { + setSerializer(state) { customCommunication.receiverSetup.call(this, (/** @type {{name, params, state?}} */parsed) => { if (!options.excludeCommunicationDebugEvents.includes(parsed.name)) { debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.name}`) @@ -42,7 +43,7 @@ class CustomChannelClient extends EventEmitter { } // eslint-disable-next-line @typescript-eslint/adjacent-overload-signatures, grouped-accessor-pairs - set state (newProperty) { + set state(newProperty) { const oldProperty = this.protocolState this.protocolState = newProperty @@ -51,13 +52,25 @@ class CustomChannelClient extends EventEmitter { this.emit('state', newProperty, oldProperty) } - end (reason) { - this._endReason = reason + end(endReason, fullReason) { + // eslint-disable-next-line unicorn/no-this-assignment + const client = this + if (client.state === states.PLAY) { + fullReason ||= loadedData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(endReason) }) + : JSON.stringify({ text: endReason }) + client.write('kick_disconnect', { reason: fullReason }) + } else if (client.state === states.LOGIN) { + fullReason ||= JSON.stringify({ text: endReason }) + client.write('disconnect', { reason: fullReason }) + } + + this._endReason = endReason this.emit('end', this._endReason) // still emits on server side only, doesn't send anything to our client } - write (name, params) { - if(!options.excludeCommunicationDebugEvents.includes(name)) { + write(name, params) { + if (!options.excludeCommunicationDebugEvents.includes(name)) { debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name) debug(params) } @@ -66,11 +79,11 @@ class CustomChannelClient extends EventEmitter { customCommunication.sendData.call(this, { name, params, state: this.state }) } - writeBundle (packets) { + writeBundle(packets) { // no-op } - writeRaw (buffer) { + writeRaw(buffer) { // no-op } } diff --git a/src/customCommands.ts b/src/customCommands.ts new file mode 100644 index 00000000..75c13f68 --- /dev/null +++ b/src/customCommands.ts @@ -0,0 +1,91 @@ +import { guiOptionsScheme, tryFindOptionConfig } from './optionsGuiScheme' +import { options } from './optionsStorage' + +export const customCommandsConfig = { + chat: { + input: [ + { + type: 'text', + placeholder: 'Command to send e.g. gamemode creative' + } + ], + handler ([command]) { + bot.chat(`/${command.replace(/^\//, '')}`) + } + }, + setOrToggleSetting: { + input: [ + { + type: 'select', + // maybe title case? + options: Object.keys(options) + }, + { + type: 'select', + options: ['toggle', 'set'] + }, + ([setting = '', action = ''] = []) => { + const value = options[setting] + if (!action || value === undefined || action === 'toggle') return null + if (action === 'set') { + const getBase = () => { + const config = tryFindOptionConfig(setting as any) + if (config && 'values' in config) { + return { + type: 'select', + options: config.values + } + } + if (config?.type === 'toggle' || typeof value === 'boolean') { + return { + type: 'select', + options: ['true', 'false'] + } + } + if (config?.type === 'slider' || value.type === 'number') { + return { + type: 'number', + } + } + return { + type: 'text' + } + } + return { + ...getBase(), + placeholder: value + } + } + } + ], + handler ([setting, action, value]) { + if (action === 'toggle' || action === undefined) { + const value = options[setting] + const config = tryFindOptionConfig(setting) + if (config && 'values' in config && config.values) { + const { values } = config + const currentIndex = values.indexOf(value) + const nextIndex = (currentIndex + 1) % values.length + options[setting] = values[nextIndex] + } else { + options[setting] = typeof value === 'boolean' ? !value : typeof value === 'number' ? value + 1 : value + } + } else { + options[setting] = value + } + } + }, + jsScripts: { + input: [ + { + type: 'text', + placeholder: 'JavaScript code to run in main thread (sensitive!)' + } + ], + handler ([code]) { + // eslint-disable-next-line no-new-func -- this is a feature, not a bug + new Function(code)() + } + }, + // openCommandsScreen: {} +} diff --git a/src/dayCycle.ts b/src/dayCycle.ts deleted file mode 100644 index b4bf58d8..00000000 --- a/src/dayCycle.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { options } from './optionsStorage' -import { assertDefined } from './utils' - -export default () => { - bot.on('time', () => { - assertDefined(viewer) - // 0 morning - const dayTotal = 24_000 - const evening = 11_500 - const night = 13_500 - const morningStart = 23_000 - const morningEnd = 23_961 - const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0 - - // todo check actual colors - const dayColorRainy = { r: 111 / 255, g: 156 / 255, b: 236 / 255 } - // todo yes, we should make animations (and rain) - // eslint-disable-next-line unicorn/numeric-separators-style - const dayColor = bot.isRaining ? dayColorRainy : { r: 0.6784313725490196, g: 0.8470588235294118, b: 0.9019607843137255 } // lightblue - // let newColor = dayColor - let int = 1 - if (timeProgress < evening) { - // stay dayily - } else if (timeProgress < night) { - const progressNorm = timeProgress - evening - const progressMax = night - evening - int = 1 - progressNorm / progressMax - } else if (timeProgress < morningStart) { - int = 0 - } else if (timeProgress < morningEnd) { - const progressNorm = timeProgress - morningStart - const progressMax = night - morningEnd - int = progressNorm / progressMax - } - // todo need to think wisely how to set these values & also move directional light around! - const colorInt = Math.max(int, 0.1) - viewer.scene.background = new THREE.Color(dayColor.r * colorInt, dayColor.g * colorInt, dayColor.b * colorInt) - viewer.ambientLight.intensity = Math.max(int, 0.25) - // directional light - viewer.directionalLight.intensity = Math.min(int, 0.5) - }) -} diff --git a/src/defaultLocalServerOptions.js b/src/defaultLocalServerOptions.js index 8e294616..3b93910d 100644 --- a/src/defaultLocalServerOptions.js +++ b/src/defaultLocalServerOptions.js @@ -8,6 +8,7 @@ module.exports = { 'gameMode': 0, 'difficulty': 0, 'worldFolder': 'world', + 'pluginsFolder': true, // todo set sid, disable entities auto-spawn 'generation': { // grass_field @@ -28,11 +29,11 @@ module.exports = { 'view-distance': 2, 'player-list-text': { 'header': 'Flying squid', - 'footer': 'Test server' + 'footer': 'Integrated server' }, keepAlive: false, 'everybody-op': true, 'max-entities': 100, - 'version': '1.14.4', - versionMajor: '1.14' + 'version': '1.18', + versionMajor: '1.18' } diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts new file mode 100644 index 00000000..48c1cfad --- /dev/null +++ b/src/defaultOptions.ts @@ -0,0 +1,159 @@ +export const defaultOptions = { + renderDistance: 3, + keepChunksDistance: 1, + multiplayerRenderDistance: 3, + closeConfirmation: true, + autoFullScreen: false, + mouseRawInput: true, + autoExitFullscreen: false, + localUsername: 'wanderer', + mouseSensX: 50, + mouseSensY: -1, + chatWidth: 320, + chatHeight: 180, + chatScale: 100, + chatOpacity: 100, + chatOpacityOpened: 100, + messagesLimit: 200, + volume: 50, + enableMusic: true, + musicVolume: 50, + // fov: 70, + fov: 75, + defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front', + guiScale: 3, + autoRequestCompletions: true, + touchButtonsSize: 40, + touchButtonsOpacity: 80, + touchButtonsPosition: 12, + touchControlsPositions: getDefaultTouchControlsPositions(), + touchControlsSize: getTouchControlsSize(), + touchMovementType: 'modern' as 'modern' | 'classic', + touchInteractionType: 'classic' as 'classic' | 'buttons', + gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power', + backgroundRendering: '20fps' as 'full' | '20fps' | '5fps', + /** @unstable */ + disableAssets: false, + /** @unstable */ + debugLogNotFrequentPackets: false, + unimplementedContainers: false, + dayCycleAndLighting: true, + loadPlayerSkins: true, + renderEars: true, + lowMemoryMode: false, + starfieldRendering: true, + defaultSkybox: true, + enabledResourcepack: null as string | null, + useVersionsTextures: 'latest', + serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', + showHand: true, + viewBobbing: true, + displayRecordButton: true, + packetsLoggerPreset: 'all' as 'all' | 'no-buffers', + serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string, + customChannels: false, + remoteContentNotSameOrigin: false as boolean | string[], + packetsRecordingAutoStart: false, + language: 'auto', + preciseMouseInput: false, + // todo ui setting, maybe enable by default? + waitForChunksRender: false as 'sp-only' | boolean, + jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>, + modsSupport: false, + modsAutoUpdate: 'check' as 'check' | 'never' | 'always', + modsUpdatePeriodCheck: 24, // hours + preventBackgroundTimeoutKick: false, + preventSleep: false, + debugContro: false, + debugChatScroll: false, + chatVanillaRestrictions: true, + debugResponseTimeIndicator: false, + chatPingExtension: true, + // antiAliasing: false, + topRightTimeDisplay: 'only-fullscreen' as 'only-fullscreen' | 'always' | 'never', + + clipWorldBelowY: undefined as undefined | number, // will be removed + disableSignsMapsSupport: false, + singleplayerAutoSave: false, + showChunkBorders: false, // todo rename option + frameLimit: false as number | false, + alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null, + alwaysShowMobileControls: false, + excludeCommunicationDebugEvents: [] as string[], + preventDevReloadWhilePlaying: false, + numWorkers: 4, + localServerOptions: { + gameMode: 1 + } as any, + saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always', + preferLoadReadonly: false, + experimentalClientSelfReload: false, + remoteSoundsSupport: false, + remoteSoundsLoadTimeout: 500, + disableLoadPrompts: false, + guestUsername: 'guest', + askGuestName: true, + errorReporting: true, + /** Actually might be useful */ + showCursorBlockInSpectator: false, + renderEntities: true, + smoothLighting: true, + newVersionsLighting: false, + chatSelect: true, + autoJump: 'auto' as 'auto' | 'always' | 'never', + autoParkour: false, + vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users + vrPageGameRendering: false, + renderDebug: 'basic' as 'none' | 'advanced' | 'basic', + rendererPerfDebugOverlay: false, + + // advanced bot options + autoRespawn: false, + mutedSounds: [] as string[], + plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>, + /** Wether to popup sign editor on server action */ + autoSignEditor: true, + wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never', + showMinimap: 'never' as 'always' | 'singleplayer' | 'never', + minimapOptimizations: true, + displayBossBars: true, + disabledUiParts: [] as string[], + neighborChunkUpdates: true, + highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic', + activeRenderer: 'threejs', + rendererSharedOptions: { + _experimentalSmoothChunkLoading: true, + _renderByChunks: false + } +} + +function getDefaultTouchControlsPositions () { + return { + action: [ + 70, + 76 + ], + sneak: [ + 84, + 76 + ], + break: [ + 70, + 57 + ], + jump: [ + 84, + 57 + ], + } as Record +} + +function getTouchControlsSize () { + return { + joystick: 55, + action: 36, + break: 36, + jump: 36, + sneak: 36, + } +} diff --git a/src/devReload.ts b/src/devReload.ts new file mode 100644 index 00000000..e778d8d4 --- /dev/null +++ b/src/devReload.ts @@ -0,0 +1,11 @@ +import { isMobile } from 'renderer/viewer/lib/simpleUtils' + +if (process.env.NODE_ENV === 'development') { + // mobile devtools + if (isMobile()) { + // can be changed to require('eruda') + //@ts-expect-error + void import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => eruda.init()) + } +} +console.log('JS Loaded in', Date.now() - window.startLoad) diff --git a/src/devtools.ts b/src/devtools.ts index e49fdcfc..1f8ef8e8 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -1,18 +1,324 @@ // global variables useful for debugging -import { getEntityCursor } from './worldInteractions' +import fs from 'fs' +import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree' +import { enable, disable, enabled } from 'debug' +import { Vec3 } from 'vec3' -// Object.defineProperty(window, 'cursorBlock', ) +customEvents.on('mineflayerBotCreated', () => { + window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => { + name = name.replace('packet_', '') + return [name, name] + })) + window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => { + name = name.replace('packet_', '') + return [name, name] + })) +}) +window.Vec3 = Vec3 window.cursorBlockRel = (x = 0, y = 0, z = 0) => { const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z) if (!newPos) return return bot.world.getBlock(newPos) } -window.cursorEntity = () => { - return getEntityCursor() +window.entityCursor = () => { + return bot.mouse.getCursorState().entity } // wanderer window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/9e487d23-2ffc-365a-b1f8-f38203f59233.dat').then(window.nbt.parse).then(console.log) + +Object.defineProperty(window, 'debugSceneChunks', { + get () { + if (!(window.world instanceof WorldRendererThree)) return undefined + return (window.world)?.getLoadedChunksRelative?.(bot.entity.position, true) + }, +}) + +window.chunkKey = (xRel = 0, zRel = 0) => { + const pos = bot.entity.position + return `${(Math.floor(pos.x / 16) + xRel) * 16},${(Math.floor(pos.z / 16) + zRel) * 16}` +} + +window.sectionKey = (xRel = 0, yRel = 0, zRel = 0) => { + const pos = bot.entity.position + return `${(Math.floor(pos.x / 16) + xRel) * 16},${(Math.floor(pos.y / 16) + yRel) * 16},${(Math.floor(pos.z / 16) + zRel) * 16}` +} + +window.keys = (obj) => Object.keys(obj) +window.values = (obj) => Object.values(obj) + +window.len = (obj) => Object.keys(obj).length + +customEvents.on('gameLoaded', () => { + bot._client.on('packet', (data, { name }) => { + if (sessionStorage.ignorePackets?.includes(name)) { + console.log('ignoring packet', name) + const oldEmit = bot._client.emit + let i = 0 + // ignore next 3 emits + //@ts-expect-error + bot._client.emit = (...args) => { + if (i++ === 3) { + oldEmit.apply(bot._client, args) + bot._client.emit = oldEmit + } + } + } + }) +}) + +window.inspectPacket = (packetName, isFromClient = false, fullOrListener: boolean | ((...args) => void) = false) => { + if (typeof isFromClient === 'function') { + fullOrListener = isFromClient + isFromClient = false + } + const listener = typeof fullOrListener === 'function' + ? (name, ...args) => fullOrListener(...args, name) + : (name, ...args) => { + const displayName = name === packetName ? name : `${name} (${packetName})` + console.log('packet', displayName, fullOrListener ? args : args[0]) + } + + // Pre-compile regex if using wildcards + const pattern = typeof packetName === 'string' && packetName.includes('*') + ? new RegExp('^' + packetName.replaceAll('*', '.*') + '$') + : null + + const packetNameListener = (name, data) => { + if (pattern) { + if (pattern.test(name)) { + listener(name, data) + } + } else if (name === packetName) { + listener(name, data) + } + } + const packetListener = (data, { name }) => { + packetNameListener(name, data) + } + + const attach = () => { + if (isFromClient) { + bot?._client.prependListener('writePacket', packetNameListener) + } else { + bot?._client.prependListener('packet_name', packetNameListener) + bot?._client.prependListener('packet', packetListener) + } + } + const detach = () => { + if (isFromClient) { + bot?._client.removeListener('writePacket', packetNameListener) + } else { + bot?._client.removeListener('packet_name', packetNameListener) + bot?._client.removeListener('packet', packetListener) + } + } + attach() + customEvents.on('mineflayerBotCreated', attach) + + const returnobj = {} + Object.defineProperty(returnobj, 'detach', { + get () { + detach() + customEvents.removeListener('mineflayerBotCreated', attach) + return true + }, + }) + return returnobj +} + +window.downloadFile = async (path: string) => { + if (!path.startsWith('/') && localServer) path = `${localServer.options.worldFolder}/${path}` + const data = await fs.promises.readFile(path) + const blob = new Blob([data], { type: 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = path.split('/').at(-1)! + a.click() + URL.revokeObjectURL(url) +} + +Object.defineProperty(window, 'debugToggle', { + get () { + localStorage.debug = localStorage.debug === '*' ? '' : '*' + if (enabled('*')) { + disable() + return 'disabled debug' + } else { + enable('*') + return 'enabled debug' + } + }, + set (v) { + enable(v) + localStorage.debug = v + console.log('Enabled debug for', v) + } +}) + +customEvents.on('gameLoaded', () => { + window.holdingBlock = (window.world as WorldRendererThree | undefined)?.holdingBlock +}) + +window.clearStorage = (...keysToKeep: string[]) => { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && !keysToKeep.includes(key)) { + localStorage.removeItem(key) + } + } + return `Cleared ${localStorage.length - keysToKeep.length} items from localStorage. Kept: ${keysToKeep.join(', ')}` +} + + +// PERF DEBUG + +// for advanced debugging, use with watch expression + +window.statsPerSecAvg = {} +let currentStatsPerSec = {} as Record +const waitingStatsPerSec = {} +window.markStart = (label) => { + waitingStatsPerSec[label] ??= [] + waitingStatsPerSec[label][0] = performance.now() +} +window.markEnd = (label) => { + if (!waitingStatsPerSec[label]?.[0]) return + currentStatsPerSec[label] ??= [] + currentStatsPerSec[label].push(performance.now() - waitingStatsPerSec[label][0]) + delete waitingStatsPerSec[label] +} +const updateStatsPerSecAvg = () => { + window.statsPerSecAvg = Object.fromEntries(Object.entries(currentStatsPerSec).map(([key, value]) => { + return [key, { + avg: value.reduce((a, b) => a + b, 0) / value.length, + count: value.length + }] + })) + currentStatsPerSec = {} +} + + +window.statsPerSec = {} +let statsPerSecCurrent = {} +let lastReset = performance.now() +window.addStatPerSec = (name) => { + statsPerSecCurrent[name] ??= 0 + statsPerSecCurrent[name]++ +} +window.statsPerSecCurrent = statsPerSecCurrent +setInterval(() => { + window.statsPerSec = { duration: Math.floor(performance.now() - lastReset), ...statsPerSecCurrent, } + statsPerSecCurrent = {} + window.statsPerSecCurrent = statsPerSecCurrent + updateStatsPerSecAvg() + lastReset = performance.now() +}, 1000) + +// --- + +// Add type declaration for performance.memory +declare global { + interface Performance { + memory?: { + usedJSHeapSize: number + totalJSHeapSize: number + jsHeapSizeLimit: number + } + } +} + +// Performance metrics WebSocket client +let ws: WebSocket | null = null +let wsReconnectTimeout: NodeJS.Timeout | null = null +let metricsInterval: NodeJS.Timeout | null = null + +// Start collecting metrics immediately +const startTime = performance.now() + +function collectAndSendMetrics () { + if (!ws || ws.readyState !== WebSocket.OPEN) return + + const metrics = { + loadTime: performance.now() - startTime, + memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024, + timestamp: Date.now() + } + + ws.send(JSON.stringify(metrics)) +} + +function getWebSocketUrl () { + const wsPort = process.env.WS_PORT + if (!wsPort) return null + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const { hostname } = window.location + return `${protocol}//${hostname}:${wsPort}` +} + +function connectWebSocket () { + if (ws) return + + const wsUrl = getWebSocketUrl() + if (!wsUrl) { + return + } + + ws = new WebSocket(wsUrl) + + ws.onopen = () => { + console.log('Connected to metrics server') + if (wsReconnectTimeout) { + clearTimeout(wsReconnectTimeout) + wsReconnectTimeout = null + } + + // Start sending metrics immediately after connection + collectAndSendMetrics() + + // Clear existing interval if any + if (metricsInterval) { + clearInterval(metricsInterval) + } + + // Set new interval + metricsInterval = setInterval(collectAndSendMetrics, 500) + } + + ws.onclose = () => { + console.log('Disconnected from metrics server') + ws = null + + // Clear metrics interval + if (metricsInterval) { + clearInterval(metricsInterval) + metricsInterval = null + } + + // Try to reconnect after 3 seconds + wsReconnectTimeout = setTimeout(connectWebSocket, 3000) + } + + ws.onerror = (error) => { + console.error('WebSocket error:', error) + } +} + +// Connect immediately +connectWebSocket() + +// Add command to request current metrics +window.requestMetrics = () => { + const metrics = { + loadTime: performance.now() - startTime, + memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024, + timestamp: Date.now() + } + console.log('Current metrics:', metrics) + return metrics +} diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index b3d3a059..1ff318ff 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -1,45 +1,38 @@ import prettyBytes from 'pretty-bytes' -import { openWorldZip } from './browserfs' -import { getResourcePackName, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './texturePack' -import { setLoadingScreenStatus } from './utils' +import { openWorldFromHttpDir, openWorldZip } from './browserfs' +import { getResourcePackNames, installResourcepackPack, resourcePackState, updateTexturePackInstalledState } from './resourcePack' +import { setLoadingScreenStatus } from './appStatus' +import { appQueryParams, appQueryParamsArray } from './appParams' +import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets' +import { createFullScreenProgressReporter } from './core/progressReporter' +import { ConnectOptions } from './connect' export const getFixedFilesize = (bytes: number) => { return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } +export const isInterestedInDownload = () => { + const { map, texturepack, replayFileUrl } = appQueryParams + const { mapDir } = appQueryParamsArray + return !!map || !!texturepack || !!replayFileUrl || !!mapDir +} + const inner = async () => { - const qs = new URLSearchParams(window.location.search) - let mapUrl = qs.get('map') - const texturepack = qs.get('texturepack') - // fixme - if (texturepack) mapUrl = texturepack - if (!mapUrl) return false + const { map, texturepack, replayFileUrl } = appQueryParams + const { mapDir } = appQueryParamsArray + return downloadAndOpenMapFromUrl(map, texturepack, mapDir, replayFileUrl) +} - if (texturepack) { - await updateTexturePackInstalledState() - if (resourcePackState.resourcePackInstalled) { - if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackName()} Continue?`)) return - } - } else { - const menu = document.getElementById('play-screen') - menu.style = 'display: none;' - } - const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25) - const downloadThing = texturepack ? 'texturepack' : 'world' - setLoadingScreenStatus(`Downloading ${downloadThing} ${name}...`) +export const downloadAndOpenMapFromUrl = async (mapUrl: string | undefined, texturepackUrl: string | undefined, mapUrlDir: string[] | undefined, replayFileUrl: string | undefined, connectOptions?: Partial) => { + if (replayFileUrl) { + setLoadingScreenStatus('Downloading replay file') + const response = await fetch(replayFileUrl) + const contentLength = response.headers?.get('Content-Length') + const size = contentLength ? +contentLength : undefined + const filename = replayFileUrl.split('/').pop() - const response = await fetch(mapUrl) - const contentType = response.headers.get('Content-Type') - if (!contentType || !contentType.startsWith('application/zip')) { - alert('Invalid map file') - } - const contentLengthStr = response.headers?.get('Content-Length') - const contentLength = contentLengthStr && +contentLengthStr - setLoadingScreenStatus(`Downloading ${downloadThing} ${name}: have to download ${contentLength && getFixedFilesize(contentLength)}...`) - - let downloadedBytes = 0 - const buffer = await new Response( - new ReadableStream({ + let downloadedBytes = 0 + const buffer = await new Response(new ReadableStream({ async start (controller) { if (!response.body) throw new Error('Server returned no response!') const reader = response.body.getReader() @@ -56,29 +49,102 @@ const inner = async () => { downloadedBytes += value.byteLength // Calculate download progress as a percentage - const progress = contentLength ? (downloadedBytes / contentLength) * 100 : undefined - setLoadingScreenStatus(`Download ${downloadThing} progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${contentLength && getFixedFilesize(contentLength)})`, false, true) - + const progress = size ? (downloadedBytes / size) * 100 : undefined + setLoadingScreenStatus(`Download replay file progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${size && getFixedFilesize(size)})`, false, true) // Pass the received data to the controller controller.enqueue(value) } }, + })).arrayBuffer() + + // Convert buffer to text, handling any compression automatically + const decoder = new TextDecoder() + const contents = decoder.decode(buffer) + + openFile({ + contents, + filename, + filesize: size }) - ).arrayBuffer() - if (texturepack) { - const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-30) - await installTexturePack(buffer, name) - } else { - await openWorldZip(buffer) + return true } + + const mapUrlDirGuess = appQueryParams.mapDirGuess + const mapUrlDirBaseUrl = appQueryParams.mapDirBaseUrl + if (mapUrlDir?.length) { + await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined) + return true + } + + if (mapUrlDirGuess) { + // await openWorldFromHttpDir(undefined, mapUrlDirGuess) + return true + } + + // fixme + if (texturepackUrl) mapUrl = texturepackUrl + if (!mapUrl) return false + + if (texturepackUrl) { + await updateTexturePackInstalledState() + if (resourcePackState.resourcePackInstalled) { + if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackNames()[0]} Continue?`)) return + } + } + const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25) + const downloadThing = texturepackUrl ? 'texturepack' : 'world' + setLoadingScreenStatus(`Downloading ${downloadThing} ${name}...`) + + const response = await fetch(mapUrl) + const contentType = response.headers.get('Content-Type') + if (!contentType || !contentType.startsWith('application/zip')) { + alert('Invalid map file') + } + const contentLengthStr = response.headers?.get('Content-Length') + const contentLength = contentLengthStr && +contentLengthStr + setLoadingScreenStatus(`Downloading ${downloadThing} ${name}: have to download ${contentLength && getFixedFilesize(contentLength)}...`) + + let downloadedBytes = 0 + const buffer = await new Response(new ReadableStream({ + async start (controller) { + if (!response.body) throw new Error('Server returned no response!') + const reader = response.body.getReader() + + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read() + + if (done) { + controller.close() + break + } + + downloadedBytes += value.byteLength + + // Calculate download progress as a percentage + const progress = contentLength ? (downloadedBytes / contentLength) * 100 : undefined + setLoadingScreenStatus(`Download ${downloadThing} progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${contentLength && getFixedFilesize(contentLength)})`, false, true) + + // Pass the received data to the controller + controller.enqueue(value) + } + }, + })).arrayBuffer() + if (texturepackUrl) { + const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-30) + await installResourcepackPack(buffer, createFullScreenProgressReporter(), name) + } else { + await openWorldZip(buffer, undefined, connectOptions) + } + return true } export default async () => { try { return await inner() } catch (err) { - setLoadingScreenStatus(`Failed to download. Either refresh page or remove mapUrl param from URL. Reason: ${err.message}`) + setLoadingScreenStatus(`Failed to download/open. Either refresh page or remove map param from URL. Reason: ${err.message}`) return true } } diff --git a/src/dragndrop.ts b/src/dragndrop.ts index 83aeb99f..5a16bc05 100644 --- a/src/dragndrop.ts +++ b/src/dragndrop.ts @@ -3,13 +3,19 @@ import fs from 'fs' import * as nbt from 'prismarine-nbt' import RegionFile from 'prismarine-provider-anvil/src/region' import { versions } from 'minecraft-data' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' import { openWorldDirectory, openWorldZip } from './browserfs' -import { isGameActive, showNotification } from './globalState' +import { isGameActive } from './globalState' +import { showNotification } from './react/NotificationProvider' +import { openFile, VALID_REPLAY_EXTENSIONS } from './packetsReplay/replayPackets' const parseNbt = promisify(nbt.parse) const simplifyNbt = nbt.simplify window.nbt = nbt +// Supported image types for skybox +const VALID_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'] + // todo display drop zone for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) { window.addEventListener(event, (e: any) => { @@ -43,6 +49,34 @@ window.addEventListener('drop', async e => { }) async function handleDroppedFile (file: File) { + // Check for image files first when game is active + if (isGameActive(false) && VALID_IMAGE_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext))) { + try { + // Convert image to base64 + const reader = new FileReader() + const base64Promise = new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + }) + reader.readAsDataURL(file) + const base64Image = await base64Promise + + // Get ThreeJS backend methods and update skybox + const setSkyboxImage = getThreeJsRendererMethods()?.setSkyboxImage + if (setSkyboxImage) { + await setSkyboxImage(base64Image) + showNotification('Skybox updated successfully') + } else { + showNotification('Cannot update skybox - renderer does not support it') + } + return + } catch (err) { + console.error('Failed to update skybox:', err) + showNotification('Failed to update skybox', 'error') + return + } + } + if (file.name.endsWith('.zip')) { void openWorldZip(file) return @@ -52,10 +86,19 @@ async function handleDroppedFile (file: File) { alert('Rar files are not supported yet!') return } + if (VALID_REPLAY_EXTENSIONS.some(ext => file.name.endsWith(ext)) || file.name.startsWith('packets-replay')) { + const contents = await file.text() + openFile({ + contents, + filename: file.name, + filesize: file.size + }) + return + } if (file.name.endsWith('.mca')) { - const tempPath = '/data/temp.mca' + const tempPath = '/temp/temp.mca' try { - await fs.promises.writeFile(tempPath, Buffer.from(await file.arrayBuffer())) + await fs.promises.writeFile(tempPath, Buffer.from(await file.arrayBuffer()) as any) const region = new RegionFile(tempPath) await region.initialize() const chunks: Record = {} @@ -64,6 +107,8 @@ async function handleDroppedFile (file: File) { let versionDetected = false for (const [i, _] of Array.from({ length: 32 }).entries()) { for (const [k, _] of Array.from({ length: 32 }).entries()) { + // todo, may use faster reading, but features is not commonly used + // eslint-disable-next-line no-await-in-loop const nbt = await region.read(i, k) chunks[`${i},${k}`] = nbt if (nbt && !versionDetected) { @@ -100,9 +145,7 @@ async function handleDroppedFile (file: File) { alert('Couldn\'t parse nbt, ensure you are opening .dat or file (or .zip/folder with a world)') throw err }) - showNotification({ - message: `${file.name} data available in browser console`, - }) + showNotification(`${file.name} data available in browser console`) console.log('raw', parsed) console.log('simplified', nbt.simplify(parsed)) } diff --git a/src/entities.ts b/src/entities.ts index 0e000c12..674f91ef 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -1,101 +1,368 @@ import { Entity } from 'prismarine-entity' +import { versionToNumber } from 'renderer/viewer/common/utils' import tracker from '@nxg-org/mineflayer-tracker' +import { loader as autoJumpPlugin } from '@nxg-org/mineflayer-auto-jump' +import { subscribeKey } from 'valtio/utils' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { Team } from 'mineflayer' import { options, watchValue } from './optionsStorage' +import { gameAdditionalState, miscUiState } from './globalState' +import { EntityStatus } from './mineflayer/entityStatus' + + +const updateAutoJump = () => { + if (!bot?.autoJumper) return + const autoJump = options.autoParkour || (options.autoJump === 'auto' ? miscUiState.currentTouch && !miscUiState.usingGamepadInput : options.autoJump === 'always') + bot.autoJumper.setOpts({ + // jumpIntoWater: options.autoParkour, + jumpOnAllEdges: options.autoParkour, + // strictBlockCollision: true, + }) + if (autoJump === bot.autoJumper.enabled) return + if (autoJump) { + bot.autoJumper.enable() + } else { + bot.autoJumper.disable() + } +} +subscribeKey(options, 'autoJump', () => { + updateAutoJump() +}) +subscribeKey(options, 'autoParkour', () => { + updateAutoJump() +}) +subscribeKey(miscUiState, 'usingGamepadInput', () => { + updateAutoJump() +}) +subscribeKey(miscUiState, 'currentTouch', () => { + updateAutoJump() +}) customEvents.on('gameLoaded', () => { bot.loadPlugin(tracker) + bot.loadPlugin(autoJumpPlugin) + updateAutoJump() - // todo cleanup (move to viewer, also shouldnt be used at all) const playerPerAnimation = {} as Record - const entityData = (e: Entity) => { + const checkEntityData = (e: Entity) => { if (!e.username) return window.debugEntityMetadata ??= {} window.debugEntityMetadata[e.username] = e - // todo entity spawn timing issue, check perf - if (viewer.entities.entities[e.id]?.playerObject) { + if (e.type === 'player') { bot.tracker.trackEntity(e) - const { playerObject } = viewer.entities.entities[e.id] - playerObject.backEquipment = e.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape' - if (playerObject.cape.map === null) { - playerObject.cape.visible = false - } - // todo (easy, important) elytra flying animation - // todo cleanup states } } + const trackBotEntity = () => { + // Always track the bot entity for animations + if (bot.entity) { + bot.tracker.trackEntity(bot.entity) + } + } + + let lastCall = 0 bot.on('physicsTick', () => { + // throttle, tps: 6 + if (Date.now() - lastCall < 166) return + lastCall = Date.now() for (const [id, { tracking, info }] of Object.entries(bot.tracker.trackingData)) { if (!tracking) continue - const e = bot.entities[id]! - const speed = info.avgSpeed + const e = bot.entities[id] + if (!e) continue + const speed = info.avgVel const WALKING_SPEED = 0.03 const SPRINTING_SPEED = 0.18 + const isCrouched = e === bot.entity ? gameAdditionalState.isSneaking : e['crouching'] const isWalking = Math.abs(speed.x) > WALKING_SPEED || Math.abs(speed.z) > WALKING_SPEED const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED - const newAnimation = isWalking ? (isSprinting ? 'running' : 'walking') : 'idle' - const username = e.username! - if (newAnimation !== playerPerAnimation[username]) { - viewer.entities.playAnimation(e.id, newAnimation) - playerPerAnimation[username] = newAnimation + + const newAnimation = + isCrouched ? (isWalking ? 'crouchWalking' : 'crouch') + : isWalking ? (isSprinting ? 'running' : 'walking') + : 'idle' + if (newAnimation !== playerPerAnimation[id]) { + // Handle bot entity animation specially (for player entity in third person) + if (e === bot.entity) { + getThreeJsRendererMethods()?.playEntityAnimation('player_entity', newAnimation) + } else { + getThreeJsRendererMethods()?.playEntityAnimation(e.id, newAnimation) + } + playerPerAnimation[id] = newAnimation } } }) bot.on('entitySwingArm', (e) => { - if (viewer.entities.entities[e.id]?.playerObject) { - viewer.entities.playAnimation(e.id, 'oneSwing') + getThreeJsRendererMethods()?.playEntityAnimation(e.id, 'oneSwing') + }) + + bot.on('botArmSwingStart', (hand) => { + if (hand === 'right') { + getThreeJsRendererMethods()?.playEntityAnimation('player_entity', 'oneSwing') } }) - const loadedSkinEntityIds = new Set() - - const playerRenderSkin = (e: Entity) => { - const mesh = viewer.entities.entities[e.id] - if (!mesh) return - if (!mesh.playerObject || !options.loadPlayerSkins) return - const MAX_DISTANCE_SKIN_LOAD = 128 - const distance = e.position.distanceTo(bot.entity.position) - if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (bot.settings.viewDistance as number) * 16) { - if (viewer.entities.entities[e.id]) { - if (loadedSkinEntityIds.has(e.id)) return - loadedSkinEntityIds.add(e.id) - viewer.entities.updatePlayerSkin(e.id, e.username, true, true) - } + bot.inventory.on('updateSlot', (slot) => { + if (slot === 5 || slot === 6 || slot === 7 || slot === 8) { + const item = bot.inventory.slots[slot]! + bot.entity.equipment[slot - 3] = item + appViewer.worldView?.emit('playerEntity', bot.entity) } + }) + bot.on('heldItemChanged', () => { + const item = bot.inventory.slots[bot.quickBarSlot + 36]! + bot.entity.equipment[0] = item + appViewer.worldView?.emit('playerEntity', bot.entity) + }) + + bot._client.on('damage_event', (data) => { + const { entityId, sourceTypeId: damage } = data + getThreeJsRendererMethods()?.damageEntity(entityId, damage) + }) + + bot._client.on('entity_status', (data) => { + if (versionToNumber(bot.version) >= versionToNumber('1.19.4')) return + const { entityId, entityStatus } = data + if (entityStatus === EntityStatus.HURT) { + getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus) + } + + if (entityStatus === EntityStatus.BURNED) { + updateEntityStates(entityId, true, true) + } + }) + + // on fire events + bot._client.on('entity_metadata', (data) => { + if (data.entityId !== bot.entity.id) return + handleEntityMetadata(data) + }) + + bot.on('end', () => { + if (onFireTimeout) { + clearTimeout(onFireTimeout) + } + }) + + bot.on('respawn', () => { + if (onFireTimeout) { + clearTimeout(onFireTimeout) + } + }) + + const updateCamera = (entity: Entity) => { + if (bot.game.gameMode !== 'spectator') return + bot.entity.position = entity.position.clone() + void bot.look(entity.yaw, entity.pitch, true) + bot.entity.yaw = entity.yaw + bot.entity.pitch = entity.pitch } - viewer.entities.addListener('remove', (e) => { - loadedSkinEntityIds.delete(e.id) - playerPerAnimation[e.username] = '' - bot.tracker.stopTrackingEntity(e, true) + + bot.on('entityGone', (entity) => { + bot.tracker.stopTrackingEntity(entity, true) }) bot.on('entityMoved', (e) => { - playerRenderSkin(e) - entityData(e) + checkEntityData(e) + if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) { + updateCamera(e) + } }) bot._client.on('entity_velocity', (packet) => { const e = bot.entities[packet.entityId] if (!e) return - entityData(e) - }) - - viewer.entities.addListener('add', (e) => { - if (!viewer.entities.entities[e.id]) throw new Error('mesh still not loaded') - playerRenderSkin(e) + checkEntityData(e) }) for (const entity of Object.values(bot.entities)) { if (entity !== bot.entity) { - entityData(entity) + checkEntityData(entity) } } - bot.on('entitySpawn', entityData) - bot.on('entityUpdate', entityData) - bot.on('entityEquip', entityData) + // Track bot entity initially + trackBotEntity() - watchValue(options, o => { - viewer.entities.setDebugMode(o.showChunkBorders ? 'basic' : 'none') + bot.on('entitySpawn', (e) => { + checkEntityData(e) + if (appViewer.playerState.reactive.cameraSpectatingEntity === e.id) { + updateCamera(e) + } }) + bot.on('entityUpdate', checkEntityData) + bot.on('entityEquip', checkEntityData) + + // Re-track bot entity after login + bot.on('login', () => { + setTimeout(() => { + trackBotEntity() + }) // Small delay to ensure bot.entity is properly set + }) + + bot._client.on('camera', (packet) => { + if (bot.player.entity.id === packet.cameraId) { + if (appViewer.playerState.utils.isSpectatingEntity() && appViewer.playerState.reactive.cameraSpectatingEntity) { + const entity = bot.entities[appViewer.playerState.reactive.cameraSpectatingEntity] + appViewer.playerState.reactive.cameraSpectatingEntity = undefined + if (entity) { + // do a force entity update + bot.emit('entityUpdate', entity) + } + } + } else if (appViewer.playerState.reactive.gameMode === 'spectator') { + const entity = bot.entities[packet.cameraId] + appViewer.playerState.reactive.cameraSpectatingEntity = packet.cameraId + if (entity) { + updateCamera(entity) + // do a force entity update + bot.emit('entityUpdate', entity) + } + } + }) + + const applySkinTexturesProxy = (url: string | undefined) => { + const { appConfig } = miscUiState + if (appConfig?.skinTexturesProxy) { + return url?.replace('http://textures.minecraft.net/', appConfig.skinTexturesProxy) + .replace('https://textures.minecraft.net/', appConfig.skinTexturesProxy) + } + return url + } + + // Texture override from packet properties + const updateSkin = (player: import('mineflayer').Player) => { + if (!player.uuid || !player.username || !player.skinData) return + + try { + const skinUrl = applySkinTexturesProxy(player.skinData.url) + const capeUrl = applySkinTexturesProxy((player.skinData as any).capeUrl) + + // Find entity with matching UUID and update skin + let entityId = '' + for (const [entId, entity] of Object.entries(bot.entities)) { + if (entity.uuid === player.uuid) { + entityId = entId + break + } + } + // even if not found, still record to cache + void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl) + } catch (err) { + reportError(new Error('Error applying skin texture:', { cause: err })) + } + } + + bot.on('playerJoined', updateSkin) + bot.on('playerUpdated', updateSkin) + for (const entity of Object.values(bot.players)) { + updateSkin(entity) + } + + const teamUpdated = (team: Team) => { + for (const entity of Object.values(bot.entities)) { + if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) { + bot.emit('entityUpdate', entity) + } + } + } + bot.on('teamUpdated', teamUpdated) + for (const team of Object.values(bot.teams)) { + teamUpdated(team) + } + + const updateEntityNameTags = (team: Team) => { + for (const entity of Object.values(bot.entities)) { + const entityTeam = entity.type === 'player' && entity.username ? bot.teamMap[entity.username] : entity.uuid ? bot.teamMap[entity.uuid] : undefined + if ((entityTeam?.nameTagVisibility === 'hideForOwnTeam' && entityTeam.name === team.name) + || (entityTeam?.nameTagVisibility === 'hideForOtherTeams' && entityTeam.name !== team.name)) { + bot.emit('entityUpdate', entity) + } + } + } + + const doEntitiesNeedUpdating = (team: Team) => { + return team.nameTagVisibility === 'never' + || (team.nameTagVisibility === 'hideForOtherTeams' && appViewer.playerState.reactive.team?.team !== team.team) + || (team.nameTagVisibility === 'hideForOwnTeam' && appViewer.playerState.reactive.team?.team === team.team) + } + + bot.on('teamMemberAdded', (team: Team, members: string[]) => { + if (members.includes(bot.username) && appViewer.playerState.reactive.team?.team !== team.team) { + appViewer.playerState.reactive.team = team + // Player was added to a team, need to check if any entities need updating + updateEntityNameTags(team) + } else if (doEntitiesNeedUpdating(team)) { + // Need to update all entities that were added + for (const entity of Object.values(bot.entities)) { + if (entity.type === 'player' && entity.username && members.includes(entity.username) || entity.uuid && members.includes(entity.uuid)) { + bot.emit('entityUpdate', entity) + } + } + } + }) + + bot.on('teamMemberRemoved', (team: Team, members: string[]) => { + if (members.includes(bot.username) && appViewer.playerState.reactive.team?.team === team.team) { + appViewer.playerState.reactive.team = undefined + // Player was removed from a team, need to check if any entities need updating + updateEntityNameTags(team) + } else if (doEntitiesNeedUpdating(team)) { + // Need to update all entities that were removed + for (const entity of Object.values(bot.entities)) { + if (entity.type === 'player' && entity.username && members.includes(entity.username) || entity.uuid && members.includes(entity.uuid)) { + bot.emit('entityUpdate', entity) + } + } + } + }) + + bot.on('teamRemoved', (team: Team) => { + if (appViewer.playerState.reactive.team?.team === team?.team) { + appViewer.playerState.reactive.team = undefined + // Player's team was removed, need to update all entities that are in a team + updateEntityNameTags(team) + } + }) + }) + +// Constants +const SHARED_FLAGS_KEY = 0 +const ENTITY_FLAGS = { + ON_FIRE: 0x01, // Bit 0 + SNEAKING: 0x02, // Bit 1 + SPRINTING: 0x08, // Bit 3 + SWIMMING: 0x10, // Bit 4 + INVISIBLE: 0x20, // Bit 5 + GLOWING: 0x40, // Bit 6 + FALL_FLYING: 0x80 // Bit 7 (elytra flying) +} + +let onFireTimeout: NodeJS.Timeout | undefined +const updateEntityStates = (entityId: number, onFire: boolean, timeout?: boolean) => { + if (entityId !== bot.entity.id) return + appViewer.playerState.reactive.onFire = onFire + if (onFireTimeout) { + clearTimeout(onFireTimeout) + } + if (timeout) { + onFireTimeout = setTimeout(() => { + updateEntityStates(entityId, false, false) + }, 5000) + } +} + +// Process entity metadata packet +function handleEntityMetadata (packet: { entityId: number, metadata: Array<{ key: number, type: string, value: number }> }) { + const { entityId, metadata } = packet + + // Find shared flags in metadata + const flagsData = metadata.find(meta => meta.key === SHARED_FLAGS_KEY && + meta.type === 'byte') + + // Update fire state if flags were found + if (flagsData) { + const wasOnFire = appViewer.playerState.reactive.onFire + appViewer.playerState.reactive.onFire = (flagsData.value & ENTITY_FLAGS.ON_FIRE) !== 0 + } +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000..e565fcec --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,37 @@ +declare namespace NodeJS { + interface ProcessEnv { + // Build configuration + NODE_ENV: 'development' | 'production' + MIN_MC_VERSION?: string + MAX_MC_VERSION?: string + ALWAYS_COMPRESS_LARGE_DATA?: 'true' | 'false' + SINGLE_FILE_BUILD?: 'true' | 'false' + WS_PORT?: string + DISABLE_SERVICE_WORKER?: 'true' | 'false' + CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE' + LOCAL_CONFIG_FILE?: string + BUILD_VERSION?: string + + // Build internals + GITHUB_REPOSITORY?: string + VERCEL_GIT_REPO_OWNER?: string + VERCEL_GIT_REPO_SLUG?: string + + // UI + MAIN_MENU_LINKS?: string + ALWAYS_MINIMAL_SERVER_UI?: 'true' | 'false' + + // App features + ENABLE_COOKIE_STORAGE?: string + COOKIE_STORAGE_PREFIX?: string + + // Build info. Release information + RELEASE_TAG?: string + RELEASE_LINK?: string + RELEASE_CHANGELOG?: string + + // Build info + INLINED_APP_CONFIG?: string + GITHUB_URL?: string + } +} diff --git a/src/errorLoadingScreenHelpers.ts b/src/errorLoadingScreenHelpers.ts new file mode 100644 index 00000000..2f882f89 --- /dev/null +++ b/src/errorLoadingScreenHelpers.ts @@ -0,0 +1,12 @@ +export const guessProblem = (errorMessage: string) => { + if (errorMessage.endsWith('Socket error: ECONNREFUSED')) { + return 'Most probably the server is not running.' + } +} + +export const loadingTexts = [ + 'Like the project? Give us a star on GitHub or rate us on AlternativeTo!', + 'To stay updated with the latest changes, go to the GitHub page, click on "Watch", choose "Custom", and then opt for "Releases"!', + 'Upvote features on GitHub issues to help us prioritize them!', + 'Want to contribute to the project? Check out Contributing.md on GitHub!', +] diff --git a/src/eruda.js b/src/eruda.js deleted file mode 100644 index 61de5964..00000000 --- a/src/eruda.js +++ /dev/null @@ -1,6 +0,0 @@ -import { isMobile } from './menus/components/common' - -if (process.env.NODE_ENV === 'development' && isMobile()) { - require('eruda').default.init() - console.log('JS Loaded in', Date.now() - window.startLoad) -} diff --git a/src/external/index.ts b/src/external/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/flyingSquidEvents.ts b/src/flyingSquidEvents.ts new file mode 100644 index 00000000..7231dd27 --- /dev/null +++ b/src/flyingSquidEvents.ts @@ -0,0 +1,27 @@ +import { saveServer } from './flyingSquidUtils' +import { watchUnloadForCleanup } from './gameUnload' +import { showModal } from './globalState' +import { options } from './optionsStorage' +import { chatInputValueGlobal } from './react/Chat' +import { showNotification } from './react/NotificationProvider' + +export default () => { + localServer!.on('warpsLoaded', () => { + if (!localServer) return + showNotification(`${localServer.warps.length} Warps loaded`, 'Use /warp to teleport to a warp point.', false, 'label-alt', () => { + chatInputValueGlobal.value = '/warp ' + showModal({ reactType: 'chat' }) + }) + }) + + if (options.singleplayerAutoSave) { + const autoSaveInterval = setInterval(() => { + if (options.singleplayerAutoSave) { + void saveServer(true) + } + }, 2000) + watchUnloadForCleanup(() => { + clearInterval(autoSaveInterval) + }) + } +} diff --git a/src/flyingSquidUtils.ts b/src/flyingSquidUtils.ts index 2590468d..2ae0be7c 100644 --- a/src/flyingSquidUtils.ts +++ b/src/flyingSquidUtils.ts @@ -18,24 +18,27 @@ export function nameToMcOfflineUUID (name) { } export async function savePlayers (autoSave: boolean) { + if (!localServer?.players[0]) return if (autoSave && new URL(location.href).searchParams.get('noSave') === 'true') return //@ts-expect-error TODO - await localServer!.savePlayersSingleplayer() + await localServer.savePlayersSingleplayer() } // todo flying squid should expose save function instead export const saveServer = async (autoSave = true) => { if (!localServer || fsState.isReadonly) return // todo + console.time('save server') const worlds = [(localServer as any).overworld] as Array - await Promise.all([savePlayers(autoSave), ...worlds.map(async world => world.saveNow())]) + await Promise.all([localServer.writeLevelDat(), savePlayers(autoSave), ...worlds.map(async world => world.saveNow())]) + console.timeEnd('save server') } export const disconnect = async () => { if (localServer) { await saveServer() - //@ts-expect-error todo expose! void localServer.quit() // todo investigate we should await } window.history.replaceState({}, '', `${window.location.pathname}`) // remove qs bot.end('You left the server') + location.reload() } diff --git a/src/generatedServerPackets.ts b/src/generatedServerPackets.ts index ffc232c5..3b3ce018 100644 --- a/src/generatedServerPackets.ts +++ b/src/generatedServerPackets.ts @@ -6,14 +6,7 @@ export interface ClientOnMap { } | /** 1.12.2 */ { keepAliveId: bigint; }; - login: /** 1.7 */ { - entityId: number; - gameMode: number; - dimension: number; - difficulty: number; - maxPlayers: number; - levelType: string; - } | /** 1.8 */ { + login:/** 1.8 */ { entityId: number; gameMode: number; dimension: number; @@ -148,9 +141,7 @@ export interface ClientOnMap { entityId: number; equipments: any; }; - spawn_position: /** 1.7 */ { - location: any; - } | /** 1.8 */ { + spawn_position:/** 1.8 */ { location: { x: number, y: number, z: number }; } | /** 1.17 */ { location: { x: number, y: number, z: number }; @@ -215,14 +206,7 @@ export interface ClientOnMap { death: any; portalCooldown: number; }; - position: /** 1.7 */ { - x: number; - y: number; - z: number; - yaw: number; - pitch: number; - onGround: boolean; - } | /** 1.8 */ { + position: /** 1.8 */ { x: number; y: number; z: number; @@ -905,11 +889,7 @@ export interface ClientOnMap { statistics: /** 1.7 */ { entries: any; }; - player_info: /** 1.7 */ { - playerName: string; - online: boolean; - ping: number; - } | /** 1.8 */ { + player_info: /** 1.8 */ { action: number; data: any; }; @@ -926,22 +906,13 @@ export interface ClientOnMap { length: number; matches: any; }; - scoreboard_objective: /** 1.7 */ { - name: string; - displayText: string; - action: number; - } | /** 1.8 */ { + scoreboard_objective:/** 1.8 */ { name: string; action: number; displayText: any; type: any; }; - scoreboard_score: /** 1.7 */ { - itemName: string; - action: number; - scoreName: any; - value: any; - } | /** 1.8 */ { + scoreboard_score:/** 1.8 */ { itemName: string; action: number; scoreName: string; @@ -1493,3 +1464,11 @@ export interface ClientOnMap { yaw: number; }; } + +type ClientOnMcProtocolEvents = ClientOnMap & { + [x: `raw.${string}`]: any + packet: any + state: any +} + +export declare const clientOn: (name: T, callback: (data: ClientOnMcProtocolEvents[T], packetMeta: import('minecraft-protocol').PacketMeta) => void) => void diff --git a/src/getCollisionInteractionShapes.ts b/src/getCollisionInteractionShapes.ts new file mode 100644 index 00000000..9dead22b --- /dev/null +++ b/src/getCollisionInteractionShapes.ts @@ -0,0 +1,17 @@ +import { getRenamedData } from 'flying-squid/dist/blockRenames' +import outputInteractionShapesJson from './interactionShapesGenerated.json' +import './getCollisionShapes' + +export default () => { + customEvents.on('gameLoaded', () => { + // todo also remap block states (e.g. redstone)! + const renamedBlocksInteraction = getRenamedData('blocks', Object.keys(outputInteractionShapesJson), '1.20.2', bot.version) + const interactionShapes = { + ...outputInteractionShapesJson, + ...Object.fromEntries(Object.entries(outputInteractionShapesJson).map(([block, shape], i) => [renamedBlocksInteraction[i], shape])) + } + interactionShapes[''] = interactionShapes['air'] + // todo make earlier + window.interactionShapes = interactionShapes + }) +} diff --git a/src/getCollisionShapes.ts b/src/getCollisionShapes.ts index 0faf5b6a..383adc0e 100644 --- a/src/getCollisionShapes.ts +++ b/src/getCollisionShapes.ts @@ -1,29 +1,14 @@ -import { adoptBlockOrItemNamesFromLatest } from 'flying-squid/dist/blockRenames' +import { getRenamedData } from 'flying-squid/dist/blockRenames' import collisionShapesInit from '../generated/latestBlockCollisionsShapes.json' -import outputInteractionShapesJson from './interactionShapesGenerated.json' // defining globally to be used in loaded data, not sure of better workaround window.globalGetCollisionShapes = (version) => { // todo use the same in resourcepack const versionFrom = collisionShapesInit.version - const renamedBlocks = adoptBlockOrItemNamesFromLatest('blocks', Object.keys(collisionShapesInit.blocks), versionFrom, version) + const renamedBlocks = getRenamedData('blocks', Object.keys(collisionShapesInit.blocks), versionFrom, version) const collisionShapes = { ...collisionShapesInit, blocks: Object.fromEntries(Object.entries(collisionShapesInit.blocks).map(([, shape], i) => [renamedBlocks[i], shape])) } return collisionShapes } - -export default () => { - customEvents.on('gameLoaded', () => { - // todo also remap block states (e.g. redstone)! - const renamedBlocksInteraction = adoptBlockOrItemNamesFromLatest('blocks', Object.keys(outputInteractionShapesJson), '1.20.2', bot.version) - const interactionShapes = { - ...outputInteractionShapesJson, - ...Object.fromEntries(Object.entries(outputInteractionShapesJson).map(([block, shape], i) => [renamedBlocksInteraction[i], shape])) - } - interactionShapes[''] = interactionShapes['air'] - // todo make earlier - window.interactionShapes = interactionShapes - }) -} diff --git a/src/globalDomListeners.ts b/src/globalDomListeners.ts index 866c9784..bfce0d42 100644 --- a/src/globalDomListeners.ts +++ b/src/globalDomListeners.ts @@ -1,6 +1,7 @@ import { saveServer } from './flyingSquidUtils' import { isGameActive, activeModalStack } from './globalState' import { options } from './optionsStorage' +import { isInRealGameSession } from './utils' window.addEventListener('unload', (e) => { if (!window.justReloaded) { @@ -25,6 +26,7 @@ window.addEventListener('beforeunload', (event) => { if (!isGameActive(true) && activeModalStack.at(-1)?.elem?.id !== 'chat') return if (sessionStorage.lastReload && !options.preventDevReloadWhilePlaying) return if (!options.closeConfirmation) return + if (!isInRealGameSession()) return // For major browsers doning only this is enough event.preventDefault() @@ -33,3 +35,12 @@ window.addEventListener('beforeunload', (event) => { event.returnValue = '' // Required for some browsers return 'The game is running. Are you sure you want to close this page?' }) + +window.addEventListener('contextmenu', (e) => { + const ALLOW_TAGS = ['INPUT', 'TEXTAREA', 'A'] + // allow if target is in ALLOW_TAGS or has selection text + if (ALLOW_TAGS.includes((e.target as HTMLElement)?.tagName) || window.getSelection()?.toString()) { + return + } + e.preventDefault() +}) diff --git a/src/globalState.ts b/src/globalState.ts index adaf4361..b8982de7 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -1,14 +1,20 @@ //@ts-check import { proxy, ref, subscribe } from 'valtio' -import { pointerLock } from './utils' +import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps' import type { OptionsGroupType } from './optionsGuiScheme' +import { options, disabledSettings } from './optionsStorage' +import { AppConfig } from './appConfig' // todo: refactor structure with support of hideNext=false -const notHideableModalsWithoutForce = new Set(['app-status']) +export const notHideableModalsWithoutForce = new Set([ + 'app-status', + 'divkit:nonclosable', + 'only-connect-server', +]) -type Modal = ({ elem?: HTMLElement & Record } & { reactType?: string }) +type Modal = ({ elem?: HTMLElement & Record } & { reactType: string }) type ContextMenuItem = { callback; label } @@ -25,45 +31,23 @@ export const activeModalStacks: Record = {} window.activeModalStack = activeModalStack -subscribe(activeModalStack, () => { - if (activeModalStack.length === 0) { - if (isGameActive(false)) { - void pointerLock.requestPointerLock() - } - } else { - document.exitPointerLock?.() - } -}) - -export const customDisplayManageKeyword = 'custom' - -const defaultModalActions = { - show (modal: Modal) { - if (modal.elem) modal.elem.style.display = 'block' - }, - hide (modal: Modal) { - if (modal.elem) modal.elem.style.display = 'none' - } -} - /** * @returns true if operation was successful */ const showModalInner = (modal: Modal) => { const cancel = modal.elem?.show?.() - if (cancel && cancel !== customDisplayManageKeyword) return false - if (cancel !== 'custom') defaultModalActions.show(modal) return true } -export const showModal = (elem: (HTMLElement & Record) | { reactType: string }) => { - const resolved = elem instanceof HTMLElement ? { elem: ref(elem) } : elem +export const showModal = (elem: /* (HTMLElement & Record) | */{ reactType: string } | string) => { + const resolved = typeof elem === 'string' ? { reactType: elem } : elem const curModal = activeModalStack.at(-1) - if (elem === curModal?.elem || (elem.reactType && elem.reactType === curModal?.reactType) || !showModalInner(resolved)) return - if (curModal) defaultModalActions.hide(curModal) + if ((resolved.reactType && resolved.reactType === curModal?.reactType) || !showModalInner(resolved)) return activeModalStack.push(resolved) } +window.showModal = showModal + /** * * @returns true if previous modal was restored @@ -71,21 +55,21 @@ export const showModal = (elem: (HTMLElement & Record) | { reactTyp export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined, options: { force?: boolean; restorePrevious?: boolean } = {}) => { const { force = false, restorePrevious = true } = options if (!modal) return - let cancel - if (modal.elem) { - cancel = modal.elem.hide?.(data) - } else if (modal.reactType) { - cancel = notHideableModalsWithoutForce.has(modal.reactType) ? !force : undefined - } - if (force && cancel !== customDisplayManageKeyword) { + let cancel = [...notHideableModalsWithoutForce].some(m => modal.reactType.startsWith(m)) ? !force : undefined + if (force) { cancel = undefined } - if (!cancel || cancel === customDisplayManageKeyword) { - if (cancel !== customDisplayManageKeyword) defaultModalActions.hide(modal) - activeModalStack.pop() + if (!cancel) { + const lastModal = activeModalStack.at(-1) + for (let i = activeModalStack.length - 1; i >= 0; i--) { + if (activeModalStack[i].reactType === modal.reactType) { + activeModalStack.splice(i, 1) + break + } + } const newModal = activeModalStack.at(-1) - if (newModal && restorePrevious) { + if (newModal && lastModal !== newModal && restorePrevious) { // would be great to ignore cancel I guess? showModalInner(newModal) } @@ -99,10 +83,21 @@ export const hideCurrentModal = (_data?, onHide?: () => void) => { } } +export const hideAllModals = () => { + while (activeModalStack.length > 0) { + if (!hideModal()) break + } + return activeModalStack.length === 0 +} + export const openOptionsMenu = (group: OptionsGroupType) => { showModal({ reactType: `options-${group}` }) } +subscribe(activeModalStack, () => { + document.body.style.setProperty('--has-modals-z', activeModalStack.length ? '-1' : null) +}) + // --- export const currentContextMenu = proxy({ items: [] as ContextMenuItem[] | null, x: 0, y: 0 }) @@ -117,29 +112,27 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY }) // --- -export type AppConfig = { - defaultHost?: string - defaultHostSave?: string - defaultProxy?: string - defaultProxySave?: string - defaultVersion?: string - mapsProvider?: string -} - export const miscUiState = proxy({ currentDisplayQr: null as string | null, currentTouch: null as boolean | null, + hasErrors: false, singleplayer: false, flyingSquid: false, wanOpened: false, + wanOpening: false, /** wether game hud is shown (in playing state) */ gameLoaded: false, + showUI: true, + showDebugHud: false, + loadedServerIndex: '', /** currently trying to load or loaded mc version, after all data is loaded */ loadedDataVersion: null as string | null, - appLoaded: false, + fsReady: false, + singleplayerAvailable: false, usingGamepadInput: false, appConfig: null as AppConfig | null, displaySearchInput: false, + displayFullmap: false }) export const isGameActive = (foregroundCheck: boolean) => { @@ -154,21 +147,13 @@ export const gameAdditionalState = proxy({ isFlying: false, isSprinting: false, isSneaking: false, + isZooming: false, + warps: [] as WorldWarp[], + noConnection: false, + poorConnection: false, + viewerConnection: false, + + usingServerResourcePack: false, }) window.gameAdditionalState = gameAdditionalState - -// rename current (non-stackable) notification to one-time (system) notification -const initialNotification = { - show: false, - autoHide: true, - message: '', - type: 'info', -} -export const notification = proxy(initialNotification) - -export const showNotification = (newNotification: Partial) => { - Object.assign(notification, { show: true, ...newNotification }, initialNotification) -} - -// todo restore auto-save on interval for player data! (or implement it in flying squid since there is already auto-save for world) diff --git a/src/globals.d.ts b/src/globals.d.ts index 6affacf7..7a2c6f1f 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,81 +1,56 @@ /// -declare const THREE: typeof import('three') // todo make optional declare const bot: Omit & { - world: import('prismarine-world').world.WorldSync - _client: import('minecraft-protocol').Client & { - write: typeof import('./generatedClientPackets').clientWrite - } + world: Omit & { + getBlock: (pos: import('vec3').Vec3) => import('prismarine-block').Block | null + } + _client: Omit & { + write: typeof import('./generatedClientPackets').clientWrite + on: typeof import('./generatedServerPackets').clientOn + } } declare const __type_bot: typeof bot -declare const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer -declare const worldView: import('prismarine-viewer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined +declare const appViewer: import('./appViewer').AppViewer +declare const worldView: import('renderer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined +declare const addStatPerSec: (name: string) => void declare const localServer: import('flying-squid/dist/index').FullServer & { options } | undefined /** all currently loaded mc data */ declare const mcData: Record -declare const loadedData: import('minecraft-data').IndexedData +declare const loadedData: import('minecraft-data').IndexedData & { sounds: Record } declare const customEvents: import('typed-emitter').default<{ - /** Singleplayer load requested */ - singleplayer (): void - digStart () - gameLoaded (): void - search (q: string): void + /** Singleplayer load requested */ + singleplayer (): void + digStart (): void + gameLoaded (): void + mineflayerBotCreated (): void + search (q: string): void + activateItem (item: Item, slot: number, offhand: boolean): void + hurtAnimation (yaw?: number): void + customChannelRegister (channel: string, parser: any): void }> declare const beforeRenderFrame: Array<() => void> +declare const translate: (key: T) => T + +// API LAYER +declare const toggleMicrophoneMuted: undefined | (() => void) +declare const translateText: undefined | ((text: string) => string) declare interface Document { - getElementById (id): any - exitPointerLock?(): void + exitPointerLock?(): void } -declare namespace JSX { - interface IntrinsicElements { - [elemName: string]: any - } +declare module '*.frag' { + const png: string + export default png +} +declare module '*.vert' { + const png: string + export default png +} +declare module '*.wgsl' { + const png: string + export default png } -declare interface DocumentFragment { - getElementById (id): HTMLElement & Record - querySelector (id): HTMLElement & Record -} - -declare interface Window extends Record { - -} - -type StringKeys = Extract - - -interface ObjectConstructor { - keys (obj: T): Array> - entries (obj: T): Array<[StringKeys, T[keyof T]]> - // todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right - fromEntries> (obj: T): Record - assign, K extends Record> (target: T, source: K): asserts target is T & K -} - -declare module '*.module.css' { - const css: Record - export default css -} -declare module '*.css' { - const css: string - export default css -} -declare module '*.json' { - const json: any - export = json -} -declare module '*.png' { - const png: string - export default png -} - -interface PromiseConstructor { - withResolvers (): { - resolve: (value: T) => void; - reject: (reason: any) => void; - promise: Promise; - } -} +declare interface Window extends Record { } diff --git a/src/globals.js b/src/globals.js index f9a1053c..11351555 100644 --- a/src/globals.js +++ b/src/globals.js @@ -1,9 +1,16 @@ import EventEmitter from 'events' +window.reportError = window.reportError ?? console.error window.bot = undefined window.THREE = undefined window.localServer = undefined window.worldView = undefined -window.viewer = undefined +window.viewer = undefined // legacy +window.appViewer = undefined window.loadedData = undefined window.customEvents = new EventEmitter() +window.customEvents.setMaxListeners(10_000) +window.translate = (key) => { + if (typeof key !== 'string') return key + return window.translateText?.(key) ?? key +} diff --git a/src/googledrive.ts b/src/googledrive.ts new file mode 100644 index 00000000..5e5e9ae9 --- /dev/null +++ b/src/googledrive.ts @@ -0,0 +1,110 @@ +import { GoogleOAuthProvider, useGoogleLogin } from '@react-oauth/google' +import { proxy, ref, subscribe } from 'valtio' +import React from 'react' +import { loadScript } from 'renderer/viewer/lib/utils' +import { loadGoogleDriveApi, loadInMemorySave } from './react/SingleplayerProvider' +import { setLoadingScreenStatus } from './appStatus' +import { showOptionsModal } from './react/SelectOption' +import { appQueryParams } from './appParams' + +const CLIENT_ID = '137156026346-igv2gkjsj2hlid92rs3q7cjjnc77s132.apps.googleusercontent.com' +// const CLIENT_ID = process.env.GOOGLE_CLIENT_ID +const SCOPES = 'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.install' +export const APP_ID = CLIENT_ID.split('-')[0] + +export const GoogleDriveProvider = ({ children }) => { + return React.createElement(GoogleOAuthProvider, { clientId: CLIENT_ID } as any, children) + // return +} + +export const isGoogleDriveAvailable = () => { + return !!CLIENT_ID +} + +export const useGoogleLogIn = () => { + const login = useGoogleLogin({ + onSuccess (tokenResponse) { + localStorage.hasEverLoggedIn = true + googleProviderState.accessToken = tokenResponse.access_token + googleProviderState.expiresIn = ref(new Date(Date.now() + tokenResponse.expires_in * 1000)) + googleProviderState.hasEverLoggedIn = true + }, + // prompt: hasEverLoggedIn ? 'none' : 'consent', + scope: SCOPES, + flow: 'implicit', + onError (error) { + const accessDenied = error.error === 'access_denied' || error.error === 'invalid_scope' || (error as any).error_subtype === 'access_denied' + if (accessDenied) { + googleProviderState.hasEverLoggedIn = false + } + } + }) + return () => login({ + prompt: googleProviderState.hasEverLoggedIn ? 'none' : 'consent' + }) +} + +export const possiblyHandleStateVariable = async () => { + const stateParam = appQueryParams.state + if (!stateParam) return + setLoadingScreenStatus('Opening world in read only mode, waiting for login...') + await loadGoogleDriveApi() + await loadScript('https://accounts.google.com/gsi/client') + const parsed = JSON.parse(stateParam) as { + ids: [string] + action: 'open' + userId: string + } + const tokenClient = window.google.accounts.oauth2.initTokenClient({ + client_id: CLIENT_ID, + scope: SCOPES, + async callback (response) { + if (response.error) { + setLoadingScreenStatus('Error: ' + response.error, true) + googleProviderState.hasEverLoggedIn = false + return + } + setLoadingScreenStatus('Opening world in read only mode...') + googleProviderState.accessToken = response.access_token + // await mountGoogleDriveFolder(true, parsed.ids[0]) + await loadInMemorySave('/google') + } + }) + const choice = await showOptionsModal('Select an action...', ['Login']) + if (choice === 'Login') { + tokenClient.requestAccessToken({ + prompt: googleProviderState.hasEverLoggedIn ? '' : 'consent', + }) + } else { + window.close() + } +} + +export const googleProviderState = proxy({ + accessToken: (localStorage.saveAccessToken ? localStorage.accessToken : null) as string | null, + hasEverLoggedIn: !!(localStorage.hasEverLoggedIn), + isReady: false, + expiresIn: localStorage.saveAccessToken ? ref(new Date(Date.now() + 1000 * 60 * 60)) : null, + readonlyMode: localStorage.googleReadonlyMode ? localStorage.googleReadonlyMode === 'true' : true, + lastSelectedFolder: (localStorage.lastSelectedFolder ? JSON.parse(localStorage.lastSelectedFolder) : null) as { + id: string + name: string + } | null +}) + +subscribe(googleProviderState, () => { + localStorage.googleReadonlyMode = googleProviderState.readonlyMode + localStorage.lastSelectedFolder = googleProviderState.lastSelectedFolder ? JSON.stringify(googleProviderState.lastSelectedFolder) : null + if (googleProviderState.hasEverLoggedIn) { + localStorage.hasEverLoggedIn = true + } else { + delete localStorage.hasEverLoggedIn + } + + if (localStorage.saveAccessToken && googleProviderState) { + // For testing only + localStorage.accessToken = googleProviderState.accessToken || null + } else { + delete localStorage.accessToken + } +}) diff --git a/src/guessProblem.ts b/src/guessProblem.ts deleted file mode 100644 index ecc7dbf1..00000000 --- a/src/guessProblem.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const guessProblem = (errorMessage: string) => { - if (errorMessage.endsWith('Socket error: ECONNREFUSED')) { - return 'Most probably the server is not running.' - } -} diff --git a/src/importsWorkaround.js b/src/importsWorkaround.js index 21bc4585..231654ca 100644 --- a/src/importsWorkaround.js +++ b/src/importsWorkaround.js @@ -1,4 +1,6 @@ // workaround for mineflayer +globalThis.window ??= globalThis +globalThis.localStorage ??= {} process.versions.node = '18.0.0' if (!navigator.getGamepads) { diff --git a/src/index.ts b/src/index.ts index 620b5597..7764188f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,261 +1,140 @@ /* eslint-disable import/order */ import './importsWorkaround' import './styles.css' +import './testCrasher' import './globals' -import 'iconify-icon' import './devtools' import './entities' +import customChannels from './customChannels' import './globalDomListeners' -import initCollisionShapes from './getCollisionShapes' -import { onGameLoad } from './playerWindows' -import { supportedVersions } from 'minecraft-protocol' +import './mineflayer/maps' +import './mineflayer/cameraShake' +import './shims/patchShims' +import './mineflayer/java-tester/index' +import './external' +import './appConfig' +import './mineflayer/timers' +import './mineflayer/plugins' +import { getServerInfo } from './mineflayer/mc-protocol' +import { onGameLoad } from './inventoryWindows' +import initCollisionShapes from './getCollisionInteractionShapes' +import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth' +import microsoftAuthflow from './microsoftAuthflow' +import { Duplex } from 'stream' -import './menus/components/button' -import './menus/components/edit_box' -import './menus/components/hotbar' -import './menus/components/health_bar' -import './menus/components/food_bar' -import './menus/components/breath_bar' -import './menus/components/debug_overlay' -import './menus/components/playerlist_overlay' -import './menus/components/bossbars_overlay' -import './menus/hud' -import './menus/play_screen' -import './menus/pause_screen' -import './menus/keybinds_screen' -import 'core-js/features/array/at' -import 'core-js/features/promise/with-resolvers' -import { initWithRenderer, statsEnd, statsStart } from './topRightStats' -import PrismarineBlock from 'prismarine-block' +import './scaleInterface' -import { options, watchValue } from './optionsStorage' -import './reactUi.jsx' -import { contro, onBotCreate } from './controls' +import { options } from './optionsStorage' +import './reactUi' +import { lockUrl, onBotCreate } from './controls' import './dragndrop' -import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs' -import './eruda' -import { watchOptionsAfterViewerInit } from './watchOptions' -import downloadAndOpenFile from './downloadAndOpenFile' +import { possiblyCleanHandle } from './browserfs' +import downloadAndOpenFile, { isInterestedInDownload } from './downloadAndOpenFile' import fs from 'fs' -import net from 'net' +import net, { Socket } from 'net' import mineflayer from 'mineflayer' -import { WorldDataEmitter, Viewer } from 'prismarine-viewer/viewer' -import pathfinder from 'mineflayer-pathfinder' -import { Vec3 } from 'vec3' -import worldInteractions from './worldInteractions' - -import * as THREE from 'three' -import MinecraftData, { versionsByMinecraftVersion } from 'minecraft-data' import debug from 'debug' -import _ from 'lodash-es' +import { defaultsDeep } from 'lodash-es' +import initializePacketsReplay from './packetsReplay/packetsReplayLegacy' -import { initVR } from './vr' import { activeModalStack, - showModal, activeModalStacks, + activeModalStacks, + hideModal, insertActiveModalStack, isGameActive, miscUiState, - notification + showModal, + gameAdditionalState, } from './globalState' - -import { - pointerLock, - toMajorVersion, - setLoadingScreenStatus -} from './utils' +import { parseServerAddress } from './parseServerAddress' +import { setLoadingScreenStatus } from './appStatus' import { isCypress } from './standaloneUtils' -import { - removePanorama -} from './panorama' - import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer' import defaultServerOptions from './defaultLocalServerOptions' -import dayCycle from './dayCycle' -import { genTexturePackTextures, watchTexturepackInViewer } from './texturePack' -import { connectToPeer } from './localServerMultiplayer' +import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack' +import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer' import CustomChannelClient from './customClient' -import { loadScript } from 'prismarine-viewer/viewer/lib/utils' import { registerServiceWorker } from './serviceWorker' -import { appStatusState, lastConnectOptions } from './react/AppStatusProvider' +import { appStatusState, lastConnectOptions, quickDevReconnect } from './react/AppStatusProvider' import { fsState } from './loadSave' import { watchFov } from './rendererUtils' import { loadInMemorySave } from './react/SingleplayerProvider' -// side effects -import { downloadSoundsIfNeeded } from './soundSystem' -import { ua } from './react/utils' -import { handleMovementStickDelta, joystickPointer } from './react/TouchAreasControls' +import { possiblyHandleStateVariable } from './googledrive' +import flyingSquidEvents from './flyingSquidEvents' +import { showNotification } from './react/NotificationProvider' +import { saveToBrowserMemory } from './react/PauseScreen' +import './devReload' +import './water' +import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData, loadMinecraftData } from './connect' +import { ref, subscribe } from 'valtio' +import { signInMessageState } from './react/SignInMessageProvider' +import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' +import { mainMenuState } from './react/MainMenuRenderApp' +import './mobileShim' +import { parseFormattedMessagePacket } from './botUtils' +import { appStartup } from './clientMods' +import { getViewerVersionData, getWsProtocolStream, onBotCreatedViewerHandler } from './viewerConnector' +import { getWebsocketStream } from './mineflayer/websocket-core' +import { appQueryParams, appQueryParamsArray } from './appParams' +import { playerState } from './mineflayer/playerState' +import { states } from 'minecraft-protocol' +import { initMotionTracking } from './react/uiMotion' +import { UserError } from './mineflayer/userError' +import { startLocalReplayServer } from './packetsReplay/replayPackets' +import { createFullScreenProgressReporter, createWrappedProgressReporter, ProgressReporter } from './core/progressReporter' +import { appViewer } from './appViewer' +import './appViewerLoad' +import { registerOpenBenchmarkListener } from './benchmark' +import { tryHandleBuiltinCommand } from './builtinCommands' +import { loadingTimerState } from './react/LoadingTimer' +import { loadPluginsIntoWorld } from './react/CreateWorldProvider' +import { getCurrentProxy, getCurrentUsername } from './react/ServersList' window.debug = debug -window.THREE = THREE -window.worldInteractions = worldInteractions window.beforeRenderFrame = [] // ACTUAL CODE -void registerServiceWorker() +void registerServiceWorker().then(() => { + mainMenuState.serviceWorkerLoaded = true +}) watchFov() initCollisionShapes() +initializePacketsReplay() +onAppLoad() +customChannels() -// Create three.js context, add to page -let renderer: THREE.WebGLRenderer -try { - renderer = new THREE.WebGLRenderer({ - powerPreference: options.gpuPreference, - }) -} catch (err) { - console.error(err) - throw new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`) -} - -// renderer.localClippingEnabled = true -initWithRenderer(renderer.domElement) -window.renderer = renderer -let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable -if (!renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped) -renderer.setPixelRatio(pixelRatio) -renderer.setSize(window.innerWidth, window.innerHeight) -renderer.domElement.id = 'viewer-canvas' -document.body.appendChild(renderer.domElement) - -const isFirefox = ua.getBrowser().name === 'Firefox' -if (isFirefox) { - // set custom property - document.body.style.setProperty('--thin-if-firefox', 'thin') -} - -// Create viewer -const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer, options.numWorkers) -window.viewer = viewer -Object.defineProperty(window, 'debugSceneChunks', { - get () { - return viewer.world.getLoadedChunksRelative(bot.entity.position) - }, -}) -Object.defineProperty(window, 'debugSceneChunksY', { - get () { - return viewer.world.getLoadedChunksRelative(bot.entity.position, true) - }, -}) -viewer.entities.entitiesOptions = { - fontFamily: 'mojangles' -} -watchOptionsAfterViewerInit() -watchTexturepackInViewer(viewer) - -let renderInterval: number | false -watchValue(options, (o) => { - renderInterval = o.frameLimit && 1000 / o.frameLimit -}) - -let postRenderFrameFn = () => { } -let delta = 0 -let lastTime = performance.now() -let previousWindowWidth = window.innerWidth -let previousWindowHeight = window.innerHeight -let max = 0 -let rendered = 0 -const renderFrame = (time: DOMHighResTimeStamp) => { - if (window.stopLoop) return - for (const fn of beforeRenderFrame) fn() - window.requestAnimationFrame(renderFrame) - if (window.stopRender || renderer.xr.isPresenting) return - if (renderInterval) { - delta += time - lastTime - lastTime = time - if (delta > renderInterval) { - delta %= renderInterval - // continue rendering - } else { - return - } - } - // ios bug: viewport dimensions are updated after the resize event - if (previousWindowWidth !== window.innerWidth || previousWindowHeight !== window.innerHeight) { - resizeHandler() - previousWindowWidth = window.innerWidth - previousWindowHeight = window.innerHeight - } - statsStart() - viewer.update() - viewer.render() - rendered++ - postRenderFrameFn() - statsEnd() -} -renderFrame(performance.now()) -setInterval(() => { - if (max > 0) { - viewer.world.droppedFpsPercentage = rendered / max - } - max = Math.max(rendered, max) - rendered = 0 -}, 1000) - -const resizeHandler = () => { - const width = window.innerWidth - const height = window.innerHeight - - viewer.camera.aspect = width / height - viewer.camera.updateProjectionMatrix() - renderer.setSize(width, height) - - if (viewer.composer) { - viewer.updateComposerSize() - } -} - -const hud = document.getElementById('hud') -const pauseMenu = document.getElementById('pause-screen') - -let mouseMovePostHandle = (e) => { } -let lastMouseMove: number -let debugMenu -const updateCursor = () => { - worldInteractions.update() - debugMenu ??= hud.shadowRoot.querySelector('#debug-overlay') - debugMenu.cursorBlock = worldInteractions.cursorBlock -} -function onCameraMove (e) { - if (e.type !== 'touchmove' && !pointerLock.hasPointerLock) return - e.stopPropagation?.() - const now = performance.now() - // todo: limit camera movement for now to avoid unexpected jumps - if (now - lastMouseMove < 4) return - lastMouseMove = now - let { mouseSensX, mouseSensY } = options - if (mouseSensY === -1) mouseSensY = mouseSensX - mouseMovePostHandle({ - x: e.movementX * mouseSensX * 0.0001, - y: e.movementY * mouseSensY * 0.0001 - }) - updateCursor() -} -window.addEventListener('mousemove', onCameraMove, { capture: true }) -contro.on('stickMovement', ({ stick, vector }) => { - if (!isGameActive(true)) return - if (stick !== 'right') return - let { x, z } = vector - if (Math.abs(x) < 0.18) x = 0 - if (Math.abs(z) < 0.18) z = 0 - onCameraMove({ movementX: x * 10, movementY: z * 10, type: 'touchmove' }) - miscUiState.usingGamepadInput = true -}) +if (appQueryParams.testCrashApp === '2') throw new Error('test') function hideCurrentScreens () { activeModalStacks['main-menu'] = [...activeModalStack] insertActiveModalStack('', []) } -const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) => { - void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides, serverOverridesFlat: flattenedServerOverrides }) +const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}, connectOptions?: Partial) => { + const serverSettingsQsRaw = appQueryParamsArray.serverSetting ?? [] + const serverSettingsQs = serverSettingsQsRaw.map(x => x.split(':')).reduce>((acc, [key, value]) => { + acc[key] = JSON.parse(value) + return acc + }, {}) + void connect({ + singleplayer: true, + username: options.localUsername, + serverOverrides, + serverOverridesFlat: { + ...flattenedServerOverrides, + ...serverSettingsQs + }, + ...connectOptions + }) } function listenGlobalEvents () { window.addEventListener('connect', e => { @@ -263,107 +142,143 @@ function listenGlobalEvents () { void connect(options) }) window.addEventListener('singleplayer', (e) => { - loadSingleplayer((e as CustomEvent).detail) + const { detail } = (e as CustomEvent) + const { connectOptions, ...rest } = detail + loadSingleplayer(rest, {}, connectOptions) }) } -let listeners = [] as Array<{ target, event, callback }> -let cleanupFunctions = [] as Array<() => void> -// only for dom listeners (no removeAllListeners) -// todo refactor them out of connect fn instead -const registerListener: import('./utilsTs').RegisterListener = (target, event, callback) => { - target.addEventListener(event, callback) - listeners.push({ target, event, callback }) -} -const removeAllListeners = () => { - for (const { target, event, callback } of listeners) { - target.removeEventListener(event, callback) - } - for (const cleanupFunction of cleanupFunctions) { - cleanupFunction() - } - cleanupFunctions = [] - listeners = [] -} - -const cleanConnectIp = (host: string | undefined, defaultPort: string | undefined) => { - const hostPort = host && /:\d+$/.exec(host) - if (hostPort) { - return { - host: host.slice(0, -hostPort[0].length), - port: hostPort[0].slice(1) - } - } else { - return { host, port: defaultPort } - } -} - -async function connect (connectOptions: { - server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; serverOverridesFlat?; peerId?: string -}) { +export async function connect (connectOptions: ConnectOptions) { if (miscUiState.gameLoaded) return + + if (sessionStorage.delayLoadUntilFocus) { + await new Promise(resolve => { + if (document.hasFocus()) { + resolve(undefined) + } else { + window.addEventListener('focus', resolve) + } + }) + } + if (sessionStorage.delayLoadUntilClick) { + await new Promise(resolve => { + window.addEventListener('click', resolve) + }) + } + + appStatusState.showReconnect = false + loadingTimerState.loading = true + loadingTimerState.start = Date.now() + miscUiState.hasErrors = false lastConnectOptions.value = connectOptions - document.getElementById('play-screen').style = 'display: none;' - removePanorama() const { singleplayer } = connectOptions const p2pMultiplayer = !!connectOptions.peerId miscUiState.singleplayer = singleplayer miscUiState.flyingSquid = singleplayer || p2pMultiplayer - const { renderDistance: renderDistanceSingleplayer, multiplayerRenderDistance } = options - const server = cleanConnectIp(connectOptions.server, '25565') - const proxy = cleanConnectIp(connectOptions.proxy, undefined) - const { username, password } = connectOptions - console.log(`connecting to ${server.host}:${server.port} with ${username}`) + // Track server connection in history + if (!singleplayer && !p2pMultiplayer && connectOptions.server && connectOptions.saveServerToHistory !== false) { + const parsedServer = parseServerAddress(connectOptions.server) + updateServerConnectionHistory(parsedServer.host, connectOptions.botVersion) + } + + const { renderDistance: renderDistanceSingleplayer, multiplayerRenderDistance } = options + + const parsedServer = parseServerAddress(connectOptions.server) + const server = { host: parsedServer.host, port: parsedServer.port } + if (connectOptions.proxy?.startsWith(':')) { + connectOptions.proxy = `${location.protocol}//${location.hostname}${connectOptions.proxy}` + } + if (connectOptions.proxy && location.port !== '80' && location.port !== '443' && !/:\d+$/.test(connectOptions.proxy)) { + const https = connectOptions.proxy.startsWith('https://') || location.protocol === 'https:' + connectOptions.proxy = `${connectOptions.proxy}:${https ? 443 : 80}` + } + const parsedProxy = parseServerAddress(connectOptions.proxy, false) + const proxy = { host: parsedProxy.host, port: parsedProxy.port } + let { username } = connectOptions + + if (connectOptions.server) { + console.log(`connecting to ${server.host}:${server.port ?? 25_565}`) + } + console.log('using player username', username) hideCurrentScreens() - setLoadingScreenStatus('Logging in') + const progress = createFullScreenProgressReporter() + const loggingInMsg = connectOptions.server ? 'Connecting to server' : 'Logging in' + progress.beginStage('connect', loggingInMsg) let ended = false let bot!: typeof __type_bot - const destroyAll = () => { + let hadConnected = false + const destroyAll = (wasKicked = false) => { if (ended) return + loadingTimerState.loading = false + const { alwaysReconnect } = appQueryParams + if ((!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) || (alwaysReconnect)) { + if (alwaysReconnect === 'quick' || alwaysReconnect === 'fast') { + quickDevReconnect() + } else { + location.reload() + } + } + errorAbortController.abort() ended = true - viewer.resetAll() + progress.end() + // dont reset viewer so we can still do debugging localServer = window.localServer = window.server = undefined + gameAdditionalState.viewerConnection = false - postRenderFrameFn = () => { } if (bot) { bot.end() // ensure mineflayer plugins receive this event for cleanup bot.emit('end', '') bot.removeAllListeners() bot._client.removeAllListeners() - //@ts-expect-error TODO? - bot._client = undefined + bot._client = { + //@ts-expect-error + write (packetName) { + console.warn('Tried to write packet', packetName, 'after bot was destroyed') + } + } //@ts-expect-error window.bot = bot = undefined } + cleanFs() + } + const cleanFs = () => { if (singleplayer && !fsState.inMemorySave) { possiblyCleanHandle(() => { // todo: this is not enough, we need to wait for all async operations to finish }) } - resetStateAfterDisconnect() - removeAllListeners() } + let lastPacket = undefined as string | undefined const onPossibleErrorDisconnect = () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (lastPacket && bot?._client && bot._client.state !== 'play') { + if (lastPacket && bot?._client && bot._client.state !== states.PLAY) { appStatusState.descriptionHint = `Last Server Packet: ${lastPacket}` } } const handleError = (err) => { - errorAbortController.abort() + console.error(err) + if (err === 'ResizeObserver loop completed with undelivered notifications.') { + return + } if (isCypress()) throw err + miscUiState.hasErrors = true if (miscUiState.gameLoaded) return + // close all modals + for (const modal of activeModalStack) { + hideModal(modal) + } setLoadingScreenStatus(`Error encountered. ${err}`, true) + appStatusState.showReconnect = true onPossibleErrorDisconnect() destroyAll() } + // todo(hard): remove it! const errorAbortController = new AbortController() window.addEventListener('unhandledrejection', (e) => { if (e.reason.name === 'ServerPluginLoadFailure') { @@ -371,6 +286,10 @@ async function connect (connectOptions: { return } } + if (e.reason?.stack?.includes('chrome-extension://')) { + // ignore issues caused by chrome extension + return + } handleError(e.reason) }, { signal: errorAbortController.signal @@ -381,47 +300,89 @@ async function connect (connectOptions: { signal: errorAbortController.signal }) - if (proxy) { - console.log(`using proxy ${proxy.host}${proxy.port && `:${proxy.port}`}`) + let clientDataStream: Duplex | undefined - net['setProxy']({ hostname: proxy.host, port: proxy.port }) + if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) { + console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`) + net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` }, artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined }) } const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance + let updateDataAfterJoin = () => { } let localServer + let localReplaySession: ReturnType | undefined + let lastKnownKickReason = undefined as string | undefined try { - const serverOptions = _.defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions) + const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions) Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {}) - const downloadMcData = async (version: string) => { - // todo expose cache - const lastVersion = supportedVersions.at(-1) - if (version === lastVersion) { - // ignore cache hit - versionsByMinecraftVersion.pc[lastVersion]!['dataVersion']!++ - } - if (!document.fonts.check('1em mojangles')) { - // todo instead re-render signs on load - await document.fonts.load('1em mojangles').catch(() => { }) - } - setLoadingScreenStatus(`Downloading data for ${version}`) - await downloadSoundsIfNeeded() - await loadScript(`./mc-data/${toMajorVersion(version)}.js`) - miscUiState.loadedDataVersion = version - try { - await genTexturePackTextures(version) - } catch (err) { - console.error(err) - const doContinue = confirm('Failed to apply texture pack. See errors in the console. Continue?') - if (!doContinue) { - throw err + + await progress.executeWithMessage('Downloading Minecraft data', 'download-mcdata', async () => { + loadingTimerState.networkOnlyStart = Date.now() + + let downloadingAssets = [] as string[] + const reportAssetDownload = (asset: string, isDone: boolean) => { + if (isDone) { + downloadingAssets = downloadingAssets.filter(a => a !== asset) + } else { + downloadingAssets.push(asset) } + progress.setSubStage('download-mcdata', `(${downloadingAssets.join(', ')})`) } - viewer.setVersion(version) + + await Promise.all([ + downloadAllMinecraftData(reportAssetDownload), + downloadOtherGameData(reportAssetDownload) + ]) + loadingTimerState.networkOnlyStart = 0 + }) + + let dataDownloaded = false + const downloadMcData = async (version: string) => { + if (dataDownloaded) return + dataDownloaded = true + appViewer.resourcesManager.currentConfig = { version, texturesVersion: options.useVersionsTextures || undefined } + + await progress.executeWithMessage( + 'Processing downloaded Minecraft data', + async () => { + await loadMinecraftData(version) + await appViewer.resourcesManager.loadSourceData(version) + } + ) + + await progress.executeWithMessage( + 'Applying user-installed resource pack', + async () => { + try { + await resourcepackReload(true) + } catch (err) { + console.error(err) + const doContinue = confirm('Failed to apply texture pack. See errors in the console. Continue?') + if (!doContinue) { + throw err + } + } + } + ) + + await progress.executeWithMessage( + 'Preparing textures', + async () => { + await appViewer.resourcesManager.updateAssetsData({}) + } + ) } - const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined) - if (downloadVersion) { - await downloadMcData(downloadVersion) + let finalVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined) + + if (connectOptions.worldStateFileContents) { + try { + localReplaySession = startLocalReplayServer(connectOptions.worldStateFileContents) + } catch (err) { + console.error(err) + throw new UserError(`Failed to start local replay server: ${err}`) + } + finalVersion = localReplaySession.version } if (singleplayer) { @@ -436,42 +397,127 @@ async function connect (connectOptions: { // Client (class) of flying-squid (in server/login.js of mc-protocol): onLogin handler: skip most logic & go to loginClient() which assigns uuid and sends 'success' back to client (onLogin handler) and emits 'login' on the server (login.js in flying-squid handler) // flying-squid: 'login' -> player.login -> now sends 'login' event to the client (handled in many plugins in mineflayer) -> then 'update_health' is sent which emits 'spawn' in mineflayer - setLoadingScreenStatus('Starting local server') + const serverPlugins = new URLSearchParams(location.search).getAll('serverPlugin') + if (serverPlugins.length > 0 && !serverOptions.worldFolder) { + console.log('Placing server plugins', serverPlugins) + + serverOptions.worldFolder ??= '/temp' + await loadPluginsIntoWorld('/temp', serverPlugins) + + console.log('Server plugins placed') + } + localServer = window.localServer = window.server = startLocalServer(serverOptions) + connectOptions?.connectEvents?.serverCreated?.() // todo need just to call quit if started // loadingScreen.maybeRecoverable = false // init world, todo: do it for any async plugins if (!localServer.pluginsReady) { - await new Promise(resolve => { - localServer.once('pluginsReady', resolve) - }) + await progress.executeWithMessage( + 'Starting local server', + async () => { + await new Promise(resolve => { + localServer.once('pluginsReady', resolve) + }) + } + ) } localServer.on('newPlayer', (player) => { - // it's you! player.on('loadingStatus', (newStatus) => { - setLoadingScreenStatus(newStatus, false, false, true) + progress.setMessage(newStatus) }) }) + flyingSquidEvents() } + if (connectOptions.authenticatedAccount) username = 'you' let initialLoadingText: string if (singleplayer) { initialLoadingText = 'Local server is still starting' } else if (p2pMultiplayer) { initialLoadingText = 'Connecting to peer' + } else if (connectOptions.server) { + if (!finalVersion) { + const versionAutoSelect = getVersionAutoSelect() + const wrapped = createWrappedProgressReporter(progress, `Fetching server version. Preffered: ${versionAutoSelect}`) + loadingTimerState.networkOnlyStart = Date.now() + const autoVersionSelect = await getServerInfo(server.host, server.port ? Number(server.port) : undefined, versionAutoSelect) + wrapped.end() + finalVersion = autoVersionSelect.version + } + initialLoadingText = `Connecting to server ${server.host}:${server.port ?? 25_565} with version ${finalVersion}` + } else if (connectOptions.viewerWsConnect) { + initialLoadingText = `Connecting to Mineflayer WebSocket server ${connectOptions.viewerWsConnect}` + } else if (connectOptions.worldStateFileContents) { + initialLoadingText = `Loading local replay server` } else { - initialLoadingText = 'Connecting to server' + initialLoadingText = 'We have no idea what to do' } - setLoadingScreenStatus(initialLoadingText) + progress.setMessage(initialLoadingText) + + if (parsedServer.isWebSocket) { + loadingTimerState.networkOnlyStart = Date.now() + clientDataStream = (await getWebsocketStream(server.host)).mineflayerStream + } + + let newTokensCacheResult = null as any + const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {} + let authData: Awaited> | undefined + if (connectOptions.authenticatedAccount) { + authData = await microsoftAuthflow({ + tokenCaches: cachedTokens, + proxyBaseUrl: connectOptions.proxy, + setProgressText (text) { + progress.setMessage(text) + }, + setCacheResult (result) { + newTokensCacheResult = result + }, + connectingServer: server.host + }) + } + + if (p2pMultiplayer) { + clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions) + } + if (connectOptions.viewerWsConnect) { + const { version, time, requiresPass } = await getViewerVersionData(connectOptions.viewerWsConnect) + let password + if (requiresPass) { + password = prompt('Enter password') + if (!password) { + throw new UserError('Password is required') + } + } + console.log('Latency:', Date.now() - time, 'ms') + // const version = '1.21.1' + finalVersion = version + await downloadMcData(version) + setLoadingScreenStatus(`Connecting to WebSocket server ${connectOptions.viewerWsConnect}`) + clientDataStream = (await getWsProtocolStream(connectOptions.viewerWsConnect)).clientDuplex + if (password) { + clientDataStream.write(password) + } + gameAdditionalState.viewerConnection = true + } + + if (finalVersion) { + // ensure data is downloaded + loadingTimerState.networkOnlyStart ??= Date.now() + await downloadMcData(finalVersion) + } + + const brand = clientDataStream ? 'minecraft-web-client' : undefined bot = mineflayer.createBot({ host: server.host, port: server.port ? +server.port : undefined, - version: connectOptions.botVersion || false, - ...p2pMultiplayer ? { - stream: await connectToPeer(connectOptions.peerId!), + brand, + version: finalVersion || false, + ...clientDataStream ? { + stream: clientDataStream as any, } : {}, - ...singleplayer || p2pMultiplayer ? { + ...singleplayer || p2pMultiplayer || localReplaySession ? { keepAlive: false, } : {}, ...singleplayer ? { @@ -479,42 +525,117 @@ async function connect (connectOptions: { connect () { }, Client: CustomChannelClient as any, } : {}, + ...localReplaySession ? { + connect () { }, + Client: CustomChannelClient as any, + } : {}, + onMsaCode (data) { + signInMessageState.code = data.user_code + signInMessageState.link = data.verification_uri + signInMessageState.expiresOn = Date.now() + data.expires_in * 1000 + }, + sessionServer: authData?.sessionEndpoint?.toString(), + auth: connectOptions.authenticatedAccount ? async (client, options) => { + authData!.setOnMsaCodeCallback(options.onMsaCode) + authData?.setConnectingVersion(client.version) + //@ts-expect-error + client.authflow = authData!.authFlow + try { + signInMessageState.abortController = ref(new AbortController()) + await Promise.race([ + protocolMicrosoftAuth.authenticate(client, options), + new Promise((_r, reject) => { + signInMessageState.abortController.signal.addEventListener('abort', () => { + reject(new UserError('Aborted by user')) + }) + }) + ]) + if (signInMessageState.shouldSaveToken) { + updateAuthenticatedAccountData(accounts => { + const existingAccount = accounts.find(a => a.username === client.username) + if (existingAccount) { + existingAccount.cachedTokens = { ...existingAccount.cachedTokens, ...newTokensCacheResult } + } else { + accounts.push({ + username: client.username, + cachedTokens: { ...cachedTokens, ...newTokensCacheResult } + }) + } + return accounts + }) + updateDataAfterJoin = () => { + updateLoadedServerData(s => ({ ...s, authenticatedAccountOverride: client.username }), connectOptions.serverIndex) + } + } else { + updateDataAfterJoin = () => { + updateLoadedServerData(s => ({ ...s, authenticatedAccountOverride: undefined }), connectOptions.serverIndex) + } + } + setLoadingScreenStatus('Authentication successful. Logging in to server') + } finally { + signInMessageState.code = '' + } + } : undefined, username, - password, viewDistance: renderDistance, checkTimeoutInterval: 240 * 1000, - noPongTimeout: 240 * 1000, + // noPongTimeout: 240 * 1000, closeTimeout: 240 * 1000, respawn: options.autoRespawn, maxCatchupTicks: 0, - async versionSelectedHook (client) { - await downloadMcData(client.version) - setLoadingScreenStatus(initialLoadingText) - } + 'mapDownloader-saveToFile': false, + // "mapDownloader-saveInternal": false, // do not save into memory, todo must be implemeneted as we do really care of ram }) as unknown as typeof __type_bot window.bot = bot - if (singleplayer || p2pMultiplayer) { - // in case of p2pMultiplayer there is still flying-squid on the host side - const _supportFeature = bot.supportFeature - bot.supportFeature = ((feature) => { - if (unsupportedLocalServerFeatures.includes(feature)) { - return false - } - return _supportFeature(feature) - }) as typeof bot.supportFeature + + if (connectOptions.viewerWsConnect) { + void onBotCreatedViewerHandler() + } + customEvents.emit('mineflayerBotCreated') + if (singleplayer || p2pMultiplayer || localReplaySession) { + if (singleplayer || p2pMultiplayer) { + // in case of p2pMultiplayer there is still flying-squid on the host side + const _supportFeature = bot.supportFeature + bot.supportFeature = ((feature) => { + if (unsupportedLocalServerFeatures.includes(feature)) { + return false + } + return _supportFeature(feature) + }) as typeof bot.supportFeature + } bot.emit('inject_allowed') bot._client.emit('connect') + } else if (clientDataStream) { + // bot.emit('inject_allowed') + bot._client.emit('connect') } else { const setupConnectHandlers = () => { + Socket.prototype['handleStringMessage'] = function (message: string) { + if (message.startsWith('proxy-message') || message.startsWith('proxy-command:')) { // for future + return false + } + if (message.startsWith('proxy-shutdown:')) { + lastKnownKickReason = message.slice('proxy-shutdown:'.length) + return false + } + return true + } bot._client.socket.on('connect', () => { - console.log('TCP connection established') + console.log('Proxy WebSocket connection established') //@ts-expect-error bot._client.socket._ws.addEventListener('close', () => { - console.log('TCP connection closed') + console.log('WebSocket connection closed') setTimeout(() => { if (bot) { - bot.emit('end', 'TCP connection closed with unknown reason') + bot.emit('end', 'WebSocket connection closed with unknown reason') + } + }, 1000) + }) + bot._client.socket.on('close', () => { + setTimeout(() => { + if (bot) { + bot.emit('end', 'WebSocket connection closed with unknown reason') } }) }) @@ -526,6 +647,7 @@ async function connect (connectOptions: { } else { const originalSetSocket = bot._client.setSocket.bind(bot._client) bot._client.setSocket = (socket) => { + if (!bot) return originalSetSocket(socket) setupConnectHandlers() } @@ -537,8 +659,7 @@ async function connect (connectOptions: { } if (!bot) return - const p2pConnectTimeout = p2pMultiplayer ? setTimeout(() => { throw new Error('Spawn timeout. There might be error on the other side, check console.') }, 20_000) : undefined - hud.preload(bot) + const p2pConnectTimeout = p2pMultiplayer ? setTimeout(() => { throw new UserError('Spawn timeout. There might be error on the other side, check console.') }, 20_000) : undefined // bot.on('inject_allowed', () => { // loadingScreen.maybeRecoverable = false @@ -547,16 +668,21 @@ async function connect (connectOptions: { bot.on('error', handleError) bot.on('kicked', (kickReason) => { - console.log('User was kicked!', kickReason) - setLoadingScreenStatus(`The Minecraft server kicked you. Kick reason: ${kickReason}`, true) - destroyAll() + console.log('You were kicked!', kickReason) + const { formatted: kickReasonFormatted, plain: kickReasonString } = parseFormattedMessagePacket(kickReason) + // close all modals + for (const modal of activeModalStack) { + hideModal(modal) + } + setLoadingScreenStatus(`The Minecraft server kicked you. Kick reason: ${kickReasonString}`, true, undefined, undefined, kickReasonFormatted) + appStatusState.showReconnect = true + destroyAll(true) }) - let lastPacket = undefined as string | undefined const packetBeforePlay = (_, __, ___, fullBuffer) => { lastPacket = fullBuffer.toString() } - bot._client.on('packet', packetBeforePlay) + bot._client.on('packet', packetBeforePlay as any) const playStateSwitch = (newState) => { if (newState === 'play') { bot._client.removeListener('packet', packetBeforePlay) @@ -567,7 +693,15 @@ async function connect (connectOptions: { bot.on('end', (endReason) => { if (ended) return console.log('disconnected for', endReason) - setLoadingScreenStatus(`You have been disconnected from the server. End reason: ${endReason}`, true) + if (endReason === 'socketClosed') { + endReason = lastKnownKickReason ?? 'Connection with proxy server lost' + } + // close all modals + for (const modal of activeModalStack) { + hideModal(modal) + } + setLoadingScreenStatus(`You have been disconnected from the server. End reason:\n${endReason}`, true) + appStatusState.showReconnect = true onPossibleErrorDisconnect() destroyAll() if (isCypress()) throw new Error(`disconnected: ${endReason}`) @@ -576,254 +710,190 @@ async function connect (connectOptions: { onBotCreate() bot.once('login', () => { - worldInteractions.initBot() - - // server is ok, add it to the history - if (!connectOptions.server) return - const serverHistory: string[] = JSON.parse(localStorage.getItem('serverHistory') || '[]') - serverHistory.unshift(connectOptions.server) - localStorage.setItem('serverHistory', JSON.stringify([...new Set(serverHistory)])) - - setLoadingScreenStatus('Loading world') + errorAbortController.abort() + loadingTimerState.networkOnlyStart = 0 + progress.setMessage('Loading world') }) + let worldWasReady = false + const waitForChunksToLoad = async (progress?: ProgressReporter) => { + await new Promise(resolve => { + if (worldWasReady) { + resolve() + return + } + const unsub = subscribe(appViewer.rendererState, () => { + if (appViewer.rendererState.world.allChunksLoaded && appViewer.nonReactiveState.world.chunksTotalNumber) { + worldWasReady = true + resolve() + unsub() + } else { + const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.nonReactiveState.world.chunksTotalNumber * 100) + progress?.reportProgress('chunks', perc / 100) + } + }) + }) + } + const spawnEarlier = !singleplayer && !p2pMultiplayer - // don't use spawn event, player can be dead - bot.once(spawnEarlier ? 'forcedMove' : 'health', () => { - errorAbortController.abort() - const mcData = MinecraftData(bot.version) - window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!) - window.loadedData = mcData - window.Vec3 = Vec3 - window.pathfinder = pathfinder - - miscUiState.gameLoaded = true - customEvents.emit('gameLoaded') - if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) - - setLoadingScreenStatus('Placing blocks (starting viewer)') - - console.log('bot spawned - starting viewer') - - const center = bot.entity.position - - const worldView = window.worldView = new WorldDataEmitter(bot.world, renderDistance, center) - - bot.on('physicsTick', () => updateCursor()) - - const debugMenu = hud.shadowRoot.querySelector('#debug-overlay') - - window.debugMenu = debugMenu - - void initVR() - - postRenderFrameFn = () => { - viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch) - } - - try { - const gl = renderer.getContext() - debugMenu.rendererDevice = gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL) - } catch (err) { - console.warn(err) - debugMenu.rendererDevice = '???' - } - - // Link WorldDataEmitter and Viewer - viewer.listen(worldView) - worldView.listenToBot(bot) - void worldView.init(bot.entity.position) - - dayCycle() - - // Bot position callback - function botPosition () { - viewer.world.lastCamUpdate = Date.now() - // this might cause lag, but not sure - viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) - void worldView.updatePosition(bot.entity.position) - } - bot.on('move', botPosition) - botPosition() - - setLoadingScreenStatus('Setting callbacks') - - const maxPitch = 0.5 * Math.PI - const minPitch = -0.5 * Math.PI - mouseMovePostHandle = ({ x, y }) => { - viewer.world.lastCamUpdate = Date.now() - bot.entity.pitch -= y - bot.entity.pitch = Math.max(minPitch, Math.min(maxPitch, bot.entity.pitch)) - bot.entity.yaw -= x - } - - function changeCallback () { - notification.show = false - if (renderer.xr.isPresenting) return // todo - if (!pointerLock.hasPointerLock && activeModalStack.length === 0) { - showModal(pauseMenu) - } - } - - registerListener(document, 'pointerlockchange', changeCallback, false) - - const cameraControlEl = hud - - /** after what time of holding the finger start breaking the block */ - const touchStartBreakingBlockMs = 500 - let virtualClickActive = false - let virtualClickTimeout - let screenTouches = 0 - let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | undefined - registerListener(document, 'pointerdown', (e) => { - const usingJoystick = options.touchControlsType === 'joystick-buttons' - const clickedEl = e.composedPath()[0] - if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || e.pointerId === undefined) { - return - } - screenTouches++ - if (screenTouches === 3) { - // todo needs fixing! - // window.dispatchEvent(new MouseEvent('mousedown', { button: 1 })) - } - if (usingJoystick) { - if (!joystickPointer.pointer && e.clientX < window.innerWidth / 2) { - joystickPointer.pointer = { - pointerId: e.pointerId, - x: e.clientX, - y: e.clientY + const displayWorld = async () => { + if (resourcePackState.isServerInstalling) { + await new Promise(resolve => { + subscribe(resourcePackState, () => { + if (!resourcePackState.isServerInstalling) { + resolve() } - return - } - } - if (capturedPointer) { - return - } - cameraControlEl.setPointerCapture(e.pointerId) - capturedPointer = { - id: e.pointerId, - x: e.clientX, - y: e.clientY, - sourceX: e.clientX, - sourceY: e.clientY, - activateCameraMove: false, - time: Date.now() - } - if (options.touchControlsType !== 'joystick-buttons') { - virtualClickTimeout ??= setTimeout(() => { - virtualClickActive = true - document.dispatchEvent(new MouseEvent('mousedown', { button: 0 })) - }, touchStartBreakingBlockMs) - } - }) - registerListener(document, 'pointermove', (e) => { - if (e.pointerId === undefined) return - const supportsPressure = (e as any).pressure !== undefined && (e as any).pressure !== 0 && (e as any).pressure !== 0.5 && (e as any).pressure !== 1 && (e.pointerType === 'touch' || e.pointerType === 'pen') - if (e.pointerId === joystickPointer.pointer?.pointerId) { - handleMovementStickDelta(e) - if (supportsPressure && (e as any).pressure > 0.5) { - bot.setControlState('sprint', true) - // todo - } - return - } - if (e.pointerId !== capturedPointer?.id) return - window.scrollTo(0, 0) - e.preventDefault() - e.stopPropagation() - - const allowedJitter = 1.1 - if (supportsPressure) { - bot.setControlState('jump', (e as any).pressure > 0.5) - } - const xDiff = Math.abs(e.pageX - capturedPointer.sourceX) > allowedJitter - const yDiff = Math.abs(e.pageY - capturedPointer.sourceY) > allowedJitter - if (!capturedPointer.activateCameraMove && (xDiff || yDiff)) capturedPointer.activateCameraMove = true - if (capturedPointer.activateCameraMove) { - clearTimeout(virtualClickTimeout) - } - onCameraMove({ movementX: e.pageX - capturedPointer.x, movementY: e.pageY - capturedPointer.y, type: 'touchmove' }) - capturedPointer.x = e.pageX - capturedPointer.y = e.pageY - }, { passive: false }) - - const pointerUpHandler = (e: PointerEvent) => { - if (e.pointerId === undefined) return - if (e.pointerId === joystickPointer.pointer?.pointerId) { - handleMovementStickDelta() - joystickPointer.pointer = null - return - } - if (e.pointerId !== capturedPointer?.id) return - clearTimeout(virtualClickTimeout) - virtualClickTimeout = undefined - - if (options.touchControlsType !== 'joystick-buttons') { - if (virtualClickActive) { - // button 0 is left click - document.dispatchEvent(new MouseEvent('mouseup', { button: 0 })) - virtualClickActive = false - } else if (!capturedPointer.activateCameraMove && (Date.now() - capturedPointer.time < touchStartBreakingBlockMs)) { - document.dispatchEvent(new MouseEvent('mousedown', { button: 2 })) - worldInteractions.update() - document.dispatchEvent(new MouseEvent('mouseup', { button: 2 })) - } - } - capturedPointer = undefined - screenTouches-- - } - registerListener(document, 'pointerup', pointerUpHandler) - registerListener(document, 'pointercancel', pointerUpHandler) - registerListener(document, 'lostpointercapture', pointerUpHandler) - - registerListener(document, 'contextmenu', (e) => e.preventDefault(), false) - - registerListener(document, 'blur', (e) => { - bot.clearControlStates() - }, false) - - console.log('Done!') - - onGameLoad(async () => { - if (!viewer.world.downloadedBlockStatesData && !viewer.world.customBlockStatesData) { - await new Promise(resolve => { - viewer.world.renderUpdateEmitter.once('blockStatesDownloaded', () => resolve()) }) - } - hud.init(renderer, bot, server.host) - hud.style.display = 'block' - }) - + }) + await appViewer.resourcesManager.promiseAssetsReady + } if (appStatusState.isError) return - setLoadingScreenStatus(undefined) - void viewer.waitForChunksToRender().then(() => { - console.log('All done and ready!') + + if (!appViewer.resourcesManager.currentResources?.itemsRenderer) { + await appViewer.resourcesManager.updateAssetsData({}) + } + + const loadWorldStart = Date.now() + console.log('try to focus window') + window.focus?.() + void waitForChunksToLoad().then(() => { + window.worldLoadTime = (Date.now() - loadWorldStart) / 1000 + console.log('All chunks done and ready! Time from renderer connect to ready', (Date.now() - loadWorldStart) / 1000, 's') document.dispatchEvent(new Event('cypress-world-ready')) }) - }) + + try { + if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) + playerState.reactive.onlineMode = !!connectOptions.authenticatedAccount + + progress.setMessage('Placing blocks (starting viewer)') + if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) { + localStorage.lastConnectOptions = JSON.stringify(connectOptions) + if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !location.search.slice(1).length) { + lockUrl() + } + } else { + localStorage.removeItem('lastConnectOptions') + } + connectOptions.onSuccessfulPlay?.() + updateDataAfterJoin() + const password = findServerPassword() + if (password) { + setTimeout(() => { + bot.chat(`/login ${password}`) + }, 500) + } + + + console.log('bot spawned - starting viewer') + await appViewer.startWorld(bot.world, renderDistance) + appViewer.worldView!.listenToBot(bot) + if (appViewer.backend) { + void appViewer.worldView!.init(bot.entity.position) + } + + initMotionTracking() + + // Bot position callback + const botPosition = () => { + appViewer.lastCamUpdate = Date.now() + // this might cause lag, but not sure + appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) + void appViewer.worldView?.updatePosition(bot.entity.position) + } + bot.on('move', botPosition) + botPosition() + + progress.setMessage('Setting callbacks') + + onGameLoad() + + if (appStatusState.isError) return + + const waitForChunks = async () => { + if (appQueryParams.sp === '1') return //todo + const waitForChunks = options.waitForChunksRender === 'sp-only' ? !!singleplayer : options.waitForChunksRender + if (!appViewer.backend || appViewer.rendererState.world.allChunksLoaded || !waitForChunks) { + return + } + + await progress.executeWithMessage( + 'Loading chunks', + 'chunks', + async () => { + await waitForChunksToLoad(progress) + } + ) + } + + await waitForChunks() + + setTimeout(() => { + if (appQueryParams.suggest_save) { + showNotification('Suggestion', 'Save the world to keep your progress!', false, undefined, async () => { + const savePath = await saveToBrowserMemory() + if (!savePath) return + const saveName = savePath.split('/').pop() + bot.end() + // todo hot reload + location.search = `loadSave=${saveName}` + }) + } + }, 600) + + miscUiState.gameLoaded = true + miscUiState.loadedServerIndex = connectOptions.serverIndex ?? '' + customEvents.emit('gameLoaded') + + // Test iOS Safari crash by creating memory pressure + if (appQueryParams.testIosCrash) { + setTimeout(() => { + console.log('Starting iOS crash test with memory pressure...') + // eslint-disable-next-line sonarjs/no-unused-collection + const arrays: number[][] = [] + try { + // Create large arrays until we run out of memory + // eslint-disable-next-line no-constant-condition + while (true) { + const arr = Array.from({ length: 1024 * 1024 }).fill(0).map((_, i) => i) + arrays.push(arr) + } + } catch (e) { + console.error('Memory allocation failed:', e) + } + }, 1000) + } + + progress.end() + setLoadingScreenStatus(undefined) + } catch (err) { + handleError(err) + } + hadConnected = true + } + // don't use spawn event, player can be dead + bot.once(spawnEarlier ? 'forcedMove' : 'health', displayWorld) + + if (singleplayer && connectOptions.serverOverrides.worldFolder) { + fsState.saveLoaded = true + } + + if (!connectOptions.ignoreQs || process.env.NODE_ENV === 'development') { + // todo cleanup + customEvents.on('gameLoaded', () => { + const commands = appQueryParamsArray.command ?? [] + for (let command of commands) { + if (!command.startsWith('/')) command = `/${command}` + const builtinHandled = tryHandleBuiltinCommand(command) + if (!builtinHandled) { + bot.chat(command) + } + } + }) + } } listenGlobalEvents() -watchValue(miscUiState, async s => { - if (s.appLoaded) { // fs ready - const qs = new URLSearchParams(window.location.search) - if (qs.get('singleplayer') === '1') { - loadSingleplayer({}, { - worldFolder: undefined - }) - } - if (qs.get('loadSave')) { - const savePath = `/data/worlds/${qs.get('loadSave')}` - try { - await fs.promises.stat(savePath) - } catch (err) { - alert(`Save ${savePath} not found`) - return - } - await loadInMemorySave(savePath) - } - } -}) // #region fire click event on touch as we disable default behaviors let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined @@ -838,7 +908,9 @@ document.body.addEventListener('touchend', (e) => { activeTouch = undefined }) document.body.addEventListener('touchstart', (e) => { - if (!isGameActive(true)) return + const targetElement = (e.target as HTMLElement).closest('#ui-root') + if (!isGameActive(true) || !targetElement) return + // we always prevent default behavior to disable magnifier on ios, but by doing so we also disable click events e.preventDefault() let firstClickable // todo remove composedPath and this workaround when lit-element is fully dropped const path = e.composedPath() as Array<{ click?: () => void }> @@ -857,42 +929,157 @@ document.body.addEventListener('touchstart', (e) => { }, { passive: false }) // #endregion -void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { - console.warn('Failed to load optional app config.json', error) - return {} -}).then((config) => { - miscUiState.appConfig = config -}) +// immediate game enter actions: reconnect or URL QS +const maybeEnterGame = () => { + const waitForConfigFsLoad = (fn: () => void) => { + let unsubscribe: () => void | undefined + const checkDone = () => { + if (miscUiState.fsReady && miscUiState.appConfig) { + fn() + unsubscribe?.() + return true + } + return false + } -downloadAndOpenFile().then((downloadAction) => { - if (downloadAction) return + if (!checkDone()) { + const text = miscUiState.appConfig ? 'Loading' : 'Loading config' + setLoadingScreenStatus(text) + unsubscribe = subscribe(miscUiState, checkDone) + } + } - window.addEventListener('hud-ready', (e) => { - // try to connect to peer - const qs = new URLSearchParams(window.location.search) - const peerId = qs.get('connectPeer') - const version = qs.get('peerVersion') - if (peerId) { - let username: string | null = options.guestUsername - if (options.askGuestName) username = prompt('Enter your username', username) - if (!username) return - options.guestUsername = username - void connect({ - username, - botVersion: version || undefined, - peerId + const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined + + if (reconnectOptions) { + sessionStorage.removeItem('reconnectOptions') + if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) { + return waitForConfigFsLoad(async () => { + void connect(reconnectOptions.value) }) } - }) - if (document.getElementById('hud').isReady) window.dispatchEvent(new Event('hud-ready')) -}, (err) => { + } + + if (appQueryParams.reconnect && localStorage.lastConnectOptions && process.env.NODE_ENV === 'development') { + const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {}) + return waitForConfigFsLoad(async () => { + void connect({ + botVersion: appQueryParams.version ?? undefined, + ...lastConnect, + ip: appQueryParams.ip || undefined + }) + }) + } + + if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') { + return waitForConfigFsLoad(async () => { + loadSingleplayer({}, { + worldFolder: undefined, + ...appQueryParams.version ? { version: appQueryParams.version } : {} + }) + }) + } + if (appQueryParams.loadSave) { + const enterSave = async () => { + const savePath = `/data/worlds/${appQueryParams.loadSave}` + try { + await fs.promises.stat(savePath) + await loadInMemorySave(savePath) + } catch (err) { + alert(`Save ${savePath} not found`) + } + } + return waitForConfigFsLoad(enterSave) + } + + if (appQueryParams.ip || appQueryParams.proxy) { + const openServerAction = () => { + if (appQueryParams.autoConnect && miscUiState.appConfig?.allowAutoConnect) { + void connect({ + server: appQueryParams.ip, + proxy: getCurrentProxy(), + botVersion: appQueryParams.version ?? undefined, + username: getCurrentUsername()!, + }) + return + } + + setLoadingScreenStatus(undefined) + if (appQueryParams.onlyConnect || process.env.ALWAYS_MINIMAL_SERVER_UI === 'true') { + showModal({ reactType: 'only-connect-server' }) + } else { + showModal({ reactType: 'editServer' }) + } + } + + // showModal({ reactType: 'empty' }) + return waitForConfigFsLoad(openServerAction) + } + + if (appQueryParams.connectPeer) { + // try to connect to peer + const peerId = appQueryParams.connectPeer + const peerOptions = {} as ConnectPeerOptions + if (appQueryParams.server) { + peerOptions.server = appQueryParams.server + } + const version = appQueryParams.peerVersion + let username: string | null = options.guestUsername + if (options.askGuestName) username = prompt('Enter your username to connect to peer', username) + if (!username) return + options.guestUsername = username + void connect({ + username, + botVersion: version || undefined, + peerId, + peerOptions + }) + return + + } + + if (appQueryParams.viewerConnect) { + void connect({ + username: `viewer-${Math.random().toString(36).slice(2, 10)}`, + viewerWsConnect: appQueryParams.viewerConnect, + }) + return + } + + if (appQueryParams.modal) { + const modals = appQueryParams.modal.split(',') + for (const modal of modals) { + showModal({ reactType: modal }) + } + return + } + + if (appQueryParams.serversList && !miscUiState.appConfig?.appParams?.serversList) { + // open UI only if it's in URL + showModal({ reactType: 'serversList' }) + } + + if (isInterestedInDownload()) { + void downloadAndOpenFile() + } + + void possiblyHandleStateVariable() +} + +try { + maybeEnterGame() +} catch (err) { console.error(err) - alert(`Failed to download file: ${err}`) -}) + alert(`Something went wrong: ${err}`) +} // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const initialLoader = document.querySelector('.initial-loader') as HTMLElement | null if (initialLoader) { initialLoader.style.opacity = '0' - window.pageLoaded = true + initialLoader.style.pointerEvents = 'none' } +window.pageLoaded = true + +appViewer.waitBackendLoadPromises.push(appStartup()) +registerOpenBenchmarkListener() diff --git a/src/interactionShapesGenerated.json b/src/interactionShapesGenerated.json index 425cfb0d..804952e0 100644 --- a/src/interactionShapesGenerated.json +++ b/src/interactionShapesGenerated.json @@ -457,7 +457,14 @@ 13, 14 ], - "powder_snow": "PowderSnowBlock", + "powder_snow": [ + 0, + 0, + 0, + 16, + 16, + 16 + ], "spore_blossom": [ 2, 13, @@ -1311,55 +1318,47 @@ 13 ], "lever": { - "face=ceiling,facing=east": [ - 4, - 0, - 5, - 12, - 6, - 11 - ], - "face=ceiling,facing=north": [ - 5, - 0, - 4, - 11, - 6, - 12 - ], - "face=ceiling,facing=south": [ - 5, - 0, - 4, - 11, - 6, - 12 - ], - "face=ceiling,facing=west": [ - 4, - 0, - 5, - 12, - 6, - 11 - ], "face=floor,facing=east": [ 4, - 10, + 0, 5, 12, - 16, + 6, 11 ], "face=floor,facing=north": [ 5, - 10, + 0, 4, 11, - 16, + 6, 12 ], "face=floor,facing=south": [ + 5, + 0, + 4, + 11, + 6, + 12 + ], + "face=floor,facing=west": [ + 4, + 0, + 5, + 12, + 6, + 11 + ], + "face=ceiling,facing=east": [ + 4, + 10, + 5, + 12, + 16, + 11 + ], + "face=ceiling,facing=north": [ 5, 10, 4, @@ -1367,7 +1366,15 @@ 16, 12 ], - "face=floor,facing=west": [ + "face=ceiling,facing=south": [ + 5, + 10, + 4, + 11, + 16, + 12 + ], + "face=ceiling,facing=west": [ 4, 10, 5, @@ -1632,20 +1639,20 @@ }, "stone_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -1664,34 +1671,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -1760,20 +1767,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -1792,34 +1799,34 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] @@ -2374,20 +2381,20 @@ }, "oak_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -2406,34 +2413,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -2502,20 +2509,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -2534,54 +2541,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "spruce_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -2600,34 +2607,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -2696,20 +2703,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -2728,54 +2735,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "birch_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -2794,34 +2801,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -2890,20 +2897,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -2922,54 +2929,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "jungle_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -2988,34 +2995,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -3084,20 +3091,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -3116,54 +3123,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "acacia_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -3182,34 +3189,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -3278,20 +3285,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -3310,54 +3317,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "cherry_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -3376,34 +3383,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -3472,20 +3479,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -3504,54 +3511,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "dark_oak_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -3570,34 +3577,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -3666,20 +3673,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -3698,54 +3705,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "mangrove_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -3764,34 +3771,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -3860,20 +3867,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -3892,54 +3899,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "bamboo_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -3958,34 +3965,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -4054,20 +4061,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -4086,34 +4093,34 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] @@ -5813,20 +5820,20 @@ }, "crimson_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -5845,34 +5852,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -5941,20 +5948,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -5973,54 +5980,54 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] }, "warped_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -6039,34 +6046,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -6135,20 +6142,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -6167,34 +6174,34 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] @@ -6303,20 +6310,20 @@ }, "polished_blackstone_button": { "face=floor,facing=north,powered=false": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 2, - 11 + 10 ], "face=floor,facing=north,powered=true": [ - 6, - 0, 5, - 10, + 0, + 6, + 11, 1, - 11 + 10 ], "face=floor,facing=south,powered=false": [ 5, @@ -6335,34 +6342,34 @@ 10 ], "face=floor,facing=west,powered=false": [ - 14, + 6, 0, 5, - 16, + 10, 2, 11 ], "face=floor,facing=west,powered=true": [ - 15, + 6, 0, 5, - 16, + 10, 1, 11 ], "face=floor,facing=east,powered=false": [ - 0, + 6, 0, 5, - 2, + 10, 2, 11 ], "face=floor,facing=east,powered=true": [ - 0, + 6, 0, 5, - 1, + 10, 1, 11 ], @@ -6431,20 +6438,20 @@ 11 ], "face=ceiling,facing=north,powered=false": [ - 6, - 14, 5, - 10, + 14, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=north,powered=true": [ - 6, - 15, 5, - 10, + 15, + 6, + 11, 16, - 11 + 10 ], "face=ceiling,facing=south,powered=false": [ 5, @@ -6463,34 +6470,34 @@ 10 ], "face=ceiling,facing=west,powered=false": [ - 14, + 6, 14, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=west,powered=true": [ - 15, + 6, 15, 5, - 16, + 10, 16, 11 ], "face=ceiling,facing=east,powered=false": [ - 0, + 6, 14, 5, - 2, + 10, 16, 11 ], "face=ceiling,facing=east,powered=true": [ - 0, + 6, 15, 5, - 1, + 10, 16, 11 ] @@ -6547,7 +6554,6 @@ 16, 15 ], - "pink_petals": "PinkPetalsBlock", "big_dripleaf_stem": { "facing=north": [ 5, diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts new file mode 100644 index 00000000..d40260df --- /dev/null +++ b/src/inventoryWindows.ts @@ -0,0 +1,666 @@ +import { proxy, subscribe } from 'valtio' +import { showInventory } from 'minecraft-inventory-gui/web/ext.mjs' + +// import Dirt from 'mc-assets/dist/other-textures/latest/blocks/dirt.png' +import { RecipeItem } from 'minecraft-data' +import { flat, fromFormattedString } from '@xmcl/text-component' +import { splitEvery, equals } from 'rambda' +import PItem, { Item } from 'prismarine-item' +import { versionToNumber } from 'renderer/viewer/common/utils' +import { getRenamedData } from 'flying-squid/dist/blockRenames' +import PrismarineChatLoader from 'prismarine-chat' +import * as nbt from 'prismarine-nbt' +import { BlockModel } from 'mc-assets' +import { renderSlot } from 'renderer/viewer/three/renderSlot' +import { loadSkinFromUsername } from 'renderer/viewer/lib/utils/skins' +import Generic95 from '../assets/generic_95.png' +import { appReplacableResources } from './generated/resources' +import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' +import { options } from './optionsStorage' +import { assertDefined, inGameError } from './utils' +import { displayClientChat } from './botUtils' +import { currentScaling } from './scaleInterface' +import { getItemDescription } from './itemsDescriptions' +import { MessageFormatPart } from './chatUtils' +import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' +import { playerState } from './mineflayer/playerState' +import { modelViewerState } from './react/OverlayModelViewer' + +const loadedImagesCache = new Map() +const cleanLoadedImagesCache = () => { + loadedImagesCache.delete('blocks') + loadedImagesCache.delete('items') +} + +let lastWindow: ReturnType +let lastWindowType: string | null | undefined // null is inventory +/** bot version */ +let version: string +let PrismarineItem: typeof Item + +export const jeiCustomCategories = proxy({ + value: [] as Array<{ id: string, categoryTitle: string, items: any[] }> +}) + +let remotePlayerSkin: string | undefined | Promise + +export const showInventoryPlayer = () => { + modelViewerState.model = { + positioning: { + windowWidth: 176, + windowHeight: 166, + x: 25, + y: 8, + width: 50, + height: 70, + scaled: true, + onlyInitialScale: true, + followCursor: true, + }, + // models: ['https://bucket.mcraft.fun/sitarbuckss.glb'], + // debug: true, + steveModelSkin: appViewer.playerState.reactive.playerSkin ?? (typeof remotePlayerSkin === 'string' ? remotePlayerSkin : ''), + } + if (remotePlayerSkin === undefined && !appViewer.playerState.reactive.playerSkin) { + remotePlayerSkin = loadSkinFromUsername(bot.username, 'skin').then(a => { + setTimeout(() => { showInventoryPlayer() }, 0) // todo patch instead and make reactive + remotePlayerSkin = a ?? '' + return remotePlayerSkin + }) + } +} + +export const onGameLoad = () => { + version = bot.version + + PrismarineItem = PItem(version) + + const mapWindowType = (type: string, inventoryStart: number) => { + if (type === 'minecraft:container') { + if (inventoryStart === 45 - 9 * 4) return 'minecraft:generic_9x1' + if (inventoryStart === 45 - 9 * 3) return 'minecraft:generic_9x2' + if (inventoryStart === 45 - 9 * 2) return 'minecraft:generic_9x3' + if (inventoryStart === 45 - 9) return 'minecraft:generic_9x4' + if (inventoryStart === 45) return 'minecraft:generic_9x5' + if (inventoryStart === 45 + 9) return 'minecraft:generic_9x6' + } + return type + } + + const maybeParseNbtJson = (data: any) => { + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch (err) { + // ignore + } + } + return nbt.simplify(data) ?? data + } + + bot.on('windowOpen', (win) => { + const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)] + if (implementedWindow) { + openWindow(implementedWindow, maybeParseNbtJson(win.title)) + } else if (options.unimplementedContainers) { + openWindow('ChestWin', maybeParseNbtJson(win.title)) + } else { + // todo format + displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`) + bot.currentWindow?.['close']() + } + }) + + // workaround: singleplayer player inventory crafting + let skipUpdate = false + bot.inventory.on('updateSlot', ((_oldSlot, oldItem, newItem) => { + const currentSlot = _oldSlot as number + if (!miscUiState.singleplayer || oldItem === newItem || skipUpdate) return + const { craftingResultSlot } = bot.inventory + if (currentSlot === craftingResultSlot && oldItem && !newItem) { + for (let i = 1; i < 5; i++) { + const count = bot.inventory.slots[i]?.count + if (count && count > 1) { + const slot = bot.inventory.slots[i]! + slot.count-- + void bot.creative.setInventorySlot(i, slot) + } else { + void bot.creative.setInventorySlot(i, null) + } + } + return + } + if (currentSlot > 4) return + const craftingSlots = bot.inventory.slots.slice(1, 5) + try { + const resultingItem = getResultingRecipe(craftingSlots, 2) + skipUpdate = true + void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null).then(() => { + skipUpdate = false + }) + } catch (err) { + console.error(err) + // todo resolve the error! and why would we ever get here on every update? + } + }) as any) + + bot.on('windowClose', () => { + // todo hide up to the window itself! + if (lastWindow) { + hideCurrentModal() + } + }) + bot.on('respawn', () => { // todo validate logic against native client (maybe login) + if (lastWindow) { + hideCurrentModal() + } + }) + + customEvents.on('search', (q) => { + if (!lastWindow) return + upJei(q) + }) + + if (!appViewer.resourcesManager['_inventoryChangeTracked']) { + appViewer.resourcesManager['_inventoryChangeTracked'] = true + const texturesChanged = () => { + cleanLoadedImagesCache() + if (!lastWindow) return + upWindowItemsLocal() + upJei(lastJeiSearch) + } + appViewer.resourcesManager.on('assetsInventoryReady', () => texturesChanged()) + appViewer.resourcesManager.on('assetsTexturesUpdated', () => texturesChanged()) + } +} + +const getImageSrc = (path): string | HTMLImageElement | ImageBitmap => { + switch (path) { + case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content + case 'blocks': return appViewer.resourcesManager.blocksAtlasParser.latestImage + case 'items': return appViewer.resourcesManager.itemsAtlasParser.latestImage + case 'gui': return appViewer.resourcesManager.currentResources!.guiAtlas!.image + case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content + case 'gui/container/furnace': return appReplacableResources.latest_gui_container_furnace.content + case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.content + case 'gui/container/shulker_box': return appReplacableResources.latest_gui_container_shulker_box.content + case 'gui/container/generic_54': return appReplacableResources.latest_gui_container_generic_54.content + case 'gui/container/generic_95': return Generic95 + case 'gui/container/hopper': return appReplacableResources.latest_gui_container_hopper.content + case 'gui/container/horse': return appReplacableResources.latest_gui_container_horse.content + case 'gui/container/villager2': return appReplacableResources.latest_gui_container_villager2.content + case 'gui/container/enchanting_table': return appReplacableResources.latest_gui_container_enchanting_table.content + case 'gui/container/anvil': return appReplacableResources.latest_gui_container_anvil.content + case 'gui/container/beacon': return appReplacableResources.latest_gui_container_beacon.content + case 'gui/widgets': return appReplacableResources.other_textures_latest_gui_widgets.content + } + // empty texture + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' +} + +const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any, image = undefined as HTMLImageElement | undefined }, onLoad = () => { }) => { + if (image) { + return image + } + if (!path && !texture) { + throw new Error('Either pass path or texture') + } + const loadPath = (blockData ? 'blocks' : path ?? texture)! + if (loadedImagesCache.has(loadPath)) { + onLoad() + } else { + const imageSrc = getImageSrc(loadPath) + if (imageSrc instanceof ImageBitmap) { + onLoad() + loadedImagesCache.set(loadPath, imageSrc) + return imageSrc + } + + let image: HTMLImageElement + if (imageSrc instanceof Image) { + image = imageSrc + } else { + image = new Image() + image.src = imageSrc + } + image.onload = onLoad + loadedImagesCache.set(loadPath, image) + } + return loadedImagesCache.get(loadPath) +} + +const getItemName = (slot: Item | RenderItem | null) => { + const parsed = getItemNameRaw(slot, appViewer.resourcesManager) + if (!parsed) return + // todo display full text renderer from sign renderer + const text = flat(parsed as MessageFormatPart).map(x => (typeof x === 'string' ? x : x.text)) + return text.join('') +} + +let lastMappedSlots = [] as any[] +const itemToVisualKey = (slot: RenderItem | Item | null) => { + if (!slot) return '' + const keys = [ + slot.name, + slot.durabilityUsed, + slot.maxDurability, + slot['count'], + slot['metadata'], + slot.nbt ? JSON.stringify(slot.nbt) : '', + slot['components'] ? JSON.stringify(slot['components']) : '', + appViewer.resourcesManager.currentResources!.guiAtlasVersion, + ].join('|') + return keys +} +const validateSlot = (slot: any, index: number) => { + if (!slot.texture) { + throw new Error(`Slot has no texture: ${index} ${slot.name}`) + } +} +const mapSlots = (slots: Array, isJei = false) => { + const newSlots = slots.map((slot, i) => { + if (!slot) return null + + if (!isJei) { + const oldKey = lastMappedSlots[i]?.cacheKey + const newKey = itemToVisualKey(slot) + slot['cacheKey'] = i + '|' + newKey + if (oldKey && oldKey === newKey) { + validateSlot(lastMappedSlots[i], i) + return lastMappedSlots[i] + } + } + + try { + if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) + const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot + const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, appViewer.playerState.reactive) + const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, appViewer.resourcesManager, debugIsQuickbar) + const itemCustomName = getItemName(slot) + Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) + //@ts-expect-error + slot.toJSON = () => { + // Allow to serialize slot to JSON as minecraft-inventory-gui creates icon property as cache (recursively) + //@ts-expect-error + const { icon, ...rest } = slot + return rest + } + validateSlot(slot, i) + } catch (err) { + inGameError(err) + } + return slot + }) + lastMappedSlots = JSON.parse(JSON.stringify(newSlots)) + return newSlots +} + +export const upInventoryItems = (isInventory: boolean, invWindow = lastWindow) => { + // inv.pwindow.inv.slots[2].displayName = 'test' + // inv.pwindow.inv.slots[2].blockData = getBlockData('dirt') + const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots) + invWindow.pwindow.setSlots(customSlots) + return customSlots +} + +export const onModalClose = (callback: () => any) => { + const modal = activeModalStack.at(-1) + const unsubscribe = subscribe(activeModalStack, () => { + const newModal = activeModalStack.at(-1) + if (modal?.reactType !== newModal?.reactType) { + callback() + unsubscribe() + } + }, true) +} + +const implementedContainersGuiMap = { + // todo allow arbitrary size instead! + 'minecraft:generic_9x1': 'ChestWin', + 'minecraft:generic_9x2': 'ChestWin', + 'minecraft:generic_9x3': 'ChestWin', + 'minecraft:generic_9x4': 'Generic95Win', + 'minecraft:generic_9x5': 'Generic95Win', + // hopper + 'minecraft:generic_5x1': 'HopperWin', + 'minecraft:generic_9x6': 'LargeChestWin', + 'minecraft:generic_3x3': 'DropDispenseWin', + 'minecraft:furnace': 'FurnaceWin', + 'minecraft:smoker': 'FurnaceWin', + 'minecraft:shulker_box': 'ChestWin', + 'minecraft:blast_furnace': 'FurnaceWin', + 'minecraft:crafting': 'CraftingWin', + 'minecraft:crafting3x3': 'CraftingWin', // todo different result slot + 'minecraft:anvil': 'AnvilWin', + // enchant + 'minecraft:enchanting_table': 'EnchantingWin', + // horse + 'minecraft:horse': 'HorseWin', + // villager + 'minecraft:villager': 'VillagerWin', +} + +let lastJeiSearch = '' +const upJei = (search: string) => { + lastJeiSearch = search + search = search.toLowerCase() + // todo fix pre flat + const itemsArray = [ + ...jeiCustomCategories.value.flatMap(x => x.items).filter(x => x !== null), + ...loadedData.itemsArray.filter(x => x.displayName.toLowerCase().includes(search)).map(item => new PrismarineItem(item.id, 1)).filter(x => x !== null) + ] + const matchedSlots = itemsArray.map(x => { + x.displayName = getItemName(x) ?? x.displayName + if (!x.displayName.toLowerCase().includes(search)) return null + return x + }).filter(a => a !== null) + lastWindow.pwindow.win.jeiSlotsPage = 0 + lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots, true) +} + +export const openItemsCanvas = (type, _bot = bot as typeof bot | null) => { + const inv = showInventory(type, getImage, {}, _bot); + (inv.canvasManager.children[0].callbacks as any).getItemRecipes = (item) => { + const allRecipes = getAllItemRecipes(item.name) + inv.canvasManager.children[0].messageDisplay = '' + const itemDescription = getItemDescription(item) + if (!allRecipes?.length && !itemDescription) { + inv.canvasManager.children[0].messageDisplay = `No recipes found for ${item.displayName}` + } + return [...allRecipes ?? [], ...itemDescription ? [ + [ + 'GenericDescription', + mapSlots([item], true)[0], + [], + itemDescription + ] + ] : []] + } + (inv.canvasManager.children[0].callbacks as any).getItemUsages = (item) => { + const allItemUsages = getAllItemUsages(item.name) + inv.canvasManager.children[0].messageDisplay = '' + if (!allItemUsages?.length) { + inv.canvasManager.children[0].messageDisplay = `No usages found for ${item.displayName}` + } + return allItemUsages + } + return inv +} + +const upWindowItemsLocal = () => { + if (!lastWindow && bot.currentWindow) { + // edge case: might happen due to high ping, inventory should be closed soon! + // openWindow(implementedContainersGuiMap[bot.currentWindow.type]) + return + } + void Promise.resolve().then(() => upInventoryItems(lastWindowType === null)) +} + +let skipClosePacketSending = false +const openWindow = (type: string | undefined, title: string | any = undefined) => { + // if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) { + if (activeModalStack.length) { // game is not in foreground, don't close current modal + if (type) { + skipClosePacketSending = true + hideCurrentModal() + } else { + bot.currentWindow?.['close']() + return + } + } + lastWindowType = type ?? null + showModal({ + reactType: `player_win:${type}`, + }) + onModalClose(() => { + // might be already closed (event fired) + if (type !== undefined && bot.currentWindow && !skipClosePacketSending) bot.currentWindow['close']() + lastWindow.destroy() + lastWindow = null as any + lastWindowType = null + window.inventory = null + miscUiState.displaySearchInput = false + destroyFn() + skipClosePacketSending = false + + modelViewerState.model = undefined + }) + if (type === undefined) { + showInventoryPlayer() + } + cleanLoadedImagesCache() + const inv = openItemsCanvas(type) + inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch + window.inventory = inv + const PrismarineChat = PrismarineChatLoader(bot.version) + try { + inv.canvasManager.children[0].customTitleText = title ? + typeof title === 'string' ? + fromFormattedString(title).text : + new PrismarineChat(title).toString() : + undefined + } catch (err) { + reportError?.(err) + inv.canvasManager.children[0].customTitleText = undefined + } + // todo + inv.canvasManager.setScale(currentScaling.scale === 1 ? 1.5 : currentScaling.scale) + inv.canvas.style.zIndex = '10' + inv.canvas.style.position = 'fixed' + inv.canvas.style.inset = '0' + + inv.canvasManager.onClose = async () => { + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + if (activeModalStack.at(-1)?.reactType?.includes('player_win:')) { + hideModal(undefined, undefined, { force: true }) + } + inv.canvasManager.destroy() + } + + lastWindow = inv + + upWindowItemsLocal() + + lastWindow.pwindow.touch = miscUiState.currentTouch ?? false + const oldOnInventoryEvent = lastWindow.pwindow.onInventoryEvent.bind(lastWindow.pwindow) + lastWindow.pwindow.onInventoryEvent = (type, containing, windowIndex, inventoryIndex, item) => { + if (inv.canvasManager.children[0].currentGuide) { + const isRightClick = type === 'rightclick' + const isLeftClick = type === 'leftclick' + if (isLeftClick || isRightClick) { + modelViewerState.model = undefined + inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item) + } + } else { + oldOnInventoryEvent(type, containing, windowIndex, inventoryIndex, item) + } + } + lastWindow.pwindow.onJeiClick = (slotItem, _index, isRightclick) => { + if (versionToNumber(bot.version) < versionToNumber('1.13')) { + alert('Item give is broken on 1.12.2 and below, we are working on it!') + return + } + // slotItem is the slot from mapSlots + const itemId = loadedData.itemsByName[slotItem.name]?.id + if (!itemId) { + inGameError(`Item for block ${slotItem.name} not found`) + return + } + const item = PrismarineItem.fromNotch({ + ...slotItem, + itemId, + itemCount: isRightclick ? 64 : 1, + components: slotItem.components ?? [], + removeComponents: slotItem.removedComponents ?? [], + itemDamage: slotItem.metadata ?? 0, + nbt: slotItem.nbt, + }) + if (bot.game.gameMode === 'creative') { + const freeSlot = bot.inventory.firstEmptyInventorySlot() + if (freeSlot === null) return + void bot.creative.setInventorySlot(freeSlot, item) + } else { + modelViewerState.model = undefined + inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0]) + } + } + + const isJeiEnabled = () => { + if (typeof options.jeiEnabled === 'boolean') return options.jeiEnabled + if (Array.isArray(options.jeiEnabled)) { + return options.jeiEnabled.includes(bot.game?.gameMode as any) + } + return false + } + + if (isJeiEnabled()) { + lastWindow.pwindow.win.jeiSlotsPage = 0 + // todo workaround so inventory opens immediately (though it still lags) + setTimeout(() => { + upJei('') + }) + miscUiState.displaySearchInput = true + } else { + lastWindow.pwindow.win.jeiSlots = [] + miscUiState.displaySearchInput = false + } + + if (type === undefined) { + // player inventory + bot.inventory.on('updateSlot', upWindowItemsLocal) + destroyFn = () => { + bot.inventory.off('updateSlot', upWindowItemsLocal) + } + } else { + //@ts-expect-error + bot.currentWindow.on('updateSlot', () => { + upWindowItemsLocal() + }) + } +} + +let destroyFn = () => { } + +export const openPlayerInventory = () => { + openWindow(undefined) +} + +const getResultingRecipe = (slots: Array, gridRows: number) => { + const inputSlotsItems = slots.map(blockSlot => blockSlot?.type) + let currentShape = splitEvery(gridRows, inputSlotsItems as Array) + // todo rewrite with candidates search + if (currentShape.length > 1) { + // eslint-disable-next-line @typescript-eslint/no-for-in-array + for (const slotX in currentShape[0]) { + if (currentShape[0][slotX] !== undefined) { + for (const [otherY] of Array.from({ length: gridRows }).entries()) { + if (currentShape[otherY]?.[slotX] === undefined) { + currentShape[otherY]![slotX] = null + } + } + } + } + } + currentShape = currentShape.map(arr => arr.filter(x => x !== undefined)).filter(x => x.length !== 0) + + // todo rewrite + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + const slotsIngredients = [...inputSlotsItems].sort().filter(item => item !== undefined) + type Result = RecipeItem | undefined + let shapelessResult: Result + let shapeResult: Result + outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes ?? {})) { + for (const recipeVariant of recipeVariants) { + if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) { + shapeResult = recipeVariant.result! + break outer + } + if ('ingredients' in recipeVariant && equals(slotsIngredients, recipeVariant.ingredients?.sort() as number[])) { + shapelessResult = recipeVariant.result + break outer + } + } + } + const result = shapeResult ?? shapelessResult + if (!result) return + const id = typeof result === 'number' ? result : Array.isArray(result) ? result[0] : result.id + if (!id) return + const count = (typeof result === 'number' ? undefined : Array.isArray(result) ? result[1] : result.count) ?? 1 + const metadata = typeof result === 'object' && !Array.isArray(result) ? result.metadata : undefined + const item = new PrismarineItem(id, count, metadata) + return item +} + +const ingredientToItem = (recipeItem) => (recipeItem === null ? null : new PrismarineItem(recipeItem, 1)) + +const getAllItemRecipes = (itemName: string) => { + const item = loadedData.itemsByName[itemName] + if (!item) return + const itemId = item.id + const recipes = loadedData.recipes?.[itemId] + if (!recipes) return + const results = [] as Array<{ + result: Item, + ingredients: Array, + description?: string + }> + + // get recipes here + for (const recipe of recipes) { + const { result } = recipe + if (!result) continue + const resultId = typeof result === 'number' ? result : Array.isArray(result) ? result[0]! : result.id + const resultCount = (typeof result === 'number' ? undefined : Array.isArray(result) ? result[1] : result.count) ?? 1 + const resultMetadata = typeof result === 'object' && !Array.isArray(result) ? result.metadata : undefined + const resultItem = new PrismarineItem(resultId!, resultCount, resultMetadata) + if ('inShape' in recipe) { + const ingredients = recipe.inShape + if (!ingredients) continue + + const ingredientsItems = ingredients.flatMap(items => items.map(item => ingredientToItem(item))) + results.push({ result: resultItem, ingredients: ingredientsItems }) + } + if ('ingredients' in recipe) { + const { ingredients } = recipe + if (!ingredients) continue + const ingredientsItems = ingredients.map(item => ingredientToItem(item)) + results.push({ result: resultItem, ingredients: ingredientsItems, description: 'Shapeless' }) + } + } + return results.map(({ result, ingredients, description }) => { + return [ + 'CraftingTableGuide', + mapSlots([result], true)[0], + mapSlots(ingredients, true), + description + ] + }) +} + +const getAllItemUsages = (itemName: string) => { + const item = loadedData.itemsByName[itemName] + if (!item) return + const foundRecipeIds = [] as string[] + + for (const [id, recipes] of Object.entries(loadedData.recipes ?? {})) { + for (const recipe of recipes) { + if ('inShape' in recipe) { + if (recipe.inShape.some(row => row.includes(item.id))) { + foundRecipeIds.push(id) + } + } + if ('ingredients' in recipe) { + if (recipe.ingredients.includes(item.id)) { + foundRecipeIds.push(id) + } + } + } + } + + return foundRecipeIds.flatMap(id => { + // todo should use exact match, not include all recipes! + return getAllItemRecipes(loadedData.items[id].name) + }) +} diff --git a/src/invsprite.json b/src/invsprite.json deleted file mode 100644 index 58cd192d..00000000 --- a/src/invsprite.json +++ /dev/null @@ -1,4812 +0,0 @@ -{ - "air": { - "name": "Air", - "x": 832, - "y": 2336 - }, - "stone": { - "name": "Stone", - "x": 224, - "y": 992 - }, - "granite": { - "name": "Granite", - "x": 96, - "y": 3360 - }, - "polished_granite": { - "name": "Polished Granite", - "x": 384, - "y": 896 - }, - "diorite": { - "name": "Diorite", - "x": 64, - "y": 3360 - }, - "polished_diorite": { - "name": "Polished Diorite", - "x": 352, - "y": 896 - }, - "andesite": { - "name": "Andesite", - "x": 832, - "y": 3328 - }, - "polished_andesite": { - "name": "Polished Andesite", - "x": 320, - "y": 896 - }, - "grass_block": { - "name": "Grass Block", - "x": 640, - "y": 960 - }, - "dirt": { - "name": "Dirt", - "x": 576, - "y": 960 - }, - "coarse_dirt": { - "name": "Coarse Dirt", - "x": 416, - "y": 960 - }, - "podzol": { - "name": "Podzol", - "x": 352, - "y": 3360 - }, - "crimson_nylium": { - "name": "Crimson Nylium", - "x": 640, - "y": 128 - }, - "warped_nylium": { - "name": "Warped Nylium", - "x": 768, - "y": 128 - }, - "cobblestone": { - "name": "Cobblestone", - "x": 960, - "y": 3328 - }, - "oak_planks": { - "name": "Oak Planks", - "x": 192, - "y": 896 - }, - "spruce_planks": { - "name": "Spruce Planks", - "x": 0, - "y": 928 - }, - "birch_planks": { - "name": "Birch Planks", - "x": 832, - "y": 832 - }, - "jungle_planks": { - "name": "Jungle Planks", - "x": 992, - "y": 864 - }, - "acacia_planks": { - "name": "Acacia Planks", - "x": 672, - "y": 832 - }, - "dark_oak_planks": { - "name": "Dark Oak Planks", - "x": 512, - "y": 864 - }, - "crimson_planks": { - "name": "Crimson Planks", - "x": 544, - "y": 128 - }, - "warped_planks": { - "name": "Warped Planks", - "x": 576, - "y": 128 - }, - "oak_sapling": { - "name": "Oak Sapling", - "x": 192, - "y": 3424 - }, - "spruce_sapling": { - "name": "Spruce Sapling", - "x": 608, - "y": 3424 - }, - "birch_sapling": { - "name": "Birch Sapling", - "x": 416, - "y": 3392 - }, - "jungle_sapling": { - "name": "Jungle Sapling", - "x": 960, - "y": 3392 - }, - "acacia_sapling": { - "name": "Acacia Sapling", - "x": 224, - "y": 3392 - }, - "dark_oak_sapling": { - "name": "Dark Oak Sapling", - "x": 768, - "y": 3392 - }, - "bedrock": { - "name": "Bedrock", - "x": 320, - "y": 960 - }, - "sand": { - "name": "Sand", - "x": 480, - "y": 3360 - }, - "red_sand": { - "name": "Red Sand", - "x": 416, - "y": 3360 - }, - "gravel": { - "name": "Gravel", - "x": 128, - "y": 3360 - }, - "gold_ore": { - "name": "Gold Ore", - "x": 32, - "y": 3392 - }, - "iron_ore": { - "name": "Iron Ore", - "x": 64, - "y": 3392 - }, - "coal_ore": { - "name": "Coal Ore", - "x": 960, - "y": 3360 - }, - "nether_gold_ore": { - "name": "Nether Gold Ore", - "x": 288, - "y": 192 - }, - "oak_log": { - "name": "Oak Log", - "x": 160, - "y": 3424 - }, - "spruce_log": { - "name": "Spruce Log", - "x": 576, - "y": 3424 - }, - "birch_log": { - "name": "Birch Log", - "x": 384, - "y": 3392 - }, - "jungle_log": { - "name": "Jungle Log", - "x": 928, - "y": 3392 - }, - "acacia_log": { - "name": "Acacia Log", - "x": 192, - "y": 3392 - }, - "dark_oak_log": { - "name": "Dark Oak Log", - "x": 736, - "y": 3392 - }, - "crimson_stem": { - "name": "Crimson Stem", - "x": 672, - "y": 128 - }, - "warped_stem": { - "name": "Warped Stem", - "x": 800, - "y": 128 - }, - "stripped_oak_log": { - "name": "Stripped Oak Log", - "x": 288, - "y": 1664 - }, - "stripped_spruce_log": { - "name": "Stripped Spruce Log", - "x": 320, - "y": 1664 - }, - "stripped_birch_log": { - "name": "Stripped Birch Log", - "x": 192, - "y": 1664 - }, - "stripped_jungle_log": { - "name": "Stripped Jungle Log", - "x": 256, - "y": 1664 - }, - "stripped_acacia_log": { - "name": "Stripped Acacia Log", - "x": 160, - "y": 1664 - }, - "stripped_dark_oak_log": { - "name": "Stripped Dark Oak Log", - "x": 224, - "y": 1664 - }, - "stripped_crimson_stem": { - "name": "Stripped Crimson Stem", - "x": 448, - "y": 160 - }, - "stripped_warped_stem": { - "name": "Stripped Warped Stem", - "x": 480, - "y": 160 - }, - "stripped_oak_wood": { - "name": "Stripped Oak Wood", - "x": 576, - "y": 2080 - }, - "stripped_spruce_wood": { - "name": "Stripped Spruce Wood", - "x": 608, - "y": 2080 - }, - "stripped_birch_wood": { - "name": "Stripped Birch Wood", - "x": 640, - "y": 2080 - }, - "stripped_jungle_wood": { - "name": "Stripped Jungle Wood", - "x": 672, - "y": 2080 - }, - "stripped_acacia_wood": { - "name": "Stripped Acacia Wood", - "x": 704, - "y": 2080 - }, - "stripped_dark_oak_wood": { - "name": "Stripped Dark Oak Wood", - "x": 736, - "y": 2080 - }, - "stripped_crimson_hyphae": { - "name": "Stripped Crimson Hyphae", - "x": 224, - "y": 192 - }, - "stripped_warped_hyphae": { - "name": "Stripped Warped Hyphae", - "x": 256, - "y": 192 - }, - "oak_wood": { - "name": "Oak Wood", - "x": 704, - "y": 3264 - }, - "spruce_wood": { - "name": "Spruce Wood", - "x": 736, - "y": 3296 - }, - "birch_wood": { - "name": "Birch Wood", - "x": 0, - "y": 3232 - }, - "jungle_wood": { - "name": "Jungle Wood", - "x": 192, - "y": 3264 - }, - "acacia_wood": { - "name": "Acacia Wood", - "x": 768, - "y": 3200 - }, - "dark_oak_wood": { - "name": "Dark Oak Wood", - "x": 704, - "y": 3232 - }, - "crimson_hyphae": { - "name": "Crimson Hyphae", - "x": 160, - "y": 192 - }, - "warped_hyphae": { - "name": "Warped Hyphae", - "x": 192, - "y": 192 - }, - "oak_leaves": { - "name": "Oak Leaves", - "x": 128, - "y": 3424 - }, - "spruce_leaves": { - "name": "Spruce Leaves", - "x": 544, - "y": 3424 - }, - "birch_leaves": { - "name": "Birch Leaves", - "x": 352, - "y": 3392 - }, - "jungle_leaves": { - "name": "Jungle Leaves", - "x": 896, - "y": 3392 - }, - "acacia_leaves": { - "name": "Acacia Leaves", - "x": 160, - "y": 3392 - }, - "dark_oak_leaves": { - "name": "Dark Oak Leaves", - "x": 704, - "y": 3392 - }, - "sponge": { - "name": "Sponge", - "x": 896, - "y": 3584 - }, - "wet_sponge": { - "name": "Wet Sponge", - "x": 0, - "y": 3616 - }, - "glass": { - "name": "Glass", - "x": 960, - "y": 3232 - }, - "lapis_ore": { - "name": "Lapis Lazuli Ore", - "x": 96, - "y": 3392 - }, - "lapis_block": { - "name": "Lapis Lazuli Block", - "x": 256, - "y": 3264 - }, - "dispenser": { - "name": "Dispenser", - "x": 576, - "y": 3584 - }, - "sandstone": { - "name": "Sandstone", - "x": 512, - "y": 3360 - }, - "chiseled_sandstone": { - "name": "Chiseled Sandstone", - "x": 384, - "y": 3232 - }, - "cut_sandstone": { - "name": "Cut Sandstone", - "x": 544, - "y": 3232 - }, - "note_block": { - "name": "Note Block", - "x": 832, - "y": 3584 - }, - "powered_rail": { - "name": "Powered Rail", - "x": 448, - "y": 3328 - }, - "detector_rail": { - "name": "Detector Rail", - "x": 160, - "y": 3328 - }, - "sticky_piston": { - "name": "Sticky Piston", - "x": 704, - "y": 3328 - }, - "cobweb": { - "name": "Cobweb", - "x": 992, - "y": 3328 - }, - "grass": { - "name": "Grass", - "x": 864, - "y": 3392 - }, - "fern": { - "name": "Fern", - "x": 832, - "y": 3392 - }, - "dead_bush": { - "name": "Dead Bush", - "x": 800, - "y": 3392 - }, - "seagrass": { - "name": "Seagrass", - "x": 384, - "y": 2016 - }, - "sea_pickle": { - "name": "Sea Pickle", - "x": 224, - "y": 1728 - }, - "piston": { - "name": "Piston", - "x": 416, - "y": 3328 - }, - "white_wool": { - "name": "White Wool", - "x": 0, - "y": 1536 - }, - "orange_wool": { - "name": "Orange Wool", - "x": 896, - "y": 1504 - }, - "magenta_wool": { - "name": "Magenta Wool", - "x": 864, - "y": 1504 - }, - "light_blue_wool": { - "name": "Light Blue Wool", - "x": 768, - "y": 1504 - }, - "yellow_wool": { - "name": "Yellow Wool", - "x": 32, - "y": 1536 - }, - "lime_wool": { - "name": "Lime Wool", - "x": 832, - "y": 1504 - }, - "pink_wool": { - "name": "Pink Wool", - "x": 928, - "y": 1504 - }, - "gray_wool": { - "name": "Gray Wool", - "x": 704, - "y": 1504 - }, - "light_gray_wool": { - "name": "Light Gray Wool", - "x": 800, - "y": 1504 - }, - "cyan_wool": { - "name": "Cyan Wool", - "x": 672, - "y": 1504 - }, - "purple_wool": { - "name": "Purple Wool", - "x": 960, - "y": 1504 - }, - "blue_wool": { - "name": "Blue Wool", - "x": 608, - "y": 1504 - }, - "brown_wool": { - "name": "Brown Wool", - "x": 640, - "y": 1504 - }, - "green_wool": { - "name": "Green Wool", - "x": 736, - "y": 1504 - }, - "red_wool": { - "name": "Red Wool", - "x": 992, - "y": 1504 - }, - "black_wool": { - "name": "Black Wool", - "x": 576, - "y": 1504 - }, - "dandelion": { - "name": "Dandelion", - "x": 672, - "y": 3392 - }, - "poppy": { - "name": "Poppy", - "x": 352, - "y": 3424 - }, - "blue_orchid": { - "name": "Blue Orchid", - "x": 448, - "y": 3392 - }, - "allium": { - "name": "Allium", - "x": 256, - "y": 3392 - }, - "azure_bluet": { - "name": "Azure Bluet", - "x": 288, - "y": 3392 - }, - "red_tulip": { - "name": "Red Tulip", - "x": 480, - "y": 3424 - }, - "orange_tulip": { - "name": "Orange Tulip", - "x": 224, - "y": 3424 - }, - "white_tulip": { - "name": "White Tulip", - "x": 768, - "y": 3424 - }, - "pink_tulip": { - "name": "Pink Tulip", - "x": 320, - "y": 3424 - }, - "oxeye_daisy": { - "name": "Oxeye Daisy", - "x": 256, - "y": 3424 - }, - "cornflower": { - "name": "Cornflower", - "x": 640, - "y": 2368 - }, - "lily_of_the_valley": { - "name": "Lily of the Valley", - "x": 672, - "y": 2368 - }, - "wither_rose": { - "name": "Wither Rose", - "x": 608, - "y": 2368 - }, - "brown_mushroom": { - "name": "Brown Mushroom", - "x": 512, - "y": 3392 - }, - "red_mushroom": { - "name": "Red Mushroom", - "x": 448, - "y": 3424 - }, - "crimson_fungus": { - "name": "Crimson Fungus", - "x": 928, - "y": 128 - }, - "warped_fungus": { - "name": "Warped Fungus", - "x": 960, - "y": 128 - }, - "crimson_roots": { - "name": "Crimson Roots", - "x": 928, - "y": 160 - }, - "warped_roots": { - "name": "Warped Roots", - "x": 992, - "y": 160 - }, - "nether_sprouts": { - "name": "Nether Sprouts", - "x": 928, - "y": 64 - }, - "weeping_vines": { - "name": "Weeping Vines", - "x": 992, - "y": 128 - }, - "twisting_vines": { - "name": "Twisting Vines", - "x": 320, - "y": 192 - }, - "sugar_cane": { - "name": "Sugar Cane", - "x": 640, - "y": 3424 - }, - "kelp": { - "name": "Kelp", - "x": 576, - "y": 1664 - }, - "bamboo": { - "name": "Bamboo", - "x": 128, - "y": 2368 - }, - "gold_block": { - "name": "Block of Gold", - "x": 128, - "y": 3232 - }, - "iron_block": { - "name": "Block of Iron", - "x": 160, - "y": 3232 - }, - "oak_slab": { - "name": "Oak Slab", - "x": 640, - "y": 3264 - }, - "spruce_slab": { - "name": "Spruce Slab", - "x": 672, - "y": 3296 - }, - "birch_slab": { - "name": "Birch Slab", - "x": 960, - "y": 3200 - }, - "jungle_slab": { - "name": "Jungle Slab", - "x": 128, - "y": 3264 - }, - "acacia_slab": { - "name": "Acacia Slab", - "x": 704, - "y": 3200 - }, - "dark_oak_slab": { - "name": "Dark Oak Slab", - "x": 640, - "y": 3232 - }, - "crimson_slab": { - "name": "Crimson Slab", - "x": 32, - "y": 160 - }, - "warped_slab": { - "name": "Warped Slab", - "x": 128, - "y": 160 - }, - "stone_slab": { - "name": "Stone Slab", - "x": 864, - "y": 3296 - }, - "smooth_stone_slab": { - "name": "Smooth Stone Slab", - "x": 544, - "y": 3296 - }, - "sandstone_slab": { - "name": "Sandstone Slab", - "x": 192, - "y": 3296 - }, - "cut_sandstone_slab": { - "name": "Cut Sandstone Slab", - "x": 64, - "y": 2464 - }, - "petrified_oak_slab": { - "name": "Petrified Oak Slab", - "x": 224, - "y": 896 - }, - "cobblestone_slab": { - "name": "Cobblestone Slab", - "x": 416, - "y": 3232 - }, - "brick_slab": { - "name": "Brick Slab", - "x": 256, - "y": 3232 - }, - "stone_brick_slab": { - "name": "Stone Brick Slab", - "x": 128, - "y": 928 - }, - "nether_brick_slab": { - "name": "Nether Brick Slab", - "x": 480, - "y": 3264 - }, - "quartz_slab": { - "name": "Quartz Slab", - "x": 960, - "y": 3264 - }, - "red_sandstone_slab": { - "name": "Red Sandstone Slab", - "x": 96, - "y": 3296 - }, - "cut_red_sandstone_slab": { - "name": "Cut Red Sandstone Slab", - "x": 32, - "y": 2464 - }, - "purpur_slab": { - "name": "Purpur Slab", - "x": 896, - "y": 3264 - }, - "prismarine_slab": { - "name": "Dark Prismarine Slab", - "x": 736, - "y": 3232 - }, - "prismarine_brick_slab": { - "name": "Prismarine Brick Slab", - "x": 864, - "y": 3264 - }, - "dark_prismarine_slab": { - "name": "Dark Prismarine Slab", - "x": 736, - "y": 3232 - }, - "smooth_quartz": { - "name": "Smooth Quartz Block", - "x": 64, - "y": 864 - }, - "smooth_red_sandstone": { - "name": "Smooth Red Sandstone", - "x": 416, - "y": 3296 - }, - "smooth_sandstone": { - "name": "Smooth Sandstone", - "x": 512, - "y": 3296 - }, - "smooth_stone": { - "name": "Smooth Stone", - "x": 832, - "y": 896 - }, - "bricks": { - "name": "Bricks", - "x": 416, - "y": 96 - }, - "tnt": { - "name": "TNT", - "x": 960, - "y": 3584 - }, - "bookshelf": { - "name": "Bookshelf", - "x": 224, - "y": 3232 - }, - "mossy_cobblestone": { - "name": "Mossy Cobblestone", - "x": 192, - "y": 3360 - }, - "obsidian": { - "name": "Obsidian", - "x": 288, - "y": 3360 - }, - "torch": { - "name": "Torch", - "x": 928, - "y": 3360 - }, - "end_rod": { - "name": "End Rod", - "x": 672, - "y": 3168 - }, - "chorus_plant": { - "name": "Chorus Plant", - "x": 640, - "y": 3392 - }, - "chorus_flower": { - "name": "Chorus Flower", - "x": 608, - "y": 3392 - }, - "purpur_block": { - "name": "Purpur Block", - "x": 416, - "y": 896 - }, - "purpur_pillar": { - "name": "Purpur Pillar", - "x": 448, - "y": 896 - }, - "purpur_stairs": { - "name": "Purpur Stairs", - "x": 928, - "y": 3264 - }, - "spawner": { - "name": "Spawner", - "x": 576, - "y": 3360 - }, - "oak_stairs": { - "name": "Oak Stairs", - "x": 672, - "y": 3264 - }, - "chest": { - "name": "Chest", - "x": 704, - "y": 1216 - }, - "diamond_ore": { - "name": "Diamond Ore", - "x": 992, - "y": 3360 - }, - "diamond_block": { - "name": "Block of Diamond", - "x": 64, - "y": 3232 - }, - "crafting_table": { - "name": "Crafting Table", - "x": 512, - "y": 3584 - }, - "farmland": { - "name": "Farmland", - "x": 672, - "y": 864 - }, - "furnace": { - "name": "Furnace", - "x": 704, - "y": 3584 - }, - "ladder": { - "name": "Ladder", - "x": 224, - "y": 3264 - }, - "rail": { - "name": "Rail", - "x": 480, - "y": 3328 - }, - "cobblestone_stairs": { - "name": "Cobblestone Stairs", - "x": 448, - "y": 3232 - }, - "lever": { - "name": "Lever", - "x": 288, - "y": 3328 - }, - "stone_pressure_plate": { - "name": "Stone Pressure Plate", - "x": 768, - "y": 3328 - }, - "oak_pressure_plate": { - "name": "Oak Pressure Plate", - "x": 384, - "y": 3328 - }, - "spruce_pressure_plate": { - "name": "Spruce Pressure Plate", - "x": 672, - "y": 3328 - }, - "birch_pressure_plate": { - "name": "Birch Pressure Plate", - "x": 32, - "y": 3328 - }, - "jungle_pressure_plate": { - "name": "Jungle Pressure Plate", - "x": 256, - "y": 3328 - }, - "acacia_pressure_plate": { - "name": "Acacia Pressure Plate", - "x": 960, - "y": 3296 - }, - "dark_oak_pressure_plate": { - "name": "Dark Oak Pressure Plate", - "x": 128, - "y": 3328 - }, - "crimson_pressure_plate": { - "name": "Crimson Pressure Plate", - "x": 256, - "y": 160 - }, - "warped_pressure_plate": { - "name": "Warped Pressure Plate", - "x": 384, - "y": 160 - }, - "polished_blackstone_pressure_plate": { - "name": "Polished Blackstone Pressure Plate", - "x": 928, - "y": 256 - }, - "redstone_ore": { - "name": "Redstone Ore", - "x": 128, - "y": 3392 - }, - "redstone_torch": { - "name": "Redstone Torch", - "x": 608, - "y": 3328 - }, - "snow": { - "name": "Snow", - "x": 544, - "y": 3360 - }, - "ice": { - "name": "Ice", - "x": 160, - "y": 3360 - }, - "snow_block": { - "name": "Snow Block", - "x": 576, - "y": 3296 - }, - "cactus": { - "name": "Cactus", - "x": 544, - "y": 3392 - }, - "clay": { - "name": "Clay", - "x": 928, - "y": 3328 - }, - "jukebox": { - "name": "Jukebox", - "x": 768, - "y": 3584 - }, - "oak_fence": { - "name": "Oak Fence", - "x": 608, - "y": 3264 - }, - "spruce_fence": { - "name": "Spruce Fence", - "x": 640, - "y": 3296 - }, - "birch_fence": { - "name": "Birch Fence", - "x": 928, - "y": 3200 - }, - "jungle_fence": { - "name": "Jungle Fence", - "x": 96, - "y": 3264 - }, - "acacia_fence": { - "name": "Acacia Fence", - "x": 672, - "y": 3200 - }, - "dark_oak_fence": { - "name": "Dark Oak Fence", - "x": 608, - "y": 3232 - }, - "crimson_fence": { - "name": "Crimson Fence", - "x": 0, - "y": 160 - }, - "warped_fence": { - "name": "Warped Fence", - "x": 96, - "y": 160 - }, - "pumpkin": { - "name": "Pumpkin", - "x": 384, - "y": 3424 - }, - "carved_pumpkin": { - "name": "Carved Pumpkin", - "x": 576, - "y": 3392 - }, - "netherrack": { - "name": "Netherrack", - "x": 832, - "y": 3360 - }, - "soul_sand": { - "name": "Soul Sand", - "x": 864, - "y": 3360 - }, - "soul_soil": { - "name": "Soul Soil", - "x": 736, - "y": 128 - }, - "basalt": { - "name": "Basalt", - "x": 608, - "y": 128 - }, - "polished_basalt": { - "name": "Polished Basalt", - "x": 416, - "y": 192 - }, - "soul_torch": { - "name": "Soul Torch", - "x": 864, - "y": 128 - }, - "glowstone": { - "name": "Glowstone", - "x": 672, - "y": 3360 - }, - "jack_o_lantern": { - "name": "Jack o'Lantern", - "x": 32, - "y": 3264 - }, - "oak_trapdoor": { - "name": "Oak Trapdoor", - "x": 928, - "y": 928 - }, - "spruce_trapdoor": { - "name": "Spruce Trapdoor", - "x": 448, - "y": 1632 - }, - "birch_trapdoor": { - "name": "Birch Trapdoor", - "x": 480, - "y": 1632 - }, - "jungle_trapdoor": { - "name": "Jungle Trapdoor", - "x": 512, - "y": 1632 - }, - "acacia_trapdoor": { - "name": "Acacia Trapdoor", - "x": 544, - "y": 1632 - }, - "dark_oak_trapdoor": { - "name": "Dark Oak Trapdoor", - "x": 576, - "y": 1632 - }, - "crimson_trapdoor": { - "name": "Crimson Trapdoor", - "x": 288, - "y": 160 - }, - "warped_trapdoor": { - "name": "Warped Trapdoor", - "x": 416, - "y": 160 - }, - "infested_stone": { - "name": "Infested Stone", - "x": 224, - "y": 992 - }, - "infested_cobblestone": { - "name": "Infested Cobblestone", - "x": 960, - "y": 3328 - }, - "infested_stone_bricks": { - "name": "Infested Stone Bricks", - "x": 608, - "y": 3360 - }, - "infested_mossy_stone_bricks": { - "name": "Infested Mossy Stone Bricks", - "x": 224, - "y": 3360 - }, - "infested_cracked_stone_bricks": { - "name": "Infested Cracked Stone Bricks", - "x": 0, - "y": 3360 - }, - "infested_chiseled_stone_bricks": { - "name": "Infested Chiseled Stone Bricks", - "x": 896, - "y": 3328 - }, - "stone_bricks": { - "name": "Stone Bricks", - "x": 608, - "y": 3360 - }, - "mossy_stone_bricks": { - "name": "Mossy Stone Bricks", - "x": 224, - "y": 3360 - }, - "cracked_stone_bricks": { - "name": "Cracked Stone Bricks", - "x": 0, - "y": 3360 - }, - "chiseled_stone_bricks": { - "name": "Chiseled Stone Bricks", - "x": 896, - "y": 3328 - }, - "brown_mushroom_block": { - "name": "Brown Mushroom Block", - "x": 480, - "y": 3392 - }, - "red_mushroom_block": { - "name": "Red Mushroom Block", - "x": 416, - "y": 3424 - }, - "mushroom_stem": { - "name": "Mushroom Stem", - "x": 96, - "y": 3424 - }, - "iron_bars": { - "name": "Iron Bars", - "x": 0, - "y": 3264 - }, - "chain": { - "name": "Chain", - "x": 32, - "y": 288 - }, - "glass_pane": { - "name": "Glass Pane", - "x": 928, - "y": 3232 - }, - "melon": { - "name": "Melon", - "x": 64, - "y": 3424 - }, - "vine": { - "name": "Vines", - "x": 736, - "y": 3424 - }, - "oak_fence_gate": { - "name": "Oak Fence Gate", - "x": 576, - "y": 3264 - }, - "spruce_fence_gate": { - "name": "Spruce Fence Gate", - "x": 608, - "y": 3296 - }, - "birch_fence_gate": { - "name": "Birch Fence Gate", - "x": 896, - "y": 3200 - }, - "jungle_fence_gate": { - "name": "Jungle Fence Gate", - "x": 64, - "y": 3264 - }, - "acacia_fence_gate": { - "name": "Acacia Fence Gate", - "x": 640, - "y": 3200 - }, - "dark_oak_fence_gate": { - "name": "Dark Oak Fence Gate", - "x": 576, - "y": 3232 - }, - "crimson_fence_gate": { - "name": "Crimson Fence Gate", - "x": 576, - "y": 160 - }, - "warped_fence_gate": { - "name": "Warped Fence Gate", - "x": 608, - "y": 160 - }, - "brick_stairs": { - "name": "Brick Stairs", - "x": 288, - "y": 3232 - }, - "stone_brick_stairs": { - "name": "Stone Brick Stairs", - "x": 800, - "y": 3296 - }, - "mycelium": { - "name": "Mycelium", - "x": 256, - "y": 3360 - }, - "lily_pad": { - "name": "Lily Pad", - "x": 32, - "y": 3424 - }, - "nether_bricks": { - "name": "Nether Bricks", - "x": 768, - "y": 3360 - }, - "cracked_nether_bricks": { - "name": "Cracked Nether Bricks", - "x": 448, - "y": 256 - }, - "chiseled_nether_bricks": { - "name": "Chiseled Nether Bricks", - "x": 384, - "y": 256 - }, - "nether_brick_fence": { - "name": "Nether Brick Fence", - "x": 704, - "y": 3360 - }, - "nether_brick_stairs": { - "name": "Nether Brick Stairs", - "x": 736, - "y": 3360 - }, - "enchanting_table": { - "name": "Enchanting Table", - "x": 640, - "y": 3584 - }, - "end_portal_frame": { - "name": "End Portal Frame", - "x": 640, - "y": 3168 - }, - "end_stone": { - "name": "End Stone", - "x": 704, - "y": 3168 - }, - "end_stone_bricks": { - "name": "End Stone Bricks", - "x": 896, - "y": 3232 - }, - "dragon_egg": { - "name": "Dragon Egg", - "x": 576, - "y": 3168 - }, - "redstone_lamp": { - "name": "Redstone Lamp", - "x": 544, - "y": 3328 - }, - "sandstone_stairs": { - "name": "Sandstone Stairs", - "x": 224, - "y": 3296 - }, - "emerald_ore": { - "name": "Emerald Ore", - "x": 0, - "y": 3392 - }, - "ender_chest": { - "name": "Ender Chest", - "x": 672, - "y": 3584 - }, - "tripwire_hook": { - "name": "Tripwire Hook", - "x": 800, - "y": 3328 - }, - "emerald_block": { - "name": "Block of Emerald", - "x": 96, - "y": 3232 - }, - "spruce_stairs": { - "name": "Spruce Stairs", - "x": 704, - "y": 3296 - }, - "birch_stairs": { - "name": "Birch Stairs", - "x": 992, - "y": 3200 - }, - "jungle_stairs": { - "name": "Jungle Stairs", - "x": 160, - "y": 3264 - }, - "crimson_stairs": { - "name": "Crimson Stairs", - "x": 64, - "y": 160 - }, - "warped_stairs": { - "name": "Warped Stairs", - "x": 160, - "y": 160 - }, - "command_block": { - "name": "Impulse Command Block Revision 1", - "x": 736, - "y": 1216 - }, - "beacon": { - "name": "Beacon", - "x": 352, - "y": 3584 - }, - "cobblestone_wall": { - "name": "Cobblestone Wall", - "x": 480, - "y": 3232 - }, - "mossy_cobblestone_wall": { - "name": "Mossy Cobblestone Wall", - "x": 352, - "y": 3264 - }, - "brick_wall": { - "name": "Brick Wall", - "x": 320, - "y": 3232 - }, - "prismarine_wall": { - "name": "Prismarine Wall", - "x": 0, - "y": 96 - }, - "red_sandstone_wall": { - "name": "Red Sandstone Wall", - "x": 160, - "y": 3296 - }, - "mossy_stone_brick_wall": { - "name": "Mossy Stone Brick Wall", - "x": 448, - "y": 3264 - }, - "granite_wall": { - "name": "Granite Wall", - "x": 32, - "y": 2400 - }, - "stone_brick_wall": { - "name": "Stone Brick Wall", - "x": 832, - "y": 3296 - }, - "nether_brick_wall": { - "name": "Nether Brick Wall", - "x": 512, - "y": 3264 - }, - "andesite_wall": { - "name": "Andesite Wall", - "x": 864, - "y": 3200 - }, - "red_nether_brick_wall": { - "name": "Red Nether Brick Wall", - "x": 64, - "y": 3296 - }, - "sandstone_wall": { - "name": "Sandstone Wall", - "x": 256, - "y": 3296 - }, - "end_stone_brick_wall": { - "name": "End Stone Brick Wall", - "x": 0, - "y": 2400 - }, - "diorite_wall": { - "name": "Diorite Wall", - "x": 864, - "y": 3232 - }, - "blackstone_wall": { - "name": "Blackstone Wall", - "x": 672, - "y": 256 - }, - "polished_blackstone_wall": { - "name": "Polished Blackstone Wall", - "x": 864, - "y": 256 - }, - "polished_blackstone_brick_wall": { - "name": "Polished Blackstone Brick Wall", - "x": 768, - "y": 256 - }, - "stone_button": { - "name": "Stone Button", - "x": 736, - "y": 3328 - }, - "oak_button": { - "name": "Oak Button", - "x": 352, - "y": 3328 - }, - "spruce_button": { - "name": "Spruce Button", - "x": 640, - "y": 3328 - }, - "birch_button": { - "name": "Birch Button", - "x": 0, - "y": 3328 - }, - "jungle_button": { - "name": "Jungle Button", - "x": 224, - "y": 3328 - }, - "acacia_button": { - "name": "Acacia Button", - "x": 928, - "y": 3296 - }, - "dark_oak_button": { - "name": "Dark Oak Button", - "x": 96, - "y": 3328 - }, - "crimson_button": { - "name": "Crimson Button", - "x": 192, - "y": 160 - }, - "warped_button": { - "name": "Warped Button", - "x": 320, - "y": 160 - }, - "polished_blackstone_button": { - "name": "Polished Blackstone Button", - "x": 896, - "y": 256 - }, - "anvil": { - "name": "Anvil", - "x": 288, - "y": 3584 - }, - "chipped_anvil": { - "name": "Chipped Anvil", - "x": 480, - "y": 3584 - }, - "damaged_anvil": { - "name": "Damaged Anvil", - "x": 544, - "y": 3584 - }, - "trapped_chest": { - "name": "Trapped Chest", - "x": 992, - "y": 3584 - }, - "light_weighted_pressure_plate": { - "name": "Light Weighted Pressure Plate", - "x": 320, - "y": 3328 - }, - "heavy_weighted_pressure_plate": { - "name": "Heavy Weighted Pressure Plate", - "x": 192, - "y": 3328 - }, - "daylight_detector": { - "name": "Daylight Detector", - "x": 800, - "y": 1216 - }, - "redstone_block": { - "name": "Block of Redstone", - "x": 64, - "y": 3328 - }, - "nether_quartz_ore": { - "name": "Nether Quartz Ore", - "x": 800, - "y": 3360 - }, - "hopper": { - "name": "Hopper", - "x": 736, - "y": 3584 - }, - "chiseled_quartz_block": { - "name": "Chiseled Quartz Block", - "x": 224, - "y": 864 - }, - "quartz_block": { - "name": "Block of Quartz", - "x": 192, - "y": 3232 - }, - "quartz_bricks": { - "name": "Quartz Bricks", - "x": 992, - "y": 256 - }, - "quartz_pillar": { - "name": "Quartz Pillar", - "x": 288, - "y": 896 - }, - "quartz_stairs": { - "name": "Quartz Stairs", - "x": 992, - "y": 3264 - }, - "activator_rail": { - "name": "Activator Rail", - "x": 992, - "y": 3296 - }, - "dropper": { - "name": "Dropper", - "x": 608, - "y": 3584 - }, - "white_terracotta": { - "name": "White Terracotta", - "x": 672, - "y": 768 - }, - "orange_terracotta": { - "name": "Orange Terracotta", - "x": 928, - "y": 736 - }, - "magenta_terracotta": { - "name": "Magenta Terracotta", - "x": 736, - "y": 736 - }, - "light_blue_terracotta": { - "name": "Light Blue Terracotta", - "x": 160, - "y": 736 - }, - "yellow_terracotta": { - "name": "Yellow Terracotta", - "x": 864, - "y": 768 - }, - "lime_terracotta": { - "name": "Lime Terracotta", - "x": 544, - "y": 736 - }, - "pink_terracotta": { - "name": "Pink Terracotta", - "x": 96, - "y": 768 - }, - "gray_terracotta": { - "name": "Gray Terracotta", - "x": 800, - "y": 704 - }, - "light_gray_terracotta": { - "name": "Light Gray Terracotta", - "x": 352, - "y": 736 - }, - "cyan_terracotta": { - "name": "Cyan Terracotta", - "x": 608, - "y": 704 - }, - "purple_terracotta": { - "name": "Purple Terracotta", - "x": 288, - "y": 768 - }, - "blue_terracotta": { - "name": "Blue Terracotta", - "x": 224, - "y": 704 - }, - "brown_terracotta": { - "name": "Brown Terracotta", - "x": 416, - "y": 704 - }, - "green_terracotta": { - "name": "Green Terracotta", - "x": 992, - "y": 704 - }, - "red_terracotta": { - "name": "Red Terracotta", - "x": 480, - "y": 768 - }, - "black_terracotta": { - "name": "Black Terracotta", - "x": 32, - "y": 704 - }, - "barrier": { - "name": "Barrier", - "x": 320, - "y": 3584 - }, - "iron_trapdoor": { - "name": "Iron Trapdoor", - "x": 416, - "y": 928 - }, - "hay_block": { - "name": "Hay Bale", - "x": 992, - "y": 3232 - }, - "white_carpet": { - "name": "White Carpet", - "x": 64, - "y": 1632 - }, - "orange_carpet": { - "name": "Orange Carpet", - "x": 960, - "y": 1600 - }, - "magenta_carpet": { - "name": "Magenta Carpet", - "x": 928, - "y": 1600 - }, - "light_blue_carpet": { - "name": "Light Blue Carpet", - "x": 832, - "y": 1600 - }, - "yellow_carpet": { - "name": "Yellow Carpet", - "x": 96, - "y": 1632 - }, - "lime_carpet": { - "name": "Lime Carpet", - "x": 896, - "y": 1600 - }, - "pink_carpet": { - "name": "Pink Carpet", - "x": 992, - "y": 1600 - }, - "gray_carpet": { - "name": "Gray Carpet", - "x": 768, - "y": 1600 - }, - "light_gray_carpet": { - "name": "Light Gray Carpet", - "x": 864, - "y": 1600 - }, - "cyan_carpet": { - "name": "Cyan Carpet", - "x": 736, - "y": 1600 - }, - "purple_carpet": { - "name": "Purple Carpet", - "x": 0, - "y": 1632 - }, - "blue_carpet": { - "name": "Blue Carpet", - "x": 672, - "y": 1600 - }, - "brown_carpet": { - "name": "Brown Carpet", - "x": 704, - "y": 1600 - }, - "green_carpet": { - "name": "Green Carpet", - "x": 800, - "y": 1600 - }, - "red_carpet": { - "name": "Red Carpet", - "x": 32, - "y": 1632 - }, - "black_carpet": { - "name": "Black Carpet", - "x": 640, - "y": 1600 - }, - "terracotta": { - "name": "Terracotta", - "x": 640, - "y": 3360 - }, - "coal_block": { - "name": "Block of Coal", - "x": 32, - "y": 3232 - }, - "packed_ice": { - "name": "Packed Ice", - "x": 320, - "y": 3360 - }, - "acacia_stairs": { - "name": "Acacia Stairs", - "x": 736, - "y": 3200 - }, - "dark_oak_stairs": { - "name": "Dark Oak Stairs", - "x": 672, - "y": 3232 - }, - "slime_block": { - "name": "Slime Block", - "x": 736, - "y": 896 - }, - "grass_path": { - "name": "Grass Path", - "x": 96, - "y": 1344 - }, - "sunflower": { - "name": "Sunflower", - "x": 672, - "y": 3424 - }, - "lilac": { - "name": "Lilac", - "x": 0, - "y": 3424 - }, - "rose_bush": { - "name": "Rose Bush", - "x": 512, - "y": 3424 - }, - "peony": { - "name": "Peony", - "x": 288, - "y": 3424 - }, - "tall_grass": { - "name": "Tall Grass", - "x": 704, - "y": 3424 - }, - "large_fern": { - "name": "Large Fern", - "x": 992, - "y": 3392 - }, - "white_stained_glass": { - "name": "White Stained Glass", - "x": 704, - "y": 768 - }, - "orange_stained_glass": { - "name": "Orange Stained Glass", - "x": 960, - "y": 736 - }, - "magenta_stained_glass": { - "name": "Magenta Stained Glass", - "x": 768, - "y": 736 - }, - "light_blue_stained_glass": { - "name": "Light Blue Stained Glass", - "x": 192, - "y": 736 - }, - "yellow_stained_glass": { - "name": "Yellow Stained Glass", - "x": 896, - "y": 768 - }, - "lime_stained_glass": { - "name": "Lime Stained Glass", - "x": 576, - "y": 736 - }, - "pink_stained_glass": { - "name": "Pink Stained Glass", - "x": 128, - "y": 768 - }, - "gray_stained_glass": { - "name": "Gray Stained Glass", - "x": 832, - "y": 704 - }, - "light_gray_stained_glass": { - "name": "Light Gray Stained Glass", - "x": 384, - "y": 736 - }, - "cyan_stained_glass": { - "name": "Cyan Stained Glass", - "x": 640, - "y": 704 - }, - "purple_stained_glass": { - "name": "Purple Stained Glass", - "x": 320, - "y": 768 - }, - "blue_stained_glass": { - "name": "Blue Stained Glass", - "x": 256, - "y": 704 - }, - "brown_stained_glass": { - "name": "Brown Stained Glass", - "x": 448, - "y": 704 - }, - "green_stained_glass": { - "name": "Green Stained Glass", - "x": 0, - "y": 736 - }, - "red_stained_glass": { - "name": "Red Stained Glass", - "x": 512, - "y": 768 - }, - "black_stained_glass": { - "name": "Black Stained Glass", - "x": 64, - "y": 704 - }, - "white_stained_glass_pane": { - "name": "White Stained Glass Pane", - "x": 736, - "y": 768 - }, - "orange_stained_glass_pane": { - "name": "Orange Stained Glass Pane", - "x": 992, - "y": 736 - }, - "magenta_stained_glass_pane": { - "name": "Magenta Stained Glass Pane", - "x": 800, - "y": 736 - }, - "light_blue_stained_glass_pane": { - "name": "Light Blue Stained Glass Pane", - "x": 224, - "y": 736 - }, - "yellow_stained_glass_pane": { - "name": "Yellow Stained Glass Pane", - "x": 928, - "y": 768 - }, - "lime_stained_glass_pane": { - "name": "Lime Stained Glass Pane", - "x": 608, - "y": 736 - }, - "pink_stained_glass_pane": { - "name": "Pink Stained Glass Pane", - "x": 160, - "y": 768 - }, - "gray_stained_glass_pane": { - "name": "Gray Stained Glass Pane", - "x": 864, - "y": 704 - }, - "light_gray_stained_glass_pane": { - "name": "Light Gray Stained Glass Pane", - "x": 416, - "y": 736 - }, - "cyan_stained_glass_pane": { - "name": "Cyan Stained Glass Pane", - "x": 672, - "y": 704 - }, - "purple_stained_glass_pane": { - "name": "Purple Stained Glass Pane", - "x": 352, - "y": 768 - }, - "blue_stained_glass_pane": { - "name": "Blue Stained Glass Pane", - "x": 288, - "y": 704 - }, - "brown_stained_glass_pane": { - "name": "Brown Stained Glass Pane", - "x": 480, - "y": 704 - }, - "green_stained_glass_pane": { - "name": "Green Stained Glass Pane", - "x": 32, - "y": 736 - }, - "red_stained_glass_pane": { - "name": "Red Stained Glass Pane", - "x": 544, - "y": 768 - }, - "black_stained_glass_pane": { - "name": "Black Stained Glass Pane", - "x": 96, - "y": 704 - }, - "prismarine": { - "name": "Prismarine Wall", - "x": 0, - "y": 96 - }, - "prismarine_bricks": { - "name": "Prismarine Bricks", - "x": 384, - "y": 3360 - }, - "dark_prismarine": { - "name": "Dark Prismarine", - "x": 32, - "y": 3360 - }, - "prismarine_stairs": { - "name": "Dark Prismarine Stairs", - "x": 768, - "y": 3232 - }, - "prismarine_brick_stairs": { - "name": "Prismarine Brick Stairs", - "x": 448, - "y": 96 - }, - "dark_prismarine_stairs": { - "name": "Dark Prismarine Stairs", - "x": 768, - "y": 3232 - }, - "sea_lantern": { - "name": "Sea Lantern BE", - "x": 512, - "y": 192 - }, - "red_sandstone": { - "name": "Red Sandstone", - "x": 448, - "y": 3360 - }, - "chiseled_red_sandstone": { - "name": "Chiseled Red Sandstone", - "x": 352, - "y": 3232 - }, - "cut_red_sandstone": { - "name": "Cut Red Sandstone", - "x": 512, - "y": 3232 - }, - "red_sandstone_stairs": { - "name": "Red Sandstone Stairs", - "x": 128, - "y": 3296 - }, - "magma_block": { - "name": "Magma Block BE", - "x": 928, - "y": 480 - }, - "nether_wart_block": { - "name": "Nether Wart Block", - "x": 544, - "y": 3264 - }, - "warped_wart_block": { - "name": "Warped Wart Block", - "x": 832, - "y": 128 - }, - "red_nether_bricks": { - "name": "Red Nether Bricks", - "x": 704, - "y": 1344 - }, - "bone_block": { - "name": "Bone Block", - "x": 864, - "y": 3328 - }, - "structure_void": { - "name": "Structure Void", - "x": 928, - "y": 3584 - }, - "observer": { - "name": "Observer", - "x": 864, - "y": 3584 - }, - "shulker_box": { - "name": "Shulker Box", - "x": 448, - "y": 1600 - }, - "white_shulker_box": { - "name": "White Shulker Box", - "x": 512, - "y": 1600 - }, - "orange_shulker_box": { - "name": "Orange Shulker Box", - "x": 384, - "y": 1600 - }, - "magenta_shulker_box": { - "name": "Magenta Shulker Box", - "x": 352, - "y": 1600 - }, - "light_blue_shulker_box": { - "name": "Light Blue Shulker Box", - "x": 256, - "y": 1600 - }, - "yellow_shulker_box": { - "name": "Yellow Shulker Box", - "x": 544, - "y": 1600 - }, - "lime_shulker_box": { - "name": "Lime Shulker Box", - "x": 320, - "y": 1600 - }, - "pink_shulker_box": { - "name": "Pink Shulker Box", - "x": 416, - "y": 1600 - }, - "gray_shulker_box": { - "name": "Gray Shulker Box", - "x": 192, - "y": 1600 - }, - "light_gray_shulker_box": { - "name": "Light Gray Shulker Box", - "x": 288, - "y": 1600 - }, - "cyan_shulker_box": { - "name": "Cyan Shulker Box", - "x": 160, - "y": 1600 - }, - "purple_shulker_box": { - "name": "Purple Shulker Box", - "x": 608, - "y": 1408 - }, - "blue_shulker_box": { - "name": "Blue Shulker Box", - "x": 96, - "y": 1600 - }, - "brown_shulker_box": { - "name": "Brown Shulker Box", - "x": 128, - "y": 1600 - }, - "green_shulker_box": { - "name": "Green Shulker Box", - "x": 224, - "y": 1600 - }, - "red_shulker_box": { - "name": "Red Shulker Box", - "x": 480, - "y": 1600 - }, - "black_shulker_box": { - "name": "Black Shulker Box", - "x": 64, - "y": 1600 - }, - "white_glazed_terracotta": { - "name": "White Glazed Terracotta", - "x": 192, - "y": 1536 - }, - "orange_glazed_terracotta": { - "name": "Orange Glazed Terracotta", - "x": 160, - "y": 1536 - }, - "magenta_glazed_terracotta": { - "name": "Magenta Glazed Terracotta", - "x": 128, - "y": 1536 - }, - "light_blue_glazed_terracotta": { - "name": "Light Blue Glazed Terracotta", - "x": 704, - "y": 1472 - }, - "yellow_glazed_terracotta": { - "name": "Yellow Glazed Terracotta", - "x": 544, - "y": 1504 - }, - "lime_glazed_terracotta": { - "name": "Lime Glazed Terracotta", - "x": 896, - "y": 1472 - }, - "pink_glazed_terracotta": { - "name": "Pink Glazed Terracotta", - "x": 160, - "y": 1504 - }, - "gray_glazed_terracotta": { - "name": "Gray Glazed Terracotta", - "x": 512, - "y": 1472 - }, - "light_gray_glazed_terracotta": { - "name": "Light Gray Glazed Terracotta", - "x": 800, - "y": 1472 - }, - "cyan_glazed_terracotta": { - "name": "Cyan Glazed Terracotta", - "x": 96, - "y": 1536 - }, - "purple_glazed_terracotta": { - "name": "Purple Glazed Terracotta", - "x": 256, - "y": 1504 - }, - "blue_glazed_terracotta": { - "name": "Blue Glazed Terracotta", - "x": 224, - "y": 1472 - }, - "brown_glazed_terracotta": { - "name": "Brown Glazed Terracotta", - "x": 320, - "y": 1472 - }, - "green_glazed_terracotta": { - "name": "Green Glazed Terracotta", - "x": 608, - "y": 1472 - }, - "red_glazed_terracotta": { - "name": "Red Glazed Terracotta", - "x": 352, - "y": 1504 - }, - "black_glazed_terracotta": { - "name": "Black Glazed Terracotta", - "x": 128, - "y": 1472 - }, - "white_concrete": { - "name": "White Concrete", - "x": 384, - "y": 1504 - }, - "orange_concrete": { - "name": "Orange Concrete", - "x": 0, - "y": 1504 - }, - "magenta_concrete": { - "name": "Magenta Concrete", - "x": 928, - "y": 1472 - }, - "light_blue_concrete": { - "name": "Light Blue Concrete", - "x": 640, - "y": 1472 - }, - "yellow_concrete": { - "name": "Yellow Concrete", - "x": 480, - "y": 1504 - }, - "lime_concrete": { - "name": "Lime Concrete", - "x": 832, - "y": 1472 - }, - "pink_concrete": { - "name": "Pink Concrete", - "x": 96, - "y": 1504 - }, - "gray_concrete": { - "name": "Gray Concrete", - "x": 448, - "y": 1472 - }, - "light_gray_concrete": { - "name": "Light Gray Concrete", - "x": 736, - "y": 1472 - }, - "cyan_concrete": { - "name": "Cyan Concrete", - "x": 352, - "y": 1472 - }, - "purple_concrete": { - "name": "Purple Concrete", - "x": 192, - "y": 1504 - }, - "blue_concrete": { - "name": "Blue Concrete", - "x": 160, - "y": 1472 - }, - "brown_concrete": { - "name": "Brown Concrete", - "x": 256, - "y": 1472 - }, - "green_concrete": { - "name": "Green Concrete", - "x": 544, - "y": 1472 - }, - "red_concrete": { - "name": "Red Concrete", - "x": 288, - "y": 1504 - }, - "black_concrete": { - "name": "Black Concrete", - "x": 64, - "y": 1472 - }, - "white_concrete_powder": { - "name": "White Concrete Powder", - "x": 416, - "y": 1504 - }, - "orange_concrete_powder": { - "name": "Orange Concrete Powder", - "x": 32, - "y": 1504 - }, - "magenta_concrete_powder": { - "name": "Magenta Concrete Powder", - "x": 960, - "y": 1472 - }, - "light_blue_concrete_powder": { - "name": "Light Blue Concrete Powder", - "x": 672, - "y": 1472 - }, - "yellow_concrete_powder": { - "name": "Yellow Concrete Powder", - "x": 512, - "y": 1504 - }, - "lime_concrete_powder": { - "name": "Lime Concrete Powder", - "x": 864, - "y": 1472 - }, - "pink_concrete_powder": { - "name": "Pink Concrete Powder", - "x": 128, - "y": 1504 - }, - "gray_concrete_powder": { - "name": "Gray Concrete Powder", - "x": 480, - "y": 1472 - }, - "light_gray_concrete_powder": { - "name": "Light Gray Concrete Powder", - "x": 768, - "y": 1472 - }, - "cyan_concrete_powder": { - "name": "Cyan Concrete Powder", - "x": 384, - "y": 1472 - }, - "purple_concrete_powder": { - "name": "Purple Concrete Powder", - "x": 224, - "y": 1504 - }, - "blue_concrete_powder": { - "name": "Blue Concrete Powder", - "x": 192, - "y": 1472 - }, - "brown_concrete_powder": { - "name": "Brown Concrete Powder", - "x": 288, - "y": 1472 - }, - "green_concrete_powder": { - "name": "Green Concrete Powder", - "x": 576, - "y": 1472 - }, - "red_concrete_powder": { - "name": "Red Concrete Powder", - "x": 320, - "y": 1504 - }, - "black_concrete_powder": { - "name": "Black Concrete Powder", - "x": 96, - "y": 1472 - }, - "turtle_egg": { - "name": "Turtle Egg", - "x": 608, - "y": 1952 - }, - "dead_tube_coral_block": { - "name": "Dead Tube Coral Block", - "x": 640, - "y": 1696 - }, - "dead_brain_coral_block": { - "name": "Dead Brain Coral Block", - "x": 672, - "y": 1696 - }, - "dead_bubble_coral_block": { - "name": "Dead Bubble Coral Block", - "x": 704, - "y": 1696 - }, - "dead_fire_coral_block": { - "name": "Dead Fire Coral Block", - "x": 736, - "y": 1696 - }, - "dead_horn_coral_block": { - "name": "Dead Horn Coral Block", - "x": 768, - "y": 1696 - }, - "tube_coral_block": { - "name": "Tube Coral Block", - "x": 288, - "y": 1696 - }, - "brain_coral_block": { - "name": "Brain Coral Block", - "x": 320, - "y": 1696 - }, - "bubble_coral_block": { - "name": "Bubble Coral Block", - "x": 352, - "y": 1696 - }, - "fire_coral_block": { - "name": "Fire Coral Block", - "x": 384, - "y": 1696 - }, - "horn_coral_block": { - "name": "Horn Coral Block", - "x": 416, - "y": 1696 - }, - "tube_coral": { - "name": "Tube Coral", - "x": 448, - "y": 1696 - }, - "brain_coral": { - "name": "Brain Coral", - "x": 480, - "y": 1696 - }, - "bubble_coral": { - "name": "Bubble Coral", - "x": 512, - "y": 1696 - }, - "fire_coral": { - "name": "Fire Coral", - "x": 544, - "y": 1696 - }, - "horn_coral": { - "name": "Horn Coral", - "x": 576, - "y": 1696 - }, - "dead_brain_coral": { - "name": "Dead Brain Coral", - "x": 992, - "y": 2336 - }, - "dead_bubble_coral": { - "name": "Dead Bubble Coral", - "x": 0, - "y": 2368 - }, - "dead_fire_coral": { - "name": "Dead Fire Coral", - "x": 32, - "y": 2368 - }, - "dead_horn_coral": { - "name": "Dead Horn Coral", - "x": 64, - "y": 2368 - }, - "dead_tube_coral": { - "name": "Dead Tube Coral", - "x": 960, - "y": 2336 - }, - "tube_coral_fan": { - "name": "Tube Coral Fan", - "x": 704, - "y": 2112 - }, - "brain_coral_fan": { - "name": "Brain Coral Fan", - "x": 608, - "y": 2112 - }, - "bubble_coral_fan": { - "name": "Bubble Coral Fan", - "x": 640, - "y": 2112 - }, - "fire_coral_fan": { - "name": "Fire Coral Fan", - "x": 896, - "y": 1696 - }, - "horn_coral_fan": { - "name": "Horn Coral Fan", - "x": 672, - "y": 2112 - }, - "dead_tube_coral_fan": { - "name": "Dead Tube Coral Fan", - "x": 160, - "y": 2144 - }, - "dead_brain_coral_fan": { - "name": "Dead Brain Coral Fan", - "x": 32, - "y": 2144 - }, - "dead_bubble_coral_fan": { - "name": "Dead Bubble Coral Fan", - "x": 64, - "y": 2144 - }, - "dead_fire_coral_fan": { - "name": "Dead Fire Coral Fan", - "x": 96, - "y": 2144 - }, - "dead_horn_coral_fan": { - "name": "Dead Horn Coral Fan", - "x": 128, - "y": 2144 - }, - "blue_ice": { - "name": "Blue Ice", - "x": 192, - "y": 1728 - }, - "conduit": { - "name": "Conduit", - "x": 512, - "y": 2016 - }, - "polished_granite_stairs": { - "name": "Polished Granite Stairs", - "x": 960, - "y": 2400 - }, - "smooth_red_sandstone_stairs": { - "name": "Smooth Red Sandstone Stairs", - "x": 384, - "y": 3296 - }, - "mossy_stone_brick_stairs": { - "name": "Mossy Stone Brick Stairs", - "x": 416, - "y": 3264 - }, - "polished_diorite_stairs": { - "name": "Polished Diorite Stairs", - "x": 832, - "y": 3264 - }, - "mossy_cobblestone_stairs": { - "name": "Mossy Cobblestone Stairs", - "x": 320, - "y": 3264 - }, - "end_stone_brick_stairs": { - "name": "End Stone Brick Stairs", - "x": 576, - "y": 2400 - }, - "stone_stairs": { - "name": "Stone Stairs", - "x": 896, - "y": 3296 - }, - "smooth_sandstone_stairs": { - "name": "Smooth Sandstone Stairs", - "x": 480, - "y": 3296 - }, - "smooth_quartz_stairs": { - "name": "Smooth Quartz Stairs", - "x": 320, - "y": 3296 - }, - "granite_stairs": { - "name": "Granite Stairs", - "x": 640, - "y": 2400 - }, - "andesite_stairs": { - "name": "Andesite Stairs", - "x": 832, - "y": 3200 - }, - "red_nether_brick_stairs": { - "name": "Red Nether Brick Stairs", - "x": 32, - "y": 3296 - }, - "polished_andesite_stairs": { - "name": "Polished Andesite Stairs", - "x": 768, - "y": 3264 - }, - "diorite_stairs": { - "name": "Diorite Stairs", - "x": 832, - "y": 3232 - }, - "polished_granite_slab": { - "name": "Polished Granite Slab", - "x": 928, - "y": 2400 - }, - "smooth_red_sandstone_slab": { - "name": "Smooth Red Sandstone Slab", - "x": 352, - "y": 3296 - }, - "mossy_stone_brick_slab": { - "name": "Mossy Stone Brick Slab", - "x": 384, - "y": 3264 - }, - "polished_diorite_slab": { - "name": "Polished Diorite Slab", - "x": 800, - "y": 3264 - }, - "mossy_cobblestone_slab": { - "name": "Mossy Cobblestone Slab", - "x": 288, - "y": 3264 - }, - "end_stone_brick_slab": { - "name": "End Stone Brick Slab", - "x": 544, - "y": 2400 - }, - "smooth_sandstone_slab": { - "name": "Smooth Sandstone Slab", - "x": 448, - "y": 3296 - }, - "smooth_quartz_slab": { - "name": "Smooth Quartz Slab", - "x": 288, - "y": 3296 - }, - "granite_slab": { - "name": "Granite Slab", - "x": 608, - "y": 2400 - }, - "andesite_slab": { - "name": "Andesite Slab", - "x": 800, - "y": 3200 - }, - "red_nether_brick_slab": { - "name": "Red Nether Brick Slab", - "x": 0, - "y": 3296 - }, - "polished_andesite_slab": { - "name": "Polished Andesite Slab", - "x": 736, - "y": 3264 - }, - "diorite_slab": { - "name": "Diorite Slab", - "x": 800, - "y": 3232 - }, - "scaffolding": { - "name": "Scaffolding", - "x": 416, - "y": 2400 - }, - "iron_door": { - "name": "Iron Door", - "x": 384, - "y": 928 - }, - "oak_door": { - "name": "Oak Door", - "x": 512, - "y": 928 - }, - "spruce_door": { - "name": "Spruce Door", - "x": 800, - "y": 928 - }, - "birch_door": { - "name": "Birch Door", - "x": 256, - "y": 928 - }, - "jungle_door": { - "name": "Jungle Door", - "x": 448, - "y": 928 - }, - "acacia_door": { - "name": "Acacia Door", - "x": 192, - "y": 928 - }, - "dark_oak_door": { - "name": "Dark Oak Door", - "x": 320, - "y": 928 - }, - "crimson_door": { - "name": "Crimson Door", - "x": 224, - "y": 160 - }, - "warped_door": { - "name": "Warped Door", - "x": 352, - "y": 160 - }, - "repeater": { - "name": "Redstone Repeater", - "x": 576, - "y": 3328 - }, - "comparator": { - "name": "Redstone Comparator", - "x": 512, - "y": 3328 - }, - "structure_block": { - "name": "Structure Block", - "x": 96, - "y": 1376 - }, - "jigsaw": { - "name": "Jigsaw Block", - "x": 64, - "y": 288 - }, - "turtle_helmet": { - "name": "Turtle Shell", - "x": 512, - "y": 1664 - }, - "scute": { - "name": "Scute", - "x": 608, - "y": 1664 - }, - "flint_and_steel": { - "name": "Flint and Steel", - "x": 480, - "y": 3552 - }, - "apple": { - "name": "Apple", - "x": 768, - "y": 3168 - }, - "bow": { - "name": "Bow", - "x": 992, - "y": 1248 - }, - "arrow": { - "name": "Arrow", - "x": 576, - "y": 1248 - }, - "coal": { - "name": "Coal", - "x": 576, - "y": 3488 - }, - "charcoal": { - "name": "Charcoal", - "x": 512, - "y": 3488 - }, - "diamond": { - "name": "Diamond", - "x": 608, - "y": 3488 - }, - "iron_ingot": { - "name": "Iron Ingot", - "x": 896, - "y": 3488 - }, - "gold_ingot": { - "name": "Gold Ingot", - "x": 800, - "y": 3488 - }, - "netherite_ingot": { - "name": "Netherite Ingot", - "x": 192, - "y": 128 - }, - "netherite_scrap": { - "name": "Netherite Scrap", - "x": 224, - "y": 128 - }, - "wooden_sword": { - "name": "Wooden Sword", - "x": 800, - "y": 3616 - }, - "wooden_shovel": { - "name": "Wooden Shovel", - "x": 192, - "y": 3584 - }, - "wooden_pickaxe": { - "name": "Wooden Pickaxe", - "x": 160, - "y": 3584 - }, - "wooden_axe": { - "name": "Wooden Axe", - "x": 96, - "y": 3584 - }, - "wooden_hoe": { - "name": "Wooden Hoe", - "x": 128, - "y": 3584 - }, - "stone_sword": { - "name": "Stone Sword", - "x": 736, - "y": 3616 - }, - "stone_shovel": { - "name": "Stone Shovel", - "x": 64, - "y": 3584 - }, - "stone_pickaxe": { - "name": "Stone Pickaxe", - "x": 32, - "y": 3584 - }, - "stone_axe": { - "name": "Stone Axe", - "x": 992, - "y": 3552 - }, - "stone_hoe": { - "name": "Stone Hoe", - "x": 0, - "y": 3584 - }, - "golden_sword": { - "name": "Golden Sword", - "x": 608, - "y": 3616 - }, - "golden_shovel": { - "name": "Golden Shovel", - "x": 640, - "y": 3552 - }, - "golden_pickaxe": { - "name": "Golden Pickaxe", - "x": 608, - "y": 3552 - }, - "golden_axe": { - "name": "Golden Axe", - "x": 544, - "y": 3552 - }, - "golden_hoe": { - "name": "Golden Hoe", - "x": 576, - "y": 3552 - }, - "iron_sword": { - "name": "Iron Sword", - "x": 640, - "y": 3616 - }, - "iron_shovel": { - "name": "Iron Shovel", - "x": 768, - "y": 3552 - }, - "iron_pickaxe": { - "name": "Iron Pickaxe", - "x": 736, - "y": 3552 - }, - "iron_axe": { - "name": "Iron Axe", - "x": 672, - "y": 3552 - }, - "iron_hoe": { - "name": "Iron Hoe", - "x": 704, - "y": 3552 - }, - "diamond_sword": { - "name": "Diamond Sword", - "x": 576, - "y": 3616 - }, - "diamond_shovel": { - "name": "Diamond Shovel", - "x": 288, - "y": 3552 - }, - "diamond_pickaxe": { - "name": "Diamond Pickaxe", - "x": 256, - "y": 3552 - }, - "diamond_axe": { - "name": "Diamond Axe", - "x": 192, - "y": 3552 - }, - "diamond_hoe": { - "name": "Diamond Hoe", - "x": 224, - "y": 3552 - }, - "netherite_sword": { - "name": "Netherite Sword", - "x": 384, - "y": 128 - }, - "netherite_shovel": { - "name": "Netherite Shovel", - "x": 352, - "y": 128 - }, - "netherite_pickaxe": { - "name": "Netherite Pickaxe", - "x": 320, - "y": 128 - }, - "netherite_axe": { - "name": "Netherite Axe", - "x": 256, - "y": 128 - }, - "netherite_hoe": { - "name": "Netherite Hoe", - "x": 288, - "y": 128 - }, - "stick": { - "name": "Stick", - "x": 608, - "y": 1120 - }, - "bowl": { - "name": "Bowl", - "x": 384, - "y": 3520 - }, - "mushroom_stew": { - "name": "Mushroom Stew", - "x": 224, - "y": 3200 - }, - "string": { - "name": "String", - "x": 320, - "y": 3520 - }, - "feather": { - "name": "Feather", - "x": 704, - "y": 3488 - }, - "gunpowder": { - "name": "Gunpowder", - "x": 864, - "y": 3488 - }, - "wheat_seeds": { - "name": "Wheat Seeds", - "x": 320, - "y": 1056 - }, - "wheat": { - "name": "Wheat", - "x": 608, - "y": 3200 - }, - "bread": { - "name": "Bread", - "x": 896, - "y": 3168 - }, - "leather_helmet": { - "name": "Leather Cap", - "x": 608, - "y": 2496 - }, - "leather_chestplate": { - "name": "Leather Tunic", - "x": 672, - "y": 2496 - }, - "leather_leggings": { - "name": "Leather Pants", - "x": 640, - "y": 2496 - }, - "leather_boots": { - "name": "Leather Boots", - "x": 576, - "y": 2496 - }, - "chainmail_helmet": { - "name": "Chainmail Helmet", - "x": 320, - "y": 2464 - }, - "chainmail_chestplate": { - "name": "Chainmail Chestplate", - "x": 288, - "y": 2464 - }, - "chainmail_leggings": { - "name": "Chainmail Leggings", - "x": 352, - "y": 2464 - }, - "chainmail_boots": { - "name": "Chainmail Boots", - "x": 256, - "y": 2464 - }, - "iron_helmet": { - "name": "Iron Helmet", - "x": 512, - "y": 2496 - }, - "iron_chestplate": { - "name": "Iron Chestplate", - "x": 480, - "y": 2496 - }, - "iron_leggings": { - "name": "Iron Leggings", - "x": 544, - "y": 2496 - }, - "iron_boots": { - "name": "Iron Boots", - "x": 448, - "y": 2496 - }, - "diamond_helmet": { - "name": "Diamond Helmet", - "x": 160, - "y": 2496 - }, - "diamond_chestplate": { - "name": "Diamond Chestplate", - "x": 128, - "y": 2496 - }, - "diamond_leggings": { - "name": "Diamond Leggings", - "x": 192, - "y": 2496 - }, - "diamond_boots": { - "name": "Diamond Boots", - "x": 96, - "y": 2496 - }, - "golden_helmet": { - "name": "Golden Helmet", - "x": 320, - "y": 2496 - }, - "golden_chestplate": { - "name": "Golden Chestplate", - "x": 288, - "y": 2496 - }, - "golden_leggings": { - "name": "Golden Leggings", - "x": 352, - "y": 2496 - }, - "golden_boots": { - "name": "Golden Boots", - "x": 256, - "y": 2496 - }, - "netherite_helmet": { - "name": "Netherite Helmet", - "x": 480, - "y": 128 - }, - "netherite_chestplate": { - "name": "Netherite Chestplate", - "x": 448, - "y": 128 - }, - "netherite_leggings": { - "name": "Netherite Leggings", - "x": 512, - "y": 128 - }, - "netherite_boots": { - "name": "Netherite Boots", - "x": 416, - "y": 128 - }, - "flint": { - "name": "Flint", - "x": 736, - "y": 3488 - }, - "porkchop": { - "name": "Raw Porkchop", - "x": 480, - "y": 3200 - }, - "cooked_porkchop": { - "name": "Cooked Porkchop", - "x": 32, - "y": 3200 - }, - "painting": { - "name": "Painting", - "x": 544, - "y": 3136 - }, - "golden_apple": { - "name": "Golden Apple", - "x": 160, - "y": 3200 - }, - "enchanted_golden_apple": { - "name": "Enchanted Golden Apple", - "x": 128, - "y": 256 - }, - "oak_sign": { - "name": "Oak Sign", - "x": 896, - "y": 3360 - }, - "spruce_sign": { - "name": "Spruce Sign", - "x": 800, - "y": 2368 - }, - "birch_sign": { - "name": "Birch Sign", - "x": 768, - "y": 2368 - }, - "jungle_sign": { - "name": "Jungle Sign", - "x": 832, - "y": 2368 - }, - "acacia_sign": { - "name": "Acacia Sign", - "x": 864, - "y": 2368 - }, - "dark_oak_sign": { - "name": "Dark Oak Sign", - "x": 896, - "y": 2368 - }, - "crimson_sign": { - "name": "Crimson Sign", - "x": 512, - "y": 160 - }, - "warped_sign": { - "name": "Warped Sign", - "x": 544, - "y": 160 - }, - "bucket": { - "name": "Bucket", - "x": 288, - "y": 3136 - }, - "water_bucket": { - "name": "Water Bucket", - "x": 384, - "y": 3136 - }, - "lava_bucket": { - "name": "Lava Bucket", - "x": 320, - "y": 3136 - }, - "minecart": { - "name": "Minecart", - "x": 160, - "y": 3616 - }, - "saddle": { - "name": "Saddle", - "x": 928, - "y": 3552 - }, - "redstone": { - "name": "Redstone Dust", - "x": 224, - "y": 3520 - }, - "snowball": { - "name": "Snowball", - "x": 672, - "y": 3616 - }, - "oak_boat": { - "name": "Oak Boat", - "x": 352, - "y": 3616 - }, - "leather": { - "name": "Leather", - "x": 960, - "y": 3488 - }, - "milk_bucket": { - "name": "Milk Bucket", - "x": 352, - "y": 3136 - }, - "pufferfish_bucket": { - "name": "Bucket of Pufferfish", - "x": 704, - "y": 1664 - }, - "salmon_bucket": { - "name": "Bucket of Salmon", - "x": 736, - "y": 1664 - }, - "cod_bucket": { - "name": "Bucket of Cod", - "x": 672, - "y": 1664 - }, - "tropical_fish_bucket": { - "name": "Bucket of Tropical Fish", - "x": 352, - "y": 2016 - }, - "brick": { - "name": "Brick", - "x": 480, - "y": 3488 - }, - "clay_ball": { - "name": "Clay Ball", - "x": 544, - "y": 3488 - }, - "dried_kelp_block": { - "name": "Dried Kelp Block", - "x": 128, - "y": 1664 - }, - "paper": { - "name": "Paper", - "x": 64, - "y": 3520 - }, - "book": { - "name": "Book", - "x": 448, - "y": 3488 - }, - "slime_ball": { - "name": "Slimeball", - "x": 288, - "y": 3520 - }, - "chest_minecart": { - "name": "Minecart with Chest", - "x": 192, - "y": 3616 - }, - "furnace_minecart": { - "name": "Minecart with Furnace", - "x": 256, - "y": 3616 - }, - "egg": { - "name": "Egg", - "x": 128, - "y": 3200 - }, - "compass": { - "name": "Compass", - "x": 992, - "y": 480 - }, - "fishing_rod": { - "name": "Fishing Rod", - "x": 448, - "y": 3552 - }, - "clock": { - "name": "Clock", - "x": 960, - "y": 480 - }, - "glowstone_dust": { - "name": "Glowstone Dust", - "x": 768, - "y": 3488 - }, - "cod": { - "name": "Raw Cod", - "x": 896, - "y": 1664 - }, - "salmon": { - "name": "Raw Salmon", - "x": 928, - "y": 1664 - }, - "tropical_fish": { - "name": "Tropical Fish", - "x": 768, - "y": 1664 - }, - "pufferfish": { - "name": "Pufferfish", - "x": 864, - "y": 1664 - }, - "cooked_cod": { - "name": "Cooked Cod", - "x": 800, - "y": 1664 - }, - "cooked_salmon": { - "name": "Cooked Salmon", - "x": 832, - "y": 1664 - }, - "ink_sac": { - "name": "Ink Sac", - "x": 736, - "y": 3136 - }, - "cocoa_beans": { - "name": "Cocoa Beans", - "x": 608, - "y": 3136 - }, - "lapis_lazuli": { - "name": "Lapis Lazuli", - "x": 768, - "y": 3136 - }, - "white_dye": { - "name": "White Dye", - "x": 288, - "y": 2368 - }, - "orange_dye": { - "name": "Orange Dye", - "x": 928, - "y": 3136 - }, - "magenta_dye": { - "name": "Magenta Dye", - "x": 896, - "y": 3136 - }, - "light_blue_dye": { - "name": "Light Blue Dye", - "x": 800, - "y": 3136 - }, - "yellow_dye": { - "name": "Yellow Dye", - "x": 32, - "y": 3168 - }, - "lime_dye": { - "name": "Lime Dye", - "x": 864, - "y": 3136 - }, - "pink_dye": { - "name": "Pink Dye", - "x": 960, - "y": 3136 - }, - "gray_dye": { - "name": "Gray Dye", - "x": 672, - "y": 3136 - }, - "light_gray_dye": { - "name": "Light Gray Dye", - "x": 832, - "y": 3136 - }, - "cyan_dye": { - "name": "Cyan Dye", - "x": 640, - "y": 3136 - }, - "purple_dye": { - "name": "Purple Dye", - "x": 992, - "y": 3136 - }, - "blue_dye": { - "name": "Blue Dye", - "x": 224, - "y": 2368 - }, - "brown_dye": { - "name": "Brown Dye", - "x": 256, - "y": 2368 - }, - "green_dye": { - "name": "Green Dye", - "x": 704, - "y": 3136 - }, - "red_dye": { - "name": "Red Dye", - "x": 0, - "y": 3168 - }, - "black_dye": { - "name": "Black Dye", - "x": 192, - "y": 2368 - }, - "bone_meal": { - "name": "Bone Meal", - "x": 576, - "y": 3136 - }, - "bone": { - "name": "Bone", - "x": 416, - "y": 3488 - }, - "sugar": { - "name": "Sugar", - "x": 576, - "y": 3200 - }, - "cake": { - "name": "Cake", - "x": 416, - "y": 3584 - }, - "white_bed": { - "name": "White Bed", - "x": 512, - "y": 3168 - }, - "orange_bed": { - "name": "Orange Bed", - "x": 384, - "y": 3168 - }, - "magenta_bed": { - "name": "Magenta Bed", - "x": 352, - "y": 3168 - }, - "light_blue_bed": { - "name": "Light Blue Bed", - "x": 256, - "y": 3168 - }, - "yellow_bed": { - "name": "Yellow Bed", - "x": 544, - "y": 3168 - }, - "lime_bed": { - "name": "Lime Bed", - "x": 320, - "y": 3168 - }, - "pink_bed": { - "name": "Pink Bed", - "x": 416, - "y": 3168 - }, - "gray_bed": { - "name": "Gray Bed", - "x": 192, - "y": 3168 - }, - "light_gray_bed": { - "name": "Light Gray Bed", - "x": 288, - "y": 3168 - }, - "cyan_bed": { - "name": "Cyan Bed", - "x": 160, - "y": 3168 - }, - "purple_bed": { - "name": "Purple Bed", - "x": 448, - "y": 3168 - }, - "blue_bed": { - "name": "Blue Bed", - "x": 96, - "y": 3168 - }, - "brown_bed": { - "name": "Brown Bed", - "x": 128, - "y": 3168 - }, - "green_bed": { - "name": "Green Bed", - "x": 224, - "y": 3168 - }, - "red_bed": { - "name": "Red Bed", - "x": 480, - "y": 3168 - }, - "black_bed": { - "name": "Black Bed", - "x": 64, - "y": 3168 - }, - "cookie": { - "name": "Cookie", - "x": 96, - "y": 3200 - }, - "filled_map": { - "name": "Map", - "x": 832, - "y": 3552 - }, - "shears": { - "name": "Shears", - "x": 960, - "y": 3552 - }, - "melon_slice": { - "name": "Melon Slice", - "x": 192, - "y": 3200 - }, - "dried_kelp": { - "name": "Dried Kelp", - "x": 544, - "y": 1664 - }, - "pumpkin_seeds": { - "name": "Pumpkin Seeds", - "x": 128, - "y": 1056 - }, - "melon_seeds": { - "name": "Melon Seeds", - "x": 800, - "y": 1024 - }, - "beef": { - "name": "Raw Beef", - "x": 384, - "y": 3200 - }, - "cooked_beef": { - "name": "Steak", - "x": 480, - "y": 832 - }, - "chicken": { - "name": "Raw Chicken", - "x": 416, - "y": 3200 - }, - "cooked_chicken": { - "name": "Cooked Chicken", - "x": 992, - "y": 3168 - }, - "rotten_flesh": { - "name": "Rotten Flesh", - "x": 544, - "y": 3200 - }, - "ender_pearl": { - "name": "Ender Pearl", - "x": 672, - "y": 3488 - }, - "blaze_rod": { - "name": "Blaze Rod", - "x": 448, - "y": 640 - }, - "ghast_tear": { - "name": "Ghast Tear", - "x": 96, - "y": 3136 - }, - "gold_nugget": { - "name": "Gold Nugget", - "x": 832, - "y": 3488 - }, - "nether_wart": { - "name": "Nether Wart", - "x": 704, - "y": 640 - }, - "potion": { - "name": "Potion of Luck", - "x": 960, - "y": 3424 - }, - "glass_bottle": { - "name": "Glass Bottle", - "x": 512, - "y": 3552 - }, - "spider_eye": { - "name": "Spider Eye", - "x": 256, - "y": 3136 - }, - "fermented_spider_eye": { - "name": "Fermented Spider Eye", - "x": 64, - "y": 3136 - }, - "blaze_powder": { - "name": "Blaze Powder", - "x": 416, - "y": 640 - }, - "magma_cream": { - "name": "Magma Cream", - "x": 192, - "y": 3136 - }, - "brewing_stand": { - "name": "Brewing Stand", - "x": 384, - "y": 3584 - }, - "cauldron": { - "name": "Cauldron", - "x": 448, - "y": 3584 - }, - "ender_eye": { - "name": "Eye of Ender", - "x": 384, - "y": 3552 - }, - "glistering_melon_slice": { - "name": "Glistering Melon Slice", - "x": 128, - "y": 3136 - }, - "bat_spawn_egg": { - "name": "Bat Spawn Egg", - "x": 672, - "y": 1120 - }, - "bee_spawn_egg": { - "name": "Bee Spawn Egg", - "x": 224, - "y": 96 - }, - "blaze_spawn_egg": { - "name": "Blaze Spawn Egg", - "x": 704, - "y": 1120 - }, - "cat_spawn_egg": { - "name": "Cat Spawn Egg", - "x": 288, - "y": 2400 - }, - "cave_spider_spawn_egg": { - "name": "Cave Spider Spawn Egg", - "x": 736, - "y": 1120 - }, - "chicken_spawn_egg": { - "name": "Chicken Spawn Egg", - "x": 768, - "y": 1120 - }, - "cod_spawn_egg": { - "name": "Cod Spawn Egg", - "x": 0, - "y": 1696 - }, - "cow_spawn_egg": { - "name": "Cow Spawn Egg", - "x": 800, - "y": 1120 - }, - "creeper_spawn_egg": { - "name": "Creeper Spawn Egg", - "x": 832, - "y": 1120 - }, - "dolphin_spawn_egg": { - "name": "Dolphin Spawn Egg", - "x": 448, - "y": 1952 - }, - "donkey_spawn_egg": { - "name": "Donkey Spawn Egg", - "x": 832, - "y": 1344 - }, - "drowned_spawn_egg": { - "name": "Drowned Spawn Egg", - "x": 960, - "y": 1696 - }, - "elder_guardian_spawn_egg": { - "name": "Elder Guardian Spawn Egg", - "x": 864, - "y": 1344 - }, - "enderman_spawn_egg": { - "name": "Enderman Spawn Egg", - "x": 864, - "y": 1120 - }, - "endermite_spawn_egg": { - "name": "Endermite Spawn Egg", - "x": 896, - "y": 1120 - }, - "evoker_spawn_egg": { - "name": "Evoker Spawn Egg", - "x": 64, - "y": 1408 - }, - "fox_spawn_egg": { - "name": "Fox Spawn Egg", - "x": 992, - "y": 2432 - }, - "ghast_spawn_egg": { - "name": "Ghast Spawn Egg", - "x": 928, - "y": 1120 - }, - "guardian_spawn_egg": { - "name": "Guardian Spawn Egg", - "x": 960, - "y": 1120 - }, - "hoglin_spawn_egg": { - "name": "Hoglin Spawn Egg", - "x": 32, - "y": 192 - }, - "horse_spawn_egg": { - "name": "Horse Spawn Egg", - "x": 992, - "y": 1120 - }, - "husk_spawn_egg": { - "name": "Husk Spawn Egg", - "x": 896, - "y": 1344 - }, - "llama_spawn_egg": { - "name": "Llama Spawn Egg", - "x": 96, - "y": 1408 - }, - "magma_cube_spawn_egg": { - "name": "Magma Cube Spawn Egg", - "x": 0, - "y": 1152 - }, - "mooshroom_spawn_egg": { - "name": "Mooshroom Spawn Egg", - "x": 32, - "y": 1152 - }, - "mule_spawn_egg": { - "name": "Mule Spawn Egg", - "x": 928, - "y": 1344 - }, - "ocelot_spawn_egg": { - "name": "Ocelot Spawn Egg", - "x": 64, - "y": 1152 - }, - "panda_spawn_egg": { - "name": "Panda Spawn Egg", - "x": 352, - "y": 2400 - }, - "parrot_spawn_egg": { - "name": "Parrot Spawn Egg", - "x": 224, - "y": 1536 - }, - "phantom_spawn_egg": { - "name": "Phantom Spawn Egg", - "x": 672, - "y": 1952 - }, - "pig_spawn_egg": { - "name": "Pig Spawn Egg", - "x": 96, - "y": 1152 - }, - "piglin_spawn_egg": { - "name": "Piglin Spawn Egg", - "x": 0, - "y": 192 - }, - "piglin_brute_spawn_egg": { - "name": "Piglin Brute Spawn Egg", - "x": 160, - "y": 256 - }, - "pillager_spawn_egg": { - "name": "Pillager Spawn Egg", - "x": 384, - "y": 2400 - }, - "polar_bear_spawn_egg": { - "name": "Polar Bear Spawn Egg", - "x": 960, - "y": 1344 - }, - "pufferfish_spawn_egg": { - "name": "Pufferfish Spawn Egg", - "x": 32, - "y": 1696 - }, - "rabbit_spawn_egg": { - "name": "Rabbit Spawn Egg", - "x": 128, - "y": 1152 - }, - "ravager_spawn_egg": { - "name": "Ravager Spawn Egg", - "x": 320, - "y": 2400 - }, - "salmon_spawn_egg": { - "name": "Salmon Spawn Egg", - "x": 64, - "y": 1696 - }, - "sheep_spawn_egg": { - "name": "Sheep Spawn Egg", - "x": 160, - "y": 1152 - }, - "shulker_spawn_egg": { - "name": "Shulker Spawn Egg", - "x": 192, - "y": 1152 - }, - "silverfish_spawn_egg": { - "name": "Silverfish Spawn Egg", - "x": 224, - "y": 1152 - }, - "skeleton_spawn_egg": { - "name": "Skeleton Spawn Egg", - "x": 256, - "y": 1152 - }, - "skeleton_horse_spawn_egg": { - "name": "Skeleton Horse Spawn Egg", - "x": 992, - "y": 1344 - }, - "slime_spawn_egg": { - "name": "Slime Spawn Egg", - "x": 288, - "y": 1152 - }, - "spider_spawn_egg": { - "name": "Spider Spawn Egg", - "x": 320, - "y": 1152 - }, - "squid_spawn_egg": { - "name": "Squid Spawn Egg", - "x": 352, - "y": 1152 - }, - "stray_spawn_egg": { - "name": "Stray Spawn Egg", - "x": 0, - "y": 1376 - }, - "strider_spawn_egg": { - "name": "Strider Spawn Egg", - "x": 960, - "y": 192 - }, - "trader_llama_spawn_egg": { - "name": "Trader Llama Spawn Egg", - "x": 928, - "y": 2432 - }, - "tropical_fish_spawn_egg": { - "name": "Tropical Fish Spawn Egg", - "x": 800, - "y": 1696 - }, - "turtle_spawn_egg": { - "name": "Turtle Spawn Egg", - "x": 0, - "y": 1728 - }, - "vex_spawn_egg": { - "name": "Vex Spawn Egg", - "x": 128, - "y": 1408 - }, - "villager_spawn_egg": { - "name": "Villager Spawn Egg", - "x": 384, - "y": 1152 - }, - "vindicator_spawn_egg": { - "name": "Vindicator Spawn Egg", - "x": 160, - "y": 1408 - }, - "wandering_trader_spawn_egg": { - "name": "Wandering Trader Spawn Egg", - "x": 960, - "y": 2432 - }, - "witch_spawn_egg": { - "name": "Witch Spawn Egg", - "x": 416, - "y": 1152 - }, - "wither_skeleton_spawn_egg": { - "name": "Wither Skeleton Spawn Egg", - "x": 32, - "y": 1376 - }, - "wolf_spawn_egg": { - "name": "Wolf Spawn Egg", - "x": 448, - "y": 1152 - }, - "zoglin_spawn_egg": { - "name": "Zoglin Spawn Egg", - "x": 352, - "y": 256 - }, - "zombie_spawn_egg": { - "name": "Zombie Spawn Egg", - "x": 480, - "y": 1152 - }, - "zombie_horse_spawn_egg": { - "name": "Zombie Horse Spawn Egg", - "x": 64, - "y": 1376 - }, - "zombie_villager_spawn_egg": { - "name": "Zombie Villager Spawn Egg", - "x": 128, - "y": 1344 - }, - "zombified_piglin_spawn_egg": { - "name": "Zombified Piglin Spawn Egg", - "x": 512, - "y": 1152 - }, - "experience_bottle": { - "name": "Bottle o' Enchanting", - "x": 288, - "y": 3456 - }, - "fire_charge": { - "name": "Fire Charge", - "x": 416, - "y": 3552 - }, - "writable_book": { - "name": "Book and Quill", - "x": 352, - "y": 3520 - }, - "written_book": { - "name": "Written Book", - "x": 256, - "y": 3584 - }, - "emerald": { - "name": "Emerald", - "x": 640, - "y": 3488 - }, - "item_frame": { - "name": "Item Frame", - "x": 480, - "y": 3136 - }, - "flower_pot": { - "name": "Flower Pot", - "x": 448, - "y": 3136 - }, - "carrot": { - "name": "Carrot", - "x": 928, - "y": 3168 - }, - "potato": { - "name": "Potato", - "x": 288, - "y": 3200 - }, - "baked_potato": { - "name": "Baked Potato", - "x": 800, - "y": 3168 - }, - "poisonous_potato": { - "name": "Poisonous Potato", - "x": 256, - "y": 3200 - }, - "map": { - "name": "Empty Map", - "x": 320, - "y": 3552 - }, - "golden_carrot": { - "name": "Golden Carrot", - "x": 160, - "y": 3136 - }, - "skeleton_skull": { - "name": "Skeleton Skull", - "x": 192, - "y": 960 - }, - "wither_skeleton_skull": { - "name": "Wither Skeleton Skull", - "x": 224, - "y": 960 - }, - "player_head": { - "name": "Player Head", - "x": 160, - "y": 960 - }, - "zombie_head": { - "name": "Zombie Head", - "x": 256, - "y": 960 - }, - "creeper_head": { - "name": "Creeper Head", - "x": 96, - "y": 960 - }, - "dragon_head": { - "name": "Dragon Head", - "x": 128, - "y": 960 - }, - "carrot_on_a_stick": { - "name": "Carrot on a Stick", - "x": 416, - "y": 3520 - }, - "warped_fungus_on_a_stick": { - "name": "Warped Fungus on a Stick", - "x": 992, - "y": 192 - }, - "nether_star": { - "name": "Nether Star", - "x": 64, - "y": 352 - }, - "pumpkin_pie": { - "name": "Pumpkin Pie", - "x": 320, - "y": 3200 - }, - "firework_rocket": { - "name": "Firework Rocket", - "x": 736, - "y": 3168 - }, - "firework_star": { - "name": "Firework Star", - "x": 256, - "y": 800 - }, - "enchanted_book": { - "name": "Enchanted Book", - "x": 352, - "y": 3552 - }, - "nether_brick": { - "name": "Nether Brick", - "x": 992, - "y": 3488 - }, - "quartz": { - "name": "Nether Quartz", - "x": 0, - "y": 3520 - }, - "tnt_minecart": { - "name": "Minecart with TNT", - "x": 320, - "y": 3616 - }, - "hopper_minecart": { - "name": "Minecart with Hopper", - "x": 288, - "y": 3616 - }, - "prismarine_shard": { - "name": "Prismarine Shard", - "x": 160, - "y": 3520 - }, - "prismarine_crystals": { - "name": "Prismarine Crystals", - "x": 128, - "y": 3520 - }, - "rabbit": { - "name": "Raw Rabbit", - "x": 512, - "y": 3200 - }, - "cooked_rabbit": { - "name": "Cooked Rabbit", - "x": 64, - "y": 3200 - }, - "rabbit_stew": { - "name": "Rabbit Stew", - "x": 352, - "y": 3200 - }, - "rabbit_foot": { - "name": "Rabbit's Foot", - "x": 224, - "y": 3136 - }, - "rabbit_hide": { - "name": "Rabbit Hide", - "x": 192, - "y": 3520 - }, - "armor_stand": { - "name": "Armor Stand", - "x": 416, - "y": 3136 - }, - "iron_horse_armor": { - "name": "Iron Horse Armor", - "x": 192, - "y": 32 - }, - "golden_horse_armor": { - "name": "Golden Horse Armor", - "x": 32, - "y": 32 - }, - "diamond_horse_armor": { - "name": "Diamond Horse Armor", - "x": 896, - "y": 0 - }, - "leather_horse_armor": { - "name": "Leather Horse Armor", - "x": 160, - "y": 1376 - }, - "lead": { - "name": "Lead", - "x": 800, - "y": 3552 - }, - "name_tag": { - "name": "Name Tag", - "x": 864, - "y": 3552 - }, - "command_block_minecart": { - "name": "Minecart with Command Block", - "x": 224, - "y": 3616 - }, - "mutton": { - "name": "Raw Mutton", - "x": 448, - "y": 3200 - }, - "cooked_mutton": { - "name": "Cooked Mutton", - "x": 0, - "y": 3200 - }, - "white_banner": { - "name": "White Banner", - "x": 608, - "y": 768 - }, - "orange_banner": { - "name": "Orange Banner", - "x": 864, - "y": 736 - }, - "magenta_banner": { - "name": "Magenta Banner", - "x": 672, - "y": 736 - }, - "light_blue_banner": { - "name": "Light Blue Banner", - "x": 96, - "y": 736 - }, - "yellow_banner": { - "name": "Yellow Banner", - "x": 800, - "y": 768 - }, - "lime_banner": { - "name": "Lime Banner", - "x": 480, - "y": 736 - }, - "pink_banner": { - "name": "Pink Banner", - "x": 32, - "y": 768 - }, - "gray_banner": { - "name": "Gray Banner", - "x": 736, - "y": 704 - }, - "light_gray_banner": { - "name": "Light Gray Banner", - "x": 288, - "y": 736 - }, - "cyan_banner": { - "name": "Cyan Banner", - "x": 544, - "y": 704 - }, - "purple_banner": { - "name": "Purple Banner", - "x": 224, - "y": 768 - }, - "blue_banner": { - "name": "Blue Banner", - "x": 160, - "y": 704 - }, - "brown_banner": { - "name": "Brown Banner", - "x": 352, - "y": 704 - }, - "green_banner": { - "name": "Green Banner", - "x": 928, - "y": 704 - }, - "red_banner": { - "name": "Red Banner", - "x": 416, - "y": 768 - }, - "black_banner": { - "name": "Black Banner", - "x": 992, - "y": 672 - }, - "end_crystal": { - "name": "End Crystal", - "x": 608, - "y": 3168 - }, - "chorus_fruit": { - "name": "Chorus Fruit", - "x": 960, - "y": 3168 - }, - "popped_chorus_fruit": { - "name": "Popped Chorus Fruit", - "x": 96, - "y": 3520 - }, - "beetroot": { - "name": "Beetroot", - "x": 864, - "y": 3168 - }, - "beetroot_seeds": { - "name": "Beetroot Seeds", - "x": 320, - "y": 3392 - }, - "beetroot_soup": { - "name": "Beetroot Soup", - "x": 832, - "y": 3168 - }, - "dragon_breath": { - "name": "Dragon's Breath", - "x": 32, - "y": 3136 - }, - "splash_potion": { - "name": "Splash Potion of Luck", - "x": 512, - "y": 3456 - }, - "spectral_arrow": { - "name": "Spectral Arrow", - "x": 704, - "y": 3616 - }, - "tipped_arrow": { - "name": "Tipped Arrow", - "x": 544, - "y": 1248 - }, - "lingering_potion": { - "name": "Lingering Potion of Luck", - "x": 0, - "y": 3488 - }, - "shield": { - "name": "Shield", - "x": 960, - "y": 2496 - }, - "elytra": { - "name": "Elytra", - "x": 224, - "y": 2496 - }, - "spruce_boat": { - "name": "Spruce Boat", - "x": 384, - "y": 3616 - }, - "birch_boat": { - "name": "Birch Boat", - "x": 64, - "y": 3616 - }, - "jungle_boat": { - "name": "Jungle Boat", - "x": 128, - "y": 3616 - }, - "acacia_boat": { - "name": "Acacia Boat", - "x": 32, - "y": 3616 - }, - "dark_oak_boat": { - "name": "Dark Oak Boat", - "x": 96, - "y": 3616 - }, - "totem_of_undying": { - "name": "Totem of Undying", - "x": 768, - "y": 3616 - }, - "shulker_shell": { - "name": "Shulker Shell", - "x": 256, - "y": 3520 - }, - "iron_nugget": { - "name": "Iron Nugget", - "x": 928, - "y": 3488 - }, - "knowledge_book": { - "name": "Knowledge Book", - "x": 800, - "y": 3584 - }, - "debug_stick": { - "name": "Debug Stick", - "x": 608, - "y": 1120 - }, - "music_disc_pigstep": { - "name": "Music Disc 13", - "x": 0, - "y": 672 - }, - "trident": { - "name": "Trident", - "x": 608, - "y": 1696 - }, - "phantom_membrane": { - "name": "Phantom Membrane", - "x": 32, - "y": 1728 - }, - "nautilus_shell": { - "name": "Nautilus Shell", - "x": 448, - "y": 1728 - }, - "heart_of_the_sea": { - "name": "Heart of the Sea", - "x": 416, - "y": 1728 - }, - "crossbow": { - "name": "Crossbow", - "x": 416, - "y": 2368 - }, - "suspicious_stew": { - "name": "Suspicious Stew", - "x": 928, - "y": 2368 - }, - "loom": { - "name": "Loom", - "x": 416, - "y": 2432 - }, - "flower_banner_pattern": { - "name": "Banner Pattern", - "x": 704, - "y": 2368 - }, - "creeper_banner_pattern": { - "name": "Banner Pattern", - "x": 704, - "y": 2368 - }, - "skull_banner_pattern": { - "name": "Banner Pattern", - "x": 704, - "y": 2368 - }, - "mojang_banner_pattern": { - "name": "Banner Pattern", - "x": 704, - "y": 2368 - }, - "globe_banner_pattern": { - "name": "Banner Pattern", - "x": 704, - "y": 2368 - }, - "piglin_banner_pattern": { - "name": "Banner Pattern", - "x": 704, - "y": 2368 - }, - "composter": { - "name": "Composter", - "x": 864, - "y": 2432 - }, - "barrel": { - "name": "Barrel", - "x": 64, - "y": 2432 - }, - "smoker": { - "name": "Smoker", - "x": 480, - "y": 2432 - }, - "blast_furnace": { - "name": "Blast Furnace", - "x": 288, - "y": 2432 - }, - "cartography_table": { - "name": "Cartography Table", - "x": 608, - "y": 2432 - }, - "fletching_table": { - "name": "Fletching Table", - "x": 640, - "y": 2432 - }, - "grindstone": { - "name": "Grindstone", - "x": 896, - "y": 2432 - }, - "lectern": { - "name": "Lectern", - "x": 576, - "y": 0 - }, - "smithing_table": { - "name": "Smithing Table", - "x": 672, - "y": 2432 - }, - "stonecutter": { - "name": "Stonecutter BE", - "x": 800, - "y": 1056 - }, - "bell": { - "name": "Bell", - "x": 256, - "y": 2400 - }, - "lantern": { - "name": "Lantern", - "x": 576, - "y": 2432 - }, - "soul_lantern": { - "name": "Soul Lantern", - "x": 896, - "y": 128 - }, - "sweet_berries": { - "name": "Sweet Berries", - "x": 768, - "y": 2432 - }, - "campfire": { - "name": "Campfire", - "x": 832, - "y": 2432 - }, - "soul_campfire": { - "name": "Soul Campfire", - "x": 960, - "y": 256 - }, - "shroomlight": { - "name": "Shroomlight", - "x": 704, - "y": 128 - }, - "honeycomb": { - "name": "Honeycomb", - "x": 928, - "y": 32 - }, - "bee_nest": { - "name": "Bee Nest", - "x": 992, - "y": 32 - }, - "beehive": { - "name": "Beehive", - "x": 960, - "y": 32 - }, - "honey_bottle": { - "name": "Honey Bottle", - "x": 896, - "y": 32 - }, - "honey_block": { - "name": "Honey Block", - "x": 320, - "y": 96 - }, - "honeycomb_block": { - "name": "Honeycomb Block", - "x": 64, - "y": 96 - }, - "lodestone": { - "name": "Lodestone", - "x": 928, - "y": 192 - }, - "netherite_block": { - "name": "Block of Netherite", - "x": 96, - "y": 128 - }, - "ancient_debris": { - "name": "Ancient Debris", - "x": 160, - "y": 128 - }, - "target": { - "name": "Target", - "x": 96, - "y": 192 - }, - "crying_obsidian": { - "name": "Crying Obsidian", - "x": 128, - "y": 192 - }, - "blackstone": { - "name": "Blackstone", - "x": 96, - "y": 256 - }, - "blackstone_slab": { - "name": "Blackstone Slab", - "x": 608, - "y": 256 - }, - "blackstone_stairs": { - "name": "Blackstone Stairs", - "x": 640, - "y": 256 - }, - "gilded_blackstone": { - "name": "Gilded Blackstone", - "x": 512, - "y": 256 - }, - "polished_blackstone": { - "name": "Polished Blackstone", - "x": 544, - "y": 256 - }, - "polished_blackstone_slab": { - "name": "Polished Blackstone Slab", - "x": 800, - "y": 256 - }, - "polished_blackstone_stairs": { - "name": "Polished Blackstone Stairs", - "x": 832, - "y": 256 - }, - "chiseled_polished_blackstone": { - "name": "Chiseled Polished Blackstone", - "x": 416, - "y": 256 - }, - "polished_blackstone_bricks": { - "name": "Polished Blackstone Bricks", - "x": 576, - "y": 256 - }, - "polished_blackstone_brick_slab": { - "name": "Polished Blackstone Brick Slab", - "x": 704, - "y": 256 - }, - "polished_blackstone_brick_stairs": { - "name": "Polished Blackstone Brick Stairs", - "x": 736, - "y": 256 - }, - "cracked_polished_blackstone_bricks": { - "name": "Cracked Polished Blackstone Bricks", - "x": 480, - "y": 256 - }, - "respawn_anchor": { - "name": "Respawn Anchor", - "x": 448, - "y": 192 - } -} \ No newline at end of file diff --git a/src/itemsDescriptions.ts b/src/itemsDescriptions.ts new file mode 100644 index 00000000..662d7331 --- /dev/null +++ b/src/itemsDescriptions.ts @@ -0,0 +1,1290 @@ + +export const descriptionGenerators = new Map string)>() +descriptionGenerators.set(/_slab$/, name => 'Craft it by placing 3 blocks of the material in a row in a crafting table.') +descriptionGenerators.set(/_stairs$/, name => 'Craft it by placing 6 blocks of the material in a stair shape in a crafting table.') +descriptionGenerators.set(/_log$/, name => 'You can get it by chopping down a tree. To chop down a tree, hold down the left mouse button until the tree breaks.') +descriptionGenerators.set(/_leaves$/, name => 'You can get it by breaking the leaves of a tree with a tool that has the Silk Touch enchantment or by using shears.') +descriptionGenerators.set(['mangrove_roots'], name => 'You can get it by breaking the roots of a mangrove tree.') +descriptionGenerators.set(['mud'], 'Mud is a block found abundantly in mangrove swamps or created by using a water bottle on a dirt block. It can be used for crafting or converted into clay using pointed dripstone.') +descriptionGenerators.set(['clay'], 'Clay is a block found underwater or created by using a water bottle on a mud block. It can be used for crafting or converted into terracotta using a furnace.') +descriptionGenerators.set(['terracotta'], 'Terracotta is a block created by smelting clay in a furnace. It can be used for crafting or decoration.') +descriptionGenerators.set(['stone'], 'Stone is a block found underground.') +descriptionGenerators.set(['dirt'], 'Dirt is a block found on the surface.') +descriptionGenerators.set(['sand'], 'Sand is a block found on the surface near water.') +descriptionGenerators.set(['gravel'], 'Gravel is a block found on the surface and sometimes underground.') +descriptionGenerators.set(['sandstone'], 'Sandstone is a block found in deserts.') +descriptionGenerators.set(['red_sandstone'], 'Red sandstone is a block found in mesas.') +descriptionGenerators.set(['granite', 'diorite', 'andesite'], name => `${name.charAt(0).toUpperCase() + name.slice(1)} is a block found underground.`) +descriptionGenerators.set(['netherrack', 'soul_sand', 'soul_soil', 'glowstone'], name => `${name.charAt(0).toUpperCase() + name.slice(1)} is a block found in the Nether.`) +descriptionGenerators.set(['end_stone'], 'End stone is a block found in the End.') +descriptionGenerators.set(['obsidian'], 'Obsidian is a block created by pouring water on lava.') +descriptionGenerators.set(['glass'], 'Glass is a block created by smelting sand in a furnace.') +descriptionGenerators.set(['bedrock'], 'Bedrock is an indestructible block found at the bottom of the world in the Overworld and at the top of the world in the Nether.') +descriptionGenerators.set(['water', 'lava'], name => `${name.charAt(0).toUpperCase() + name.slice(1)} is a fluid found in the Overworld.`) +descriptionGenerators.set(/_sapling$/, name => `${name} drops from the leaves of a tree when it decays or is broken. It can be planted on dirt to grow a new tree.`) +descriptionGenerators.set(/^stripped_/, name => `${name} is created by using an axe on the block.`) +descriptionGenerators.set(['sponge'], 'Sponge is a block found in ocean monuments.') +descriptionGenerators.set(/^music_disc_/, name => `Music discs are rare items that can be found in dungeons or by trading with villagers. Also dropped by creepers when killed by a skeleton.`) +descriptionGenerators.set(/^enchanted_book$/, 'Enchanted books are rare items that can be found in dungeons or by trading with villagers.') +descriptionGenerators.set(/_spawn_egg$/, name => `${name} is an item that can be used to spawn a mob in Creative mode. Cannot be obtained in Survival mode.`) +descriptionGenerators.set(/_pottery_sherd$/, name => `${name} can be obtained only by brushing suspicious blocks, with the variants of sherd obtainable being dependent on the structure.`) +descriptionGenerators.set(['cracked_deepslate_bricks'], `Deepslate Bricks and Cracked Deepslate Bricks generate naturally in ancient cities.`) + +const moreGeneratedBlocks = { + 'natural_blocks': { + 'air': { + 'obtained_from': 'Naturally occurs in the world.' + }, + 'deepslate': { + 'obtained_from': 'Mined with a pickaxe in layers -64 to 16.', + 'rarity': 'Common' + }, + 'cobbled_deepslate': { + 'obtained_from': 'Mined from deepslate with any pickaxe.' + }, + 'calcite': { + 'obtained_from': 'Mined with a pickaxe, found in geodes.' + }, + 'tuff': { + 'obtained_from': 'Mined with a pickaxe in layers -64 to 16.', + 'rarity': 'Common' + }, + 'chiseled_tuff': { + 'obtained_from': 'Crafted from tuff.' + }, + 'polished_tuff': { + 'obtained_from': 'Crafted from tuff.' + }, + 'tuff_bricks': { + 'obtained_from': 'Crafted from tuff.' + }, + 'chiseled_tuff_bricks': { + 'obtained_from': 'Crafted from tuff bricks.' + }, + 'grass_block': { + 'obtained_from': 'Mined with a tool enchanted with Silk Touch.' + }, + 'podzol': { + 'obtained_from': 'Mined with a tool enchanted with Silk Touch, found in giant tree taiga biomes.' + }, + 'rooted_dirt': { + 'obtained_from': 'Mined with a shovel, found under azalea trees.' + }, + 'crimson_nylium': { + 'obtained_from': 'Mined with a pickaxe, found in the Nether.' + }, + 'warped_nylium': { + 'obtained_from': 'Mined with a pickaxe, found in the Nether.' + }, + 'cobblestone': { + 'obtained_from': 'Mined from stone, or from breaking stone structures.' + }, + 'mangrove_propagule': { + 'obtained_from': 'Harvested from mangrove trees.' + }, + 'suspicious_sand': { + 'obtained_from': 'Found in deserts and beaches.' + }, + 'suspicious_gravel': { + 'obtained_from': 'Found underwater.' + }, + 'red_sand': { + 'obtained_from': 'Mined from red sand in badlands biomes.' + }, + 'coal_ore': { + 'obtained_from': 'Mined with a pickaxe in layers 0 to 128.', + 'rarity': 'Common' + }, + 'deepslate_coal_ore': { + 'obtained_from': 'Mined with a pickaxe in layers -64 to 0.', + 'rarity': 'Rare' + }, + 'iron_ore': { + 'obtained_from': 'Mined with a pickaxe in layers 0 to 63.', + 'rarity': 'Common' + }, + 'deepslate_iron_ore': { + 'obtained_from': 'Mined with a pickaxe in layers -64 to 0.', + 'rarity': 'Uncommon' + }, + 'copper_ore': { + 'obtained_from': 'Mined with a pickaxe in layers 0 to 96.', + 'rarity': 'Common' + }, + 'deepslate_copper_ore': { + 'obtained_from': 'Mined with a pickaxe in layers -16 to 64.', + 'rarity': 'Uncommon' + }, + 'gold_ore': { + 'obtained_from': 'Mined with a pickaxe in layers -64 to 32.', + 'rarity': 'Uncommon' + }, + 'deepslate_gold_ore': { + 'obtained_from': 'Mined with a pickaxe in layers -64 to 0.', + 'rarity': 'Rare' + }, + 'redstone_ore': { + 'obtained_from': 'Mined with an iron pickaxe or higher in layers -64 to 16.', + 'rarity': 'Uncommon' + }, + 'deepslate_redstone_ore': { + 'obtained_from': 'Mined with an iron pickaxe or higher in layers -64 to 0.', + 'rarity': 'Uncommon' + }, + 'emerald_ore': { + 'obtained_from': 'Mined with an iron pickaxe or higher in mountain biomes, layers -16 to 256.', + 'rarity': 'Rare' + }, + 'deepslate_emerald_ore': { + 'obtained_from': 'Mined with an iron pickaxe or higher in mountain biomes, layers -64 to 0.', + 'rarity': 'Very Rare' + }, + 'lapis_ore': { + 'obtained_from': 'Mined with a stone pickaxe or higher in layers -64 to 32.', + 'rarity': 'Uncommon' + }, + 'deepslate_lapis_ore': { + 'obtained_from': 'Mined with a stone pickaxe or higher in layers -64 to 0.', + 'rarity': 'Rare' + }, + 'diamond_ore': { + 'obtained_from': 'Mined with an iron pickaxe or higher in layers -64 to 16.', + 'rarity': 'Rare' + }, + 'deepslate_diamond_ore': { + 'obtained_from': 'Mined with an iron pickaxe or higher in layers -64 to 0.', + 'rarity': 'Very Rare' + }, + 'nether_gold_ore': { + 'obtained_from': 'Mined with any pickaxe in the Nether.' + }, + 'nether_quartz_ore': { + 'obtained_from': 'Mined with any pickaxe in the Nether.' + }, + 'ancient_debris': { + 'obtained_from': 'Mined with a diamond or netherite pickaxe in the Nether, layers 8 to 22.', + 'rarity': 'Very Rare' + }, + 'budding_amethyst': { + 'obtained_from': 'Found in amethyst geodes, cannot be obtained as an item.' + }, + 'exposed_copper': { + 'obtained_from': 'Exposed copper block obtained through mining.' + }, + 'weathered_copper': { + 'obtained_from': 'Weathered copper block obtained through mining.' + }, + 'oxidized_copper': { + 'obtained_from': 'Oxidized copper block obtained through mining.' + }, + 'chiseled_copper': { + 'obtained_from': 'Crafted from copper blocks.' + }, + 'exposed_chiseled_copper': { + 'obtained_from': 'Exposed chiseled copper block obtained through mining.' + }, + 'weathered_chiseled_copper': { + 'obtained_from': 'Weathered chiseled copper block obtained through mining.' + }, + 'oxidized_chiseled_copper': { + 'obtained_from': 'Oxidized chiseled copper block obtained through mining.' + }, + 'waxed_chiseled_copper': { + 'obtained_from': 'Crafted from copper blocks, waxed to prevent oxidation.' + }, + 'waxed_exposed_chiseled_copper': { + 'obtained_from': 'Waxed exposed chiseled copper block obtained through mining.' + }, + 'waxed_weathered_chiseled_copper': { + 'obtained_from': 'Waxed weathered chiseled copper block obtained through mining.' + }, + 'waxed_oxidized_chiseled_copper': { + 'obtained_from': 'Waxed oxidized chiseled copper block obtained through mining.' + }, + 'crimson_stem': { + 'obtained_from': 'Mined from crimson trees in the Nether.' + }, + 'warped_stem': { + 'obtained_from': 'Mined from warped trees in the Nether.' + }, + 'stripped_crimson_stem': { + 'obtained_from': 'Stripped from crimson stem with an axe.' + }, + 'stripped_warped_stem': { + 'obtained_from': 'Stripped from warped stem with an axe.' + }, + 'stripped_bamboo_block': { + 'obtained_from': 'Crafted from bamboo.' + }, + 'sponge': { + 'obtained_from': 'Found in ocean monuments.' + }, + 'wet_sponge': { + 'obtained_from': 'Absorbs water, can be dried in a furnace.' + }, + 'cobweb': { + 'obtained_from': 'Mined with a sword or shears, found in mineshafts.' + }, + 'short_grass': { + 'obtained_from': 'Sheared from grass.' + }, + 'fern': { + 'obtained_from': 'Sheared from ferns in forest biomes.' + }, + 'azalea': { + 'obtained_from': 'Found in lush caves.' + }, + 'flowering_azalea': { + 'obtained_from': 'Found in lush caves.' + }, + 'dead_bush': { + 'obtained_from': 'Mined with shears in desert biomes.' + }, + 'seagrass': { + 'obtained_from': 'Sheared from underwater grass.' + }, + 'sea_pickle': { + 'obtained_from': 'Mined with shears from coral reefs.' + }, + 'dandelion': { + 'type': 'natural', + 'description': 'Dandelions are common flowers that spawn in plains, forests, and meadows.', + 'spawn_range': 'Surface' + }, + 'poppy': { + 'type': 'natural', + 'description': 'Poppies are common flowers that generate in plains, forests, and meadows.', + 'spawn_range': 'Surface' + }, + 'blue_orchid': { + 'type': 'natural', + 'description': 'Blue orchids spawn naturally in swamp biomes.', + 'spawn_range': 'Surface' + }, + 'allium': { + 'type': 'natural', + 'description': 'Alliums are flowers that generate in flower forest biomes.', + 'spawn_range': 'Surface' + }, + 'azure_bluet': { + 'type': 'natural', + 'description': 'Azure bluets are common flowers that spawn in plains and flower forest biomes.', + 'spawn_range': 'Surface' + }, + 'red_tulip': { + 'type': 'natural', + 'description': 'Red tulips are flowers found in flower forests and plains.', + 'spawn_range': 'Surface' + }, + 'orange_tulip': { + 'type': 'natural', + 'description': 'Orange tulips are flowers found in flower forests and plains.', + 'spawn_range': 'Surface' + }, + 'white_tulip': { + 'type': 'natural', + 'description': 'White tulips are flowers found in flower forests and plains.', + 'spawn_range': 'Surface' + }, + 'pink_tulip': { + 'type': 'natural', + 'description': 'Pink tulips are flowers found in flower forests and plains.', + 'spawn_range': 'Surface' + }, + 'oxeye_daisy': { + 'type': 'natural', + 'description': 'Oxeye daisies are common flowers that generate in plains and flower forest biomes.', + 'spawn_range': 'Surface' + }, + 'cornflower': { + 'type': 'natural', + 'description': 'Cornflowers spawn in plains, flower forests, and meadows.', + 'spawn_range': 'Surface' + }, + 'lily_of_the_valley': { + 'type': 'natural', + 'description': 'Lily of the valleys generate in flower forest biomes.', + 'spawn_range': 'Surface' + }, + 'wither_rose': { + 'type': 'dropped', + 'description': 'Wither roses are dropped when a mob is killed by the Wither boss.', + 'spawn_range': 'N/A' + }, + 'torchflower': { + 'type': 'crafted', + 'description': 'Torchflowers can be grown using torchflower seeds, which are found in archeology loot or by trading.', + 'spawn_range': 'N/A' + }, + 'pitcher_plant': { + 'type': 'crafted', + 'description': 'Pitcher plants can be grown using pitcher pods, which are found in archeology loot or by trading.', + 'spawn_range': 'N/A' + }, + 'spore_blossom': { + 'type': 'natural', + 'description': 'Spore blossoms generate naturally on the ceilings of lush caves.', + 'spawn_range': 'Underground' + }, + 'brown_mushroom': { + 'type': 'natural', + 'description': 'Brown mushrooms are found in dark areas, swamps, mushroom fields, and forests.', + 'spawn_range': 'Surface' + }, + 'red_mushroom': { + 'type': 'natural', + 'description': 'Red mushrooms are found in dark areas, swamps, mushroom fields, and forests.', + 'spawn_range': 'Surface' + }, + 'crimson_fungus': { + 'type': 'natural', + 'description': 'Crimson fungi generate naturally in crimson forests in the Nether.', + 'spawn_range': 'Nether' + }, + 'warped_fungus': { + 'type': 'natural', + 'description': 'Warped fungi generate naturally in warped forests in the Nether.', + 'spawn_range': 'Nether' + }, + 'crimson_roots': { + 'type': 'natural', + 'description': 'Crimson roots generate naturally in crimson forests in the Nether.', + 'spawn_range': 'Nether' + }, + 'warped_roots': { + 'type': 'natural', + 'description': 'Warped roots generate naturally in warped forests in the Nether.', + 'spawn_range': 'Nether' + }, + 'nether_sprouts': { + 'type': 'natural', + 'description': 'Nether sprouts generate naturally in warped forests in the Nether.', + 'spawn_range': 'Nether' + }, + 'weeping_vines': { + 'type': 'natural', + 'description': 'Weeping vines generate naturally in crimson forests in the Nether and grow downward from netherrack.', + 'spawn_range': 'Nether' + }, + 'twisting_vines': { + 'type': 'natural', + 'description': 'Twisting vines generate naturally in warped forests in the Nether and grow upward from the ground.', + 'spawn_range': 'Nether' + }, + 'sugar_cane': { + 'type': 'natural', + 'description': 'Sugar cane is found near water in most biomes.', + 'spawn_range': 'Surface' + }, + 'kelp': { + 'type': 'natural', + 'description': 'Kelp generates underwater in most ocean biomes.', + 'spawn_range': 'Water' + }, + 'pink_petals': { + 'type': 'natural', + 'description': 'Pink petals generate naturally in cherry grove biomes.', + 'spawn_range': 'Surface' + }, + 'moss_block': { + 'type': 'natural', + 'description': 'Moss blocks generate in lush caves and can also be obtained through trading or by using bone meal on moss carpets.', + 'spawn_range': 'Underground' + }, + 'hanging_roots': { + 'type': 'natural', + 'description': 'Hanging roots generate naturally in lush caves.', + 'spawn_range': 'Underground' + }, + 'big_dripleaf': { + 'type': 'natural', + 'description': 'Big dripleaf plants generate in lush caves and can also be obtained through trading.', + 'spawn_range': 'Underground' + }, + 'small_dripleaf': { + 'type': 'natural', + 'description': 'Small dripleaf plants generate in lush caves and can also be obtained through trading.', + 'spawn_range': 'Underground' + }, + 'bamboo': { + 'type': 'natural', + 'description': 'Bamboo generates in jungle biomes, especially bamboo jungles.', + 'spawn_range': 'Surface' + }, + 'smooth_quartz': { + 'type': 'crafted', + 'description': 'Smooth quartz is obtained by smelting blocks of quartz.', + 'spawn_range': 'N/A' + }, + 'smooth_red_sandstone': { + 'type': 'crafted', + 'description': 'Smooth red sandstone is obtained by smelting red sandstone.', + 'spawn_range': 'N/A' + }, + 'smooth_sandstone': { + 'type': 'crafted', + 'description': 'Smooth sandstone is obtained by smelting sandstone.', + 'spawn_range': 'N/A' + }, + 'smooth_stone': { + 'type': 'crafted', + 'description': 'Smooth stone is obtained by smelting regular stone.', + 'spawn_range': 'N/A' + }, + 'chorus_plant': { + 'type': 'natural', + 'description': 'Chorus plants generate naturally in the End and can be grown from chorus flowers.', + 'spawn_range': 'End' + }, + 'chorus_flower': { + 'type': 'natural', + 'description': 'Chorus flowers generate naturally in the End on top of chorus plants.', + 'spawn_range': 'End' + }, + 'spawner': { + 'type': 'natural', + 'description': 'Spawners generate in dungeons, mineshafts, and other structures.', + 'spawn_range': 'Underground' + }, + 'farmland': { + 'type': 'crafted', + 'description': 'Farmland is created by using a hoe on dirt or grass blocks.', + 'spawn_range': 'N/A' + }, + 'ice': { + 'type': 'natural', + 'description': 'Ice generates in snowy and icy biomes and can also be obtained by breaking ice blocks with a Silk Touch tool.', + 'spawn_range': 'Surface' + }, + 'cactus': { + 'type': 'natural', + 'description': 'Cacti generate naturally in desert biomes.', + 'spawn_range': 'Surface' + }, + 'pumpkin': { + 'type': 'natural', + 'description': 'Pumpkins generate naturally in most grassy biomes and can also be grown from pumpkin seeds.', + 'spawn_range': 'Surface' + }, + 'carved_pumpkin': { + 'type': 'crafted', + 'description': 'Carved pumpkins are obtained by using shears on a pumpkin.', + 'spawn_range': 'N/A' + }, + 'basalt': { + 'type': 'natural', + 'description': 'Basalt generates in the Nether in basalt deltas and can also be created by lava flowing over soul soil next to blue ice.', + 'spawn_range': 'Nether' + }, + 'smooth_basalt': { + 'type': 'natural', + 'description': 'Smooth basalt is found around amethyst geodes or can be obtained by smelting basalt.', + 'spawn_range': 'Underground' + }, + 'infested_stone': { + 'type': 'natural', + 'description': 'Infested stone blocks contain silverfish and generate in strongholds, underground.', + 'spawn_range': 'Underground' + }, + 'infested_cobblestone': { + 'type': 'natural', + 'description': 'Infested cobblestone blocks contain silverfish and generate in strongholds, underground.', + 'spawn_range': 'Underground' + }, + 'infested_stone_bricks': { + 'type': 'natural', + 'description': 'Infested stone bricks contain silverfish and generate in strongholds, underground.', + 'spawn_range': 'Underground' + }, + 'infested_mossy_stone_bricks': { + 'type': 'natural', + 'description': 'Infested mossy stone bricks contain silverfish and generate in strongholds, underground.', + 'spawn_range': 'Underground' + }, + 'infested_cracked_stone_bricks': { + 'type': 'natural', + 'description': 'Infested cracked stone bricks contain silverfish and generate in strongholds, underground.', + 'spawn_range': 'Underground' + }, + 'infested_chiseled_stone_bricks': { + 'type': 'natural', + 'description': 'Infested chiseled stone bricks contain silverfish and generate in strongholds, underground.', + 'spawn_range': 'Underground' + }, + 'infested_deepslate': { + 'type': 'natural', + 'description': 'Infested deepslate contains silverfish and generates in the deepslate layer underground.', + 'spawn_range': 'Underground' + }, + 'cracked_stone_bricks': { + 'type': 'crafted', + 'description': 'Cracked stone bricks are obtained by smelting stone bricks.', + 'spawn_range': 'N/A' + }, + 'cracked_deepslate_bricks': { + 'type': 'crafted', + 'description': 'Cracked deepslate bricks are obtained by smelting deepslate bricks.', + 'spawn_range': 'N/A' + }, + 'cracked_deepslate_tiles': { + 'type': 'crafted', + 'description': 'Cracked deepslate tiles are obtained by smelting deepslate tiles.', + 'spawn_range': 'N/A' + }, + 'reinforced_deepslate': { + 'type': 'crafted', + 'description': 'Reinforced deepslate is a strong block that cannot be obtained in survival mode.', + 'spawn_range': 'N/A' + }, + 'brown_mushroom_block': { + 'type': 'natural', + 'description': 'Brown mushroom blocks generate as part of huge mushrooms in dark forest biomes and mushroom fields.', + 'spawn_range': 'Surface' + }, + 'red_mushroom_block': { + 'type': 'natural', + 'description': 'Red mushroom blocks generate as part of huge mushrooms in dark forest biomes and mushroom fields.', + 'spawn_range': 'Surface' + }, + 'mushroom_stem': { + 'type': 'natural', + 'description': 'Mushroom stems generate as part of huge mushrooms in dark forest biomes and mushroom fields.', + 'spawn_range': 'Surface' + }, + 'vine': { + 'type': 'natural', + 'description': 'Vines generate naturally on trees and walls in jungle biomes, swamps, and lush caves.', + 'spawn_range': 'Surface' + }, + 'glow_lichen': { + 'type': 'natural', + 'description': 'Glow lichen generates naturally in caves and can spread to other blocks using bone meal.', + 'spawn_range': 'Underground' + }, + 'mycelium': { + 'type': 'natural', + 'description': 'Mycelium generates naturally in mushroom field biomes and spreads to dirt blocks.', + 'spawn_range': 'Surface' + }, + 'lily_pad': { + 'type': 'natural', + 'description': 'Lily pads generate naturally on the surface of water in swamps.', + 'spawn_range': 'Water' + }, + 'cracked_nether_bricks': { + 'type': 'crafted', + 'description': 'Cracked nether bricks are obtained by smelting nether bricks.', + 'spawn_range': 'N/A' + }, + 'sculk': { + 'type': 'natural', + 'description': 'Sculk generates naturally in the deep dark biome and spreads using a sculk catalyst.', + 'spawn_range': 'Underground' + }, + 'sculk_vein': { + 'type': 'natural', + 'description': 'Sculk veins generate naturally in the deep dark biome and spread using a sculk catalyst.', + 'spawn_range': 'Underground' + }, + 'sculk_catalyst': { + 'type': 'natural', + 'description': 'Sculk catalysts generate naturally in the deep dark biome and spread sculk blocks when mobs die nearby.', + 'spawn_range': 'Underground' + }, + 'sculk_shrieker': { + 'type': 'natural', + 'description': 'Sculk shriekers generate naturally in the deep dark biome and emit a loud shriek when activated.', + 'spawn_range': 'Underground' + }, + 'end_portal_frame': { + 'type': 'natural', + 'description': 'End portal frames generate naturally in strongholds, forming the structure of end portals.', + 'spawn_range': 'Underground' + }, + 'dragon_egg': { + 'type': 'dropped', + 'description': 'The dragon egg is dropped when the Ender Dragon is defeated for the first time.', + 'spawn_range': 'End' + }, + 'command_block': { + 'type': 'crafted', + 'description': 'Command blocks are powerful blocks used in commands and redstone, obtainable only via commands.', + 'spawn_range': 'N/A' + }, + 'chipped_anvil': { + 'type': 'crafted', + 'description': 'Chipped anvils are damaged versions of anvils and are used for repairing and enchanting.', + 'spawn_range': 'N/A' + }, + 'damaged_anvil': { + 'type': 'crafted', + 'description': 'Damaged anvils are further damaged versions of anvils and are used for repairing and enchanting.', + 'spawn_range': 'N/A' + }, + 'barrier': { + 'type': 'crafted', + 'description': 'Barriers are invisible blocks used in map-making and obtainable only via commands.', + 'spawn_range': 'N/A' + }, + 'light': { + 'type': 'crafted', + 'description': 'Light blocks are invisible blocks that emit light, obtainable only via commands.', + 'spawn_range': 'N/A' + }, + 'dirt_path': { + 'type': 'crafted', + 'description': 'Dirt paths are created by using a shovel on grass blocks and are commonly found in villages.', + 'spawn_range': 'Surface' + }, + 'sunflower': { + 'type': 'natural', + 'description': 'Sunflowers generate naturally in sunflower plains biomes.', + 'spawn_range': 'Surface' + }, + 'lilac': { + 'type': 'natural', + 'description': 'Lilacs generate naturally in forest biomes.', + 'spawn_range': 'Surface' + }, + 'rose_bush': { + 'type': 'natural', + 'description': 'Rose bushes generate naturally in forest biomes.', + 'spawn_range': 'Surface' + }, + 'peony': { + 'type': 'natural', + 'description': 'Peonies generate naturally in forest biomes.', + 'spawn_range': 'Surface' + }, + 'tall_grass': { + 'type': 'natural', + 'description': 'Tall grass generates naturally in various biomes and can be grown using bone meal.', + 'spawn_range': 'Surface' + }, + 'large_fern': { + 'type': 'natural', + 'description': 'Large ferns generate naturally in taiga biomes.', + 'spawn_range': 'Surface' + }, + 'repeating_command_block': { + 'type': 'crafted', + 'description': 'Repeating command blocks execute commands every tick and are obtainable only via commands.', + 'spawn_range': 'N/A' + }, + 'chain_command_block': { + 'type': 'crafted', + 'description': 'Chain command blocks execute commands when triggered and are obtainable only via commands.', + 'spawn_range': 'N/A' + }, + 'warped_wart_block': { + 'type': 'natural', + 'description': 'Warped wart blocks generate naturally in warped forests in the Nether.', + 'spawn_range': 'Nether' + }, + 'structure_void': { + 'type': 'crafted', + 'description': 'Structure voids are used in structure blocks to exclude certain blocks from being saved and are obtainable only via commands.', + 'spawn_range': 'N/A' + }, + 'white_shulker_box': { + 'type': 'crafted', + 'description': 'White shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'orange_shulker_box': { + 'type': 'crafted', + 'description': 'Orange shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'magenta_shulker_box': { + 'type': 'crafted', + 'description': 'Magenta shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'light_blue_shulker_box': { + 'type': 'crafted', + 'description': 'Light blue shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'yellow_shulker_box': { + 'type': 'crafted', + 'description': 'Yellow shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'lime_shulker_box': { + 'type': 'crafted', + 'description': 'Lime shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'pink_shulker_box': { + 'type': 'crafted', + 'description': 'Pink shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'gray_shulker_box': { + 'type': 'crafted', + 'description': 'Gray shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'light_gray_shulker_box': { + 'type': 'crafted', + 'description': 'Light gray shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'cyan_shulker_box': { + 'type': 'crafted', + 'description': 'Cyan shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'purple_shulker_box': { + 'type': 'crafted', + 'description': 'Purple shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'blue_shulker_box': { + 'type': 'crafted', + 'description': 'Blue shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'brown_shulker_box': { + 'type': 'crafted', + 'description': 'Brown shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'green_shulker_box': { + 'type': 'crafted', + 'description': 'Green shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'red_shulker_box': { + 'type': 'crafted', + 'description': 'Red shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'black_shulker_box': { + 'type': 'crafted', + 'description': 'Black shulker boxes are crafted from shulker shells and dye, and they function as portable storage.', + 'spawn_range': 'N/A' + }, + 'white_glazed_terracotta': { + 'type': 'crafted', + 'description': 'White glazed terracotta is obtained by smelting white terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'orange_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Orange glazed terracotta is obtained by smelting orange terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'magenta_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Magenta glazed terracotta is obtained by smelting magenta terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'light_blue_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Light blue glazed terracotta is obtained by smelting light blue terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'yellow_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Yellow glazed terracotta is obtained by smelting yellow terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'lime_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Lime glazed terracotta is obtained by smelting lime terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'pink_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Pink glazed terracotta is obtained by smelting pink terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'gray_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Gray glazed terracotta is obtained by smelting gray terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'light_gray_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Light gray glazed terracotta is obtained by smelting light gray terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'cyan_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Cyan glazed terracotta is obtained by smelting cyan terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'purple_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Purple glazed terracotta is obtained by smelting purple terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'blue_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Blue glazed terracotta is obtained by smelting blue terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'brown_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Brown glazed terracotta is obtained by smelting brown terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'green_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Green glazed terracotta is obtained by smelting green terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'red_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Red glazed terracotta is obtained by smelting red terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'black_glazed_terracotta': { + 'type': 'crafted', + 'description': 'Black glazed terracotta is obtained by smelting black terracotta and features decorative patterns.', + 'spawn_range': 'N/A' + }, + 'white_concrete': { + 'type': 'crafted', + 'description': 'White concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'orange_concrete': { + 'type': 'crafted', + 'description': 'Orange concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'magenta_concrete': { + 'type': 'crafted', + 'description': 'Magenta concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'light_blue_concrete': { + 'type': 'crafted', + 'description': 'Light blue concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'yellow_concrete': { + 'type': 'crafted', + 'description': 'Yellow concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'lime_concrete': { + 'type': 'crafted', + 'description': 'Lime concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'pink_concrete': { + 'type': 'crafted', + 'description': 'Pink concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'gray_concrete': { + 'type': 'crafted', + 'description': 'Gray concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'light_gray_concrete': { + 'type': 'crafted', + 'description': 'Light gray concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'cyan_concrete': { + 'type': 'crafted', + 'description': 'Cyan concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'purple_concrete': { + 'type': 'crafted', + 'description': 'Purple concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'blue_concrete': { + 'type': 'crafted', + 'description': 'Blue concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'brown_concrete': { + 'type': 'crafted', + 'description': 'Brown concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'green_concrete': { + 'type': 'crafted', + 'description': 'Green concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'red_concrete': { + 'type': 'crafted', + 'description': 'Red concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'black_concrete': { + 'type': 'crafted', + 'description': 'Black concrete is crafted from concrete powder and hardens when in contact with water.', + 'spawn_range': 'N/A' + }, + 'white_concrete_powder': { + 'type': 'crafted', + 'description': 'White concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'orange_concrete_powder': { + 'type': 'crafted', + 'description': 'Orange concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'magenta_concrete_powder': { + 'type': 'crafted', + 'description': 'Magenta concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'light_blue_concrete_powder': { + 'type': 'crafted', + 'description': 'Light blue concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'yellow_concrete_powder': { + 'type': 'crafted', + 'description': 'Yellow concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'lime_concrete_powder': { + 'type': 'crafted', + 'description': 'Lime concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'pink_concrete_powder': { + 'type': 'crafted', + 'description': 'Pink concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'gray_concrete_powder': { + 'type': 'crafted', + 'description': 'Gray concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'light_gray_concrete_powder': { + 'type': 'crafted', + 'description': 'Light gray concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'cyan_concrete_powder': { + 'type': 'crafted', + 'description': 'Cyan concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'purple_concrete_powder': { + 'type': 'crafted', + 'description': 'Purple concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'blue_concrete_powder': { + 'type': 'crafted', + 'description': 'Blue concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'brown_concrete_powder': { + 'type': 'crafted', + 'description': 'Brown concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'green_concrete_powder': { + 'type': 'crafted', + 'description': 'Green concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'red_concrete_powder': { + 'type': 'crafted', + 'description': 'Red concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'black_concrete_powder': { + 'type': 'crafted', + 'description': 'Black concrete powder is crafted from sand, gravel, and dye, and hardens into concrete when in contact with water.', + 'spawn_range': 'N/A' + }, + 'cyan_candle': { + 'type': 'crafted', + 'description': 'Cyan candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'pink_candle': { + 'type': 'crafted', + 'description': 'Pink candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'purple_candle': { + 'type': 'crafted', + 'description': 'Purple candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'blue_candle': { + 'type': 'crafted', + 'description': 'Blue candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'brown_candle': { + 'type': 'crafted', + 'description': 'Brown candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'green_candle': { + 'type': 'crafted', + 'description': 'Green candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'red_candle': { + 'type': 'crafted', + 'description': 'Red candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'black_candle': { + 'type': 'crafted', + 'description': 'Black candles are crafted from string and dye and can be placed on blocks to emit light.', + 'spawn_range': 'N/A' + }, + 'turtle_egg': 'can be obtained via turtle breeding on beaches, where turtles lay eggs that can be collected.', + 'sniffer_egg': 'can be found in buried treasure or ancient ruins, used to hatch sniffers.', + 'dead_tube_coral_block': 'can be obtained by mining tube coral blocks with a pickaxe without Silk Touch or when exposed to air.', + 'dead_brain_coral_block': 'can be obtained by mining brain coral blocks with a pickaxe without Silk Touch or when exposed to air.', + 'dead_bubble_coral_block': 'can be obtained by mining bubble coral blocks with a pickaxe without Silk Touch or when exposed to air.', + 'dead_fire_coral_block': 'can be obtained by mining fire coral blocks with a pickaxe without Silk Touch or when exposed to air.', + 'dead_horn_coral_block': 'can be obtained by mining horn coral blocks with a pickaxe without Silk Touch or when exposed to air.', + 'tube_coral_block': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'brain_coral_block': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'bubble_coral_block': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'fire_coral_block': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'horn_coral_block': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'tube_coral': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'brain_coral': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'bubble_coral': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'fire_coral': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'horn_coral': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'dead_brain_coral': 'can be obtained by mining brain coral without Silk Touch or when exposed to air.', + 'dead_bubble_coral': 'can be obtained by mining bubble coral without Silk Touch or when exposed to air.', + 'dead_fire_coral': 'can be obtained by mining fire coral without Silk Touch or when exposed to air.', + 'dead_horn_coral': 'can be obtained by mining horn coral without Silk Touch or when exposed to air.', + 'dead_tube_coral': 'can be obtained by mining tube coral without Silk Touch or when exposed to air.', + 'tube_coral_fan': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'brain_coral_fan': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'bubble_coral_fan': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'fire_coral_fan': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'horn_coral_fan': 'can be obtained by mining with a pickaxe enchanted with Silk Touch, found in warm ocean biomes.', + 'dead_tube_coral_fan': 'can be obtained by mining tube coral fans without Silk Touch or when exposed to air.', + 'dead_brain_coral_fan': 'can be obtained by mining brain coral fans without Silk Touch or when exposed to air.', + 'dead_bubble_coral_fan': 'can be obtained by mining bubble coral fans without Silk Touch or when exposed to air.', + 'dead_fire_coral_fan': 'can be obtained by mining fire coral fans without Silk Touch or when exposed to air.', + 'dead_horn_coral_fan': 'can be obtained by mining horn coral fans without Silk Touch or when exposed to air.', + 'sculk_sensor': 'can be obtained via Silk Touch enchantment on a pickaxe or found in ancient cities in the deep dark biome.', + 'copper_door': 'can be crafted using copper ingots.', + 'exposed_copper_door': 'can be obtained by allowing copper doors to oxidize or can be crafted directly.', + 'weathered_copper_door': 'can be obtained by allowing exposed copper doors to further oxidize or can be crafted directly.', + 'oxidized_copper_door': 'can be obtained by allowing weathered copper doors to fully oxidize or can be crafted directly.', + 'waxed_copper_door': 'can be crafted using copper ingots and honeycomb.', + 'waxed_exposed_copper_door': 'can be crafted using exposed copper doors and honeycomb.', + 'waxed_weathered_copper_door': 'can be crafted using weathered copper doors and honeycomb.', + 'waxed_oxidized_copper_door': 'can be crafted using oxidized copper doors and honeycomb.', + 'copper_trapdoor': 'can be crafted using copper ingots.', + 'exposed_copper_trapdoor': 'can be obtained by allowing copper trapdoors to oxidize or can be crafted directly.', + 'weathered_copper_trapdoor': 'can be obtained by allowing exposed copper trapdoors to further oxidize or can be crafted directly.', + 'oxidized_copper_trapdoor': 'can be obtained by allowing weathered copper trapdoors to fully oxidize or can be crafted directly.', + 'waxed_copper_trapdoor': 'can be crafted using copper ingots and honeycomb.', + 'waxed_exposed_copper_trapdoor': 'can be crafted using exposed copper trapdoors and honeycomb.', + 'waxed_weathered_copper_trapdoor': 'can be crafted using weathered copper trapdoors and honeycomb.', + 'waxed_oxidized_copper_trapdoor': 'can be crafted using oxidized copper trapdoors and honeycomb.', + 'saddle': 'can be obtained from fishing, dungeon chests, or trading with leatherworkers.', + 'elytra': 'can be found in end ships within end cities.', + 'structure_block': 'can be obtained using commands or in creative mode, used to save and load structures.', + 'jigsaw': 'can be obtained using commands or in creative mode, used to generate structures.', + 'scute': 'can be obtained when baby turtles grow into adults.', + 'apple': 'can be obtained by breaking oak and dark oak leaves or found in chests.', + 'charcoal': 'can be obtained by smelting logs or wood in a furnace.', + 'quartz': 'can be obtained by mining nether quartz ore in the Nether.', + 'amethyst_shard': 'can be obtained by mining amethyst clusters found in geodes with a pickaxe.', + 'netherite_scrap': 'can be obtained by smelting ancient debris found in the Nether.', + 'netherite_sword': 'can be crafted using a diamond sword and netherite ingot.', + 'netherite_shovel': 'can be crafted using a diamond shovel and netherite ingot.', + 'netherite_pickaxe': 'can be crafted using a diamond pickaxe and netherite ingot.', + 'netherite_axe': 'can be crafted using a diamond axe and netherite ingot.', + 'netherite_hoe': 'can be crafted using a diamond hoe and netherite ingot.', + 'string': 'can be obtained from killing spiders or breaking cobwebs.', + 'feather': 'can be obtained from killing chickens.', + 'gunpowder': 'can be obtained from killing creepers, ghasts, and witches.', + 'wheat_seeds': 'can be obtained by breaking tall grass or harvesting wheat crops.', + 'chainmail_helmet': 'can be obtained from chest loot, trading with villagers, or killing mobs wearing it.', + 'chainmail_chestplate': 'can be obtained from chest loot, trading with villagers, or killing mobs wearing it.', + 'chainmail_leggings': 'can be obtained from chest loot, trading with villagers, or killing mobs wearing it.', + 'chainmail_boots': 'can be obtained from chest loot, trading with villagers, or killing mobs wearing it.', + 'netherite_helmet': 'can be crafted using a diamond helmet and netherite ingot.', + 'netherite_chestplate': 'can be crafted using a diamond chestplate and netherite ingot.', + 'netherite_leggings': 'can be crafted using diamond leggings and netherite ingot.', + 'netherite_boots': 'can be crafted using diamond boots and netherite ingot.', + 'flint': 'can be obtained by breaking gravel blocks.', + 'porkchop': 'can be obtained by killing pigs.', + 'cooked_porkchop': 'can be obtained by cooking porkchop in a furnace, smoker, or campfire.', + 'enchanted_golden_apple': 'can be found in dungeon, bastion remnant, and mineshaft chests.', + 'water_bucket': 'can be obtained by using a bucket on a water source block.', + 'lava_bucket': 'can be obtained by using a bucket on a lava source block.', + 'powder_snow_bucket': 'can be obtained by using a bucket on powder snow.', + 'snowball': 'can be obtained by breaking snow blocks or using a shovel on snow.', + 'milk_bucket': 'can be obtained by using a bucket on a cow or mooshroom.', + 'pufferfish_bucket': 'can be obtained by using a bucket on a pufferfish in water.', + 'salmon_bucket': 'can be obtained by using a bucket on a salmon in water.', + 'cod_bucket': 'can be obtained by using a bucket on a cod in water.', + 'tropical_fish_bucket': 'can be obtained by using a bucket on a tropical fish in water.', + 'axolotl_bucket': 'can be obtained by using a bucket on an axolotl in water.', + 'tadpole_bucket': 'can be obtained by using a bucket on a tadpole in water.', + 'brick': 'can be obtained by smelting clay in a furnace.', + 'clay_ball': 'can be obtained by breaking clay blocks or from chest loot.', + 'egg': 'can be obtained from chickens periodically.', + 'bundle': 'can be crafted using rabbit hide and string.', + 'glowstone_dust': 'can be obtained by breaking glowstone blocks or killing witches.', + 'cod': 'can be obtained by fishing or killing cod in water.', + 'salmon': 'can be obtained by fishing or killing salmon in water.', + 'tropical_fish': 'can be obtained by fishing or killing tropical fish in water.', + 'pufferfish': 'can be obtained by fishing or killing pufferfish in water.', + 'cooked_cod': 'can be obtained by cooking cod in a furnace, smoker, or campfire.', + 'cooked_salmon': 'can be obtained by cooking salmon in a furnace, smoker, or campfire.', + 'ink_sac': 'can be obtained by killing squid or as loot from wandering traders.', + 'glow_ink_sac': 'can be obtained by killing glow squid.', + 'cocoa_beans': 'can be obtained from cocoa pods found on jungle trees.', + 'green_dye': 'can be obtained by smelting cactus in a furnace.', + 'bone': 'can be obtained by killing skeletons or from chest loot.', + 'crafter': 'can be obtained via crafting using specific materials (details vary by mod or version).', + 'filled_map': 'can be obtained by using an empty map item.', + 'melon_slice': 'can be obtained by breaking melon blocks.', + 'beef': 'can be obtained by killing cows.', + 'cooked_beef': 'can be obtained by cooking beef in a furnace, smoker, or campfire.', + 'chicken': 'can be obtained by killing chickens.', + 'cooked_chicken': 'can be obtained by cooking chicken in a furnace, smoker, or campfire.', + 'rotten_flesh': 'can be obtained by killing zombies or drowned.', + 'ender_pearl': 'can be obtained by killing endermen.', + 'blaze_rod': 'can be obtained by killing blazes in the Nether.', + 'ghast_tear': 'can be obtained by killing ghasts in the Nether.', + 'nether_wart': 'can be found in Nether fortresses and bastion remnants.', + 'potion': 'can be brewed using a brewing stand with various ingredients.', + 'spider_eye': 'can be obtained by killing spiders or witches.', + 'experience_bottle': 'can be obtained from trading with villagers or found in chest loot.', + 'written_book': 'can be crafted using a book and quill after writing in it.', + 'carrot': 'can be obtained by harvesting carrot crops or found in village farms.', + 'potato': 'can be obtained by harvesting potato crops or found in village farms.', + 'baked_potato': 'can be obtained by cooking potatoes in a furnace, smoker, or campfire.', + 'poisonous_potato': 'can be obtained by harvesting potato crops (rare chance).', + 'skeleton_skull': 'can be obtained by killing skeletons with a charged creeper explosion.', + 'wither_skeleton_skull': 'can be obtained by killing wither skeletons (rare drop).', + 'player_head': 'can be obtained via commands or by killing players in certain conditions (e.g., with a charged creeper).', + 'zombie_head': 'can be obtained by killing zombies with a charged creeper explosion.', + 'creeper_head': 'can be obtained by killing creepers with a charged creeper explosion.', + 'dragon_head': 'can be found at the end of end ships in end cities.', + 'piglin_head': 'can be obtained by killing piglins with a charged creeper explosion.', + 'nether_star': 'can be obtained by defeating the Wither boss.', + 'firework_star': 'can be crafted using gunpowder and dye.', + 'nether_brick': 'can be obtained by smelting netherrack in a furnace or found in Nether fortresses.', + 'prismarine_shard': 'can be obtained by killing guardians and elder guardians.', + 'prismarine_crystals': 'can be obtained by killing guardians and elder guardians or breaking sea lanterns.', + 'rabbit': 'can be obtained by killing rabbits.', + 'cooked_rabbit': 'can be obtained by cooking rabbit in a furnace, smoker, or campfire.', + 'rabbit_foot': 'can be obtained by killing rabbits (rare drop).', + 'rabbit_hide': 'can be obtained by killing rabbits.', + 'iron_horse_armor': 'can be found in dungeon, temple, and stronghold chests.', + 'golden_horse_armor': 'can be found in dungeon, temple, and stronghold chests.', + 'diamond_horse_armor': 'can be found in dungeon, temple, and stronghold chests.', + 'name_tag': 'can be obtained by fishing, dungeon chests, or trading with librarians.', + 'command_block_minecart': 'can be obtained using commands in creative mode.', + 'mutton': 'can be obtained by killing sheep.', + 'cooked_mutton': 'can be obtained by cooking mutton in a furnace, smoker, or campfire.', + 'chorus_fruit': 'can be obtained by breaking chorus plants found in the End.', + 'popped_chorus_fruit': 'can be obtained by smelting chorus fruit in a furnace.', + 'torchflower_seeds': 'can be obtained from torchflower plants, used for breeding and decoration.', + 'pitcher_pod': 'can be obtained from pitcher plants, used for breeding and decoration.', + 'beetroot': 'can be obtained by harvesting beetroot crops or found in village farms.', + 'beetroot_seeds': 'can be obtained by harvesting beetroot crops or from chests.', + 'dragon_breath': 'can be obtained by using an empty bottle on the ender dragon\'s breath attack.', + 'splash_potion': 'can be brewed using a brewing stand and gunpowder with various potions.', + 'tipped_arrow': 'can be crafted using arrows and lingering potions.', + 'lingering_potion': 'can be brewed using a brewing stand and dragon\'s breath with various potions.', + 'totem_of_undying': 'can be obtained by killing evokers in woodland mansions and during raids.', + 'shulker_shell': 'can be obtained by killing shulkers in end cities.', + 'knowledge_book': 'can be obtained using commands or given in custom advancements.', + 'debug_stick': 'can be obtained using commands in creative mode.', + 'disc_fragment_5': 'can be found in ancient city chests, used to craft music disc 5.', + 'trident': 'can be obtained by killing drowned (rare drop).', + 'phantom_membrane': 'can be obtained by killing phantoms.', + 'nautilus_shell': 'can be obtained from fishing, drowned, or wandering traders.', + 'heart_of_the_sea': 'can be found in buried treasure chests.', + 'suspicious_stew': 'can be crafted using mushrooms and various flowers or found in chests.', + 'globe_banner_pattern': 'can be obtained from trading with cartographer villagers.', + 'piglin_banner_pattern': 'can be obtained from bastion remnant chests.', + 'goat_horn': 'can be obtained when a goat rams a solid block.', + 'bell': 'can be obtained from village structures or crafted using iron ingots and wood.', + 'sweet_berries': 'can be obtained from sweet berry bushes found in taiga biomes.', + 'glow_berries': 'can be found in lush cave biomes or by trading with wandering traders.', + 'shroomlight': 'can be obtained by breaking shroomlights found in Nether forests.', + 'honeycomb': 'can be obtained by using shears on beehives or bee nests.', + 'bee_nest': 'can be found in forest biomes with birch or oak trees, especially in flower forests.', + 'crying_obsidian': 'can be found in ruined portals, bastion remnants, or bartered from piglins.', + 'blackstone': 'can be found in basalt deltas, bastion remnants, or crafted from polished blackstone.', + 'gilded_blackstone': 'can be found in bastion remnants.', + 'cracked_polished_blackstone_bricks': 'can be obtained by smelting polished blackstone bricks.', + 'small_amethyst_bud': 'can be found growing in amethyst geodes.', + 'medium_amethyst_bud': 'can be found growing in amethyst geodes.', + 'large_amethyst_bud': 'can be found growing in amethyst geodes.', + 'amethyst_cluster': 'can be found growing in amethyst geodes.', + 'pointed_dripstone': 'can be found in dripstone caves or created by placing a dripstone block under a water source block.', + 'ochre_froglight': 'can be obtained by leading a frog to eat a magma cube, dropping this item.', + 'verdant_froglight': 'can be obtained by leading a frog to eat a magma cube, dropping this item.', + 'pearlescent_froglight': 'can be obtained by leading a frog to eat a magma cube, dropping this item.', + 'frogspawn': 'Frogspawn is an item that can be found in the game Minecraft and is primarily used to breed frogs.', + 'echo_shard': 'Echo Shard is an item in Minecraft Dungeons, primarily used as a currency for trading with Piglin vendors.', + 'copper_grate': 'Copper Grate is a block in Minecraft that can be crafted from copper ingots, primarily used as a decorative block.', + 'exposed_copper_grate': 'Exposed Copper Grate is a variant of Copper Grate in Minecraft that has weathered to the exposed state over time.', + 'weathered_copper_grate': 'Weathered Copper Grate is a variant of Copper Grate in Minecraft that has weathered to the weathered state over time.', + 'oxidized_copper_grate': 'Oxidized Copper Grate is a variant of Copper Grate in Minecraft that has weathered to the oxidized state over time.', + 'waxed_copper_grate': 'Waxed Copper Grate is a variant of Copper Grate in Minecraft that has been waxed to prevent further weathering.', + 'waxed_exposed_copper_grate': 'Waxed Exposed Copper Grate is a variant of Exposed Copper Grate in Minecraft that has been waxed to prevent further weathering.', + 'waxed_weathered_copper_grate': 'Waxed Weathered Copper Grate is a variant of Weathered Copper Grate in Minecraft that has been waxed to prevent further weathering.', + 'waxed_oxidized_copper_grate': 'Waxed Oxidized Copper Grate is a variant of Oxidized Copper Grate in Minecraft that has been waxed to prevent further weathering.', + 'copper_bulb': 'Copper Bulb is a block in Minecraft that can be crafted from copper ingots, primarily used as a decorative block.', + 'exposed_copper_bulb': 'Exposed Copper Bulb is a variant of Copper Bulb in Minecraft that has weathered to the exposed state over time.', + 'weathered_copper_bulb': 'Weathered Copper Bulb is a variant of Copper Bulb in Minecraft that has weathered to the weathered state over time.', + 'oxidized_copper_bulb': 'Oxidized Copper Bulb is a variant of Copper Bulb in Minecraft that has weathered to the oxidized state over time.', + 'waxed_copper_bulb': 'Waxed Copper Bulb is a variant of Copper Bulb in Minecraft that has been waxed to prevent further weathering.', + 'waxed_exposed_copper_bulb': 'Waxed Exposed Copper Bulb is a variant of Exposed Copper Bulb in Minecraft that has been waxed to prevent further weathering.', + 'waxed_weathered_copper_bulb': 'Waxed Weathered Copper Bulb is a variant of Weathered Copper Bulb in Minecraft that has been waxed to prevent further weathering.', + 'waxed_oxidized_copper_bulb': 'Waxed Oxidized Copper Bulb is a variant of Oxidized Copper Bulb in Minecraft that has been waxed to prevent further weathering.', + 'trial_spawner': 'Trial Spawner is an item in Minecraft Dungeons, used in the Ancient Hunt game mode to summon trials for unique rewards.', + 'trial_key': 'Trial Key is an item in Minecraft Dungeons, obtained from defeating Ancient mobs in the Ancient Hunt game mode, used to unlock trials.' + } +} + +const lowerCaseFirstLetter = (string) => string.charAt(0).toLowerCase() + string.slice(1) +for (const [name, data] of Object.entries(moreGeneratedBlocks.natural_blocks)) { + let description = '' as string | ((name: string) => string) + if (typeof data === 'object') { + const obtainedFrom = 'obtained_from' in data ? data.obtained_from : 'description' in data ? data.description : '' + description = obtainedFrom + ('rarity' in data ? ` Rarity: ${data.rarity}` : '') + ('spawn_range' in data ? ` Spawn range: ${data.spawn_range}` : '') + } else { + description = (name) => `${lowerCaseFirstLetter(name)}: ${data}` + } + descriptionGenerators.set([name], description) +} + +export const getItemDescription = (item: import('prismarine-item').Item) => { + const { name } = item + let result: string | ((name: string) => string) = '' + for (const [names, description] of descriptionGenerators) { + if (Array.isArray(names) && names.includes(name)) { + result = description + } + if (typeof names === 'string' && names === name) { + result = description + } + if (names instanceof RegExp && names.test(name)) { + result = description + } + } + return typeof result === 'function' ? result(item.displayName) : result +} diff --git a/src/loadSave.ts b/src/loadSave.ts index d8531344..f1676cff 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -1,14 +1,18 @@ import fs from 'fs' -import { supportedVersions } from 'flying-squid/dist/lib/version' +import path from 'path' import * as nbt from 'prismarine-nbt' import { proxy } from 'valtio' import { gzip } from 'node-gzip' +import { versionToNumber } from 'renderer/viewer/common/utils' import { options } from './optionsStorage' import { nameToMcOfflineUUID, disconnect } from './flyingSquidUtils' -import { forceCachedDataPaths } from './browserfs' +import { existsViaStats, forceCachedDataPaths, forceRedirectPaths, mkdirRecursive } from './browserfs' import { isMajorVersionGreater } from './utils' import { activeModalStacks, insertActiveModalStack, miscUiState } from './globalState' +import supportedVersions from './supportedVersions.mjs' +import { ConnectOptions } from './connect' +import { appQueryParams } from './appParams' // todo include name of opened handle (zip)! // additional fs metadata @@ -16,7 +20,11 @@ export const fsState = proxy({ isReadonly: false, syncFs: false, inMemorySave: false, - saveLoaded: false + saveLoaded: false, + openReadOperations: 0, + openWriteOperations: 0, + remoteBackend: false, + inMemorySavePath: '' }) const PROPOSE_BACKUP = true @@ -42,15 +50,28 @@ export const readLevelDat = async (path) => { return { levelDat, dataRaw: parsed.value.Data!.value as Record } } -export const loadSave = async (root = '/world') => { +export const loadSave = async (root = '/world', connectOptions?: Partial) => { + // todo test + if (miscUiState.gameLoaded) { + await disconnect() + await new Promise(resolve => { + setTimeout(resolve) + }) + } + const disablePrompts = options.disableLoadPrompts // todo do it in singleplayer as well // eslint-disable-next-line guard-for-in for (const key in forceCachedDataPaths) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete forceCachedDataPaths[key] } + // eslint-disable-next-line guard-for-in + for (const key in forceRedirectPaths) { + + delete forceRedirectPaths[key] + } // todo check jsHeapSizeLimit const warnings: string[] = [] @@ -64,38 +85,23 @@ export const loadSave = async (root = '/world') => { } let version: string | undefined | null - let isFlat = false if (levelDat) { - const qs = new URLSearchParams(window.location.search) - version = qs.get('mapVersion') ?? levelDat.Version?.Name + version = appQueryParams.mapVersion ?? levelDat.Version?.Name if (!version) { - const newVersion = disablePrompts ? '1.8.8' : prompt(`In 1.8 and before world save doesn't contain version info, please enter version you want to use to load the world.\nSupported versions ${supportedVersions.join(', ')}`, '1.8.8') - if (!newVersion) return + // const newVersion = disablePrompts ? '1.8.8' : prompt(`In 1.8 and before world save doesn't contain version info, please enter version you want to use to load the world.\nSupported versions ${supportedVersions.join(', ')}`, '1.8.8') + // if (!newVersion) return + // todo detect world load issues + const newVersion = '1.8.8' version = newVersion } const lastSupportedVersion = supportedVersions.at(-1)! const firstSupportedVersion = supportedVersions[0] const lowerBound = isMajorVersionGreater(firstSupportedVersion, version) - const upperBound = isMajorVersionGreater(version, lastSupportedVersion) + const upperBound = versionToNumber(version) > versionToNumber(lastSupportedVersion) if (lowerBound || upperBound) { version = prompt(`Version ${version} is not supported, supported versions are ${supportedVersions.join(', ')}, what try to use instead?`, lowerBound ? firstSupportedVersion : lastSupportedVersion) if (!version) return } - if (levelDat.WorldGenSettings) { - for (const [key, value] of Object.entries(levelDat.WorldGenSettings.dimensions)) { - if (key.slice(10) === 'overworld') { - if (value.generator.type === 'flat') isFlat = true - break - } - } - } - - if (levelDat.generatorName) { - isFlat = levelDat.generatorName === 'flat' - } - if (!isFlat && levelDat.generatorName !== 'default' && levelDat.generatorName !== 'customized') { - warnings.push(`Generator ${levelDat.generatorName} may not be supported yet`) - } const playerUuid = nameToMcOfflineUUID(options.localUsername) const playerDatPath = `${root}/playerdata/${playerUuid}.dat` @@ -105,6 +111,7 @@ export const loadSave = async (root = '/world') => { if (fsState.isReadonly) { forceCachedDataPaths[playerDatPath] = playerDat } else { + await mkdirRecursive(path.dirname(playerDatPath)) await fs.promises.writeFile(playerDatPath, playerDat) } } @@ -133,13 +140,18 @@ export const loadSave = async (root = '/world') => { if (!fsState.isReadonly && !fsState.inMemorySave && !disablePrompts) { // todo allow also to ctrl+s - alert('Note: the world is saved only on /save or disconnect! Ensure you have backup!') + alert('Note: the world is saved on interval, /save or disconnect! Ensure you have backup and be careful of new chunks writes!') } - // todo fix these - if (miscUiState.gameLoaded) { - await disconnect() + // improve compatibility with community saves + const rootRemapFiles = ['Warp files'] + for (const rootRemapFile of rootRemapFiles) { + // eslint-disable-next-line no-await-in-loop + if (await existsViaStats(path.join(root, '..', rootRemapFile))) { + forceRedirectPaths[path.join(root, rootRemapFile)] = path.join(root, '..', rootRemapFile) + } } + // todo reimplement if (activeModalStacks['main-menu']) { insertActiveModalStack('main-menu') @@ -153,19 +165,17 @@ export const loadSave = async (root = '/world') => { // hideModal(undefined, undefined, { force: true }) // } + // todo should not be set here fsState.saveLoaded = true + fsState.inMemorySavePath = root window.dispatchEvent(new CustomEvent('singleplayer', { // todo check gamemode level.dat data etc detail: { version, - ...isFlat ? { - generation: { - name: 'superflat' - } - } : {}, ...root === '/world' ? {} : { 'worldFolder': root - } + }, + connectOptions }, })) } diff --git a/src/localServerMultiplayer.ts b/src/localServerMultiplayer.ts index c7c2cd28..7eaa2427 100644 --- a/src/localServerMultiplayer.ts +++ b/src/localServerMultiplayer.ts @@ -1,7 +1,8 @@ import { Duplex } from 'stream' -import Peer, { DataConnection } from 'peerjs' +import { Peer, DataConnection } from 'peerjs' import Client from 'minecraft-protocol/src/client' -import { resolveTimeout, setLoadingScreenStatus } from './utils' +import { resolveTimeout } from './utils' +import { setLoadingScreenStatus } from './appStatus' import { miscUiState } from './globalState' class CustomDuplex extends Duplex { @@ -19,6 +20,8 @@ class CustomDuplex extends Duplex { let peerInstance: Peer | undefined +let overridePeerJsServer = null as string | null + export const getJoinLink = () => { if (!peerInstance) return const url = new URL(window.location.href) @@ -27,6 +30,11 @@ export const getJoinLink = () => { } url.searchParams.set('connectPeer', peerInstance.id) url.searchParams.set('peerVersion', localServer!.options.version) + const host = (overridePeerJsServer ?? miscUiState.appConfig?.peerJsServer) ?? undefined + if (host) { + // TODO! use miscUiState.appConfig.peerJsServer + url.searchParams.set('server', host) + } return url.toString() } @@ -46,13 +54,18 @@ export const openToWanAndCopyJoinLink = async (writeText: (text) => void, doCopy if (doCopy) await copyJoinLink() return 'Already opened to wan. Join link copied' } + miscUiState.wanOpening = true + const host = (overridePeerJsServer ?? miscUiState.appConfig?.peerJsServer) || undefined + const params = host ? parseUrl(host) : undefined const peer = new Peer({ debug: 3, + secure: true, + ...params }) peerInstance = peer peer.on('connection', (connection) => { console.log('connection') - const serverDuplex = new CustomDuplex({}, (data) => connection.send(data)) + const serverDuplex = new CustomDuplex({}, async (data) => connection.send(data)) const client = new Client(true, localServer.options.version, undefined) client.setSocket(serverDuplex) localServer._server.emit('connection', client) @@ -83,34 +96,98 @@ export const openToWanAndCopyJoinLink = async (writeText: (text) => void, doCopy connection.on('close', disconnected) connection.on('error', disconnected) }) + const fallbackServer = miscUiState.appConfig?.peerJsServerFallback + const hasFallback = fallbackServer && peer.options.host !== fallbackServer + let hadErrorReported = false peer.on('error', (error) => { - console.error(error) - writeText(error.message) + console.error('peerJS error', error) + if (error.type === 'server-error' && hasFallback) { + return + } + hadErrorReported = true + writeText(error.message || JSON.stringify(error)) }) - return new Promise(resolve => { + let timeout + const destroy = () => { + clearTimeout(timeout) + timeout = undefined + peer.destroy() + peerInstance = undefined + } + + const result = await new Promise(resolve => { peer.on('open', async () => { await copyJoinLink() resolve('Copied join link to clipboard') }) - setTimeout(() => { - resolve('Failed to open to wan (timeout)') - }, 5000) + timeout = setTimeout(async () => { + if (!hadErrorReported && timeout !== undefined) { + if (hasFallback && overridePeerJsServer === null) { + destroy() + overridePeerJsServer = fallbackServer + console.log('Trying fallback server due to timeout', fallbackServer) + resolve((await openToWanAndCopyJoinLink(writeText, doCopy))!) + } else { + writeText('timeout') + resolve('Failed to open to wan (timeout)') + } + } + }, 6000) + + // fallback + peer.on('error', async (error) => { + if (!peer.open) { + if (hasFallback) { + destroy() + + overridePeerJsServer = fallbackServer + console.log('Trying fallback server', fallbackServer) + resolve((await openToWanAndCopyJoinLink(writeText, doCopy))!) + } + } + }) }) + if (peerInstance && !peerInstance.open) { + destroy() + } + miscUiState.wanOpening = false + return result +} + +const parseUrl = (url: string) => { + // peerJS does this internally for some reason: const url = new URL(`${protocol}://${host}:${port}${path}${key}/${method}`) + if (!url.startsWith('http')) url = `${location.protocol}//${url}` + const urlObj = new URL(url) + const key = urlObj.searchParams.get('key') + return { + host: urlObj.hostname, + path: urlObj.pathname, + protocol: urlObj.protocol.slice(0, -1), + ...urlObj.port ? { port: +urlObj.port } : {}, + ...key ? { key } : {}, + } } export const closeWan = () => { - if (!peerInstance) return - peerInstance.destroy() + peerInstance?.destroy() peerInstance = undefined miscUiState.wanOpened = false - return 'Closed to wan' + return 'Closed WAN' } -export const connectToPeer = async (peerId: string) => { +export type ConnectPeerOptions = { + server?: string +} + +export const connectToPeer = async (peerId: string, options: ConnectPeerOptions = {}) => { setLoadingScreenStatus('Connecting to peer server') // todo destroy connection on error + // TODO! use miscUiState.appConfig.peerJsServer + const host = options.server + const params = host ? parseUrl(host) : undefined const peer = new Peer({ debug: 3, + ...params }) await resolveTimeout(new Promise(resolve => { peer.once('open', resolve) @@ -129,9 +206,9 @@ export const connectToPeer = async (peerId: string) => { })) const clientDuplex = new CustomDuplex({}, (data) => { - // todo rm debug - console.debug('sending', data.toString()) - connection.send(data) + // todo debug until play state + // console.debug('sending', data.toString()) + void connection.send(data) }) connection.on('data', (data: any) => { console.debug('received', Buffer.from(data).toString()) diff --git a/src/markdownToFormattedText.test.ts b/src/markdownToFormattedText.test.ts new file mode 100644 index 00000000..5c1d5f35 --- /dev/null +++ b/src/markdownToFormattedText.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import markdownToFormattedText from './markdownToFormattedText' + +describe('markdownToFormattedText', () => { + it('should convert markdown to formatted text', () => { + const markdown = '**bold** *italic* [link](https://example.com) k `code`' + const text = markdownToFormattedText(markdown) + const command = '/data merge block ~ ~ ~ {Text1:\'' + JSON.stringify(text[0]) + '\',Text2: \'' + JSON.stringify(text[1]) + '\',Text3:\'' + JSON.stringify(text[2]) + '\',Text4:\'' + JSON.stringify(text[3]) + '\'}' // mojangson + expect(text).toMatchInlineSnapshot(` + [ + [ + [ + { + "bold": true, + "text": "bold", + }, + { + "text": " ", + }, + { + "italic": true, + "text": "italic", + }, + { + "text": " ", + }, + { + "text": " k ", + }, + "code", + ], + ], + "", + "", + "", + ] + `) + }) +}) diff --git a/src/markdownToFormattedText.ts b/src/markdownToFormattedText.ts new file mode 100644 index 00000000..5fff20f8 --- /dev/null +++ b/src/markdownToFormattedText.ts @@ -0,0 +1,58 @@ +import { remark } from 'remark' + +export default (markdown: string) => { + const arr = markdown.split('\n\n') + const lines = ['', '', '', ''] as any[] + for (const [i, ast] of arr.map(md => remark().parse(md)).entries()) { + lines[i] = transformToMinecraftJSON(ast as Element) + } + return lines +} + +function transformToMinecraftJSON (element: Element): any { + switch (element.type) { + case 'root': { + if (!element.children) return + return element.children.map(child => transformToMinecraftJSON(child)).filter(Boolean) + } + case 'paragraph': { + if (!element.children) return + const transformedChildren = element.children.map(child => transformToMinecraftJSON(child)).filter(Boolean) + return transformedChildren.flat() + } + case 'strong': { + if (!element.children) return + return [{ bold: true, text: element.children[0].value }] + } + case 'text': { + return { text: element.value } + } + case 'emphasis': { + if (!element.children) return + return [{ italic: true, text: element.children[0].value }] + } + default: + // todo leave untouched eg links + return element.value + } +} + +interface Position { + start: { + line: number; + column: number; + offset: number; + }; + end: { + line: number; + column: number; + offset: number; + }; +} + +interface Element { + type: string; + children?: Element[]; + value?: string; + position: Position; +} diff --git a/src/mcDataTypes.ts b/src/mcDataTypes.ts new file mode 100644 index 00000000..3375cf28 --- /dev/null +++ b/src/mcDataTypes.ts @@ -0,0 +1,4318 @@ + +export type BlockNames = 'air' | 'stone' | 'granite' | 'polished_granite' | 'diorite' | 'polished_diorite' | 'andesite' | 'polished_andesite' | 'grass_block' | 'dirt' | 'coarse_dirt' | 'podzol' | 'cobblestone' | 'oak_planks' | 'spruce_planks' | 'birch_planks' | 'jungle_planks' | 'acacia_planks' | 'cherry_planks' | 'dark_oak_planks' | 'mangrove_planks' | 'bamboo_planks' | 'bamboo_mosaic' | 'oak_sapling' | 'spruce_sapling' | 'birch_sapling' | 'jungle_sapling' | 'acacia_sapling' | 'cherry_sapling' | 'dark_oak_sapling' | 'mangrove_propagule' | 'bedrock' | 'water' | 'lava' | 'sand' | 'suspicious_sand' | 'red_sand' | 'gravel' | 'suspicious_gravel' | 'gold_ore' | 'deepslate_gold_ore' | 'iron_ore' | 'deepslate_iron_ore' | 'coal_ore' | 'deepslate_coal_ore' | 'nether_gold_ore' | 'oak_log' | 'spruce_log' | 'birch_log' | 'jungle_log' | 'acacia_log' | 'cherry_log' | 'dark_oak_log' | 'mangrove_log' | 'mangrove_roots' | 'muddy_mangrove_roots' | 'bamboo_block' | 'stripped_spruce_log' | 'stripped_birch_log' | 'stripped_jungle_log' | 'stripped_acacia_log' | 'stripped_cherry_log' | 'stripped_dark_oak_log' | 'stripped_oak_log' | 'stripped_mangrove_log' | 'stripped_bamboo_block' | 'oak_wood' | 'spruce_wood' | 'birch_wood' | 'jungle_wood' | 'acacia_wood' | 'cherry_wood' | 'dark_oak_wood' | 'mangrove_wood' | 'stripped_oak_wood' | 'stripped_spruce_wood' | 'stripped_birch_wood' | 'stripped_jungle_wood' | 'stripped_acacia_wood' | 'stripped_cherry_wood' | 'stripped_dark_oak_wood' | 'stripped_mangrove_wood' | 'oak_leaves' | 'spruce_leaves' | 'birch_leaves' | 'jungle_leaves' | 'acacia_leaves' | 'cherry_leaves' | 'dark_oak_leaves' | 'mangrove_leaves' | 'azalea_leaves' | 'flowering_azalea_leaves' | 'sponge' | 'wet_sponge' | 'glass' | 'lapis_ore' | 'deepslate_lapis_ore' | 'lapis_block' | 'dispenser' | 'sandstone' | 'chiseled_sandstone' | 'cut_sandstone' | 'note_block' | 'white_bed' | 'orange_bed' | 'magenta_bed' | 'light_blue_bed' | 'yellow_bed' | 'lime_bed' | 'pink_bed' | 'gray_bed' | 'light_gray_bed' | 'cyan_bed' | 'purple_bed' | 'blue_bed' | 'brown_bed' | 'green_bed' | 'red_bed' | 'black_bed' | 'powered_rail' | 'detector_rail' | 'sticky_piston' | 'cobweb' | 'grass' | 'fern' | 'dead_bush' | 'seagrass' | 'tall_seagrass' | 'piston' | 'piston_head' | 'white_wool' | 'orange_wool' | 'magenta_wool' | 'light_blue_wool' | 'yellow_wool' | 'lime_wool' | 'pink_wool' | 'gray_wool' | 'light_gray_wool' | 'cyan_wool' | 'purple_wool' | 'blue_wool' | 'brown_wool' | 'green_wool' | 'red_wool' | 'black_wool' | 'moving_piston' | 'dandelion' | 'torchflower' | 'poppy' | 'blue_orchid' | 'allium' | 'azure_bluet' | 'red_tulip' | 'orange_tulip' | 'white_tulip' | 'pink_tulip' | 'oxeye_daisy' | 'cornflower' | 'wither_rose' | 'lily_of_the_valley' | 'brown_mushroom' | 'red_mushroom' | 'gold_block' | 'iron_block' | 'bricks' | 'tnt' | 'bookshelf' | 'chiseled_bookshelf' | 'mossy_cobblestone' | 'obsidian' | 'torch' | 'wall_torch' | 'fire' | 'soul_fire' | 'spawner' | 'oak_stairs' | 'chest' | 'redstone_wire' | 'diamond_ore' | 'deepslate_diamond_ore' | 'diamond_block' | 'crafting_table' | 'wheat' | 'farmland' | 'furnace' | 'oak_sign' | 'spruce_sign' | 'birch_sign' | 'acacia_sign' | 'cherry_sign' | 'jungle_sign' | 'dark_oak_sign' | 'mangrove_sign' | 'bamboo_sign' | 'oak_door' | 'ladder' | 'rail' | 'cobblestone_stairs' | 'oak_wall_sign' | 'spruce_wall_sign' | 'birch_wall_sign' | 'acacia_wall_sign' | 'cherry_wall_sign' | 'jungle_wall_sign' | 'dark_oak_wall_sign' | 'mangrove_wall_sign' | 'bamboo_wall_sign' | 'oak_hanging_sign' | 'spruce_hanging_sign' | 'birch_hanging_sign' | 'acacia_hanging_sign' | 'cherry_hanging_sign' | 'jungle_hanging_sign' | 'dark_oak_hanging_sign' | 'crimson_hanging_sign' | 'warped_hanging_sign' | 'mangrove_hanging_sign' | 'bamboo_hanging_sign' | 'oak_wall_hanging_sign' | 'spruce_wall_hanging_sign' | 'birch_wall_hanging_sign' | 'acacia_wall_hanging_sign' | 'cherry_wall_hanging_sign' | 'jungle_wall_hanging_sign' | 'dark_oak_wall_hanging_sign' | 'mangrove_wall_hanging_sign' | 'crimson_wall_hanging_sign' | 'warped_wall_hanging_sign' | 'bamboo_wall_hanging_sign' | 'lever' | 'stone_pressure_plate' | 'iron_door' | 'oak_pressure_plate' | 'spruce_pressure_plate' | 'birch_pressure_plate' | 'jungle_pressure_plate' | 'acacia_pressure_plate' | 'cherry_pressure_plate' | 'dark_oak_pressure_plate' | 'mangrove_pressure_plate' | 'bamboo_pressure_plate' | 'redstone_ore' | 'deepslate_redstone_ore' | 'redstone_torch' | 'redstone_wall_torch' | 'stone_button' | 'snow' | 'ice' | 'snow_block' | 'cactus' | 'clay' | 'sugar_cane' | 'jukebox' | 'oak_fence' | 'pumpkin' | 'netherrack' | 'soul_sand' | 'soul_soil' | 'basalt' | 'polished_basalt' | 'soul_torch' | 'soul_wall_torch' | 'glowstone' | 'nether_portal' | 'carved_pumpkin' | 'jack_o_lantern' | 'cake' | 'repeater' | 'white_stained_glass' | 'orange_stained_glass' | 'magenta_stained_glass' | 'light_blue_stained_glass' | 'yellow_stained_glass' | 'lime_stained_glass' | 'pink_stained_glass' | 'gray_stained_glass' | 'light_gray_stained_glass' | 'cyan_stained_glass' | 'purple_stained_glass' | 'blue_stained_glass' | 'brown_stained_glass' | 'green_stained_glass' | 'red_stained_glass' | 'black_stained_glass' | 'oak_trapdoor' | 'spruce_trapdoor' | 'birch_trapdoor' | 'jungle_trapdoor' | 'acacia_trapdoor' | 'cherry_trapdoor' | 'dark_oak_trapdoor' | 'mangrove_trapdoor' | 'bamboo_trapdoor' | 'stone_bricks' | 'mossy_stone_bricks' | 'cracked_stone_bricks' | 'chiseled_stone_bricks' | 'packed_mud' | 'mud_bricks' | 'infested_stone' | 'infested_cobblestone' | 'infested_stone_bricks' | 'infested_mossy_stone_bricks' | 'infested_cracked_stone_bricks' | 'infested_chiseled_stone_bricks' | 'brown_mushroom_block' | 'red_mushroom_block' | 'mushroom_stem' | 'iron_bars' | 'chain' | 'glass_pane' | 'melon' | 'attached_pumpkin_stem' | 'attached_melon_stem' | 'pumpkin_stem' | 'melon_stem' | 'vine' | 'glow_lichen' | 'oak_fence_gate' | 'brick_stairs' | 'stone_brick_stairs' | 'mud_brick_stairs' | 'mycelium' | 'lily_pad' | 'nether_bricks' | 'nether_brick_fence' | 'nether_brick_stairs' | 'nether_wart' | 'enchanting_table' | 'brewing_stand' | 'cauldron' | 'water_cauldron' | 'lava_cauldron' | 'powder_snow_cauldron' | 'end_portal' | 'end_portal_frame' | 'end_stone' | 'dragon_egg' | 'redstone_lamp' | 'cocoa' | 'sandstone_stairs' | 'emerald_ore' | 'deepslate_emerald_ore' | 'ender_chest' | 'tripwire_hook' | 'tripwire' | 'emerald_block' | 'spruce_stairs' | 'birch_stairs' | 'jungle_stairs' | 'command_block' | 'beacon' | 'cobblestone_wall' | 'mossy_cobblestone_wall' | 'flower_pot' | 'potted_torchflower' | 'potted_oak_sapling' | 'potted_spruce_sapling' | 'potted_birch_sapling' | 'potted_jungle_sapling' | 'potted_acacia_sapling' | 'potted_cherry_sapling' | 'potted_dark_oak_sapling' | 'potted_mangrove_propagule' | 'potted_fern' | 'potted_dandelion' | 'potted_poppy' | 'potted_blue_orchid' | 'potted_allium' | 'potted_azure_bluet' | 'potted_red_tulip' | 'potted_orange_tulip' | 'potted_white_tulip' | 'potted_pink_tulip' | 'potted_oxeye_daisy' | 'potted_cornflower' | 'potted_lily_of_the_valley' | 'potted_wither_rose' | 'potted_red_mushroom' | 'potted_brown_mushroom' | 'potted_dead_bush' | 'potted_cactus' | 'carrots' | 'potatoes' | 'oak_button' | 'spruce_button' | 'birch_button' | 'jungle_button' | 'acacia_button' | 'cherry_button' | 'dark_oak_button' | 'mangrove_button' | 'bamboo_button' | 'skeleton_skull' | 'skeleton_wall_skull' | 'wither_skeleton_skull' | 'wither_skeleton_wall_skull' | 'zombie_head' | 'zombie_wall_head' | 'player_head' | 'player_wall_head' | 'creeper_head' | 'creeper_wall_head' | 'dragon_head' | 'dragon_wall_head' | 'piglin_head' | 'piglin_wall_head' | 'anvil' | 'chipped_anvil' | 'damaged_anvil' | 'trapped_chest' | 'light_weighted_pressure_plate' | 'heavy_weighted_pressure_plate' | 'comparator' | 'daylight_detector' | 'redstone_block' | 'nether_quartz_ore' | 'hopper' | 'quartz_block' | 'chiseled_quartz_block' | 'quartz_pillar' | 'quartz_stairs' | 'activator_rail' | 'dropper' | 'white_terracotta' | 'orange_terracotta' | 'magenta_terracotta' | 'light_blue_terracotta' | 'yellow_terracotta' | 'lime_terracotta' | 'pink_terracotta' | 'gray_terracotta' | 'light_gray_terracotta' | 'cyan_terracotta' | 'purple_terracotta' | 'blue_terracotta' | 'brown_terracotta' | 'green_terracotta' | 'red_terracotta' | 'black_terracotta' | 'white_stained_glass_pane' | 'orange_stained_glass_pane' | 'magenta_stained_glass_pane' | 'light_blue_stained_glass_pane' | 'yellow_stained_glass_pane' | 'lime_stained_glass_pane' | 'pink_stained_glass_pane' | 'gray_stained_glass_pane' | 'light_gray_stained_glass_pane' | 'cyan_stained_glass_pane' | 'purple_stained_glass_pane' | 'blue_stained_glass_pane' | 'brown_stained_glass_pane' | 'green_stained_glass_pane' | 'red_stained_glass_pane' | 'black_stained_glass_pane' | 'acacia_stairs' | 'cherry_stairs' | 'dark_oak_stairs' | 'mangrove_stairs' | 'bamboo_stairs' | 'bamboo_mosaic_stairs' | 'slime_block' | 'barrier' | 'light' | 'iron_trapdoor' | 'prismarine' | 'prismarine_bricks' | 'dark_prismarine' | 'prismarine_stairs' | 'prismarine_brick_stairs' | 'dark_prismarine_stairs' | 'prismarine_slab' | 'prismarine_brick_slab' | 'dark_prismarine_slab' | 'sea_lantern' | 'hay_block' | 'white_carpet' | 'orange_carpet' | 'magenta_carpet' | 'light_blue_carpet' | 'yellow_carpet' | 'lime_carpet' | 'pink_carpet' | 'gray_carpet' | 'light_gray_carpet' | 'cyan_carpet' | 'purple_carpet' | 'blue_carpet' | 'brown_carpet' | 'green_carpet' | 'red_carpet' | 'black_carpet' | 'terracotta' | 'coal_block' | 'packed_ice' | 'sunflower' | 'lilac' | 'rose_bush' | 'peony' | 'tall_grass' | 'large_fern' | 'white_banner' | 'orange_banner' | 'magenta_banner' | 'light_blue_banner' | 'yellow_banner' | 'lime_banner' | 'pink_banner' | 'gray_banner' | 'light_gray_banner' | 'cyan_banner' | 'purple_banner' | 'blue_banner' | 'brown_banner' | 'green_banner' | 'red_banner' | 'black_banner' | 'white_wall_banner' | 'orange_wall_banner' | 'magenta_wall_banner' | 'light_blue_wall_banner' | 'yellow_wall_banner' | 'lime_wall_banner' | 'pink_wall_banner' | 'gray_wall_banner' | 'light_gray_wall_banner' | 'cyan_wall_banner' | 'purple_wall_banner' | 'blue_wall_banner' | 'brown_wall_banner' | 'green_wall_banner' | 'red_wall_banner' | 'black_wall_banner' | 'red_sandstone' | 'chiseled_red_sandstone' | 'cut_red_sandstone' | 'red_sandstone_stairs' | 'oak_slab' | 'spruce_slab' | 'birch_slab' | 'jungle_slab' | 'acacia_slab' | 'cherry_slab' | 'dark_oak_slab' | 'mangrove_slab' | 'bamboo_slab' | 'bamboo_mosaic_slab' | 'stone_slab' | 'smooth_stone_slab' | 'sandstone_slab' | 'cut_sandstone_slab' | 'petrified_oak_slab' | 'cobblestone_slab' | 'brick_slab' | 'stone_brick_slab' | 'mud_brick_slab' | 'nether_brick_slab' | 'quartz_slab' | 'red_sandstone_slab' | 'cut_red_sandstone_slab' | 'purpur_slab' | 'smooth_stone' | 'smooth_sandstone' | 'smooth_quartz' | 'smooth_red_sandstone' | 'spruce_fence_gate' | 'birch_fence_gate' | 'jungle_fence_gate' | 'acacia_fence_gate' | 'cherry_fence_gate' | 'dark_oak_fence_gate' | 'mangrove_fence_gate' | 'bamboo_fence_gate' | 'spruce_fence' | 'birch_fence' | 'jungle_fence' | 'acacia_fence' | 'cherry_fence' | 'dark_oak_fence' | 'mangrove_fence' | 'bamboo_fence' | 'spruce_door' | 'birch_door' | 'jungle_door' | 'acacia_door' | 'cherry_door' | 'dark_oak_door' | 'mangrove_door' | 'bamboo_door' | 'end_rod' | 'chorus_plant' | 'chorus_flower' | 'purpur_block' | 'purpur_pillar' | 'purpur_stairs' | 'end_stone_bricks' | 'torchflower_crop' | 'pitcher_crop' | 'pitcher_plant' | 'beetroots' | 'dirt_path' | 'end_gateway' | 'repeating_command_block' | 'chain_command_block' | 'frosted_ice' | 'magma_block' | 'nether_wart_block' | 'red_nether_bricks' | 'bone_block' | 'structure_void' | 'observer' | 'shulker_box' | 'white_shulker_box' | 'orange_shulker_box' | 'magenta_shulker_box' | 'light_blue_shulker_box' | 'yellow_shulker_box' | 'lime_shulker_box' | 'pink_shulker_box' | 'gray_shulker_box' | 'light_gray_shulker_box' | 'cyan_shulker_box' | 'purple_shulker_box' | 'blue_shulker_box' | 'brown_shulker_box' | 'green_shulker_box' | 'red_shulker_box' | 'black_shulker_box' | 'white_glazed_terracotta' | 'orange_glazed_terracotta' | 'magenta_glazed_terracotta' | 'light_blue_glazed_terracotta' | 'yellow_glazed_terracotta' | 'lime_glazed_terracotta' | 'pink_glazed_terracotta' | 'gray_glazed_terracotta' | 'light_gray_glazed_terracotta' | 'cyan_glazed_terracotta' | 'purple_glazed_terracotta' | 'blue_glazed_terracotta' | 'brown_glazed_terracotta' | 'green_glazed_terracotta' | 'red_glazed_terracotta' | 'black_glazed_terracotta' | 'white_concrete' | 'orange_concrete' | 'magenta_concrete' | 'light_blue_concrete' | 'yellow_concrete' | 'lime_concrete' | 'pink_concrete' | 'gray_concrete' | 'light_gray_concrete' | 'cyan_concrete' | 'purple_concrete' | 'blue_concrete' | 'brown_concrete' | 'green_concrete' | 'red_concrete' | 'black_concrete' | 'white_concrete_powder' | 'orange_concrete_powder' | 'magenta_concrete_powder' | 'light_blue_concrete_powder' | 'yellow_concrete_powder' | 'lime_concrete_powder' | 'pink_concrete_powder' | 'gray_concrete_powder' | 'light_gray_concrete_powder' | 'cyan_concrete_powder' | 'purple_concrete_powder' | 'blue_concrete_powder' | 'brown_concrete_powder' | 'green_concrete_powder' | 'red_concrete_powder' | 'black_concrete_powder' | 'kelp' | 'kelp_plant' | 'dried_kelp_block' | 'turtle_egg' | 'sniffer_egg' | 'dead_tube_coral_block' | 'dead_brain_coral_block' | 'dead_bubble_coral_block' | 'dead_fire_coral_block' | 'dead_horn_coral_block' | 'tube_coral_block' | 'brain_coral_block' | 'bubble_coral_block' | 'fire_coral_block' | 'horn_coral_block' | 'dead_tube_coral' | 'dead_brain_coral' | 'dead_bubble_coral' | 'dead_fire_coral' | 'dead_horn_coral' | 'tube_coral' | 'brain_coral' | 'bubble_coral' | 'fire_coral' | 'horn_coral' | 'dead_tube_coral_fan' | 'dead_brain_coral_fan' | 'dead_bubble_coral_fan' | 'dead_fire_coral_fan' | 'dead_horn_coral_fan' | 'tube_coral_fan' | 'brain_coral_fan' | 'bubble_coral_fan' | 'fire_coral_fan' | 'horn_coral_fan' | 'dead_tube_coral_wall_fan' | 'dead_brain_coral_wall_fan' | 'dead_bubble_coral_wall_fan' | 'dead_fire_coral_wall_fan' | 'dead_horn_coral_wall_fan' | 'tube_coral_wall_fan' | 'brain_coral_wall_fan' | 'bubble_coral_wall_fan' | 'fire_coral_wall_fan' | 'horn_coral_wall_fan' | 'sea_pickle' | 'blue_ice' | 'conduit' | 'bamboo_sapling' | 'bamboo' | 'potted_bamboo' | 'void_air' | 'cave_air' | 'bubble_column' | 'polished_granite_stairs' | 'smooth_red_sandstone_stairs' | 'mossy_stone_brick_stairs' | 'polished_diorite_stairs' | 'mossy_cobblestone_stairs' | 'end_stone_brick_stairs' | 'stone_stairs' | 'smooth_sandstone_stairs' | 'smooth_quartz_stairs' | 'granite_stairs' | 'andesite_stairs' | 'red_nether_brick_stairs' | 'polished_andesite_stairs' | 'diorite_stairs' | 'polished_granite_slab' | 'smooth_red_sandstone_slab' | 'mossy_stone_brick_slab' | 'polished_diorite_slab' | 'mossy_cobblestone_slab' | 'end_stone_brick_slab' | 'smooth_sandstone_slab' | 'smooth_quartz_slab' | 'granite_slab' | 'andesite_slab' | 'red_nether_brick_slab' | 'polished_andesite_slab' | 'diorite_slab' | 'brick_wall' | 'prismarine_wall' | 'red_sandstone_wall' | 'mossy_stone_brick_wall' | 'granite_wall' | 'stone_brick_wall' | 'mud_brick_wall' | 'nether_brick_wall' | 'andesite_wall' | 'red_nether_brick_wall' | 'sandstone_wall' | 'end_stone_brick_wall' | 'diorite_wall' | 'scaffolding' | 'loom' | 'barrel' | 'smoker' | 'blast_furnace' | 'cartography_table' | 'fletching_table' | 'grindstone' | 'lectern' | 'smithing_table' | 'stonecutter' | 'bell' | 'lantern' | 'soul_lantern' | 'campfire' | 'soul_campfire' | 'sweet_berry_bush' | 'warped_stem' | 'stripped_warped_stem' | 'warped_hyphae' | 'stripped_warped_hyphae' | 'warped_nylium' | 'warped_fungus' | 'warped_wart_block' | 'warped_roots' | 'nether_sprouts' | 'crimson_stem' | 'stripped_crimson_stem' | 'crimson_hyphae' | 'stripped_crimson_hyphae' | 'crimson_nylium' | 'crimson_fungus' | 'shroomlight' | 'weeping_vines' | 'weeping_vines_plant' | 'twisting_vines' | 'twisting_vines_plant' | 'crimson_roots' | 'crimson_planks' | 'warped_planks' | 'crimson_slab' | 'warped_slab' | 'crimson_pressure_plate' | 'warped_pressure_plate' | 'crimson_fence' | 'warped_fence' | 'crimson_trapdoor' | 'warped_trapdoor' | 'crimson_fence_gate' | 'warped_fence_gate' | 'crimson_stairs' | 'warped_stairs' | 'crimson_button' | 'warped_button' | 'crimson_door' | 'warped_door' | 'crimson_sign' | 'warped_sign' | 'crimson_wall_sign' | 'warped_wall_sign' | 'structure_block' | 'jigsaw' | 'composter' | 'target' | 'bee_nest' | 'beehive' | 'honey_block' | 'honeycomb_block' | 'netherite_block' | 'ancient_debris' | 'crying_obsidian' | 'respawn_anchor' | 'potted_crimson_fungus' | 'potted_warped_fungus' | 'potted_crimson_roots' | 'potted_warped_roots' | 'lodestone' | 'blackstone' | 'blackstone_stairs' | 'blackstone_wall' | 'blackstone_slab' | 'polished_blackstone' | 'polished_blackstone_bricks' | 'cracked_polished_blackstone_bricks' | 'chiseled_polished_blackstone' | 'polished_blackstone_brick_slab' | 'polished_blackstone_brick_stairs' | 'polished_blackstone_brick_wall' | 'gilded_blackstone' | 'polished_blackstone_stairs' | 'polished_blackstone_slab' | 'polished_blackstone_pressure_plate' | 'polished_blackstone_button' | 'polished_blackstone_wall' | 'chiseled_nether_bricks' | 'cracked_nether_bricks' | 'quartz_bricks' | 'candle' | 'white_candle' | 'orange_candle' | 'magenta_candle' | 'light_blue_candle' | 'yellow_candle' | 'lime_candle' | 'pink_candle' | 'gray_candle' | 'light_gray_candle' | 'cyan_candle' | 'purple_candle' | 'blue_candle' | 'brown_candle' | 'green_candle' | 'red_candle' | 'black_candle' | 'candle_cake' | 'white_candle_cake' | 'orange_candle_cake' | 'magenta_candle_cake' | 'light_blue_candle_cake' | 'yellow_candle_cake' | 'lime_candle_cake' | 'pink_candle_cake' | 'gray_candle_cake' | 'light_gray_candle_cake' | 'cyan_candle_cake' | 'purple_candle_cake' | 'blue_candle_cake' | 'brown_candle_cake' | 'green_candle_cake' | 'red_candle_cake' | 'black_candle_cake' | 'amethyst_block' | 'budding_amethyst' | 'amethyst_cluster' | 'large_amethyst_bud' | 'medium_amethyst_bud' | 'small_amethyst_bud' | 'tuff' | 'calcite' | 'tinted_glass' | 'powder_snow' | 'sculk_sensor' | 'calibrated_sculk_sensor' | 'sculk' | 'sculk_vein' | 'sculk_catalyst' | 'sculk_shrieker' | 'oxidized_copper' | 'weathered_copper' | 'exposed_copper' | 'copper_block' | 'copper_ore' | 'deepslate_copper_ore' | 'oxidized_cut_copper' | 'weathered_cut_copper' | 'exposed_cut_copper' | 'cut_copper' | 'oxidized_cut_copper_stairs' | 'weathered_cut_copper_stairs' | 'exposed_cut_copper_stairs' | 'cut_copper_stairs' | 'oxidized_cut_copper_slab' | 'weathered_cut_copper_slab' | 'exposed_cut_copper_slab' | 'cut_copper_slab' | 'waxed_copper_block' | 'waxed_weathered_copper' | 'waxed_exposed_copper' | 'waxed_oxidized_copper' | 'waxed_oxidized_cut_copper' | 'waxed_weathered_cut_copper' | 'waxed_exposed_cut_copper' | 'waxed_cut_copper' | 'waxed_oxidized_cut_copper_stairs' | 'waxed_weathered_cut_copper_stairs' | 'waxed_exposed_cut_copper_stairs' | 'waxed_cut_copper_stairs' | 'waxed_oxidized_cut_copper_slab' | 'waxed_weathered_cut_copper_slab' | 'waxed_exposed_cut_copper_slab' | 'waxed_cut_copper_slab' | 'lightning_rod' | 'pointed_dripstone' | 'dripstone_block' | 'cave_vines' | 'cave_vines_plant' | 'spore_blossom' | 'azalea' | 'flowering_azalea' | 'moss_carpet' | 'pink_petals' | 'moss_block' | 'big_dripleaf' | 'big_dripleaf_stem' | 'small_dripleaf' | 'hanging_roots' | 'rooted_dirt' | 'mud' | 'deepslate' | 'cobbled_deepslate' | 'cobbled_deepslate_stairs' | 'cobbled_deepslate_slab' | 'cobbled_deepslate_wall' | 'polished_deepslate' | 'polished_deepslate_stairs' | 'polished_deepslate_slab' | 'polished_deepslate_wall' | 'deepslate_tiles' | 'deepslate_tile_stairs' | 'deepslate_tile_slab' | 'deepslate_tile_wall' | 'deepslate_bricks' | 'deepslate_brick_stairs' | 'deepslate_brick_slab' | 'deepslate_brick_wall' | 'chiseled_deepslate' | 'cracked_deepslate_bricks' | 'cracked_deepslate_tiles' | 'infested_deepslate' | 'smooth_basalt' | 'raw_iron_block' | 'raw_copper_block' | 'raw_gold_block' | 'potted_azalea_bush' | 'potted_flowering_azalea_bush' | 'ochre_froglight' | 'verdant_froglight' | 'pearlescent_froglight' | 'frogspawn' | 'reinforced_deepslate' | 'decorated_pot'; +export type ItemNames = 'air' | 'stone' | 'granite' | 'polished_granite' | 'diorite' | 'polished_diorite' | 'andesite' | 'polished_andesite' | 'deepslate' | 'cobbled_deepslate' | 'polished_deepslate' | 'calcite' | 'tuff' | 'dripstone_block' | 'grass_block' | 'dirt' | 'coarse_dirt' | 'podzol' | 'rooted_dirt' | 'mud' | 'crimson_nylium' | 'warped_nylium' | 'cobblestone' | 'oak_planks' | 'spruce_planks' | 'birch_planks' | 'jungle_planks' | 'acacia_planks' | 'cherry_planks' | 'dark_oak_planks' | 'mangrove_planks' | 'bamboo_planks' | 'crimson_planks' | 'warped_planks' | 'bamboo_mosaic' | 'oak_sapling' | 'spruce_sapling' | 'birch_sapling' | 'jungle_sapling' | 'acacia_sapling' | 'cherry_sapling' | 'dark_oak_sapling' | 'mangrove_propagule' | 'bedrock' | 'sand' | 'suspicious_sand' | 'suspicious_gravel' | 'red_sand' | 'gravel' | 'coal_ore' | 'deepslate_coal_ore' | 'iron_ore' | 'deepslate_iron_ore' | 'copper_ore' | 'deepslate_copper_ore' | 'gold_ore' | 'deepslate_gold_ore' | 'redstone_ore' | 'deepslate_redstone_ore' | 'emerald_ore' | 'deepslate_emerald_ore' | 'lapis_ore' | 'deepslate_lapis_ore' | 'diamond_ore' | 'deepslate_diamond_ore' | 'nether_gold_ore' | 'nether_quartz_ore' | 'ancient_debris' | 'coal_block' | 'raw_iron_block' | 'raw_copper_block' | 'raw_gold_block' | 'amethyst_block' | 'budding_amethyst' | 'iron_block' | 'copper_block' | 'gold_block' | 'diamond_block' | 'netherite_block' | 'exposed_copper' | 'weathered_copper' | 'oxidized_copper' | 'cut_copper' | 'exposed_cut_copper' | 'weathered_cut_copper' | 'oxidized_cut_copper' | 'cut_copper_stairs' | 'exposed_cut_copper_stairs' | 'weathered_cut_copper_stairs' | 'oxidized_cut_copper_stairs' | 'cut_copper_slab' | 'exposed_cut_copper_slab' | 'weathered_cut_copper_slab' | 'oxidized_cut_copper_slab' | 'waxed_copper_block' | 'waxed_exposed_copper' | 'waxed_weathered_copper' | 'waxed_oxidized_copper' | 'waxed_cut_copper' | 'waxed_exposed_cut_copper' | 'waxed_weathered_cut_copper' | 'waxed_oxidized_cut_copper' | 'waxed_cut_copper_stairs' | 'waxed_exposed_cut_copper_stairs' | 'waxed_weathered_cut_copper_stairs' | 'waxed_oxidized_cut_copper_stairs' | 'waxed_cut_copper_slab' | 'waxed_exposed_cut_copper_slab' | 'waxed_weathered_cut_copper_slab' | 'waxed_oxidized_cut_copper_slab' | 'oak_log' | 'spruce_log' | 'birch_log' | 'jungle_log' | 'acacia_log' | 'cherry_log' | 'dark_oak_log' | 'mangrove_log' | 'mangrove_roots' | 'muddy_mangrove_roots' | 'crimson_stem' | 'warped_stem' | 'bamboo_block' | 'stripped_oak_log' | 'stripped_spruce_log' | 'stripped_birch_log' | 'stripped_jungle_log' | 'stripped_acacia_log' | 'stripped_cherry_log' | 'stripped_dark_oak_log' | 'stripped_mangrove_log' | 'stripped_crimson_stem' | 'stripped_warped_stem' | 'stripped_oak_wood' | 'stripped_spruce_wood' | 'stripped_birch_wood' | 'stripped_jungle_wood' | 'stripped_acacia_wood' | 'stripped_cherry_wood' | 'stripped_dark_oak_wood' | 'stripped_mangrove_wood' | 'stripped_crimson_hyphae' | 'stripped_warped_hyphae' | 'stripped_bamboo_block' | 'oak_wood' | 'spruce_wood' | 'birch_wood' | 'jungle_wood' | 'acacia_wood' | 'cherry_wood' | 'dark_oak_wood' | 'mangrove_wood' | 'crimson_hyphae' | 'warped_hyphae' | 'oak_leaves' | 'spruce_leaves' | 'birch_leaves' | 'jungle_leaves' | 'acacia_leaves' | 'cherry_leaves' | 'dark_oak_leaves' | 'mangrove_leaves' | 'azalea_leaves' | 'flowering_azalea_leaves' | 'sponge' | 'wet_sponge' | 'glass' | 'tinted_glass' | 'lapis_block' | 'sandstone' | 'chiseled_sandstone' | 'cut_sandstone' | 'cobweb' | 'grass' | 'fern' | 'azalea' | 'flowering_azalea' | 'dead_bush' | 'seagrass' | 'sea_pickle' | 'white_wool' | 'orange_wool' | 'magenta_wool' | 'light_blue_wool' | 'yellow_wool' | 'lime_wool' | 'pink_wool' | 'gray_wool' | 'light_gray_wool' | 'cyan_wool' | 'purple_wool' | 'blue_wool' | 'brown_wool' | 'green_wool' | 'red_wool' | 'black_wool' | 'dandelion' | 'poppy' | 'blue_orchid' | 'allium' | 'azure_bluet' | 'red_tulip' | 'orange_tulip' | 'white_tulip' | 'pink_tulip' | 'oxeye_daisy' | 'cornflower' | 'lily_of_the_valley' | 'wither_rose' | 'torchflower' | 'pitcher_plant' | 'spore_blossom' | 'brown_mushroom' | 'red_mushroom' | 'crimson_fungus' | 'warped_fungus' | 'crimson_roots' | 'warped_roots' | 'nether_sprouts' | 'weeping_vines' | 'twisting_vines' | 'sugar_cane' | 'kelp' | 'moss_carpet' | 'pink_petals' | 'moss_block' | 'hanging_roots' | 'big_dripleaf' | 'small_dripleaf' | 'bamboo' | 'oak_slab' | 'spruce_slab' | 'birch_slab' | 'jungle_slab' | 'acacia_slab' | 'cherry_slab' | 'dark_oak_slab' | 'mangrove_slab' | 'bamboo_slab' | 'bamboo_mosaic_slab' | 'crimson_slab' | 'warped_slab' | 'stone_slab' | 'smooth_stone_slab' | 'sandstone_slab' | 'cut_sandstone_slab' | 'petrified_oak_slab' | 'cobblestone_slab' | 'brick_slab' | 'stone_brick_slab' | 'mud_brick_slab' | 'nether_brick_slab' | 'quartz_slab' | 'red_sandstone_slab' | 'cut_red_sandstone_slab' | 'purpur_slab' | 'prismarine_slab' | 'prismarine_brick_slab' | 'dark_prismarine_slab' | 'smooth_quartz' | 'smooth_red_sandstone' | 'smooth_sandstone' | 'smooth_stone' | 'bricks' | 'bookshelf' | 'chiseled_bookshelf' | 'decorated_pot' | 'mossy_cobblestone' | 'obsidian' | 'torch' | 'end_rod' | 'chorus_plant' | 'chorus_flower' | 'purpur_block' | 'purpur_pillar' | 'purpur_stairs' | 'spawner' | 'chest' | 'crafting_table' | 'farmland' | 'furnace' | 'ladder' | 'cobblestone_stairs' | 'snow' | 'ice' | 'snow_block' | 'cactus' | 'clay' | 'jukebox' | 'oak_fence' | 'spruce_fence' | 'birch_fence' | 'jungle_fence' | 'acacia_fence' | 'cherry_fence' | 'dark_oak_fence' | 'mangrove_fence' | 'bamboo_fence' | 'crimson_fence' | 'warped_fence' | 'pumpkin' | 'carved_pumpkin' | 'jack_o_lantern' | 'netherrack' | 'soul_sand' | 'soul_soil' | 'basalt' | 'polished_basalt' | 'smooth_basalt' | 'soul_torch' | 'glowstone' | 'infested_stone' | 'infested_cobblestone' | 'infested_stone_bricks' | 'infested_mossy_stone_bricks' | 'infested_cracked_stone_bricks' | 'infested_chiseled_stone_bricks' | 'infested_deepslate' | 'stone_bricks' | 'mossy_stone_bricks' | 'cracked_stone_bricks' | 'chiseled_stone_bricks' | 'packed_mud' | 'mud_bricks' | 'deepslate_bricks' | 'cracked_deepslate_bricks' | 'deepslate_tiles' | 'cracked_deepslate_tiles' | 'chiseled_deepslate' | 'reinforced_deepslate' | 'brown_mushroom_block' | 'red_mushroom_block' | 'mushroom_stem' | 'iron_bars' | 'chain' | 'glass_pane' | 'melon' | 'vine' | 'glow_lichen' | 'brick_stairs' | 'stone_brick_stairs' | 'mud_brick_stairs' | 'mycelium' | 'lily_pad' | 'nether_bricks' | 'cracked_nether_bricks' | 'chiseled_nether_bricks' | 'nether_brick_fence' | 'nether_brick_stairs' | 'sculk' | 'sculk_vein' | 'sculk_catalyst' | 'sculk_shrieker' | 'enchanting_table' | 'end_portal_frame' | 'end_stone' | 'end_stone_bricks' | 'dragon_egg' | 'sandstone_stairs' | 'ender_chest' | 'emerald_block' | 'oak_stairs' | 'spruce_stairs' | 'birch_stairs' | 'jungle_stairs' | 'acacia_stairs' | 'cherry_stairs' | 'dark_oak_stairs' | 'mangrove_stairs' | 'bamboo_stairs' | 'bamboo_mosaic_stairs' | 'crimson_stairs' | 'warped_stairs' | 'command_block' | 'beacon' | 'cobblestone_wall' | 'mossy_cobblestone_wall' | 'brick_wall' | 'prismarine_wall' | 'red_sandstone_wall' | 'mossy_stone_brick_wall' | 'granite_wall' | 'stone_brick_wall' | 'mud_brick_wall' | 'nether_brick_wall' | 'andesite_wall' | 'red_nether_brick_wall' | 'sandstone_wall' | 'end_stone_brick_wall' | 'diorite_wall' | 'blackstone_wall' | 'polished_blackstone_wall' | 'polished_blackstone_brick_wall' | 'cobbled_deepslate_wall' | 'polished_deepslate_wall' | 'deepslate_brick_wall' | 'deepslate_tile_wall' | 'anvil' | 'chipped_anvil' | 'damaged_anvil' | 'chiseled_quartz_block' | 'quartz_block' | 'quartz_bricks' | 'quartz_pillar' | 'quartz_stairs' | 'white_terracotta' | 'orange_terracotta' | 'magenta_terracotta' | 'light_blue_terracotta' | 'yellow_terracotta' | 'lime_terracotta' | 'pink_terracotta' | 'gray_terracotta' | 'light_gray_terracotta' | 'cyan_terracotta' | 'purple_terracotta' | 'blue_terracotta' | 'brown_terracotta' | 'green_terracotta' | 'red_terracotta' | 'black_terracotta' | 'barrier' | 'light' | 'hay_block' | 'white_carpet' | 'orange_carpet' | 'magenta_carpet' | 'light_blue_carpet' | 'yellow_carpet' | 'lime_carpet' | 'pink_carpet' | 'gray_carpet' | 'light_gray_carpet' | 'cyan_carpet' | 'purple_carpet' | 'blue_carpet' | 'brown_carpet' | 'green_carpet' | 'red_carpet' | 'black_carpet' | 'terracotta' | 'packed_ice' | 'dirt_path' | 'sunflower' | 'lilac' | 'rose_bush' | 'peony' | 'tall_grass' | 'large_fern' | 'white_stained_glass' | 'orange_stained_glass' | 'magenta_stained_glass' | 'light_blue_stained_glass' | 'yellow_stained_glass' | 'lime_stained_glass' | 'pink_stained_glass' | 'gray_stained_glass' | 'light_gray_stained_glass' | 'cyan_stained_glass' | 'purple_stained_glass' | 'blue_stained_glass' | 'brown_stained_glass' | 'green_stained_glass' | 'red_stained_glass' | 'black_stained_glass' | 'white_stained_glass_pane' | 'orange_stained_glass_pane' | 'magenta_stained_glass_pane' | 'light_blue_stained_glass_pane' | 'yellow_stained_glass_pane' | 'lime_stained_glass_pane' | 'pink_stained_glass_pane' | 'gray_stained_glass_pane' | 'light_gray_stained_glass_pane' | 'cyan_stained_glass_pane' | 'purple_stained_glass_pane' | 'blue_stained_glass_pane' | 'brown_stained_glass_pane' | 'green_stained_glass_pane' | 'red_stained_glass_pane' | 'black_stained_glass_pane' | 'prismarine' | 'prismarine_bricks' | 'dark_prismarine' | 'prismarine_stairs' | 'prismarine_brick_stairs' | 'dark_prismarine_stairs' | 'sea_lantern' | 'red_sandstone' | 'chiseled_red_sandstone' | 'cut_red_sandstone' | 'red_sandstone_stairs' | 'repeating_command_block' | 'chain_command_block' | 'magma_block' | 'nether_wart_block' | 'warped_wart_block' | 'red_nether_bricks' | 'bone_block' | 'structure_void' | 'shulker_box' | 'white_shulker_box' | 'orange_shulker_box' | 'magenta_shulker_box' | 'light_blue_shulker_box' | 'yellow_shulker_box' | 'lime_shulker_box' | 'pink_shulker_box' | 'gray_shulker_box' | 'light_gray_shulker_box' | 'cyan_shulker_box' | 'purple_shulker_box' | 'blue_shulker_box' | 'brown_shulker_box' | 'green_shulker_box' | 'red_shulker_box' | 'black_shulker_box' | 'white_glazed_terracotta' | 'orange_glazed_terracotta' | 'magenta_glazed_terracotta' | 'light_blue_glazed_terracotta' | 'yellow_glazed_terracotta' | 'lime_glazed_terracotta' | 'pink_glazed_terracotta' | 'gray_glazed_terracotta' | 'light_gray_glazed_terracotta' | 'cyan_glazed_terracotta' | 'purple_glazed_terracotta' | 'blue_glazed_terracotta' | 'brown_glazed_terracotta' | 'green_glazed_terracotta' | 'red_glazed_terracotta' | 'black_glazed_terracotta' | 'white_concrete' | 'orange_concrete' | 'magenta_concrete' | 'light_blue_concrete' | 'yellow_concrete' | 'lime_concrete' | 'pink_concrete' | 'gray_concrete' | 'light_gray_concrete' | 'cyan_concrete' | 'purple_concrete' | 'blue_concrete' | 'brown_concrete' | 'green_concrete' | 'red_concrete' | 'black_concrete' | 'white_concrete_powder' | 'orange_concrete_powder' | 'magenta_concrete_powder' | 'light_blue_concrete_powder' | 'yellow_concrete_powder' | 'lime_concrete_powder' | 'pink_concrete_powder' | 'gray_concrete_powder' | 'light_gray_concrete_powder' | 'cyan_concrete_powder' | 'purple_concrete_powder' | 'blue_concrete_powder' | 'brown_concrete_powder' | 'green_concrete_powder' | 'red_concrete_powder' | 'black_concrete_powder' | 'turtle_egg' | 'sniffer_egg' | 'dead_tube_coral_block' | 'dead_brain_coral_block' | 'dead_bubble_coral_block' | 'dead_fire_coral_block' | 'dead_horn_coral_block' | 'tube_coral_block' | 'brain_coral_block' | 'bubble_coral_block' | 'fire_coral_block' | 'horn_coral_block' | 'tube_coral' | 'brain_coral' | 'bubble_coral' | 'fire_coral' | 'horn_coral' | 'dead_brain_coral' | 'dead_bubble_coral' | 'dead_fire_coral' | 'dead_horn_coral' | 'dead_tube_coral' | 'tube_coral_fan' | 'brain_coral_fan' | 'bubble_coral_fan' | 'fire_coral_fan' | 'horn_coral_fan' | 'dead_tube_coral_fan' | 'dead_brain_coral_fan' | 'dead_bubble_coral_fan' | 'dead_fire_coral_fan' | 'dead_horn_coral_fan' | 'blue_ice' | 'conduit' | 'polished_granite_stairs' | 'smooth_red_sandstone_stairs' | 'mossy_stone_brick_stairs' | 'polished_diorite_stairs' | 'mossy_cobblestone_stairs' | 'end_stone_brick_stairs' | 'stone_stairs' | 'smooth_sandstone_stairs' | 'smooth_quartz_stairs' | 'granite_stairs' | 'andesite_stairs' | 'red_nether_brick_stairs' | 'polished_andesite_stairs' | 'diorite_stairs' | 'cobbled_deepslate_stairs' | 'polished_deepslate_stairs' | 'deepslate_brick_stairs' | 'deepslate_tile_stairs' | 'polished_granite_slab' | 'smooth_red_sandstone_slab' | 'mossy_stone_brick_slab' | 'polished_diorite_slab' | 'mossy_cobblestone_slab' | 'end_stone_brick_slab' | 'smooth_sandstone_slab' | 'smooth_quartz_slab' | 'granite_slab' | 'andesite_slab' | 'red_nether_brick_slab' | 'polished_andesite_slab' | 'diorite_slab' | 'cobbled_deepslate_slab' | 'polished_deepslate_slab' | 'deepslate_brick_slab' | 'deepslate_tile_slab' | 'scaffolding' | 'redstone' | 'redstone_torch' | 'redstone_block' | 'repeater' | 'comparator' | 'piston' | 'sticky_piston' | 'slime_block' | 'honey_block' | 'observer' | 'hopper' | 'dispenser' | 'dropper' | 'lectern' | 'target' | 'lever' | 'lightning_rod' | 'daylight_detector' | 'sculk_sensor' | 'calibrated_sculk_sensor' | 'tripwire_hook' | 'trapped_chest' | 'tnt' | 'redstone_lamp' | 'note_block' | 'stone_button' | 'polished_blackstone_button' | 'oak_button' | 'spruce_button' | 'birch_button' | 'jungle_button' | 'acacia_button' | 'cherry_button' | 'dark_oak_button' | 'mangrove_button' | 'bamboo_button' | 'crimson_button' | 'warped_button' | 'stone_pressure_plate' | 'polished_blackstone_pressure_plate' | 'light_weighted_pressure_plate' | 'heavy_weighted_pressure_plate' | 'oak_pressure_plate' | 'spruce_pressure_plate' | 'birch_pressure_plate' | 'jungle_pressure_plate' | 'acacia_pressure_plate' | 'cherry_pressure_plate' | 'dark_oak_pressure_plate' | 'mangrove_pressure_plate' | 'bamboo_pressure_plate' | 'crimson_pressure_plate' | 'warped_pressure_plate' | 'iron_door' | 'oak_door' | 'spruce_door' | 'birch_door' | 'jungle_door' | 'acacia_door' | 'cherry_door' | 'dark_oak_door' | 'mangrove_door' | 'bamboo_door' | 'crimson_door' | 'warped_door' | 'iron_trapdoor' | 'oak_trapdoor' | 'spruce_trapdoor' | 'birch_trapdoor' | 'jungle_trapdoor' | 'acacia_trapdoor' | 'cherry_trapdoor' | 'dark_oak_trapdoor' | 'mangrove_trapdoor' | 'bamboo_trapdoor' | 'crimson_trapdoor' | 'warped_trapdoor' | 'oak_fence_gate' | 'spruce_fence_gate' | 'birch_fence_gate' | 'jungle_fence_gate' | 'acacia_fence_gate' | 'cherry_fence_gate' | 'dark_oak_fence_gate' | 'mangrove_fence_gate' | 'bamboo_fence_gate' | 'crimson_fence_gate' | 'warped_fence_gate' | 'powered_rail' | 'detector_rail' | 'rail' | 'activator_rail' | 'saddle' | 'minecart' | 'chest_minecart' | 'furnace_minecart' | 'tnt_minecart' | 'hopper_minecart' | 'carrot_on_a_stick' | 'warped_fungus_on_a_stick' | 'elytra' | 'oak_boat' | 'oak_chest_boat' | 'spruce_boat' | 'spruce_chest_boat' | 'birch_boat' | 'birch_chest_boat' | 'jungle_boat' | 'jungle_chest_boat' | 'acacia_boat' | 'acacia_chest_boat' | 'cherry_boat' | 'cherry_chest_boat' | 'dark_oak_boat' | 'dark_oak_chest_boat' | 'mangrove_boat' | 'mangrove_chest_boat' | 'bamboo_raft' | 'bamboo_chest_raft' | 'structure_block' | 'jigsaw' | 'turtle_helmet' | 'scute' | 'flint_and_steel' | 'apple' | 'bow' | 'arrow' | 'coal' | 'charcoal' | 'diamond' | 'emerald' | 'lapis_lazuli' | 'quartz' | 'amethyst_shard' | 'raw_iron' | 'iron_ingot' | 'raw_copper' | 'copper_ingot' | 'raw_gold' | 'gold_ingot' | 'netherite_ingot' | 'netherite_scrap' | 'wooden_sword' | 'wooden_shovel' | 'wooden_pickaxe' | 'wooden_axe' | 'wooden_hoe' | 'stone_sword' | 'stone_shovel' | 'stone_pickaxe' | 'stone_axe' | 'stone_hoe' | 'golden_sword' | 'golden_shovel' | 'golden_pickaxe' | 'golden_axe' | 'golden_hoe' | 'iron_sword' | 'iron_shovel' | 'iron_pickaxe' | 'iron_axe' | 'iron_hoe' | 'diamond_sword' | 'diamond_shovel' | 'diamond_pickaxe' | 'diamond_axe' | 'diamond_hoe' | 'netherite_sword' | 'netherite_shovel' | 'netherite_pickaxe' | 'netherite_axe' | 'netherite_hoe' | 'stick' | 'bowl' | 'mushroom_stew' | 'string' | 'feather' | 'gunpowder' | 'wheat_seeds' | 'wheat' | 'bread' | 'leather_helmet' | 'leather_chestplate' | 'leather_leggings' | 'leather_boots' | 'chainmail_helmet' | 'chainmail_chestplate' | 'chainmail_leggings' | 'chainmail_boots' | 'iron_helmet' | 'iron_chestplate' | 'iron_leggings' | 'iron_boots' | 'diamond_helmet' | 'diamond_chestplate' | 'diamond_leggings' | 'diamond_boots' | 'golden_helmet' | 'golden_chestplate' | 'golden_leggings' | 'golden_boots' | 'netherite_helmet' | 'netherite_chestplate' | 'netherite_leggings' | 'netherite_boots' | 'flint' | 'porkchop' | 'cooked_porkchop' | 'painting' | 'golden_apple' | 'enchanted_golden_apple' | 'oak_sign' | 'spruce_sign' | 'birch_sign' | 'jungle_sign' | 'acacia_sign' | 'cherry_sign' | 'dark_oak_sign' | 'mangrove_sign' | 'bamboo_sign' | 'crimson_sign' | 'warped_sign' | 'oak_hanging_sign' | 'spruce_hanging_sign' | 'birch_hanging_sign' | 'jungle_hanging_sign' | 'acacia_hanging_sign' | 'cherry_hanging_sign' | 'dark_oak_hanging_sign' | 'mangrove_hanging_sign' | 'bamboo_hanging_sign' | 'crimson_hanging_sign' | 'warped_hanging_sign' | 'bucket' | 'water_bucket' | 'lava_bucket' | 'powder_snow_bucket' | 'snowball' | 'leather' | 'milk_bucket' | 'pufferfish_bucket' | 'salmon_bucket' | 'cod_bucket' | 'tropical_fish_bucket' | 'axolotl_bucket' | 'tadpole_bucket' | 'brick' | 'clay_ball' | 'dried_kelp_block' | 'paper' | 'book' | 'slime_ball' | 'egg' | 'compass' | 'recovery_compass' | 'bundle' | 'fishing_rod' | 'clock' | 'spyglass' | 'glowstone_dust' | 'cod' | 'salmon' | 'tropical_fish' | 'pufferfish' | 'cooked_cod' | 'cooked_salmon' | 'ink_sac' | 'glow_ink_sac' | 'cocoa_beans' | 'white_dye' | 'orange_dye' | 'magenta_dye' | 'light_blue_dye' | 'yellow_dye' | 'lime_dye' | 'pink_dye' | 'gray_dye' | 'light_gray_dye' | 'cyan_dye' | 'purple_dye' | 'blue_dye' | 'brown_dye' | 'green_dye' | 'red_dye' | 'black_dye' | 'bone_meal' | 'bone' | 'sugar' | 'cake' | 'white_bed' | 'orange_bed' | 'magenta_bed' | 'light_blue_bed' | 'yellow_bed' | 'lime_bed' | 'pink_bed' | 'gray_bed' | 'light_gray_bed' | 'cyan_bed' | 'purple_bed' | 'blue_bed' | 'brown_bed' | 'green_bed' | 'red_bed' | 'black_bed' | 'cookie' | 'filled_map' | 'shears' | 'melon_slice' | 'dried_kelp' | 'pumpkin_seeds' | 'melon_seeds' | 'beef' | 'cooked_beef' | 'chicken' | 'cooked_chicken' | 'rotten_flesh' | 'ender_pearl' | 'blaze_rod' | 'ghast_tear' | 'gold_nugget' | 'nether_wart' | 'potion' | 'glass_bottle' | 'spider_eye' | 'fermented_spider_eye' | 'blaze_powder' | 'magma_cream' | 'brewing_stand' | 'cauldron' | 'ender_eye' | 'glistering_melon_slice' | 'allay_spawn_egg' | 'axolotl_spawn_egg' | 'bat_spawn_egg' | 'bee_spawn_egg' | 'blaze_spawn_egg' | 'cat_spawn_egg' | 'camel_spawn_egg' | 'cave_spider_spawn_egg' | 'chicken_spawn_egg' | 'cod_spawn_egg' | 'cow_spawn_egg' | 'creeper_spawn_egg' | 'dolphin_spawn_egg' | 'donkey_spawn_egg' | 'drowned_spawn_egg' | 'elder_guardian_spawn_egg' | 'ender_dragon_spawn_egg' | 'enderman_spawn_egg' | 'endermite_spawn_egg' | 'evoker_spawn_egg' | 'fox_spawn_egg' | 'frog_spawn_egg' | 'ghast_spawn_egg' | 'glow_squid_spawn_egg' | 'goat_spawn_egg' | 'guardian_spawn_egg' | 'hoglin_spawn_egg' | 'horse_spawn_egg' | 'husk_spawn_egg' | 'iron_golem_spawn_egg' | 'llama_spawn_egg' | 'magma_cube_spawn_egg' | 'mooshroom_spawn_egg' | 'mule_spawn_egg' | 'ocelot_spawn_egg' | 'panda_spawn_egg' | 'parrot_spawn_egg' | 'phantom_spawn_egg' | 'pig_spawn_egg' | 'piglin_spawn_egg' | 'piglin_brute_spawn_egg' | 'pillager_spawn_egg' | 'polar_bear_spawn_egg' | 'pufferfish_spawn_egg' | 'rabbit_spawn_egg' | 'ravager_spawn_egg' | 'salmon_spawn_egg' | 'sheep_spawn_egg' | 'shulker_spawn_egg' | 'silverfish_spawn_egg' | 'skeleton_spawn_egg' | 'skeleton_horse_spawn_egg' | 'slime_spawn_egg' | 'sniffer_spawn_egg' | 'snow_golem_spawn_egg' | 'spider_spawn_egg' | 'squid_spawn_egg' | 'stray_spawn_egg' | 'strider_spawn_egg' | 'tadpole_spawn_egg' | 'trader_llama_spawn_egg' | 'tropical_fish_spawn_egg' | 'turtle_spawn_egg' | 'vex_spawn_egg' | 'villager_spawn_egg' | 'vindicator_spawn_egg' | 'wandering_trader_spawn_egg' | 'warden_spawn_egg' | 'witch_spawn_egg' | 'wither_spawn_egg' | 'wither_skeleton_spawn_egg' | 'wolf_spawn_egg' | 'zoglin_spawn_egg' | 'zombie_spawn_egg' | 'zombie_horse_spawn_egg' | 'zombie_villager_spawn_egg' | 'zombified_piglin_spawn_egg' | 'experience_bottle' | 'fire_charge' | 'writable_book' | 'written_book' | 'item_frame' | 'glow_item_frame' | 'flower_pot' | 'carrot' | 'potato' | 'baked_potato' | 'poisonous_potato' | 'map' | 'golden_carrot' | 'skeleton_skull' | 'wither_skeleton_skull' | 'player_head' | 'zombie_head' | 'creeper_head' | 'dragon_head' | 'piglin_head' | 'nether_star' | 'pumpkin_pie' | 'firework_rocket' | 'firework_star' | 'enchanted_book' | 'nether_brick' | 'prismarine_shard' | 'prismarine_crystals' | 'rabbit' | 'cooked_rabbit' | 'rabbit_stew' | 'rabbit_foot' | 'rabbit_hide' | 'armor_stand' | 'iron_horse_armor' | 'golden_horse_armor' | 'diamond_horse_armor' | 'leather_horse_armor' | 'lead' | 'name_tag' | 'command_block_minecart' | 'mutton' | 'cooked_mutton' | 'white_banner' | 'orange_banner' | 'magenta_banner' | 'light_blue_banner' | 'yellow_banner' | 'lime_banner' | 'pink_banner' | 'gray_banner' | 'light_gray_banner' | 'cyan_banner' | 'purple_banner' | 'blue_banner' | 'brown_banner' | 'green_banner' | 'red_banner' | 'black_banner' | 'end_crystal' | 'chorus_fruit' | 'popped_chorus_fruit' | 'torchflower_seeds' | 'pitcher_pod' | 'beetroot' | 'beetroot_seeds' | 'beetroot_soup' | 'dragon_breath' | 'splash_potion' | 'spectral_arrow' | 'tipped_arrow' | 'lingering_potion' | 'shield' | 'totem_of_undying' | 'shulker_shell' | 'iron_nugget' | 'knowledge_book' | 'debug_stick' | 'music_disc_13' | 'music_disc_cat' | 'music_disc_blocks' | 'music_disc_chirp' | 'music_disc_far' | 'music_disc_mall' | 'music_disc_mellohi' | 'music_disc_stal' | 'music_disc_strad' | 'music_disc_ward' | 'music_disc_11' | 'music_disc_wait' | 'music_disc_otherside' | 'music_disc_relic' | 'music_disc_5' | 'music_disc_pigstep' | 'disc_fragment_5' | 'trident' | 'phantom_membrane' | 'nautilus_shell' | 'heart_of_the_sea' | 'crossbow' | 'suspicious_stew' | 'loom' | 'flower_banner_pattern' | 'creeper_banner_pattern' | 'skull_banner_pattern' | 'mojang_banner_pattern' | 'globe_banner_pattern' | 'piglin_banner_pattern' | 'goat_horn' | 'composter' | 'barrel' | 'smoker' | 'blast_furnace' | 'cartography_table' | 'fletching_table' | 'grindstone' | 'smithing_table' | 'stonecutter' | 'bell' | 'lantern' | 'soul_lantern' | 'sweet_berries' | 'glow_berries' | 'campfire' | 'soul_campfire' | 'shroomlight' | 'honeycomb' | 'bee_nest' | 'beehive' | 'honey_bottle' | 'honeycomb_block' | 'lodestone' | 'crying_obsidian' | 'blackstone' | 'blackstone_slab' | 'blackstone_stairs' | 'gilded_blackstone' | 'polished_blackstone' | 'polished_blackstone_slab' | 'polished_blackstone_stairs' | 'chiseled_polished_blackstone' | 'polished_blackstone_bricks' | 'polished_blackstone_brick_slab' | 'polished_blackstone_brick_stairs' | 'cracked_polished_blackstone_bricks' | 'respawn_anchor' | 'candle' | 'white_candle' | 'orange_candle' | 'magenta_candle' | 'light_blue_candle' | 'yellow_candle' | 'lime_candle' | 'pink_candle' | 'gray_candle' | 'light_gray_candle' | 'cyan_candle' | 'purple_candle' | 'blue_candle' | 'brown_candle' | 'green_candle' | 'red_candle' | 'black_candle' | 'small_amethyst_bud' | 'medium_amethyst_bud' | 'large_amethyst_bud' | 'amethyst_cluster' | 'pointed_dripstone' | 'ochre_froglight' | 'verdant_froglight' | 'pearlescent_froglight' | 'frogspawn' | 'echo_shard' | 'brush' | 'netherite_upgrade_smithing_template' | 'sentry_armor_trim_smithing_template' | 'dune_armor_trim_smithing_template' | 'coast_armor_trim_smithing_template' | 'wild_armor_trim_smithing_template' | 'ward_armor_trim_smithing_template' | 'eye_armor_trim_smithing_template' | 'vex_armor_trim_smithing_template' | 'tide_armor_trim_smithing_template' | 'snout_armor_trim_smithing_template' | 'rib_armor_trim_smithing_template' | 'spire_armor_trim_smithing_template' | 'wayfinder_armor_trim_smithing_template' | 'shaper_armor_trim_smithing_template' | 'silence_armor_trim_smithing_template' | 'raiser_armor_trim_smithing_template' | 'host_armor_trim_smithing_template' | 'angler_pottery_sherd' | 'archer_pottery_sherd' | 'arms_up_pottery_sherd' | 'blade_pottery_sherd' | 'brewer_pottery_sherd' | 'burn_pottery_sherd' | 'danger_pottery_sherd' | 'explorer_pottery_sherd' | 'friend_pottery_sherd' | 'heart_pottery_sherd' | 'heartbreak_pottery_sherd' | 'howl_pottery_sherd' | 'miner_pottery_sherd' | 'mourner_pottery_sherd' | 'plenty_pottery_sherd' | 'prize_pottery_sherd' | 'sheaf_pottery_sherd' | 'shelter_pottery_sherd' | 'skull_pottery_sherd' | 'snort_pottery_sherd'; +export type EntityNames = 'allay' | 'area_effect_cloud' | 'armor_stand' | 'arrow' | 'axolotl' | 'bat' | 'bee' | 'blaze' | 'block_display' | 'boat' | 'camel' | 'cat' | 'cave_spider' | 'chest_boat' | 'chest_minecart' | 'chicken' | 'cod' | 'command_block_minecart' | 'cow' | 'creeper' | 'dolphin' | 'donkey' | 'dragon_fireball' | 'drowned' | 'egg' | 'elder_guardian' | 'end_crystal' | 'ender_dragon' | 'ender_pearl' | 'enderman' | 'endermite' | 'evoker' | 'evoker_fangs' | 'experience_bottle' | 'experience_orb' | 'eye_of_ender' | 'falling_block' | 'firework_rocket' | 'fox' | 'frog' | 'furnace_minecart' | 'ghast' | 'giant' | 'glow_item_frame' | 'glow_squid' | 'goat' | 'guardian' | 'hoglin' | 'hopper_minecart' | 'horse' | 'husk' | 'illusioner' | 'interaction' | 'iron_golem' | 'item' | 'item_display' | 'item_frame' | 'fireball' | 'leash_knot' | 'lightning_bolt' | 'llama' | 'llama_spit' | 'magma_cube' | 'marker' | 'minecart' | 'mooshroom' | 'mule' | 'ocelot' | 'painting' | 'panda' | 'parrot' | 'phantom' | 'pig' | 'piglin' | 'piglin_brute' | 'pillager' | 'polar_bear' | 'potion' | 'pufferfish' | 'rabbit' | 'ravager' | 'salmon' | 'sheep' | 'shulker' | 'shulker_bullet' | 'silverfish' | 'skeleton' | 'skeleton_horse' | 'slime' | 'small_fireball' | 'sniffer' | 'snow_golem' | 'snowball' | 'spawner_minecart' | 'spectral_arrow' | 'spider' | 'squid' | 'stray' | 'strider' | 'tadpole' | 'text_display' | 'tnt' | 'tnt_minecart' | 'trader_llama' | 'trident' | 'tropical_fish' | 'turtle' | 'vex' | 'villager' | 'vindicator' | 'wandering_trader' | 'warden' | 'witch' | 'wither' | 'wither_skeleton' | 'wither_skull' | 'wolf' | 'zoglin' | 'zombie' | 'zombie_horse' | 'zombie_villager' | 'zombified_piglin' | 'player' | 'fishing_bobber'; +export type BiomesNames = 'badlands' | 'bamboo_jungle' | 'basalt_deltas' | 'beach' | 'birch_forest' | 'cherry_grove' | 'cold_ocean' | 'crimson_forest' | 'dark_forest' | 'deep_cold_ocean' | 'deep_dark' | 'deep_frozen_ocean' | 'deep_lukewarm_ocean' | 'deep_ocean' | 'desert' | 'dripstone_caves' | 'end_barrens' | 'end_highlands' | 'end_midlands' | 'eroded_badlands' | 'flower_forest' | 'forest' | 'frozen_ocean' | 'frozen_peaks' | 'frozen_river' | 'grove' | 'ice_spikes' | 'jagged_peaks' | 'jungle' | 'lukewarm_ocean' | 'lush_caves' | 'mangrove_swamp' | 'meadow' | 'mushroom_fields' | 'nether_wastes' | 'ocean' | 'old_growth_birch_forest' | 'old_growth_pine_taiga' | 'old_growth_spruce_taiga' | 'plains' | 'river' | 'savanna' | 'savanna_plateau' | 'small_end_islands' | 'snowy_beach' | 'snowy_plains' | 'snowy_slopes' | 'snowy_taiga' | 'soul_sand_valley' | 'sparse_jungle' | 'stony_peaks' | 'stony_shore' | 'sunflower_plains' | 'swamp' | 'taiga' | 'the_end' | 'the_void' | 'warm_ocean' | 'warped_forest' | 'windswept_forest' | 'windswept_gravelly_hills' | 'windswept_hills' | 'windswept_savanna' | 'wooded_badlands'; +export type EnchantmentNames = 'protection' | 'fire_protection' | 'feather_falling' | 'blast_protection' | 'projectile_protection' | 'respiration' | 'aqua_affinity' | 'thorns' | 'depth_strider' | 'frost_walker' | 'binding_curse' | 'soul_speed' | 'swift_sneak' | 'sharpness' | 'smite' | 'bane_of_arthropods' | 'knockback' | 'fire_aspect' | 'looting' | 'sweeping' | 'efficiency' | 'silk_touch' | 'unbreaking' | 'fortune' | 'power' | 'punch' | 'flame' | 'infinity' | 'luck_of_the_sea' | 'lure' | 'loyalty' | 'impaling' | 'riptide' | 'channeling' | 'multishot' | 'quick_charge' | 'piercing' | 'mending' | 'vanishing_curse'; + +export type EntityMetadataVersions = { +'Mob': {},'Monster': {},'Creeper': {},'Skeleton': {},'Spider': {},'Giant': {},'Zombie': {},'Slime': {},'Ghast': {},'PigZombie': {},'Enderman': {},'CaveSpider': {},'Silverfish': {},'Blaze': {},'LavaSlime': {},'EnderDragon': {},'WitherBoss': {},'Bat': {},'Witch': {},'Endermite': {},'Guardian': {},'Pig': {},'Sheep': {},'Cow': {},'Chicken': {},'Squid': {},'Wolf': {},'MushroomCow': {},'SnowMan': {},'Ozelot': {},'VillagerGolem': {},'EntityHorse': {},'Rabbit': {},'Villager': {},'Boat': {},'Item': {},'MinecartRideable': {},'PrimedTnt': {},'EnderCrystal': {},'Arrow': {},'Snowball': {},'ThrownEgg': {},'Fireball': {},'SmallFireball': {},'ThrownEnderpearl': {},'WitherSkull': {},'FallingSand': {},'ItemFrame': {},'EyeOfEnderSignal': {},'ThrownPotion': {},'ThrownExpBottle': {},'FireworksRocketEntity': {},'LeashKnot': {},'ArmorStand': {},'Fishing Float': {},'Shulker': {},'XPOrb': {},'Dragon Fireball': {},'item': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item': string;},'xp_orb': {},'area_effect_cloud': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'radius': string; +/** 1.19.4+ (9) */ +'color': string; +/** 1.19.4+ (10) */ +'waiting': string; +/** 1.19.4+ (11) */ +'particle': string;},'elder_guardian': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'moving': string; +/** 1.19.4+ (17) */ +'attack_target': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'wither_skeleton': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'stray': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'egg': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'leash_knot': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'painting': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'painting_variant': string;},'arrow': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'flags': string; +/** 1.19.4+ (9) */ +'pierce_level': string; +/** 1.19.4+ (10) */ +'effect_color': string;},'snowball': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'fireball': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'small_fireball': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'ender_pearl': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'eye_of_ender_signal': {},'potion': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'xp_bottle': {},'item_frame': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item': string; +/** 1.19.4+ (9) */ +'rotation': string;},'wither_skull': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'dangerous': string;},'tnt': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'fuse': string; +/** 1.20.3+ (9) */ +'block_state': string;},'falling_block': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'start_pos': string;},'fireworks_rocket': {},'husk': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'special_type': string; +/** 1.19.4+ (18) */ +'drowned_conversion': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'spectral_arrow': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'flags': string; +/** 1.19.4+ (9) */ +'pierce_level': string;},'shulker_bullet': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'dragon_fireball': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'zombie_villager': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'special_type': string; +/** 1.19.4+ (18) */ +'drowned_conversion': string; +/** 1.19.4+ (19) */ +'converting': string; +/** 1.19.4+ (20) */ +'villager_data': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'skeleton_horse': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'zombie_horse': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'armor_stand': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'client_flags': string; +/** 1.19.4+ (16) */ +'head_pose': string; +/** 1.19.4+ (17) */ +'body_pose': string; +/** 1.19.4+ (18) */ +'left_arm_pose': string; +/** 1.19.4+ (19) */ +'right_arm_pose': string; +/** 1.19.4+ (20) */ +'left_leg_pose': string; +/** 1.19.4+ (21) */ +'right_leg_pose': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'donkey': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'chest': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'mule': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'chest': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'evocation_fangs': {},'evocation_illager': {},'vex': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'vindication_illager': {},'commandblock_minecart': {},'boat': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'type': string; +/** 1.19.4+ (12) */ +'paddle_left': string; +/** 1.19.4+ (13) */ +'paddle_right': string; +/** 1.19.4+ (14) */ +'bubble_time': string;},'minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string;},'chest_minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string;},'furnace_minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string; +/** 1.19.4+ (14) */ +'fuel': string;},'tnt_minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string;},'hopper_minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string;},'spawner_minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string;},'creeper': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'swell_dir': string; +/** 1.19.4+ (17) */ +'is_powered': string; +/** 1.19.4+ (18) */ +'is_ignited': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'skeleton': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'stray_conversion': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'spider': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'giant': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'zombie': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'special_type': string; +/** 1.19.4+ (18) */ +'drowned_conversion': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'slime': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'size': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'ghast': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_charging': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'zombie_pigman': {},'enderman': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'carry_state': string; +/** 1.19.4+ (17) */ +'creepy': string; +/** 1.19.4+ (18) */ +'stared_at': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'cave_spider': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'silverfish': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'blaze': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'magma_cube': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'size': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'ender_dragon': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'phase': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'wither': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'target_a': string; +/** 1.19.4+ (17) */ +'target_b': string; +/** 1.19.4+ (18) */ +'target_c': string; +/** 1.19.4+ (19) */ +'inv': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'bat': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'witch': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_celebrating': string; +/** 1.19.4+ (17) */ +'using_item': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'endermite': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'guardian': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'moving': string; +/** 1.19.4+ (17) */ +'attack_target': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'shulker': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'attach_face': string; +/** 1.19.4+ (17) */ +'peek': string; +/** 1.19.4+ (18) */ +'color': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'pig': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'saddle': string; +/** 1.19.4+ (18) */ +'boost_time': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'sheep': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'wool': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'cow': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'chicken': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'squid': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'wolf': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'owneruuid': string; +/** 1.19.4+ (19) */ +'interested': string; +/** 1.19.4+ (20) */ +'collar_color': string; +/** 1.19.4+ (21) */ +'remaining_anger_time': string; +/** 1.20.5+ (10) */ +'effect_particles': string; +/** 1.20.5+ (22) */ +'variant': string;},'mooshroom': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'type': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'snowman': {},'ocelot': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'trusting': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'villager_golem': {},'horse': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'type_variant': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'rabbit': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'type': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'polar_bear': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'standing': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'llama': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'chest': string; +/** 1.19.4+ (19) */ +'strength': string; +/** 1.19.4+ (20) */ +'swag': string; +/** 1.19.4+ (21) */ +'variant': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'llama_spit': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'villager': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'unhappy_counter': string; +/** 1.19.4+ (18) */ +'villager_data': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'ender_crystal': {},'Fishing Hook': {},'illusion_illager': {},'parrot': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'owneruuid': string; +/** 1.19.4+ (19) */ +'variant': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'cod': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'from_bucket': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'dolphin': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'treasure_pos': string; +/** 1.19.4+ (17) */ +'got_fish': string; +/** 1.19.4+ (18) */ +'moistness_level': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'drowned': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'special_type': string; +/** 1.19.4+ (18) */ +'drowned_conversion': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'end_crystal': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'beam_target': string; +/** 1.19.4+ (9) */ +'show_bottom': string;},'evoker_fangs': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'evoker': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_celebrating': string; +/** 1.19.4+ (17) */ +'spell_casting': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'experience_orb': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'eye_of_ender': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'illusioner': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_celebrating': string; +/** 1.19.4+ (17) */ +'spell_casting': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'pufferfish': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'from_bucket': string; +/** 1.19.4+ (17) */ +'puff_state': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'salmon': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'from_bucket': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'snow_golem': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'pumpkin': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'tropical_fish': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'from_bucket': string; +/** 1.19.4+ (17) */ +'type_variant': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'turtle': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'home_pos': string; +/** 1.19.4+ (18) */ +'has_egg': string; +/** 1.19.4+ (19) */ +'laying_egg': string; +/** 1.19.4+ (20) */ +'travel_pos': string; +/** 1.19.4+ (21) */ +'going_home': string; +/** 1.19.4+ (22) */ +'travelling': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'experience_bottle': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item_stack': string;},'iron_golem}': {},'vindicator': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_celebrating': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'phantom': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'size': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'lightning_bolt': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'player': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'player_absorption': string; +/** 1.19.4+ (16) */ +'score': string; +/** 1.19.4+ (17) */ +'player_mode_customisation': string; +/** 1.19.4+ (18) */ +'player_main_hand': string; +/** 1.19.4+ (19) */ +'shoulder_left': string; +/** 1.19.4+ (20) */ +'shoulder_right': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'fishing_bobber': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hooked_entity': string; +/** 1.19.4+ (9) */ +'biting': string;},'trident': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'flags': string; +/** 1.19.4+ (9) */ +'pierce_level': string; +/** 1.19.4+ (10) */ +'loyalty': string; +/** 1.19.4+ (11) */ +'foil': string;},'item_stack': {},'area_effect cloud': {},'activated_tnt': {},'endercrystal': {},'tipped_arrow': {},'firecharge': {},'thrown_enderpearl': {},'falling_objects': {},'item_frames': {},'eye_of ender': {},'thrown_potion': {},'thrown_exp bottle': {},'firework_rocket': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'fireworks_item': string; +/** 1.19.4+ (9) */ +'attached_to_target': string; +/** 1.19.4+ (10) */ +'shot_at_angle': string;},'armorstand': {},'fishing_hook': {},'cat': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'owneruuid': string; +/** 1.19.4+ (19) */ +'variant': string; +/** 1.19.4+ (20) */ +'is_lying': string; +/** 1.19.4+ (21) */ +'relax_state_one': string; +/** 1.19.4+ (22) */ +'collar_color': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'fox': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'type': string; +/** 1.19.4+ (18) */ +'flags': string; +/** 1.19.4+ (19) */ +'trusted_0': string; +/** 1.19.4+ (20) */ +'trusted_1': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'command_block_minecart': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'display_block': string; +/** 1.19.4+ (12) */ +'display_offset': string; +/** 1.19.4+ (13) */ +'custom_display': string; +/** 1.19.4+ (14) */ +'command_name': string; +/** 1.19.4+ (15) */ +'last_output': string;},'panda': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'unhappy_counter': string; +/** 1.19.4+ (18) */ +'sneeze_counter': string; +/** 1.19.4+ (19) */ +'eat_counter': string; +/** 1.19.4+ (20) */ +'main_gene': string; +/** 1.19.4+ (21) */ +'hidden_gene': string; +/** 1.19.4+ (22) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'trader_llama': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'chest': string; +/** 1.19.4+ (19) */ +'strength': string; +/** 1.19.4+ (20) */ +'swag': string; +/** 1.19.4+ (21) */ +'variant': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'iron_golem': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'pillager': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_celebrating': string; +/** 1.19.4+ (17) */ +'is_charging_crossbow': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'wandering_trader': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'unhappy_counter': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'ravager': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'is_celebrating': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'bee': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'remaining_anger_time': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'hoglin': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'immune_to_zombification': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'piglin': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'immune_to_zombification': string; +/** 1.19.4+ (17) */ +'baby': string; +/** 1.19.4+ (18) */ +'is_charging_crossbow': string; +/** 1.19.4+ (19) */ +'is_dancing': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'strider': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'boost_time': string; +/** 1.19.4+ (18) */ +'suffocating': string; +/** 1.19.4+ (19) */ +'saddle': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'zoglin': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'zombified_piglin': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'special_type': string; +/** 1.19.4+ (18) */ +'drowned_conversion': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'piglin_brute': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'immune_to_zombification': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'axolotl': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'variant': string; +/** 1.19.4+ (18) */ +'playing_dead': string; +/** 1.19.4+ (19) */ +'from_bucket': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'glow_item_frame': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'item': string; +/** 1.19.4+ (9) */ +'rotation': string;},'glow_squid': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'dark_ticks_remaining': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'goat': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'is_screaming_goat': string; +/** 1.19.4+ (18) */ +'has_left_horn': string; +/** 1.19.4+ (19) */ +'has_right_horn': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'marker': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string;},'allay': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'dancing': string; +/** 1.19.4+ (17) */ +'can_duplicate': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'chest_boat': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'hurt': string; +/** 1.19.4+ (9) */ +'hurtdir': string; +/** 1.19.4+ (10) */ +'damage': string; +/** 1.19.4+ (11) */ +'type': string; +/** 1.19.4+ (12) */ +'paddle_left': string; +/** 1.19.4+ (13) */ +'paddle_right': string; +/** 1.19.4+ (14) */ +'bubble_time': string;},'frog': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'variant': string; +/** 1.19.4+ (18) */ +'tongue_target': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'tadpole': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'from_bucket': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'warden': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'client_anger_level': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'camel': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'flags': string; +/** 1.19.4+ (18) */ +'dash': string; +/** 1.19.4+ (19) */ +'last_pose_change_tick': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'block_display': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'interpolation_start_delta_ticks': string; +/** 1.19.4+ (9) */ +'interpolation_duration': string; +/** 1.19.4+ (10) */ +'translation': string; +/** 1.19.4+ (11) */ +'scale': string; +/** 1.19.4+ (12) */ +'left_rotation': string; +/** 1.19.4+ (13) */ +'right_rotation': string; +/** 1.19.4+ (14) */ +'billboard_render_constraints': string; +/** 1.19.4+ (15) */ +'brightness_override': string; +/** 1.19.4+ (16) */ +'view_range': string; +/** 1.19.4+ (17) */ +'shadow_radius': string; +/** 1.19.4+ (18) */ +'shadow_strength': string; +/** 1.19.4+ (19) */ +'width': string; +/** 1.19.4+ (20) */ +'height': string; +/** 1.19.4+ (21) */ +'glow_color_override': string; +/** 1.19.4+ (22) */ +'block_state': string; +/** 1.20.2+ (8) */ +'transformation_interpolation_start_delta_ticks': string; +/** 1.20.2+ (9) */ +'transformation_interpolation_duration': string; +/** 1.20.2+ (10) */ +'pos_rot_interpolation_duration': string;},'interaction': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'width': string; +/** 1.19.4+ (9) */ +'height': string; +/** 1.19.4+ (10) */ +'response': string;},'item_display': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'interpolation_start_delta_ticks': string; +/** 1.19.4+ (9) */ +'interpolation_duration': string; +/** 1.19.4+ (10) */ +'translation': string; +/** 1.19.4+ (11) */ +'scale': string; +/** 1.19.4+ (12) */ +'left_rotation': string; +/** 1.19.4+ (13) */ +'right_rotation': string; +/** 1.19.4+ (14) */ +'billboard_render_constraints': string; +/** 1.19.4+ (15) */ +'brightness_override': string; +/** 1.19.4+ (16) */ +'view_range': string; +/** 1.19.4+ (17) */ +'shadow_radius': string; +/** 1.19.4+ (18) */ +'shadow_strength': string; +/** 1.19.4+ (19) */ +'width': string; +/** 1.19.4+ (20) */ +'height': string; +/** 1.19.4+ (21) */ +'glow_color_override': string; +/** 1.19.4+ (22) */ +'item_stack': string; +/** 1.19.4+ (23) */ +'item_display': string; +/** 1.20.2+ (8) */ +'transformation_interpolation_start_delta_ticks': string; +/** 1.20.2+ (9) */ +'transformation_interpolation_duration': string; +/** 1.20.2+ (10) */ +'pos_rot_interpolation_duration': string;},'sniffer': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'living_entity_flags': string; +/** 1.19.4+ (9) */ +'health': string; +/** 1.19.4+ (10) */ +'effect_color': string; +/** 1.19.4+ (11) */ +'effect_ambience': string; +/** 1.19.4+ (12) */ +'arrow_count': string; +/** 1.19.4+ (13) */ +'stinger_count': string; +/** 1.19.4+ (14) */ +'sleeping_pos': string; +/** 1.19.4+ (15) */ +'mob_flags': string; +/** 1.19.4+ (16) */ +'baby': string; +/** 1.19.4+ (17) */ +'state': string; +/** 1.19.4+ (18) */ +'drop_seed_at_tick': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'text_display': { +/** 1.19.4+ (0) */ +'shared_flags': string; +/** 1.19.4+ (1) */ +'air_supply': string; +/** 1.19.4+ (2) */ +'custom_name': string; +/** 1.19.4+ (3) */ +'custom_name_visible': string; +/** 1.19.4+ (4) */ +'silent': string; +/** 1.19.4+ (5) */ +'no_gravity': string; +/** 1.19.4+ (6) */ +'pose': string; +/** 1.19.4+ (7) */ +'ticks_frozen': string; +/** 1.19.4+ (8) */ +'interpolation_start_delta_ticks': string; +/** 1.19.4+ (9) */ +'interpolation_duration': string; +/** 1.19.4+ (10) */ +'translation': string; +/** 1.19.4+ (11) */ +'scale': string; +/** 1.19.4+ (12) */ +'left_rotation': string; +/** 1.19.4+ (13) */ +'right_rotation': string; +/** 1.19.4+ (14) */ +'billboard_render_constraints': string; +/** 1.19.4+ (15) */ +'brightness_override': string; +/** 1.19.4+ (16) */ +'view_range': string; +/** 1.19.4+ (17) */ +'shadow_radius': string; +/** 1.19.4+ (18) */ +'shadow_strength': string; +/** 1.19.4+ (19) */ +'width': string; +/** 1.19.4+ (20) */ +'height': string; +/** 1.19.4+ (21) */ +'glow_color_override': string; +/** 1.19.4+ (22) */ +'text': string; +/** 1.19.4+ (23) */ +'line_width': string; +/** 1.19.4+ (24) */ +'background_color': string; +/** 1.19.4+ (25) */ +'text_opacity': string; +/** 1.19.4+ (26) */ +'style_flags': string; +/** 1.20.2+ (8) */ +'transformation_interpolation_start_delta_ticks': string; +/** 1.20.2+ (9) */ +'transformation_interpolation_duration': string; +/** 1.20.2+ (10) */ +'pos_rot_interpolation_duration': string;},'breeze': { +/** 1.20.3+ (0) */ +'shared_flags': string; +/** 1.20.3+ (1) */ +'air_supply': string; +/** 1.20.3+ (2) */ +'custom_name': string; +/** 1.20.3+ (3) */ +'custom_name_visible': string; +/** 1.20.3+ (4) */ +'silent': string; +/** 1.20.3+ (5) */ +'no_gravity': string; +/** 1.20.3+ (6) */ +'pose': string; +/** 1.20.3+ (7) */ +'ticks_frozen': string; +/** 1.20.3+ (8) */ +'living_entity_flags': string; +/** 1.20.3+ (9) */ +'health': string; +/** 1.20.3+ (10) */ +'effect_color': string; +/** 1.20.3+ (11) */ +'effect_ambience': string; +/** 1.20.3+ (12) */ +'arrow_count': string; +/** 1.20.3+ (13) */ +'stinger_count': string; +/** 1.20.3+ (14) */ +'sleeping_pos': string; +/** 1.20.3+ (15) */ +'mob_flags': string; +/** 1.20.5+ (10) */ +'effect_particles': string;},'wind_charge': { +/** 1.20.3+ (0) */ +'shared_flags': string; +/** 1.20.3+ (1) */ +'air_supply': string; +/** 1.20.3+ (2) */ +'custom_name': string; +/** 1.20.3+ (3) */ +'custom_name_visible': string; +/** 1.20.3+ (4) */ +'silent': string; +/** 1.20.3+ (5) */ +'no_gravity': string; +/** 1.20.3+ (6) */ +'pose': string; +/** 1.20.3+ (7) */ +'ticks_frozen': string;},'armadillo': { +/** 1.20.5+ (0) */ +'shared_flags': string; +/** 1.20.5+ (1) */ +'air_supply': string; +/** 1.20.5+ (2) */ +'custom_name': string; +/** 1.20.5+ (3) */ +'custom_name_visible': string; +/** 1.20.5+ (4) */ +'silent': string; +/** 1.20.5+ (5) */ +'no_gravity': string; +/** 1.20.5+ (6) */ +'pose': string; +/** 1.20.5+ (7) */ +'ticks_frozen': string; +/** 1.20.5+ (8) */ +'living_entity_flags': string; +/** 1.20.5+ (9) */ +'health': string; +/** 1.20.5+ (10) */ +'effect_particles': string; +/** 1.20.5+ (11) */ +'effect_ambience': string; +/** 1.20.5+ (12) */ +'arrow_count': string; +/** 1.20.5+ (13) */ +'stinger_count': string; +/** 1.20.5+ (14) */ +'sleeping_pos': string; +/** 1.20.5+ (15) */ +'mob_flags': string; +/** 1.20.5+ (16) */ +'baby': string; +/** 1.20.5+ (17) */ +'armadillo_state': string;},'bogged': { +/** 1.20.5+ (0) */ +'shared_flags': string; +/** 1.20.5+ (1) */ +'air_supply': string; +/** 1.20.5+ (2) */ +'custom_name': string; +/** 1.20.5+ (3) */ +'custom_name_visible': string; +/** 1.20.5+ (4) */ +'silent': string; +/** 1.20.5+ (5) */ +'no_gravity': string; +/** 1.20.5+ (6) */ +'pose': string; +/** 1.20.5+ (7) */ +'ticks_frozen': string; +/** 1.20.5+ (8) */ +'living_entity_flags': string; +/** 1.20.5+ (9) */ +'health': string; +/** 1.20.5+ (10) */ +'effect_particles': string; +/** 1.20.5+ (11) */ +'effect_ambience': string; +/** 1.20.5+ (12) */ +'arrow_count': string; +/** 1.20.5+ (13) */ +'stinger_count': string; +/** 1.20.5+ (14) */ +'sleeping_pos': string; +/** 1.20.5+ (15) */ +'mob_flags': string; +/** 1.20.5+ (16) */ +'sheared': string;},'breeze_wind_charge': { +/** 1.20.5+ (0) */ +'shared_flags': string; +/** 1.20.5+ (1) */ +'air_supply': string; +/** 1.20.5+ (2) */ +'custom_name': string; +/** 1.20.5+ (3) */ +'custom_name_visible': string; +/** 1.20.5+ (4) */ +'silent': string; +/** 1.20.5+ (5) */ +'no_gravity': string; +/** 1.20.5+ (6) */ +'pose': string; +/** 1.20.5+ (7) */ +'ticks_frozen': string;},'ominous_item_spawner': { +/** 1.20.5+ (0) */ +'shared_flags': string; +/** 1.20.5+ (1) */ +'air_supply': string; +/** 1.20.5+ (2) */ +'custom_name': string; +/** 1.20.5+ (3) */ +'custom_name_visible': string; +/** 1.20.5+ (4) */ +'silent': string; +/** 1.20.5+ (5) */ +'no_gravity': string; +/** 1.20.5+ (6) */ +'pose': string; +/** 1.20.5+ (7) */ +'ticks_frozen': string; +/** 1.20.5+ (8) */ +'item': string;}, +} \ No newline at end of file diff --git a/src/mcTypes.ts b/src/mcTypes.ts index ee87b9c9..ebb4f560 100644 --- a/src/mcTypes.ts +++ b/src/mcTypes.ts @@ -1,108 +1,108 @@ -/* eslint-disable no-multi-spaces */ + // todo move from here //@ts-format-ignore-region // 1.8.8 export interface LevelDat { WorldGenSettings?: WorldGenSettings; - RandomSeed: number[]; - generatorName?: string; - BorderCenterX: number; - BorderCenterZ: number; - Difficulty: number; - DifficultyLocked: number; - BorderSizeLerpTime: number[]; + RandomSeed: number[]; + generatorName?: string; + BorderCenterX: number; + BorderCenterZ: number; + Difficulty: number; + DifficultyLocked: number; + BorderSizeLerpTime: number[]; Version?: { Name: string // id, snapshot } /** 0,1 */ - raining: number; - Time: number[]; - GameType: number; - MapFeatures: number; + raining: number; + Time: number[]; + GameType: number; + MapFeatures: number; BorderDamagePerBlock: number; - BorderWarningBlocks: number; + BorderWarningBlocks: number; BorderSizeLerpTarget: number; - DayTime: number[]; - initialized: number; - allowCommands: number; - SizeOnDisk: number[]; - GameRules: GameRules; - Player: Player; - SpawnY: number; - rainTime: number; - thunderTime: number; - SpawnZ: number; - hardcore: number; - SpawnX: number; - clearWeatherTime: number; - thundering: number; - generatorVersion?: number; - version: number; - BorderSafeZone: number; - generatorOptions?: string; - LastPlayed: number[]; - BorderWarningTime: number; - LevelName: string; - BorderSize: number; + DayTime: number[]; + initialized: number; + allowCommands: number; + SizeOnDisk: number[]; + GameRules: GameRules; + Player: Player; + SpawnY: number; + rainTime: number; + thunderTime: number; + SpawnZ: number; + hardcore: number; + SpawnX: number; + clearWeatherTime: number; + thundering: number; + generatorVersion?: number; + version: number; + BorderSafeZone: number; + generatorOptions?: string; + LastPlayed: number[]; + BorderWarningTime: number; + LevelName: string; + BorderSize: number; } export interface GameRules { - doTileDrops: string; - doFireTick: string; - reducedDebugInfo: string; + doTileDrops: string; + doFireTick: string; + reducedDebugInfo: string; naturalRegeneration: string; - doMobLoot: string; - keepInventory: string; - doEntityDrops: string; - mobGriefing: string; - randomTickSpeed: string; - commandBlockOutput: string; - doMobSpawning: string; - logAdminCommands: string; + doMobLoot: string; + keepInventory: string; + doEntityDrops: string; + mobGriefing: string; + randomTickSpeed: string; + commandBlockOutput: string; + doMobSpawning: string; + logAdminCommands: string; sendCommandFeedback: string; - doDaylightCycle: string; - showDeathMessages: string; + doDaylightCycle: string; + showDeathMessages: string; } export interface Player { - HurtByTimestamp: number; - SleepTimer: number; - Attributes: Attribute[]; - Invulnerable: number; - PortalCooldown: number; - AbsorptionAmount: number; - abilities: Abilities; - FallDistance: number; - DeathTime: number; - XpSeed: number; - HealF: number; - XpTotal: number; - playerGameType: number; - SelectedItem: SelectedItem; - Motion: number[]; - UUIDLeast: number[]; - Health: number; + HurtByTimestamp: number; + SleepTimer: number; + Attributes: Attribute[]; + Invulnerable: number; + PortalCooldown: number; + AbsorptionAmount: number; + abilities: Abilities; + FallDistance: number; + DeathTime: number; + XpSeed: number; + HealF: number; + XpTotal: number; + playerGameType: number; + SelectedItem: SelectedItem; + Motion: number[]; + UUIDLeast: number[]; + Health: number; foodSaturationLevel: number; - Air: number; - OnGround: number; - Dimension: number; - Rotation: number[]; - XpLevel: number; - Score: number; - UUIDMost: number[]; - Sleeping: number; - Pos: number[]; - Fire: number; - XpP: number; - EnderItems: any[]; - foodLevel: number; + Air: number; + OnGround: number; + Dimension: number; + Rotation: number[]; + XpLevel: number; + Score: number; + UUIDMost: number[]; + Sleeping: number; + Pos: number[]; + Fire: number; + XpP: number; + EnderItems: any[]; + foodLevel: number; foodExhaustionLevel: number; - HurtTime: number; - SelectedItemSlot: number; - Inventory: SelectedItem[]; - foodTickTimer: number; + HurtTime: number; + SelectedItemSlot: number; + Inventory: SelectedItem[]; + foodTickTimer: number; } export interface Attribute { @@ -111,55 +111,55 @@ export interface Attribute { } export interface SelectedItem { - Slot?: number; - id: string; - Count: number; + Slot?: number; + id: string; + Count: number; Damage: number; } export interface Abilities { invulnerable: number; - mayfly: number; - instabuild: number; - walkSpeed: number; - mayBuild: number; - flying: number; - flySpeed: number; + mayfly: number; + instabuild: number; + walkSpeed: number; + mayBuild: number; + flying: number; + flySpeed: number; } // 1.16+ export interface WorldGenSettings { /** 0,1 */ - bonus_chest: number; - seed: number[]; + bonus_chest: number; + seed: number[]; /** 0,1 */ generate_features: number; - dimensions: Dimensions; + dimensions: Dimensions; } export interface Dimensions { // :overworld, :the_nether, :the_end - [key: string]: WorldGen; + [key: string]: WorldGen; } export interface WorldGen { generator: WorldGenGenerator; // same as key - type: string; + type: string; } export interface WorldGenGenerator { - settings: string; - seed: number[]; + settings: string; + seed: number[]; biome_source: PurpleBiomeSource; - type: string; + type: string; } export interface PurpleBiomeSource { - seed: number[]; + seed: number[]; /** only for overworld 0,1 */ large_biomes?: number; // :noise, :flat, ? - type: string; + type: string; } diff --git a/src/menus/components/bossbars_overlay.js b/src/menus/components/bossbars_overlay.js deleted file mode 100644 index 015dc6db..00000000 --- a/src/menus/components/bossbars_overlay.js +++ /dev/null @@ -1,150 +0,0 @@ -const { LitElement, html, css } = require('lit') -const { styleMap } = require('lit/directives/style-map.js') - -const colors = ['pink', 'blue', 'red', 'green', 'yellow', 'purple', 'white'] -const divs = [0, 6, 10, 12, 20] -const translations = { - 'entity.minecraft.ender_dragon': 'Ender Dragon', - 'entity.minecraft.wither': 'Wither' -} -class BossBar extends LitElement { - constructor (bar) { - super() - this.bar = bar - this.title = '' - this.progress = 0 - this.bossBarStyles = {} - this.fillStyles = {} - this.div1Styles = {} - this.div2Styles = {} - } - - static get styles () { - return css` - .container { - display: flex; - flex-direction: column; - align-items: center; - } - .title { - font-size: 7px; - color: #fff; - } - .bossbar { - background-image: url("textures/1.18.1/gui/bars.png"); - width: 182px; - height: 5px; - position: relative; - } - .bossbar .fill { - content: ""; - position: absolute; - top: 0; - left: 0; - height: 5px; - width: 0; - background-image: url("textures/1.18.1/gui/bars.png"); - }` - } - - static get properties () { - return { - bar: { type: Object } - } - } - - render () { - this.updateBar(this.bar) - - return html` -
-
${this.title}
-
-
-
-
-
-
- ` - } - - setTitle (bar) { - if (bar._title.text) this.title = bar.title.text - else this.title = translations[bar.title.translate] || 'Unknown Entity' - } - - setColor (bar) { - this.bossBarStyles.backgroundPositionY = `-${colors.indexOf(bar._color) * 10}px` - this.fillStyles.backgroundPositionY = `-${colors.indexOf(bar._color) * 10 + 5}px` - } - - setProgress (bar) { - this.fillStyles.width = `${bar._health * 100}%` - this.div2Styles.width = `${bar._health * 100}%` - } - - setDiv (bar) { - this.div1Styles.backgroundPositionY = `-${divs.indexOf(bar._dividers) * 10 + 70}px` - this.div2Styles.backgroundPositionY = `-${divs.indexOf(bar._dividers) * 10 + 75}px` - } - - updateBar (bar) { - this.setTitle(bar) - this.setColor(bar) - this.setDiv(bar) - this.setProgress(bar) - } -} - -class BossBars extends LitElement { - constructor () { - super() - this.bossBars = new Map() - } - - static get styles () { - return css` - .bossBars { - display: flex; - flex-direction: column; - gap: 5px; - position: absolute; - top: 9px; - left: 50%; - transform: translate(-50%); - }` - } - - static get properties () { - return { - bossBars: { type: Map } - } - } - - render () { - return html` -
- ${[...this.bossBars.values()]} -
- ` - } - - init () { - this.bot.on('bossBarCreated', async (bossBar) => { - this.bossBars.set(bossBar.entityUUID, new BossBar(bossBar)) - this.requestUpdate() - }) - this.bot.on('bossBarUpdated', (bossBar) => { - const bar = this.bossBars.get(bossBar.entityUUID) - bar.bar = bossBar - bar.requestUpdate() - }) - this.bot.on('bossBarDeleted', (bossBar) => { - this.bossBars.delete(bossBar.entityUUID) - this.requestUpdate() - }) - } -} - -window.customElements.define('pmui-bossbars-overlay', BossBars) -window.customElements.define('pmui-bossbar', BossBar) diff --git a/src/menus/components/breath_bar.js b/src/menus/components/breath_bar.js deleted file mode 100644 index b6fcfd9c..00000000 --- a/src/menus/components/breath_bar.js +++ /dev/null @@ -1,89 +0,0 @@ -const { LitElement, html, css, unsafeCSS } = require('lit') -const { guiIcons1_17_1 } = require('../hud') - -class BreathBar extends LitElement { - static get styles () { - return css` - .breathbar { - position: absolute; - display: flex; - flex-direction: row-reverse; - left: calc(50% + 91px); - transform: translate(-100%); - bottom: 40px; - --offset: calc(-1 * 16px); - --bg-x: calc(-1 * 16px); - --bg-y: calc(-1 * 18px); - } - - .breath { - width: 9px; - height: 9px; - margin-left: -1px; - } - - .breath.full { - background-image: url('${unsafeCSS(guiIcons1_17_1)}'); - background-size: 256px; - background-position: var(--offset) var(--bg-y); - } - - .breath.half { - background-image: url('${unsafeCSS(guiIcons1_17_1)}'); - background-size: 256px; - background-position: calc(var(--offset) - 9) var(--bg-y); - } - ` - } - - gameModeChanged () { - this.shadowRoot.querySelector('#breathbar').classList.toggle('creative', bot.game.gameMode === 'creative' || bot.game.gameMode === 'spectator') - } - - updateOxygen (hValue) { - const breathbar = this.shadowRoot.querySelector('#breathbar') - breathbar.style.display = 'block' - - const breaths = breathbar.children - - for (const breath of breaths) { - breath.classList.remove('full') - breath.classList.remove('half') - } - - for (let i = 0; i < Math.ceil(hValue / 2); i++) { - if (i >= breaths.length) break - - if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) { - breaths[i].classList.add('half') - } else { - breaths[i].classList.add('full') - } - } - - // if (hValue === 20) { - // setTimeout(() => { - // breathbar.style.display = 'none' - // }, 1000) - // } - } - - render () { - return html` -
-
-
-
-
-
-
-
-
-
-
-
- ` - } -} - -window.customElements.define('pmui-breathbar', BreathBar) diff --git a/src/menus/components/button.js b/src/menus/components/button.js deleted file mode 100644 index 1f726821..00000000 --- a/src/menus/components/button.js +++ /dev/null @@ -1,136 +0,0 @@ -//@ts-check -import { LitElement, html, css, unsafeCSS } from 'lit' -import widgetsGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png' -import { playSound, loadSound } from '../../basicSounds' - -class Button extends LitElement { - static get styles () { - return css` - .button { - --txrV: 66px; - position: relative; - width: 200px; - height: 20px; - font-family: minecraft, mojangles, monospace; - font-size: 10px; - color: white; - text-shadow: 1px 1px #222; - border: none; - z-index: 1; - outline: none; - display: inline-flex; - justify-content: center; - align-items: center; - } - - .button:hover, - .button:focus-visible { - --txrV: 86px; - } - - .button:disabled { - --txrV: 46px; - color: #A0A0A0; - text-shadow: 1px 1px #111; - } - - .button::after { - content: ''; - display: block; - position: absolute; - top: 0; - left: 0; - width: calc(50% + 1px); - height: 20px; - background: url('${unsafeCSS(widgetsGui)}'); - background-size: 256px; - background-position-y: calc(var(--txrV) * -1); - z-index: -1; - } - - .button::before { - content: ''; - display: block; - position: absolute; - top: 0; - left: 50%; - width: 50%; - height: 20px; - background: url('${unsafeCSS(widgetsGui)}'); - background-size: 256px; - background-position-x: calc(-200px + 100%); - background-position-y: calc(var(--txrV) * -1); - z-index: -1; - } - - .icon { - position: absolute; - top: 3px; - left: 3px; - font-size: 14px; - } - ` - } - - static get properties () { - return { - label: { - type: String, - attribute: 'pmui-label' - }, - width: { - type: String, - attribute: 'pmui-width' - }, - disabled: { - type: Boolean, - }, - onPress: { - type: Function, - attribute: 'pmui-click' - }, - icon: { - type: Function, - attribute: 'pmui-icon' - }, - testId: { - type: String, - attribute: 'pmui-test-id' - } - } - } - - constructor () { - super() - this.label = '' - this.icon = undefined - this.testId = undefined - this.disabled = false - this.width = '200px' - this.onPress = () => { } - } - - render () { - return html` - - ` - } - - onBtnClick (e) { - playSound('button_click.mp3') - this.dispatchEvent(new window.CustomEvent('pmui-click', { detail: e })) - } -} - -loadSound('button_click.mp3') -window.customElements.define('pmui-button', Button) diff --git a/src/menus/components/common.js b/src/menus/components/common.js deleted file mode 100644 index 83c74abd..00000000 --- a/src/menus/components/common.js +++ /dev/null @@ -1,60 +0,0 @@ -import { css } from 'lit' - -const commonCss = css` - .bg { - position: absolute; - top: 0; - left: 0; - background: rgba(0, 0, 0, 0.75); - width: 100%; - height: 100%; - } - - .title { - position: absolute; - top: 0; - left: 50%; - transform: translate(-50%); - font-size: 10px; - color: white; - text-align: center; - text-shadow: 1px 1px #222; - } - - .text { - color: white; - font-size: 10px; - text-shadow: 1px 1px #222; - } -` - -/** @returns {boolean} */ -function isMobile () { - return window.matchMedia('(pointer: coarse)').matches || navigator.userAgent.includes('Mobile') -} - -// todo there are better workarounds and proper way to detect notch -/** @returns {boolean} */ -function isProbablyIphone () { - if (!isMobile()) return false - const smallest = window.innerWidth < window.innerHeight ? window.innerWidth : window.innerHeight - return smallest < 600 -} - -/** - * @param {string} url - */ -function openURL (url, newTab = true) { - if (newTab) { - window.open(url, '_blank', 'noopener,noreferrer') - } else { - window.open(url, '_self') - } -} - -export { - isProbablyIphone, - commonCss, - isMobile, - openURL, -} diff --git a/src/menus/components/debug_overlay.js b/src/menus/components/debug_overlay.js deleted file mode 100644 index 6cee482a..00000000 --- a/src/menus/components/debug_overlay.js +++ /dev/null @@ -1,272 +0,0 @@ -const { LitElement, html, css } = require('lit') -const { subscribeKey } = require('valtio/utils') -const { miscUiState } = require('../../globalState') -const { options } = require('../../optionsStorage') -const { getFixedFilesize } = require('../../downloadAndOpenFile') - -class DebugOverlay extends LitElement { - static get styles () { - return css` - .debug-left-side, - .debug-right-side { - padding-left: calc(env(safe-area-inset-left) / 2); - padding-right: calc(env(safe-area-inset-right) / 2); - position: absolute; - display: flex; - flex-direction: column; - z-index: 40; - pointer-events: none; - } - - .debug-left-side { - top: 1px; - left: 1px; - } - - .debug-right-side { - top: 5px; - right: 1px; - /* limit renderer long text width */ - width: 50%; - } - - p { - display: block; - color: white; - font-size: 10px; - width: fit-content; - line-height: 9px; - margin: 0; - padding: 0; - padding-bottom: 1px; - background: rgba(110, 110, 110, 0.5); - } - - .debug-right-side p { - margin-left: auto; - } - - .empty { - display: block; - height: 9px; - } - ` - } - - static get properties () { - return { - showOverlay: { type: Boolean }, - cursorBlock: { type: Object }, - rendererDevice: { type: String }, - bot: { type: Object }, - customEntries: { type: Object }, - packetsString: { type: String } - } - } - - constructor () { - super() - this.showOverlay = false - this.customEntries = {} - this.packetsString = '' - } - - firstUpdated () { - document.addEventListener('keydown', e => { - if (e.code === 'F3') { - this.showOverlay = !this.showOverlay - e.preventDefault() - } - }) - - let receivedTotal = 0 - let received = { - count: 0, - size: 0 - } - let sent = { - count: 0, - size: 0 - } - const packetsCountByNamePerSec = { - received: {}, - sent: {} - } - const hardcodedListOfDebugPacketsToIgnore = { - received: [ - 'entity_velocity', - 'sound_effect', - 'rel_entity_move', - 'entity_head_rotation', - 'entity_metadata', - 'entity_move_look', - 'teams', - 'entity_teleport', - 'entity_look', - 'ping', - 'entity_update_attributes', - 'player_info', - 'update_time', - 'animation', - 'entity_equipment', - 'entity_destroy', - 'named_entity_spawn', - 'update_light', - 'set_slot', - 'block_break_animation', - 'map_chunk', - 'spawn_entity', - 'world_particles', - 'keep_alive', - 'chat', - 'playerlist_header', - 'scoreboard_objective', - 'scoreboard_score' - ], - sent: [ - 'pong', - 'position', - 'look', - 'keep_alive', - 'position_look' - ] - } // todo cleanup? - const ignoredPackets = new Set('') - Object.defineProperty(window, 'debugTopPackets', { - get () { - return Object.fromEntries(Object.entries(packetsCountByName).map(([s, packets]) => [s, Object.fromEntries(Object.entries(packets).sort(([, n1], [, n2]) => { - return n2 - n1 - }))])) - } - }) - setInterval(() => { - this.packetsString = `↓ ${received.count} (${(received.size / 1024).toFixed(2)} KB/s, ${getFixedFilesize(receivedTotal)}) ↑ ${sent.count}` - received = { - count: 0, - size: 0 - } - sent = { - count: 0, - size: 0 - } - packetsCountByNamePerSec.received = {} - packetsCountByNamePerSec.sent = {} - }, 1000) - const packetsCountByName = { - received: {}, - sent: {} - } - - const managePackets = (type, name, data) => { - packetsCountByName[type][name] ??= 0 - packetsCountByName[type][name]++ - if (options.debugLogNotFrequentPackets && !ignoredPackets.has(name) && !hardcodedListOfDebugPacketsToIgnore[type].includes(name)) { - packetsCountByNamePerSec[type][name] ??= 0 - packetsCountByNamePerSec[type][name]++ - if (packetsCountByNamePerSec[type][name] > 5 || packetsCountByName[type][name] > 100) { // todo think of tracking the count within 10s - console.info(`[packet ${name} was ${type} too frequent] Ignoring...`) - ignoredPackets.add(name) - } else { - console.info(`[packet ${type}] ${name}`, /* ${JSON.stringify(data, null, 2)}` */ data) - } - } - } - - subscribeKey(miscUiState, 'gameLoaded', () => { - if (!miscUiState.gameLoaded) return - packetsCountByName.received = {} - packetsCountByName.sent = {} - const readPacket = (data, { name }, _buf, fullBuffer) => { - if (fullBuffer) { - const size = fullBuffer.byteLength - receivedTotal += size - received.size += size - } - received.count++ - managePackets('received', name, data) - } - bot._client.on('packet', readPacket) - bot._client.on('packet_name', (name, data) => readPacket(data, { name })) // custom client - bot._client.on('writePacket', (name, data) => { - sent.count++ - managePackets('sent', name, data) - }) - }) - } - - updated (changedProperties) { - if (changedProperties.has('bot')) { - this.bot.on('move', () => { - this.requestUpdate() - }) - this.bot.on('time', () => { - this.requestUpdate() - }) - this.bot.on('entitySpawn', () => { - this.requestUpdate() - }) - this.bot.on('entityGone', () => { - this.requestUpdate() - }) - } - } - - render () { - if (!this.showOverlay) { - return html`` - } - - const target = this.cursorBlock - - const pos = this.bot.entity.position - const rot = [this.bot.entity.yaw, this.bot.entity.pitch] - - const viewDegToMinecraft = (yaw) => yaw % 360 - 180 * (yaw < 0 ? -1 : 1) - - const quadsDescription = [ - 'north (towards negative Z)', - 'east (towards positive X)', - 'south (towards positive Z)', - 'west (towards negative X)' - ] - - const minecraftYaw = viewDegToMinecraft(rot[0] * -180 / Math.PI) - const minecraftQuad = Math.floor(((minecraftYaw + 180) / 90 + 0.5) % 4) - - const renderProp = (name, value) => { - return html`

${name}: ${typeof value === 'boolean' ? html`${value}` : value}

` - } - - const skyL = this.bot.world.getSkyLight(this.bot.entity.position) - const biomeId = this.bot.world.getBiome(this.bot.entity.position) - - return html` -
-

Prismarine Web Client (${this.bot.version})

-

E: ${Object.values(this.bot.entities).length}

-

${this.bot.game.dimension}

-
-

XYZ: ${pos.x.toFixed(3)} / ${pos.y.toFixed(3)} / ${pos.z.toFixed(3)}

-

Chunk: ${Math.floor(pos.x % 16)} ~ ${Math.floor(pos.z % 16)} in ${Math.floor(pos.x / 16)} ~ ${Math.floor(pos.z / 16)}

-

Packets: ${this.packetsString}

-

Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}

-

Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})

-

Light: ${skyL} (${skyL} sky)

- -

Biome: minecraft:${window.loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}

-

Day: ${this.bot.time.day}

-
- ${Object.entries(this.customEntries).map(([name, value]) => html`

${name}: ${value}

`)} -
- -
-

Renderer: ${this.rendererDevice} powered by three.js r${global.THREE.REVISION}

-
- ${target ? html`

${target.name}

${Object.entries(target.getProperties()).map(([n, p], idx, arr) => renderProp(n, p, arr[idx + 1]))}` : ''} - ${target ? html`

Looking at: ${target.position.x} ${target.position.y} ${target.position.z}

` : ''} -
- ` - } -} - -window.customElements.define('pmui-debug-overlay', DebugOverlay) diff --git a/src/menus/components/edit_box.js b/src/menus/components/edit_box.js deleted file mode 100644 index c7210d43..00000000 --- a/src/menus/components/edit_box.js +++ /dev/null @@ -1,161 +0,0 @@ -const { LitElement, html, css } = require('lit') -const { ifDefined } = require('lit/directives/if-defined.js') - -class EditBox extends LitElement { - static get styles () { - return css` - .edit-container { - position: relative; - width: 200px; - height: 20px; - background: black; - border: 1px solid grey; - } - .edit-container.invalid { - border: 1px solid #c70000; - } - - .edit-container.warning { - border: 1px solid rgb(159, 151, 0); - } - - .edit-container.invalid:hover, - .edit-container.invalid:focus-within { - border-color: red; - } - .edit-container.warning:hover, - .edit-container.warning:focus-within { - border-color: yellow; - } - - .edit-container:hover, - .edit-container:focus-within { - border-color: white; - } - - .edit-container label { - position: absolute; - z-index: 2; - pointer-events: none; - bottom: 21px; - left: 0; - font-size: 10px; - color: rgb(206, 206, 206); - text-shadow: 1px 1px black; - } - - .edit-box { - position: relative; - outline: none; - border: none; - background: none; - left: 1px; - width: calc(100% - 2px); - height: 100%; - font-family: minecraft, mojangles, monospace; - font-size: 10px; - color: white; - text-shadow: 1px 1px #222; - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - /* Firefox */ - input[type=number] { - appearance: textfield; - -moz-appearance: textfield; - } - ` - } - - constructor () { - super() - this.width = '200px' - this.id = '' - this.value = '' - this.label = '' - this.required = false - } - - static get properties () { - return { - width: { - type: String, - attribute: 'pmui-width' - }, - id: { - type: String, - attribute: 'pmui-id' - }, - label: { - type: String, - attribute: 'pmui-label' - }, - value: { - type: String, - attribute: 'pmui-value' - }, - autocompleteValues: { - type: Array, - }, - type: { - type: String, - attribute: 'pmui-type' - }, - inputMode: { - type: String, - attribute: 'pmui-inputmode' - }, - required: { - type: Boolean, - attribute: 'pmui-required' - }, - placeholder: { - type: String, - attribute: 'pmui-placeholder' - }, - state: { - type: String, - attribute: true - } - } - } - - render () { - return html` -
- - ${this.autocompleteValues ? html` - - ${this.autocompleteValues.map(value => html` - - `)} - - ` : ''} - { this.value = this.inputMode === 'decimal' ? value.replaceAll(',', '.') : value }} - class="edit-box"> -
- ` - } -} - -window.customElements.define('pmui-editbox', EditBox) diff --git a/src/menus/components/food_bar.js b/src/menus/components/food_bar.js deleted file mode 100644 index f64413fe..00000000 --- a/src/menus/components/food_bar.js +++ /dev/null @@ -1,122 +0,0 @@ -const { LitElement, html, css, unsafeCSS } = require('lit') -const { guiIcons1_17_1 } = require('../hud') - -class FoodBar extends LitElement { - static get styles () { - return css` - .foodbar { - position: absolute; - display: flex; - flex-direction: row-reverse; - left: calc(50% + 91px); - transform: translate(-100%); - bottom: 30px; - --lightened: 0; - --offset: calc(-1 * (52px)); - --bg-x: calc(-1 * (16px + 9px * var(--lightened))); - --bg-y: calc(-1 * 27px); - } - - .food { - width: 9px; - height: 9px; - background-image: url('${unsafeCSS(guiIcons1_17_1)}'), url('${unsafeCSS(guiIcons1_17_1)}'); - background-size: 256px, 256px; - background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y); - margin-left: -1px; - } - - .food.full { - background-position: var(--offset) var(--bg-y), var(--bg-x) var(--bg-y); - } - - .food.half { - background-position: calc(var(--offset) - 9px) var(--bg-y), var(--bg-x) var(--bg-y); - } - - .foodbar.low .food { - animation: lowHungerAnim 0.2s steps(2, end) infinite; - } - - .foodbar.low .food:nth-of-type(2n) { - animation-direction: reverse; - } - - .foodbar.low .food:nth-of-type(3n) { - animation-duration: 0.1s; - } - - .foodbar.updated { - animation: updatedAnim 0.3s steps(2, end) 2; - } - - .creative { - display: none; - } - - @keyframes lowHungerAnim { - to { transform: translateY(1px); } - } - - @keyframes updatedAnim { - to { --lightened: 1; } - } - ` - } - - gameModeChanged (gamemode) { - this.shadowRoot.querySelector('#foodbar').classList.toggle('creative', gamemode === 1) - } - - onHungerUpdate () { - this.shadowRoot.querySelector('#foodbar').classList.toggle('updated', true) - if (this.hungerTimeout) clearTimeout(this.hungerTimeout) - this.hungerTimeout = setTimeout(() => { - this.shadowRoot.querySelector('#foodbar').classList.toggle('updated', false) - this.hungerTimeout = null - }, 1000) - } - - updateHunger (hValue, d) { - const foodbar = this.shadowRoot.querySelector('#foodbar') - foodbar.classList.toggle('low', hValue <= 5) - - const foods = foodbar.children - - for (const food of foods) { - food.classList.remove('full') - food.classList.remove('half') - } - - // if (d) this.onHungerUpdate() - - for (let i = 0; i < Math.ceil(hValue / 2); i++) { - if (i >= foods.length) break - - if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) { - foods[i].classList.add('half') - } else { - foods[i].classList.add('full') - } - } - } - - render () { - return html` -
-
-
-
-
-
-
-
-
-
-
-
- ` - } -} - -window.customElements.define('pmui-foodbar', FoodBar) diff --git a/src/menus/components/health_bar.js b/src/menus/components/health_bar.js deleted file mode 100644 index c49c510b..00000000 --- a/src/menus/components/health_bar.js +++ /dev/null @@ -1,163 +0,0 @@ -const { LitElement, html, css, unsafeCSS } = require('lit') -const { guiIcons1_17_1 } = require('../hud') - -function getEffectClass (effect) { - switch (effect.id) { - case 19: return 'poisoned' - case 20: return 'withered' - case 22: return 'absorption' - default: return '' - } -} - -class HealthBar extends LitElement { - static get styles () { - return css` - .health { - position: fixed; - display: flex; - flex-direction: row; - left: calc(50% - 91px); - bottom: 30px; - --hardcore: 0; - --kind: 0; - --lightened: 0; - --offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2)) )); - --bg-x: calc(-1 * (16px + 9px * var(--lightened))); - --bg-y: calc(-1 * var(--hardcore) * 45px); - } - - .health.creative { - display: none; - } - - .health.hardcore { - --hardcore: 1; - } - - .health.poisoned { - --kind: 1; - } - - .health.withered { - --kind: 2; - } - - .health.absorption { - --kind: 3; - } - - .heart { - width: 9px; - height: 9px; - background-image: url('${unsafeCSS(guiIcons1_17_1)}'), url('${unsafeCSS(guiIcons1_17_1)}'); - background-size: 256px, 256px; - background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y); - margin-left: -1px; - } - - .heart.full { - background-position: var(--offset) var(--bg-y), var(--bg-x) var(--bg-y); - } - - .heart.half { - background-position: calc(var(--offset) - 9px) var(--bg-y), var(--bg-x) var(--bg-y); - } - - .health.low .heart { - animation: lowHealthAnim 0.2s steps(2, end) infinite; - } - - .health.low .heart:nth-of-type(2n) { - animation-direction: reverse; - } - - .health.low .heart:nth-of-type(3n) { - animation-duration: 0.1s; - } - - .health.damaged { - animation: damagedAnim 0.3s steps(2, end) 2; - } - - @keyframes lowHealthAnim { - to { - transform: translateY(1px); - } - } - - @keyframes damagedAnim { - to { --lightened: 1; } - } - ` - } - - effectAdded (effect) { - const effectClass = getEffectClass(effect) - if (!effectClass) return - this.shadowRoot.querySelector('#health').classList.add(effectClass) - } - - effectEnded (effect) { - const effectClass = getEffectClass(effect) - if (!effectClass) return - this.shadowRoot.querySelector('#health').classList.remove(effectClass) - } - - onDamage () { - this.shadowRoot.querySelector('#health').classList.toggle('damaged', true) - if (this.hurtTimeout) clearTimeout(this.hurtTimeout) - this.hurtTimeout = setTimeout(() => { - this.shadowRoot.querySelector('#health').classList.toggle('damaged', false) - this.hurtTimeout = null - }, 1000) - } - - gameModeChanged (gamemode, hardcore) { - this.shadowRoot.querySelector('#health').classList.toggle('creative', bot.game.gameMode === 'creative' || bot.game.gameMode === 'spectator') - this.shadowRoot.querySelector('#health').classList.toggle('hardcore', hardcore) - } - - updateHealth (hValue, d) { - const health = this.shadowRoot.querySelector('#health') - health.classList.toggle('low', hValue <= 4) - - const hearts = health.children - - for (const heart of hearts) { - heart.classList.remove('full') - heart.classList.remove('half') - } - - if (d) this.onDamage() - - for (let i = 0; i < Math.ceil(hValue / 2); i++) { - if (i >= hearts.length) break - - if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) { - hearts[i].classList.add('half') - } else { - hearts[i].classList.add('full') - } - } - } - - render () { - return html` -
-
-
-
-
-
-
-
-
-
-
-
- ` - } -} - -window.customElements.define('pmui-healthbar', HealthBar) diff --git a/src/menus/components/hotbar.js b/src/menus/components/hotbar.js deleted file mode 100644 index b4d14b5b..00000000 --- a/src/menus/components/hotbar.js +++ /dev/null @@ -1,206 +0,0 @@ -const { LitElement, html, css, unsafeCSS } = require('lit') -const widgetsTexture = require('minecraft-assets/minecraft-assets/data/1.16.4/gui/widgets.png') -const { subscribeKey } = require('valtio/utils') -const invsprite = require('../../invsprite.json') -const { isGameActive, miscUiState, showModal } = require('../../globalState') - -const { openPlayerInventory, renderSlotExternal } = require('../../playerWindows') -const { isProbablyIphone } = require('./common') - -class Hotbar extends LitElement { - static get styles () { - return css` - .hotbar { - position: fixed; - /* todo use env(safe-area-inset-bottom) instead */ - bottom: ${unsafeCSS(isProbablyIphone() ? '40px' : '0')}; - left: 50%; - transform: translate(-50%); - width: 182px; - height: 22px; - background: url("${unsafeCSS(widgetsTexture)}"); - background-size: 256px; - } - - #hotbar-selected { - position: absolute; - left: -1px; - top: -1px; - width: 24px; - height: 24px; - background: url("${unsafeCSS(widgetsTexture)}"); - background-size: 256px; - background-position-y: -22px; - } - - #hotbar-items-wrapper { - position: absolute; - top: 0; - left: 1px; - display: flex; - flex-direction: row; - height: 22px; - margin: 0; - padding: 0; - } - - .hotbar-item { - position: relative; - width: 20px; - height: 22px; - } - - .item-icon { - top: 3px; - left: 2px; - position: absolute; - width: 32px; - height: 32px; - transform-origin: top left; - transform: scale(0.5); - background-size: 1024px auto; - } - - .item-stack { - position: absolute; - color: white; - font-size: 10px; - text-shadow: 1px 1px 0 rgb(63, 63, 63); - right: 1px; - bottom: 1px; - } - - #hotbar-item-name { - color: white; - position: absolute; - bottom: 51px; - left: 50%; - transform: translate(-50%); - text-shadow: rgb(63, 63, 63) 1px 1px 0px; - font-family: mojangles, minecraft, monospace; - font-size: 10px; - text-align: center; - } - - .hotbar-item-name-fader { - opacity: 0; - transition: visibility 0s, opacity 1s linear; - transition-delay: 2s; - } - - .hotbar-more { - display:flex; - justify-content: center; - border: 1px solid white; - } - .hotbar-more::before { - content: '...'; - margin-top: -1px; - } - ` - } - - static get properties () { - return { - activeItemName: { type: String }, - } - } - - constructor () { - super() - subscribeKey(miscUiState, 'currentTouch', () => { - this.requestUpdate() - }) - this.activeItemName = '' - - document.addEventListener('wheel', (e) => { - if (!isGameActive(true)) return - e.preventDefault() - const newSlot = ((bot.quickBarSlot + Math.sign(e.deltaY)) % 9 + 9) % 9 - this.reloadHotbarSelected(newSlot) - }, { - passive: false, - }) - - document.addEventListener('keydown', (e) => { - if (!isGameActive(true)) return - const numPressed = +((/Digit(\d)/.exec(e.code))?.[1] ?? -1) - if (numPressed < 1 || numPressed > 9) return - this.reloadHotbarSelected(numPressed - 1) - }) - } - - init () { - this.reloadHotbar() - this.reloadHotbarSelected(0) - - bot.inventory.on('updateSlot', (slot, oldItem, newItem) => { - if (slot >= bot.inventory.hotbarStart + 9) return - if (slot < bot.inventory.hotbarStart) return - - this.reloadHotbar(slot - bot.inventory.hotbarStart) - }) - } - - reloadHotbar (onlySlot = undefined) { - for (let i = 0; i < 9; i++) { - if (onlySlot !== undefined && onlySlot !== i) continue - const item = bot.inventory.slots[bot.inventory.hotbarStart + i] - const slotEl = this.shadowRoot.getElementById('hotbar-' + i) - const slotIcon = slotEl.children[0] - const slotStack = slotEl.children[1] - const data = item ? renderSlotExternal(item) : { sprite: [invsprite.air.x, invsprite.air.y] } - if (item) item.displayName = data?.displayName ?? item.displayName - if (data?.imageDataUrl) { - slotIcon.style['background-image'] = `url('${data.imageDataUrl}')` - } else { - slotIcon.style['background-image'] = `url('invsprite.png')` - } - const [x, y] = data?.sprite ?? [0, 0] - slotIcon.style['background-position-x'] = `-${x}px` - slotIcon.style['background-position-y'] = `-${y}px` - slotStack.textContent = item?.count > 1 ? item.count : '' - } - } - - async reloadHotbarSelected (slot) { - const item = bot.inventory.slots[bot.inventory.hotbarStart + slot] - const newLeftPos = (-1 + 20 * slot) + 'px' - this.shadowRoot.getElementById('hotbar-selected').style.left = newLeftPos - bot.setQuickBarSlot(slot) - // todo highlight on item type change - this.activeItemName = item?.displayName ?? '' - const name = this.shadowRoot.getElementById('hotbar-item-name') - name.classList.remove('hotbar-item-name-fader') - setTimeout(() => name.classList.add('hotbar-item-name-fader'), 10) - } - - render () { - return html` -
-

${this.activeItemName}

-
-
{ - if (!e.target.id.startsWith('hotbar')) return - const slot = +e.target.id.split('-')[1] - this.reloadHotbarSelected(slot) - }}> - ${Array.from({ length: 9 }).map((_, i) => html` -
{ - this.reloadHotbarSelected(i) - }}> -
- -
- `)} - ${miscUiState.currentTouch ? html`
{ - openPlayerInventory() - }}>` : undefined} -
-
-
- ` - } -} - -window.customElements.define('pmui-hotbar', Hotbar) diff --git a/src/menus/components/playerlist_overlay.js b/src/menus/components/playerlist_overlay.js deleted file mode 100644 index b4dd0ba1..00000000 --- a/src/menus/components/playerlist_overlay.js +++ /dev/null @@ -1,185 +0,0 @@ -const { LitElement, html, css } = require('lit') -const { isGameActive } = require('../../globalState') - -const MAX_ROWS_PER_COL = 10 - -class PlayerListOverlay extends LitElement { - static get styles () { - return css` - .playerlist-container { - position: absolute; - background-color: rgba(0, 0, 0, 0.3); - top: 9px; - left: 50%; - transform: translate(-50%); - width: fit-content; - padding: 1px; - display: flex; - flex-direction: column; - gap: 1px 0; - place-items: center; - z-index: 30; - } - - .title { - color: white; - text-shadow: 1px 1px 0px #3f3f3f; - font-size: 10px; - margin: 0; - padding: 0; - } - - .playerlist-entry { - overflow: hidden; - color: white; - font-size: 10px; - margin: 0px; - line-height: calc(100% - 1px); - text-shadow: 1px 1px 0px #3f3f3f; - font-family: mojangles, minecraft, monospace; - background: rgba(255, 255, 255, 0.1); - width: 100%; - } - - .active-player { - color: rgb(42, 204, 237); - text-shadow: 1px 1px 0px rgb(4, 44, 67); - } - - .playerlist-ping { - text-align: right; - float: right; - padding-left: 10px; - } - - .playerlist-ping-value { - color: rgb(114, 255, 114); - text-shadow: 1px 1px 0px rgb(28, 105, 28); - float: left; - margin: 0; - margin-right: 1px; - } - - .playerlist-ping-label { - text-shadow: 1px 1px 0px #3f3f3f; - color: white; - float: right; - margin: 0px; - } - - .player-lists { - display: flex; - flex-direction: row; - place-items: center; - place-content: center; - gap: 0 4px; - } - - .player-list { - display: flex; - flex-direction: column; - gap: 1px 0; - min-width: 80px; - } - ` - } - - static get properties () { - return { - serverIP: { type: String }, - clientId: { type: String }, - players: { type: Object } - } - } - - constructor () { - super() - this.serverIP = '' - this.clientId = '' - this.players = {} - } - - init (ip) { - const playerList = this.shadowRoot.querySelector('#playerlist-container') - - this.isOpen = false - this.players = bot.players - if (bot.player) { - this.clientId = bot.player.uuid - } else { - bot._client.on('player_info', () => { - this.clientId = bot.player?.uuid - }) - } - this.serverIP = ip - - this.requestUpdate() - - const showList = (shouldShow = true) => { - playerList.style.display = shouldShow ? 'block' : 'none' - this.isOpen = shouldShow - } - - document.addEventListener('keydown', e => { - if (!isGameActive(true)) return - if (e.key === 'Tab') { - showList(true) - e.preventDefault() - } - }) - - document.addEventListener('keyup', e => { - if (!this.isOpen) return - if (e.key === 'Tab') { - showList(false) - e.preventDefault() - } - }) - - bot.on('playerUpdated', () => this.requestUpdate()) // LitElement seems to be batching requests, so it should be fine? - bot.on('playerJoined', () => this.requestUpdate()) - bot.on('playerLeft', () => this.requestUpdate()) - } - - render () { - const lists = [] - const players = Object.values(this.players).sort((a, b) => { - if (a.username > b.username) return 1 - if (a.username < b.username) return -1 - return 0 - }) - - let tempList = [] - for (let i = 0; i < players.length; i++) { - tempList.push(players[i]) - - if ((i + 1) / MAX_ROWS_PER_COL === 1 || i + 1 === players.length) { - lists.push([...tempList]) - tempList = [] - } - } - - return html` - - ` - } -} - -window.customElements.define('pmui-playerlist-overlay', PlayerListOverlay) diff --git a/src/menus/hud.js b/src/menus/hud.js deleted file mode 100644 index 17537e0e..00000000 --- a/src/menus/hud.js +++ /dev/null @@ -1,258 +0,0 @@ -import { f3Keybinds } from '../controls' -import { showOptionsModal } from '../react/SelectOption' - -const { LitElement, html, css, unsafeCSS } = require('lit') -const { showModal, miscUiState, activeModalStack, hideCurrentModal } = require('../globalState') -const { options, watchValue } = require('../optionsStorage') -const { getGamemodeNumber } = require('../utils') -const { isMobile } = require('./components/common') - -export const guiIcons1_17_1 = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/icons.png') -export const guiIcons1_16_4 = require('minecraft-assets/minecraft-assets/data/1.16.4/gui/icons.png') - -class Hud extends LitElement { - static get styles () { - return css` - :host { - position: fixed; - top: 0; - left: 0; - z-index: -2; - width: 100%; - height: 100vh; - touch-action: none; - } - - .crosshair { - width: 16px; - height: 16px; - background: url('${unsafeCSS(guiIcons1_17_1)}'); - background-size: calc(256px * var(--crosshair-scale)); - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 2; - } - - #xp-label { - position: fixed; - top: -8px; - left: 50%; - transform: translate(-50%); - font-size: 10px; - font-family: minecraft, mojangles, monospace; - color: rgb(30, 250, 30); - text-shadow: 0px -1px #000, 0px 1px #000, 1px 0px #000, -1px 0px #000; - z-index: 10; - } - - #xp-bar-bg { - position: fixed; - left: 50%; - bottom: 24px; - transform: translate(-50%); - width: 182px; - height: 5px; - background-image: url('${unsafeCSS(guiIcons1_16_4)}'); - background-size: 256px; - background-position-y: -64px; - } - - .xp-bar { - width: 182px; - height: 5px; - background-image: url('${unsafeCSS(guiIcons1_17_1)}'); - background-size: 256px; - background-position-y: -69px; - } - - .mobile-top-btns { - display: none; - flex-direction: row; - position: fixed; - top: 0; - left: 50%; - transform: translate(-50%); - gap: 0 5px; - z-index: 20; - } - - .pause-btn, - .chat-btn { - --scale: 1.3; - border: none; - outline: 0.5px solid white; - width: calc(14px * var(--scale)); - height: calc(14px * var(--scale)); - background-image: url('extra-textures/gui.png'); - background-size: calc(256px * var(--scale)); - background-position-x: calc(var(--scale) * -202px); - background-position-y: calc(var(--scale) * -66px); - } - - .chat-btn { - background-position-y: calc(var(--scale) * -84px); - } - .debug-btn { - background: #9c8c86; - font-size: 8px; - /* todo make other buttons centered */ - /* margin-right: 5px; */ - color: white; - font-family: minecraft, mojangles, monospace; - padding: 4px 6px; - outline: 0.5px solid white; - } - ` - } - - static get properties () { - return { - bot: { type: Object } - } - } - - firstUpdated () { - this.isReady = true - window.dispatchEvent(new CustomEvent('hud-ready', { detail: this })) - - watchValue(miscUiState, o => { - this.showMobileControls(o.currentTouch) - //@ts-expect-error - this.shadowRoot.host.style.display = o.gameLoaded ? 'block' : 'none' - }) - } - - /** - * @param {import('mineflayer').Bot} bot - */ - preload (bot) { - const bossBars = this.shadowRoot.getElementById('bossbars-overlay') - bossBars.bot = bot - - bossBars.init() - } - - /** - * @param {globalThis.THREE.Renderer} renderer - * @param {import('mineflayer').Bot} bot - * @param {string} host - */ - init (renderer, bot, host) { - const debugMenu = this.shadowRoot.querySelector('#debug-overlay') - const playerList = this.shadowRoot.querySelector('#playerlist-overlay') - const healthbar = this.shadowRoot.querySelector('#health-bar') - const foodbar = this.shadowRoot.querySelector('#food-bar') - // const breathbar = this.shadowRoot.querySelector('#breath-bar') - const chat = this.shadowRoot.querySelector('#chat') - const hotbar = this.shadowRoot.querySelector('#hotbar') - const xpLabel = this.shadowRoot.querySelector('#xp-label') - - this.bot = bot - debugMenu.bot = bot - - hotbar.init() - playerList.init(host) - - bot.on('entityHurt', (entity) => { - if (entity !== bot.entity) return - healthbar.onDamage() - }) - - bot.on('entityEffect', (entity, effect) => { - if (entity !== bot.entity) return - healthbar.effectAdded(effect) - }) - - bot.on('entityEffectEnd', (entity, effect) => { - if (entity !== bot.entity) return - healthbar.effectEnded(effect) - }) - - const onGameModeChange = () => { - const gamemode = getGamemodeNumber(bot) - healthbar.gameModeChanged(gamemode, bot.game.hardcore) - foodbar.gameModeChanged(gamemode) - // breathbar.gameModeChanged(gamemode) - const creativeLike = gamemode === 1 || gamemode === 3 - this.shadowRoot.querySelector('#xp-bar-bg').style.display = creativeLike ? 'none' : 'block' - } - bot.on('game', onGameModeChange) - onGameModeChange() - - const onHealthUpdate = () => { - healthbar.updateHealth(bot.health, true) - foodbar.updateHunger(bot.food, true) - } - bot.on('health', onHealthUpdate) - onHealthUpdate() - - const onXpUpdate = () => { - // @ts-expect-error - this.shadowRoot.querySelector('#xp-bar-bg').firstElementChild.style.width = `${182 * bot.experience.progress}px` - xpLabel.innerHTML = String(bot.experience.level) - xpLabel.style.display = bot.experience.level > 0 ? 'block' : 'none' - } - bot.on('experience', onXpUpdate) - onXpUpdate() - - // bot.on('breath', () => { - // breathbar.updateOxygen(bot.oxygenLevel) - // }) - - // TODO - // breathbar.updateOxygen(bot.oxygenLevel ?? 20) - } - - /** @param {boolean} bl */ - showMobileControls (bl) { - this.shadowRoot.querySelector('#mobile-top').style.display = bl ? 'flex' : 'none' - } - - render () { - return html` - -
-
{ - window.dispatchEvent(new MouseEvent('mousedown', { button: 1 })) - }}>S
-
{ - const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => f3Keybind.mobileTitle).map(f3Keybind => f3Keybind.mobileTitle)) - if (!select) return - const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select) - f3Keybind.action() - }} @pointerdown=${(e) => { - this.shadowRoot.getElementById('debug-overlay').showOverlay = !this.shadowRoot.getElementById('debug-overlay').showOverlay - }}>F3
-
{ - e.stopPropagation() - if (activeModalStack.at(-1)?.reactType === 'chat') { - hideCurrentModal() - } else { - showModal({ reactType: 'chat' }) - } - }}>
-
{ - e.stopPropagation() - showModal(document.getElementById('pause-screen')) - }}>
-
- - - - -
- - - -
-
- -
- - ` - } -} - -window.customElements.define('pmui-hud', Hud) diff --git a/src/menus/keybinds_screen.js b/src/menus/keybinds_screen.js deleted file mode 100644 index 739199d9..00000000 --- a/src/menus/keybinds_screen.js +++ /dev/null @@ -1,173 +0,0 @@ -const { LitElement, html, css } = require('lit') -const { hideCurrentModal } = require('../globalState') -const { commonCss } = require('./components/common') - -class KeyBindsScreen extends LitElement { - static get styles () { - return css` - ${commonCss} - .title { - top: 4px; - } - - main { - display: flex; - flex-direction: column; - position: absolute; - top: 30px; - left: 50%; - transform: translate(-50%); - width: 100%; - height: calc(100% - 64px); - place-items: center; - background: rgba(0, 0, 0, 0.5); - box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.7), inset 0 -3px 6px rgba(0, 0, 0, 0.7); - } - - .keymap-list { - width: 288px; - display: flex; - flex-direction: column; - padding: 4px 0; - overflow-y: auto; - } - - .keymap-list::-webkit-scrollbar { - width: 6px; - } - - .keymap-list::-webkit-scrollbar-track { - background: #000; - } - - .keymap-list::-webkit-scrollbar-thumb { - background: #ccc; - box-shadow: inset -1px -1px 0 #4f4f4f; - } - - .keymap-entry { - display: flex; - flex-direction: row; - width: 100%; - height: 20px; - place-content: center; - place-items: center; - justify-content: space-between; - } - - span { - color: white; - text-shadow: 1px 1px 0 rgb(63, 63, 63); - font-size: 10px; - } - - .keymap-entry-btns { - display: flex; - flex-direction: row; - gap: 4px; - } - - .bottom-btns { - display: flex; - flex-direction: row; - width: 310px; - height: 20px; - justify-content: space-between; - position: absolute; - bottom: 9px; - left: 50%; - transform: translate(-50%); - } - - ` - } - - static get properties () { - return { - keymaps: { type: Object }, - selected: { type: Number } - } - } - - constructor () { - super() - this.selected = -1 - this.keymaps = [ - { defaultKey: 'KeyW', key: 'KeyW', name: 'Walk Forwards' }, - { defaultKey: 'KeyS', key: 'KeyS', name: 'Walk Backwards' }, - { defaultKey: 'KeyA', key: 'KeyA', name: 'Strafe Left' }, - { defaultKey: 'KeyD', key: 'KeyD', name: 'Strafe Right' }, - { defaultKey: 'Space', key: 'Space', name: 'Jump' }, - { defaultKey: 'ShiftLeft', key: 'ShiftLeft', name: 'Sneak' }, - { defaultKey: 'ControlLeft', key: 'ControlLeft', name: 'Sprint' }, - { defaultKey: 'KeyT', key: 'KeyT', name: 'Open Chat' }, - { defaultKey: 'Slash', key: 'Slash', name: 'Open Command' }, - // { defaultKey: '0', key: '0', name: 'Attack/Destroy' }, - // { defaultKey: '1', key: '1', name: 'Place Block' }, - { defaultKey: 'KeyQ', key: 'KeyQ', name: 'Drop Item' }, - // { defaultKey: 'Digit1', key: 'Digit1', name: 'Hotbar Slot 1' }, - // { defaultKey: 'Digit2', key: 'Digit2', name: 'Hotbar Slot 2' }, - // { defaultKey: 'Digit3', key: 'Digit3', name: 'Hotbar Slot 3' }, - // { defaultKey: 'Digit4', key: 'Digit4', name: 'Hotbar Slot 4' }, - // { defaultKey: 'Digit5', key: 'Digit5', name: 'Hotbar Slot 5' }, - // { defaultKey: 'Digit6', key: 'Digit6', name: 'Hotbar Slot 6' }, - // { defaultKey: 'Digit7', key: 'Digit7', name: 'Hotbar Slot 7' }, - // { defaultKey: 'Digit8', key: 'Digit8', name: 'Hotbar Slot 8' }, - // { defaultKey: 'Digit9', key: 'Digit9', name: 'Hotbar Slot 9' }, - { defaultKey: 'KeyE', key: 'KeyE', name: 'Open Inventory' }, - ] - - document.addEventListener('keydown', (e) => { - if (this.selected !== -1) { - this.keymaps[this.selected].key = e.code - this.selected = -1 - this.requestUpdate() - } - }) - } - - render () { - return html` -
- -

Key Binds

- -
-
- ${this.keymaps.map((m, i) => html` -
- ${m.name} - -
- { - e.target.setAttribute('pmui-label', `> ${m.key} <`) - this.selected = i - this.requestUpdate() - }}> - { - this.keymaps[i].key = this.keymaps[i].defaultKey - this.requestUpdate() - this.selected = -1 - }}> -
-
- `)} -
-
- -
- v.key !== v.defaultKey)} @pmui-click=${this.onResetAllPress}> - hideCurrentModal()}> -
- ` - } - - onResetAllPress () { - for (const keymap of this.keymaps) { - keymap.key = keymap.defaultKey - } - this.requestUpdate() - } -} - -window.customElements.define('pmui-keybindsscreen', KeyBindsScreen) diff --git a/src/menus/notification.js b/src/menus/notification.js deleted file mode 100644 index 0ac6db16..00000000 --- a/src/menus/notification.js +++ /dev/null @@ -1,81 +0,0 @@ -//@ts-check - -// create lit element -const { LitElement, html, css } = require('lit') -const { subscribe } = require('valtio') -const { notification } = require('../globalState') - -class Notification extends LitElement { - static get properties () { - return { - renderHtml: { type: Boolean }, - } - } - - constructor () { - super() - this.renderHtml = false - let timeout - subscribe(notification, () => { - if (timeout) clearTimeout(timeout) - this.requestUpdate() - if (!notification.show) return - this.renderHtml = true - if (!notification.autoHide) return - timeout = setTimeout(() => { - notification.show = false - }, 3000) - }) - } - - render () { - if (!this.renderHtml) return - const show = notification.show && notification.message - return html` -
- ${notification.message} -
- ` - } - - ontransitionend = (event) => { - if (event.propertyName !== 'opacity') return - - if (!notification.show) { - this.renderHtml = false - } - } - - static get styles () { - return css` - .notification { - position: absolute; - bottom: 0; - right: 0; - min-width: 200px; - padding: 10px; - white-space: nowrap; - font-size: 12px; - color: #fff; - text-align: center; - background: #000; - opacity: 0; - transition: opacity 0.3s ease-in-out; - } - - .notification-info { - background: #000; - } - - .notification-error { - background: #d00; - } - - .notification-show { - opacity: 1; - } - ` - } -} - -window.customElements.define('pmui-notification', Notification) diff --git a/src/menus/pause_screen.js b/src/menus/pause_screen.js deleted file mode 100644 index ce376645..00000000 --- a/src/menus/pause_screen.js +++ /dev/null @@ -1,139 +0,0 @@ -//@ts-check -const { LitElement, html, css } = require('lit') -const { subscribe } = require('valtio') -const { subscribeKey } = require('valtio/utils') -const { hideCurrentModal, showModal, miscUiState, notification, openOptionsMenu } = require('../globalState') -const { fsState } = require('../loadSave') -const { openGithub } = require('../utils') -const { disconnect } = require('../flyingSquidUtils') -const { closeWan, openToWanAndCopyJoinLink, getJoinLink } = require('../localServerMultiplayer') -const { uniqueFileNameFromWorldName, copyFilesAsyncWithProgress } = require('../browserfs') -const { showOptionsModal } = require('../react/SelectOption') -const { openURL } = require('./components/common') - -class PauseScreen extends LitElement { - static get styles () { - return css` - .bg { - position: absolute; - top: 0; - left: 0; - background: rgba(0, 0, 0, 0.75); - width: 100%; - height: 100%; - } - - .title { - position: absolute; - top: 40px; - left: 50%; - transform: translate(-50%); - font-size: 10px; - color: white; - text-shadow: 1px 1px #222; - } - - main { - display: flex; - flex-direction: column; - gap: 4px 0; - position: absolute; - left: 50%; - width: 204px; - top: calc(48px); - transform: translate(-50%); - } - - .row { - display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; - } - ` - } - - constructor () { - super() - - subscribe(fsState, () => { - this.requestUpdate() - }) - subscribeKey(miscUiState, 'singleplayer', () => this.requestUpdate()) - subscribeKey(miscUiState, 'wanOpened', () => this.requestUpdate()) - } - - async openWorldActions () { - if (fsState.inMemorySave || !miscUiState.singleplayer) { - return showOptionsModal('World actions...', []) - } - const action = await showOptionsModal('World actions...', ['Save to browser memory']) - if (action === 'Save to browser memory') { - //@ts-expect-error - const { worldFolder } = localServer.options - const savePath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`) - await copyFilesAsyncWithProgress(worldFolder, savePath) - } - } - - render () { - const joinButton = miscUiState.singleplayer - const isOpenedToWan = miscUiState.wanOpened - - return html` -
- - - -

Game Menu

- -
- -
- openGithub()}> - openURL('https://discord.gg/4Ucm684Fq3')}> -
- openOptionsMenu('main')}> - - - ${joinButton ? html` -
- this.clickJoinLinkButton()}> - this.clickJoinLinkButton(true)}> -
- ` : ''} - { - disconnect() - }}> -
- ` - } - - async clickJoinLinkButton (qr = false) { - if (!qr && miscUiState.wanOpened) { - closeWan() - return - } - if (!miscUiState.wanOpened || !qr) { - await openToWanAndCopyJoinLink(() => { }, !qr) - } - if (qr) { - const joinLink = getJoinLink() - //@ts-expect-error - miscUiState.currentDisplayQr = joinLink - - } - } - - show () { - this.focus() - // todo? - notification.show = false - } - - onReturnPress () { - hideCurrentModal() - } -} - -window.customElements.define('pmui-pausescreen', PauseScreen) diff --git a/src/menus/play_screen.js b/src/menus/play_screen.js deleted file mode 100644 index e9517d9e..00000000 --- a/src/menus/play_screen.js +++ /dev/null @@ -1,250 +0,0 @@ -//@ts-check -const { LitElement, html, css } = require('lit') -const viewerSupportedVersions = require('prismarine-viewer/viewer/supportedVersions.json') -const { supportedVersions } = require('minecraft-protocol') -const { hideCurrentModal, miscUiState } = require('../globalState') -const { commonCss } = require('./components/common') - -const fullySupporedVersions = viewerSupportedVersions - -class PlayScreen extends LitElement { - static get styles () { - return css` - ${commonCss} - .title { - top: 12px; - } - - .edit-boxes { - position: fixed; - top: 59px; - left: 50%; - display: flex; - flex-direction: column; - gap: 14px 0; - transform: translate(-50%); - width: 310px; - } - - .wrapper { - width: 100%; - display: flex; - flex-direction: row; - gap: 0 4px; - } - - .button-wrapper { - display: flex; - flex-direction: row; - gap: 0 4px; - position: absolute; - bottom: 9px; - left: 50%; - transform: translate(-50%); - width: 310px; - } - - .extra-info-version { - font-size: 10px; - color: rgb(206, 206, 206); - text-shadow: 1px 1px black; - position: absolute; - left: calc(50% + 2px); - bottom: -34px; - } - - .extra-info-proxy { - font-size: 8px; - color: rgb(206, 206, 206); - text-shadow: 1px 1px black; - margin:0; - margin-top:-12px; - } - - a { - color: white; - } - ` - } - - static get properties () { - return { - server: { type: String }, - serverImplicit: { type: String }, - serverport: { type: Number }, - proxy: { type: String }, - proxyImplicit: { type: String }, - proxyport: { type: Number }, - username: { type: String }, - password: { type: String }, - version: { type: String } - } - } - - constructor () { - super() - this.version = '' - this.serverport = '' - this.proxyport = '' - this.server = '' - this.proxy = '' - this.username = '' - this.password = '' - this.serverImplicit = '' - this.proxyImplicit = '' - // todo set them sooner add indicator - void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { - console.warn('Failed to load optional config.json', error) - return {} - }).then(async (/** @type {import('../globalState').AppConfig} */config) => { - miscUiState.appConfig = config - const params = new URLSearchParams(window.location.search) - - const getParam = (localStorageKey, qs = localStorageKey) => { - const qsValue = qs ? params.get(qs) : undefined - if (qsValue) { - this.style.display = 'block' - } - return qsValue || window.localStorage.getItem(localStorageKey) - } - - if (config.defaultHost === '' || config.defaultHostSave === '') { - let proxy = config.defaultProxy || config.defaultProxySave || params.get('proxy') - const cleanUrl = url => url.replaceAll(/(https?:\/\/|\/$)/g, '') - if (proxy && cleanUrl(proxy) !== cleanUrl(location.origin + location.pathname)) { - if (!proxy.startsWith('http')) proxy = 'https://' + proxy - const proxyConfig = await fetch(proxy + '/config.json').then(async res => res.json()).then(c => c, (error) => { - console.warn(`Failed to load config.json from proxy ${proxy}`, error) - return {} - }) - if (config.defaultHost === '' && proxyConfig.defaultHost) { - config.defaultHost = proxyConfig.defaultHost - } else { - config.defaultHost = '' - } - if (config.defaultHostSave === '' && proxyConfig.defaultHostSave) { - config.defaultHostSave = proxyConfig.defaultHostSave - } else { - config.defaultHostSave = '' - } - } - this.server = this.serverImplicit - } - - this.serverImplicit = config.defaultHost ?? '' - this.proxyImplicit = config.defaultProxy ?? '' - this.server = getParam('server', 'ip') ?? config.defaultHostSave ?? '' - this.proxy = getParam('proxy') ?? config.defaultProxySave ?? '' - this.version = getParam('version') || (window.localStorage.getItem('version') ?? config.defaultVersion ?? '') - this.username = getParam('username') || 'pviewer' + (Math.floor(Math.random() * 1000)) - this.password = getParam('password') || '' - if (process.env.NODE_ENV === 'development' && params.get('reconnect') && this.server && this.username) { - this.onConnectPress() - } - }) - } - - render () { - return html` -
- -

Join a Server

- -
-
- { this.server = e.target.value }} - > - { this.serverport = e.target.value }} - > -
-
- { this.proxy = e.target.value }} - > - { this.proxyport = e.target.value }} - > -
-
-

Enter proxy url you want to use. Learn more.

-
-
- { this.username = e.target.value }} - > - { this.version = e.target.value = e.target.value.replaceAll(',', '.') }} - > -
-

Leave blank and it will be chosen automatically

-
- -
- - hideCurrentModal()}> -
- ` - } - - onConnectPress () { - const server = this.server ? `${this.server}${this.serverport && `:${this.serverport}`}` : this.serverImplicit - const proxy = this.proxy ? `${this.proxy}${this.proxyport && `:${this.proxyport}`}` : this.proxyImplicit - - window.localStorage.setItem('username', this.username) - window.localStorage.setItem('password', this.password) - window.localStorage.setItem('server', server) - window.localStorage.setItem('proxy', proxy) - window.localStorage.setItem('version', this.version) - - window.dispatchEvent(new window.CustomEvent('connect', { - detail: { - server, - proxy, - username: this.username, - password: this.password, - botVersion: this.version - } - })) - } -} - -window.customElements.define('pmui-playscreen', PlayScreen) diff --git a/src/microsoftAuthflow.ts b/src/microsoftAuthflow.ts new file mode 100644 index 00000000..d759a7dc --- /dev/null +++ b/src/microsoftAuthflow.ts @@ -0,0 +1,182 @@ +export const getProxyDetails = async (proxyBaseUrl: string) => { + if (!proxyBaseUrl.startsWith('http')) proxyBaseUrl = `${isPageSecure() ? 'https' : 'http'}://${proxyBaseUrl}` + const url = `${proxyBaseUrl}/api/vm/net/connect` + let result: Response + try { + result = await fetch(url) + } catch (err) { + throw new Error(`Selected proxy server ${proxyBaseUrl} most likely is down`) + } + return result +} + +export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => { }, setCacheResult, connectingServer }) => { + let onMsaCodeCallback + let connectingVersion = '' + // const authEndpoint = 'http://localhost:3000/' + // const sessionEndpoint = 'http://localhost:3000/session' + let authEndpoint: URL | undefined + let sessionEndpoint: URL | undefined + const result = await getProxyDetails(proxyBaseUrl) + + try { + const json = await result.json() + authEndpoint = urlWithBase(json.capabilities.authEndpoint, proxyBaseUrl) + sessionEndpoint = urlWithBase(json.capabilities.sessionEndpoint, proxyBaseUrl) + if (!authEndpoint) throw new Error('No auth endpoint') + } catch (err) { + console.error(err) + throw new Error(`Selected proxy server ${proxyBaseUrl} does not support Microsoft authentication`) + } + const authFlow = { + async getMinecraftJavaToken () { + setProgressText('Authenticating with Microsoft account') + if (!window.crypto && !isPageSecure()) throw new Error('Crypto API is available only in secure contexts. Be sure to use https!') + let result = null + await fetch(authEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...tokenCaches, + // important to set this param and not fake it as auth server might reject the request otherwise + connectingServer, + connectingServerVersion: connectingVersion + }), + }) + .catch(e => { + throw new Error(`Failed to connect to auth server (network error): ${e.message}`) + }) + .then(async response => { + if (!response.ok) { + throw new Error(`Auth server error (${response.status}): ${await response.text()}`) + } + + const reader = response.body!.getReader() + const decoder = new TextDecoder('utf8') + + const processText = ({ done, value = undefined as Uint8Array | undefined }) => { + if (done) { + return + } + + const processChunk = (chunkStr) => { + let json: any + try { + json = JSON.parse(chunkStr) + } catch (err) {} + if (!json) return + if (json.user_code) { + onMsaCodeCallback(json) + // this.codeCallback(json) + } + if (json.error) throw new Error(`Auth server error: ${json.error}`) + if (json.token) result = json + if (json.newCache) setCacheResult(json.newCache) + } + + const strings = decoder.decode(value) + + for (const chunk of strings.split('\n\n')) { + processChunk(chunk) + } + + return reader.read().then(processText) + } + return reader.read().then(processText) + }) + const restoredData = await restoreData(result) + if (restoredData?.certificates?.profileKeys?.privatePEM) { + restoredData.certificates.profileKeys.private = restoredData.certificates.profileKeys.privatePEM + } + return restoredData + } + } + return { + authFlow, + sessionEndpoint, + setOnMsaCodeCallback (callback) { + onMsaCodeCallback = callback + }, + setConnectingVersion (version) { + connectingVersion = version + } + } +} + +function isPageSecure (url = window.location.href) { + return !url.startsWith('http:') +} + +// restore dates from strings +const restoreData = async (json) => { + const promises = [] as Array> + if (typeof json === 'object' && json) { + for (const [key, value] of Object.entries(json)) { + if (typeof value === 'string') { + promises.push(tryRestorePublicKey(value, key, json)) + if (value.endsWith('Z')) { + const date = new Date(value) + if (!isNaN(date.getTime())) { + json[key] = date + } + } + } + if (typeof value === 'object') { + // eslint-disable-next-line no-await-in-loop + await restoreData(value) + } + } + } + + await Promise.all(promises) + + return json +} + +const tryRestorePublicKey = async (value: string, name: string, parent: { [x: string]: any }) => { + value = value.trim() + if (!name.endsWith('PEM') || !value.startsWith('-----BEGIN RSA PUBLIC KEY-----') || !value.endsWith('-----END RSA PUBLIC KEY-----')) return + const der = pemToArrayBuffer(value) + const key = await window.crypto.subtle.importKey( + 'spki', // Specify that the data is in SPKI format + der, + { + name: 'RSA-OAEP', + hash: { name: 'SHA-256' } + }, + true, + ['encrypt'] // Specify key usages + ) + const originalName = name.replace('PEM', '') + const exported = await window.crypto.subtle.exportKey('spki', key) + const exportedBuffer = new Uint8Array(exported) + parent[originalName] = { + export () { + return exportedBuffer + } + } +} + +function pemToArrayBuffer (pem) { + // Fetch the part of the PEM string between header and footer + const pemHeader = '-----BEGIN RSA PUBLIC KEY-----' + const pemFooter = '-----END RSA PUBLIC KEY-----' + const pemContents = pem.slice(pemHeader.length, pem.length - pemFooter.length).trim() + const binaryDerString = atob(pemContents.replaceAll(/\s/g, '')) + const binaryDer = new Uint8Array(binaryDerString.length) + for (let i = 0; i < binaryDerString.length; i++) { + binaryDer[i] = binaryDerString.codePointAt(i)! + } + return binaryDer.buffer +} + +const urlWithBase = (url: string, base: string) => { + const defaultBase = isPageSecure() ? 'https' : 'http' + if (!base.startsWith('http')) base = `${defaultBase}://${base}` + const urlObj = new URL(url, base) + base = base.replace(/^https?:\/\//, '') + urlObj.host = base.includes(':') ? base : `${base}:${isPageSecure(base) ? '443' : '80'}` + return urlObj +} diff --git a/src/mineflayer/cameraShake.ts b/src/mineflayer/cameraShake.ts new file mode 100644 index 00000000..9e271da5 --- /dev/null +++ b/src/mineflayer/cameraShake.ts @@ -0,0 +1,25 @@ +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' + +customEvents.on('mineflayerBotCreated', () => { + customEvents.on('hurtAnimation', (yaw) => { + getThreeJsRendererMethods()?.shakeFromDamage() + }) + + bot._client.on('hurt_animation', ({ entityId, yaw }) => { + if (entityId === bot.entity.id) { + customEvents.emit('hurtAnimation', yaw) + } + }) + bot.on('entityHurt', ({ id }) => { + if (id === bot.entity.id) { + customEvents.emit('hurtAnimation') + } + }) + let { health } = bot + bot.on('health', () => { + if (bot.health < health) { + customEvents.emit('hurtAnimation') + } + health = bot.health + }) +}) diff --git a/src/mineflayer/entityStatus.ts b/src/mineflayer/entityStatus.ts new file mode 100644 index 00000000..e13784bc --- /dev/null +++ b/src/mineflayer/entityStatus.ts @@ -0,0 +1,70 @@ +export const EntityStatus = { + JUMP: 1, + HURT: 2, // legacy + DEATH: 3, + START_ATTACKING: 4, + STOP_ATTACKING: 5, + TAMING_FAILED: 6, + TAMING_SUCCEEDED: 7, + SHAKE_WETNESS: 8, + USE_ITEM_COMPLETE: 9, + EAT_GRASS: 10, + OFFER_FLOWER: 11, + LOVE_HEARTS: 12, + VILLAGER_ANGRY: 13, + VILLAGER_HAPPY: 14, + WITCH_HAT_MAGIC: 15, + ZOMBIE_CONVERTING: 16, + FIREWORKS_EXPLODE: 17, + IN_LOVE_HEARTS: 18, + SQUID_ANIM_SYNCH: 19, + SILVERFISH_MERGE_ANIM: 20, + GUARDIAN_ATTACK_SOUND: 21, + REDUCED_DEBUG_INFO: 22, + FULL_DEBUG_INFO: 23, + PERMISSION_LEVEL_ALL: 24, + PERMISSION_LEVEL_MODERATORS: 25, + PERMISSION_LEVEL_GAMEMASTERS: 26, + PERMISSION_LEVEL_ADMINS: 27, + PERMISSION_LEVEL_OWNERS: 28, + ATTACK_BLOCKED: 29, + SHIELD_DISABLED: 30, + FISHING_ROD_REEL_IN: 31, + ARMORSTAND_WOBBLE: 32, + THORNED: 33, // legacy + STOP_OFFER_FLOWER: 34, + TALISMAN_ACTIVATE: 35, // legacy + DROWNED: 36, // legacy + BURNED: 37, // legacy + DOLPHIN_LOOKING_FOR_TREASURE: 38, + RAVAGER_STUNNED: 39, + TRUSTING_FAILED: 40, + TRUSTING_SUCCEEDED: 41, + VILLAGER_SWEAT: 42, + BAD_OMEN_TRIGGERED: 43, // legacy + POKED: 44, // legacy + FOX_EAT: 45, + TELEPORT: 46, + MAINHAND_BREAK: 47, + OFFHAND_BREAK: 48, + HEAD_BREAK: 49, + CHEST_BREAK: 50, + LEGS_BREAK: 51, + FEET_BREAK: 52, + HONEY_SLIDE: 53, + HONEY_JUMP: 54, + SWAP_HANDS: 55, + CANCEL_SHAKE_WETNESS: 56, + FROZEN: 57, // legacy + START_RAM: 58, + END_RAM: 59, + POOF: 60, + TENDRILS_SHIVER: 61, + SONIC_CHARGE: 62, + SNIFFER_DIGGING_SOUND: 63, + ARMADILLO_PEEK: 64, + BODY_BREAK: 65, + SHAKE: 66 +} as const + +export type EntityStatusName = keyof typeof EntityStatus diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts new file mode 100644 index 00000000..48d0dfe0 --- /dev/null +++ b/src/mineflayer/items.ts @@ -0,0 +1,139 @@ +import mojangson from 'mojangson' +import nbt from 'prismarine-nbt' +import { fromFormattedString } from '@xmcl/text-component' +import { getItemSelector, ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' +import { MessageFormatPart } from '../chatUtils' +import { ResourcesManager, ResourcesManagerCommon, ResourcesManagerTransferred } from '../resourcesManager' + +type RenderSlotComponent = { + type: string, + data: any + // example + // { + // "type": "item_model", + // "data": "aa:ss" + // } +} +export type RenderItem = Pick & { + components?: RenderSlotComponent[], + // componentMap?: Map +} +export type GeneralInputItem = Pick & { + components?: RenderSlotComponent[], + displayName?: string + modelResolved?: boolean +} + +type JsonString = string +type PossibleItemProps = { + CustomModelData?: number + Damage?: number + display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"} +} + +export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManagerCommon) => { + let customText = undefined as string | any | undefined + let customModel = undefined as string | undefined + + let itemId = item.name + if (!itemId.includes(':')) { + itemId = `minecraft:${itemId}` + } + const customModelDataDefinitions = resourcesManager.currentResources?.customItemModelNames[itemId] + + if (item.components) { + const componentMap = new Map() + for (const component of item.components) { + componentMap.set(component.type, component) + } + + const customTextComponent = componentMap.get('custom_name') || componentMap.get('item_name') + if (customTextComponent) { + customText = typeof customTextComponent.data === 'string' ? customTextComponent.data : nbt.simplify(customTextComponent.data) + } + const customModelComponent = componentMap.get('item_model') + if (customModelComponent) { + customModel = customModelComponent.data + } + if (customModelDataDefinitions) { + const customModelDataComponent: any = componentMap.get('custom_model_data') + if (customModelDataComponent?.data) { + let customModelData: number | undefined + if (typeof customModelDataComponent.data === 'number') { + customModelData = customModelDataComponent.data + } else if (typeof customModelDataComponent.data === 'object' + && 'floats' in customModelDataComponent.data + && Array.isArray(customModelDataComponent.data.floats) + && customModelDataComponent.data.floats.length > 0) { + customModelData = customModelDataComponent.data.floats[0] + } + if (customModelData && customModelDataDefinitions[customModelData]) { + customModel = customModelDataDefinitions[customModelData] + } + } + } + const loreComponent = componentMap.get('lore') + if (loreComponent) { + customText ??= item.displayName ?? item.name + // todo test + customText += `\n${JSON.stringify(loreComponent.data)}` + } + } + if (item.nbt) { + const itemNbt: PossibleItemProps = nbt.simplify(item.nbt) + const customName = itemNbt.display?.Name + if (customName) { + customText = customName + } + if (customModelDataDefinitions && itemNbt.CustomModelData && customModelDataDefinitions[itemNbt.CustomModelData]) { + customModel = customModelDataDefinitions[itemNbt.CustomModelData] + } + } + + return { + customText, + customModel + } +} + + +export const getItemNameRaw = (item: Pick | null, resourcesManager: ResourcesManagerCommon) => { + if (!item) return '' + const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager) + if (!customText) return + try { + if (typeof customText === 'object') { + return customText + } + const parsed = customText.startsWith('{') && customText.endsWith('}') ? mojangson.simplify(mojangson.parse(customText)) : fromFormattedString(customText) + if (parsed.extra) { + return parsed as Record + } else { + return parsed as MessageFormatPart + } + } catch (err) { + return { + text: JSON.stringify(customText) + } + } +} + +export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerCommon, playerState: PlayerStateRenderer) => { + let itemModelName = item.name + const { customModel } = getItemMetadata(item, resourcesManager) + if (customModel) { + itemModelName = customModel + } + + const itemSelector = getItemSelector(playerState, { + ...specificProps + }) + const modelFromDef = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, { + name: itemModelName, + version: appViewer.resourcesManager.currentResources!.version, + properties: itemSelector + })?.model + const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName + return model +} diff --git a/src/mineflayer/java-tester/commands.ts b/src/mineflayer/java-tester/commands.ts new file mode 100644 index 00000000..1925c4d7 --- /dev/null +++ b/src/mineflayer/java-tester/commands.ts @@ -0,0 +1,68 @@ +import { versionToNumber } from 'flying-squid/dist/utils' + +const customStickNbt = (tags: Record) => { + let cmd = '/give @p stick' + const wrapIntoQuotes = versionToNumber(bot.version) < versionToNumber('1.21.5') + cmd += `[${Object.entries(tags).map(([key, value]) => { + if (typeof value === 'object') { + value = JSON.stringify(value) + } + return `${key}=${wrapIntoQuotes ? `'${value}'` : value}` + }).join(',')}]` + return cmd +} + +const writeCmd = (cmd: string) => { + if (!cmd.startsWith('/')) cmd = `/${cmd}` + console.log('Executing', cmd) + bot.chat(cmd) +} + +let msg = 0 +const LIMIT_MSG = 100 +export const javaServerTester = { + itemCustomLore () { + const cmd = customStickNbt({ + lore: [{ text: 'This Stick is very sticky.' }] + }) + writeCmd(cmd) + }, + + itemCustomModel () { + const cmd = customStickNbt({ + item_model: 'minecraft:diamond' + }) + writeCmd(cmd) + }, + itemCustomModel2 () { + const cmd = customStickNbt({ + item_model: 'diamond' + }) + writeCmd(cmd) + }, + + itemCustomName () { + const cmd = customStickNbt({ + custom_name: [{ text: 'diamond' }] + }) + writeCmd(cmd) + }, + itemCustomName2 () { + const cmd = customStickNbt({ + custom_name: [{ translate: 'item.diamond.name' }] + }) + writeCmd(cmd) + }, + + spamChat () { + for (let i = msg; i < msg + LIMIT_MSG; i++) { + bot.chat('Hello, world, ' + i) + } + msg += LIMIT_MSG + }, + spamChatComplexMessage () { + for (let i = msg; i < msg + LIMIT_MSG; i++) { + bot.chat('/tell @a ' + i) + } + } +} diff --git a/src/mineflayer/java-tester/index.ts b/src/mineflayer/java-tester/index.ts new file mode 100644 index 00000000..d395b8f3 --- /dev/null +++ b/src/mineflayer/java-tester/index.ts @@ -0,0 +1,6 @@ +import { javaServerTester } from './commands' + +window.javaServerTester = javaServerTester +customEvents.on('mineflayerBotCreated', () => { + // +}) diff --git a/src/mineflayer/maps.ts b/src/mineflayer/maps.ts new file mode 100644 index 00000000..5e968205 --- /dev/null +++ b/src/mineflayer/maps.ts @@ -0,0 +1,24 @@ +import { mapDownloader } from 'mineflayer-item-map-downloader' +import { setImageConverter } from 'mineflayer-item-map-downloader/lib/util' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' + +setImageConverter((buf: Uint8Array) => { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d')! + canvas.width = 128 + canvas.height = 128 + const imageData = ctx.createImageData(canvas.width, canvas.height) + imageData.data.set(buf) + ctx.putImageData(imageData, 0, 0) + // data url + return canvas.toDataURL('image/png') +}) + +customEvents.on('mineflayerBotCreated', () => { + bot.on('login', () => { + bot.loadPlugin(mapDownloader) + bot.mapDownloader.on('new_map', ({ png, id }) => { + getThreeJsRendererMethods()?.updateMap(id, png) + }) + }) +}) diff --git a/src/mineflayer/mc-protocol.ts b/src/mineflayer/mc-protocol.ts new file mode 100644 index 00000000..cd21d01f --- /dev/null +++ b/src/mineflayer/mc-protocol.ts @@ -0,0 +1,139 @@ +import net from 'net' +import { Client } from 'minecraft-protocol' +import { appQueryParams } from '../appParams' +import { downloadAllMinecraftData, getVersionAutoSelect } from '../connect' +import { gameAdditionalState } from '../globalState' +import { ProgressReporter } from '../core/progressReporter' +import { parseServerAddress } from '../parseServerAddress' +import { getCurrentProxy } from '../react/ServersList' +import { pingServerVersion, validatePacket } from './minecraft-protocol-extra' +import { getWebsocketStream } from './websocket-core' + +let lastPacketTime = 0 +customEvents.on('mineflayerBotCreated', () => { + // const oldParsePacketBuffer = bot._client.deserializer.parsePacketBuffer + // try { + // const parsed = oldParsePacketBuffer(buffer) + // } catch (err) { + // debugger + // reportError(new Error(`Error parsing packet ${buffer.subarray(0, 30).toString('hex')}`, { cause: err })) + // throw err + // } + // } + class MinecraftProtocolError extends Error { + constructor (message: string, cause?: Error, public data?: any) { + if (data?.customPayload) { + message += ` (Custom payload: ${data.customPayload.channel})` + } + super(message, { cause }) + this.name = 'MinecraftProtocolError' + } + } + + const onClientError = (err, data) => { + const error = new MinecraftProtocolError(`Minecraft protocol client error: ${err.message}`, err, data) + reportError(error) + } + if (typeof bot._client['_events'].error === 'function') { + // dont report to bot for more explicit error + bot._client['_events'].error = onClientError + } else { + bot._client.on('error' as any, onClientError) + } + + // todo move more code here + if (!appQueryParams.noPacketsValidation) { + (bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => { + validatePacket(packetMeta.name, data, fullBuffer, true) + lastPacketTime = performance.now() + }); + (bot._client as unknown as Client).on('writePacket', (name, params) => { + validatePacket(name, params, Buffer.alloc(0), false) + }) + } +}) + +setInterval(() => { + if (!bot || !lastPacketTime) return + if (bot.player?.ping > 500) { // TODO: we cant rely on server ping 1. weird calculations 2. available with delays instead patch minecraft-protocol to get latency of keep_alive packet + gameAdditionalState.poorConnection = true + } else { + gameAdditionalState.poorConnection = false + } + if (performance.now() - lastPacketTime < 2000) { + gameAdditionalState.noConnection = false + return + } + gameAdditionalState.noConnection = true +}, 1000) + + +export const getServerInfo = async (ip: string, port?: number, preferredVersion = getVersionAutoSelect(), ping = false, progressReporter?: ProgressReporter, setProxyParams?: ProxyParams) => { + await downloadAllMinecraftData() + const isWebSocket = ip.startsWith('ws://') || ip.startsWith('wss://') + let stream + if (isWebSocket) { + progressReporter?.setMessage('Connecting to WebSocket server') + stream = (await getWebsocketStream(ip)).mineflayerStream + progressReporter?.setMessage('WebSocket connected. Ping packet sent, waiting for response') + } else if (setProxyParams) { + setProxy(setProxyParams) + } + window.setLoadingMessage = (message?: string) => { + if (message === undefined) { + progressReporter?.endStage('dns') + } else { + progressReporter?.beginStage('dns', message) + } + } + return pingServerVersion(ip, port, { + ...(stream ? { stream } : {}), + ...(ping ? { noPongTimeout: 3000 } : {}), + ...(preferredVersion ? { version: preferredVersion } : {}), + }).finally(() => { + window.setLoadingMessage = undefined + }) +} + +globalThis.debugTestPing = async (ip: string) => { + const parsed = parseServerAddress(ip, false) + const result = await getServerInfo(parsed.host, parsed.port ? Number(parsed.port) : undefined, undefined, true, undefined, { address: getCurrentProxy(), }) + console.log('result', result) + return result +} + +export const getDefaultProxyParams = () => { + return { + headers: { + Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` + } + } +} + +export type ProxyParams = { + address?: string + headers?: Record +} + +export const setProxy = (proxyParams: ProxyParams) => { + if (proxyParams.address?.startsWith(':')) { + proxyParams.address = `${location.protocol}//${location.hostname}${proxyParams.address}` + } + if (proxyParams.address && location.port !== '80' && location.port !== '443' && !/:\d+$/.test(proxyParams.address)) { + const https = proxyParams.address.startsWith('https://') || location.protocol === 'https:' + proxyParams.address = `${proxyParams.address}:${https ? 443 : 80}` + } + + const parsedProxy = parseServerAddress(proxyParams.address, false) + const proxy = { host: parsedProxy.host, port: parsedProxy.port } + proxyParams.headers ??= getDefaultProxyParams().headers + net['setProxy']({ + hostname: proxy.host, + port: proxy.port, + headers: proxyParams.headers, + artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined + }) + return { + proxy + } +} diff --git a/src/mineflayer/minecraft-protocol-extra.ts b/src/mineflayer/minecraft-protocol-extra.ts new file mode 100644 index 00000000..65260979 --- /dev/null +++ b/src/mineflayer/minecraft-protocol-extra.ts @@ -0,0 +1,119 @@ +import EventEmitter from 'events' +import clientAutoVersion from 'minecraft-protocol/src/client/autoVersion' + +export const pingServerVersion = async (ip: string, port?: number, mergeOptions: Record = {}) => { + const fakeClient = new EventEmitter() as any + const options = { + host: ip, + port, + noPongTimeout: 10_000, + closeTimeout: 20_000, + ...mergeOptions, + } + let latency = 0 + let fullInfo: any = null + fakeClient.autoVersionHooks = [(res) => { + latency = res.latency + fullInfo = res + }] + + // TODO use client.socket.destroy() instead of client.end() for faster cleanup + clientAutoVersion(fakeClient, options) + await Promise.race([ + new Promise((resolve, reject) => { + fakeClient.once('connect_allowed', () => { + resolve() + }) + }), + new Promise((resolve, reject) => { + fakeClient.on('error', (err) => { + reject(new Error(err.message ?? err)) + }) + if (mergeOptions.stream) { + mergeOptions.stream.on('end', (err) => { + setTimeout(() => { + reject(new Error('Connection closed. Please report if you see this but the server is actually fine.')) + }) + }) + } + }) + ]) + + return { + version: fakeClient.version, + latency, + fullInfo, + } +} + +const MAX_PACKET_SIZE = 2_097_152 // 2mb +const CHAT_MAX_PACKET_DEPTH = 200 // todo improve perf + +const CHAT_VALIDATE_PACKETS = new Set([ + 'chat', + 'system_chat', + 'player_chat', + 'profileless_chat', + 'kick_disconnect', + 'resource_pack_send', + 'action_bar', + 'set_title_text', + 'set_title_subtitle', + 'title', + 'death_combat_event', + 'server_data', + 'scoreboard_objective', + 'scoreboard_team', + 'playerlist_header', + 'boss_bar' +]) + +export const validatePacket = (name: string, data: any, fullBuffer: Buffer, isFromServer: boolean) => { + // todo find out why chat is so slow with react + if (!isFromServer) return + + if (fullBuffer.length > MAX_PACKET_SIZE) { + console.groupCollapsed(`Packet ${name} is too large: ${fullBuffer.length} bytes`) + console.log(data) + console.groupEnd() + throw new Error(`Packet ${name} is too large: ${fullBuffer.length} bytes`) + } + + if (CHAT_VALIDATE_PACKETS.has(name)) { + // todo count total number of objects instead of max depth + const maxDepth = getObjectMaxDepth(data) + if (maxDepth > CHAT_MAX_PACKET_DEPTH) { + console.groupCollapsed(`Packet ${name} have too many nested objects: ${maxDepth}`) + console.log(data) + console.groupEnd() + throw new Error(`Packet ${name} have too many nested objects: ${maxDepth}`) + } + } +} + +function getObjectMaxDepth (obj: unknown, currentDepth = 0): number { + // Base case: null or primitive types have depth 0 + if (obj === null || typeof obj !== 'object' || obj instanceof Buffer) { + return currentDepth + } + + // Handle arrays and objects + let maxDepth = currentDepth + + if (Array.isArray(obj)) { + // For arrays, check each element + for (const item of obj) { + const depth = getObjectMaxDepth(item, currentDepth + 1) + maxDepth = Math.max(maxDepth, depth) + } + } else { + // For objects, check each value + // eslint-disable-next-line guard-for-in + for (const key in obj) { + const depth = getObjectMaxDepth(obj[key], currentDepth + 1) + maxDepth = Math.max(maxDepth, depth) + } + } + + return maxDepth +} diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts new file mode 100644 index 00000000..33f7af77 --- /dev/null +++ b/src/mineflayer/playerState.ts @@ -0,0 +1,200 @@ +import { HandItemBlock } from 'renderer/viewer/three/holdingBlock' +import { getInitialPlayerState, getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from 'renderer/viewer/lib/basePlayerState' +import { subscribe } from 'valtio' +import { subscribeKey } from 'valtio/utils' +import { gameAdditionalState } from '../globalState' +import { options } from '../optionsStorage' + +/** + * can be used only in main thread. Mainly for more convenient reactive state updates. + * In renderer/ directory, use PlayerStateControllerRenderer type or worldRenderer.playerState. + */ +export class PlayerStateControllerMain { + disableStateUpdates = false + + private timeOffGround = 0 + private lastUpdateTime = performance.now() + + // Held item state + private isUsingItem = false + ready = false + + reactive: PlayerStateReactive + utils: PlayerStateUtils + + constructor () { + customEvents.on('mineflayerBotCreated', () => { + this.ready = false + bot.on('inject_allowed', () => { + if (this.ready) return + this.ready = true + this.botCreated() + }) + bot.on('end', () => { + this.ready = false + }) + }) + } + + private onBotCreatedOrGameJoined () { + this.reactive.username = bot.username ?? '' + } + + private botCreated () { + console.log('bot created & plugins injected') + this.reactive = getInitialPlayerState() + this.reactive.perspective = options.defaultPerspective + this.utils = getPlayerStateUtils(this.reactive) + this.onBotCreatedOrGameJoined() + + const handleDimensionData = (data) => { + let hasSkyLight = 1 + try { + hasSkyLight = data.dimension.value.has_skylight.value + } catch {} + this.reactive.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight + } + + bot._client.on('login', (packet) => { + handleDimensionData(packet) + }) + bot._client.on('respawn', (packet) => { + handleDimensionData(packet) + }) + + // Movement tracking + bot.on('move', () => { + this.updateMovementState() + }) + + // Item tracking + bot.on('heldItemChanged', () => { + return this.updateHeldItem(false) + }) + bot.inventory.on('updateSlot', (index) => { + if (index === 45) this.updateHeldItem(true) + }) + const updateSneakingOrFlying = () => { + this.updateMovementState() + this.reactive.sneaking = bot.controlState.sneak + this.reactive.flying = gameAdditionalState.isFlying + this.reactive.eyeHeight = bot.controlState.sneak && !gameAdditionalState.isFlying ? 1.27 : 1.62 + } + bot.on('physicsTick', () => { + if (this.isUsingItem) this.reactive.itemUsageTicks++ + updateSneakingOrFlying() + }) + // todo move from gameAdditionalState to reactive directly + subscribeKey(gameAdditionalState, 'isSneaking', () => { + updateSneakingOrFlying() + }) + subscribeKey(gameAdditionalState, 'isFlying', () => { + updateSneakingOrFlying() + }) + + // Initial held items setup + this.updateHeldItem(false) + this.updateHeldItem(true) + + bot.on('game', () => { + this.reactive.gameMode = bot.game.gameMode + }) + this.reactive.gameMode = bot.game?.gameMode + + customEvents.on('gameLoaded', () => { + this.reactive.team = bot.teamMap[bot.username] + }) + + this.watchReactive() + } + + // #region Movement and Physics State + private updateMovementState () { + if (!bot?.entity || this.disableStateUpdates) return + + const { velocity } = bot.entity + const isOnGround = bot.entity.onGround + const VELOCITY_THRESHOLD = 0.01 + const SPRINTING_VELOCITY = 0.15 + const OFF_GROUND_THRESHOLD = 0 // ms before switching to SNEAKING when off ground + + const now = performance.now() + const deltaTime = now - this.lastUpdateTime + this.lastUpdateTime = now + + // this.lastVelocity = velocity + + // Update time off ground + if (isOnGround) { + this.timeOffGround = 0 + } else { + this.timeOffGround += deltaTime + } + + if (gameAdditionalState.isSneaking || gameAdditionalState.isFlying || (this.timeOffGround > OFF_GROUND_THRESHOLD)) { + this.reactive.movementState = 'SNEAKING' + } else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) { + this.reactive.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY + ? 'SPRINTING' + : 'WALKING' + } else { + this.reactive.movementState = 'NOT_MOVING' + } + } + + // #region Held Item State + private updateHeldItem (isLeftHand: boolean) { + const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem + if (!newItem) { + if (isLeftHand) { + this.reactive.heldItemOff = undefined + } else { + this.reactive.heldItemMain = undefined + } + return + } + + const block = loadedData.blocksByName[newItem.name] + const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {} + const item: HandItemBlock = { + name: newItem.name, + properties: blockProperties, + id: newItem.type, + type: block ? 'block' : 'item', + fullItem: newItem, + } + + if (isLeftHand) { + this.reactive.heldItemOff = item + } else { + this.reactive.heldItemMain = item + } + // this.events.emit('heldItemChanged', item, isLeftHand) + } + + startUsingItem () { + if (this.isUsingItem) return + this.isUsingItem = true + this.reactive.itemUsageTicks = 0 + } + + stopUsingItem () { + this.isUsingItem = false + this.reactive.itemUsageTicks = 0 + } + + getItemUsageTicks (): number { + return this.reactive.itemUsageTicks + } + + watchReactive () { + subscribeKey(this.reactive, 'eyeHeight', () => { + appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) + }) + } + + // #endregion +} + +export const playerState = new PlayerStateControllerMain() +window.playerState = playerState diff --git a/src/mineflayer/plugins/index.ts b/src/mineflayer/plugins/index.ts new file mode 100644 index 00000000..6ac11376 --- /dev/null +++ b/src/mineflayer/plugins/index.ts @@ -0,0 +1,21 @@ +import { lastConnectOptions } from '../../react/AppStatusProvider' +import mouse from './mouse' +import packetsPatcher from './packetsPatcher' +import { localRelayServerPlugin } from './packetsRecording' +import ping from './ping' +import webFeatures from './webFeatures' + +// register +webFeatures() +packetsPatcher() + + +customEvents.on('mineflayerBotCreated', () => { + if (lastConnectOptions.value!.server) { + bot.loadPlugin(ping) + } + bot.loadPlugin(mouse) + if (!lastConnectOptions.value!.worldStateFileContents) { + bot.loadPlugin(localRelayServerPlugin) + } +}) diff --git a/src/mineflayer/plugins/mouse.ts b/src/mineflayer/plugins/mouse.ts new file mode 100644 index 00000000..14e19345 --- /dev/null +++ b/src/mineflayer/plugins/mouse.ts @@ -0,0 +1,122 @@ +import { createMouse } from 'mineflayer-mouse' +import { Bot } from 'mineflayer' +import { Block } from 'prismarine-block' +import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' +import { isGameActive, showModal } from '../../globalState' + +import { isCypress } from '../../standaloneUtils' +import { playerState } from '../playerState' +import { sendVideoInteraction, videoCursorInteraction } from '../../customChannels' + +function cursorBlockDisplay (bot: Bot) { + const updateCursorBlock = (data?: { block: Block }) => { + if (!data?.block || bot.game.gameMode === 'spectator') { + playerState.reactive.lookingAtBlock = undefined + return + } + + const { block } = data + playerState.reactive.lookingAtBlock = { + x: block.position.x, + y: block.position.y, + z: block.position.z, + shapes: bot.mouse.getBlockCursorShapes(block).map(shape => { + return bot.mouse.getDataFromShape(shape) + }) + } + } + + bot.on('highlightCursorBlock', updateCursorBlock) + bot.on('game', () => { + const block = bot.mouse.getCursorState().cursorBlock + updateCursorBlock(block ? { block } : undefined) + }) + + bot.on('blockBreakProgressStage', (block, stage) => { + const mergedShape = bot.mouse.getMergedCursorShape(block) + playerState.reactive.diggingBlock = stage === null ? undefined : { + x: block.position.x, + y: block.position.y, + z: block.position.z, + stage, + mergedShape: mergedShape ? bot.mouse.getDataFromShape(mergedShape) : undefined + } + }) +} + +export default (bot: Bot) => { + bot.loadPlugin(createMouse({})) + + domListeners(bot) + cursorBlockDisplay(bot) + + otherListeners() +} + +const otherListeners = () => { + bot.on('startDigging', (block) => { + customEvents.emit('digStart') + }) + + bot.on('goingToSleep', () => { + showModal({ reactType: 'bed' }) + }) + + bot.on('botArmSwingStart', (hand) => { + getThreeJsRendererMethods()?.changeHandSwingingState(true, hand === 'left') + }) + + bot.on('botArmSwingEnd', (hand) => { + getThreeJsRendererMethods()?.changeHandSwingingState(false, hand === 'left') + }) + + bot.on('startUsingItem', (item, slot, isOffhand, duration) => { + customEvents.emit('activateItem', item, isOffhand ? 45 : bot.quickBarSlot, isOffhand) + playerState.startUsingItem() + }) + + bot.on('stopUsingItem', () => { + playerState.stopUsingItem() + }) +} + +const domListeners = (bot: Bot) => { + const abortController = new AbortController() + document.addEventListener('mousedown', (e) => { + if (e.isTrusted && !document.pointerLockElement && !isCypress()) return + if (!isGameActive(true)) return + + getThreeJsRendererMethods()?.onPageInteraction() + + const videoInteraction = videoCursorInteraction() + if (videoInteraction) { + sendVideoInteraction(videoInteraction.id, videoInteraction.x, videoInteraction.y, e.button === 0) + return + } + + if (e.button === 0) { + bot.leftClickStart() + } else if (e.button === 2) { + bot.rightClickStart() + } + }, { signal: abortController.signal }) + + document.addEventListener('mouseup', (e) => { + if (e.button === 0) { + bot.leftClickEnd() + } else if (e.button === 2) { + bot.rightClickEnd() + } + }, { signal: abortController.signal }) + + bot.mouse.beforeUpdateChecks = () => { + if (!document.hasFocus() || !isGameActive(true)) { + // deactive all buttons + bot.mouse.buttons.fill(false) + } + } + + bot.on('end', () => { + abortController.abort() + }) +} diff --git a/src/mineflayer/plugins/packetsPatcher.ts b/src/mineflayer/plugins/packetsPatcher.ts new file mode 100644 index 00000000..5e93ef60 --- /dev/null +++ b/src/mineflayer/plugins/packetsPatcher.ts @@ -0,0 +1,50 @@ +export default () => { + // not plugin so its loaded earlier + customEvents.on('mineflayerBotCreated', () => { + botInit() + }) +} + +const waitingPackets = {} as Record> + +const botInit = () => { + // PATCH READING + bot._client.on('packet', (data, meta) => { + if (meta.name === 'map_chunk') { + if (data.groundUp && data.bitMap === 1 && data.chunkData.every(x => x === 0)) { + data.chunkData = Buffer.from(Array.from({ length: 12_544 }).fill(0) as any) + } + } + }) + + // PATCH WRITING + + const clientWrite = bot._client.write.bind(bot._client) + const sendAllPackets = (name: string, data: any) => { + for (const packet of waitingPackets[name]) { + clientWrite(packet.name, packet.data) + } + delete waitingPackets[name] + } + + //@ts-expect-error + bot._client.write = (name: string, data: any) => { + // if (name === 'position' || name === 'position_look' || name === 'look' || name === 'teleport_confirm') { + // const chunkX = Math.floor(bot.entity.position.x / 16) + // const chunkZ = Math.floor(bot.entity.position.z / 16) + // const loadedColumns = bot.world.getColumns() + // if (loadedColumns.some((c) => c.chunkX === chunkX && c.chunkZ === chunkZ)) { + // sendAllPackets('position', data) + // } else { + // waitingPackets['position'] = [...(waitingPackets['position'] || []), { name, data }] + // return + // } + // } + if (name === 'settings') { + data['viewDistance'] = Math.max(data['viewDistance'], 3) + } + return clientWrite(name, data) + } + + // PATCH INTERACTIONS +} diff --git a/src/mineflayer/plugins/packetsRecording.ts b/src/mineflayer/plugins/packetsRecording.ts new file mode 100644 index 00000000..b9ba028c --- /dev/null +++ b/src/mineflayer/plugins/packetsRecording.ts @@ -0,0 +1,147 @@ +import { viewerConnector } from 'mcraft-fun-mineflayer' +import { PACKETS_REPLAY_FILE_EXTENSION, WORLD_STATE_FILE_EXTENSION } from 'mcraft-fun-mineflayer/build/worldState' +import { Bot } from 'mineflayer' +import CircularBuffer from 'flying-squid/dist/circularBuffer' +import { PacketsLogger } from 'mcraft-fun-mineflayer/build/packetsLogger' +import { subscribe } from 'valtio' +import { lastConnectOptions } from '../../react/AppStatusProvider' +import { packetsRecordingState } from '../../packetsReplay/packetsReplayLegacy' +import { packetsReplayState } from '../../react/state/packetsReplayState' + +const AUTO_CAPTURE_PACKETS_COUNT = 30 +let circularBuffer: CircularBuffer | undefined +let lastConnectVersion = '' + +export const localRelayServerPlugin = (bot: Bot) => { + lastConnectVersion = bot.version + let ended = false + bot.on('end', () => { + ended = true + }) + + bot.loadPlugin( + viewerConnector({ + tcpEnabled: false, + websocketEnabled: false, + }) + ) + + const downloadFile = (contents: string, filename: string) => { + const a = document.createElement('a') + const blob = new Blob([contents], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) + } + + bot.downloadCurrentWorldState = () => { + const worldState = bot.webViewer._unstable.createStateCaptureFile() + // add readable timestamp to filename + const timestamp = new Date().toISOString().replaceAll(/[-:Z]/g, '') + downloadFile(worldState.contents, `${bot.username}-world-state-${timestamp}.${WORLD_STATE_FILE_EXTENSION}`) + } + + let logger: PacketsLogger | undefined + bot.startPacketsRecording = () => { + bot.webViewer._unstable.startRecording((l) => { + logger = l + }) + } + + bot.stopPacketsRecording = () => { + if (!logger) return + const packets = logger?.contents + logger = undefined + const timestamp = new Date().toISOString().replaceAll(/[-:Z]/g, '') + downloadFile(packets, `${bot.username}-packets-${timestamp}.${PACKETS_REPLAY_FILE_EXTENSION}`) + bot.webViewer._unstable.stopRecording() + } + + circularBuffer = new CircularBuffer(AUTO_CAPTURE_PACKETS_COUNT) + let position = 0 + bot._client.on('writePacket' as any, (name, params) => { + circularBuffer!.add({ name, state: bot._client.state, params, isFromServer: false, timestamp: Date.now() }) + if (packetsRecordingState.active) { + packetsReplayState.packetsPlayback.push({ + name, + data: params, + isFromClient: true, + isUpcoming: false, + position: position++, + timestamp: Date.now(), + }) + packetsReplayState.progress.current++ + } + }) + bot._client.on('packet', (data, { name }) => { + if (name === 'map_chunk') data = { x: data.x, z: data.z } + circularBuffer!.add({ name, state: bot._client.state, params: data, isFromServer: true, timestamp: Date.now() }) + if (packetsRecordingState.active) { + packetsReplayState.packetsPlayback.push({ + name, + data, + isFromClient: false, + isUpcoming: false, + position: position++, + timestamp: Date.now(), + }) + packetsReplayState.progress.total++ + } + }) + const oldWriteChannel = bot._client.writeChannel.bind(bot._client) + bot._client.writeChannel = (channel, params) => { + packetsReplayState.packetsPlayback.push({ + name: channel, + data: params, + isFromClient: true, + isUpcoming: false, + position: position++, + timestamp: Date.now(), + isCustomChannel: true, + }) + oldWriteChannel(channel, params) + } + + upPacketsReplayPanel() +} + +const upPacketsReplayPanel = () => { + if (packetsRecordingState.active && bot) { + packetsReplayState.isOpen = true + packetsReplayState.isMinimized = true + packetsReplayState.isRecording = true + packetsReplayState.replayName = 'Recording all packets for ' + bot.username + } +} + +subscribe(packetsRecordingState, () => { + upPacketsReplayPanel() +}) + +declare module 'mineflayer' { + interface Bot { + downloadCurrentWorldState: () => void + startPacketsRecording: () => void + stopPacketsRecording: () => void + } +} + +export const getLastAutoCapturedPackets = () => circularBuffer?.size +export const downloadAutoCapturedPackets = () => { + const logger = new PacketsLogger({ minecraftVersion: lastConnectVersion }) + logger.relativeTime = false + logger.formattedTime = true + for (const packet of circularBuffer?.getLastElements() ?? []) { + logger.log(packet.isFromServer, { name: packet.name, state: packet.state, time: packet.timestamp }, packet.params) + } + const textContents = logger.contents + const blob = new Blob([textContents], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${lastConnectOptions.value?.server ?? 'unknown-server'}-${lastConnectOptions.value?.username ?? 'unknown-username'}-auto-captured-packets.txt` + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/mineflayer/plugins/ping.ts b/src/mineflayer/plugins/ping.ts new file mode 100644 index 00000000..d6a23554 --- /dev/null +++ b/src/mineflayer/plugins/ping.ts @@ -0,0 +1,42 @@ +import { versionToNumber } from 'renderer/viewer/common/utils' + +export default () => { + let i = 0 + bot.pingProxy = async () => { + const curI = ++i + return new Promise(resolve => { + //@ts-expect-error + bot._client.socket._ws.send(`ping:${curI}`) + const date = Date.now() + const onPong = (received) => { + if (received !== curI.toString()) return + bot._client.socket.off('pong' as any, onPong) + resolve(Date.now() - date) + } + bot._client.socket.on('pong' as any, onPong) + }) + } + + let pingId = 0 + bot.pingServer = async () => { + if (versionToNumber(bot.version) < versionToNumber('1.20.2')) return bot.player?.ping ?? -1 + return new Promise((resolve) => { + const curId = pingId++ + bot._client.write('ping_request', { id: BigInt(curId) }) + const date = Date.now() + const onPong = (data: { id: bigint }) => { + if (BigInt(data.id) !== BigInt(curId)) return + bot._client.off('ping_response' as any, onPong) + resolve(Date.now() - date) + } + bot._client.on('ping_response' as any, onPong) + }) + } +} + +declare module 'mineflayer' { + interface Bot { + pingProxy: () => Promise + pingServer: () => Promise + } +} diff --git a/src/mineflayer/plugins/webFeatures.ts b/src/mineflayer/plugins/webFeatures.ts new file mode 100644 index 00000000..c56d7d66 --- /dev/null +++ b/src/mineflayer/plugins/webFeatures.ts @@ -0,0 +1,12 @@ +import { Bot } from 'mineflayer' +import { getAppLanguage } from '../../optionsStorage' + +export default () => { + customEvents.on('mineflayerBotCreated', () => { + bot.loadPlugin(plugin) + }) +} + +const plugin = (bot: Bot) => { + bot.settings['locale'] = getAppLanguage() +} diff --git a/src/mineflayer/timers.ts b/src/mineflayer/timers.ts new file mode 100644 index 00000000..99110718 --- /dev/null +++ b/src/mineflayer/timers.ts @@ -0,0 +1,71 @@ +import { subscribeKey } from 'valtio/utils' +import { preventThrottlingWithSound } from '../core/timers' +import { options } from '../optionsStorage' + +customEvents.on('mineflayerBotCreated', () => { + const abortController = new AbortController() + + const maybeGoBackgroundKickPrevention = () => { + if (options.preventBackgroundTimeoutKick && !bot.backgroundKickPrevention) { + const unsub = preventThrottlingWithSound() + bot.on('end', unsub) + bot.backgroundKickPrevention = true + } + } + maybeGoBackgroundKickPrevention() + subscribeKey(options, 'preventBackgroundTimeoutKick', (value) => { + maybeGoBackgroundKickPrevention() + }) + + // wake lock + const requestWakeLock = async () => { + if (!('wakeLock' in navigator)) { + console.warn('Wake Lock API is not supported in this browser') + return + } + + if (options.preventSleep && !bot.wakeLock && !bot.lockRequested) { + bot.lockRequested = true + bot.wakeLock = await navigator.wakeLock.request('screen').finally(() => { + bot.lockRequested = false + }) + + bot.wakeLock.addEventListener('release', () => { + bot.wakeLock = undefined + }, { + once: true, + }) + } + + if (!options.preventSleep && bot.wakeLock) { + void bot.wakeLock.release() + } + } + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + // we are back to the tab, request wake lock again + void requestWakeLock() + } + }, { + signal: abortController.signal, + }) + void requestWakeLock() + subscribeKey(options, 'preventSleep', (value) => { + void requestWakeLock() + }) + + bot.on('end', () => { + if (bot.wakeLock) { + void bot.wakeLock.release() + } + abortController.abort() + }) +}) + +declare module 'mineflayer' { + interface Bot { + backgroundKickPrevention?: boolean + wakeLock?: WakeLockSentinel + lockRequested?: boolean + } +} diff --git a/src/mineflayer/userError.ts b/src/mineflayer/userError.ts new file mode 100644 index 00000000..9d3e08a7 --- /dev/null +++ b/src/mineflayer/userError.ts @@ -0,0 +1,6 @@ +export class UserError extends Error { + constructor (message: string) { + super(message) + this.name = 'UserError' + } +} diff --git a/src/mineflayer/websocket-core.ts b/src/mineflayer/websocket-core.ts new file mode 100644 index 00000000..f8163102 --- /dev/null +++ b/src/mineflayer/websocket-core.ts @@ -0,0 +1,63 @@ +import { Duplex } from 'stream' +import { UserError } from './userError' + +class CustomDuplex extends Duplex { + constructor (options, public writeAction) { + super(options) + } + + override _read () {} + + override _write (chunk, encoding, callback) { + this.writeAction(chunk) + callback() + } +} + +export const getWebsocketStream = async (host: string) => { + const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss' + const hostClean = host.replace('ws://', '').replace('wss://', '') + const hostURL = new URL(`${baseProtocol}://${hostClean}`) + const hostParams = hostURL.searchParams + hostParams.append('client_mcraft', '') + const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`) + const clientDuplex = new CustomDuplex(undefined, data => { + ws.send(data) + }) + + clientDuplex.on('error', () => {}) + + ws.addEventListener('message', async message => { + let { data } = message + if (data instanceof Blob) { + data = await data.arrayBuffer() + } + clientDuplex.push(Buffer.from(data)) + }) + + ws.addEventListener('close', () => { + console.log('ws closed') + clientDuplex.end() + setTimeout(() => { + clientDuplex.emit('end', 'Connection lost') + }, 500) + }) + + ws.addEventListener('error', err => { + console.log('ws error', err) + clientDuplex.emit('error', err) + }) + + await new Promise((resolve, reject) => { + ws.addEventListener('open', resolve) + ws.addEventListener('error', err => { + console.log('ws error', err) + reject(new UserError('Failed to open websocket connection')) + }) + }) + + return { + mineflayerStream: clientDuplex, + ws, + } +} diff --git a/src/mobileShim.ts b/src/mobileShim.ts new file mode 100644 index 00000000..ebf33d6e --- /dev/null +++ b/src/mobileShim.ts @@ -0,0 +1,20 @@ +// fix double tap on mobile + +let lastElement = null as { + clickTime: number + element: HTMLElement +} | null +document.addEventListener('touchstart', (e) => { + if (e.touches.length > 1) { + lastElement = null + return + } + if (lastElement && Date.now() - lastElement.clickTime < 500 && lastElement.element === e.target) { + lastElement.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + lastElement = null + } + lastElement = { + clickTime: Date.now(), + element: e.target as HTMLElement + } +}, { passive: false }) diff --git a/src/optimizeJson.ts b/src/optimizeJson.ts new file mode 100644 index 00000000..a7fe7d4e --- /dev/null +++ b/src/optimizeJson.ts @@ -0,0 +1,369 @@ +import { versionToNumber } from 'renderer/viewer/common/utils' + +type IdMap = Record + +type DiffData = { + removed: number[], + changed: any[], + removedProps: Array<[number, number[]]>, + added +} + +type SourceData = { + keys: IdMap, + properties: IdMap + source: Record + diffs: Record + arrKey? + __IS_OPTIMIZED__: true +} + +function getRecipesProcessorProcessRecipes (items, blocks) { + return (current) => { + // can require the same multiple times per different versions + const itemsIdsMap = Object.fromEntries(items.map((b) => [b.name, b.id])) + const blocksIdsMap = Object.fromEntries(blocks.map((b) => [b.name, b.id])) + const keys = Object.keys(current) + for (const key of keys) { + if (key === '_proccessed') { + delete current[key] + continue + } + const mapId = (id) => { + if (typeof id !== 'string' && typeof id !== 'number') throw new Error('Incorrect type') + const mapped = itemsIdsMap[id] ?? blocksIdsMap[id] + if (!mapped) { + throw new Error(`No item/block name with id ${id}`) + } + return mapped + } + const processRecipe = (obj) => { + // if (!obj) return + // if (Array.isArray(obj)) { + // obj.forEach((id, i) => { + // obj[i] = mapId(obj[id]) + // }) + // } else if (obj && typeof obj === 'object') { + // if (!'count metadata id'.split(' ').every(x => x in obj)) { + // throw new Error(`process error: Unknown deep object pattern: ${JSON.stringify(obj)}`) + // } + // obj.id = mapId(obj.id) + // } else { + // throw new Error('unknown type') + // } + const parseRecipeItem = (item) => { + if (typeof item === 'number' || typeof item === 'string') return mapId(item) + if (Array.isArray(item)) return [mapId(item), ...item.slice(1)] + if (!item) { + return item + } + if ('id' in item) { + item.id = mapId(item.id) + return item + } + throw new Error('unhandled') + } + const maybeProccessShape = (shape) => { + if (!shape) return + for (const shapeRow of shape) { + for (const [i, item] of shapeRow.entries()) { + shapeRow[i] = parseRecipeItem(item) + } + } + } + if (obj.result) obj.result = parseRecipeItem(obj.result) + maybeProccessShape(obj.inShape) + maybeProccessShape(obj.outShape) + if (obj.ingredients) { + for (const [i, ingredient] of obj.ingredients.entries()) { + obj.ingredients[i] = parseRecipeItem(ingredient) + } + } + } + // eslint-disable-next-line no-useless-catch + try { + const name = mapId(key) + for (const [i, recipe] of current[key].entries()) { + // eslint-disable-next-line no-useless-catch + try { + processRecipe(recipe) + } catch (err) { + // console.warn(`${version} [warn] Removing incorrect recipe: ${err}`) + // delete current[i] + throw err + } + } + current[name] = current[key] + } catch (err) { + // console.warn(`${version} [warn] Removing incorrect recipe: ${err}`) + throw err + } + delete current[key] + } + } +} + +export const restoreMinecraftData = (allVersionData: any, type: string, version: string) => { + let restorer + if (type === 'recipes') { + restorer = getRecipesProcessorProcessRecipes( + JsonOptimizer.restoreData(allVersionData.items, version, undefined), + JsonOptimizer.restoreData(allVersionData.blocks, version, undefined), + ) + } + return JsonOptimizer.restoreData(allVersionData[type], version, restorer) +} + +export default class JsonOptimizer { + keys = {} as IdMap + idToKey = {} as Record + properties = {} as IdMap + source = {} + previousKeys = [] as number[] + previousValues = {} as Record + diffs = {} as Record + + constructor (public arrKey?: string, public ignoreChanges = false, public ignoreRemoved = false) { } + + export () { + const { keys, properties, source, arrKey, diffs } = this + return { + keys, + properties, + source, + arrKey, + diffs, + '__IS_OPTIMIZED__': true + } satisfies SourceData + } + + diffObj (diffing): DiffData { + const removed = [] as number[] + const changed = [] as any[] + const removedProps = [] as any[] + const { arrKey, ignoreChanges, ignoreRemoved } = this + const added = [] as number[] + + if (!diffing || typeof diffing !== 'object') throw new Error('diffing data is not object') + if (Array.isArray(diffing) && !arrKey) throw new Error('arrKey is required for arrays') + const diffingObj = Array.isArray(diffing) ? Object.fromEntries(diffing.map(x => { + const key = JsonOptimizer.getByArrKey(x, arrKey!) + return [key, x] + })) : diffing + + const possiblyNewKeys = Object.keys(diffingObj) + this.keys ??= {} + this.properties ??= {} + let lastRootKeyId = Object.values(this.keys).length + let lastItemKeyId = Object.values(this.properties).length + for (const key of possiblyNewKeys) { + this.keys[key] ??= lastRootKeyId++ + this.idToKey[this.keys[key]] = key + } + const DEBUG = false + + const addDiff = (key, newVal, prevVal) => { + const valueMapped = [] as any[] + const isItemObj = typeof newVal === 'object' && newVal + const keyId = this.keys[key] + if (isItemObj) { + const removedPropsLocal = [] as any[] + for (const [prop, val] of Object.entries(newVal)) { + // mc-data: why push only changed props? eg for blocks only stateId are different between all versions so we skip a lot of duplicated data like block props + if (!isEqualStructured(newVal[prop], prevVal[prop])) { + let keyMapped = this.properties[prop] + if (keyMapped === undefined) { + this.properties[prop] = lastItemKeyId++ + keyMapped = this.properties[prop] + } + valueMapped.push(DEBUG ? prop : keyMapped, newVal[prop]) + } + } + // also add undefined for removed props + for (const prop of Object.keys(prevVal)) { + if (prop in newVal) continue + let keyMapped = this.properties[prop] + if (keyMapped === undefined) { + this.properties[prop] = lastItemKeyId++ + keyMapped = this.properties[prop] + } + removedPropsLocal.push(DEBUG ? prop : keyMapped) + } + removedProps.push([keyId, removedPropsLocal]) + } + changed.push(DEBUG ? key : keyId, isItemObj ? valueMapped : newVal) + } + for (const [id, sourceVal] of Object.entries(this.source)) { + const key = this.idToKey[id] + const diffVal = diffingObj[key] + if (!ignoreChanges && diffVal !== undefined) { + this.previousValues[id] ??= this.source[id] + const prevVal = this.previousValues[id] + if (!isEqualStructured(prevVal, diffVal)) { + addDiff(key, diffVal, prevVal) + } + this.previousValues[id] = diffVal + } + } + for (const [key, val] of Object.entries(diffingObj)) { + const id = this.keys[key] + if (!this.source[id]) { + this.source[id] = val + } + added.push(id) + } + + for (const previousKey of this.previousKeys) { + const key = this.idToKey[previousKey] + if (diffingObj[key] === undefined && !ignoreRemoved) { + removed.push(previousKey) + } + } + + for (const toRemove of removed) { + this.previousKeys.splice(this.previousKeys.indexOf(toRemove), 1) + } + + for (const previousKey of this.previousKeys) { + const index = added.indexOf(previousKey) + if (index === -1) continue + added.splice(index, 1) + } + + this.previousKeys = [...this.previousKeys, ...added] + + return { + removed, + changed, + added, + removedProps + } + } + + recordDiff (key: string, diffObj: string) { + const diff = this.diffObj(diffObj) + // problem is that 274 key 10.20.6 no removed keys in diff created + this.diffs[key] = diff + } + + static isOptimizedChangeDiff (changePossiblyArrDiff) { + if (!Array.isArray(changePossiblyArrDiff)) return false + if (changePossiblyArrDiff.length % 2 !== 0) return false + for (let i = 0; i < changePossiblyArrDiff.length; i += 2) { + if (typeof changePossiblyArrDiff[i] !== 'number') return false + } + return true + } + + static restoreData ({ keys, properties, source, arrKey, diffs }: SourceData, targetKey: string, dataRestorer: ((data) => void) | undefined) { + // if (!diffs[targetKey]) throw new Error(`The requested data to restore with key ${targetKey} does not exist`) + source = structuredClone(source) + const keysById = Object.fromEntries(Object.entries(keys).map(x => [x[1], x[0]])) + const propertiesById = Object.fromEntries(Object.entries(properties).map(x => [x[1], x[0]])) + const dataByKeys = {} as Record + for (const [versionKey, { added, changed, removed, removedProps }] of Object.entries(diffs)) { + for (const toAdd of added) { + dataByKeys[toAdd] = source[toAdd] + } + for (const toRemove of removed) { + delete dataByKeys[toRemove] + } + for (let i = 0; i < changed.length; i += 2) { + const key = changed[i] + const change = changed[i + 1] + const isOptimizedChange = JsonOptimizer.isOptimizedChangeDiff(change) + if (isOptimizedChange) { + // apply optimized diff + for (let k = 0; k < change.length; k += 2) { + const propId = change[k] + const newVal = change[k + 1] + const prop = propertiesById[propId] + // const prop = propId + if (prop === undefined) throw new Error(`Property id change is undefined: ${propId}`) + dataByKeys[key][prop] = newVal + } + } else { + dataByKeys[key] = change + } + } + for (const [key, removePropsId] of removedProps) { + for (const removePropId of removePropsId) { + const removeProp = propertiesById[removePropId] + // todo: this is not correct! + if (Array.isArray(dataByKeys[key])) { + dataByKeys[key].splice(removeProp as any, 1) // splice accepts strings as well + } else { + delete dataByKeys[key][removeProp] + } + } + } + if (versionToNumber(versionKey) <= versionToNumber(targetKey)) { + break + } + } + let data + if (arrKey) { + data = Object.values(dataByKeys) + } else { + data = Object.fromEntries(Object.entries(dataByKeys).map(([key, val]) => [keysById[key], val])) + } + dataRestorer?.(data) + return data + } + + static getByArrKey (item: any, arrKey: string) { + return arrKey.split('+').map(x => item[x]).join('+') + } + + static resolveDefaults (arr) { + if (!Array.isArray(arr)) throw new Error('not an array') + const propsValueCount = {} as { + [key: string]: { + [val: string]: number + } + } + for (const obj of arr) { + if (typeof obj !== 'object' || !obj) continue + for (const [key, val] of Object.entries(obj)) { + const valJson = JSON.stringify(val) + propsValueCount[key] ??= {} + propsValueCount[key][valJson] ??= 0 + propsValueCount[key][valJson] += 1 + } + } + const defaults = Object.fromEntries(Object.entries(propsValueCount).map(([prop, values]) => { + const defaultValue = Object.entries(values).sort(([, count1], [, count2]) => count2 - count1)[0][0] + return [prop, defaultValue] + })) + + const newData = [] as any[] + const noData = {} + for (const [i, obj] of arr.entries()) { + if (typeof obj !== 'object' || !obj) { + newData.push(obj) + continue + } + for (const key of Object.keys(defaults)) { + const val = obj[key] + if (!val) { + noData[key] ??= [] + noData[key].push(key) + continue + } + if (defaults[key] === JSON.stringify(val)) { + delete obj[key] + } + } + newData.push(obj) + } + + return { + data: newData, + defaults + } + } +} + +const isEqualStructured = (val1, val2) => { + return JSON.stringify(val1) === JSON.stringify(val2) +} diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index c0b64979..0cb0fe1e 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -1,18 +1,29 @@ -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useSnapshot } from 'valtio' -import { miscUiState, openOptionsMenu, showModal } from './globalState' -import { openURL } from './menus/components/common' -import { AppOptions, options } from './optionsStorage' +import { openURL } from 'renderer/viewer/lib/simpleUtils' +import { noCase } from 'change-case' +import { versionToNumber } from 'mc-assets/dist/utils' +import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState' +import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage' import Button from './react/Button' import { OptionMeta, OptionSlider } from './react/OptionsItems' import Slider from './react/Slider' -import { getScreenRefreshRate, setLoadingScreenStatus } from './utils' -import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs' -import { getResourcePackName, resourcePackState, uninstallTexturePack } from './texturePack' - +import { getScreenRefreshRate } from './utils' +import { setLoadingScreenStatus } from './appStatus' +import { openFilePicker, resetLocalStorage } from './browserfs' +import { completeResourcepackPackInstall, getResourcePackNames, resourcepackReload, resourcePackState, uninstallResourcePack } from './resourcePack' +import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy' +import { showInputsModal, showOptionsModal } from './react/SelectOption' +import { ClientMod, getAllMods, modsUpdateStatus } from './clientMods' +import supportedVersions from './supportedVersions.mjs' +import { getVersionAutoSelect } from './connect' +import { createNotificationProgressReporter } from './core/progressReporter' +import { customKeymaps } from './controls' +import { appStorage } from './react/appStorageProvider' +import { exportData, importData } from './core/importExport' export const guiOptionsScheme: { - [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial> } & { custom?}> + [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial> } & { custom? }> } = { render: [ { @@ -21,13 +32,23 @@ export const guiOptionsScheme: { const [frameLimitMax, setFrameLimitMax] = useState(null as number | null) return
- { - options.frameLimit = newVal > frameLimitMax! ? false : newVal - }} /> -
} }, @@ -42,6 +63,20 @@ export const guiOptionsScheme: { custom () { return + }, mouseSensX: {}, mouseSensY: { min: -1, @@ -169,9 +408,6 @@ export const guiOptionsScheme: { // eslint-disable-next-line no-extra-boolean-cast disabledReason: Boolean(document.documentElement.requestPointerLock) ? undefined : 'Your browser does not support pointer lock.', }, - alwaysShowMobileControls: { - text: 'Always Mobile Controls', - }, autoFullScreen: { tooltip: 'Auto Fullscreen allows you to use Ctrl+W and Escape having to wait/click on screen again.', disabledReason: navigator['keyboard'] ? undefined : 'Your browser doesn\'t support keyboard lock API' @@ -179,29 +415,89 @@ export const guiOptionsScheme: { autoExitFullscreen: { tooltip: 'Exit fullscreen on escape (pause menu open). But note you can always do it with F11.', }, + }, + { + custom () { + return Touch Controls + }, + alwaysShowMobileControls: { + text: 'Always Mobile Controls', + }, touchButtonsSize: { - min: 40 + min: 40, + disableIf: [ + 'touchMovementType', + 'modern' + ], }, touchButtonsOpacity: { min: 10, - max: 90 + max: 90, + disableIf: [ + 'touchMovementType', + 'modern' + ], }, touchButtonsPosition: { - max: 80 + max: 80, + disableIf: [ + 'touchMovementType', + 'modern' + ], }, - touchControlsType: { - values: [['classic', 'Classic'], ['joystick-buttons', 'New']], + touchMovementType: { + text: 'Movement Controls', + values: [['modern', 'Modern'], ['classic', 'Classic']], + }, + touchInteractionType: { + text: 'Interaction Controls', + values: [['classic', 'Classic'], ['buttons', 'Buttons']], }, }, { custom () { - const { touchControlsType } = useSnapshot(options) - return + return + }, + }, + { + custom () { + return + }, + }, + { + custom () { + return + }, + }, + { + custom () { + const { active, hasRecordedPackets } = useSnapshot(packetsRecordingState) + return + }, + }, + { + packetsLoggerPreset: { + text: 'Packets Logger Preset', + values: [ + ['all', 'All'], + ['no-buffers', 'No Buffers'] + ], + }, + }, + { + debugContro: { + text: 'Debug Controls', + }, + }, + { + debugResponseTimeIndicator: { + text: 'Debug Input Lag', + }, + }, + { + debugChatScroll: { }, } ], + 'export-import': [ + { + custom () { + return Export/Import Data + } + }, + { + custom () { + return + } + }, + { + custom () { + return + } + }, + { + custom () { + return + } + }, + { + custom () { + return + } + } + ], } -export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' +export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' | 'export-import' const Category = ({ children }) =>
{children}
+ +const UiToggleButton = ({ name, addUiText = false, label = noCase(name) }: { name: string, addUiText?: boolean, label?: string }) => { + const { disabledUiParts } = useSnapshot(options) + + const currentlyDisabled = disabledUiParts.includes(name) + if (addUiText) label = `${label} UI` + return +} + +export const tryFindOptionConfig = (option: keyof AppOptions) => { + for (const group of Object.values(guiOptionsScheme)) { + for (const optionConfig of group) { + if (option in optionConfig) { + return optionConfig[option] + } + } + } + + return null +} diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 63ad0b98..22d5ef26 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -1,81 +1,26 @@ -// todo implement async options storage - import { proxy, subscribe } from 'valtio/vanilla' -// weird webpack configuration bug: it cant import valtio/utils in this file import { subscribeKey } from 'valtio/utils' +import { omitObj } from '@zardoy/utils' +import { appQueryParams, appQueryParamsArray } from './appParams' +import type { AppConfig } from './appConfig' +import { appStorage } from './react/appStorageProvider' +import { miscUiState } from './globalState' +import { defaultOptions } from './defaultOptions' -const defaultOptions = { - renderDistance: 2, - multiplayerRenderDistance: 2, - closeConfirmation: true, - autoFullScreen: false, - mouseRawInput: false, - autoExitFullscreen: false, - localUsername: 'wanderer', - mouseSensX: 50, - mouseSensY: -1, - // mouseInvertX: false, - chatWidth: 320, - chatHeight: 180, - chatScale: 100, - chatOpacity: 100, - chatOpacityOpened: 100, - messagesLimit: 200, - volume: 50, - // fov: 70, - fov: 75, - guiScale: 3, - autoRequestCompletions: true, - touchButtonsSize: 40, - touchButtonsOpacity: 80, - touchButtonsPosition: 12, - touchControlsPositions: { - action: [ - 90, - 70 - ], - sneak: [ - 90, - 90 - ], - break: [ - 70, - 70 - ] - } as Record, - touchControlsType: 'classic' as 'classic' | 'joystick-buttons', - gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power', - /** @unstable */ - disableAssets: false, - /** @unstable */ - debugLogNotFrequentPackets: false, - unimplementedContainers: false, - dayCycleAndLighting: true, - loadPlayerSkins: true, - antiAliasing: false, +const isDev = process.env.NODE_ENV === 'development' +const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {} - showChunkBorders: false, // todo rename option - frameLimit: false as number | false, - alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null, - alwaysShowMobileControls: false, - excludeCommunicationDebugEvents: [], - preventDevReloadWhilePlaying: false, - numWorkers: 4, - localServerOptions: {} as any, - preferLoadReadonly: false, - disableLoadPrompts: false, - guestUsername: 'guest', - askGuestName: true, - /** Actually might be useful */ - showCursorBlockInSpectator: false, - renderEntities: true, - chatSelect: false, +// const qsOptionsRaw = new URLSearchParams(location.search).getAll('setting') +const qsOptionsRaw = appQueryParamsArray.setting ?? [] +export const qsOptions = Object.fromEntries(qsOptionsRaw.map(o => { + const [key, value] = o.split(':') + return [key, JSON.parse(value)] +})) - // advanced bot options - autoRespawn: false, - mutedSounds: [] as string[], - plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>, -} +// Track which settings are disabled (controlled by QS or forced by config) +export const disabledSettings = proxy({ + value: new Set(Object.keys(qsOptions)) +}) const migrateOptions = (options: Partial>) => { if (options.highPerformanceGpu) { @@ -85,15 +30,57 @@ const migrateOptions = (options: Partial>) => { if (Object.keys(options.touchControlsPositions ?? {}).length === 0) { options.touchControlsPositions = defaultOptions.touchControlsPositions } + if (options.touchControlsPositions?.jump === undefined) { + options.touchControlsPositions!.jump = defaultOptions.touchControlsPositions.jump + } + if (options.touchControlsType === 'joystick-buttons') { + options.touchInteractionType = 'buttons' + } return options } +const migrateOptionsLocalStorage = () => { + if (Object.keys(appStorage['options'] ?? {}).length) { + for (const key of Object.keys(appStorage['options'])) { + if (!(key in defaultOptions)) continue // drop unknown options + const defaultValue = defaultOptions[key] + if (JSON.stringify(defaultValue) !== JSON.stringify(appStorage['options'][key])) { + appStorage.changedSettings[key] = appStorage['options'][key] + } + } + delete appStorage['options'] + } +} export type AppOptions = typeof defaultOptions +const isDeepEqual = (a: any, b: any): boolean => { + if (a === b) return true + if (typeof a !== typeof b) return false + if (typeof a !== 'object') return false + if (a === null || b === null) return a === b + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false + return a.every((item, index) => isDeepEqual(item, b[index])) + } + const keysA = Object.keys(a) + const keysB = Object.keys(b) + if (keysA.length !== keysB.length) return false + return keysA.every(key => isDeepEqual(a[key], b[key])) +} + +export const getChangedSettings = () => { + return Object.fromEntries( + Object.entries(appStorage.changedSettings).filter(([key, value]) => !isDeepEqual(defaultOptions[key], value)) + ) +} + +migrateOptionsLocalStorage() export const options: AppOptions = proxy({ ...defaultOptions, - ...migrateOptions(JSON.parse(localStorage.options || '{}')) + ...initialAppConfig.defaultSettings, + ...migrateOptions(appStorage.changedSettings), + ...qsOptions }) window.options = window.settings = options @@ -104,15 +91,25 @@ export const resetOptions = () => { Object.defineProperty(window, 'debugChangedOptions', { get () { - return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v)) + return getChangedSettings() }, }) -subscribe(options, () => { - localStorage.options = JSON.stringify(options) +subscribe(options, (ops) => { + if (appQueryParams.freezeSettings === 'true') return + for (const op of ops) { + const [type, path, value] = op + // let patch + // let accessor = options + // for (const part of path) { + // } + const key = path[0] as string + if (disabledSettings.value.has(key)) continue + appStorage.changedSettings[key] = options[key] + } }) -type WatchValue = >(proxy: T, callback: (p: T) => void) => void +type WatchValue = >(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void export const watchValue: WatchValue = (proxy, callback) => { const watchedProps = new Set() @@ -121,11 +118,20 @@ export const watchValue: WatchValue = (proxy, callback) => { watchedProps.add(p.toString()) return Reflect.get(target, p, receiver) }, - })) + }), false) + const unsubscribes = [] as Array<() => void> for (const prop of watchedProps) { - subscribeKey(proxy, prop, () => { - callback(proxy) - }) + unsubscribes.push( + subscribeKey(proxy, prop, () => { + callback(proxy, true) + }) + ) + } + + return () => { + for (const unsubscribe of unsubscribes) { + unsubscribe() + } } } @@ -147,3 +153,12 @@ export const useOptionValue = (setting, valueCallback) => { valueCallback(setting) subscribe(setting, valueCallback) } + +export const getAppLanguage = () => { + if (options.language === 'auto') { + return miscUiState.appConfig?.defaultLanguage ?? navigator.language + } + return options.language +} + +export { defaultOptions } from './defaultOptions' diff --git a/src/packetsReplay/packetsReplayLegacy.ts b/src/packetsReplay/packetsReplayLegacy.ts new file mode 100644 index 00000000..a9cc71ec --- /dev/null +++ b/src/packetsReplay/packetsReplayLegacy.ts @@ -0,0 +1,70 @@ +import { proxy } from 'valtio' +import { PacketsLogger } from 'mcraft-fun-mineflayer/build/packetsLogger' +import { options } from '../optionsStorage' + +export const packetsRecordingState = proxy({ + active: options.packetsRecordingAutoStart, + hasRecordedPackets: false +}) + +// eslint-disable-next-line import/no-mutable-exports +export let replayLogger: PacketsLogger | undefined + +const isBufferData = (data: any): boolean => { + if (Buffer.isBuffer(data) || data instanceof Uint8Array) return true + if (typeof data === 'object' && data !== null) { + return Object.values(data).some(value => isBufferData(value)) + } + return false +} + +const processPacketData = (data: any): any => { + if (options.packetsLoggerPreset === 'no-buffers') { + if (Buffer.isBuffer(data)) { + return '[buffer]' + } + if (typeof data === 'object' && data !== null) { + const processed = {} + for (const [key, value] of Object.entries(data)) { + processed[key] = isBufferData(value) ? '[buffer]' : value + } + return processed + } + } + return data +} + +export default () => { + customEvents.on('mineflayerBotCreated', () => { + replayLogger = new PacketsLogger({ minecraftVersion: bot.version }) + replayLogger.contents = '' + packetsRecordingState.hasRecordedPackets = false + const handleServerPacket = (data, { name, state = bot._client.state }) => { + if (!packetsRecordingState.active) { + return + } + replayLogger!.log(true, { name, state }, processPacketData(data)) + packetsRecordingState.hasRecordedPackets = true + } + bot._client.on('packet', handleServerPacket) + bot._client.on('packet_name' as any, (name, data) => { + handleServerPacket(data, { name }) + }) + + bot._client.on('writePacket' as any, (name, data) => { + if (!packetsRecordingState.active) { + return + } + replayLogger!.log(false, { name, state: bot._client.state }, processPacketData(data)) + packetsRecordingState.hasRecordedPackets = true + }) + }) +} + +export const downloadPacketsReplay = async () => { + const a = document.createElement('a') + a.href = `data:text/plain;charset=utf-8,${encodeURIComponent(replayLogger!.contents)}` + a.download = `packets-replay-${new Date().toISOString()}.txt` + a.click() +} +globalThis.downloadPacketsReplay = downloadPacketsReplay diff --git a/src/packetsReplay/replayPackets.ts b/src/packetsReplay/replayPackets.ts new file mode 100644 index 00000000..54b3d652 --- /dev/null +++ b/src/packetsReplay/replayPackets.ts @@ -0,0 +1,366 @@ +/* eslint-disable no-await-in-loop */ +import { createServer, ServerClient } from 'minecraft-protocol' +import { ParsedReplayPacket, parseReplayContents } from 'mcraft-fun-mineflayer/build/packetsLogger' +import { PACKETS_REPLAY_FILE_EXTENSION, WORLD_STATE_FILE_EXTENSION } from 'mcraft-fun-mineflayer/build/worldState' +import MinecraftData from 'minecraft-data' +import { GameMode } from 'mineflayer' +import { UserError } from '../mineflayer/userError' +import { packetsReplayState } from '../react/state/packetsReplayState' +import { getFixedFilesize } from '../react/simpleUtils' +import { appQueryParams } from '../appParams' +import { LocalServer } from '../customServer' + +const SUPPORTED_FORMAT_VERSION = 1 + +type ReplayDefinition = { + minecraftVersion: string + replayAgainst?: 'client' | 'server' + serverIp?: string +} + +interface OpenFileOptions { + contents: string + filename?: string + filesize?: number +} + +export function openFile ({ contents, filename = 'unnamed', filesize }: OpenFileOptions) { + packetsReplayState.replayName = `${filename} (${getFixedFilesize(filesize ?? contents.length)})` + packetsReplayState.isPlaying = false + + const connectOptions = { + worldStateFileContents: contents, + username: 'replay' + } + dispatchEvent(new CustomEvent('connect', { detail: connectOptions })) +} + +export const startLocalReplayServer = (contents: string) => { + const { packets, header } = parseReplayContents(contents) + + packetsReplayState.packetsPlayback = [] + packetsReplayState.isOpen = true + packetsReplayState.isPlaying = true + packetsReplayState.progress = { + current: 0, + total: packets.filter(packet => packet.isFromServer).length + } + packetsReplayState.speed = 1 + packetsReplayState.replayName ||= `local ${getFixedFilesize(contents.length)}` + packetsReplayState.replayName = `${header.minecraftVersion} ${packetsReplayState.replayName}` + + if ('formatVersion' in header && header.formatVersion !== SUPPORTED_FORMAT_VERSION) { + throw new UserError(`Unsupported format version: ${header.formatVersion}`) + } + if ('replayAgainst' in header && header.replayAgainst === 'server') { + throw new Error('not supported') + } + + const server = createServer({ + Server: LocalServer as any, + version: header.minecraftVersion, + keepAlive: false, + 'online-mode': false + }) + + const data = MinecraftData(header.minecraftVersion) + server.on(data.supportFeature('hasConfigurationState') ? 'playerJoin' : 'login' as any, async client => { + await mainPacketsReplayer( + client, + packets, + packetsReplayState.customButtons.validateClientPackets.state ? undefined : true + ) + }) + + return { + server, + version: header.minecraftVersion + } +} + +// time based packets +// const FLATTEN_CLIENT_PACKETS = new Set(['position', 'position_look']) +const FLATTEN_CLIENT_PACKETS = new Set([] as string[]) + +const positions = { + client: 0, + server: 0 +} +const addPacketToReplayer = (name: string, data, isFromClient: boolean, wasUpcoming = false) => { + const side = isFromClient ? 'client' : 'server' + + if (wasUpcoming) { + const lastUpcoming = packetsReplayState.packetsPlayback.find(p => p.isUpcoming && p.name === name) + if (lastUpcoming) { + lastUpcoming.isUpcoming = false + } + } else { + packetsReplayState.packetsPlayback.push({ + name, + data, + isFromClient, + position: ++positions[side]!, + isUpcoming: false, + timestamp: Date.now() + }) + } + + if (!isFromClient && !wasUpcoming) { + packetsReplayState.progress.current++ + } +} + +const IGNORE_SERVER_PACKETS = new Set([ + 'kick_disconnect', +]) + +const ADDITIONAL_DELAY = 500 + +const mainPacketsReplayer = async (client: ServerClient, packets: ParsedReplayPacket[], ignoreClientPacketsWait: string[] | true = []) => { + const writePacket = (name: string, data: any) => { + data = restoreData(data) + client.write(name, data) + } + + const playPackets = packets.filter(p => p.state === 'play') + + let clientPackets = [] as Array<{ name: string, params: any }> + const clientsPacketsWaiter = createPacketsWaiter({ + unexpectedPacketReceived (name, params) { + console.log('unexpectedPacketReceived', name, params) + addPacketToReplayer(name, params, true) + }, + expectedPacketReceived (name, params) { + console.log('expectedPacketReceived', name, params) + addPacketToReplayer(name, params, true, true) + }, + unexpectedPacketsLimit: 15, + onUnexpectedPacketsLimitReached () { + addPacketToReplayer('...', {}, true) + } + }) + + // Patch console.error to detect errors + const originalConsoleError = console.error + let lastSentPacket: { name: string, params: any } | null = null + console.error = (...args) => { + if (lastSentPacket) { + console.log('Got error after packet', lastSentPacket.name, lastSentPacket.params) + } + originalConsoleError.apply(console, args) + if (packetsReplayState.customButtons.stopOnError.state) { + packetsReplayState.isPlaying = false + throw new Error('Replay stopped due to error: ' + args.join(' ')) + } + } + + const playServerPacket = (name: string, params: any) => { + try { + writePacket(name, params) + addPacketToReplayer(name, params, false) + lastSentPacket = { name, params } + } catch (err) { + console.error('Error processing packet:', err) + if (packetsReplayState.customButtons.stopOnError.state) { + packetsReplayState.isPlaying = false + } + } + } + + try { + bot.on('error', (err) => { + console.error('Mineflayer error:', err) + }) + + bot._client.on('writePacket' as any, (name, params) => { + clientsPacketsWaiter.addPacket(name, params) + }) + + console.log('start replaying!') + for (const [i, packet] of playPackets.entries()) { + if (!packetsReplayState.isPlaying) { + await new Promise(resolve => { + const interval = setInterval(() => { + if (packetsReplayState.isPlaying) { + clearInterval(interval) + resolve() + } + }, 100) + }) + } + + if (packet.isFromServer) { + if (packet.params === null) { + console.warn('packet.params is null', packet) + continue + } + playServerPacket(packet.name, packet.params) + if (packet.diff) { + await new Promise(resolve => { + setTimeout(resolve, packet.diff * packetsReplayState.speed + ADDITIONAL_DELAY * (packetsReplayState.customButtons.packetsSenderDelay.state ? 1 : 0)) + }) + } + } else if (ignoreClientPacketsWait !== true && !ignoreClientPacketsWait.includes(packet.name)) { + clientPackets.push({ name: packet.name, params: packet.params }) + if (playPackets[i + 1]?.isFromServer) { + // eslint-disable-next-line @typescript-eslint/no-loop-func + clientPackets = clientPackets.filter((p, index) => { + return !FLATTEN_CLIENT_PACKETS.has(p.name) || index === clientPackets.findIndex(clientPacket => clientPacket.name === p.name) + }) + for (const packet of clientPackets) { + packetsReplayState.packetsPlayback.push({ + name: packet.name, + data: packet.params, + isFromClient: true, + position: positions.client++, + timestamp: Date.now(), + isUpcoming: true, + }) + } + + await Promise.race([ + clientsPacketsWaiter.waitForPackets(clientPackets.map(p => p.name)), + ...(packetsReplayState.customButtons.skipMissingOnTimeout.state ? [new Promise(resolve => { + setTimeout(resolve, 1000) + })] : []) + ]) + clientsPacketsWaiter.stopWaiting() + clientPackets = [] + } + } + } + } finally { + // Restore original console.error + console.error = originalConsoleError + } +} + +export const switchGameMode = (gameMode: GameMode) => { + const gamemodes = { + survival: 0, + creative: 1, + adventure: 2, + spectator: 3 + } + if (gameMode === 'spectator') { + bot._client.emit('abilities', { + // can fly + is flying + flags: 6 + }) + } + bot._client.emit('game_state_change', { + reason: 3, + gameMode: gamemodes[gameMode] + }) +} + +interface PacketsWaiterOptions { + unexpectedPacketReceived?: (name: string, params: any) => void + expectedPacketReceived?: (name: string, params: any) => void + onUnexpectedPacketsLimitReached?: () => void + unexpectedPacketsLimit?: number +} + +interface PacketsWaiter { + addPacket(name: string, params: any): void + waitForPackets(packets: string[]): Promise + stopWaiting(): void +} + +const createPacketsWaiter = (options: PacketsWaiterOptions = {}): PacketsWaiter => { + let packetHandler: ((data: any, name: string) => void) | null = null + const queuedPackets: Array<{ name: string, params: any }> = [] + let isWaiting = false + let unexpectedPacketsCount = 0 + const handlePacket = (data: any, name: string, waitingPackets: string[], resolve: () => void) => { + if (waitingPackets.includes(name)) { + waitingPackets.splice(waitingPackets.indexOf(name), 1) + options.expectedPacketReceived?.(name, data) + } else { + if (options.unexpectedPacketsLimit && unexpectedPacketsCount < options.unexpectedPacketsLimit) { + options.unexpectedPacketReceived?.(name, data) + } + if (options.onUnexpectedPacketsLimitReached && unexpectedPacketsCount === options.unexpectedPacketsLimit) { + options.onUnexpectedPacketsLimitReached?.() + } + unexpectedPacketsCount++ + } + + if (waitingPackets.length === 0) { + resolve() + } + } + + return { + addPacket (name: string, params: any) { + if (packetHandler) { + packetHandler(params, name) + } else { + queuedPackets.push({ name, params }) + } + }, + + async waitForPackets (packets: string[]) { + if (isWaiting) { + throw new Error('Already waiting for packets') + } + unexpectedPacketsCount = 0 + isWaiting = true + + try { + await new Promise(resolve => { + const waitingPackets = [...packets] + + packetHandler = (data: any, name: string) => { + handlePacket(data, name, waitingPackets, resolve) + } + + // Process any queued packets + for (const packet of queuedPackets) { + handlePacket(packet.params, packet.name, waitingPackets, resolve) + } + queuedPackets.length = 0 + }) + } finally { + isWaiting = false + packetHandler = null + } + }, + stopWaiting () { + isWaiting = false + packetHandler = null + queuedPackets.length = 0 + } + } +} + +const isArrayEqual = (a: any[], b: any[]) => { + if (a.length !== b.length) return false + for (const [i, element] of a.entries()) { + if (element !== b[i]) return false + } + return true +} + +const restoreData = (json: any) => { + if (!json) return json + const keys = Object.keys(json) + + if (isArrayEqual(keys.sort(), ['data', 'type'].sort())) { + if (json.type === 'Buffer') { + return Buffer.from(json.data) + } + } + + if (typeof json === 'object' && json) { + for (const [key, value] of Object.entries(json)) { + if (typeof value === 'object') { + json[key] = restoreData(value) + } + } + } + + return json +} + +export const VALID_REPLAY_EXTENSIONS = [`.${PACKETS_REPLAY_FILE_EXTENSION}`, `.${WORLD_STATE_FILE_EXTENSION}`] diff --git a/src/panorama.ts b/src/panorama.ts deleted file mode 100644 index 1f614371..00000000 --- a/src/panorama.ts +++ /dev/null @@ -1,126 +0,0 @@ -//@ts-check - -import { join } from 'path' -import fs from 'fs' -import { subscribeKey } from 'valtio/utils' -import Entity from 'prismarine-viewer/viewer/lib/entity/Entity' -import { fromTexturePackPath, resourcePackState } from './texturePack' -import { options, watchValue } from './optionsStorage' -import { miscUiState } from './globalState' - -let panoramaCubeMap -let shouldDisplayPanorama = false -let panoramaUsesResourcePack = null as boolean | null - -const panoramaFiles = [ - 'panorama_1.png', // WS - 'panorama_3.png', // ES - 'panorama_4.png', // Up - 'panorama_5.png', // Down - 'panorama_0.png', // NS - 'panorama_2.png' // SS -] - -const panoramaResourcePackPath = 'assets/minecraft/textures/gui/title/background' -const possiblyLoadPanoramaFromResourcePack = async (file) => { - let base64Texture - if (panoramaUsesResourcePack) { - try { - base64Texture = await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, file)), 'base64') - } catch (err) { - panoramaUsesResourcePack = false - } - } - if (base64Texture) return `data:image/png;base64,${base64Texture}` - else return join('extra-textures/background', file) -} - -const updateResourcePackSupportPanorama = async () => { - try { - await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, panoramaFiles[0])), 'base64') - panoramaUsesResourcePack = true - } catch (err) { - panoramaUsesResourcePack = false - } -} - -watchValue(miscUiState, m => { - if (m.appLoaded) { - // Also adds panorama on app load here - watchValue(resourcePackState, async (s) => { - const oldState = panoramaUsesResourcePack - const newState = s.resourcePackInstalled && (await updateResourcePackSupportPanorama(), panoramaUsesResourcePack) - if (newState === oldState) return - removePanorama() - void addPanoramaCubeMap() - }) - } -}) - -subscribeKey(miscUiState, 'loadedDataVersion', () => { - if (miscUiState.loadedDataVersion) removePanorama() - else void addPanoramaCubeMap() -}) - -// Menu panorama background -// TODO-low use abort controller -export async function addPanoramaCubeMap () { - if (panoramaCubeMap || miscUiState.loadedDataVersion || options.disableAssets) return - shouldDisplayPanorama = true - - let time = 0 - viewer.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000) - viewer.camera.updateProjectionMatrix() - viewer.camera.position.set(0, 0, 0) - viewer.camera.rotation.set(0, 0, 0) - const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000) - - const loader = new THREE.TextureLoader() - const panorMaterials = [] as THREE.MeshBasicMaterial[] - await updateResourcePackSupportPanorama() - for (const file of panoramaFiles) { - panorMaterials.push(new THREE.MeshBasicMaterial({ - map: loader.load(await possiblyLoadPanoramaFromResourcePack(file)), - transparent: true, - side: THREE.DoubleSide - })) - } - - if (!shouldDisplayPanorama) return - - const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials) - - panoramaBox.onBeforeRender = () => { - time += 0.01 - panoramaBox.rotation.y = Math.PI + time * 0.01 - panoramaBox.rotation.z = Math.sin(-time * 0.001) * 0.001 - } - - const group = new THREE.Object3D() - group.add(panoramaBox) - - // should be rewritten entirely - for (let i = 0; i < 20; i++) { - const m = new Entity('1.16.4', 'squid').mesh - m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17) - m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX') - const v = Math.random() * 0.01 - m.children[0].onBeforeRender = () => { - m.rotation.y += v - m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2 - } - group.add(m) - } - - viewer.scene.add(group) - panoramaCubeMap = group -} - -export function removePanorama () { - shouldDisplayPanorama = false - if (!panoramaCubeMap) return - viewer.camera = new THREE.PerspectiveCamera(options.fov, window.innerWidth / window.innerHeight, 0.1, 1000) - viewer.camera.updateProjectionMatrix() - viewer.scene.remove(panoramaCubeMap) - panoramaCubeMap = null -} diff --git a/src/parseServerAddress.ts b/src/parseServerAddress.ts new file mode 100644 index 00000000..acedf70a --- /dev/null +++ b/src/parseServerAddress.ts @@ -0,0 +1,54 @@ + + +export const parseServerAddress = (address: string | undefined, removeHttp = true): ParsedServerAddress => { + if (!address) { + return { host: '', isWebSocket: false, serverIpFull: '' } + } + + if (/^ws:[^/]/.test(address)) address = address.replace('ws:', 'ws://') + if (/^wss:[^/]/.test(address)) address = address.replace('wss:', 'wss://') + const isWebSocket = address.startsWith('ws://') || address.startsWith('wss://') + if (isWebSocket) { + return { host: address, isWebSocket: true, serverIpFull: address } + } + + if (removeHttp) { + address = address.replace(/^https?:\/\//, '') + } + + const parts = address.split(':') + + let version: string | null = null + let port: string | null = null + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + if (/^\d+\.\d+(\.\d+)?$/.test(part)) { + version = part + parts.splice(i, 1) + i-- + } + if (/^\d+$/.test(part)) { + port = part + parts.splice(i, 1) + i-- + } + } + + const host = parts.join(':') + return { + host, + ...(port ? { port } : {}), + ...(version ? { version } : {}), + isWebSocket: false, + serverIpFull: port ? `${host}:${port}` : host + } +} + +export interface ParsedServerAddress { + host: string + port?: string + version?: string + isWebSocket: boolean + serverIpFull: string +} diff --git a/src/perf_hooks_replacement.js b/src/perf_hooks_replacement.js deleted file mode 100644 index 69b0e2ed..00000000 --- a/src/perf_hooks_replacement.js +++ /dev/null @@ -1 +0,0 @@ -module.exports.performance = window.performance diff --git a/src/playerWindows.ts b/src/playerWindows.ts deleted file mode 100644 index eff7262c..00000000 --- a/src/playerWindows.ts +++ /dev/null @@ -1,501 +0,0 @@ -import { subscribe } from 'valtio' -import { showInventory } from 'minecraft-inventory-gui/web/ext.mjs' -import InventoryGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/inventory.png' -import ChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/shulker_box.png' -import LargeChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/generic_54.png' -import FurnaceGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/furnace.png' -import CraftingTableGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/crafting_table.png' -import DispenserGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/dispenser.png' -import HopperGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/hopper.png' -import HorseGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/horse.png' -import VillagerGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/villager2.png' -import EnchantingGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/enchanting_table.png' -import AnvilGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/anvil.png' -import BeaconGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/beacon.png' - -import Dirt from 'minecraft-assets/minecraft-assets/data/1.17.1/blocks/dirt.png' -import { subscribeKey } from 'valtio/utils' -import MinecraftData, { RecipeItem } from 'minecraft-data' -import { getVersion } from 'prismarine-viewer/viewer/lib/version' -import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' -import itemsPng from 'prismarine-viewer/public/textures/items.png' -import itemsLegacyPng from 'prismarine-viewer/public/textures/items-legacy.png' -import _itemsAtlases from 'prismarine-viewer/public/textures/items.json' -import type { ItemsAtlasesOutputJson } from 'prismarine-viewer/viewer/prepare/genItemsAtlas' -import PrismarineBlockLoader from 'prismarine-block' -import { flat } from '@xmcl/text-component' -import mojangson from 'mojangson' -import nbt from 'prismarine-nbt' -import { splitEvery, equals } from 'rambda' -import PItem, { Item } from 'prismarine-item' -import Generic95 from '../assets/generic_95.png' -import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState' -import invspriteJson from './invsprite.json' -import { options } from './optionsStorage' -import { assertDefined } from './utils' - -export const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases -const loadedImagesCache = new Map() -const cleanLoadedImagesCache = () => { - loadedImagesCache.delete('blocks') -} -export type BlockStates = Record -}> - -let lastWindow -/** bot version */ -let version: string -let PrismarineBlock: typeof PrismarineBlockLoader.Block -let PrismarineItem: typeof Item - -export const onGameLoad = (onLoad) => { - let loaded = 0 - const onImageLoaded = () => { - loaded++ - if (loaded === 3) onLoad?.() - } - version = bot.version - getImage({ path: 'invsprite' }, onImageLoaded) - getImage({ path: 'items' }, onImageLoaded) - getImage({ path: 'items-legacy' }, onImageLoaded) - PrismarineBlock = PrismarineBlockLoader(version) - PrismarineItem = PItem(version) - - bot.on('windowOpen', (win) => { - if (implementedContainersGuiMap[win.type]) { - // todo also render title! - openWindow(implementedContainersGuiMap[win.type]) - } else if (options.unimplementedContainers) { - openWindow('ChestWin') - } else { - // todo format - bot._client.emit('chat', { - message: JSON.stringify({ - text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item) ?? '(empty)').join(', ')}` - }) - }) - bot.currentWindow?.['close']() - } - }) - - bot.inventory.on('updateSlot', ((_oldSlot, oldItem, newItem) => { - const oldSlot = _oldSlot as number - if (!miscUiState.singleplayer) return - const { craftingResultSlot } = bot.inventory - if (oldSlot === craftingResultSlot && oldItem && !newItem) { - for (let i = 1; i < 5; i++) { - const count = bot.inventory.slots[i]?.count - if (count && count > 1) { - const slot = bot.inventory.slots[i]! - slot.count-- - void bot.creative.setInventorySlot(i, slot) - } else { - void bot.creative.setInventorySlot(i, null) - } - } - return - } - const craftingSlots = bot.inventory.slots.slice(1, 5) - const resultingItem = getResultingRecipe(craftingSlots, 2) - void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null) - }) as any) - - bot.on('windowClose', () => { - // todo hide up to the window itself! - hideCurrentModal() - }) - - customEvents.on('search', (q) => { - if (!lastWindow) return - upJei(q) - }) -} - -const findTextureInBlockStates = (name) => { - assertDefined(viewer) - const blockStates: BlockStates = viewer.world.customBlockStatesData || viewer.world.downloadedBlockStatesData - const vars = blockStates[name]?.variants - if (!vars) return - let firstVar = Object.values(vars)[0] - if (Array.isArray(firstVar)) firstVar = firstVar[0] - if (!firstVar) return - const elements = firstVar.model?.elements - if (elements?.length !== 1) return - return elements[0].faces -} - -const svSuToCoordinates = (path: string, u, v, su, sv = su) => { - const img = getImage({ path })! - if (!img.width) throw new Error(`Image ${path} is not loaded`) - return [u * img.width, v * img.height, su * img.width, sv * img.height] -} - -const getBlockData = (name) => { - const data = findTextureInBlockStates(name) - if (!data) return - - const getSpriteBlockSide = (side) => { - const d = data[side]?.texture - if (!d) return - const spriteSide = svSuToCoordinates('blocks', d.u, d.v, d.su, d.sv) - const blockSideData = { - slice: spriteSide, - path: 'blocks' - } - return blockSideData - } - - return { - // todo look at grass bug - top: getSpriteBlockSide('up') || getSpriteBlockSide('top'), - left: getSpriteBlockSide('east') || getSpriteBlockSide('side'), - right: getSpriteBlockSide('north') || getSpriteBlockSide('side'), - } -} - -const getInvspriteSlice = (name) => { - const invspriteImg = loadedImagesCache.get('invsprite') - if (!invspriteImg?.width) return - - const { x, y } = invspriteJson[name] ?? /* unknown item */ { x: 0, y: 0 } - const sprite = [x, y, 32, 32] - return sprite -} - -const getImageSrc = (path): string | HTMLImageElement => { - assertDefined(viewer) - switch (path) { - case 'gui/container/inventory': return InventoryGui - case 'blocks': return viewer.world.customTexturesDataUrl || viewer.world.downloadedTextureImage - case 'invsprite': return `invsprite.png` - case 'items': return itemsPng - case 'items-legacy': return itemsLegacyPng - case 'gui/container/dispenser': return DispenserGui - case 'gui/container/furnace': return FurnaceGui - case 'gui/container/crafting_table': return CraftingTableGui - case 'gui/container/shulker_box': return ChestLikeGui - case 'gui/container/generic_54': return LargeChestLikeGui - case 'gui/container/generic_95': return Generic95 - case 'gui/container/hopper': return HopperGui - case 'gui/container/horse': return HorseGui - case 'gui/container/villager2': return VillagerGui - case 'gui/container/enchanting_table': return EnchantingGui - case 'gui/container/anvil': return AnvilGui - case 'gui/container/beacon': return BeaconGui - } - return Dirt -} - -const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => { }) => { - if (!path && !texture) throw new Error('Either pass path or texture') - const loadPath = (blockData ? 'blocks' : path ?? texture)! - if (loadedImagesCache.has(loadPath)) { - onLoad() - } else { - const imageSrc = getImageSrc(loadPath) - let image: HTMLImageElement - if (imageSrc instanceof Image) { - image = imageSrc - } else { - image = new Image() - image.src = imageSrc - } - image.onload = onLoad - loadedImagesCache.set(loadPath, image) - } - return loadedImagesCache.get(loadPath) -} - -const getItemVerToRender = (version: string, item: string, itemsMapSortedEntries: any[]) => { - const verNumber = versionToNumber(version) - for (const [itemsVer, items] of itemsMapSortedEntries) { - // 1.18 < 1.18.1 - // 1.13 < 1.13.2 - if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) { - return itemsVer as string - } - } -} - -const isFullBlock = (block: string) => { - const blockData = loadedData.blocksByName[block] - if (!blockData) return false - const pBlock = new PrismarineBlock(blockData.id, 0, 0) - if (pBlock.shapes?.length !== 1) return false - const shape = pBlock.shapes[0]! - return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 -} - -type RenderSlot = Pick -const renderSlot = (slot: RenderSlot, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => { - const itemName = slot.name - const isItem = loadedData.itemsByName[itemName] - const fullBlock = isFullBlock(itemName) - - if (isItem) { - const legacyItemVersion = getItemVerToRender(version, itemName, itemsAtlases.legacyMap) - const vuToSlice = ({ u, v }, size) => [...svSuToCoordinates('items', u, v, size).slice(0, 2), 16, 16] // item size is fixed - if (legacyItemVersion) { - const textureData = itemsAtlases.legacy.textures[`${legacyItemVersion}-${itemName}`]! - return { - texture: 'items-legacy', - slice: vuToSlice(textureData, itemsAtlases.legacy.size) - } - } - const textureData = itemsAtlases.latest.textures[itemName] - if (textureData) { - return { - texture: 'items', - slice: vuToSlice(textureData, itemsAtlases.latest.size) - } - } - } - if (fullBlock && !skipBlock) { - const blockData = getBlockData(itemName) - if (blockData) { - return { - texture: 'blocks', - blockData - } - } - } - const invspriteSlice = getInvspriteSlice(itemName) - if (invspriteSlice) { - return { - texture: 'invsprite', - scale: 0.5, - slice: invspriteSlice - } - } -} - -type JsonString = string -type PossibleItemProps = { - Damage?: number - display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"} -} -export const getItemName = (item: import('prismarine-item').Item | null) => { - if (!item?.nbt) return - const itemNbt: PossibleItemProps = nbt.simplify(item.nbt) - const customName = itemNbt.display?.Name - if (!customName) return - const parsed = mojangson.simplify(mojangson.parse(customName)) - // todo display damage and full text renderer from sign renderer - const text = flat(parsed).map(x => x.text) - return text -} - -export const renderSlotExternal = (slot) => { - const data = renderSlot(slot, true) - if (!data) return - return { - imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture })?.src, - sprite: data.slice && data.texture !== 'invsprite' ? data.slice.map(x => x * 2) : data.slice, - displayName: getItemName(slot) ?? slot.displayName, - } -} - -const mapSlots = (slots: Array) => { - return slots.map(slot => { - // todo stateid - if (!slot) return - - try { - const slotCustomProps = renderSlot(slot) - Object.assign(slot, { ...slotCustomProps, displayName: ('nbt' in slot ? getItemName(slot) : undefined) ?? slot.displayName }) - } catch (err) { - console.error(err) - } - return slot - }) -} - -const upInventory = (isInventory: boolean) => { - // inv.pwindow.inv.slots[2].displayName = 'test' - // inv.pwindow.inv.slots[2].blockData = getBlockData('dirt') - const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots) - lastWindow.pwindow.setSlots(customSlots) -} - -export const onModalClose = (callback: () => any) => { - const { length } = activeModalStack - const unsubscribe = subscribe(activeModalStack, () => { - if (activeModalStack.length < length) { - callback() - unsubscribe() - } - }) -} - -const implementedContainersGuiMap = { - // todo allow arbitrary size instead! - 'minecraft:generic_9x3': 'ChestWin', - 'minecraft:generic_9x5': 'Generic95Win', - // hopper - 'minecraft:generic_5x1': 'HopperWin', - 'minecraft:generic_9x6': 'LargeChestWin', - 'minecraft:generic_3x3': 'DropDispenseWin', - 'minecraft:furnace': 'FurnaceWin', - 'minecraft:smoker': 'FurnaceWin', - 'minecraft:crafting': 'CraftingWin', - 'minecraft:anvil': 'AnvilWin', - // enchant - 'minecraft:enchanting_table': 'EnchantingWin', - // horse - 'minecraft:horse': 'HorseWin', - // villager - 'minecraft:villager': 'VillagerWin', -} - -const upJei = (search: string) => { - search = search.toLowerCase() - // todo fix pre flat - const matchedSlots = loadedData.itemsArray.map(x => { - if (!x.displayName.toLowerCase().includes(search)) return null! - return new PrismarineItem(x.id, 1) - }).filter(Boolean) - lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots) -} - -const openWindow = (type: string | undefined) => { - // if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) { - if (activeModalStack.length) { // game is not in foreground, don't close current modal - if (type) bot.currentWindow?.['close']() - return - } - showModal({ - reactType: `player_win:${type}`, - }) - onModalClose(() => { - // might be already closed (event fired) - if (type !== undefined && bot.currentWindow) bot.currentWindow['close']() - lastWindow.destroy() - lastWindow = null - miscUiState.displaySearchInput = false - destroyFn() - }) - cleanLoadedImagesCache() - const inv = showInventory(type, getImage, {}, bot) - inv.canvas.style.zIndex = '10' - inv.canvas.style.position = 'fixed' - inv.canvas.style.inset = '0' - // todo scaling - inv.canvasManager.setScale(window.innerWidth < 470 ? 1.5 : window.innerHeight < 480 || window.innerWidth < 760 ? 2 : window.innerHeight < 700 ? 3 : 4) - - inv.canvasManager.onClose = () => { - hideCurrentModal() - inv.canvasManager.destroy() - } - - lastWindow = inv - const upWindowItems = () => { - void Promise.resolve().then(() => upInventory(type === undefined)) - } - upWindowItems() - - lastWindow.pwindow.touch = miscUiState.currentTouch - lastWindow.pwindow.onJeiClick = (slotItem, _index, isRightclick) => { - // slotItem is the slot from mapSlots - const itemId = loadedData.itemsByName[slotItem.name]?.id - if (!itemId) { - console.error(`Item for block ${slotItem.name} not found`) - return - } - const item = new PrismarineItem(itemId, isRightclick ? 64 : 1, slotItem.metadata) - const freeSlot = bot.inventory.firstEmptyInventorySlot() - if (freeSlot === null) return - void bot.creative.setInventorySlot(freeSlot, item) - } - - if (bot.game.gameMode === 'creative') { - lastWindow.pwindow.win.jeiSlotsPage = 0 - // todo workaround so inventory opens immediately (but still lags) - setTimeout(() => { - upJei('') - }) - miscUiState.displaySearchInput = true - } else { - lastWindow.pwindow.win.jeiSlots = [] - } - - if (type === undefined) { - // player inventory - bot.inventory.on('updateSlot', upWindowItems) - destroyFn = () => { - bot.inventory.off('updateSlot', upWindowItems) - } - } else { - //@ts-expect-error - bot.currentWindow.on('updateSlot', () => { - upWindowItems() - }) - } -} - -let destroyFn = () => { } - -export const openPlayerInventory = () => { - openWindow(undefined) -} - -const getResultingRecipe = (slots: Array, gridRows: number) => { - const inputSlotsItems = slots.map(blockSlot => blockSlot?.type) - let currentShape = splitEvery(gridRows, inputSlotsItems as Array) - // todo rewrite with candidates search - if (currentShape.length > 1) { - // eslint-disable-next-line @typescript-eslint/no-for-in-array - for (const slotX in currentShape[0]) { - if (currentShape[0][slotX] !== undefined) { - for (const [otherY] of Array.from({ length: gridRows }).entries()) { - if (currentShape[otherY]?.[slotX] === undefined) { - currentShape[otherY]![slotX] = null - } - } - } - } - } - currentShape = currentShape.map(arr => arr.filter(x => x !== undefined)).filter(x => x.length !== 0) - - // todo rewrite - // eslint-disable-next-line @typescript-eslint/require-array-sort-compare - const slotsIngredients = [...inputSlotsItems].sort().filter(item => item !== undefined) - type Result = RecipeItem | undefined - let shapelessResult: Result - let shapeResult: Result - outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes)) { - for (const recipeVariant of recipeVariants) { - if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) { - shapeResult = recipeVariant.result! - break outer - } - if ('ingredients' in recipeVariant && equals(slotsIngredients, recipeVariant.ingredients?.sort() as number[])) { - shapelessResult = recipeVariant.result - break outer - } - } - } - const result = shapeResult ?? shapelessResult - if (!result) return - const id = typeof result === 'number' ? result : Array.isArray(result) ? result[0] : result.id - if (!id) return - const count = (typeof result === 'number' ? undefined : Array.isArray(result) ? result[1] : result.count) ?? 1 - const metadata = typeof result === 'object' && !Array.isArray(result) ? result.metadata : undefined - const item = new PrismarineItem(id, count, metadata) - return item -} diff --git a/src/preflatMap.json b/src/preflatMap.json new file mode 100644 index 00000000..81c2a20a --- /dev/null +++ b/src/preflatMap.json @@ -0,0 +1,1741 @@ +{ + "blocks": { + "0:0": "air", + "1:0": "stone", + "1:1": "granite", + "1:2": "polished_granite", + "1:3": "diorite", + "1:4": "polished_diorite", + "1:5": "andesite", + "1:6": "polished_andesite", + "2:0": "grass_block[snowy=false]", + "3:0": "dirt", + "3:1": "coarse_dirt", + "3:2": "podzol[snowy=false]", + "4:0": "cobblestone", + "5:0": "oak_planks", + "5:1": "spruce_planks", + "5:2": "birch_planks", + "5:3": "jungle_planks", + "5:4": "acacia_planks", + "5:5": "dark_oak_planks", + "6:0": "oak_sapling[stage=0]", + "6:1": "spruce_sapling[stage=0]", + "6:2": "birch_sapling[stage=0]", + "6:3": "jungle_sapling[stage=0]", + "6:4": "acacia_sapling[stage=0]", + "6:5": "dark_oak_sapling[stage=0]", + "6:8": "oak_sapling[stage=1]", + "6:9": "spruce_sapling[stage=1]", + "6:10": "birch_sapling[stage=1]", + "6:11": "jungle_sapling[stage=1]", + "6:12": "acacia_sapling[stage=1]", + "6:13": "dark_oak_sapling[stage=1]", + "7:0": "bedrock", + "8:0": "water[level=0]", + "8:1": "water[level=1]", + "8:2": "water[level=2]", + "8:3": "water[level=3]", + "8:4": "water[level=4]", + "8:5": "water[level=5]", + "8:6": "water[level=6]", + "8:7": "water[level=7]", + "8:8": "water[level=8]", + "8:9": "water[level=9]", + "8:10": "water[level=10]", + "8:11": "water[level=11]", + "8:12": "water[level=12]", + "8:13": "water[level=13]", + "8:14": "water[level=14]", + "8:15": "water[level=15]", + "9:0": "water[level=0]", + "9:1": "water[level=1]", + "9:2": "water[level=2]", + "9:3": "water[level=3]", + "9:4": "water[level=4]", + "9:5": "water[level=5]", + "9:6": "water[level=6]", + "9:7": "water[level=7]", + "9:8": "water[level=8]", + "9:9": "water[level=9]", + "9:10": "water[level=10]", + "9:11": "water[level=11]", + "9:12": "water[level=12]", + "9:13": "water[level=13]", + "9:14": "water[level=14]", + "9:15": "water[level=15]", + "10:0": "lava[level=0]", + "10:1": "lava[level=1]", + "10:2": "lava[level=2]", + "10:3": "lava[level=3]", + "10:4": "lava[level=4]", + "10:5": "lava[level=5]", + "10:6": "lava[level=6]", + "10:7": "lava[level=7]", + "10:8": "lava[level=8]", + "10:9": "lava[level=9]", + "10:10": "lava[level=10]", + "10:11": "lava[level=11]", + "10:12": "lava[level=12]", + "10:13": "lava[level=13]", + "10:14": "lava[level=14]", + "10:15": "lava[level=15]", + "11:0": "lava[level=0]", + "11:1": "lava[level=1]", + "11:2": "lava[level=2]", + "11:3": "lava[level=3]", + "11:4": "lava[level=4]", + "11:5": "lava[level=5]", + "11:6": "lava[level=6]", + "11:7": "lava[level=7]", + "11:8": "lava[level=8]", + "11:9": "lava[level=9]", + "11:10": "lava[level=10]", + "11:11": "lava[level=11]", + "11:12": "lava[level=12]", + "11:13": "lava[level=13]", + "11:14": "lava[level=14]", + "11:15": "lava[level=15]", + "12:0": "sand", + "12:1": "red_sand", + "13:0": "gravel", + "14:0": "gold_ore", + "15:0": "iron_ore", + "16:0": "coal_ore", + "17:0": "oak_log[axis=y]", + "17:1": "spruce_log[axis=y]", + "17:2": "birch_log[axis=y]", + "17:3": "jungle_log[axis=y]", + "17:4": "oak_log[axis=x]", + "17:5": "spruce_log[axis=x]", + "17:6": "birch_log[axis=x]", + "17:7": "jungle_log[axis=x]", + "17:8": "oak_log[axis=z]", + "17:9": "spruce_log[axis=z]", + "17:10": "birch_log[axis=z]", + "17:11": "jungle_log[axis=z]", + "17:12": "oak_bark", + "17:13": "spruce_bark", + "17:14": "birch_bark", + "17:15": "jungle_bark", + "18:0": "oak_leaves[check_decay=false,decayable=true]", + "18:1": "spruce_leaves[check_decay=false,decayable=true]", + "18:2": "birch_leaves[check_decay=false,decayable=true]", + "18:3": "jungle_leaves[check_decay=false,decayable=true]", + "18:4": "oak_leaves[check_decay=false,decayable=false]", + "18:5": "spruce_leaves[check_decay=false,decayable=false]", + "18:6": "birch_leaves[check_decay=false,decayable=false]", + "18:7": "jungle_leaves[check_decay=false,decayable=false]", + "18:8": "oak_leaves[check_decay=true,decayable=true]", + "18:9": "spruce_leaves[check_decay=true,decayable=true]", + "18:10": "birch_leaves[check_decay=true,decayable=true]", + "18:11": "jungle_leaves[check_decay=true,decayable=true]", + "18:12": "oak_leaves[check_decay=true,decayable=false]", + "18:13": "spruce_leaves[check_decay=true,decayable=false]", + "18:14": "birch_leaves[check_decay=true,decayable=false]", + "18:15": "jungle_leaves[check_decay=true,decayable=false]", + "19:0": "sponge", + "19:1": "wet_sponge", + "20:0": "glass", + "21:0": "lapis_ore", + "22:0": "lapis_block", + "23:0": "dispenser[facing=down,triggered=false]", + "23:1": "dispenser[facing=up,triggered=false]", + "23:2": "dispenser[facing=north,triggered=false]", + "23:3": "dispenser[facing=south,triggered=false]", + "23:4": "dispenser[facing=west,triggered=false]", + "23:5": "dispenser[facing=east,triggered=false]", + "23:8": "dispenser[facing=down,triggered=true]", + "23:9": "dispenser[facing=up,triggered=true]", + "23:10": "dispenser[facing=north,triggered=true]", + "23:11": "dispenser[facing=south,triggered=true]", + "23:12": "dispenser[facing=west,triggered=true]", + "23:13": "dispenser[facing=east,triggered=true]", + "24:0": "sandstone", + "24:1": "chiseled_sandstone", + "24:2": "cut_sandstone", + "25:0": "note_block", + "26:0": "red_bed[facing=south,occupied=false,part=foot]", + "26:1": "red_bed[facing=west,occupied=false,part=foot]", + "26:2": "red_bed[facing=north,occupied=false,part=foot]", + "26:3": "red_bed[facing=east,occupied=false,part=foot]", + "26:8": "red_bed[facing=south,occupied=false,part=head]", + "26:9": "red_bed[facing=west,occupied=false,part=head]", + "26:10": "red_bed[facing=north,occupied=false,part=head]", + "26:11": "red_bed[facing=east,occupied=false,part=head]", + "26:12": "red_bed[facing=south,occupied=true,part=head]", + "26:13": "red_bed[facing=west,occupied=true,part=head]", + "26:14": "red_bed[facing=north,occupied=true,part=head]", + "26:15": "red_bed[facing=east,occupied=true,part=head]", + "27:0": "powered_rail[powered=false,shape=north_south]", + "27:1": "powered_rail[powered=false,shape=east_west]", + "27:2": "powered_rail[powered=false,shape=ascending_east]", + "27:3": "powered_rail[powered=false,shape=ascending_west]", + "27:4": "powered_rail[powered=false,shape=ascending_north]", + "27:5": "powered_rail[powered=false,shape=ascending_south]", + "27:8": "powered_rail[powered=true,shape=north_south]", + "27:9": "powered_rail[powered=true,shape=east_west]", + "27:10": "powered_rail[powered=true,shape=ascending_east]", + "27:11": "powered_rail[powered=true,shape=ascending_west]", + "27:12": "powered_rail[powered=true,shape=ascending_north]", + "27:13": "powered_rail[powered=true,shape=ascending_south]", + "28:0": "detector_rail[powered=false,shape=north_south]", + "28:1": "detector_rail[powered=false,shape=east_west]", + "28:2": "detector_rail[powered=false,shape=ascending_east]", + "28:3": "detector_rail[powered=false,shape=ascending_west]", + "28:4": "detector_rail[powered=false,shape=ascending_north]", + "28:5": "detector_rail[powered=false,shape=ascending_south]", + "28:8": "detector_rail[powered=true,shape=north_south]", + "28:9": "detector_rail[powered=true,shape=east_west]", + "28:10": "detector_rail[powered=true,shape=ascending_east]", + "28:11": "detector_rail[powered=true,shape=ascending_west]", + "28:12": "detector_rail[powered=true,shape=ascending_north]", + "28:13": "detector_rail[powered=true,shape=ascending_south]", + "29:0": "sticky_piston[extended=false,facing=down]", + "29:1": "sticky_piston[extended=false,facing=up]", + "29:2": "sticky_piston[extended=false,facing=north]", + "29:3": "sticky_piston[extended=false,facing=south]", + "29:4": "sticky_piston[extended=false,facing=west]", + "29:5": "sticky_piston[extended=false,facing=east]", + "29:8": "sticky_piston[extended=true,facing=down]", + "29:9": "sticky_piston[extended=true,facing=up]", + "29:10": "sticky_piston[extended=true,facing=north]", + "29:11": "sticky_piston[extended=true,facing=south]", + "29:12": "sticky_piston[extended=true,facing=west]", + "29:13": "sticky_piston[extended=true,facing=east]", + "30:0": "cobweb", + "31:0": "dead_bush", + "31:1": "grass", + "31:2": "fern", + "32:0": "dead_bush", + "33:0": "piston[extended=false,facing=down]", + "33:1": "piston[extended=false,facing=up]", + "33:2": "piston[extended=false,facing=north]", + "33:3": "piston[extended=false,facing=south]", + "33:4": "piston[extended=false,facing=west]", + "33:5": "piston[extended=false,facing=east]", + "33:8": "piston[extended=true,facing=down]", + "33:9": "piston[extended=true,facing=up]", + "33:10": "piston[extended=true,facing=north]", + "33:11": "piston[extended=true,facing=south]", + "33:12": "piston[extended=true,facing=west]", + "33:13": "piston[extended=true,facing=east]", + "34:0": "piston_head[facing=down,short=false,type=normal]", + "34:1": "piston_head[facing=up,short=false,type=normal]", + "34:2": "piston_head[facing=north,short=false,type=normal]", + "34:3": "piston_head[facing=south,short=false,type=normal]", + "34:4": "piston_head[facing=west,short=false,type=normal]", + "34:5": "piston_head[facing=east,short=false,type=normal]", + "34:8": "piston_head[facing=down,short=false,type=sticky]", + "34:9": "piston_head[facing=up,short=false,type=sticky]", + "34:10": "piston_head[facing=north,short=false,type=sticky]", + "34:11": "piston_head[facing=south,short=false,type=sticky]", + "34:12": "piston_head[facing=west,short=false,type=sticky]", + "34:13": "piston_head[facing=east,short=false,type=sticky]", + "35:0": "white_wool", + "35:1": "orange_wool", + "35:2": "magenta_wool", + "35:3": "light_blue_wool", + "35:4": "yellow_wool", + "35:5": "lime_wool", + "35:6": "pink_wool", + "35:7": "gray_wool", + "35:8": "light_gray_wool", + "35:9": "cyan_wool", + "35:10": "purple_wool", + "35:11": "blue_wool", + "35:12": "brown_wool", + "35:13": "green_wool", + "35:14": "red_wool", + "35:15": "black_wool", + "36:0": "moving_piston[facing=down,type=normal]", + "36:1": "moving_piston[facing=up,type=normal]", + "36:2": "moving_piston[facing=north,type=normal]", + "36:3": "moving_piston[facing=south,type=normal]", + "36:4": "moving_piston[facing=west,type=normal]", + "36:5": "moving_piston[facing=east,type=normal]", + "36:8": "moving_piston[facing=down,type=sticky]", + "36:9": "moving_piston[facing=up,type=sticky]", + "36:10": "moving_piston[facing=north,type=sticky]", + "36:11": "moving_piston[facing=south,type=sticky]", + "36:12": "moving_piston[facing=west,type=sticky]", + "36:13": "moving_piston[facing=east,type=sticky]", + "37:0": "dandelion", + "38:0": "poppy", + "38:1": "blue_orchid", + "38:2": "allium", + "38:3": "azure_bluet", + "38:4": "red_tulip", + "38:5": "orange_tulip", + "38:6": "white_tulip", + "38:7": "pink_tulip", + "38:8": "oxeye_daisy", + "39:0": "brown_mushroom", + "40:0": "red_mushroom", + "41:0": "gold_block", + "42:0": "iron_block", + "43:0": "stone_slab[type=double]", + "43:1": "sandstone_slab[type=double]", + "43:2": "petrified_oak_slab[type=double]", + "43:3": "cobblestone_slab[type=double]", + "43:4": "brick_slab[type=double]", + "43:5": "stone_brick_slab[type=double]", + "43:6": "nether_brick_slab[type=double]", + "43:7": "quartz_slab[type=double]", + "43:8": "smooth_stone", + "43:9": "smooth_sandstone", + "43:10": "petrified_oak_slab[type=double]", + "43:11": "cobblestone_slab[type=double]", + "43:12": "brick_slab[type=double]", + "43:13": "stone_brick_slab[type=double]", + "43:14": "nether_brick_slab[type=double]", + "43:15": "smooth_quartz", + "44:0": "stone_slab[type=bottom]", + "44:1": "sandstone_slab[type=bottom]", + "44:2": "petrified_oak_slab[type=bottom]", + "44:3": "cobblestone_slab[type=bottom]", + "44:4": "brick_slab[type=bottom]", + "44:5": "stone_brick_slab[type=bottom]", + "44:6": "nether_brick_slab[type=bottom]", + "44:7": "quartz_slab[type=bottom]", + "44:8": "stone_slab[type=top]", + "44:9": "sandstone_slab[type=top]", + "44:10": "petrified_oak_slab[type=top]", + "44:11": "cobblestone_slab[type=top]", + "44:12": "brick_slab[type=top]", + "44:13": "stone_brick_slab[type=top]", + "44:14": "nether_brick_slab[type=top]", + "44:15": "quartz_slab[type=top]", + "45:0": "bricks", + "46:0": "tnt[unstable=false]", + "46:1": "tnt[unstable=true]", + "47:0": "bookshelf", + "48:0": "mossy_cobblestone", + "49:0": "obsidian", + "50:1": "wall_torch[facing=east]", + "50:2": "wall_torch[facing=west]", + "50:3": "wall_torch[facing=south]", + "50:4": "wall_torch[facing=north]", + "50:5": "torch", + "51:0": "fire[age=0,east=false,north=false,south=false,up=false,west=false]", + "51:1": "fire[age=1,east=false,north=false,south=false,up=false,west=false]", + "51:2": "fire[age=2,east=false,north=false,south=false,up=false,west=false]", + "51:3": "fire[age=3,east=false,north=false,south=false,up=false,west=false]", + "51:4": "fire[age=4,east=false,north=false,south=false,up=false,west=false]", + "51:5": "fire[age=5,east=false,north=false,south=false,up=false,west=false]", + "51:6": "fire[age=6,east=false,north=false,south=false,up=false,west=false]", + "51:7": "fire[age=7,east=false,north=false,south=false,up=false,west=false]", + "51:8": "fire[age=8,east=false,north=false,south=false,up=false,west=false]", + "51:9": "fire[age=9,east=false,north=false,south=false,up=false,west=false]", + "51:10": "fire[age=10,east=false,north=false,south=false,up=false,west=false]", + "51:11": "fire[age=11,east=false,north=false,south=false,up=false,west=false]", + "51:12": "fire[age=12,east=false,north=false,south=false,up=false,west=false]", + "51:13": "fire[age=13,east=false,north=false,south=false,up=false,west=false]", + "51:14": "fire[age=14,east=false,north=false,south=false,up=false,west=false]", + "51:15": "fire[age=15,east=false,north=false,south=false,up=false,west=false]", + "52:0": "mob_spawner", + "53:0": "oak_stairs[facing=east,half=bottom,shape=straight]", + "53:1": "oak_stairs[facing=west,half=bottom,shape=straight]", + "53:2": "oak_stairs[facing=south,half=bottom,shape=straight]", + "53:3": "oak_stairs[facing=north,half=bottom,shape=straight]", + "53:4": "oak_stairs[facing=east,half=top,shape=straight]", + "53:5": "oak_stairs[facing=west,half=top,shape=straight]", + "53:6": "oak_stairs[facing=south,half=top,shape=straight]", + "53:7": "oak_stairs[facing=north,half=top,shape=straight]", + "54:2": "chest[facing=north,type=single]", + "54:3": "chest[facing=south,type=single]", + "54:4": "chest[facing=west,type=single]", + "54:5": "chest[facing=east,type=single]", + "55:0": "redstone_wire[east=none,north=none,power=0,south=none,west=none]", + "55:1": "redstone_wire[east=none,north=none,power=1,south=none,west=none]", + "55:2": "redstone_wire[east=none,north=none,power=2,south=none,west=none]", + "55:3": "redstone_wire[east=none,north=none,power=3,south=none,west=none]", + "55:4": "redstone_wire[east=none,north=none,power=4,south=none,west=none]", + "55:5": "redstone_wire[east=none,north=none,power=5,south=none,west=none]", + "55:6": "redstone_wire[east=none,north=none,power=6,south=none,west=none]", + "55:7": "redstone_wire[east=none,north=none,power=7,south=none,west=none]", + "55:8": "redstone_wire[east=none,north=none,power=8,south=none,west=none]", + "55:9": "redstone_wire[east=none,north=none,power=9,south=none,west=none]", + "55:10": "redstone_wire[east=none,north=none,power=10,south=none,west=none]", + "55:11": "redstone_wire[east=none,north=none,power=11,south=none,west=none]", + "55:12": "redstone_wire[east=none,north=none,power=12,south=none,west=none]", + "55:13": "redstone_wire[east=none,north=none,power=13,south=none,west=none]", + "55:14": "redstone_wire[east=none,north=none,power=14,south=none,west=none]", + "55:15": "redstone_wire[east=none,north=none,power=15,south=none,west=none]", + "56:0": "diamond_ore", + "57:0": "diamond_block", + "58:0": "crafting_table", + "59:0": "wheat[age=0]", + "59:1": "wheat[age=1]", + "59:2": "wheat[age=2]", + "59:3": "wheat[age=3]", + "59:4": "wheat[age=4]", + "59:5": "wheat[age=5]", + "59:6": "wheat[age=6]", + "59:7": "wheat[age=7]", + "60:0": "farmland[moisture=0]", + "60:1": "farmland[moisture=1]", + "60:2": "farmland[moisture=2]", + "60:3": "farmland[moisture=3]", + "60:4": "farmland[moisture=4]", + "60:5": "farmland[moisture=5]", + "60:6": "farmland[moisture=6]", + "60:7": "farmland[moisture=7]", + "61:2": "furnace[facing=north,lit=false]", + "61:3": "furnace[facing=south,lit=false]", + "61:4": "furnace[facing=west,lit=false]", + "61:5": "furnace[facing=east,lit=false]", + "62:2": "furnace[facing=north,lit=true]", + "62:3": "furnace[facing=south,lit=true]", + "62:4": "furnace[facing=west,lit=true]", + "62:5": "furnace[facing=east,lit=true]", + "63:0": "sign[rotation=0]", + "63:1": "sign[rotation=1]", + "63:2": "sign[rotation=2]", + "63:3": "sign[rotation=3]", + "63:4": "sign[rotation=4]", + "63:5": "sign[rotation=5]", + "63:6": "sign[rotation=6]", + "63:7": "sign[rotation=7]", + "63:8": "sign[rotation=8]", + "63:9": "sign[rotation=9]", + "63:10": "sign[rotation=10]", + "63:11": "sign[rotation=11]", + "63:12": "sign[rotation=12]", + "63:13": "sign[rotation=13]", + "63:14": "sign[rotation=14]", + "63:15": "sign[rotation=15]", + "64:0": "oak_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "64:1": "oak_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "64:2": "oak_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "64:3": "oak_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "64:4": "oak_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "64:5": "oak_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "64:6": "oak_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "64:7": "oak_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "64:8": "oak_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "64:9": "oak_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "64:10": "oak_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "64:11": "oak_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "65:2": "ladder[facing=north]", + "65:3": "ladder[facing=south]", + "65:4": "ladder[facing=west]", + "65:5": "ladder[facing=east]", + "66:0": "rail[shape=north_south]", + "66:1": "rail[shape=east_west]", + "66:2": "rail[shape=ascending_east]", + "66:3": "rail[shape=ascending_west]", + "66:4": "rail[shape=ascending_north]", + "66:5": "rail[shape=ascending_south]", + "66:6": "rail[shape=south_east]", + "66:7": "rail[shape=south_west]", + "66:8": "rail[shape=north_west]", + "66:9": "rail[shape=north_east]", + "67:0": "cobblestone_stairs[facing=east,half=bottom,shape=straight]", + "67:1": "cobblestone_stairs[facing=west,half=bottom,shape=straight]", + "67:2": "cobblestone_stairs[facing=south,half=bottom,shape=straight]", + "67:3": "cobblestone_stairs[facing=north,half=bottom,shape=straight]", + "67:4": "cobblestone_stairs[facing=east,half=top,shape=straight]", + "67:5": "cobblestone_stairs[facing=west,half=top,shape=straight]", + "67:6": "cobblestone_stairs[facing=south,half=top,shape=straight]", + "67:7": "cobblestone_stairs[facing=north,half=top,shape=straight]", + "68:2": "wall_sign[facing=north]", + "68:3": "wall_sign[facing=south]", + "68:4": "wall_sign[facing=west]", + "68:5": "wall_sign[facing=east]", + "69:0": "lever[face=ceiling,facing=west,powered=false]", + "69:1": "lever[face=wall,facing=east,powered=false]", + "69:2": "lever[face=wall,facing=west,powered=false]", + "69:3": "lever[face=wall,facing=south,powered=false]", + "69:4": "lever[face=wall,facing=north,powered=false]", + "69:5": "lever[face=floor,facing=north,powered=false]", + "69:6": "lever[face=floor,facing=west,powered=false]", + "69:7": "lever[face=ceiling,facing=north,powered=false]", + "69:8": "lever[face=ceiling,facing=west,powered=true]", + "69:9": "lever[face=wall,facing=east,powered=true]", + "69:10": "lever[face=wall,facing=west,powered=true]", + "69:11": "lever[face=wall,facing=south,powered=true]", + "69:12": "lever[face=wall,facing=north,powered=true]", + "69:13": "lever[face=floor,facing=north,powered=true]", + "69:14": "lever[face=floor,facing=west,powered=true]", + "69:15": "lever[face=ceiling,facing=north,powered=true]", + "70:0": "stone_pressure_plate[powered=false]", + "70:1": "stone_pressure_plate[powered=true]", + "71:0": "iron_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "71:1": "iron_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "71:2": "iron_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "71:3": "iron_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "71:4": "iron_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "71:5": "iron_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "71:6": "iron_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "71:7": "iron_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "71:8": "iron_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "71:9": "iron_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "71:10": "iron_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "71:11": "iron_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "72:0": "oak_pressure_plate[powered=false]", + "72:1": "oak_pressure_plate[powered=true]", + "73:0": "redstone_ore[lit=false]", + "74:0": "redstone_ore[lit=true]", + "75:1": "redstone_wall_torch[facing=east,lit=false]", + "75:2": "redstone_wall_torch[facing=west,lit=false]", + "75:3": "redstone_wall_torch[facing=south,lit=false]", + "75:4": "redstone_wall_torch[facing=north,lit=false]", + "75:5": "redstone_torch[lit=false]", + "76:1": "redstone_wall_torch[facing=east,lit=true]", + "76:2": "redstone_wall_torch[facing=west,lit=true]", + "76:3": "redstone_wall_torch[facing=south,lit=true]", + "76:4": "redstone_wall_torch[facing=north,lit=true]", + "76:5": "redstone_torch[lit=true]", + "77:0": "stone_button[face=ceiling,facing=north,powered=false]", + "77:1": "stone_button[face=wall,facing=east,powered=false]", + "77:2": "stone_button[face=wall,facing=west,powered=false]", + "77:3": "stone_button[face=wall,facing=south,powered=false]", + "77:4": "stone_button[face=wall,facing=north,powered=false]", + "77:5": "stone_button[face=floor,facing=north,powered=false]", + "77:8": "stone_button[face=ceiling,facing=north,powered=true]", + "77:9": "stone_button[face=wall,facing=east,powered=true]", + "77:10": "stone_button[face=wall,facing=west,powered=true]", + "77:11": "stone_button[face=wall,facing=south,powered=true]", + "77:12": "stone_button[face=wall,facing=north,powered=true]", + "77:13": "stone_button[face=floor,facing=north,powered=true]", + "78:0": "snow[layers=1]", + "78:1": "snow[layers=2]", + "78:2": "snow[layers=3]", + "78:3": "snow[layers=4]", + "78:4": "snow[layers=5]", + "78:5": "snow[layers=6]", + "78:6": "snow[layers=7]", + "78:7": "snow[layers=8]", + "79:0": "ice", + "80:0": "snow_block", + "81:0": "cactus[age=0]", + "81:1": "cactus[age=1]", + "81:2": "cactus[age=2]", + "81:3": "cactus[age=3]", + "81:4": "cactus[age=4]", + "81:5": "cactus[age=5]", + "81:6": "cactus[age=6]", + "81:7": "cactus[age=7]", + "81:8": "cactus[age=8]", + "81:9": "cactus[age=9]", + "81:10": "cactus[age=10]", + "81:11": "cactus[age=11]", + "81:12": "cactus[age=12]", + "81:13": "cactus[age=13]", + "81:14": "cactus[age=14]", + "81:15": "cactus[age=15]", + "82:0": "clay", + "83:0": "sugar_cane[age=0]", + "83:1": "sugar_cane[age=1]", + "83:2": "sugar_cane[age=2]", + "83:3": "sugar_cane[age=3]", + "83:4": "sugar_cane[age=4]", + "83:5": "sugar_cane[age=5]", + "83:6": "sugar_cane[age=6]", + "83:7": "sugar_cane[age=7]", + "83:8": "sugar_cane[age=8]", + "83:9": "sugar_cane[age=9]", + "83:10": "sugar_cane[age=10]", + "83:11": "sugar_cane[age=11]", + "83:12": "sugar_cane[age=12]", + "83:13": "sugar_cane[age=13]", + "83:14": "sugar_cane[age=14]", + "83:15": "sugar_cane[age=15]", + "84:0": "jukebox[has_record=false]", + "84:1": "jukebox[has_record=true]", + "85:0": "oak_fence[east=false,north=false,south=false,west=false]", + "86:0": "carved_pumpkin[facing=south]", + "86:1": "carved_pumpkin[facing=west]", + "86:2": "carved_pumpkin[facing=north]", + "86:3": "carved_pumpkin[facing=east]", + "87:0": "netherrack", + "88:0": "soul_sand", + "89:0": "glowstone", + "90:1": "portal[axis=x]", + "90:2": "portal[axis=z]", + "91:0": "jack_o_lantern[facing=south]", + "91:1": "jack_o_lantern[facing=west]", + "91:2": "jack_o_lantern[facing=north]", + "91:3": "jack_o_lantern[facing=east]", + "92:0": "cake[bites=0]", + "92:1": "cake[bites=1]", + "92:2": "cake[bites=2]", + "92:3": "cake[bites=3]", + "92:4": "cake[bites=4]", + "92:5": "cake[bites=5]", + "92:6": "cake[bites=6]", + "93:0": "repeater[delay=1,facing=south,locked=false,powered=false]", + "93:1": "repeater[delay=1,facing=west,locked=false,powered=false]", + "93:2": "repeater[delay=1,facing=north,locked=false,powered=false]", + "93:3": "repeater[delay=1,facing=east,locked=false,powered=false]", + "93:4": "repeater[delay=2,facing=south,locked=false,powered=false]", + "93:5": "repeater[delay=2,facing=west,locked=false,powered=false]", + "93:6": "repeater[delay=2,facing=north,locked=false,powered=false]", + "93:7": "repeater[delay=2,facing=east,locked=false,powered=false]", + "93:8": "repeater[delay=3,facing=south,locked=false,powered=false]", + "93:9": "repeater[delay=3,facing=west,locked=false,powered=false]", + "93:10": "repeater[delay=3,facing=north,locked=false,powered=false]", + "93:11": "repeater[delay=3,facing=east,locked=false,powered=false]", + "93:12": "repeater[delay=4,facing=south,locked=false,powered=false]", + "93:13": "repeater[delay=4,facing=west,locked=false,powered=false]", + "93:14": "repeater[delay=4,facing=north,locked=false,powered=false]", + "93:15": "repeater[delay=4,facing=east,locked=false,powered=false]", + "94:0": "repeater[delay=1,facing=south,locked=false,powered=true]", + "94:1": "repeater[delay=1,facing=west,locked=false,powered=true]", + "94:2": "repeater[delay=1,facing=north,locked=false,powered=true]", + "94:3": "repeater[delay=1,facing=east,locked=false,powered=true]", + "94:4": "repeater[delay=2,facing=south,locked=false,powered=true]", + "94:5": "repeater[delay=2,facing=west,locked=false,powered=true]", + "94:6": "repeater[delay=2,facing=north,locked=false,powered=true]", + "94:7": "repeater[delay=2,facing=east,locked=false,powered=true]", + "94:8": "repeater[delay=3,facing=south,locked=false,powered=true]", + "94:9": "repeater[delay=3,facing=west,locked=false,powered=true]", + "94:10": "repeater[delay=3,facing=north,locked=false,powered=true]", + "94:11": "repeater[delay=3,facing=east,locked=false,powered=true]", + "94:12": "repeater[delay=4,facing=south,locked=false,powered=true]", + "94:13": "repeater[delay=4,facing=west,locked=false,powered=true]", + "94:14": "repeater[delay=4,facing=north,locked=false,powered=true]", + "94:15": "repeater[delay=4,facing=east,locked=false,powered=true]", + "95:0": "white_stained_glass", + "95:1": "orange_stained_glass", + "95:2": "magenta_stained_glass", + "95:3": "light_blue_stained_glass", + "95:4": "yellow_stained_glass", + "95:5": "lime_stained_glass", + "95:6": "pink_stained_glass", + "95:7": "gray_stained_glass", + "95:8": "light_gray_stained_glass", + "95:9": "cyan_stained_glass", + "95:10": "purple_stained_glass", + "95:11": "blue_stained_glass", + "95:12": "brown_stained_glass", + "95:13": "green_stained_glass", + "95:14": "red_stained_glass", + "95:15": "black_stained_glass", + "96:0": "oak_trapdoor[facing=north,half=bottom,open=false]", + "96:1": "oak_trapdoor[facing=south,half=bottom,open=false]", + "96:2": "oak_trapdoor[facing=west,half=bottom,open=false]", + "96:3": "oak_trapdoor[facing=east,half=bottom,open=false]", + "96:4": "oak_trapdoor[facing=north,half=bottom,open=true]", + "96:5": "oak_trapdoor[facing=south,half=bottom,open=true]", + "96:6": "oak_trapdoor[facing=west,half=bottom,open=true]", + "96:7": "oak_trapdoor[facing=east,half=bottom,open=true]", + "96:8": "oak_trapdoor[facing=north,half=top,open=false]", + "96:9": "oak_trapdoor[facing=south,half=top,open=false]", + "96:10": "oak_trapdoor[facing=west,half=top,open=false]", + "96:11": "oak_trapdoor[facing=east,half=top,open=false]", + "96:12": "oak_trapdoor[facing=north,half=top,open=true]", + "96:13": "oak_trapdoor[facing=south,half=top,open=true]", + "96:14": "oak_trapdoor[facing=west,half=top,open=true]", + "96:15": "oak_trapdoor[facing=east,half=top,open=true]", + "97:0": "infested_stone", + "97:1": "infested_cobblestone", + "97:2": "infested_stone_bricks", + "97:3": "infested_mossy_stone_bricks", + "97:4": "infested_cracked_stone_bricks", + "97:5": "infested_chiseled_stone_bricks", + "98:0": "stone_bricks", + "98:1": "mossy_stone_bricks", + "98:2": "cracked_stone_bricks", + "98:3": "chiseled_stone_bricks", + "99:0": "brown_mushroom_block[north=false,east=false,south=false,west=false,up=false,down=false]", + "99:1": "brown_mushroom_block[north=true,east=false,south=false,west=true,up=true,down=false]", + "99:2": "brown_mushroom_block[north=true,east=false,south=false,west=false,up=true,down=false]", + "99:3": "brown_mushroom_block[north=true,east=true,south=false,west=false,up=true,down=false]", + "99:4": "brown_mushroom_block[north=false,east=false,south=false,west=true,up=true,down=false]", + "99:5": "brown_mushroom_block[north=false,east=false,south=false,west=false,up=true,down=false]", + "99:6": "brown_mushroom_block[north=false,east=true,south=false,west=false,up=true,down=false]", + "99:7": "brown_mushroom_block[north=false,east=false,south=true,west=true,up=true,down=false]", + "99:8": "brown_mushroom_block[north=false,east=false,south=true,west=false,up=true,down=false]", + "99:9": "brown_mushroom_block[north=false,east=true,south=true,west=false,up=true,down=false]", + "99:10": "mushroom_stem[north=true,east=true,south=true,west=true,up=false,down=false]", + "99:14": "brown_mushroom_block[north=true,east=true,south=true,west=true,up=true,down=true]", + "99:15": "mushroom_stem[north=true,east=true,south=true,west=true,up=true,down=true]", + "100:0": "red_mushroom_block[north=false,east=false,south=false,west=false,up=false,down=false]", + "100:1": "red_mushroom_block[north=true,east=false,south=false,west=true,up=true,down=false]", + "100:2": "red_mushroom_block[north=true,east=false,south=false,west=false,up=true,down=false]", + "100:3": "red_mushroom_block[north=true,east=true,south=false,west=false,up=true,down=false]", + "100:4": "red_mushroom_block[north=false,east=false,south=false,west=true,up=true,down=false]", + "100:5": "red_mushroom_block[north=false,east=false,south=false,west=false,up=true,down=false]", + "100:6": "red_mushroom_block[north=false,east=true,south=false,west=false,up=true,down=false]", + "100:7": "red_mushroom_block[north=false,east=false,south=true,west=true,up=true,down=false]", + "100:8": "red_mushroom_block[north=false,east=false,south=true,west=false,up=true,down=false]", + "100:9": "red_mushroom_block[north=false,east=true,south=true,west=false,up=true,down=false]", + "100:10": "mushroom_stem[north=true,east=true,south=true,west=true,up=false,down=false]", + "100:14": "red_mushroom_block[north=true,east=true,south=true,west=true,up=true,down=true]", + "100:15": "mushroom_stem[north=true,east=true,south=true,west=true,up=true,down=true]", + "101:0": "iron_bars[east=false,north=false,south=false,west=false]", + "102:0": "glass_pane[east=false,north=false,south=false,west=false]", + "103:0": "melon_block", + "104:0": "pumpkin_stem[age=0]", + "104:1": "pumpkin_stem[age=1]", + "104:2": "pumpkin_stem[age=2]", + "104:3": "pumpkin_stem[age=3]", + "104:4": "pumpkin_stem[age=4]", + "104:5": "pumpkin_stem[age=5]", + "104:6": "pumpkin_stem[age=6]", + "104:7": "pumpkin_stem[age=7]", + "105:0": "melon_stem[age=0]", + "105:1": "melon_stem[age=1]", + "105:2": "melon_stem[age=2]", + "105:3": "melon_stem[age=3]", + "105:4": "melon_stem[age=4]", + "105:5": "melon_stem[age=5]", + "105:6": "melon_stem[age=6]", + "105:7": "melon_stem[age=7]", + "106:0": "vine[east=false,north=false,south=false,up=true,west=false]", + "106:1": "vine[east=false,north=false,south=true,up=true,west=false]", + "106:2": "vine[east=false,north=false,south=false,up=true,west=true]", + "106:3": "vine[east=false,north=false,south=true,up=true,west=true]", + "106:4": "vine[east=false,north=true,south=false,up=true,west=false]", + "106:5": "vine[east=false,north=true,south=true,up=true,west=false]", + "106:6": "vine[east=false,north=true,south=false,up=true,west=true]", + "106:7": "vine[east=false,north=true,south=true,up=true,west=true]", + "106:8": "vine[east=true,north=false,south=false,up=true,west=false]", + "106:9": "vine[east=true,north=false,south=true,up=true,west=false]", + "106:10": "vine[east=true,north=false,south=false,up=true,west=true]", + "106:11": "vine[east=true,north=false,south=true,up=true,west=true]", + "106:12": "vine[east=true,north=true,south=false,up=true,west=false]", + "106:13": "vine[east=true,north=true,south=true,up=true,west=false]", + "106:14": "vine[east=true,north=true,south=false,up=true,west=true]", + "106:15": "vine[east=true,north=true,south=true,up=true,west=true]", + "107:0": "oak_fence_gate[facing=south,in_wall=false,open=false,powered=false]", + "107:1": "oak_fence_gate[facing=west,in_wall=false,open=false,powered=false]", + "107:2": "oak_fence_gate[facing=north,in_wall=false,open=false,powered=false]", + "107:3": "oak_fence_gate[facing=east,in_wall=false,open=false,powered=false]", + "107:4": "oak_fence_gate[facing=south,in_wall=false,open=true,powered=false]", + "107:5": "oak_fence_gate[facing=west,in_wall=false,open=true,powered=false]", + "107:6": "oak_fence_gate[facing=north,in_wall=false,open=true,powered=false]", + "107:7": "oak_fence_gate[facing=east,in_wall=false,open=true,powered=false]", + "107:8": "oak_fence_gate[facing=south,in_wall=false,open=false,powered=true]", + "107:9": "oak_fence_gate[facing=west,in_wall=false,open=false,powered=true]", + "107:10": "oak_fence_gate[facing=north,in_wall=false,open=false,powered=true]", + "107:11": "oak_fence_gate[facing=east,in_wall=false,open=false,powered=true]", + "107:12": "oak_fence_gate[facing=south,in_wall=false,open=true,powered=true]", + "107:13": "oak_fence_gate[facing=west,in_wall=false,open=true,powered=true]", + "107:14": "oak_fence_gate[facing=north,in_wall=false,open=true,powered=true]", + "107:15": "oak_fence_gate[facing=east,in_wall=false,open=true,powered=true]", + "108:0": "brick_stairs[facing=east,half=bottom,shape=straight]", + "108:1": "brick_stairs[facing=west,half=bottom,shape=straight]", + "108:2": "brick_stairs[facing=south,half=bottom,shape=straight]", + "108:3": "brick_stairs[facing=north,half=bottom,shape=straight]", + "108:4": "brick_stairs[facing=east,half=top,shape=straight]", + "108:5": "brick_stairs[facing=west,half=top,shape=straight]", + "108:6": "brick_stairs[facing=south,half=top,shape=straight]", + "108:7": "brick_stairs[facing=north,half=top,shape=straight]", + "109:0": "stone_brick_stairs[facing=east,half=bottom,shape=straight]", + "109:1": "stone_brick_stairs[facing=west,half=bottom,shape=straight]", + "109:2": "stone_brick_stairs[facing=south,half=bottom,shape=straight]", + "109:3": "stone_brick_stairs[facing=north,half=bottom,shape=straight]", + "109:4": "stone_brick_stairs[facing=east,half=top,shape=straight]", + "109:5": "stone_brick_stairs[facing=west,half=top,shape=straight]", + "109:6": "stone_brick_stairs[facing=south,half=top,shape=straight]", + "109:7": "stone_brick_stairs[facing=north,half=top,shape=straight]", + "110:0": "mycelium[snowy=false]", + "111:0": "lily_pad", + "112:0": "nether_bricks", + "113:0": "nether_brick_fence[east=false,north=false,south=false,west=false]", + "114:0": "nether_brick_stairs[facing=east,half=bottom,shape=straight]", + "114:1": "nether_brick_stairs[facing=west,half=bottom,shape=straight]", + "114:2": "nether_brick_stairs[facing=south,half=bottom,shape=straight]", + "114:3": "nether_brick_stairs[facing=north,half=bottom,shape=straight]", + "114:4": "nether_brick_stairs[facing=east,half=top,shape=straight]", + "114:5": "nether_brick_stairs[facing=west,half=top,shape=straight]", + "114:6": "nether_brick_stairs[facing=south,half=top,shape=straight]", + "114:7": "nether_brick_stairs[facing=north,half=top,shape=straight]", + "115:0": "nether_wart[age=0]", + "115:1": "nether_wart[age=1]", + "115:2": "nether_wart[age=2]", + "115:3": "nether_wart[age=3]", + "116:0": "enchanting_table", + "117:0": "brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=false]", + "117:1": "brewing_stand[has_bottle_0=true,has_bottle_1=false,has_bottle_2=false]", + "117:2": "brewing_stand[has_bottle_0=false,has_bottle_1=true,has_bottle_2=false]", + "117:3": "brewing_stand[has_bottle_0=true,has_bottle_1=true,has_bottle_2=false]", + "117:4": "brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=true]", + "117:5": "brewing_stand[has_bottle_0=true,has_bottle_1=false,has_bottle_2=true]", + "117:6": "brewing_stand[has_bottle_0=false,has_bottle_1=true,has_bottle_2=true]", + "117:7": "brewing_stand[has_bottle_0=true,has_bottle_1=true,has_bottle_2=true]", + "118:0": "cauldron[level=0]", + "118:1": "cauldron[level=1]", + "118:2": "cauldron[level=2]", + "118:3": "cauldron[level=3]", + "119:0": "end_portal", + "120:0": "end_portal_frame[eye=false,facing=south]", + "120:1": "end_portal_frame[eye=false,facing=west]", + "120:2": "end_portal_frame[eye=false,facing=north]", + "120:3": "end_portal_frame[eye=false,facing=east]", + "120:4": "end_portal_frame[eye=true,facing=south]", + "120:5": "end_portal_frame[eye=true,facing=west]", + "120:6": "end_portal_frame[eye=true,facing=north]", + "120:7": "end_portal_frame[eye=true,facing=east]", + "121:0": "end_stone", + "122:0": "dragon_egg", + "123:0": "redstone_lamp[lit=false]", + "124:0": "redstone_lamp[lit=true]", + "125:0": "oak_slab[type=double]", + "125:1": "spruce_slab[type=double]", + "125:2": "birch_slab[type=double]", + "125:3": "jungle_slab[type=double]", + "125:4": "acacia_slab[type=double]", + "125:5": "dark_oak_slab[type=double]", + "126:0": "oak_slab[type=bottom]", + "126:1": "spruce_slab[type=bottom]", + "126:2": "birch_slab[type=bottom]", + "126:3": "jungle_slab[type=bottom]", + "126:4": "acacia_slab[type=bottom]", + "126:5": "dark_oak_slab[type=bottom]", + "126:8": "oak_slab[type=top]", + "126:9": "spruce_slab[type=top]", + "126:10": "birch_slab[type=top]", + "126:11": "jungle_slab[type=top]", + "126:12": "acacia_slab[type=top]", + "126:13": "dark_oak_slab[type=top]", + "127:0": "cocoa[age=0,facing=south]", + "127:1": "cocoa[age=0,facing=west]", + "127:2": "cocoa[age=0,facing=north]", + "127:3": "cocoa[age=0,facing=east]", + "127:4": "cocoa[age=1,facing=south]", + "127:5": "cocoa[age=1,facing=west]", + "127:6": "cocoa[age=1,facing=north]", + "127:7": "cocoa[age=1,facing=east]", + "127:8": "cocoa[age=2,facing=south]", + "127:9": "cocoa[age=2,facing=west]", + "127:10": "cocoa[age=2,facing=north]", + "127:11": "cocoa[age=2,facing=east]", + "128:0": "sandstone_stairs[facing=east,half=bottom,shape=straight]", + "128:1": "sandstone_stairs[facing=west,half=bottom,shape=straight]", + "128:2": "sandstone_stairs[facing=south,half=bottom,shape=straight]", + "128:3": "sandstone_stairs[facing=north,half=bottom,shape=straight]", + "128:4": "sandstone_stairs[facing=east,half=top,shape=straight]", + "128:5": "sandstone_stairs[facing=west,half=top,shape=straight]", + "128:6": "sandstone_stairs[facing=south,half=top,shape=straight]", + "128:7": "sandstone_stairs[facing=north,half=top,shape=straight]", + "129:0": "emerald_ore", + "130:2": "ender_chest[facing=north]", + "130:3": "ender_chest[facing=south]", + "130:4": "ender_chest[facing=west]", + "130:5": "ender_chest[facing=east]", + "131:0": "tripwire_hook[attached=false,facing=south,powered=false]", + "131:1": "tripwire_hook[attached=false,facing=west,powered=false]", + "131:2": "tripwire_hook[attached=false,facing=north,powered=false]", + "131:3": "tripwire_hook[attached=false,facing=east,powered=false]", + "131:4": "tripwire_hook[attached=true,facing=south,powered=false]", + "131:5": "tripwire_hook[attached=true,facing=west,powered=false]", + "131:6": "tripwire_hook[attached=true,facing=north,powered=false]", + "131:7": "tripwire_hook[attached=true,facing=east,powered=false]", + "131:8": "tripwire_hook[attached=false,facing=south,powered=true]", + "131:9": "tripwire_hook[attached=false,facing=west,powered=true]", + "131:10": "tripwire_hook[attached=false,facing=north,powered=true]", + "131:11": "tripwire_hook[attached=false,facing=east,powered=true]", + "131:12": "tripwire_hook[attached=true,facing=south,powered=true]", + "131:13": "tripwire_hook[attached=true,facing=west,powered=true]", + "131:14": "tripwire_hook[attached=true,facing=north,powered=true]", + "131:15": "tripwire_hook[attached=true,facing=east,powered=true]", + "132:0": "tripwire[attached=false,disarmed=false,east=false,north=false,powered=false,south=false,west=false]", + "132:1": "tripwire[attached=false,disarmed=false,east=false,north=false,powered=true,south=false,west=false]", + "132:4": "tripwire[attached=true,disarmed=false,east=false,north=false,powered=false,south=false,west=false]", + "132:5": "tripwire[attached=true,disarmed=false,east=false,north=false,powered=true,south=false,west=false]", + "132:8": "tripwire[attached=false,disarmed=true,east=false,north=false,powered=false,south=false,west=false]", + "132:9": "tripwire[attached=false,disarmed=true,east=false,north=false,powered=true,south=false,west=false]", + "132:12": "tripwire[attached=true,disarmed=true,east=false,north=false,powered=false,south=false,west=false]", + "132:13": "tripwire[attached=true,disarmed=true,east=false,north=false,powered=true,south=false,west=false]", + "133:0": "emerald_block", + "134:0": "spruce_stairs[facing=east,half=bottom,shape=straight]", + "134:1": "spruce_stairs[facing=west,half=bottom,shape=straight]", + "134:2": "spruce_stairs[facing=south,half=bottom,shape=straight]", + "134:3": "spruce_stairs[facing=north,half=bottom,shape=straight]", + "134:4": "spruce_stairs[facing=east,half=top,shape=straight]", + "134:5": "spruce_stairs[facing=west,half=top,shape=straight]", + "134:6": "spruce_stairs[facing=south,half=top,shape=straight]", + "134:7": "spruce_stairs[facing=north,half=top,shape=straight]", + "135:0": "birch_stairs[facing=east,half=bottom,shape=straight]", + "135:1": "birch_stairs[facing=west,half=bottom,shape=straight]", + "135:2": "birch_stairs[facing=south,half=bottom,shape=straight]", + "135:3": "birch_stairs[facing=north,half=bottom,shape=straight]", + "135:4": "birch_stairs[facing=east,half=top,shape=straight]", + "135:5": "birch_stairs[facing=west,half=top,shape=straight]", + "135:6": "birch_stairs[facing=south,half=top,shape=straight]", + "135:7": "birch_stairs[facing=north,half=top,shape=straight]", + "136:0": "jungle_stairs[facing=east,half=bottom,shape=straight]", + "136:1": "jungle_stairs[facing=west,half=bottom,shape=straight]", + "136:2": "jungle_stairs[facing=south,half=bottom,shape=straight]", + "136:3": "jungle_stairs[facing=north,half=bottom,shape=straight]", + "136:4": "jungle_stairs[facing=east,half=top,shape=straight]", + "136:5": "jungle_stairs[facing=west,half=top,shape=straight]", + "136:6": "jungle_stairs[facing=south,half=top,shape=straight]", + "136:7": "jungle_stairs[facing=north,half=top,shape=straight]", + "137:0": "command_block[conditional=false,facing=down]", + "137:1": "command_block[conditional=false,facing=up]", + "137:2": "command_block[conditional=false,facing=north]", + "137:3": "command_block[conditional=false,facing=south]", + "137:4": "command_block[conditional=false,facing=west]", + "137:5": "command_block[conditional=false,facing=east]", + "137:8": "command_block[conditional=true,facing=down]", + "137:9": "command_block[conditional=true,facing=up]", + "137:10": "command_block[conditional=true,facing=north]", + "137:11": "command_block[conditional=true,facing=south]", + "137:12": "command_block[conditional=true,facing=west]", + "137:13": "command_block[conditional=true,facing=east]", + "138:0": "beacon", + "139:0": "cobblestone_wall[east=false,north=false,south=false,up=false,west=false]", + "139:1": "mossy_cobblestone_wall[east=false,north=false,south=false,up=false,west=false]", + "140:0": "potted_cactus", + "140:1": "potted_cactus", + "140:2": "potted_cactus", + "140:3": "potted_cactus", + "140:4": "potted_cactus", + "140:5": "potted_cactus", + "140:6": "potted_cactus", + "140:7": "potted_cactus", + "140:8": "potted_cactus", + "140:9": "potted_cactus", + "140:10": "potted_cactus", + "140:11": "potted_cactus", + "140:12": "potted_cactus", + "140:13": "potted_cactus", + "140:14": "potted_cactus", + "140:15": "potted_cactus", + "141:0": "carrots[age=0]", + "141:1": "carrots[age=1]", + "141:2": "carrots[age=2]", + "141:3": "carrots[age=3]", + "141:4": "carrots[age=4]", + "141:5": "carrots[age=5]", + "141:6": "carrots[age=6]", + "141:7": "carrots[age=7]", + "142:0": "potatoes[age=0]", + "142:1": "potatoes[age=1]", + "142:2": "potatoes[age=2]", + "142:3": "potatoes[age=3]", + "142:4": "potatoes[age=4]", + "142:5": "potatoes[age=5]", + "142:6": "potatoes[age=6]", + "142:7": "potatoes[age=7]", + "143:0": "oak_button[face=ceiling,facing=north,powered=false]", + "143:1": "oak_button[face=wall,facing=east,powered=false]", + "143:2": "oak_button[face=wall,facing=west,powered=false]", + "143:3": "oak_button[face=wall,facing=south,powered=false]", + "143:4": "oak_button[face=wall,facing=north,powered=false]", + "143:5": "oak_button[face=floor,facing=north,powered=false]", + "143:8": "oak_button[face=ceiling,facing=north,powered=true]", + "143:9": "oak_button[face=wall,facing=east,powered=true]", + "143:10": "oak_button[face=wall,facing=west,powered=true]", + "143:11": "oak_button[face=wall,facing=south,powered=true]", + "143:12": "oak_button[face=wall,facing=north,powered=true]", + "143:13": "oak_button[face=floor,facing=north,powered=true]", + "144:0": "player_head[facing=down]", + "144:1": "player_head[facing=up]", + "144:2": "player_head[facing=north]", + "144:3": "player_head[facing=south]", + "144:4": "player_head[facing=west]", + "144:5": "player_head[facing=east]", + "144:8": "player_head[facing=down]", + "144:9": "player_head[facing=up]", + "144:10": "player_head[facing=north]", + "144:11": "player_head[facing=south]", + "144:12": "player_head[facing=west]", + "144:13": "player_head[facing=east]", + "145:0": "anvil[facing=south]", + "145:1": "anvil[facing=west]", + "145:2": "anvil[facing=north]", + "145:3": "anvil[facing=east]", + "145:4": "chipped_anvil[facing=south]", + "145:5": "chipped_anvil[facing=west]", + "145:6": "chipped_anvil[facing=north]", + "145:7": "chipped_anvil[facing=east]", + "145:8": "damaged_anvil[facing=south]", + "145:9": "damaged_anvil[facing=west]", + "145:10": "damaged_anvil[facing=north]", + "145:11": "damaged_anvil[facing=east]", + "146:2": "trapped_chest[facing=north,type=single]", + "146:3": "trapped_chest[facing=south,type=single]", + "146:4": "trapped_chest[facing=west,type=single]", + "146:5": "trapped_chest[facing=east,type=single]", + "147:0": "light_weighted_pressure_plate[power=0]", + "147:1": "light_weighted_pressure_plate[power=1]", + "147:2": "light_weighted_pressure_plate[power=2]", + "147:3": "light_weighted_pressure_plate[power=3]", + "147:4": "light_weighted_pressure_plate[power=4]", + "147:5": "light_weighted_pressure_plate[power=5]", + "147:6": "light_weighted_pressure_plate[power=6]", + "147:7": "light_weighted_pressure_plate[power=7]", + "147:8": "light_weighted_pressure_plate[power=8]", + "147:9": "light_weighted_pressure_plate[power=9]", + "147:10": "light_weighted_pressure_plate[power=10]", + "147:11": "light_weighted_pressure_plate[power=11]", + "147:12": "light_weighted_pressure_plate[power=12]", + "147:13": "light_weighted_pressure_plate[power=13]", + "147:14": "light_weighted_pressure_plate[power=14]", + "147:15": "light_weighted_pressure_plate[power=15]", + "148:0": "heavy_weighted_pressure_plate[power=0]", + "148:1": "heavy_weighted_pressure_plate[power=1]", + "148:2": "heavy_weighted_pressure_plate[power=2]", + "148:3": "heavy_weighted_pressure_plate[power=3]", + "148:4": "heavy_weighted_pressure_plate[power=4]", + "148:5": "heavy_weighted_pressure_plate[power=5]", + "148:6": "heavy_weighted_pressure_plate[power=6]", + "148:7": "heavy_weighted_pressure_plate[power=7]", + "148:8": "heavy_weighted_pressure_plate[power=8]", + "148:9": "heavy_weighted_pressure_plate[power=9]", + "148:10": "heavy_weighted_pressure_plate[power=10]", + "148:11": "heavy_weighted_pressure_plate[power=11]", + "148:12": "heavy_weighted_pressure_plate[power=12]", + "148:13": "heavy_weighted_pressure_plate[power=13]", + "148:14": "heavy_weighted_pressure_plate[power=14]", + "148:15": "heavy_weighted_pressure_plate[power=15]", + "149:0": "comparator[facing=south,mode=compare,powered=false]", + "149:1": "comparator[facing=west,mode=compare,powered=false]", + "149:2": "comparator[facing=north,mode=compare,powered=false]", + "149:3": "comparator[facing=east,mode=compare,powered=false]", + "149:4": "comparator[facing=south,mode=subtract,powered=false]", + "149:5": "comparator[facing=west,mode=subtract,powered=false]", + "149:6": "comparator[facing=north,mode=subtract,powered=false]", + "149:7": "comparator[facing=east,mode=subtract,powered=false]", + "149:8": "comparator[facing=south,mode=compare,powered=true]", + "149:9": "comparator[facing=west,mode=compare,powered=true]", + "149:10": "comparator[facing=north,mode=compare,powered=true]", + "149:11": "comparator[facing=east,mode=compare,powered=true]", + "149:12": "comparator[facing=south,mode=subtract,powered=true]", + "149:13": "comparator[facing=west,mode=subtract,powered=true]", + "149:14": "comparator[facing=north,mode=subtract,powered=true]", + "149:15": "comparator[facing=east,mode=subtract,powered=true]", + "150:0": "comparator[facing=south,mode=compare,powered=false]", + "150:1": "comparator[facing=west,mode=compare,powered=false]", + "150:2": "comparator[facing=north,mode=compare,powered=false]", + "150:3": "comparator[facing=east,mode=compare,powered=false]", + "150:4": "comparator[facing=south,mode=subtract,powered=false]", + "150:5": "comparator[facing=west,mode=subtract,powered=false]", + "150:6": "comparator[facing=north,mode=subtract,powered=false]", + "150:7": "comparator[facing=east,mode=subtract,powered=false]", + "150:8": "comparator[facing=south,mode=compare,powered=true]", + "150:9": "comparator[facing=west,mode=compare,powered=true]", + "150:10": "comparator[facing=north,mode=compare,powered=true]", + "150:11": "comparator[facing=east,mode=compare,powered=true]", + "150:12": "comparator[facing=south,mode=subtract,powered=true]", + "150:13": "comparator[facing=west,mode=subtract,powered=true]", + "150:14": "comparator[facing=north,mode=subtract,powered=true]", + "150:15": "comparator[facing=east,mode=subtract,powered=true]", + "151:0": "daylight_detector[inverted=false,power=0]", + "151:1": "daylight_detector[inverted=false,power=1]", + "151:2": "daylight_detector[inverted=false,power=2]", + "151:3": "daylight_detector[inverted=false,power=3]", + "151:4": "daylight_detector[inverted=false,power=4]", + "151:5": "daylight_detector[inverted=false,power=5]", + "151:6": "daylight_detector[inverted=false,power=6]", + "151:7": "daylight_detector[inverted=false,power=7]", + "151:8": "daylight_detector[inverted=false,power=8]", + "151:9": "daylight_detector[inverted=false,power=9]", + "151:10": "daylight_detector[inverted=false,power=10]", + "151:11": "daylight_detector[inverted=false,power=11]", + "151:12": "daylight_detector[inverted=false,power=12]", + "151:13": "daylight_detector[inverted=false,power=13]", + "151:14": "daylight_detector[inverted=false,power=14]", + "151:15": "daylight_detector[inverted=false,power=15]", + "152:0": "redstone_block", + "153:0": "nether_quartz_ore", + "154:0": "hopper[enabled=true,facing=down]", + "154:2": "hopper[enabled=true,facing=north]", + "154:3": "hopper[enabled=true,facing=south]", + "154:4": "hopper[enabled=true,facing=west]", + "154:5": "hopper[enabled=true,facing=east]", + "154:8": "hopper[enabled=false,facing=down]", + "154:10": "hopper[enabled=false,facing=north]", + "154:11": "hopper[enabled=false,facing=south]", + "154:12": "hopper[enabled=false,facing=west]", + "154:13": "hopper[enabled=false,facing=east]", + "155:0": "quartz_block", + "155:1": "chiseled_quartz_block", + "155:2": "quartz_pillar[axis=y]", + "155:3": "quartz_pillar[axis=x]", + "155:4": "quartz_pillar[axis=z]", + "156:0": "quartz_stairs[facing=east,half=bottom,shape=straight]", + "156:1": "quartz_stairs[facing=west,half=bottom,shape=straight]", + "156:2": "quartz_stairs[facing=south,half=bottom,shape=straight]", + "156:3": "quartz_stairs[facing=north,half=bottom,shape=straight]", + "156:4": "quartz_stairs[facing=east,half=top,shape=straight]", + "156:5": "quartz_stairs[facing=west,half=top,shape=straight]", + "156:6": "quartz_stairs[facing=south,half=top,shape=straight]", + "156:7": "quartz_stairs[facing=north,half=top,shape=straight]", + "157:0": "activator_rail[powered=false,shape=north_south]", + "157:1": "activator_rail[powered=false,shape=east_west]", + "157:2": "activator_rail[powered=false,shape=ascending_east]", + "157:3": "activator_rail[powered=false,shape=ascending_west]", + "157:4": "activator_rail[powered=false,shape=ascending_north]", + "157:5": "activator_rail[powered=false,shape=ascending_south]", + "157:8": "activator_rail[powered=true,shape=north_south]", + "157:9": "activator_rail[powered=true,shape=east_west]", + "157:10": "activator_rail[powered=true,shape=ascending_east]", + "157:11": "activator_rail[powered=true,shape=ascending_west]", + "157:12": "activator_rail[powered=true,shape=ascending_north]", + "157:13": "activator_rail[powered=true,shape=ascending_south]", + "158:0": "dropper[facing=down,triggered=false]", + "158:1": "dropper[facing=up,triggered=false]", + "158:2": "dropper[facing=north,triggered=false]", + "158:3": "dropper[facing=south,triggered=false]", + "158:4": "dropper[facing=west,triggered=false]", + "158:5": "dropper[facing=east,triggered=false]", + "158:8": "dropper[facing=down,triggered=true]", + "158:9": "dropper[facing=up,triggered=true]", + "158:10": "dropper[facing=north,triggered=true]", + "158:11": "dropper[facing=south,triggered=true]", + "158:12": "dropper[facing=west,triggered=true]", + "158:13": "dropper[facing=east,triggered=true]", + "159:0": "white_terracotta", + "159:1": "orange_terracotta", + "159:2": "magenta_terracotta", + "159:3": "light_blue_terracotta", + "159:4": "yellow_terracotta", + "159:5": "lime_terracotta", + "159:6": "pink_terracotta", + "159:7": "gray_terracotta", + "159:8": "light_gray_terracotta", + "159:9": "cyan_terracotta", + "159:10": "purple_terracotta", + "159:11": "blue_terracotta", + "159:12": "brown_terracotta", + "159:13": "green_terracotta", + "159:14": "red_terracotta", + "159:15": "black_terracotta", + "160:0": "white_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:1": "orange_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:2": "magenta_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:3": "light_blue_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:4": "yellow_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:5": "lime_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:6": "pink_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:7": "gray_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:8": "light_gray_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:9": "cyan_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:10": "purple_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:11": "blue_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:12": "brown_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:13": "green_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:14": "red_stained_glass_pane[east=false,north=false,south=false,west=false]", + "160:15": "black_stained_glass_pane[east=false,north=false,south=false,west=false]", + "161:0": "acacia_leaves[check_decay=false,decayable=true]", + "161:1": "dark_oak_leaves[check_decay=false,decayable=true]", + "161:4": "acacia_leaves[check_decay=false,decayable=false]", + "161:5": "dark_oak_leaves[check_decay=false,decayable=false]", + "161:8": "acacia_leaves[check_decay=true,decayable=true]", + "161:9": "dark_oak_leaves[check_decay=true,decayable=true]", + "161:12": "acacia_leaves[check_decay=true,decayable=false]", + "161:13": "dark_oak_leaves[check_decay=true,decayable=false]", + "162:0": "acacia_log[axis=y]", + "162:1": "dark_oak_log[axis=y]", + "162:4": "acacia_log[axis=x]", + "162:5": "dark_oak_log[axis=x]", + "162:8": "acacia_log[axis=z]", + "162:9": "dark_oak_log[axis=z]", + "162:12": "acacia_bark", + "162:13": "dark_oak_bark", + "163:0": "acacia_stairs[facing=east,half=bottom,shape=straight]", + "163:1": "acacia_stairs[facing=west,half=bottom,shape=straight]", + "163:2": "acacia_stairs[facing=south,half=bottom,shape=straight]", + "163:3": "acacia_stairs[facing=north,half=bottom,shape=straight]", + "163:4": "acacia_stairs[facing=east,half=top,shape=straight]", + "163:5": "acacia_stairs[facing=west,half=top,shape=straight]", + "163:6": "acacia_stairs[facing=south,half=top,shape=straight]", + "163:7": "acacia_stairs[facing=north,half=top,shape=straight]", + "164:0": "dark_oak_stairs[facing=east,half=bottom,shape=straight]", + "164:1": "dark_oak_stairs[facing=west,half=bottom,shape=straight]", + "164:2": "dark_oak_stairs[facing=south,half=bottom,shape=straight]", + "164:3": "dark_oak_stairs[facing=north,half=bottom,shape=straight]", + "164:4": "dark_oak_stairs[facing=east,half=top,shape=straight]", + "164:5": "dark_oak_stairs[facing=west,half=top,shape=straight]", + "164:6": "dark_oak_stairs[facing=south,half=top,shape=straight]", + "164:7": "dark_oak_stairs[facing=north,half=top,shape=straight]", + "165:0": "slime_block", + "166:0": "barrier", + "167:0": "iron_trapdoor[facing=north,half=bottom,open=false]", + "167:1": "iron_trapdoor[facing=south,half=bottom,open=false]", + "167:2": "iron_trapdoor[facing=west,half=bottom,open=false]", + "167:3": "iron_trapdoor[facing=east,half=bottom,open=false]", + "167:4": "iron_trapdoor[facing=north,half=bottom,open=true]", + "167:5": "iron_trapdoor[facing=south,half=bottom,open=true]", + "167:6": "iron_trapdoor[facing=west,half=bottom,open=true]", + "167:7": "iron_trapdoor[facing=east,half=bottom,open=true]", + "167:8": "iron_trapdoor[facing=north,half=top,open=false]", + "167:9": "iron_trapdoor[facing=south,half=top,open=false]", + "167:10": "iron_trapdoor[facing=west,half=top,open=false]", + "167:11": "iron_trapdoor[facing=east,half=top,open=false]", + "167:12": "iron_trapdoor[facing=north,half=top,open=true]", + "167:13": "iron_trapdoor[facing=south,half=top,open=true]", + "167:14": "iron_trapdoor[facing=west,half=top,open=true]", + "167:15": "iron_trapdoor[facing=east,half=top,open=true]", + "168:0": "prismarine", + "168:1": "prismarine_bricks", + "168:2": "dark_prismarine", + "169:0": "sea_lantern", + "170:0": "hay_block[axis=y]", + "170:4": "hay_block[axis=x]", + "170:8": "hay_block[axis=z]", + "171:0": "white_carpet", + "171:1": "orange_carpet", + "171:2": "magenta_carpet", + "171:3": "light_blue_carpet", + "171:4": "yellow_carpet", + "171:5": "lime_carpet", + "171:6": "pink_carpet", + "171:7": "gray_carpet", + "171:8": "light_gray_carpet", + "171:9": "cyan_carpet", + "171:10": "purple_carpet", + "171:11": "blue_carpet", + "171:12": "brown_carpet", + "171:13": "green_carpet", + "171:14": "red_carpet", + "171:15": "black_carpet", + "172:0": "terracotta", + "173:0": "coal_block", + "174:0": "packed_ice", + "175:0": "sunflower[half=lower]", + "175:1": "lilac[half=lower]", + "175:2": "tall_grass[half=lower]", + "175:3": "large_fern[half=lower]", + "175:4": "rose_bush[half=lower]", + "175:5": "peony[half=lower]", + "175:8": "peony[half=upper]", + "175:9": "peony[half=upper]", + "175:10": "peony[half=upper]", + "175:11": "peony[half=upper]", + "176:0": "white_banner[rotation=0]", + "176:1": "white_banner[rotation=1]", + "176:2": "white_banner[rotation=2]", + "176:3": "white_banner[rotation=3]", + "176:4": "white_banner[rotation=4]", + "176:5": "white_banner[rotation=5]", + "176:6": "white_banner[rotation=6]", + "176:7": "white_banner[rotation=7]", + "176:8": "white_banner[rotation=8]", + "176:9": "white_banner[rotation=9]", + "176:10": "white_banner[rotation=10]", + "176:11": "white_banner[rotation=11]", + "176:12": "white_banner[rotation=12]", + "176:13": "white_banner[rotation=13]", + "176:14": "white_banner[rotation=14]", + "176:15": "white_banner[rotation=15]", + "177:2": "white_wall_banner[facing=north]", + "177:3": "white_wall_banner[facing=south]", + "177:4": "white_wall_banner[facing=west]", + "177:5": "white_wall_banner[facing=east]", + "178:0": "daylight_detector[inverted=true,power=0]", + "178:1": "daylight_detector[inverted=true,power=1]", + "178:2": "daylight_detector[inverted=true,power=2]", + "178:3": "daylight_detector[inverted=true,power=3]", + "178:4": "daylight_detector[inverted=true,power=4]", + "178:5": "daylight_detector[inverted=true,power=5]", + "178:6": "daylight_detector[inverted=true,power=6]", + "178:7": "daylight_detector[inverted=true,power=7]", + "178:8": "daylight_detector[inverted=true,power=8]", + "178:9": "daylight_detector[inverted=true,power=9]", + "178:10": "daylight_detector[inverted=true,power=10]", + "178:11": "daylight_detector[inverted=true,power=11]", + "178:12": "daylight_detector[inverted=true,power=12]", + "178:13": "daylight_detector[inverted=true,power=13]", + "178:14": "daylight_detector[inverted=true,power=14]", + "178:15": "daylight_detector[inverted=true,power=15]", + "179:0": "red_sandstone", + "179:1": "chiseled_red_sandstone", + "179:2": "cut_red_sandstone", + "180:0": "red_sandstone_stairs[facing=east,half=bottom,shape=straight]", + "180:1": "red_sandstone_stairs[facing=west,half=bottom,shape=straight]", + "180:2": "red_sandstone_stairs[facing=south,half=bottom,shape=straight]", + "180:3": "red_sandstone_stairs[facing=north,half=bottom,shape=straight]", + "180:4": "red_sandstone_stairs[facing=east,half=top,shape=straight]", + "180:5": "red_sandstone_stairs[facing=west,half=top,shape=straight]", + "180:6": "red_sandstone_stairs[facing=south,half=top,shape=straight]", + "180:7": "red_sandstone_stairs[facing=north,half=top,shape=straight]", + "181:0": "red_sandstone_slab[type=double]", + "181:8": "smooth_red_sandstone", + "182:0": "red_sandstone_slab[type=bottom]", + "182:8": "red_sandstone_slab[type=top]", + "183:0": "spruce_fence_gate[facing=south,in_wall=false,open=false,powered=false]", + "183:1": "spruce_fence_gate[facing=west,in_wall=false,open=false,powered=false]", + "183:2": "spruce_fence_gate[facing=north,in_wall=false,open=false,powered=false]", + "183:3": "spruce_fence_gate[facing=east,in_wall=false,open=false,powered=false]", + "183:4": "spruce_fence_gate[facing=south,in_wall=false,open=true,powered=false]", + "183:5": "spruce_fence_gate[facing=west,in_wall=false,open=true,powered=false]", + "183:6": "spruce_fence_gate[facing=north,in_wall=false,open=true,powered=false]", + "183:7": "spruce_fence_gate[facing=east,in_wall=false,open=true,powered=false]", + "183:8": "spruce_fence_gate[facing=south,in_wall=false,open=false,powered=true]", + "183:9": "spruce_fence_gate[facing=west,in_wall=false,open=false,powered=true]", + "183:10": "spruce_fence_gate[facing=north,in_wall=false,open=false,powered=true]", + "183:11": "spruce_fence_gate[facing=east,in_wall=false,open=false,powered=true]", + "183:12": "spruce_fence_gate[facing=south,in_wall=false,open=true,powered=true]", + "183:13": "spruce_fence_gate[facing=west,in_wall=false,open=true,powered=true]", + "183:14": "spruce_fence_gate[facing=north,in_wall=false,open=true,powered=true]", + "183:15": "spruce_fence_gate[facing=east,in_wall=false,open=true,powered=true]", + "184:0": "birch_fence_gate[facing=south,in_wall=false,open=false,powered=false]", + "184:1": "birch_fence_gate[facing=west,in_wall=false,open=false,powered=false]", + "184:2": "birch_fence_gate[facing=north,in_wall=false,open=false,powered=false]", + "184:3": "birch_fence_gate[facing=east,in_wall=false,open=false,powered=false]", + "184:4": "birch_fence_gate[facing=south,in_wall=false,open=true,powered=false]", + "184:5": "birch_fence_gate[facing=west,in_wall=false,open=true,powered=false]", + "184:6": "birch_fence_gate[facing=north,in_wall=false,open=true,powered=false]", + "184:7": "birch_fence_gate[facing=east,in_wall=false,open=true,powered=false]", + "184:8": "birch_fence_gate[facing=south,in_wall=false,open=false,powered=true]", + "184:9": "birch_fence_gate[facing=west,in_wall=false,open=false,powered=true]", + "184:10": "birch_fence_gate[facing=north,in_wall=false,open=false,powered=true]", + "184:11": "birch_fence_gate[facing=east,in_wall=false,open=false,powered=true]", + "184:12": "birch_fence_gate[facing=south,in_wall=false,open=true,powered=true]", + "184:13": "birch_fence_gate[facing=west,in_wall=false,open=true,powered=true]", + "184:14": "birch_fence_gate[facing=north,in_wall=false,open=true,powered=true]", + "184:15": "birch_fence_gate[facing=east,in_wall=false,open=true,powered=true]", + "185:0": "jungle_fence_gate[facing=south,in_wall=false,open=false,powered=false]", + "185:1": "jungle_fence_gate[facing=west,in_wall=false,open=false,powered=false]", + "185:2": "jungle_fence_gate[facing=north,in_wall=false,open=false,powered=false]", + "185:3": "jungle_fence_gate[facing=east,in_wall=false,open=false,powered=false]", + "185:4": "jungle_fence_gate[facing=south,in_wall=false,open=true,powered=false]", + "185:5": "jungle_fence_gate[facing=west,in_wall=false,open=true,powered=false]", + "185:6": "jungle_fence_gate[facing=north,in_wall=false,open=true,powered=false]", + "185:7": "jungle_fence_gate[facing=east,in_wall=false,open=true,powered=false]", + "185:8": "jungle_fence_gate[facing=south,in_wall=false,open=false,powered=true]", + "185:9": "jungle_fence_gate[facing=west,in_wall=false,open=false,powered=true]", + "185:10": "jungle_fence_gate[facing=north,in_wall=false,open=false,powered=true]", + "185:11": "jungle_fence_gate[facing=east,in_wall=false,open=false,powered=true]", + "185:12": "jungle_fence_gate[facing=south,in_wall=false,open=true,powered=true]", + "185:13": "jungle_fence_gate[facing=west,in_wall=false,open=true,powered=true]", + "185:14": "jungle_fence_gate[facing=north,in_wall=false,open=true,powered=true]", + "185:15": "jungle_fence_gate[facing=east,in_wall=false,open=true,powered=true]", + "186:0": "dark_oak_fence_gate[facing=south,in_wall=false,open=false,powered=false]", + "186:1": "dark_oak_fence_gate[facing=west,in_wall=false,open=false,powered=false]", + "186:2": "dark_oak_fence_gate[facing=north,in_wall=false,open=false,powered=false]", + "186:3": "dark_oak_fence_gate[facing=east,in_wall=false,open=false,powered=false]", + "186:4": "dark_oak_fence_gate[facing=south,in_wall=false,open=true,powered=false]", + "186:5": "dark_oak_fence_gate[facing=west,in_wall=false,open=true,powered=false]", + "186:6": "dark_oak_fence_gate[facing=north,in_wall=false,open=true,powered=false]", + "186:7": "dark_oak_fence_gate[facing=east,in_wall=false,open=true,powered=false]", + "186:8": "dark_oak_fence_gate[facing=south,in_wall=false,open=false,powered=true]", + "186:9": "dark_oak_fence_gate[facing=west,in_wall=false,open=false,powered=true]", + "186:10": "dark_oak_fence_gate[facing=north,in_wall=false,open=false,powered=true]", + "186:11": "dark_oak_fence_gate[facing=east,in_wall=false,open=false,powered=true]", + "186:12": "dark_oak_fence_gate[facing=south,in_wall=false,open=true,powered=true]", + "186:13": "dark_oak_fence_gate[facing=west,in_wall=false,open=true,powered=true]", + "186:14": "dark_oak_fence_gate[facing=north,in_wall=false,open=true,powered=true]", + "186:15": "dark_oak_fence_gate[facing=east,in_wall=false,open=true,powered=true]", + "187:0": "acacia_fence_gate[facing=south,in_wall=false,open=false,powered=false]", + "187:1": "acacia_fence_gate[facing=west,in_wall=false,open=false,powered=false]", + "187:2": "acacia_fence_gate[facing=north,in_wall=false,open=false,powered=false]", + "187:3": "acacia_fence_gate[facing=east,in_wall=false,open=false,powered=false]", + "187:4": "acacia_fence_gate[facing=south,in_wall=false,open=true,powered=false]", + "187:5": "acacia_fence_gate[facing=west,in_wall=false,open=true,powered=false]", + "187:6": "acacia_fence_gate[facing=north,in_wall=false,open=true,powered=false]", + "187:7": "acacia_fence_gate[facing=east,in_wall=false,open=true,powered=false]", + "187:8": "acacia_fence_gate[facing=south,in_wall=false,open=false,powered=true]", + "187:9": "acacia_fence_gate[facing=west,in_wall=false,open=false,powered=true]", + "187:10": "acacia_fence_gate[facing=north,in_wall=false,open=false,powered=true]", + "187:11": "acacia_fence_gate[facing=east,in_wall=false,open=false,powered=true]", + "187:12": "acacia_fence_gate[facing=south,in_wall=false,open=true,powered=true]", + "187:13": "acacia_fence_gate[facing=west,in_wall=false,open=true,powered=true]", + "187:14": "acacia_fence_gate[facing=north,in_wall=false,open=true,powered=true]", + "187:15": "acacia_fence_gate[facing=east,in_wall=false,open=true,powered=true]", + "188:0": "spruce_fence[east=false,north=false,south=false,west=false]", + "189:0": "birch_fence[east=false,north=false,south=false,west=false]", + "190:0": "jungle_fence[east=false,north=false,south=false,west=false]", + "191:0": "dark_oak_fence[east=false,north=false,south=false,west=false]", + "192:0": "acacia_fence[east=false,north=false,south=false,west=false]", + "193:0": "spruce_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "193:1": "spruce_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "193:2": "spruce_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "193:3": "spruce_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "193:4": "spruce_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "193:5": "spruce_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "193:6": "spruce_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "193:7": "spruce_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "193:8": "spruce_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "193:9": "spruce_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "193:10": "spruce_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "193:11": "spruce_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "194:0": "birch_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "194:1": "birch_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "194:2": "birch_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "194:3": "birch_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "194:4": "birch_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "194:5": "birch_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "194:6": "birch_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "194:7": "birch_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "194:8": "birch_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "194:9": "birch_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "194:10": "birch_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "194:11": "birch_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "195:0": "jungle_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "195:1": "jungle_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "195:2": "jungle_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "195:3": "jungle_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "195:4": "jungle_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "195:5": "jungle_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "195:6": "jungle_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "195:7": "jungle_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "195:8": "jungle_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "195:9": "jungle_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "195:10": "jungle_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "195:11": "jungle_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "196:0": "acacia_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "196:1": "acacia_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "196:2": "acacia_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "196:3": "acacia_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "196:4": "acacia_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "196:5": "acacia_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "196:6": "acacia_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "196:7": "acacia_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "196:8": "acacia_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "196:9": "acacia_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "196:10": "acacia_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "196:11": "acacia_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "197:0": "dark_oak_door[facing=east,half=lower,hinge=right,open=false,powered=false]", + "197:1": "dark_oak_door[facing=south,half=lower,hinge=right,open=false,powered=false]", + "197:2": "dark_oak_door[facing=west,half=lower,hinge=right,open=false,powered=false]", + "197:3": "dark_oak_door[facing=north,half=lower,hinge=right,open=false,powered=false]", + "197:4": "dark_oak_door[facing=east,half=lower,hinge=right,open=true,powered=false]", + "197:5": "dark_oak_door[facing=south,half=lower,hinge=right,open=true,powered=false]", + "197:6": "dark_oak_door[facing=west,half=lower,hinge=right,open=true,powered=false]", + "197:7": "dark_oak_door[facing=north,half=lower,hinge=right,open=true,powered=false]", + "197:8": "dark_oak_door[facing=east,half=upper,hinge=left,open=false,powered=false]", + "197:9": "dark_oak_door[facing=east,half=upper,hinge=right,open=false,powered=false]", + "197:10": "dark_oak_door[facing=east,half=upper,hinge=left,open=false,powered=true]", + "197:11": "dark_oak_door[facing=east,half=upper,hinge=right,open=false,powered=true]", + "198:0": "end_rod[facing=down]", + "198:1": "end_rod[facing=up]", + "198:2": "end_rod[facing=north]", + "198:3": "end_rod[facing=south]", + "198:4": "end_rod[facing=west]", + "198:5": "end_rod[facing=east]", + "199:0": "chorus_plant[down=false,east=false,north=false,south=false,up=false,west=false]", + "200:0": "chorus_flower[age=0]", + "200:1": "chorus_flower[age=1]", + "200:2": "chorus_flower[age=2]", + "200:3": "chorus_flower[age=3]", + "200:4": "chorus_flower[age=4]", + "200:5": "chorus_flower[age=5]", + "201:0": "purpur_block", + "202:0": "purpur_pillar[axis=y]", + "202:4": "purpur_pillar[axis=x]", + "202:8": "purpur_pillar[axis=z]", + "203:0": "purpur_stairs[facing=east,half=bottom,shape=straight]", + "203:1": "purpur_stairs[facing=west,half=bottom,shape=straight]", + "203:2": "purpur_stairs[facing=south,half=bottom,shape=straight]", + "203:3": "purpur_stairs[facing=north,half=bottom,shape=straight]", + "203:4": "purpur_stairs[facing=east,half=top,shape=straight]", + "203:5": "purpur_stairs[facing=west,half=top,shape=straight]", + "203:6": "purpur_stairs[facing=south,half=top,shape=straight]", + "203:7": "purpur_stairs[facing=north,half=top,shape=straight]", + "204:0": "purpur_slab[type=double]", + "205:0": "purpur_slab[type=bottom]", + "205:8": "purpur_slab[type=top]", + "206:0": "end_stone_bricks", + "207:0": "beetroots[age=0]", + "207:1": "beetroots[age=1]", + "207:2": "beetroots[age=2]", + "207:3": "beetroots[age=3]", + "208:0": "grass_path", + "209:0": "end_gateway", + "210:0": "repeating_command_block[conditional=false,facing=down]", + "210:1": "repeating_command_block[conditional=false,facing=up]", + "210:2": "repeating_command_block[conditional=false,facing=north]", + "210:3": "repeating_command_block[conditional=false,facing=south]", + "210:4": "repeating_command_block[conditional=false,facing=west]", + "210:5": "repeating_command_block[conditional=false,facing=east]", + "210:8": "repeating_command_block[conditional=true,facing=down]", + "210:9": "repeating_command_block[conditional=true,facing=up]", + "210:10": "repeating_command_block[conditional=true,facing=north]", + "210:11": "repeating_command_block[conditional=true,facing=south]", + "210:12": "repeating_command_block[conditional=true,facing=west]", + "210:13": "repeating_command_block[conditional=true,facing=east]", + "211:0": "chain_command_block[conditional=false,facing=down]", + "211:1": "chain_command_block[conditional=false,facing=up]", + "211:2": "chain_command_block[conditional=false,facing=north]", + "211:3": "chain_command_block[conditional=false,facing=south]", + "211:4": "chain_command_block[conditional=false,facing=west]", + "211:5": "chain_command_block[conditional=false,facing=east]", + "211:8": "chain_command_block[conditional=true,facing=down]", + "211:9": "chain_command_block[conditional=true,facing=up]", + "211:10": "chain_command_block[conditional=true,facing=north]", + "211:11": "chain_command_block[conditional=true,facing=south]", + "211:12": "chain_command_block[conditional=true,facing=west]", + "211:13": "chain_command_block[conditional=true,facing=east]", + "212:0": "frosted_ice[age=0]", + "212:1": "frosted_ice[age=1]", + "212:2": "frosted_ice[age=2]", + "212:3": "frosted_ice[age=3]", + "213:0": "magma_block", + "214:0": "nether_wart_block", + "215:0": "red_nether_bricks", + "216:0": "bone_block[axis=y]", + "216:4": "bone_block[axis=x]", + "216:8": "bone_block[axis=z]", + "217:0": "structure_void", + "218:0": "observer[facing=down,powered=false]", + "218:1": "observer[facing=up,powered=false]", + "218:2": "observer[facing=north,powered=false]", + "218:3": "observer[facing=south,powered=false]", + "218:4": "observer[facing=west,powered=false]", + "218:5": "observer[facing=east,powered=false]", + "218:8": "observer[facing=down,powered=true]", + "218:9": "observer[facing=up,powered=true]", + "218:10": "observer[facing=north,powered=true]", + "218:11": "observer[facing=south,powered=true]", + "218:12": "observer[facing=west,powered=true]", + "218:13": "observer[facing=east,powered=true]", + "219:0": "white_shulker_box[facing=down]", + "219:1": "white_shulker_box[facing=up]", + "219:2": "white_shulker_box[facing=north]", + "219:3": "white_shulker_box[facing=south]", + "219:4": "white_shulker_box[facing=west]", + "219:5": "white_shulker_box[facing=east]", + "220:0": "orange_shulker_box[facing=down]", + "220:1": "orange_shulker_box[facing=up]", + "220:2": "orange_shulker_box[facing=north]", + "220:3": "orange_shulker_box[facing=south]", + "220:4": "orange_shulker_box[facing=west]", + "220:5": "orange_shulker_box[facing=east]", + "221:0": "magenta_shulker_box[facing=down]", + "221:1": "magenta_shulker_box[facing=up]", + "221:2": "magenta_shulker_box[facing=north]", + "221:3": "magenta_shulker_box[facing=south]", + "221:4": "magenta_shulker_box[facing=west]", + "221:5": "magenta_shulker_box[facing=east]", + "222:0": "light_blue_shulker_box[facing=down]", + "222:1": "light_blue_shulker_box[facing=up]", + "222:2": "light_blue_shulker_box[facing=north]", + "222:3": "light_blue_shulker_box[facing=south]", + "222:4": "light_blue_shulker_box[facing=west]", + "222:5": "light_blue_shulker_box[facing=east]", + "223:0": "yellow_shulker_box[facing=down]", + "223:1": "yellow_shulker_box[facing=up]", + "223:2": "yellow_shulker_box[facing=north]", + "223:3": "yellow_shulker_box[facing=south]", + "223:4": "yellow_shulker_box[facing=west]", + "223:5": "yellow_shulker_box[facing=east]", + "224:0": "lime_shulker_box[facing=down]", + "224:1": "lime_shulker_box[facing=up]", + "224:2": "lime_shulker_box[facing=north]", + "224:3": "lime_shulker_box[facing=south]", + "224:4": "lime_shulker_box[facing=west]", + "224:5": "lime_shulker_box[facing=east]", + "225:0": "pink_shulker_box[facing=down]", + "225:1": "pink_shulker_box[facing=up]", + "225:2": "pink_shulker_box[facing=north]", + "225:3": "pink_shulker_box[facing=south]", + "225:4": "pink_shulker_box[facing=west]", + "225:5": "pink_shulker_box[facing=east]", + "226:0": "gray_shulker_box[facing=down]", + "226:1": "gray_shulker_box[facing=up]", + "226:2": "gray_shulker_box[facing=north]", + "226:3": "gray_shulker_box[facing=south]", + "226:4": "gray_shulker_box[facing=west]", + "226:5": "gray_shulker_box[facing=east]", + "227:0": "light_gray_shulker_box[facing=down]", + "227:1": "light_gray_shulker_box[facing=up]", + "227:2": "light_gray_shulker_box[facing=north]", + "227:3": "light_gray_shulker_box[facing=south]", + "227:4": "light_gray_shulker_box[facing=west]", + "227:5": "light_gray_shulker_box[facing=east]", + "228:0": "cyan_shulker_box[facing=down]", + "228:1": "cyan_shulker_box[facing=up]", + "228:2": "cyan_shulker_box[facing=north]", + "228:3": "cyan_shulker_box[facing=south]", + "228:4": "cyan_shulker_box[facing=west]", + "228:5": "cyan_shulker_box[facing=east]", + "229:0": "purple_shulker_box[facing=down]", + "229:1": "purple_shulker_box[facing=up]", + "229:2": "purple_shulker_box[facing=north]", + "229:3": "purple_shulker_box[facing=south]", + "229:4": "purple_shulker_box[facing=west]", + "229:5": "purple_shulker_box[facing=east]", + "230:0": "blue_shulker_box[facing=down]", + "230:1": "blue_shulker_box[facing=up]", + "230:2": "blue_shulker_box[facing=north]", + "230:3": "blue_shulker_box[facing=south]", + "230:4": "blue_shulker_box[facing=west]", + "230:5": "blue_shulker_box[facing=east]", + "231:0": "brown_shulker_box[facing=down]", + "231:1": "brown_shulker_box[facing=up]", + "231:2": "brown_shulker_box[facing=north]", + "231:3": "brown_shulker_box[facing=south]", + "231:4": "brown_shulker_box[facing=west]", + "231:5": "brown_shulker_box[facing=east]", + "232:0": "green_shulker_box[facing=down]", + "232:1": "green_shulker_box[facing=up]", + "232:2": "green_shulker_box[facing=north]", + "232:3": "green_shulker_box[facing=south]", + "232:4": "green_shulker_box[facing=west]", + "232:5": "green_shulker_box[facing=east]", + "233:0": "red_shulker_box[facing=down]", + "233:1": "red_shulker_box[facing=up]", + "233:2": "red_shulker_box[facing=north]", + "233:3": "red_shulker_box[facing=south]", + "233:4": "red_shulker_box[facing=west]", + "233:5": "red_shulker_box[facing=east]", + "234:0": "black_shulker_box[facing=down]", + "234:1": "black_shulker_box[facing=up]", + "234:2": "black_shulker_box[facing=north]", + "234:3": "black_shulker_box[facing=south]", + "234:4": "black_shulker_box[facing=west]", + "234:5": "black_shulker_box[facing=east]", + "235:0": "white_glazed_terracotta[facing=south]", + "235:1": "white_glazed_terracotta[facing=west]", + "235:2": "white_glazed_terracotta[facing=north]", + "235:3": "white_glazed_terracotta[facing=east]", + "236:0": "orange_glazed_terracotta[facing=south]", + "236:1": "orange_glazed_terracotta[facing=west]", + "236:2": "orange_glazed_terracotta[facing=north]", + "236:3": "orange_glazed_terracotta[facing=east]", + "237:0": "magenta_glazed_terracotta[facing=south]", + "237:1": "magenta_glazed_terracotta[facing=west]", + "237:2": "magenta_glazed_terracotta[facing=north]", + "237:3": "magenta_glazed_terracotta[facing=east]", + "238:0": "light_blue_glazed_terracotta[facing=south]", + "238:1": "light_blue_glazed_terracotta[facing=west]", + "238:2": "light_blue_glazed_terracotta[facing=north]", + "238:3": "light_blue_glazed_terracotta[facing=east]", + "239:0": "yellow_glazed_terracotta[facing=south]", + "239:1": "yellow_glazed_terracotta[facing=west]", + "239:2": "yellow_glazed_terracotta[facing=north]", + "239:3": "yellow_glazed_terracotta[facing=east]", + "240:0": "lime_glazed_terracotta[facing=south]", + "240:1": "lime_glazed_terracotta[facing=west]", + "240:2": "lime_glazed_terracotta[facing=north]", + "240:3": "lime_glazed_terracotta[facing=east]", + "241:0": "pink_glazed_terracotta[facing=south]", + "241:1": "pink_glazed_terracotta[facing=west]", + "241:2": "pink_glazed_terracotta[facing=north]", + "241:3": "pink_glazed_terracotta[facing=east]", + "242:0": "gray_glazed_terracotta[facing=south]", + "242:1": "gray_glazed_terracotta[facing=west]", + "242:2": "gray_glazed_terracotta[facing=north]", + "242:3": "gray_glazed_terracotta[facing=east]", + "243:0": "light_gray_glazed_terracotta[facing=south]", + "243:1": "light_gray_glazed_terracotta[facing=west]", + "243:2": "light_gray_glazed_terracotta[facing=north]", + "243:3": "light_gray_glazed_terracotta[facing=east]", + "244:0": "cyan_glazed_terracotta[facing=south]", + "244:1": "cyan_glazed_terracotta[facing=west]", + "244:2": "cyan_glazed_terracotta[facing=north]", + "244:3": "cyan_glazed_terracotta[facing=east]", + "245:0": "purple_glazed_terracotta[facing=south]", + "245:1": "purple_glazed_terracotta[facing=west]", + "245:2": "purple_glazed_terracotta[facing=north]", + "245:3": "purple_glazed_terracotta[facing=east]", + "246:0": "blue_glazed_terracotta[facing=south]", + "246:1": "blue_glazed_terracotta[facing=west]", + "246:2": "blue_glazed_terracotta[facing=north]", + "246:3": "blue_glazed_terracotta[facing=east]", + "247:0": "brown_glazed_terracotta[facing=south]", + "247:1": "brown_glazed_terracotta[facing=west]", + "247:2": "brown_glazed_terracotta[facing=north]", + "247:3": "brown_glazed_terracotta[facing=east]", + "248:0": "green_glazed_terracotta[facing=south]", + "248:1": "green_glazed_terracotta[facing=west]", + "248:2": "green_glazed_terracotta[facing=north]", + "248:3": "green_glazed_terracotta[facing=east]", + "249:0": "red_glazed_terracotta[facing=south]", + "249:1": "red_glazed_terracotta[facing=west]", + "249:2": "red_glazed_terracotta[facing=north]", + "249:3": "red_glazed_terracotta[facing=east]", + "250:0": "black_glazed_terracotta[facing=south]", + "250:1": "black_glazed_terracotta[facing=west]", + "250:2": "black_glazed_terracotta[facing=north]", + "250:3": "black_glazed_terracotta[facing=east]", + "251:0": "white_concrete", + "251:1": "orange_concrete", + "251:2": "magenta_concrete", + "251:3": "light_blue_concrete", + "251:4": "yellow_concrete", + "251:5": "lime_concrete", + "251:6": "pink_concrete", + "251:7": "gray_concrete", + "251:8": "light_gray_concrete", + "251:9": "cyan_concrete", + "251:10": "purple_concrete", + "251:11": "blue_concrete", + "251:12": "brown_concrete", + "251:13": "green_concrete", + "251:14": "red_concrete", + "251:15": "black_concrete", + "252:0": "white_concrete_powder", + "252:1": "orange_concrete_powder", + "252:2": "magenta_concrete_powder", + "252:3": "light_blue_concrete_powder", + "252:4": "yellow_concrete_powder", + "252:5": "lime_concrete_powder", + "252:6": "pink_concrete_powder", + "252:7": "gray_concrete_powder", + "252:8": "light_gray_concrete_powder", + "252:9": "cyan_concrete_powder", + "252:10": "purple_concrete_powder", + "252:11": "blue_concrete_powder", + "252:12": "brown_concrete_powder", + "252:13": "green_concrete_powder", + "252:14": "red_concrete_powder", + "252:15": "black_concrete_powder", + "255:0": "structure_block[mode=save]", + "255:1": "structure_block[mode=load]", + "255:2": "structure_block[mode=corner]", + "255:3": "structure_block[mode=data]" + }, + "clientCalculatedBlocks": { + "block_snowy": [ + "grass_block", + "dirt", + "coarse_dirt", + "podzol", + "mycelium" + ], + "directional": [ + "fire", + "redstone_wire", + "oak_fence", + "iron_bars", + "glass_pane", + "vine", + "nether_brick_fence", + "tripwire", + "cobblestone_wall", + "mossy_cobblestone_wall", + "white_stained_glass_pane", + "orange_stained_glass_pane", + "magenta_stained_glass_pane", + "light_blue_stained_glass_pane", + "yellow_stained_glass_pane", + "lime_stained_glass_pane", + "pink_stained_glass_pane", + "gray_stained_glass_pane", + "light_gray_stained_glass_pane", + "cyan_stained_glass_pane", + "purple_stained_glass_pane", + "blue_stained_glass_pane", + "brown_stained_glass_pane", + "green_stained_glass_pane", + "red_stained_glass_pane", + "black_stained_glass_pane", + "spruce_fence", + "birch_fence", + "jungle_fence", + "dark_oak_fence", + "acacia_fence", + "chorus_plant" + ], + "door": [ + "oak_door", + "iron_door", + "spruce_door", + "birch_door", + "jungle_door", + "acacia_door", + "dark_oak_door" + ], + "repeater_locked": [ + "repeater" + ], + "gate_in_wall": [ + "oak_fence_gate", + "spruce_fence_gate", + "birch_fence_gate", + "jungle_fence_gate", + "dark_oak_fence_gate", + "acacia_fence_gate" + ] + } +} diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx new file mode 100644 index 00000000..36fd5264 --- /dev/null +++ b/src/react/AddServerOrConnect.tsx @@ -0,0 +1,256 @@ +import React, { useEffect } from 'react' +import { appQueryParams } from '../appParams' +import { fetchServerStatus, isServerValid } from '../api/mcStatusApi' +import { parseServerAddress } from '../parseServerAddress' +import Screen from './Screen' +import Input, { INPUT_LABEL_WIDTH, InputWithLabel } from './Input' +import Button from './Button' +import SelectGameVersion from './SelectGameVersion' +import { usePassesScaledDimensions } from './UIProvider' + +export interface BaseServerInfo { + ip: string + name?: string + versionOverride?: string + proxyOverride?: string + usernameOverride?: string + /** Username or always use new if true */ + authenticatedAccountOverride?: string | true +} + +interface Props { + onBack: () => void + onConfirm: (info: BaseServerInfo) => void + title?: string + initialData?: BaseServerInfo + parseQs?: boolean + onQsConnect?: (server: BaseServerInfo) => void + placeholders?: Pick + accounts?: string[] + authenticatedAccounts?: number + versions?: string[] +} + +export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions }: Props) => { + const isSmallHeight = !usePassesScaledDimensions(null, 350) + const qsParamName = parseQs ? appQueryParams.name : undefined + const qsParamIp = parseQs ? appQueryParams.ip : undefined + const qsParamVersion = parseQs ? appQueryParams.version : undefined + const qsParamProxy = parseQs ? appQueryParams.proxy : undefined + const qsParamUsername = parseQs ? appQueryParams.username : undefined + const qsParamLockConnect = parseQs ? appQueryParams.lockConnect : undefined + + const parsedQsIp = parseServerAddress(qsParamIp) + const parsedInitialIp = parseServerAddress(initialData?.ip) + + const [serverName, setServerName] = React.useState(initialData?.name ?? qsParamName ?? '') + const [serverIp, setServerIp] = React.useState(parsedQsIp.serverIpFull || parsedInitialIp.serverIpFull || '') + const [versionOverride, setVersionOverride] = React.useState(initialData?.versionOverride ?? /* legacy */ initialData?.['version'] ?? qsParamVersion ?? '') + const [proxyOverride, setProxyOverride] = React.useState(initialData?.proxyOverride ?? qsParamProxy ?? '') + const [usernameOverride, setUsernameOverride] = React.useState(initialData?.usernameOverride ?? qsParamUsername ?? '') + const lockConnect = qsParamLockConnect === 'true' + + const smallWidth = !usePassesScaledDimensions(400) + const initialAccount = initialData?.authenticatedAccountOverride + const [accountIndex, setAccountIndex] = React.useState(initialAccount === true ? -2 : initialAccount ? (accounts?.includes(initialAccount) ? accounts.indexOf(initialAccount) : -2) : -1) + + const freshAccount = accountIndex === -2 + const noAccountSelected = accountIndex === -1 + const authenticatedAccountOverride = noAccountSelected ? undefined : freshAccount ? true : accounts?.[accountIndex] + + let ipFinal = serverIp + ipFinal = ipFinal.replace(/:$/, '') + const commonUseOptions: BaseServerInfo = { + name: serverName, + ip: ipFinal, + versionOverride: versionOverride || undefined, + proxyOverride: proxyOverride || undefined, + usernameOverride: usernameOverride || undefined, + authenticatedAccountOverride, + } + + const [fetchedServerInfoIp, setFetchedServerInfoIp] = React.useState(undefined) + const [serverOnline, setServerOnline] = React.useState(null as boolean | null) + const [onlinePlayersList, setOnlinePlayersList] = React.useState([]) + + useEffect(() => { + const controller = new AbortController() + + const checkServer = async () => { + if (!qsParamIp || !isServerValid(qsParamIp)) return + + try { + const status = await fetchServerStatus(qsParamIp) + if (!status) return + + setServerOnline(status.raw.online) + setOnlinePlayersList(status.raw.players?.list.map(p => p.name_raw) ?? []) + setFetchedServerInfoIp(qsParamIp) + } catch (err) { + console.error('Failed to fetch server status:', err) + } + } + + void checkServer() + return () => controller.abort() + }, [qsParamIp]) + + const validateUsername = (username: string) => { + if (!username) return undefined + if (onlinePlayersList.includes(username)) { + return { border: 'red solid 1px' } + } + const MINECRAFT_USERNAME_REGEX = /^\w{3,16}$/ + if (!MINECRAFT_USERNAME_REGEX.test(username)) { + return { border: 'red solid 1px' } + } + return undefined + } + + const validateServerIp = () => { + if (!serverIp) return undefined + if (serverOnline) { + return { border: 'lightgreen solid 1px' } + } else { + return { border: 'red solid 1px' } + } + } + + const displayConnectButton = qsParamIp + const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun'] + // pick random example + const example = serverExamples[Math.floor(Math.random() * serverExamples.length)] + + return +
{ + e.preventDefault() + onConfirm(commonUseOptions) + }} + > +
+ { + setServerIp(value) + setServerOnline(false) + }} + validateInput={serverOnline === null || fetchedServerInfoIp !== serverIp ? undefined : validateServerIp} + placeholder={example} + /> + {!lockConnect && <> +
+ setServerName(value)} placeholder='Defaults to IP' /> +
+ } + {isSmallHeight ?
:
Overrides:
} +
+ + { return { value: v, label: v } }) ?? []} + onChange={(value) => { + setVersionOverride(value) + }} + placeholder="Optional, but recommended to specify" + disabled={lockConnect} + /> +
+ + setProxyOverride(value)} + placeholder={serverIp.startsWith('ws://') || serverIp.startsWith('wss://') ? 'Not needed for websocket servers' : placeholders?.proxyOverride} + /> + setUsernameOverride(value)} + placeholder={placeholders?.usernameOverride} + validateInput={!serverOnline || fetchedServerInfoIp !== serverIp ? undefined : validateUsername} + /> + + + {!lockConnect && <> + { + onBack() + }}> + Cancel + + + {displayConnectButton ? translate('Save') : {translate('Save')}} + + } + {displayConnectButton && ( +
+ { + onQsConnect?.(commonUseOptions) + }} + > + {translate('Connect')} + +
+ )} +
+ + +} + +const ButtonWrapper = ({ ...props }: React.ComponentProps) => { + props.style ??= {} + props.style.width = INPUT_LABEL_WIDTH + return - - )} - +
+ + + {status} + + +

{description}

+

{lastStatus ? `Last status: ${lastStatus}` : lastStatus}

+ + } + backdrop='dirt' + > + {isError && ( + <> + {showReconnect && onReconnect && } + {actionsSlot} + {!lockConnect && } + {backAction &&
) } diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index c8d7f696..9c7b34ac 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -1,13 +1,21 @@ import { proxy, useSnapshot } from 'valtio' -import { useEffect } from 'react' +import { useEffect, useRef, useState } from 'react' import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack, miscUiState } from '../globalState' -import { resetLocalStorageWorld } from '../browserfs' -import { fsState } from '../loadSave' -import { guessProblem } from '../guessProblem' +import { guessProblem } from '../errorLoadingScreenHelpers' +import type { ConnectOptions } from '../connect' +import { downloadPacketsReplay, packetsRecordingState, replayLogger } from '../packetsReplay/packetsReplayLegacy' +import { getProxyDetails } from '../microsoftAuthflow' +import { downloadAutoCapturedPackets, getLastAutoCapturedPackets } from '../mineflayer/plugins/packetsRecording' +import { appQueryParams } from '../appParams' import AppStatus from './AppStatus' import DiveTransition from './DiveTransition' -import { useDidUpdateEffect, useIsModalActive } from './utils' +import { useDidUpdateEffect } from './utils' +import { useIsModalActive } from './utilsApp' import Button from './Button' +import { updateAuthenticatedAccountData, updateLoadedServerData, AuthenticatedAccount } from './serversStorage' +import { showOptionsModal } from './SelectOption' +import LoadingChunks from './LoadingChunks' +import MessageFormattedString from './MessageFormattedString' const initialState = { status: '', @@ -16,30 +24,94 @@ const initialState = { descriptionHint: '', isError: false, hideDots: false, + loadingChunksData: null as null | Record, + loadingChunksDataPlayerChunk: null as null | { x: number, z: number }, + isDisplaying: false, + minecraftJsonMessage: null as null | Record, + showReconnect: false } export const appStatusState = proxy(initialState) -const resetState = () => { +export const resetAppStatusState = () => { Object.assign(appStatusState, initialState) } export const lastConnectOptions = { - value: null as any | null + value: null as ConnectOptions | null +} +globalThis.lastConnectOptions = lastConnectOptions + +const saveReconnectOptions = (options: ConnectOptions) => { + sessionStorage.setItem('reconnectOptions', JSON.stringify({ + value: options, + timestamp: Date.now() + })) +} + +export const reconnectReload = () => { + if (lastConnectOptions.value) { + saveReconnectOptions(lastConnectOptions.value) + window.location.reload() + } +} + +export const quickDevReconnect = () => { + if (!lastConnectOptions.value) { + return + } + + resetAppStatusState() + window.dispatchEvent(new window.CustomEvent('connect', { + detail: lastConnectOptions.value + })) } export default () => { - const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint } = useSnapshot(appStatusState) + const lastState = useRef(JSON.parse(JSON.stringify(appStatusState))) + const currentState = useSnapshot(appStatusState) + const { active: replayActive } = useSnapshot(packetsRecordingState) const isOpen = useIsModalActive('app-status') + if (isOpen) { + lastState.current = JSON.parse(JSON.stringify(currentState)) + } + + const usingState = (isOpen ? currentState : lastState.current) as typeof currentState + const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint, loadingChunksData, loadingChunksDataPlayerChunk, minecraftJsonMessage, showReconnect } = usingState + useDidUpdateEffect(() => { // todo play effect only when world successfully loaded if (!isOpen) { - const divingElem: HTMLElement = document.querySelector('#viewer-canvas')! - divingElem.style.animationName = 'dive-animation' - divingElem.parentElement!.style.perspective = '1200px' - divingElem.onanimationend = () => { - divingElem.parentElement!.style.perspective = '' - divingElem.onanimationend = null + const startDiveAnimation = (divingElem: HTMLElement) => { + divingElem.style.animationName = 'dive-animation' + divingElem.parentElement!.style.perspective = '1200px' + divingElem.onanimationend = () => { + divingElem.parentElement!.style.perspective = '' + divingElem.onanimationend = null + } + } + + const divingElem = document.querySelector('#viewer-canvas') + let observer: MutationObserver | null = null + if (divingElem) { + startDiveAnimation(divingElem as HTMLElement) + } else { + observer = new MutationObserver((mutations) => { + const divingElem = document.querySelector('#viewer-canvas') + if (divingElem) { + startDiveAnimation(divingElem as HTMLElement) + observer!.disconnect() + } + }) + observer.observe(document.body, { + childList: true, + subtree: true + }) + } + return () => { + if (observer) { + observer.disconnect() + } } } }, [isOpen]) @@ -47,44 +119,124 @@ export default () => { useEffect(() => { const controller = new AbortController() window.addEventListener('keyup', (e) => { + if ('input textarea select'.split(' ').includes((e.target as HTMLElement).tagName?.toLowerCase() ?? '')) return if (activeModalStack.at(-1)?.reactType !== 'app-status') return + // todo do only if reconnect is possible if (e.code !== 'KeyR' || !lastConnectOptions.value) return - resetState() - window.dispatchEvent(new window.CustomEvent('connect', { - detail: lastConnectOptions.value - })) + quickDevReconnect() }, { signal: controller.signal }) return () => controller.abort() }, []) - return + const displayAuthButton = status.includes('This server appears to be an online server and you are providing no authentication.') + || JSON.stringify(minecraftJsonMessage ?? {}).toLowerCase().includes('authenticate') + const hasVpnText = (text: string) => text.includes('VPN') || text.includes('Proxy') + const displayVpnButton = hasVpnText(status) || (minecraftJsonMessage && hasVpnText(JSON.stringify(minecraftJsonMessage))) + const authReconnectAction = async () => { + let accounts = [] as AuthenticatedAccount[] + updateAuthenticatedAccountData(oldAccounts => { + accounts = oldAccounts + return oldAccounts + }) + + const account = await showOptionsModal('Choose account to connect with', [...accounts.map(account => account.username), 'Use other account']) + if (!account) return + lastConnectOptions.value!.authenticatedAccount = accounts.find(acc => acc.username === account) || true + quickDevReconnect() + } + + const lastAutoCapturedPackets = getLastAutoCapturedPackets() + const lockConnect = appQueryParams.lockConnect === 'true' + const wasDisconnected = showReconnect + let backAction = undefined as (() => void) | undefined + if (maybeRecoverable && (!lockConnect || !wasDisconnected)) { + backAction = () => { + if (!wasDisconnected) { + hideModal(undefined, undefined, { force: true }) + return + } + resetAppStatusState() + miscUiState.gameLoaded = false + miscUiState.loadedDataVersion = null + window.loadedData = undefined + if (activeModalStacks['main-menu']) { + insertActiveModalStack('main-menu') + if (activeModalStack.at(-1)?.reactType === 'app-status') { + hideModal(undefined, undefined, { force: true }) // workaround: hide loader that was shown on world loading + } + } else { + hideModal(undefined, undefined, { force: true }) + } + } + } + return { - resetState() - miscUiState.gameLoaded = false - miscUiState.loadedDataVersion = null - window.loadedData = undefined - if (activeModalStacks['main-menu']) { - insertActiveModalStack('main-menu') - } else { - hideModal(undefined, undefined, { force: true }) - } - } : undefined} - // actionsSlot={ - // +
+} diff --git a/src/react/Book.module.css b/src/react/Book.module.css new file mode 100644 index 00000000..d672baf6 --- /dev/null +++ b/src/react/Book.module.css @@ -0,0 +1,605 @@ +.bookWrapper * { + box-sizing: border-box; +} +.bookWrapper { + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5vh; +} +.bookContainer { + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: 3%; + padding: 0; +} + +.bookImages { + position: relative; +} + +.outSide { + display: none; + position: absolute; + z-index: 2; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 40%; + height: 100%; + margin: 0 auto; +} +.titleIcon { + display: none; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 50%; + height: 100%; + margin: 0 auto; + z-index: 2; +} +.titleContent { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} +.titleContent span { + font-size: 10px; +} +.titleContent input { + text-align: center; + width: 100%; + height: 10%; + font-size: 10px; + margin: 4% 0%; + padding: 2% 4%; + background-color: transparent; + color: white; + caret-color: greenyellow; + font-family: inherit; + border: 1px solid black; + padding: 8px 15px; +} + +.insideIcon { + width: 100%; + height: 20vh; +} +.insideHalfIcon { + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; +} + +.inside { + position: absolute; + top: 0%; + width: 90%; + height: 100%; + display: flex; + justify-content: flex-start; + align-items: flex-start; + gap: 5%; + background-color: transparent; + box-shadow: none; + padding: 3% 0%; +} +.inside.uneditable { + height: 90%; +} + +.page { + position: relative; + width: 50%; + height: 100%; + overflow-y: auto; + background-color: transparent; + overflow: hidden; +} + +.messageFormattedString { + position: relative; + width: 100%; + height: 100%; + white-space: pre-wrap; + font-family: minecraft; + font-size: 10px; + padding: 10px; +} + +.messageFormattedString > span { + text-shadow: none !important; +} + +.textArea { + position: relative; + width: 100%; + height: 100%; + border: none; + outline: none; + resize: none; + font-family: minecraft; + font-size: 10px; + background-color: transparent; + box-shadow: none; + overflow: hidden; + text-overflow: ellipsis; +} +.textArea > span { + animation: blink 1s step-end infinite; + border-bottom: 2px solid white; +} + +@keyframes blink { + from, + to { + border-color: transparent; + } + 50% { + border-color: #fff; + } +} + +.controlPrev, .controlNext, +.controlPrev::before, .controlNext::before, +.controlPrev::after, .controlNext::after { + border: none !important; + background-color: transparent !important; + text-shadow: none !important; + width: auto !important; +} + +.inside Button.controlPrev { + position: absolute !important; + background-image: url('./book_icons/prev.webp'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + left: 2%; + bottom: 6%; + margin: 0; + padding: 4%; +} +.inside Button.controlPrev:active { + background-image: url('./book_icons/prev-click.webp') !important; +} +.inside Button.controlNext { + position: absolute !important; + background-image: url('./book_icons/next.webp') !important; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + right: 2%; + bottom: 6%; + margin: 0; + padding: 4%; +} +.inside Button.controlNext:active { + background-image: url('./book_icons/next-click.webp') !important; +} + +.actions { + display: flex; + flex-wrap: wrap; + width: 50%; + gap: 0.5vw; + row-gap: 0.5vw; + align-items: center; + justify-content: center; +} + +.actions button { + cursor: pointer; + font-family: minecraft; + width: 45%; +} + +/* Animations */ + +@keyframes titleAnimation { + 0% { + transform: translateX(-50%) rotateY(85deg); + } + 100% { + transform: translateX(-50%) rotateY(0); + } +} + +@keyframes titleContentAnimation { + 0% { + transform: translateX(-50%) rotateY(85deg); + } + 100% { + transform: translateX(-50%) rotateY(0); + } +} + +@keyframes insideAnimation { + 0% { + clip-path: inset(0% 0% 0% 0%); + transform: translateX(0%); + } + 25% { + transform: translateX(5%); + } + 50% { + clip-path: inset(0% 0% 0% 50%); + transform: translateX(0%); + } + 99% { + opacity: 1; + } + 100% { + clip-path: inset(0% 0% 0% 75%); + transform: translateX(-25%); + opacity: 0; + } +} + +@keyframes pageAnimation { + 0% { + transform: translateX(0) rotateY(0); + } + 99% { + display: flex; + } + 100% { + transform: translateX(55%) rotateY(90deg); + padding: 0%; + display: none; + } +} +@keyframes pageTextAnimation { + 0% { + transform: translateX(0) rotateY(0); + } + 100% { + transform: translateX(65%) rotateY(88deg); + display: none; + } +} +@keyframes pageSecondTextAnimation { + 0% { + transform: translateX(0%); + } + 25% { + transform: translateX(10%); + } + 50% { + transform: translateX(0%); + display: flex; + } + 100% { + transform: translateX(-50%); + display: none; + } +} + +.pageAnimation { + animation: pageAnimation .15s forwards; +} + +.titleAnimation { + display: flex; + animation: titleAnimation .3s forwards; +} + +.titleContentAnimation { + display: flex; + animation: titleContentAnimation .3s forwards; +} + +.insideAnimation { + animation: insideAnimation .3s forwards; +} + +.pageTextAnimation { + animation: pageTextAnimation .15s forwards; +} + +.pageSecondTextAnimation { + animation: pageSecondTextAnimation .3s forwards; +} + +.hidden { + display: none !important; +} + +/* Animation Reverse */ + +@keyframes titleAnimationReverse { + 0% { + transform: translateX(-50%) rotateY(0); + } + 50% { + transform: translateX(0%) + } + 100% { + transform: translateX(-50%) rotateY(-90deg); + display: none; + } +} + +@keyframes titleContentAnimationReverse { + 0% { + transform: translateX(-50%) rotateY(0); + } + 50% { + transform: translateX(10%) + } + 100% { + transform: translateX(-50%) rotateY(-90deg); + display: none; + } +} + +@keyframes insideAnimationReverse { + 0% { + clip-path: inset(0% 0% 0% 50%); + transform: translateX(0%); + opacity: 1; + } + 50% { + clip-path: inset(0% 0% 0% 50%); + } + 100% { + clip-path: inset(0% 0% 0% 0%); + transform: translateX(0%); + } +} + +@keyframes pageAnimationReverse { + 0% { + transform: translateX(55%) rotateY(-90deg); + padding: 0%; + display: none; + } + 1% { + display: flex; + } + 100% { + transform: translateX(0) rotateY(0); + } +} + +@keyframes pageTextAnimationReverse { + 0% { + transform: translateX(65%) rotateY(88deg); + display: none; + } + 100% { + transform: translateX(0) rotateY(0); + } +} + +@keyframes pageSecondTextAnimationReverse { + 0% { + transform: translateX(-50%); + display: none; + } + 50% { + transform: translateX(0%); + display: flex; + } + 75% { + transform: translateX(10%); + } + 100% { + transform: translateX(0%); + } +} + +@keyframes pageButtonAnimationReverse { + 0% { + opacity: 0; + } + 99% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.titleAnimationReverse { + display: flex; + animation: titleAnimationReverse .3s forwards; +} + +.titleContentAnimationReverse { + display: flex; + animation: titleContentAnimationReverse .3s forwards; +} + +.insideAnimationReverse { + animation: insideAnimationReverse .3s forwards; +} + +.pageAnimationReverse { + animation: pageAnimationReverse .15s forwards; +} + +.pageTextAnimationReverse { + animation: pageTextAnimationReverse .15s forwards; +} + +.pageSecondTextAnimationReverse { + animation: pageSecondTextAnimationReverse .3s forwards; +} + +.pageButtonAnimationReverse { + animation: pageButtonAnimationReverse .3s forwards +} + +@media screen and (min-width: 972px) and (max-width: 1024px) { + .textArea, .text { + font-size: 10px; + } +} + +@media screen and (max-width: 972px) { + .outSide { + width: 80%; + padding: 4% 0%; + } + .titleIcon { + width: 100%; + } + .titleContent span { + font-size: 10px; + } + .titleContent input { + font-size: 10px; + } + + .insideIcon { + width: 100%; + height: 40vh; + } + .inside { + padding: 9% 8%; + } + .insideHalfIcon { + display: none; + } + + .page { + width: 100%; + height: 100%; + } + .inside Button.controlPrev { + left: 10%; + bottom: 6%; + padding: 6%; + } + .inside Button.controlNext { + right: 15%; + bottom: 6%; + padding: 6%; + } + + .textArea, .text { + font-size: 10px; + } + /* Animations width < 768px */ + @keyframes titleAnimation { + 0% { + transform: translateX(-0%) rotateY(90deg); + } + 100% { + transform: translateX(-50%) rotateY(0); + } + } + @keyframes titleContentAnimation { + 0% { + transform: translateX(-0%) rotateY(90deg); + } + 100% { + transform: translateX(-50%) rotateY(0); + } + } + @keyframes insideAnimation { + 0% { + transform: translateX(0) rotateY(0); + } + 100% { + transform: translateX(50%) rotateY(-90deg); + } + } + + @keyframes pageTextAnimation { + 0% { + transform: translateX(0) rotateY(0); + } + 100% { + transform: translateX(50%) rotateY(-90deg); + display: none; + } + } + + /* Animations Reverse width < 768px */ + @keyframes titleAnimationReverse { + 0% { + transform: translateX(-60%) rotateY(0); + } + 75% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateX(0%) rotateY(90deg); + } + } + @keyframes titleContentAnimationReverse { + 0% { + transform: translateX(-60%) rotateY(0); + } + 100% { + transform: translateX(0%) rotateY(90deg); + } + } + @keyframes insideAnimationReverse { + 0% { + z-index: 5; + transform: translateX(50%) rotateY(-90deg); + } + 100% { + transform: translateX(0) rotateY(0); + } + } + + @keyframes pageTextAnimationReverse { + 0% { + transform: translateX(25%) rotateY(-20deg); + } + 100% { + transform: translateX(0) rotateY(0); + display: flex; + } + } +} + +@media screen and (max-width: 591px) { + .textArea .text { + font-size: 14px; + } +} + +@media screen and (max-height: 768px) { + .textArea, .text { + font-size: 5px; + } + .outSide { + padding: 4% 0%; + } + .titleContent span { + font-size: 5px; + } +} +@media screen and (max-height: 632px) { + .titleContent span { + font-size: 5px; + } +} +@media screen and (max-height: 392px) { + .insideIcon { + height: 40vh; + } +} diff --git a/src/react/Book.module.css.d.ts b/src/react/Book.module.css.d.ts new file mode 100644 index 00000000..06d3d007 --- /dev/null +++ b/src/react/Book.module.css.d.ts @@ -0,0 +1,38 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + actions: string; + blink: string; + bookContainer: string; + bookImages: string; + bookWrapper: string; + controlNext: string; + controlPrev: string; + hidden: string; + inside: string; + insideAnimation: string; + insideAnimationReverse: string; + insideHalfIcon: string; + insideIcon: string; + messageFormattedString: string; + outSide: string; + page: string; + pageAnimation: string; + pageAnimationReverse: string; + pageButtonAnimationReverse: string; + pageSecondTextAnimation: string; + pageSecondTextAnimationReverse: string; + pageTextAnimation: string; + pageTextAnimationReverse: string; + text: string; + textArea: string; + titleAnimation: string; + titleAnimationReverse: string; + titleContent: string; + titleContentAnimation: string; + titleContentAnimationReverse: string; + titleIcon: string; + uneditable: string; +} +declare const cssExports: CssExports; +export default cssExports; diff --git a/src/react/Book.stories.tsx b/src/react/Book.stories.tsx new file mode 100644 index 00000000..f772122d --- /dev/null +++ b/src/react/Book.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, Story } from '@storybook/react' +import Book, { BookProps } from './Book' + +export default { + title: 'Book', + component: Book, +} as Meta + +const Template: Story = (args) => + +export const Default = Template.bind({}) +Default.args = { + textPages: [ + 'Page 1: This is some text for page 1.', + 'Page 2: This is some text for page 2.', + 'Page 3: This is some text for page 3.', + 'Page 4: This is some text for page 4.', + 'Page 5: This is some text for page 5.', + ], + editable: true, + onSign: (pages, title) => console.log('Signed with pages:', pages, 'Title:', title), + onEdit: (pages) => console.log('Edit with pages:', pages), + onClose: () => console.log('Closed book'), + author: 'Author' +} diff --git a/src/react/Book.tsx b/src/react/Book.tsx new file mode 100644 index 00000000..0c53a221 --- /dev/null +++ b/src/react/Book.tsx @@ -0,0 +1,323 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react' +import insideIcon from './book_icons/book.webp' +import insideHalfIcon from './book_icons/book-half.webp' +import singlePageInsideIcon from './book_icons/notebook.webp' +import titleIcon from './book_icons/title.webp' +import styles from './Book.module.css' +import Button from './Button' +import MessageFormattedString from './MessageFormattedString' + +export interface BookProps { + textPages: string[] + editable: boolean + onSign: (textPages: string[], title: string) => void + onEdit: (textPages: string[]) => void + onClose: () => void + author: string +} + +const Book: React.FC = ({ textPages, editable, onSign, onEdit, onClose, author }) => { + const [pages, setPages] = useState(textPages) + const [currentPage, setCurrentPage] = useState(0) + const [isSinglePage, setIsSinglePage] = useState(window.innerWidth < 972) + const [insideImage, setInsideImage] = useState(window.innerWidth < 972 ? singlePageInsideIcon : insideIcon) + const [animateInsideIcon, setAnimateInsideIcon] = useState(0) + const [animatePageIcon, setAnimatePageIcon] = useState(0) + const [animateTitleIcon, setAnimateTitleIcon] = useState(0) + const [signClickedOnce, setSignClickedOnce] = useState(false) + const textAreaRefs = useRef([]) + const inputRef = useRef(null) + + const handleResize = useCallback(() => { + const isSingle = window.innerWidth < 972 + setIsSinglePage(isSingle) + setInsideImage(isSingle ? singlePageInsideIcon : insideIcon) + }, []) + + useEffect(() => { + handleResize() + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [handleResize]) + + useEffect(() => { + const index = currentPage * (isSinglePage ? 1 : 2) + if (textAreaRefs.current[index]) textAreaRefs.current[index].focus() + }, [currentPage, isSinglePage]) + + useEffect(() => { + if (signClickedOnce) { + setTimeout(() => { + inputRef.current!.focus() + }, 300) // wait for animation + } + }, [signClickedOnce]) + + const handlePageChange = (direction: number) => { + setCurrentPage((prevPage) => Math.min(Math.max(prevPage + direction, 0), Math.ceil(pages.length / (isSinglePage ? 1 : 2)) - 1)) + } + + const updatePage = (index, text) => { + setPages((prevPages) => { + const updatedPages = [...prevPages] + updatedPages[index] = text + return updatedPages + }) + } + + const handleTextChange = (e, pageIndex) => { + const text = e.target.value + updatePage(pageIndex, text) + + const nextPageIndex = pageIndex + 1 + const isMaxLengthReached = text.length >= e.target.maxLength + + if (isMaxLengthReached) { + if (nextPageIndex < pages.length) { + setCurrentPage(Math.floor(nextPageIndex / (isSinglePage ? 1 : 2))) + } else { + setPages((prevPages) => [...prevPages, '']) + setCurrentPage(Math.floor(nextPageIndex / (isSinglePage ? 1 : 2))) + } + textAreaRefs.current[nextPageIndex]?.focus() + } else if (text === '' && pageIndex > 0 && e.nativeEvent.inputType === 'deleteContentBackward') { + setCurrentPage(Math.floor((pageIndex - 1) / (isSinglePage ? 1 : 2))) + textAreaRefs.current[pageIndex - 1]?.focus() + } + } + + useEffect(() => { + const index = currentPage * (isSinglePage ? 1 : 2) + textAreaRefs.current[index]?.focus() + }, [currentPage, isSinglePage]) + + const handlePaste = (e: React.ClipboardEvent, pageIndex: number) => { + const pasteText = e.clipboardData.getData('text') + const updatedPages = [...pages] + const currentText = updatedPages[pageIndex] + const selectionStart = e.currentTarget.selectionStart || 0 + const selectionEnd = e.currentTarget.selectionEnd || 0 + + const newText = currentText.slice(0, selectionStart) + pasteText + currentText.slice(selectionEnd) + updatedPages[pageIndex] = newText + setPages(updatedPages) + + if (newText.length > e.currentTarget.maxLength) { + const remainingText = newText.slice(e.currentTarget.maxLength) + updatedPages[pageIndex] = newText.slice(0, e.currentTarget.maxLength) + setPages(updatedPages) + + const nextPageIndex = pageIndex + 1 + + if (nextPageIndex < pages.length) { + handlePasteRemainingText(remainingText, nextPageIndex) + } else { + setPages((prevPages) => [...prevPages, remainingText]) + setCurrentPage(Math.floor(nextPageIndex / (isSinglePage ? 1 : 2))) + focusOnTextArea(nextPageIndex) + } + } + } + + const handlePasteRemainingText = (remainingText: string, nextPageIndex: number) => { + const updatedPages = [...pages] + updatedPages[nextPageIndex] = remainingText + setPages(updatedPages) + focusOnTextArea(nextPageIndex) + } + + const focusOnTextArea = (index: number) => { + setTimeout(() => { + textAreaRefs.current[index]?.focus() + }, 0) + } + + const handleSign = useCallback(() => { + if (editable && signClickedOnce) { + const title = inputRef.current?.value || '' + onSign(pages, title) + } + setSignClickedOnce(true) + setAnimatePageIcon(1) + setAnimateInsideIcon(1) + setTimeout(() => { + setAnimateTitleIcon(1) + }, 150) + }, [pages, onSign, editable, signClickedOnce]) + + const handleEdit = useCallback(() => { + setSignClickedOnce(false) + onEdit(pages) + }, [pages, onEdit]) + + const handleCancel = useCallback(() => { + if (signClickedOnce) { + setSignClickedOnce(false) + setAnimateTitleIcon(2) + setTimeout(() => { + setAnimateInsideIcon(2) + setTimeout(() => { + setAnimatePageIcon(2) + }, 150) + }, 150) + } else { + onClose() + } + }, [signClickedOnce, onClose]) + + const setRef = (index: number) => (el: HTMLTextAreaElement | null) => { + textAreaRefs.current[index] = el! + } + + const getAnimationClass = (animationState, baseClass) => { + switch (animationState) { + case 1: + return `${baseClass} ${styles.pageAnimation}` + case 2: + return `${baseClass} ${styles.pageAnimationReverse}` + default: + return baseClass + } + } + + const renderPage = (index) => ( +
+ {editable ? ( +