Compare commits
3 commits
next
...
autodeploy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c71d7d90e | ||
|
|
b285b42d78 | ||
|
|
5775bd435d |
481 changed files with 37967 additions and 53909 deletions
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
description: Restricts usage of the global Mineflayer `bot` variable to only src/ files; prohibits usage in renderer/. Specifies correct usage of player state and appViewer globals.
|
|
||||||
globs: src/**/*.ts,renderer/**/*.ts
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
Ask AI
|
|
||||||
|
|
||||||
- The global variable `bot` refers to the Mineflayer bot instance.
|
|
||||||
- You may use `bot` directly in any file under the `src/` directory (e.g., `src/mineflayer/playerState.ts`).
|
|
||||||
- Do **not** use `bot` directly in any file under the `renderer/` directory or its subfolders (e.g., `renderer/viewer/three/worldrendererThree.ts`).
|
|
||||||
- In renderer code, all bot/player state and events must be accessed via explicit interfaces, state managers, or passed-in objects, never by referencing `bot` directly.
|
|
||||||
- In renderer code (such as in `WorldRendererThree`), use the `playerState` property (e.g., `worldRenderer.playerState.gameMode`) to access player state. The implementation for `playerState` lives in `src/mineflayer/playerState.ts`.
|
|
||||||
- In `src/` code, you may use the global variable `appViewer` from `src/appViewer.ts` directly. Do **not** import `appViewer` or use `window.appViewer`; use the global `appViewer` variable as-is.
|
|
||||||
- Some other global variables that can be used without window prefixes are listed in src/globals.d.ts
|
|
||||||
|
|
||||||
Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state.
|
|
||||||
|
|
||||||
For more general project contributing guides see CONTRIBUTING.md on like how to setup the project. Use pnpm tsc if needed to validate result with typechecking the whole project.
|
|
||||||
|
|
@ -6,4 +6,3 @@ generated
|
||||||
dist
|
dist
|
||||||
public
|
public
|
||||||
**/*/rsbuildSharedConfig.ts
|
**/*/rsbuildSharedConfig.ts
|
||||||
src/mcDataTypes.ts
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
// ],
|
// ],
|
||||||
"@stylistic/arrow-spacing": "error",
|
"@stylistic/arrow-spacing": "error",
|
||||||
"@stylistic/block-spacing": "error",
|
"@stylistic/block-spacing": "error",
|
||||||
"@typescript-eslint/no-this-alias": "off",
|
|
||||||
"@stylistic/brace-style": [
|
"@stylistic/brace-style": [
|
||||||
"error",
|
"error",
|
||||||
"1tbs",
|
"1tbs",
|
||||||
|
|
@ -103,7 +102,6 @@
|
||||||
// "@stylistic/multiline-ternary": "error", // not needed
|
// "@stylistic/multiline-ternary": "error", // not needed
|
||||||
// "@stylistic/newline-per-chained-call": "error", // not sure if needed
|
// "@stylistic/newline-per-chained-call": "error", // not sure if needed
|
||||||
"@stylistic/new-parens": "error",
|
"@stylistic/new-parens": "error",
|
||||||
"@typescript-eslint/class-literal-property-style": "off",
|
|
||||||
"@stylistic/no-confusing-arrow": "error",
|
"@stylistic/no-confusing-arrow": "error",
|
||||||
"@stylistic/wrap-iife": "error",
|
"@stylistic/wrap-iife": "error",
|
||||||
"@stylistic/space-before-blocks": "error",
|
"@stylistic/space-before-blocks": "error",
|
||||||
|
|
@ -199,8 +197,7 @@
|
||||||
"no-async-promise-executor": "off",
|
"no-async-promise-executor": "off",
|
||||||
"no-bitwise": "off",
|
"no-bitwise": "off",
|
||||||
"unicorn/filename-case": "off",
|
"unicorn/filename-case": "off",
|
||||||
"max-depth": "off",
|
"max-depth": "off"
|
||||||
"unicorn/no-typeof-undefined": "off"
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
59
.github/workflows/benchmark.yml
vendored
59
.github/workflows/benchmark.yml
vendored
|
|
@ -1,59 +0,0 @@
|
||||||
name: Benchmark
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- perf-test
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
(github.event_name == 'push' && github.ref == 'refs/heads/perf-test') ||
|
|
||||||
(
|
|
||||||
github.event_name == 'issue_comment' &&
|
|
||||||
github.event.issue.pull_request != '' &&
|
|
||||||
(startsWith(github.event.comment.body, '/benchmark'))
|
|
||||||
)
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- run: lscpu
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: "pnpm"
|
|
||||||
- name: Move Cypress to dependencies
|
|
||||||
run: |
|
|
||||||
jq '.dependencies.cypress = .optionalDependencies.cypress | del(.optionalDependencies.cypress)' package.json > package.json.tmp
|
|
||||||
mv package.json.tmp package.json
|
|
||||||
- run: pnpm install --no-frozen-lockfile
|
|
||||||
|
|
||||||
- run: pnpm build
|
|
||||||
- run: nohup pnpm prod-start &
|
|
||||||
|
|
||||||
- run: pnpm test:benchmark
|
|
||||||
id: benchmark
|
|
||||||
continue-on-error: true
|
|
||||||
# read benchmark results from stdout
|
|
||||||
- run: |
|
|
||||||
if [ -f benchmark.txt ]; then
|
|
||||||
# Format the benchmark results for GitHub comment
|
|
||||||
BENCHMARK_RESULT=$(cat benchmark.txt | sed 's/^/- /')
|
|
||||||
echo "BENCHMARK_RESULT<<EOF" >> $GITHUB_ENV
|
|
||||||
echo "$BENCHMARK_RESULT" >> $GITHUB_ENV
|
|
||||||
echo "EOF" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "BENCHMARK_RESULT=Benchmark failed to run or produce results" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@v2
|
|
||||||
with:
|
|
||||||
allow-repeats: true
|
|
||||||
message: |
|
|
||||||
Benchmark result: ${{ env.BENCHMARK_RESULT }}
|
|
||||||
33
.github/workflows/build-single-file.yml
vendored
33
.github/workflows/build-single-file.yml
vendored
|
|
@ -1,33 +0,0 @@
|
||||||
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
|
|
||||||
45
.github/workflows/build-zip.yml
vendored
45
.github/workflows/build-zip.yml
vendored
|
|
@ -1,45 +0,0 @@
|
||||||
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
|
|
||||||
201
.github/workflows/ci.yml
vendored
201
.github/workflows/ci.yml
vendored
|
|
@ -2,7 +2,7 @@ name: CI
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
qualilty-checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -13,70 +13,25 @@ jobs:
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
java-package: jre
|
java-package: jre
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm i -g pnpm@9.0.4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 18
|
||||||
# cache: "pnpm"
|
# cache: "pnpm"
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm build-single-file
|
|
||||||
- name: Store minecraft.html size
|
|
||||||
run: |
|
|
||||||
SIZE_BYTES=$(du -s dist/single/minecraft.html 2>/dev/null | cut -f1)
|
|
||||||
echo "SIZE_BYTES=$SIZE_BYTES" >> $GITHUB_ENV
|
|
||||||
- run: pnpm check-build
|
- 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-playground
|
||||||
# - run: pnpm build-storybook
|
- run: pnpm build-storybook
|
||||||
- run: pnpm test-unit
|
- run: pnpm test-unit
|
||||||
- run: pnpm lint
|
- run: pnpm lint
|
||||||
|
- run: pnpm tsx scripts/buildNpmReact.ts
|
||||||
- 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 prod-start &
|
||||||
- run: nohup pnpm test-mc-server &
|
- run: nohup pnpm test-mc-server &
|
||||||
- uses: cypress-io/github-action@v5
|
- uses: cypress-io/github-action@v5
|
||||||
with:
|
with:
|
||||||
install: false
|
install: false
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-images
|
name: cypress-images
|
||||||
|
|
@ -86,92 +41,56 @@ jobs:
|
||||||
# env:
|
# env:
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# - name: Store Bundle Stats
|
vercel-auto-deploy:
|
||||||
# if: github.event.pull_request.base.ref == 'next'
|
runs-on: ubuntu-latest
|
||||||
# uses: actions/github-script@v6
|
if: >-
|
||||||
# env:
|
env.AUTO_DEPLOY_PRS contains $'\n' + toString(github.event.pull_request.number) + $'\n' ||
|
||||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
startsWith(env.AUTO_DEPLOY_PRS, toString(github.event.pull_request.number) + $'\n') ||
|
||||||
# with:
|
endsWith(env.AUTO_DEPLOY_PRS, $'\n' + toString(github.event.pull_request.number)) ||
|
||||||
# script: |
|
env.AUTO_DEPLOY_PRS == toString(github.event.pull_request.number)
|
||||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
# async function getGistContent() {
|
steps:
|
||||||
# const { data } = await github.rest.gists.get({
|
- name: Checkout
|
||||||
# gist_id: gistId,
|
uses: actions/checkout@v2
|
||||||
# headers: {
|
- run: npm i -g pnpm@9.0.4
|
||||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
- uses: actions/setup-node@v4
|
||||||
# }
|
with:
|
||||||
# });
|
node-version: 18
|
||||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
cache: "pnpm"
|
||||||
# }
|
- name: Install Global Dependencies
|
||||||
|
run: npm install --global vercel
|
||||||
# async function updateGistContent(content) {
|
- name: Pull Vercel Environment Information
|
||||||
# await github.rest.gists.update({
|
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
# gist_id: gistId,
|
- name: Build Project Artifacts
|
||||||
# headers: {
|
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
- run: pnpm build-storybook
|
||||||
# },
|
- name: Copy playground files
|
||||||
# files: {
|
run: |
|
||||||
# 'bundle-stats.json': {
|
mkdir -p .vercel/output/static/playground
|
||||||
# content: JSON.stringify(content, null, 2)
|
pnpm build-playground
|
||||||
# }
|
cp -r prismarine-viewer/dist/* .vercel/output/static/playground/
|
||||||
# }
|
- name: Download Generated Sounds map
|
||||||
# });
|
run: node scripts/downloadSoundsMap.mjs
|
||||||
# }
|
- name: Deploy Project Artifacts to Vercel
|
||||||
|
uses: mathiasvr/command-output@v2.0.0
|
||||||
# const stats = require('/tmp/bundle-stats.json');
|
with:
|
||||||
# const content = await getGistContent();
|
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
# content['${{ github.event.pull_request.base.ref }}'] = stats;
|
id: deploy
|
||||||
# await updateGistContent(content);
|
- uses: mshick/add-pr-comment@v2
|
||||||
|
with:
|
||||||
# - name: Update PR Description
|
allow-repeats: true
|
||||||
# uses: actions/github-script@v6
|
message: |
|
||||||
# with:
|
Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||||
# script: |
|
[Playground](${{ steps.deploy.outputs.stdout }}/playground/)
|
||||||
# const { data: pr } = await github.rest.pulls.get({
|
[Storybook](${{ steps.deploy.outputs.stdout }}/storybook/)
|
||||||
# owner: context.repo.owner,
|
# - run: git checkout next scripts/githubActions.mjs
|
||||||
# repo: context.repo.repo,
|
- name: Get deployment alias
|
||||||
# pull_number: context.issue.number
|
run: node scripts/githubActions.mjs getAlias
|
||||||
# });
|
id: alias
|
||||||
|
env:
|
||||||
# let body = pr.body || '';
|
ALIASES: ${{ env.ALIASES }}
|
||||||
# const statsMarker = '### Bundle Size';
|
PULL_URL: ${{ github.event.issue.pull_request.url }}
|
||||||
# const comparison = '${{ steps.compare.outputs.stats }}';
|
- name: Set deployment alias
|
||||||
|
if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }}
|
||||||
# if (body.includes(statsMarker)) {
|
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ steps.alias.outputs.alias }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||||
# 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
|
|
||||||
|
|
|
||||||
6
.github/workflows/fix-lint.yml
vendored
6
.github/workflows/fix-lint.yml
vendored
|
|
@ -16,12 +16,6 @@ jobs:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
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
|
- run: pnpm lint --fix
|
||||||
- name: Push Changes
|
- name: Push Changes
|
||||||
uses: ad-m/github-push-action@master
|
uses: ad-m/github-push-action@master
|
||||||
|
|
|
||||||
70
.github/workflows/next-deploy.yml
vendored
70
.github/workflows/next-deploy.yml
vendored
|
|
@ -16,76 +16,28 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
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
|
- name: Install Global Dependencies
|
||||||
run: pnpm add -g vercel
|
run: npm install --global vercel pnpm@9.0.4
|
||||||
- name: Install Dependencies
|
|
||||||
run: pnpm install
|
|
||||||
- name: Pull Vercel Environment Information
|
- name: Pull Vercel Environment Information
|
||||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
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
|
- name: Build Project Artifacts
|
||||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
env:
|
- run: pnpm build-storybook
|
||||||
CONFIG_JSON_SOURCE: BUNDLED
|
|
||||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
|
||||||
- name: Copy playground files
|
- name: Copy playground files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .vercel/output/static/playground
|
mkdir -p .vercel/output/static/playground
|
||||||
pnpm build-playground
|
pnpm build-playground
|
||||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
cp -r prismarine-viewer/dist/* .vercel/output/static/playground/
|
||||||
|
- name: Download Generated Sounds map
|
||||||
|
run: node scripts/downloadSoundsMap.mjs
|
||||||
- name: Deploy Project Artifacts to Vercel
|
- name: Deploy Project Artifacts to Vercel
|
||||||
uses: mathiasvr/command-output@v2.0.0
|
uses: mathiasvr/command-output@v2.0.0
|
||||||
with:
|
with:
|
||||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
id: deploy
|
id: deploy
|
||||||
- name: Start servers for testing
|
- name: Set deployment alias
|
||||||
run: |
|
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ secrets.TEST_PREVIEW_DOMAIN }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||||
nohup pnpm prod-start &
|
# - uses: mshick/add-pr-comment@v2
|
||||||
nohup pnpm test-mc-server &
|
# with:
|
||||||
- name: Run Cypress smoke tests
|
# message: |
|
||||||
uses: cypress-io/github-action@v5
|
# Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||||
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!',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
87
.github/workflows/preview.yml
vendored
87
.github/workflows/preview.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: Vercel PR Deploy (Preview)
|
name: Vercel Deploy Preview
|
||||||
env:
|
env:
|
||||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
|
@ -6,99 +6,47 @@ env:
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
pull_request_target:
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# todo skip already created deploys on that commit
|
||||||
if: >-
|
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:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Base To Temp
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
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:
|
with:
|
||||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||||
- name: Checkout PR (pull_request)
|
- run: npm i -g pnpm@9.0.4
|
||||||
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
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 18
|
||||||
cache: "pnpm"
|
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
|
- name: Install Global Dependencies
|
||||||
run: pnpm add -g vercel
|
run: npm install --global vercel
|
||||||
- name: Pull Vercel Environment Information
|
- name: Pull Vercel Environment Information
|
||||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
- 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
|
- name: Build Project Artifacts
|
||||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
env:
|
- run: pnpm build-storybook
|
||||||
CONFIG_JSON_SOURCE: BUNDLED
|
|
||||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
|
||||||
- name: Copy playground files
|
- name: Copy playground files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p .vercel/output/static/playground
|
mkdir -p .vercel/output/static/playground
|
||||||
pnpm build-playground
|
pnpm build-playground
|
||||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
cp -r prismarine-viewer/dist/* .vercel/output/static/playground/
|
||||||
- name: Write pr redirect index.html
|
- name: Download Generated Sounds map
|
||||||
run: |
|
run: node scripts/downloadSoundsMap.mjs
|
||||||
mkdir -p .vercel/output/static/pr
|
|
||||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}'>" > .vercel/output/static/pr/index.html
|
|
||||||
- name: Write commit redirect index.html
|
|
||||||
run: |
|
|
||||||
mkdir -p .vercel/output/static/commit
|
|
||||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}/commits/${{ github.event.pull_request.head.sha }}'>" > .vercel/output/static/commit/index.html
|
|
||||||
- name: Deploy Project Artifacts to Vercel
|
- name: Deploy Project Artifacts to Vercel
|
||||||
uses: mathiasvr/command-output@v2.0.0
|
uses: mathiasvr/command-output@v2.0.0
|
||||||
with:
|
with:
|
||||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
id: deploy
|
id: deploy
|
||||||
- uses: mshick/add-pr-comment@v2
|
- uses: mshick/add-pr-comment@v2
|
||||||
# if: github.event_name == 'issue_comment'
|
|
||||||
with:
|
with:
|
||||||
allow-repeats: true
|
allow-repeats: true
|
||||||
message: |
|
message: |
|
||||||
|
|
@ -106,9 +54,12 @@ jobs:
|
||||||
[Playground](${{ steps.deploy.outputs.stdout }}/playground/)
|
[Playground](${{ steps.deploy.outputs.stdout }}/playground/)
|
||||||
[Storybook](${{ steps.deploy.outputs.stdout }}/storybook/)
|
[Storybook](${{ steps.deploy.outputs.stdout }}/storybook/)
|
||||||
# - run: git checkout next scripts/githubActions.mjs
|
# - run: git checkout next scripts/githubActions.mjs
|
||||||
|
- name: Get deployment alias
|
||||||
|
run: node scripts/githubActions.mjs getAlias
|
||||||
|
id: alias
|
||||||
|
env:
|
||||||
|
ALIASES: ${{ env.ALIASES }}
|
||||||
|
PULL_URL: ${{ github.event.issue.pull_request.url }}
|
||||||
- name: Set deployment alias
|
- name: Set deployment alias
|
||||||
if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }}
|
if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }}
|
||||||
run: |
|
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ steps.alias.outputs.alias }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||||
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
|
|
||||||
|
|
|
||||||
53
.github/workflows/publish.yml
vendored
Normal file
53
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
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
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm i -g vercel pnpm@9.0.4
|
||||||
|
# - 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
|
||||||
|
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||||
|
- run: pnpm build-storybook
|
||||||
|
- name: Copy playground files
|
||||||
|
run: |
|
||||||
|
mkdir -p .vercel/output/static/playground
|
||||||
|
pnpm build-playground
|
||||||
|
cp -r prismarine-viewer/dist/* .vercel/output/static/playground/
|
||||||
|
- name: Download Generated Sounds map
|
||||||
|
run: node scripts/downloadSoundsMap.mjs
|
||||||
|
- name: Deploy Project to Vercel
|
||||||
|
uses: mathiasvr/command-output@v2.0.0
|
||||||
|
with:
|
||||||
|
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||||
|
id: deploy
|
||||||
|
- run: |
|
||||||
|
pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# has possible output: tag
|
||||||
|
id: release
|
||||||
|
# has output
|
||||||
|
- 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
|
||||||
|
- run: pnpm tsx scripts/buildNpmReact.ts ${{ steps.release.outputs.tag }}
|
||||||
|
if: steps.release.outputs.tag
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
116
.github/workflows/release.yml
vendored
116
.github/workflows/release.yml
vendored
|
|
@ -1,116 +0,0 @@
|
||||||
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 <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title>
|
|
||||||
sed -i 's/<title>Minecraft Web Client<\/title>/<title>Minecraft Web Client — Free Online Browser Version<\/title>/' .vercel/output/static/index.html
|
|
||||||
|
|
||||||
- name: Deploy Project to Vercel
|
|
||||||
uses: mathiasvr/command-output@v2.0.0
|
|
||||||
with:
|
|
||||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
|
||||||
id: deploy
|
|
||||||
- name: Get releasing alias
|
|
||||||
run: node scripts/githubActions.mjs getReleasingAlias
|
|
||||||
id: alias
|
|
||||||
- name: Set deployment alias
|
|
||||||
run: |
|
|
||||||
for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do
|
|
||||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Build single-file version - minecraft.html
|
|
||||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
|
||||||
- name: Build self-host version
|
|
||||||
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 }}
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -10,7 +10,7 @@ localSettings.mjs
|
||||||
dist*
|
dist*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
/world
|
world
|
||||||
data*.json
|
data*.json
|
||||||
out
|
out
|
||||||
*.iml
|
*.iml
|
||||||
|
|
@ -18,7 +18,5 @@ out
|
||||||
generated
|
generated
|
||||||
storybook-static
|
storybook-static
|
||||||
server-jar
|
server-jar
|
||||||
config.local.json
|
|
||||||
logs/
|
|
||||||
|
|
||||||
src/react/npmReactComponents.ts
|
src/react/npmReactComponents.ts
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,12 @@
|
||||||
|
|
||||||
After forking the repository, run the following commands to get started:
|
After forking the repository, run the following commands to get started:
|
||||||
|
|
||||||
0. Ensure you have [Node.js](https://nodejs.org) installed. Enable corepack with `corepack enable` *(1).
|
0. Ensure you have [Node.js](https://nodejs.org) and `pnpm` installed. To install pnpm run `npm i -g pnpm@9.0.4`.
|
||||||
1. Install dependencies: `pnpm i`
|
1. Install dependencies: `pnpm i`
|
||||||
2. Start the project in development mode: `pnpm start` or build the project for production: `pnpm build`
|
2. Start the project in development mode: `pnpm start`
|
||||||
3. Read the [Tasks Categories](#tasks-categories) and [Workflow](#workflow) sections below
|
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!
|
4. Let us know if you are working on something and be sure to open a PR if you got any changes. Happy coding!
|
||||||
|
|
||||||
*(1): If you are getting `Cannot find matching keyid` update corepack to the latest version with `npm i -g corepack`.
|
|
||||||
|
|
||||||
*(2): If still something doesn't work ensure you have the right nodejs version with `node -v` (tested on 22.x)
|
|
||||||
|
|
||||||
<!-- *(3): For GitHub codespaces (cloud ide): Run `pnpm i @rsbuild/core@1.2.4 @rsbuild/plugin-node-polyfill@1.3.0 @rsbuild/plugin-react@1.1.0 @rsbuild/plugin-typed-css-modules@1.0.2` command to avoid crashes because of limited ram -->
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
There are 3 main parts of the project:
|
There are 3 main parts of the project:
|
||||||
|
|
@ -33,11 +27,11 @@ Paths:
|
||||||
- `src` - main app source code
|
- `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.
|
- `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`)
|
### Renderer: Playground & Mesher (`prismarine-viewer`)
|
||||||
|
|
||||||
- Playground Scripts:
|
- Playground Scripts:
|
||||||
- Start: `pnpm run-playground` (playground, mesher + server) or `pnpm watch-playground`
|
- Start: `pnpm run-playground` (playground, mesher + server) or `pnpm watch-playground`
|
||||||
- Build: `pnpm build-playground` or `node renderer/esbuild.mjs`
|
- Build: `pnpm build-playground` or `node prismarine-viewer/esbuild.mjs`
|
||||||
|
|
||||||
- Mesher Scripts:
|
- Mesher Scripts:
|
||||||
- Start: `pnpm watch-mesher`
|
- Start: `pnpm watch-mesher`
|
||||||
|
|
@ -45,10 +39,10 @@ Paths:
|
||||||
|
|
||||||
Paths:
|
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:
|
- `prismarine-viewer` - 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)
|
- `prismarine-viewer/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`
|
- `prismarine-viewer/viewer/lib/models.ts` - preparing data for rendering (blocks) - happens in worker: out file - `worker.js`, building - `prismarine-viewer/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.
|
- `prismarine-viewer/examples/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 (`.storybook`)
|
||||||
|
|
||||||
|
|
@ -76,7 +70,7 @@ Cypress tests are located in `cypress` folder. To run them, run `pnpm test-mc-se
|
||||||
## Unit Tests
|
## Unit Tests
|
||||||
|
|
||||||
There are not many unit tests for now (which we are trying to improve).
|
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.
|
Location of unit tests: `**/*.test.ts` files in `src` folder and `prismarine-viewer` folder.
|
||||||
Start them with `pnpm test-unit`.
|
Start them with `pnpm test-unit`.
|
||||||
|
|
||||||
## Making protocol-related changes
|
## Making protocol-related changes
|
||||||
|
|
@ -177,13 +171,8 @@ New React components, improve UI (including mobile support).
|
||||||
|
|
||||||
## Updating Dependencies
|
## Updating Dependencies
|
||||||
|
|
||||||
1. Use `pnpm update-git-deps` to check and update git dependencies (like mineflayer fork, prismarine packages etc). The script will:
|
1. Ensure mineflayer fork is up to date with the latest version of mineflayer original repo
|
||||||
- Show which git dependencies have updates available
|
|
||||||
- Ask if you want to update them
|
|
||||||
- Skip dependencies listed in `pnpm.updateConfig.ignoreDependencies`
|
|
||||||
|
|
||||||
2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ...
|
2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ...
|
||||||
|
|
||||||
3. If `minecraft-protocol` patch fails, do this:
|
3. If `minecraft-protocol` patch fails, do this:
|
||||||
1. Remove the patch from `patchedDependencies` in `package.json`
|
1. Remove the patch from `patchedDependencies` in `package.json`
|
||||||
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
||||||
|
|
|
||||||
19
Dockerfile
19
Dockerfile
|
|
@ -4,28 +4,20 @@ FROM node:18-alpine AS build
|
||||||
RUN apk add git
|
RUN apk add git
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
# install pnpm with corepack
|
# install pnpm
|
||||||
RUN corepack enable
|
RUN npm i -g pnpm@9.0.4
|
||||||
# Build arguments
|
|
||||||
ARG DOWNLOAD_SOUNDS=false
|
|
||||||
ARG DISABLE_SERVICE_WORKER=false
|
|
||||||
ARG CONFIG_JSON_SOURCE=REMOTE
|
|
||||||
# TODO need flat --no-root-optional
|
# TODO need flat --no-root-optional
|
||||||
RUN node ./scripts/dockerPrepare.mjs
|
RUN node ./scripts/dockerPrepare.mjs
|
||||||
RUN pnpm i
|
RUN pnpm i
|
||||||
# Download sounds if flag is enabled
|
|
||||||
RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs ; fi
|
|
||||||
|
|
||||||
# TODO for development
|
# TODO for development
|
||||||
# EXPOSE 9090
|
# EXPOSE 9090
|
||||||
# VOLUME /app/src
|
# VOLUME /app/src
|
||||||
# VOLUME /app/renderer
|
# VOLUME /app/prismarine-viewer
|
||||||
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
||||||
|
|
||||||
# only for prod
|
# only for prod
|
||||||
RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
|
RUN pnpm run build
|
||||||
CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ---- Run Stage ----
|
# ---- Run Stage ----
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
|
|
@ -35,9 +27,8 @@ WORKDIR /app
|
||||||
COPY --from=build /app/dist /app/dist
|
COPY --from=build /app/dist /app/dist
|
||||||
COPY server.js /app/server.js
|
COPY server.js /app/server.js
|
||||||
# Install express
|
# Install express
|
||||||
RUN npm i -g pnpm@10.8.0
|
RUN npm i -g pnpm@9.0.4
|
||||||
RUN npm init -yp
|
RUN npm init -yp
|
||||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
VOLUME /app/public
|
|
||||||
ENTRYPOINT ["node", "server.js", "--prod"]
|
ENTRYPOINT ["node", "server.js", "--prod"]
|
||||||
|
|
|
||||||
85
README.MD
85
README.MD
|
|
@ -2,66 +2,34 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Minecraft **clone** rewritten in TypeScript using the best modern web technologies. Minecraft vanilla-compatible client and integrated server packaged into a single web app.
|
A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using the best modern web technologies.
|
||||||
|
|
||||||
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)
|
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). If you encounter any bugs or usability issues, please report them!
|
||||||
|
|
||||||
Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, meanwhile this project is aimed for *device-compatiiblity* and better performance so it feels portable, flexible and lightweight. It's also a very strong example on how to build true HTML games for the web at scale entirely with the JS ecosystem. Have fun!
|
|
||||||
|
|
||||||
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft).
|
|
||||||
|
|
||||||
> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere)
|
|
||||||
|
|
||||||
### Big Features
|
### Big Features
|
||||||
|
|
||||||
- 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!
|
- Open any zip world file or even folder in read-write mode!
|
||||||
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
|
- 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!
|
- Singleplayer mode with simple world generations!
|
||||||
|
- Google Drive support for reading / saving worlds
|
||||||
- Works offline
|
- Works offline
|
||||||
- First-class touch (mobile) & controller support
|
|
||||||
- First-class keybindings configuration
|
|
||||||
- Advanced Resource pack support: Custom GUI, all textures. Server resource packs are supported with proper CORS configuration.
|
|
||||||
- Builtin JEI with recipes & descriptions for 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)
|
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||||
- ~~Google Drive support for reading / saving worlds back to the cloud~~
|
- First-class touch (mobile) & controller support
|
||||||
- Support for custom rendering 3D engines. Modular architecture.
|
- Basic Resource pack support: Custom GUI, all textures. Server resource packs are not supported yet.
|
||||||
|
- Builtin JEI with recipes & guides for every item (also replaces creative inventory)
|
||||||
- even even more!
|
- even even more!
|
||||||
|
|
||||||
All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||||
|
|
||||||
### Recommended Settings
|
### Recommended Settings
|
||||||
|
|
||||||
- Controls -> **Touch Controls Type** -> **Joystick**
|
- Controls -> **Touch Controls Type** -> **Joystick**
|
||||||
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
- 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)
|
- 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)
|
- 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
|
### 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.
|
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.
|
||||||
|
|
@ -70,15 +38,11 @@ Whatever offline mode you used (zip, folder, just single player), you can always
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Servers & Proxy
|
### Servers
|
||||||
|
|
||||||
You can play almost on any Java server, vanilla servers are fully supported.
|
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).
|
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 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:
|
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.
|
||||||
|
|
||||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
|
||||||
|
|
||||||
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
|
||||||
|
|
||||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||||
|
|
||||||
|
|
@ -127,12 +91,12 @@ There is world renderer playground ([link](https://mcon.vercel.app/playground/))
|
||||||
|
|
||||||
However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples:
|
However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples:
|
||||||
|
|
||||||
- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam.
|
- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam.
|
||||||
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
||||||
|
|
||||||
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
||||||
- `world` - Three.js world instance, basically does all the rendering (part of renderer backend).
|
- `viewer` - Three.js viewer instance, basically does all the rendering.
|
||||||
- `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
- `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||||
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
||||||
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
||||||
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
||||||
|
|
@ -141,7 +105,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u
|
||||||
|
|
||||||
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
|
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
|
||||||
|
|
||||||
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `world.getCameraPosition()` to see the camera position and so on.
|
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `viewer.camera.position` to see the camera position and so on.
|
||||||
|
|
||||||
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
|
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
|
||||||
|
|
||||||
|
|
@ -162,12 +126,6 @@ Press `Y` to set query parameters to url of your current game state.
|
||||||
|
|
||||||
There are some parameters you can set in the url to archive some specific behaviors:
|
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:
|
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.
|
- `?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.
|
||||||
|
|
@ -176,14 +134,12 @@ Server specific:
|
||||||
- `?proxy=<proxy_address>` - Set the proxy server address to use for the server
|
- `?proxy=<proxy_address>` - Set the proxy server address to use for the server
|
||||||
- `?username=<username>` - Set the username 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.
|
- `?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.
|
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
|
||||||
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.
|
|
||||||
- `?addPing=<ping>` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping.
|
|
||||||
|
|
||||||
Single player specific:
|
Single player specific:
|
||||||
|
|
||||||
- `?loadSave=<save_name>` - Load the save on load with the specified folder name (not title)
|
- `?loadSave=<save_name>` - Load the save on load with the specified folder name (not title)
|
||||||
- `?singleplayer=1` or `?sp=1` - Create empty world on load. Nothing will be saved
|
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
|
||||||
- `?version=<version>` - Set the version for the singleplayer world (when used with `?singleplayer=1`)
|
- `?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.
|
- `?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.
|
- `?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.
|
||||||
|
|
@ -211,12 +167,12 @@ In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the
|
||||||
|
|
||||||
- `?mapDirBaseUrl` - See above.
|
- `?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. -->
|
<!-- - `?mapDirGuess=<base_url>` - Load the map from the provided URL and paths will be guessed with a few additional fetch requests. -->
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
### Notable Things that Power this Project
|
### 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
|
- [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked
|
||||||
|
|
@ -235,4 +191,3 @@ Only during development:
|
||||||
|
|
||||||
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
|
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
|
||||||
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
|
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
|
||||||
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun
|
||||||
pnpm i minecraft-react
|
pnpm i minecraft-react
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
|
||||||
58
TECH.md
58
TECH.md
|
|
@ -1,58 +0,0 @@
|
||||||
### 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) |
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Configure client</title>
|
|
||||||
<script>
|
|
||||||
function removeSettings() {
|
|
||||||
if (confirm('Are you sure you want to RESET ALL SETTINGS?')) {
|
|
||||||
localStorage.setItem('options', '{}');
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAllData() {
|
|
||||||
localStorage.removeItem('serversList')
|
|
||||||
localStorage.removeItem('serversHistory')
|
|
||||||
localStorage.removeItem('authenticatedAccounts')
|
|
||||||
localStorage.removeItem('modsAutoUpdateLastCheck')
|
|
||||||
localStorage.removeItem('firstModsPageVisit')
|
|
||||||
localStorage.removeItem('proxiesData')
|
|
||||||
localStorage.removeItem('keybindings')
|
|
||||||
localStorage.removeItem('username')
|
|
||||||
localStorage.removeItem('customCommands')
|
|
||||||
localStorage.removeItem('options')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="display: flex;gap: 10px;">
|
|
||||||
<button onclick="removeSettings()">Reset all settings</button>
|
|
||||||
<button onclick="removeAllData()">Remove all user data (but not mods or worlds)</button>
|
|
||||||
<!-- <button>Remove all user data (worlds, resourcepacks)</button> -->
|
|
||||||
<!-- <button>Remove all mods</button> -->
|
|
||||||
<!-- <button>Remove all mod repositories</button> -->
|
|
||||||
</div>
|
|
||||||
<input />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png
|
|
||||||
get file names from here (blocks/items) https://zardoy.github.io/mc-assets/
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Web Input Debugger</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
.key-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 60px);
|
|
||||||
gap: 5px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.key {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border: 2px solid #333;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
background: white;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.key.pressed {
|
|
||||||
background: #90EE90;
|
|
||||||
}
|
|
||||||
.key .duration {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.key .count {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 10px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.wasd-container {
|
|
||||||
position: relative;
|
|
||||||
width: 190px;
|
|
||||||
height: 130px;
|
|
||||||
}
|
|
||||||
#KeyW {
|
|
||||||
position: absolute;
|
|
||||||
left: 65px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
#KeyA {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 65px;
|
|
||||||
}
|
|
||||||
#KeyS {
|
|
||||||
position: absolute;
|
|
||||||
left: 65px;
|
|
||||||
top: 65px;
|
|
||||||
}
|
|
||||||
#KeyD {
|
|
||||||
position: absolute;
|
|
||||||
left: 130px;
|
|
||||||
top: 65px;
|
|
||||||
}
|
|
||||||
.space-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
#Space {
|
|
||||||
width: 190px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="controls">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="repeatMode"> Use keydown repeat mode (auto key-up after 150ms of no repeat)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wasd-container">
|
|
||||||
<div id="KeyW" class="key" data-code="KeyW">W</div>
|
|
||||||
<div id="KeyA" class="key" data-code="KeyA">A</div>
|
|
||||||
<div id="KeyS" class="key" data-code="KeyS">S</div>
|
|
||||||
<div id="KeyD" class="key" data-code="KeyD">D</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="key-container">
|
|
||||||
<div id="ControlLeft" class="key" data-code="ControlLeft">Ctrl</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-container">
|
|
||||||
<div id="Space" class="key" data-code="Space">Space</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const keys = {};
|
|
||||||
const keyStats = {};
|
|
||||||
const pressStartTimes = {};
|
|
||||||
const keyTimeouts = {};
|
|
||||||
|
|
||||||
function initKeyStats(code) {
|
|
||||||
if (!keyStats[code]) {
|
|
||||||
keyStats[code] = {
|
|
||||||
pressCount: 0,
|
|
||||||
duration: 0,
|
|
||||||
startTime: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateKeyVisuals(code) {
|
|
||||||
const element = document.getElementById(code);
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const stats = keyStats[code];
|
|
||||||
if (keys[code]) {
|
|
||||||
element.classList.add('pressed');
|
|
||||||
const currentDuration = ((Date.now() - stats.startTime) / 1000).toFixed(1);
|
|
||||||
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="duration">${currentDuration}s</span><span class="count">${stats.pressCount}</span>`;
|
|
||||||
} else {
|
|
||||||
element.classList.remove('pressed');
|
|
||||||
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="count">${stats.pressCount}</span>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function releaseKey(code) {
|
|
||||||
keys[code] = false;
|
|
||||||
if (pressStartTimes[code]) {
|
|
||||||
keyStats[code].duration += (Date.now() - pressStartTimes[code]) / 1000;
|
|
||||||
delete pressStartTimes[code];
|
|
||||||
}
|
|
||||||
updateKeyVisuals(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
|
||||||
const code = event.code;
|
|
||||||
const isRepeatMode = document.getElementById('repeatMode').checked;
|
|
||||||
|
|
||||||
initKeyStats(code);
|
|
||||||
|
|
||||||
// Clear any existing timeout for this key
|
|
||||||
if (keyTimeouts[code]) {
|
|
||||||
clearTimeout(keyTimeouts[code]);
|
|
||||||
delete keyTimeouts[code];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRepeatMode) {
|
|
||||||
// In repeat mode, always handle the keydown
|
|
||||||
if (!keys[code] || event.repeat) {
|
|
||||||
keys[code] = true;
|
|
||||||
if (!event.repeat) {
|
|
||||||
// Only increment count on initial press, not repeats
|
|
||||||
keyStats[code].pressCount++;
|
|
||||||
keyStats[code].startTime = Date.now();
|
|
||||||
pressStartTimes[code] = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set timeout to release key if no repeat events come
|
|
||||||
keyTimeouts[code] = setTimeout(() => {
|
|
||||||
releaseKey(code);
|
|
||||||
}, 150);
|
|
||||||
} else {
|
|
||||||
// In normal mode, only handle keydown if key is not already pressed
|
|
||||||
if (!keys[code]) {
|
|
||||||
keys[code] = true;
|
|
||||||
keyStats[code].pressCount++;
|
|
||||||
keyStats[code].startTime = Date.now();
|
|
||||||
pressStartTimes[code] = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateKeyVisuals(code);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyUp(event) {
|
|
||||||
const code = event.code;
|
|
||||||
const isRepeatMode = document.getElementById('repeatMode').checked;
|
|
||||||
|
|
||||||
if (!isRepeatMode) {
|
|
||||||
releaseKey(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize all monitored keys
|
|
||||||
const monitoredKeys = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ControlLeft', 'Space'];
|
|
||||||
monitoredKeys.forEach(code => {
|
|
||||||
initKeyStats(code);
|
|
||||||
const element = document.getElementById(code);
|
|
||||||
if (element) {
|
|
||||||
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="count">0</span>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start visual updates
|
|
||||||
setInterval(() => {
|
|
||||||
monitoredKeys.forEach(code => {
|
|
||||||
if (keys[code]) {
|
|
||||||
updateKeyVisuals(code);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
document.addEventListener('keyup', handleKeyUp);
|
|
||||||
|
|
||||||
// Handle mode changes
|
|
||||||
document.getElementById('repeatMode').addEventListener('change', () => {
|
|
||||||
// Release all keys when switching modes
|
|
||||||
monitoredKeys.forEach(code => {
|
|
||||||
if (keys[code]) {
|
|
||||||
releaseKey(code);
|
|
||||||
}
|
|
||||||
if (keyTimeouts[code]) {
|
|
||||||
clearTimeout(keyTimeouts[code]);
|
|
||||||
delete keyTimeouts[code];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Minecraft Web Client",
|
"name": "Prismarine Web Client",
|
||||||
"short_name": "Minecraft Web Client",
|
"short_name": "Prismarine Web Client",
|
||||||
"scope": "./",
|
"scope": "./",
|
||||||
"start_url": "./",
|
"start_url": "./",
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|
|
||||||
73
config.json
73
config.json
|
|
@ -1,80 +1,25 @@
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"defaultHost": "<from-proxy>",
|
"defaultHost": "<from-proxy>",
|
||||||
"defaultProxy": "https://proxy.mcraft.fun",
|
"defaultProxy": "proxy.mcraft.fun",
|
||||||
"mapsProvider": "https://maps.mcraft.fun/",
|
"mapsProvider": "https://maps.mcraft.fun/",
|
||||||
"skinTexturesProxy": "",
|
|
||||||
"peerJsServer": "",
|
"peerJsServer": "",
|
||||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||||
"promoteServers": [
|
"promoteServers": [
|
||||||
{
|
|
||||||
"ip": "wss://play.mcraft.fun"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ip": "wss://play.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",
|
"ip": "kaboom.pw",
|
||||||
"version": "1.20.3",
|
"version": "1.18.2",
|
||||||
"description": "Very nice a polite server. Must try for everyone!"
|
"description": "Chaos and destruction server. Free 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",
|
"ip": "play.applemc.fun",
|
||||||
"actionHold": "",
|
"version": "1.18.2",
|
||||||
"label": "S"
|
"description": "Very nice server. Try it now!"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"action": "general.debugOverlay",
|
"ip": "sus.shhnowisnottheti.me",
|
||||||
"actionHold": "general.debugOverlayHelpMenu",
|
"version": "1.18.2",
|
||||||
"label": "F3"
|
"description": "Creative, your own 'boxes' (islands)"
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "general.playersList",
|
|
||||||
"actionHold": "",
|
|
||||||
"icon": "pixelarticons:users",
|
|
||||||
"label": "TAB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "general.chat",
|
|
||||||
"actionHold": "",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "ui.pauseMenu",
|
|
||||||
"actionHold": "",
|
|
||||||
"label": ""
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"alwaysReconnectButton": true,
|
|
||||||
"reportBugButtonWithReconnect": true,
|
|
||||||
"allowAutoConnect": true
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { defineConfig } from 'cypress'
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true'
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
video: false,
|
video: false,
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
|
|
@ -34,7 +32,7 @@ export default defineConfig({
|
||||||
return require('./cypress/plugins/index.js')(on, config)
|
return require('./cypress/plugins/index.js')(on, config)
|
||||||
},
|
},
|
||||||
baseUrl: 'http://localhost:8080',
|
baseUrl: 'http://localhost:8080',
|
||||||
specPattern: !isPerformanceTest ? 'cypress/e2e/smoke.spec.ts' : 'cypress/e2e/rendering_performance.spec.ts',
|
specPattern: 'cypress/e2e/**/*.spec.ts',
|
||||||
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
|
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,14 @@ it('Loads & renders singleplayer', () => {
|
||||||
testWorldLoad()
|
testWorldLoad()
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('Joins to local flying-squid server', () => {
|
it('Joins to local flying-squid server', () => {
|
||||||
visit('/?ip=localhost&version=1.16.1')
|
visit('/?ip=localhost&version=1.16.1')
|
||||||
window.localStorage.version = ''
|
window.localStorage.version = ''
|
||||||
// todo replace with data-test
|
// todo replace with data-test
|
||||||
// cy.get('[data-test-id="servers-screen-button"]').click()
|
// 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="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="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
|
cy.get('[data-test-id="connect-qs"]').click()
|
||||||
testWorldLoad()
|
testWorldLoad()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
/// <reference types="cypress" />
|
|
||||||
import { BenchmarkAdapterInfo, getAllInfoLines } from '../../src/benchmarkAdapter'
|
|
||||||
import { cleanVisit } from './shared'
|
|
||||||
|
|
||||||
it('Benchmark rendering performance', () => {
|
|
||||||
cleanVisit('/?openBenchmark=true&renderDistance=5')
|
|
||||||
// wait for render end event
|
|
||||||
return cy.document().then({ timeout: 180_000 }, doc => {
|
|
||||||
return new Cypress.Promise(resolve => {
|
|
||||||
cy.log('Waiting for world to load')
|
|
||||||
doc.addEventListener('cypress-world-ready', resolve)
|
|
||||||
}).then(() => {
|
|
||||||
cy.log('World loaded')
|
|
||||||
})
|
|
||||||
}).then(() => {
|
|
||||||
cy.window().then(win => {
|
|
||||||
const adapter = win.benchmarkAdapter as BenchmarkAdapterInfo
|
|
||||||
|
|
||||||
const messages = getAllInfoLines(adapter)
|
|
||||||
// wait for 10 seconds
|
|
||||||
cy.wait(10_000)
|
|
||||||
const messages2 = getAllInfoLines(adapter, true)
|
|
||||||
for (const message of messages) {
|
|
||||||
cy.log(message)
|
|
||||||
}
|
|
||||||
for (const message of messages2) {
|
|
||||||
cy.log(message)
|
|
||||||
}
|
|
||||||
cy.writeFile('benchmark.txt', [...messages, ...messages2].join('\n'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//@ts-check
|
//@ts-check
|
||||||
import mcServer from 'flying-squid'
|
import mcServer from 'flying-squid'
|
||||||
import defaultOptions from 'flying-squid/config/default-settings.json' with { type: 'json' }
|
import defaultOptions from 'flying-squid/config/default-settings.json' assert { type: 'json' }
|
||||||
|
|
||||||
/** @type {Options} */
|
/** @type {Options} */
|
||||||
const serverOptions = {
|
const serverOptions = {
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<script src="state.ts" type="module"></script>
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
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()
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Minecraft Item Viewer</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; overflow: hidden; }
|
|
||||||
canvas { display: block; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="module" src="./three-item.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
import * as THREE from 'three'
|
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
|
||||||
import itemsAtlas from 'mc-assets/dist/itemsAtlasLegacy.png'
|
|
||||||
import { createItemMeshFromCanvas, createItemMesh } from '../renderer/viewer/three/itemMesh'
|
|
||||||
|
|
||||||
// Create scene, camera and renderer
|
|
||||||
const scene = new THREE.Scene()
|
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
document.body.appendChild(renderer.domElement)
|
|
||||||
|
|
||||||
// Setup camera and controls
|
|
||||||
camera.position.set(0, 0, 3)
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement)
|
|
||||||
controls.enableDamping = true
|
|
||||||
|
|
||||||
// Background and lights
|
|
||||||
scene.background = new THREE.Color(0x333333)
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
|
|
||||||
scene.add(ambientLight)
|
|
||||||
|
|
||||||
// Animation loop
|
|
||||||
function animate () {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
controls.update()
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupItemMesh () {
|
|
||||||
try {
|
|
||||||
const loader = new THREE.TextureLoader()
|
|
||||||
const atlasTexture = await loader.loadAsync(itemsAtlas)
|
|
||||||
|
|
||||||
// Pixel-art configuration
|
|
||||||
atlasTexture.magFilter = THREE.NearestFilter
|
|
||||||
atlasTexture.minFilter = THREE.NearestFilter
|
|
||||||
atlasTexture.generateMipmaps = false
|
|
||||||
atlasTexture.wrapS = atlasTexture.wrapT = THREE.ClampToEdgeWrapping
|
|
||||||
|
|
||||||
// Extract the tile at x=2, y=0 (16x16)
|
|
||||||
const tileSize = 16
|
|
||||||
const tileX = 2
|
|
||||||
const tileY = 0
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = tileSize
|
|
||||||
canvas.height = tileSize
|
|
||||||
const ctx = canvas.getContext('2d')!
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = false
|
|
||||||
ctx.drawImage(
|
|
||||||
atlasTexture.image,
|
|
||||||
tileX * tileSize,
|
|
||||||
tileY * tileSize,
|
|
||||||
tileSize,
|
|
||||||
tileSize,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
tileSize,
|
|
||||||
tileSize
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test both approaches - working manual extraction:
|
|
||||||
const meshOld = createItemMeshFromCanvas(canvas, { depth: 0.1 })
|
|
||||||
meshOld.position.x = -1
|
|
||||||
meshOld.rotation.x = -Math.PI / 12
|
|
||||||
meshOld.rotation.y = Math.PI / 12
|
|
||||||
scene.add(meshOld)
|
|
||||||
|
|
||||||
// And new unified function:
|
|
||||||
const atlasWidth = atlasTexture.image.width
|
|
||||||
const atlasHeight = atlasTexture.image.height
|
|
||||||
const u = (tileX * tileSize) / atlasWidth
|
|
||||||
const v = (tileY * tileSize) / atlasHeight
|
|
||||||
const sizeX = tileSize / atlasWidth
|
|
||||||
const sizeY = tileSize / atlasHeight
|
|
||||||
|
|
||||||
console.log('Debug texture coords:', {u, v, sizeX, sizeY, atlasWidth, atlasHeight})
|
|
||||||
|
|
||||||
const resultNew = createItemMesh(atlasTexture, {
|
|
||||||
u, v, sizeX, sizeY
|
|
||||||
}, {
|
|
||||||
faceCamera: false,
|
|
||||||
use3D: true,
|
|
||||||
depth: 0.1
|
|
||||||
})
|
|
||||||
|
|
||||||
resultNew.mesh.position.x = 1
|
|
||||||
resultNew.mesh.rotation.x = -Math.PI / 12
|
|
||||||
resultNew.mesh.rotation.y = Math.PI / 12
|
|
||||||
scene.add(resultNew.mesh)
|
|
||||||
|
|
||||||
animate()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to create item mesh:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start
|
|
||||||
setupItemMesh()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<script type="module" src="three-labels.ts"></script>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; }
|
|
||||||
canvas { display: block; }
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import * as THREE from 'three'
|
|
||||||
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'
|
|
||||||
import { createWaypointSprite, WAYPOINT_CONFIG } from '../renderer/viewer/three/waypointSprite'
|
|
||||||
|
|
||||||
// Create scene, camera and renderer
|
|
||||||
const scene = new THREE.Scene()
|
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
document.body.appendChild(renderer.domElement)
|
|
||||||
|
|
||||||
// Add FirstPersonControls
|
|
||||||
const controls = new FirstPersonControls(camera, renderer.domElement)
|
|
||||||
controls.lookSpeed = 0.1
|
|
||||||
controls.movementSpeed = 10
|
|
||||||
controls.lookVertical = true
|
|
||||||
controls.constrainVertical = true
|
|
||||||
controls.verticalMin = 0.1
|
|
||||||
controls.verticalMax = Math.PI - 0.1
|
|
||||||
|
|
||||||
// Position camera
|
|
||||||
camera.position.y = 1.6 // Typical eye height
|
|
||||||
camera.lookAt(0, 1.6, -1)
|
|
||||||
|
|
||||||
// Create a helper grid and axes
|
|
||||||
const grid = new THREE.GridHelper(20, 20)
|
|
||||||
scene.add(grid)
|
|
||||||
const axes = new THREE.AxesHelper(5)
|
|
||||||
scene.add(axes)
|
|
||||||
|
|
||||||
// Create waypoint sprite via utility
|
|
||||||
const waypoint = createWaypointSprite({
|
|
||||||
position: new THREE.Vector3(0, 0, -5),
|
|
||||||
color: 0xff0000,
|
|
||||||
label: 'Target',
|
|
||||||
})
|
|
||||||
scene.add(waypoint.group)
|
|
||||||
|
|
||||||
// Use built-in offscreen arrow from utils
|
|
||||||
waypoint.enableOffscreenArrow(true)
|
|
||||||
waypoint.setArrowParent(scene)
|
|
||||||
|
|
||||||
// Animation loop
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
|
|
||||||
const delta = Math.min(clock.getDelta(), 0.1)
|
|
||||||
controls.update(delta)
|
|
||||||
|
|
||||||
// Unified camera update (size, distance text, arrow, visibility)
|
|
||||||
const sizeVec = renderer.getSize(new THREE.Vector2())
|
|
||||||
waypoint.updateForCamera(camera.position, camera, sizeVec.width, sizeVec.height)
|
|
||||||
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add clock for controls
|
|
||||||
const clock = new THREE.Clock()
|
|
||||||
|
|
||||||
animate()
|
|
||||||
|
|
@ -1,60 +1,101 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
|
import * as tweenJs from '@tweenjs/tween.js'
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import Jimp from 'jimp';
|
||||||
|
|
||||||
// Create scene, camera and renderer
|
|
||||||
const scene = new THREE.Scene()
|
const scene = new THREE.Scene()
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
|
camera.position.set(0, 0, 5)
|
||||||
const renderer = new THREE.WebGLRenderer()
|
const renderer = new THREE.WebGLRenderer()
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
document.body.appendChild(renderer.domElement)
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
// Position camera
|
const controls = new OrbitControls(camera, renderer.domElement)
|
||||||
camera.position.z = 5
|
|
||||||
|
|
||||||
// Create a canvas with some content
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||||
const canvas = document.createElement('canvas')
|
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
||||||
canvas.width = 256
|
const cube = new THREE.Mesh(geometry, material)
|
||||||
canvas.height = 256
|
cube.position.set(0.5, 0.5, 0.5);
|
||||||
const ctx = canvas.getContext('2d')
|
const group = new THREE.Group()
|
||||||
|
group.add(cube)
|
||||||
|
group.position.set(-0.5, -0.5, -0.5);
|
||||||
|
const outerGroup = new THREE.Group()
|
||||||
|
outerGroup.add(group)
|
||||||
|
outerGroup.scale.set(0.2, 0.2, 0.2)
|
||||||
|
outerGroup.position.set(1, 1, 0)
|
||||||
|
scene.add(outerGroup)
|
||||||
|
|
||||||
scene.background = new THREE.Color(0x444444)
|
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||||
|
// mesh.position.set(0.5, 1, 0.5)
|
||||||
|
// const group = new THREE.Group()
|
||||||
|
// group.add(mesh)
|
||||||
|
// group.position.set(-0.5, -1, -0.5)
|
||||||
|
// const outerGroup = new THREE.Group()
|
||||||
|
// outerGroup.add(group)
|
||||||
|
// // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
||||||
|
// scene.add(outerGroup)
|
||||||
|
|
||||||
// Draw something on the canvas
|
new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
||||||
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
|
const tweenGroup = new tweenJs.Group()
|
||||||
async function createTexturedBox() {
|
function animate () {
|
||||||
const canvas2 = new OffscreenCanvas(256, 256)
|
tweenGroup.update()
|
||||||
const ctx2 = canvas2.getContext('2d')!
|
requestAnimationFrame(animate)
|
||||||
ctx2.drawImage(canvas, 0, 0)
|
// cube.rotation.x += 0.01
|
||||||
const texture = new THREE.Texture(canvas2)
|
// cube.rotation.y += 0.01
|
||||||
texture.magFilter = THREE.NearestFilter
|
renderer.render(scene, camera)
|
||||||
texture.minFilter = THREE.NearestFilter
|
|
||||||
texture.needsUpdate = true
|
|
||||||
texture.flipY = false
|
|
||||||
|
|
||||||
// Create box with texture
|
|
||||||
const geometry = new THREE.BoxGeometry(2, 2, 2)
|
|
||||||
const material = new THREE.MeshBasicMaterial({
|
|
||||||
map: texture,
|
|
||||||
side: THREE.DoubleSide,
|
|
||||||
premultipliedAlpha: false,
|
|
||||||
})
|
|
||||||
const cube = new THREE.Mesh(geometry, material)
|
|
||||||
scene.add(cube)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the textured box
|
|
||||||
createTexturedBox()
|
|
||||||
|
|
||||||
// Animation loop
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
}
|
||||||
animate()
|
animate()
|
||||||
|
|
||||||
|
// let animation
|
||||||
|
|
||||||
|
window.animate = () => {
|
||||||
|
// new Tween.Tween(group.position).to({ y: group.position.y - 1}, 1000 * 0.35/2).yoyo(true).repeat(1).start()
|
||||||
|
new tweenJs.Tween(group.rotation, tweenGroup).to({ z: THREE.MathUtils.degToRad(90) }, 1000 * 0.35 / 2).yoyo(true).repeat(Infinity).start().onRepeat(() => {
|
||||||
|
console.log('done')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.stop = () => {
|
||||||
|
tweenGroup.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createGeometryFromImage() {
|
||||||
|
return new Promise<THREE.ShapeGeometry>((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABEElEQVQ4jWNkIAPw2Zv9J0cfXPOSvx/+L/n74T+HqsJ/JlI1T9u3i6H91B7ybdY+vgZuO1majV+fppFmPnuz/+ihy2dv9t/49Wm8mlECkV1FHh5FfPZm/1XXTGX4cechA4eKPMNVq1CGH7cfMBJ0rlxX+X8OVYX/xq9P/5frKifoZ0Z0AwS8HRkYGBgYvt+8xyDXUUbQZgwJPnuz/+wq8gw/7zxk+PXsFUFno0h6mon+l5fgZFhwnYmBTUqMgYGBgaAhLMiaHQyFGOZvf8Lw49FXRgYGhv8MDAwwg/7jMoQFFury/C8Y5m9/wnADohnZVryJhoWBARJ9Cw69gtmMAgiFAcuvZ68Yfj17hU8NXgAATdKfkzbQhBEAAAAASUVORK5CYII='
|
||||||
|
console.log('img.complete', img.complete)
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
context.drawImage(img, 0, 0, img.width, img.height);
|
||||||
|
const imgData = context.getImageData(0, 0, img.width, img.height);
|
||||||
|
|
||||||
|
const shape = new THREE.Shape();
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
for (let x = 0; x < img.width; x++) {
|
||||||
|
const index = (y * img.width + x) * 4;
|
||||||
|
const alpha = imgData.data[index + 3];
|
||||||
|
if (alpha !== 0) {
|
||||||
|
shape.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = new THREE.ShapeGeometry(shape);
|
||||||
|
resolve(geometry);
|
||||||
|
};
|
||||||
|
img.onerror = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
const shapeGeomtry = createGeometryFromImage().then(geometry => {
|
||||||
|
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
scene.add(mesh);
|
||||||
|
})
|
||||||
|
|
|
||||||
84
index.html
84
index.html
|
|
@ -4,15 +4,6 @@
|
||||||
<meta name="darkreader-lock">
|
<meta name="darkreader-lock">
|
||||||
<script>
|
<script>
|
||||||
window.startLoad = Date.now()
|
window.startLoad = Date.now()
|
||||||
// g663 fix: forbid change of string prototype
|
|
||||||
Object.defineProperty(String.prototype, 'format', {
|
|
||||||
writable: false,
|
|
||||||
configurable: false
|
|
||||||
});
|
|
||||||
Object.defineProperty(String.prototype, 'replaceAll', {
|
|
||||||
writable: false,
|
|
||||||
configurable: false
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<!-- // #region initial loader -->
|
<!-- // #region initial loader -->
|
||||||
<script async>
|
<script async>
|
||||||
|
|
@ -25,9 +16,6 @@
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;" class="title">Loading...</div>
|
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;" class="title">Loading...</div>
|
||||||
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div>
|
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div>
|
||||||
<!-- small text pre -->
|
|
||||||
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(150, 150, 150);margin-top: 3px;text-align: center;white-space: pre-line;" class="advanced-info"></div>
|
|
||||||
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(255, 100, 100);margin-top: 10px;text-align: center;display: none;" class="ios-warning">Only iOS 15+ is supported due to performance optimizations</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
@ -37,48 +25,17 @@
|
||||||
if (!window.pageLoaded) {
|
if (!window.pageLoaded) {
|
||||||
document.documentElement.appendChild(loadingDivElem)
|
document.documentElement.appendChild(loadingDivElem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS version detection
|
|
||||||
const getIOSVersion = () => {
|
|
||||||
const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
|
||||||
return match ? parseInt(match[1], 10) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load error handling
|
// load error handling
|
||||||
const onError = (errorOrMessage, log = false) => {
|
const onError = (message) => {
|
||||||
let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage
|
console.log(message)
|
||||||
if (log) console.log(message)
|
|
||||||
if (typeof message !== 'string') message = String(message)
|
|
||||||
if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') {
|
if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') {
|
||||||
document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error'
|
document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error'
|
||||||
const [errorMessage, ...errorStack] = message.split('\n')
|
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = message
|
||||||
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage
|
|
||||||
document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n')
|
|
||||||
|
|
||||||
// Show iOS warning if applicable
|
|
||||||
const iosVersion = getIOSVersion();
|
|
||||||
if (iosVersion !== null && iosVersion < 15) {
|
|
||||||
document.querySelector('.initial-loader').querySelector('.ios-warning').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
||||||
// unregister all sw
|
|
||||||
if (window.navigator.serviceWorker && document.querySelector('.initial-loader').style.opacity !== 0) {
|
|
||||||
console.log('got worker')
|
|
||||||
window.navigator.serviceWorker.getRegistrations().then(registrations => {
|
|
||||||
registrations.forEach(registration => {
|
|
||||||
console.log('got registration')
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
console.log('worker unregistered')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
window.lastError = errorOrMessage instanceof Error ? errorOrMessage : new Error(errorOrMessage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('unhandledrejection', (e) => onError(e.reason, true))
|
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
|
||||||
window.addEventListener('error', (e) => onError(e.error ?? e.message))
|
window.addEventListener('error', (e) => onError(e.message))
|
||||||
}
|
}
|
||||||
insertLoadingDiv()
|
insertLoadingDiv()
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
@ -95,25 +52,6 @@
|
||||||
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
|
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
|
||||||
eruda.init()
|
eruda.init()
|
||||||
})
|
})
|
||||||
Promise.all([import('https://cdn.skypack.dev/stacktrace-gps'), import('https://cdn.skypack.dev/error-stack-parser')]).then(async ([{ default: StackTraceGPS }, { default: ErrorStackParser }]) => {
|
|
||||||
if (!window.lastError) return
|
|
||||||
|
|
||||||
let stackFrames = [];
|
|
||||||
if (window.lastError instanceof Error) {
|
|
||||||
stackFrames = ErrorStackParser.parse(window.lastError);
|
|
||||||
}
|
|
||||||
console.log('stackFrames', stackFrames)
|
|
||||||
const gps = new StackTraceGPS()
|
|
||||||
const mappedFrames = await Promise.all(
|
|
||||||
stackFrames.map(frame => gps.pinpoint(frame))
|
|
||||||
);
|
|
||||||
console.log('mappedFrames', mappedFrames)
|
|
||||||
|
|
||||||
const stackTrace = mappedFrames
|
|
||||||
.map(frame => `at ${frame.functionName} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`)
|
|
||||||
.join('\n');
|
|
||||||
console.log('stackTrace', stackTrace)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkLoadEruda()
|
checkLoadEruda()
|
||||||
|
|
@ -147,17 +85,21 @@
|
||||||
window.loadedPlugins[pluginName] = await import(script)
|
window.loadedPlugins[pluginName] = await import(script)
|
||||||
}
|
}
|
||||||
</script> -->
|
</script> -->
|
||||||
<title>Minecraft Web Client</title>
|
<title>Prismarine Web Client</title>
|
||||||
<!-- <link rel="canonical" href="https://mcraft.fun"> -->
|
<link rel="favicon" href="favicon.png">
|
||||||
<meta name="description" content="Minecraft Java Edition Client in Browser — Full Multiplayer Support, Server Connect, Offline Play — Join real Minecraft servers">
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<link rel="canonical" href="https://mcraft.fun">
|
||||||
|
<meta name="description" content="Minecraft web client running in your browser">
|
||||||
<meta name="keywords" content="Play, Minecraft, Online, Web, Java, Server, Single player, Javascript, PrismarineJS, Voxel, WebGL, Three.js">
|
<meta name="keywords" content="Play, Minecraft, Online, Web, Java, Server, Single player, Javascript, PrismarineJS, Voxel, WebGL, Three.js">
|
||||||
<meta name="date" content="2024-07-11" scheme="YYYY-MM-DD">
|
<meta name="date" content="2024-07-11" scheme="YYYY-MM-DD">
|
||||||
<meta name="language" content="English">
|
<meta name="language" content="English">
|
||||||
<meta name="theme-color" content="#349474">
|
<meta name="theme-color" content="#349474">
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'>
|
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'>
|
||||||
<meta property="og:title" content="Minecraft Web Client" />
|
<meta property="og:title" content="Prismarine Web Client" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:image" content="favicon.png" />
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="react-root"></div>
|
<div id="react-root"></div>
|
||||||
|
|
|
||||||
111
package.json
111
package.json
|
|
@ -5,45 +5,33 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev-rsbuild": "rsbuild dev",
|
"dev-rsbuild": "rsbuild dev",
|
||||||
"dev-proxy": "node server.js",
|
"dev-proxy": "node server.js",
|
||||||
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
"start": "run-p dev-rsbuild dev-proxy watch-mesher",
|
||||||
"start2": "run-p dev-rsbuild watch-mesher",
|
|
||||||
"start-metrics": "ENABLE_METRICS=true rsbuild dev",
|
|
||||||
"build": "pnpm build-other-workers && rsbuild build",
|
"build": "pnpm build-other-workers && rsbuild build",
|
||||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
|
||||||
"prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts",
|
|
||||||
"check-build": "pnpm prepare-project && tsc && pnpm build",
|
|
||||||
"test:cypress": "cypress run",
|
"test:cypress": "cypress run",
|
||||||
"test:benchmark": "PERFORMANCE_TEST=true cypress run",
|
|
||||||
"test:cypress:open": "cypress open",
|
|
||||||
"test-unit": "vitest",
|
"test-unit": "vitest",
|
||||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||||
"prod-start": "node server.js --prod",
|
"prod-start": "node server.js --prod",
|
||||||
"test-mc-server": "tsx cypress/minecraft-server.mjs",
|
"test-mc-server": "tsx cypress/minecraft-server.mjs",
|
||||||
"lint": "eslint \"{src,cypress,renderer}/**/*.{ts,js,jsx,tsx}\"",
|
"lint": "eslint \"{src,cypress,prismarine-viewer}/**/*.{ts,js,jsx,tsx}\"",
|
||||||
"lint-fix": "pnpm lint --fix",
|
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
||||||
"start-experiments": "vite --config experiments/vite.config.ts --host",
|
"start-experiments": "vite --config experiments/vite.config.ts --host",
|
||||||
"watch-other-workers": "echo NOT IMPLEMENTED",
|
"watch-other-workers": "echo NOT IMPLEMENTED",
|
||||||
"build-other-workers": "echo NOT IMPLEMENTED",
|
"build-other-workers": "echo NOT IMPLEMENTED",
|
||||||
"build-mesher": "node renderer/buildMesherWorker.mjs",
|
"build-mesher": "node prismarine-viewer/buildMesherWorker.mjs",
|
||||||
"watch-mesher": "pnpm build-mesher -w",
|
"watch-mesher": "pnpm build-mesher -w",
|
||||||
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
||||||
"run-all": "run-p start run-playground",
|
"run-all": "run-p start run-playground",
|
||||||
"build-playground": "rsbuild build --config renderer/rsbuild.config.ts",
|
"build-playground": "rsbuild build --config prismarine-viewer/rsbuild.config.ts",
|
||||||
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts",
|
"watch-playground": "rsbuild dev --config prismarine-viewer/rsbuild.config.ts"
|
||||||
"update-git-deps": "tsx scripts/updateGitDeps.ts",
|
|
||||||
"request-data": "tsx scripts/requestData.ts"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"prismarine",
|
"prismarine",
|
||||||
"web",
|
"web",
|
||||||
"client"
|
"client"
|
||||||
],
|
],
|
||||||
"release": {
|
|
||||||
"attachReleaseFiles": "{self-host.zip,minecraft.html}"
|
|
||||||
},
|
|
||||||
"publish": {
|
"publish": {
|
||||||
"preset": {
|
"preset": {
|
||||||
"publishOnlyIfChanged": true,
|
"publishOnlyIfChanged": true,
|
||||||
|
|
@ -54,9 +42,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimaka/interface": "0.0.3-alpha.0",
|
"@dimaka/interface": "0.0.3-alpha.0",
|
||||||
"@floating-ui/react": "^0.26.1",
|
"@floating-ui/react": "^0.26.1",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@mui/base": "5.0.0-beta.40",
|
||||||
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
|
"@nxg-org/mineflayer-auto-jump": "^0.7.12",
|
||||||
"@nxg-org/mineflayer-tracker": "1.3.0",
|
"@nxg-org/mineflayer-tracker": "1.2.1",
|
||||||
"@react-oauth/google": "^0.12.1",
|
"@react-oauth/google": "^0.12.1",
|
||||||
"@stylistic/eslint-plugin": "^2.6.1",
|
"@stylistic/eslint-plugin": "^2.6.1",
|
||||||
"@types/gapi": "^0.0.47",
|
"@types/gapi": "^0.0.47",
|
||||||
|
|
@ -73,21 +61,18 @@
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepslate": "^0.23.5",
|
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"eruda": "^3.0.1",
|
"eruda": "^3.0.1",
|
||||||
"esbuild": "^0.19.3",
|
"esbuild": "^0.19.3",
|
||||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"filesize": "^10.0.12",
|
"filesize": "^10.0.12",
|
||||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104",
|
"flying-squid": "npm:@zardoy/flying-squid@^0.0.44",
|
||||||
"framer-motion": "^12.9.2",
|
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mcraft-fun-mineflayer": "^0.1.23",
|
"minecraft-data": "3.76.0",
|
||||||
"minecraft-data": "3.98.0",
|
|
||||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||||
"mojangson": "^2.0.4",
|
"mojangson": "^2.0.4",
|
||||||
|
|
@ -106,7 +91,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-select": "^5.8.0",
|
"react-select": "^5.8.0",
|
||||||
"react-zoom-pan-pinch": "3.4.4",
|
"react-transition-group": "^4.4.5",
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"skinview3d": "^3.0.1",
|
"skinview3d": "^3.0.1",
|
||||||
|
|
@ -123,11 +108,11 @@
|
||||||
"workbox-build": "^7.0.0"
|
"workbox-build": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rsbuild/core": "1.3.5",
|
"@rsbuild/core": "^1.0.1-beta.9",
|
||||||
"@rsbuild/plugin-node-polyfill": "1.3.0",
|
"@rsbuild/plugin-node-polyfill": "^1.0.3",
|
||||||
"@rsbuild/plugin-react": "1.2.0",
|
"@rsbuild/plugin-react": "^1.0.1-beta.9",
|
||||||
"@rsbuild/plugin-type-check": "1.2.1",
|
"@rsbuild/plugin-type-check": "^1.0.1-beta.9",
|
||||||
"@rsbuild/plugin-typed-css-modules": "1.0.2",
|
"@rsbuild/plugin-typed-css-modules": "^1.0.1",
|
||||||
"@storybook/addon-essentials": "^7.4.6",
|
"@storybook/addon-essentials": "^7.4.6",
|
||||||
"@storybook/addon-links": "^7.4.6",
|
"@storybook/addon-links": "^7.4.6",
|
||||||
"@storybook/blocks": "^7.4.6",
|
"@storybook/blocks": "^7.4.6",
|
||||||
|
|
@ -135,6 +120,7 @@
|
||||||
"@storybook/react-vite": "^7.4.6",
|
"@storybook/react-vite": "^7.4.6",
|
||||||
"@types/diff-match-patch": "^1.0.36",
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
"@types/lodash-es": "^4.17.9",
|
"@types/lodash-es": "^4.17.9",
|
||||||
|
"@types/react-transition-group": "^4.4.7",
|
||||||
"@types/stats.js": "^0.17.1",
|
"@types/stats.js": "^0.17.1",
|
||||||
"@types/three": "0.154.0",
|
"@types/three": "0.154.0",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
|
@ -144,7 +130,7 @@
|
||||||
"browserify-zlib": "^0.2.0",
|
"browserify-zlib": "^0.2.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"constants-browserify": "^1.0.0",
|
"constants-browserify": "^1.0.0",
|
||||||
"contro-max": "^0.1.9",
|
"contro-max": "^0.1.8",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"cypress-esbuild-preprocessor": "^1.0.2",
|
"cypress-esbuild-preprocessor": "^1.0.2",
|
||||||
"eslint": "^8.50.0",
|
"eslint": "^8.50.0",
|
||||||
|
|
@ -154,16 +140,16 @@
|
||||||
"http-browserify": "^1.7.0",
|
"http-browserify": "^1.7.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"mc-assets": "^0.2.62",
|
"mc-assets": "^0.2.12",
|
||||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
"mineflayer": "github:zardoy/mineflayer",
|
||||||
"mineflayer-mouse": "^0.1.21",
|
"mineflayer-pathfinder": "^2.4.4",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"path-exists-cli": "^2.0.0",
|
"path-exists-cli": "^2.0.0",
|
||||||
|
"prismarine-viewer": "link:prismarine-viewer",
|
||||||
"process": "github:PrismarineJS/node-process",
|
"process": "github:PrismarineJS/node-process",
|
||||||
"renderer": "link:renderer",
|
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"storybook": "^7.4.6",
|
"storybook": "^7.4.6",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
|
@ -176,67 +162,32 @@
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"cypress": "^10.11.0",
|
"cypress": "^10.11.0",
|
||||||
"cypress-plugin-snapshots": "^1.4.4",
|
"cypress-plugin-snapshots": "^1.4.4",
|
||||||
"sharp": "^0.33.5",
|
|
||||||
"systeminformation": "^5.21.22"
|
"systeminformation": "^5.21.22"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
"iOS >= 14",
|
|
||||||
"Android >= 13",
|
|
||||||
"Chrome >= 103",
|
|
||||||
"not dead",
|
|
||||||
"not ie <= 11",
|
|
||||||
"not op_mini all",
|
|
||||||
"> 0.5%"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
|
||||||
"@nxg-org/mineflayer-physics-util": "1.8.10",
|
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"vec3": "0.1.10",
|
"@nxg-org/mineflayer-physics-util": "1.5.8",
|
||||||
"three": "0.154.0",
|
"three": "0.154.0",
|
||||||
"diamond-square": "github:zardoy/diamond-square",
|
"diamond-square": "github:zardoy/diamond-square",
|
||||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||||
"minecraft-data": "3.98.0",
|
"minecraft-data": "3.76.0",
|
||||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
|
||||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"prismarine-chunk": "github:zardoy/prismarine-chunk#master",
|
"prismarine-chunk": "github:zardoy/prismarine-chunk",
|
||||||
"prismarine-item": "latest"
|
"prismarine-item": "latest"
|
||||||
},
|
},
|
||||||
"updateConfig": {
|
"updateConfig": {
|
||||||
"ignoreDependencies": [
|
"ignoreDependencies": []
|
||||||
"browserfs",
|
|
||||||
"google-drive-browserfs"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
"three@0.154.0": "patches/three@0.154.0.patch",
|
||||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
||||||
"minecraft-protocol": "patches/minecraft-protocol.patch"
|
"minecraft-protocol@1.49.0": "patches/minecraft-protocol@1.49.0.patch"
|
||||||
},
|
}
|
||||||
"ignoredBuiltDependencies": [
|
|
||||||
"canvas",
|
|
||||||
"core-js",
|
|
||||||
"gl"
|
|
||||||
],
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"sharp",
|
|
||||||
"cypress",
|
|
||||||
"esbuild",
|
|
||||||
"fsevents"
|
|
||||||
],
|
|
||||||
"ignorePatchFailures": false,
|
|
||||||
"allowUnusedPatches": false
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
|
"packageManager": "pnpm@9.0.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
diff --git a/src/client/chat.js b/src/client/chat.js
|
|
||||||
index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644
|
|
||||||
--- a/src/client/chat.js
|
|
||||||
+++ b/src/client/chat.js
|
|
||||||
@@ -116,7 +116,7 @@ module.exports = function (client, options) {
|
|
||||||
for (const player of packet.data) {
|
|
||||||
if (player.chatSession) {
|
|
||||||
client._players[player.uuid] = {
|
|
||||||
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
|
||||||
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
|
||||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
|
||||||
sessionUuid: player.chatSession.uuid
|
|
||||||
}
|
|
||||||
@@ -126,7 +126,7 @@ module.exports = function (client, options) {
|
|
||||||
|
|
||||||
if (player.crypto) {
|
|
||||||
client._players[player.uuid] = {
|
|
||||||
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
|
||||||
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
|
||||||
publicKeyDER: player.crypto.publicKey,
|
|
||||||
signature: player.crypto.signature,
|
|
||||||
displayName: player.displayName || player.name
|
|
||||||
@@ -196,7 +196,7 @@ module.exports = function (client, options) {
|
|
||||||
if (mcData.supportFeature('useChatSessions')) {
|
|
||||||
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
|
||||||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
|
||||||
- const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
|
||||||
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
|
||||||
if (verified) client._signatureCache.push(packet.signature)
|
|
||||||
client.emit('playerChat', {
|
|
||||||
globalIndex: packet.globalIndex,
|
|
||||||
@@ -362,7 +362,7 @@ module.exports = function (client, options) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- client._signedChat = (message, options = {}) => {
|
|
||||||
+ client._signedChat = async (message, options = {}) => {
|
|
||||||
options.timestamp = options.timestamp || BigInt(Date.now())
|
|
||||||
options.salt = options.salt || 1n
|
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ module.exports = function (client, options) {
|
|
||||||
message,
|
|
||||||
timestamp: options.timestamp,
|
|
||||||
salt: options.salt,
|
|
||||||
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
|
||||||
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
|
||||||
offset: client._lastSeenMessages.pending,
|
|
||||||
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
|
|
||||||
acknowledged
|
|
||||||
@@ -422,7 +422,7 @@ module.exports = function (client, options) {
|
|
||||||
message,
|
|
||||||
timestamp: options.timestamp,
|
|
||||||
salt: options.salt,
|
|
||||||
- signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
|
||||||
+ signature: client.profileKeys ? await client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
|
||||||
signedPreview: options.didPreview,
|
|
||||||
previousMessages: client._lastSeenMessages.map((e) => ({
|
|
||||||
messageSender: e.sender,
|
|
||||||
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
|
||||||
index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644
|
|
||||||
--- a/src/client/encrypt.js
|
|
||||||
+++ b/src/client/encrypt.js
|
|
||||||
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
|
||||||
if (packet.serverId !== '-') {
|
|
||||||
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail')
|
|
||||||
}
|
|
||||||
- sendEncryptionKeyResponse()
|
|
||||||
+ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.')
|
|
||||||
+ // sendEncryptionKeyResponse()
|
|
||||||
+ // client.once('set_compression', () => {
|
|
||||||
+ // clearTimeout(loginTimeout)
|
|
||||||
+ // })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onJoinServerResponse (err) {
|
|
||||||
diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js
|
|
||||||
index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644
|
|
||||||
--- a/src/client/pluginChannels.js
|
|
||||||
+++ b/src/client/pluginChannels.js
|
|
||||||
@@ -57,7 +57,7 @@ module.exports = function (client, options) {
|
|
||||||
try {
|
|
||||||
packet.data = proto.parsePacketBuffer(channel, packet.data).data
|
|
||||||
} catch (error) {
|
|
||||||
- client.emit('error', error)
|
|
||||||
+ client.emit('error', error, { customPayload: packet })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/src/client.js b/src/client.js
|
|
||||||
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644
|
|
||||||
--- a/src/client.js
|
|
||||||
+++ b/src/client.js
|
|
||||||
@@ -111,7 +111,13 @@ class Client extends EventEmitter {
|
|
||||||
this._hasBundlePacket = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
- emitPacket(parsed)
|
|
||||||
+ try {
|
|
||||||
+ emitPacket(parsed)
|
|
||||||
+ } catch (err) {
|
|
||||||
+ console.log('Client incorrectly handled packet ' + parsed.metadata.name)
|
|
||||||
+ console.error(err)
|
|
||||||
+ // todo investigate why it doesn't close the stream even if unhandled there
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -169,7 +175,10 @@ class Client extends EventEmitter {
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFatalError = (err) => {
|
|
||||||
- this.emit('error', err)
|
|
||||||
+ // todo find out what is trying to write after client disconnect
|
|
||||||
+ if(err.code !== 'ECONNABORTED') {
|
|
||||||
+ this.emit('error', err)
|
|
||||||
+ }
|
|
||||||
endSocket()
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -198,6 +207,10 @@ class Client extends EventEmitter {
|
|
||||||
serializer -> framer -> socket -> splitter -> deserializer */
|
|
||||||
if (this.serializer) {
|
|
||||||
this.serializer.end()
|
|
||||||
+ setTimeout(() => {
|
|
||||||
+ this.socket?.end()
|
|
||||||
+ this.socket?.emit('end')
|
|
||||||
+ }, 2000) // allow the serializer to finish writing
|
|
||||||
} else {
|
|
||||||
if (this.socket) this.socket.end()
|
|
||||||
}
|
|
||||||
@@ -243,6 +256,7 @@ class Client extends EventEmitter {
|
|
||||||
debug('writing packet ' + this.state + '.' + name)
|
|
||||||
debug(params)
|
|
||||||
}
|
|
||||||
+ this.emit('writePacket', name, params)
|
|
||||||
this.serializer.write({ name, params })
|
|
||||||
}
|
|
||||||
|
|
||||||
188
patches/minecraft-protocol@1.49.0.patch
Normal file
188
patches/minecraft-protocol@1.49.0.patch
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js
|
||||||
|
index c437ecf3a0e4ab5758a48538c714b7e9651bb5da..d9c9895ae8614550aa09ad60a396ac32ffdf1287 100644
|
||||||
|
--- a/src/client/autoVersion.js
|
||||||
|
+++ b/src/client/autoVersion.js
|
||||||
|
@@ -9,7 +9,7 @@ module.exports = function (client, options) {
|
||||||
|
client.wait_connect = true // don't let src/client/setProtocol proceed on socket 'connect' until 'connect_allowed'
|
||||||
|
debug('pinging', options.host)
|
||||||
|
// TODO: use 0xfe ping instead for better compatibility/performance? https://github.com/deathcap/node-minecraft-ping
|
||||||
|
- ping(options, function (err, response) {
|
||||||
|
+ ping(options, async function (err, response) {
|
||||||
|
if (err) { return client.emit('error', err) }
|
||||||
|
debug('ping response', response)
|
||||||
|
// TODO: could also use ping pre-connect to save description, type, max players, etc.
|
||||||
|
@@ -40,6 +40,7 @@ module.exports = function (client, options) {
|
||||||
|
|
||||||
|
// Reinitialize client object with new version TODO: move out of its constructor?
|
||||||
|
client.version = minecraftVersion
|
||||||
|
+ await options.versionSelectedHook?.(client)
|
||||||
|
client.state = states.HANDSHAKING
|
||||||
|
|
||||||
|
// Let other plugins such as Forge/FML (modinfo) respond to the ping response
|
||||||
|
diff --git a/src/client/chat.js b/src/client/chat.js
|
||||||
|
index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644
|
||||||
|
--- a/src/client/chat.js
|
||||||
|
+++ b/src/client/chat.js
|
||||||
|
@@ -111,7 +111,7 @@ module.exports = function (client, options) {
|
||||||
|
for (const player of packet.data) {
|
||||||
|
if (!player.chatSession) continue
|
||||||
|
client._players[player.UUID] = {
|
||||||
|
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||||
|
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||||
|
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||||
|
sessionUuid: player.chatSession.uuid
|
||||||
|
}
|
||||||
|
@@ -127,7 +127,7 @@ module.exports = function (client, options) {
|
||||||
|
for (const player of packet.data) {
|
||||||
|
if (player.crypto) {
|
||||||
|
client._players[player.UUID] = {
|
||||||
|
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||||
|
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||||
|
publicKeyDER: player.crypto.publicKey,
|
||||||
|
signature: player.crypto.signature,
|
||||||
|
displayName: player.displayName || player.name
|
||||||
|
@@ -198,7 +198,7 @@ module.exports = function (client, options) {
|
||||||
|
if (mcData.supportFeature('useChatSessions')) {
|
||||||
|
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
||||||
|
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
||||||
|
- const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||||
|
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||||
|
if (verified) client._signatureCache.push(packet.signature)
|
||||||
|
client.emit('playerChat', {
|
||||||
|
plainMessage: packet.plainMessage,
|
||||||
|
@@ -363,7 +363,7 @@ module.exports = function (client, options) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- client._signedChat = (message, options = {}) => {
|
||||||
|
+ client._signedChat = async (message, options = {}) => {
|
||||||
|
options.timestamp = options.timestamp || BigInt(Date.now())
|
||||||
|
options.salt = options.salt || 1n
|
||||||
|
|
||||||
|
@@ -405,7 +405,7 @@ module.exports = function (client, options) {
|
||||||
|
message,
|
||||||
|
timestamp: options.timestamp,
|
||||||
|
salt: options.salt,
|
||||||
|
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||||
|
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||||
|
offset: client._lastSeenMessages.pending,
|
||||||
|
acknowledged
|
||||||
|
})
|
||||||
|
@@ -419,7 +419,7 @@ module.exports = function (client, options) {
|
||||||
|
message,
|
||||||
|
timestamp: options.timestamp,
|
||||||
|
salt: options.salt,
|
||||||
|
- signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||||
|
+ signature: client.profileKeys ? await client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||||
|
signedPreview: options.didPreview,
|
||||||
|
previousMessages: client._lastSeenMessages.map((e) => ({
|
||||||
|
messageSender: e.sender,
|
||||||
|
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
||||||
|
index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644
|
||||||
|
--- a/src/client/encrypt.js
|
||||||
|
+++ b/src/client/encrypt.js
|
||||||
|
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
||||||
|
if (packet.serverId !== '-') {
|
||||||
|
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail')
|
||||||
|
}
|
||||||
|
- sendEncryptionKeyResponse()
|
||||||
|
+ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.')
|
||||||
|
+ // sendEncryptionKeyResponse()
|
||||||
|
+ // client.once('set_compression', () => {
|
||||||
|
+ // clearTimeout(loginTimeout)
|
||||||
|
+ // })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onJoinServerResponse (err) {
|
||||||
|
diff --git a/src/client.js b/src/client.js
|
||||||
|
index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c278950ba7f396 100644
|
||||||
|
--- a/src/client.js
|
||||||
|
+++ b/src/client.js
|
||||||
|
@@ -88,10 +88,12 @@ class Client extends EventEmitter {
|
||||||
|
parsed.metadata.name = parsed.data.name
|
||||||
|
parsed.data = parsed.data.params
|
||||||
|
parsed.metadata.state = state
|
||||||
|
- debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||||
|
- if (debug.enabled) {
|
||||||
|
- const s = JSON.stringify(parsed.data, null, 2)
|
||||||
|
- debug(s && s.length > 10000 ? parsed.data : s)
|
||||||
|
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) {
|
||||||
|
+ debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||||
|
+ if (debug.enabled) {
|
||||||
|
+ const s = JSON.stringify(parsed.data, null, 2)
|
||||||
|
+ debug(s && s.length > 10000 ? parsed.data : s)
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
|
||||||
|
if (this._mcBundle.length) { // End bundle
|
||||||
|
@@ -109,7 +111,13 @@ class Client extends EventEmitter {
|
||||||
|
this._hasBundlePacket = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
- emitPacket(parsed)
|
||||||
|
+ try {
|
||||||
|
+ emitPacket(parsed)
|
||||||
|
+ } catch (err) {
|
||||||
|
+ console.log('Client incorrectly handled packet ' + parsed.metadata.name)
|
||||||
|
+ console.error(err)
|
||||||
|
+ // todo investigate why it doesn't close the stream even if unhandled there
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -167,7 +175,10 @@ class Client extends EventEmitter {
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFatalError = (err) => {
|
||||||
|
- this.emit('error', err)
|
||||||
|
+ // todo find out what is trying to write after client disconnect
|
||||||
|
+ if(err.code !== 'ECONNABORTED') {
|
||||||
|
+ this.emit('error', err)
|
||||||
|
+ }
|
||||||
|
endSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -196,6 +207,8 @@ class Client extends EventEmitter {
|
||||||
|
serializer -> framer -> socket -> splitter -> deserializer */
|
||||||
|
if (this.serializer) {
|
||||||
|
this.serializer.end()
|
||||||
|
+ this.socket?.end()
|
||||||
|
+ this.socket?.emit('end')
|
||||||
|
} else {
|
||||||
|
if (this.socket) this.socket.end()
|
||||||
|
}
|
||||||
|
@@ -237,8 +250,11 @@ class Client extends EventEmitter {
|
||||||
|
|
||||||
|
write (name, params) {
|
||||||
|
if (!this.serializer.writable) { return }
|
||||||
|
- debug('writing packet ' + this.state + '.' + name)
|
||||||
|
- debug(params)
|
||||||
|
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) {
|
||||||
|
+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
|
||||||
|
+ debug(params)
|
||||||
|
+ }
|
||||||
|
+ this.emit('writePacket', name, params)
|
||||||
|
this.serializer.write({ name, params })
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||||
|
index 423085259176a10c9dfeb617f00975df077d02be..ae0558b7c53ab24474b22240e8a2f4e4cde02f19 100644
|
||||||
|
--- a/src/index.d.ts
|
||||||
|
+++ b/src/index.d.ts
|
||||||
|
@@ -134,6 +134,7 @@ declare module 'minecraft-protocol' {
|
||||||
|
sessionServer?: string
|
||||||
|
keepAlive?: boolean
|
||||||
|
closeTimeout?: number
|
||||||
|
+ closeTimeout?: number
|
||||||
|
noPongTimeout?: number
|
||||||
|
checkTimeoutInterval?: number
|
||||||
|
version?: string
|
||||||
|
@@ -154,6 +155,8 @@ declare module 'minecraft-protocol' {
|
||||||
|
disableChatSigning?: boolean
|
||||||
|
/** Pass custom client implementation if needed. */
|
||||||
|
Client?: Client
|
||||||
|
+ /** Can be used to prepare mc data on autoVersion (client.version has selected version) */
|
||||||
|
+ versionSelectedHook?: (client: Client) => Promise<void> | void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Server extends EventEmitter {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
||||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f69979165ab4 100644
|
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac392fbf41 100644
|
||||||
--- a/fonts/pixelart-icons-font.css
|
--- a/fonts/pixelart-icons-font.css
|
||||||
+++ b/fonts/pixelart-icons-font.css
|
+++ b/fonts/pixelart-icons-font.css
|
||||||
@@ -1,16 +1,13 @@
|
@@ -1,16 +1,13 @@
|
||||||
|
|
@ -10,9 +10,8 @@ index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f699
|
||||||
+ src:
|
+ src:
|
||||||
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
||||||
url("pixelart-icons-font.woff?t=1711815892278") format("woff"),
|
url("pixelart-icons-font.woff?t=1711815892278") format("woff"),
|
||||||
- url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||||
- url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */
|
- url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */
|
||||||
+ url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
||||||
|
|
|
||||||
16
patches/three@0.154.0.patch
Normal file
16
patches/three@0.154.0.patch
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
diff --git a/examples/jsm/webxr/VRButton.js b/examples/jsm/webxr/VRButton.js
|
||||||
|
index 6856a21b17aa45d7922bbf776fd2d7e63c7a9b4e..0925b706f7629bd52f0bb5af469536af8f5fce2c 100644
|
||||||
|
--- a/examples/jsm/webxr/VRButton.js
|
||||||
|
+++ b/examples/jsm/webxr/VRButton.js
|
||||||
|
@@ -62,7 +62,10 @@ class VRButton {
|
||||||
|
// ('local' is always available for immersive sessions and doesn't need to
|
||||||
|
// be requested separately.)
|
||||||
|
|
||||||
|
- const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] };
|
||||||
|
+ const sessionInit = {
|
||||||
|
+ optionalFeatures: ['local-floor', 'bounded-floor', 'layers'],
|
||||||
|
+ domOverlay: { root: document.body },
|
||||||
|
+ };
|
||||||
|
navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
|
||||||
|
|
||||||
|
} else {
|
||||||
13522
pnpm-lock.yaml
generated
13522
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
||||||
packages:
|
packages:
|
||||||
- "."
|
- "."
|
||||||
- "renderer"
|
- "prismarine-viewer"
|
||||||
- "renderer/viewer/sign-renderer/"
|
- "prismarine-viewer/viewer/sign-renderer/"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# Prismarine Viewer
|
|
||||||
|
|
||||||
Renamed to `renderer`.
|
|
||||||
|
|
||||||
For more info see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
|
|
@ -22,28 +22,23 @@ const buildOptions = {
|
||||||
},
|
},
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')],
|
entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')],
|
||||||
minify: !watch,
|
minify: true,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
drop: !watch ? [
|
drop: !watch ? [
|
||||||
'debugger'
|
'debugger'
|
||||||
] : [],
|
] : [],
|
||||||
sourcemap: 'linked',
|
sourcemap: 'linked',
|
||||||
target: watch ? undefined : ['ios14'],
|
|
||||||
write: false,
|
write: false,
|
||||||
metafile: true,
|
metafile: true,
|
||||||
outdir: path.join(__dirname, './dist'),
|
outdir: path.join(__dirname, './dist'),
|
||||||
define: {
|
define: {
|
||||||
'process.env.BROWSER': '"true"',
|
'process.env.BROWSER': '"true"',
|
||||||
},
|
},
|
||||||
loader: {
|
|
||||||
'.png': 'dataurl',
|
|
||||||
'.obj': 'text'
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
...mesherSharedPlugins,
|
...mesherSharedPlugins,
|
||||||
{
|
{
|
||||||
name: 'external-json',
|
name: 'external-json',
|
||||||
setup(build) {
|
setup (build) {
|
||||||
build.onResolve({ filter: /\.json$/ }, args => {
|
build.onResolve({ filter: /\.json$/ }, args => {
|
||||||
const fileName = args.path.split('/').pop().replace('.json', '')
|
const fileName = args.path.split('/').pop().replace('.json', '')
|
||||||
if (args.resolveDir.includes('minecraft-data')) {
|
if (args.resolveDir.includes('minecraft-data')) {
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
//@ts-nocheck
|
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import '../../src/getCollisionShapes'
|
import '../../src/getCollisionShapes'
|
||||||
|
|
@ -18,14 +17,12 @@ import { WorldDataEmitter } from '../viewer'
|
||||||
import { Viewer } from '../viewer/lib/viewer'
|
import { Viewer } from '../viewer/lib/viewer'
|
||||||
import { BlockNames } from '../../src/mcDataTypes'
|
import { BlockNames } from '../../src/mcDataTypes'
|
||||||
import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats'
|
import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats'
|
||||||
import { defaultWorldRendererConfig } from '../viewer/lib/worldrendererCommon'
|
|
||||||
import { getSyncWorld } from './shared'
|
import { getSyncWorld } from './shared'
|
||||||
|
|
||||||
window.THREE = THREE
|
window.THREE = THREE
|
||||||
|
|
||||||
export class BasePlaygroundScene {
|
export class BasePlaygroundScene {
|
||||||
continuousRender = false
|
continuousRender = false
|
||||||
stopRender = false
|
|
||||||
guiParams = {}
|
guiParams = {}
|
||||||
viewDistance = 0
|
viewDistance = 0
|
||||||
targetPos = new Vec3(2, 90, 2)
|
targetPos = new Vec3(2, 90, 2)
|
||||||
|
|
@ -51,15 +48,6 @@ export class BasePlaygroundScene {
|
||||||
windowHidden = false
|
windowHidden = false
|
||||||
world: ReturnType<typeof getSyncWorld>
|
world: ReturnType<typeof getSyncWorld>
|
||||||
|
|
||||||
_worldConfig = defaultWorldRendererConfig
|
|
||||||
get worldConfig () {
|
|
||||||
return this._worldConfig
|
|
||||||
}
|
|
||||||
set worldConfig (value) {
|
|
||||||
this._worldConfig = value
|
|
||||||
viewer.world.config = value
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
void this.initData().then(() => {
|
void this.initData().then(() => {
|
||||||
this.addKeyboardShortcuts()
|
this.addKeyboardShortcuts()
|
||||||
|
|
@ -67,19 +55,16 @@ export class BasePlaygroundScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
onParamsUpdate (paramName: string, object: any) {}
|
onParamsUpdate (paramName: string, object: any) {}
|
||||||
updateQs (paramName: string, valueSet: any) {
|
updateQs () {
|
||||||
if (this.skipUpdateQs) return
|
if (this.skipUpdateQs) return
|
||||||
const newQs = new URLSearchParams(window.location.search)
|
const oldQs = new URLSearchParams(window.location.search)
|
||||||
// if (oldQs.get('scene')) {
|
const newQs = new URLSearchParams()
|
||||||
// newQs.set('scene', oldQs.get('scene')!)
|
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
|
for (const [key, value] of Object.entries(this.params)) {
|
||||||
if (value) {
|
if (!value || typeof value === 'function' || this.params.skipQs?.includes(key) || this.alwaysIgnoreQs.includes(key)) continue
|
||||||
newQs.set(key, value)
|
newQs.set(key, value)
|
||||||
} else {
|
|
||||||
newQs.delete(key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`)
|
window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
@ -103,20 +88,7 @@ export class BasePlaygroundScene {
|
||||||
if (option?.hide) continue
|
if (option?.hide) continue
|
||||||
this.gui.add(this.params, param, option?.options ?? option?.min, option?.max)
|
this.gui.add(this.params, param, option?.options ?? option?.min, option?.max)
|
||||||
}
|
}
|
||||||
if (window.innerHeight < 700) {
|
this.gui.open(false)
|
||||||
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 }) => {
|
this.gui.onChange(({ property, object }) => {
|
||||||
if (object === this.params) {
|
if (object === this.params) {
|
||||||
|
|
@ -128,18 +100,16 @@ export class BasePlaygroundScene {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.updateQs(property, value)
|
|
||||||
} else {
|
} else {
|
||||||
this.onParamsUpdate(property, object)
|
this.onParamsUpdate(property, object)
|
||||||
}
|
}
|
||||||
|
this.updateQs()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// mainChunk: import('prismarine-chunk/types/index').PCChunk
|
// mainChunk: import('prismarine-chunk/types/index').PCChunk
|
||||||
|
|
||||||
// overridables
|
|
||||||
setupWorld () { }
|
setupWorld () { }
|
||||||
sceneReset () {}
|
|
||||||
|
|
||||||
// eslint-disable-next-line max-params
|
// eslint-disable-next-line max-params
|
||||||
addWorldBlock (xOffset: number, yOffset: number, zOffset: number, blockName: BlockNames, properties?: Record<string, any>) {
|
addWorldBlock (xOffset: number, yOffset: number, zOffset: number, blockName: BlockNames, properties?: Record<string, any>) {
|
||||||
|
|
@ -147,7 +117,7 @@ export class BasePlaygroundScene {
|
||||||
const block =
|
const block =
|
||||||
properties ?
|
properties ?
|
||||||
this.Block.fromProperties(loadedData.blocksByName[blockName].id, properties ?? {}, 0) :
|
this.Block.fromProperties(loadedData.blocksByName[blockName].id, properties ?? {}, 0) :
|
||||||
this.Block.fromStateId(loadedData.blocksByName[blockName].defaultState, 0)
|
this.Block.fromStateId(loadedData.blocksByName[blockName].defaultState!, 0)
|
||||||
this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block)
|
this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,9 +158,8 @@ export class BasePlaygroundScene {
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
|
||||||
// Create viewer
|
// Create viewer
|
||||||
const viewer = new Viewer(renderer, this.worldConfig)
|
const viewer = new Viewer(renderer, { numWorkers: 6, showChunkBorders: false, })
|
||||||
window.viewer = viewer
|
window.viewer = viewer
|
||||||
window.world = window.viewer.world
|
|
||||||
const isWebgpu = false
|
const isWebgpu = false
|
||||||
const promises = [] as Array<Promise<void>>
|
const promises = [] as Array<Promise<void>>
|
||||||
if (isWebgpu) {
|
if (isWebgpu) {
|
||||||
|
|
@ -299,14 +268,12 @@ export class BasePlaygroundScene {
|
||||||
|
|
||||||
loop () {
|
loop () {
|
||||||
if (this.continuousRender && !this.windowHidden) {
|
if (this.continuousRender && !this.windowHidden) {
|
||||||
this.render(true)
|
this.render()
|
||||||
requestAnimationFrame(() => this.loop())
|
requestAnimationFrame(() => this.loop())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render (fromLoop = false) {
|
render () {
|
||||||
if (!fromLoop && this.continuousRender) return
|
|
||||||
if (this.stopRender) return
|
|
||||||
statsStart()
|
statsStart()
|
||||||
viewer.render()
|
viewer.render()
|
||||||
statsEnd()
|
statsEnd()
|
||||||
|
|
@ -314,19 +281,9 @@ export class BasePlaygroundScene {
|
||||||
|
|
||||||
addKeyboardShortcuts () {
|
addKeyboardShortcuts () {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
if (e.code === 'KeyR' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||||
if (e.code === 'KeyR') {
|
this.controls?.reset()
|
||||||
this.controls?.reset()
|
this.resetCamera()
|
||||||
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', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
|
@ -340,9 +297,6 @@ export class BasePlaygroundScene {
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateKeys = () => {
|
const updateKeys = () => {
|
||||||
if (pressedKeys.has('ControlLeft') || pressedKeys.has('MetaLeft')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// if (typeof viewer === 'undefined') return
|
// if (typeof viewer === 'undefined') return
|
||||||
// Create a vector that points in the direction the camera is looking
|
// Create a vector that points in the direction the camera is looking
|
||||||
const direction = new THREE.Vector3(0, 0, 0)
|
const direction = new THREE.Vector3(0, 0, 0)
|
||||||
11
prismarine-viewer/examples/playground.ts
Normal file
11
prismarine-viewer/examples/playground.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
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']
|
||||||
|
playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||||
|
|
||||||
|
const scene = new Scene()
|
||||||
|
globalThis.scene = scene
|
||||||
|
|
@ -4,14 +4,10 @@ import { proxy, useSnapshot } from 'valtio'
|
||||||
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
|
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
|
||||||
import { css } from '@emotion/css'
|
import { css } from '@emotion/css'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import useLongPress from '../../src/react/useLongPress'
|
|
||||||
import { isMobile } from '../viewer/lib/simpleUtils'
|
|
||||||
|
|
||||||
export const playgroundGlobalUiState = proxy({
|
export const playgroundGlobalUiState = proxy({
|
||||||
scenes: [] as string[],
|
scenes: [] as string[],
|
||||||
selected: '',
|
selected: ''
|
||||||
selectorOpened: false,
|
|
||||||
actions: {} as Record<string, () => void>,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
renderToDom(<Playground />)
|
renderToDom(<Playground />)
|
||||||
|
|
@ -21,7 +17,7 @@ function Playground () {
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.innerHTML = /* css */ `
|
style.innerHTML = /* css */ `
|
||||||
.lil-gui {
|
.lil-gui {
|
||||||
top: 60px !important;
|
top: 40px !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
@ -37,31 +33,24 @@ function Playground () {
|
||||||
}}>
|
}}>
|
||||||
<Controls />
|
<Controls />
|
||||||
<SceneSelector />
|
<SceneSelector />
|
||||||
<ActionsSelector />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function SceneSelector () {
|
function SceneSelector () {
|
||||||
const mobile = isMobile()
|
|
||||||
const { scenes, selected } = useSnapshot(playgroundGlobalUiState)
|
const { scenes, selected } = useSnapshot(playgroundGlobalUiState)
|
||||||
const longPressEvents = useLongPress(() => {
|
|
||||||
playgroundGlobalUiState.selectorOpened = true
|
|
||||||
}, () => { })
|
|
||||||
|
|
||||||
return <div
|
return <div style={{
|
||||||
style={{
|
position: 'fixed',
|
||||||
position: 'fixed',
|
top: 0,
|
||||||
top: 0,
|
left: 0,
|
||||||
left: 0,
|
}}>
|
||||||
}} {...longPressEvents}>
|
|
||||||
{scenes.map(scene => <div
|
{scenes.map(scene => <div
|
||||||
key={scene}
|
key={scene}
|
||||||
style={{
|
style={{
|
||||||
padding: mobile ? '5px' : '2px 5px',
|
padding: '2px 5px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
background: scene === selected ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.6)',
|
background: scene === selected ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.6)',
|
||||||
fontWeight: scene === selected ? 'bold' : 'normal',
|
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const qs = new URLSearchParams(window.location.search)
|
const qs = new URLSearchParams(window.location.search)
|
||||||
|
|
@ -72,41 +61,6 @@ function SceneSelector () {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionsSelector = () => {
|
|
||||||
const { actions, selectorOpened } = useSnapshot(playgroundGlobalUiState)
|
|
||||||
|
|
||||||
if (!selectorOpened) return null
|
|
||||||
return <div style={{
|
|
||||||
position: 'fixed',
|
|
||||||
inset: 0,
|
|
||||||
background: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
zIndex: 10,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 5,
|
|
||||||
fontSize: 24,
|
|
||||||
}}>{Object.entries({
|
|
||||||
...actions,
|
|
||||||
'Close' () {
|
|
||||||
playgroundGlobalUiState.selectorOpened = false
|
|
||||||
}
|
|
||||||
}).map(([name, action]) => <div
|
|
||||||
key={name}
|
|
||||||
style={{
|
|
||||||
padding: '2px 5px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
userSelect: 'none',
|
|
||||||
background: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
action()
|
|
||||||
playgroundGlobalUiState.selectorOpened = false
|
|
||||||
}}
|
|
||||||
>{name}</div>)}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Controls = () => {
|
const Controls = () => {
|
||||||
// todo setting
|
// todo setting
|
||||||
const usingTouch = navigator.maxTouchPoints > 0
|
const usingTouch = navigator.maxTouchPoints > 0
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//@ts-nocheck
|
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import { BasePlaygroundScene } from '../baseScene'
|
import { BasePlaygroundScene } from '../baseScene'
|
||||||
import { WorldRendererThree } from '../../viewer/three/worldrendererThree'
|
import { WorldRendererThree } from '../../viewer/lib/worldrendererThree'
|
||||||
|
|
||||||
export default class extends BasePlaygroundScene {
|
export default class extends BasePlaygroundScene {
|
||||||
continuousRender = true
|
continuousRender = true
|
||||||
85
prismarine-viewer/examples/scenes/frequentUpdates.ts
Normal file
85
prismarine-viewer/examples/scenes/frequentUpdates.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { BasePlaygroundScene } from '../baseScene'
|
||||||
|
|
||||||
|
export default class extends BasePlaygroundScene {
|
||||||
|
viewDistance = 5
|
||||||
|
continuousRender = true
|
||||||
|
|
||||||
|
override initGui (): void {
|
||||||
|
this.params = {
|
||||||
|
squareSize: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
super.initGui()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTimer () {
|
||||||
|
// const limit = 1000
|
||||||
|
// const limit = 100
|
||||||
|
const limit = 1
|
||||||
|
const updatedChunks = new Set<string>()
|
||||||
|
const updatedBlocks = new Set<string>()
|
||||||
|
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.params.squareSize ??= 30
|
||||||
|
const { squareSize } = this.params
|
||||||
|
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)
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,5 +7,3 @@ export { default as transparencyIssue } from './transparencyIssue'
|
||||||
export { default as rotationIssue } from './rotationIssue'
|
export { default as rotationIssue } from './rotationIssue'
|
||||||
export { default as entities } from './entities'
|
export { default as entities } from './entities'
|
||||||
export { default as frequentUpdates } from './frequentUpdates'
|
export { default as frequentUpdates } from './frequentUpdates'
|
||||||
export { default as slabsOptimization } from './slabsOptimization'
|
|
||||||
export { default as allEntities } from './allEntities'
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//@ts-nocheck
|
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import { BasePlaygroundScene } from '../baseScene'
|
import { BasePlaygroundScene } from '../baseScene'
|
||||||
import { WorldRendererThree } from '../../viewer/three/worldrendererThree'
|
import { WorldRendererThree } from '../../viewer/lib/worldrendererThree'
|
||||||
|
|
||||||
export default class extends BasePlaygroundScene {
|
export default class extends BasePlaygroundScene {
|
||||||
continuousRender = true
|
continuousRender = true
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
//@ts-nocheck
|
|
||||||
// eslint-disable-next-line import/no-named-as-default
|
// eslint-disable-next-line import/no-named-as-default
|
||||||
import GUI, { Controller } from 'lil-gui'
|
import GUI, { Controller } from 'lil-gui'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { BasePlaygroundScene } from '../baseScene'
|
import { BasePlaygroundScene } from '../baseScene'
|
||||||
import { TWEEN_DURATION } from '../../viewer/three/entities'
|
import { TWEEN_DURATION } from '../../viewer/lib/entities'
|
||||||
import { EntityMesh } from '../../viewer/three/entity/EntityMesh'
|
import { EntityMesh } from '../../viewer/lib/entity/EntityMesh'
|
||||||
|
|
||||||
class MainScene extends BasePlaygroundScene {
|
class MainScene extends BasePlaygroundScene {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
|
|
@ -113,7 +112,7 @@ class MainScene extends BasePlaygroundScene {
|
||||||
this.entityUpdateShared()
|
this.entityUpdateShared()
|
||||||
if (!this.params.entity) return
|
if (!this.params.entity) return
|
||||||
if (this.params.entity === 'player') {
|
if (this.params.entity === 'player') {
|
||||||
viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, undefined, true, true)
|
viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, true, true)
|
||||||
viewer.entities.playAnimation('id', 'running')
|
viewer.entities.playAnimation('id', 'running')
|
||||||
}
|
}
|
||||||
// let prev = false
|
// let prev = false
|
||||||
|
|
@ -174,6 +173,7 @@ class MainScene extends BasePlaygroundScene {
|
||||||
canvas.height = size
|
canvas.height = size
|
||||||
renderer.setSize(size, size)
|
renderer.setSize(size, size)
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||||
viewer.scene.background = null
|
viewer.scene.background = null
|
||||||
|
|
||||||
|
|
@ -222,7 +222,7 @@ class MainScene extends BasePlaygroundScene {
|
||||||
})
|
})
|
||||||
|
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
zip.file('description.txt', 'Generated with mcraft.fun/playground')
|
zip.file('description.txt', 'Generated with prismarine-viewer')
|
||||||
|
|
||||||
const end = async () => {
|
const end = async () => {
|
||||||
// download zip file
|
// download zip file
|
||||||
|
|
@ -295,7 +295,7 @@ class MainScene extends BasePlaygroundScene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
worldView!.setBlockStateId(this.targetPos, block.stateId ?? 0)
|
worldView!.setBlockStateId(this.targetPos, block.stateId!)
|
||||||
console.log('up stateId', block.stateId)
|
console.log('up stateId', block.stateId)
|
||||||
this.params.metadata = block.metadata
|
this.params.metadata = block.metadata
|
||||||
this.metadataGui.updateDisplay()
|
this.metadataGui.updateDisplay()
|
||||||
|
|
@ -65,7 +65,7 @@ function getAllMethods (obj) {
|
||||||
return [...methods] as string[]
|
return [...methods] as string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => Promise<void>, chunkSize = 1) => {
|
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => void, chunkSize = 1) => {
|
||||||
// if delay is 0 then don't use setTimeout
|
// if delay is 0 then don't use setTimeout
|
||||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
if (delay) {
|
if (delay) {
|
||||||
|
|
@ -74,6 +74,6 @@ export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item:
|
||||||
setTimeout(resolve, delay)
|
setTimeout(resolve, delay)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await exec(arr[i], i)
|
exec(arr[i], i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "renderer",
|
"name": "prismarine-viewer",
|
||||||
"version": "1.25.0",
|
"version": "1.25.0",
|
||||||
"description": "Web based viewer",
|
"description": "Web based viewer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
"prismarine-block": "^1.7.3",
|
"prismarine-block": "^1.7.3",
|
||||||
"prismarine-chunk": "^1.22.0",
|
"prismarine-chunk": "^1.22.0",
|
||||||
"prismarine-schematic": "^1.2.0",
|
"prismarine-schematic": "^1.2.0",
|
||||||
"renderer": "link:./",
|
"prismarine-viewer": "link:./",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"socket.io": "^4.0.0",
|
"socket.io": "^4.0.0",
|
||||||
"socket.io-client": "^4.0.0",
|
"socket.io-client": "^4.0.0",
|
||||||
|
|
@ -11,17 +11,11 @@
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
touch-action: none;
|
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core';
|
import { defineConfig, mergeRsbuildConfig } from '@rsbuild/core';
|
||||||
import supportedVersions from '../src/supportedVersions.mjs'
|
import supportedVersions from '../src/supportedVersions.mjs'
|
||||||
import childProcess from 'child_process'
|
import childProcess from 'child_process'
|
||||||
import path, { dirname, join } from 'path'
|
import path, { dirname, join } from 'path'
|
||||||
import { pluginReact } from '@rsbuild/plugin-react';
|
import { pluginReact } from '@rsbuild/plugin-react';
|
||||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import fsExtra from 'fs-extra'
|
|
||||||
import { appAndRendererSharedConfig, rspackViewerConfig } from './rsbuildSharedConfig';
|
import { appAndRendererSharedConfig, rspackViewerConfig } from './rsbuildSharedConfig';
|
||||||
|
|
||||||
const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json')
|
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)) {
|
if (!fs.existsSync(mcDataPath)) {
|
||||||
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: path.join(__dirname, '..') })
|
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: path.join(__dirname, '..') })
|
||||||
}
|
}
|
||||||
|
|
@ -35,25 +30,11 @@ export default mergeRsbuildConfig(
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
entry: {
|
entry: {
|
||||||
index: join(__dirname, './playground/playground.ts')
|
index: join(__dirname, './examples/playground.ts')
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'globalThis.includedVersions': JSON.stringify(supportedVersions),
|
'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())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
@ -2,7 +2,6 @@ import { defineConfig, ModifyRspackConfigUtils } from '@rsbuild/core';
|
||||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||||
import { pluginReact } from '@rsbuild/plugin-react';
|
import { pluginReact } from '@rsbuild/plugin-react';
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
|
||||||
|
|
||||||
export const appAndRendererSharedConfig = () => defineConfig({
|
export const appAndRendererSharedConfig = () => defineConfig({
|
||||||
dev: {
|
dev: {
|
||||||
|
|
@ -47,7 +46,7 @@ export const appAndRendererSharedConfig = () => defineConfig({
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
htmlFallback: false,
|
htmlFallback: false,
|
||||||
// publicDir: false,
|
publicDir: false,
|
||||||
headers: {
|
headers: {
|
||||||
// enable shared array buffer
|
// enable shared array buffer
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
|
@ -61,36 +60,24 @@ export const appAndRendererSharedConfig = () => defineConfig({
|
||||||
],
|
],
|
||||||
tools: {
|
tools: {
|
||||||
rspack (config, helpers) {
|
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)
|
rspackViewerConfig(config, helpers)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
|
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
|
||||||
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data|prismarine-physics/, (resource) => {
|
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data/, (resource) => {
|
||||||
let absolute: string
|
let absolute: string
|
||||||
const request = resource.request.replaceAll('\\', '/')
|
const request = resource.request.replaceAll('\\', '/')
|
||||||
absolute = path.join(resource.context, request).replaceAll('\\', '/')
|
absolute = path.join(resource.context, request).replaceAll('\\', '/')
|
||||||
if (request.includes('minecraft-data/data/pc/1.') || request.includes('prismarine-physics')) {
|
if (request.includes('minecraft-data/data/pc/1.')) {
|
||||||
console.log('Error: incompatible resource', request, 'from', resource.contextInfo.issuer)
|
console.log('Error: incompatible resource', request, resource.contextInfo.issuer)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
||||||
}
|
}
|
||||||
if (absolute.endsWith('/minecraft-data/data.js')) {
|
if (absolute.endsWith('/minecraft-data/data.js')) {
|
||||||
resource.request = path.join(__dirname, `../src/shims/minecraftData.ts`)
|
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([
|
addRules([
|
||||||
{
|
{
|
||||||
|
|
@ -104,21 +91,11 @@ export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }:
|
||||||
{
|
{
|
||||||
test: /\.mp3$/,
|
test: /\.mp3$/,
|
||||||
type: 'asset/source',
|
type: 'asset/source',
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.txt$/,
|
|
||||||
type: 'asset/source',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.log$/,
|
|
||||||
type: 'asset/source',
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
config.ignoreWarnings = [
|
config.ignoreWarnings = [
|
||||||
/the request of a dependency is an expression/,
|
/the request of a dependency is an expression/,
|
||||||
/Unsupported pseudo class or element: xr-overlay/
|
/Unsupported pseudo class or element: xr-overlay/
|
||||||
]
|
]
|
||||||
if (process.env.SINGLE_FILE_BUILD === 'true') {
|
|
||||||
config.module!.parser!.javascript!.dynamicImportMode = 'eager'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
6
prismarine-viewer/viewer/index.js
Normal file
6
prismarine-viewer/viewer/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
Viewer: require('./lib/viewer').Viewer,
|
||||||
|
WorldDataEmitter: require('./lib/worldDataEmitter').WorldDataEmitter,
|
||||||
|
Entity: require('./lib/entity/EntityMesh'),
|
||||||
|
getBufferFromStream: require('./lib/simpleUtils').getBufferFromStream
|
||||||
|
}
|
||||||
536
prismarine-viewer/viewer/lib/entities.ts
Normal file
536
prismarine-viewer/viewer/lib/entities.ts
Normal file
|
|
@ -0,0 +1,536 @@
|
||||||
|
//@ts-check
|
||||||
|
import EventEmitter from 'events'
|
||||||
|
import nbt from 'prismarine-nbt'
|
||||||
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||||
|
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
||||||
|
// todo replace with url
|
||||||
|
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||||
|
import { NameTagObject } from 'skinview3d/libs/nametag'
|
||||||
|
import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||||
|
import mojangson from 'mojangson'
|
||||||
|
import * as Entity from './entity/EntityMesh'
|
||||||
|
import { WalkingGeneralSwing } from './entity/animations'
|
||||||
|
import externalTexturesJson from './entity/externalTextures.json'
|
||||||
|
import { disposeObject } from './threeJsUtils'
|
||||||
|
|
||||||
|
export const TWEEN_DURATION = 120
|
||||||
|
|
||||||
|
type PlayerObjectType = PlayerObject & { animation?: PlayerAnimation }
|
||||||
|
|
||||||
|
function getUsernameTexture (username: string, { fontFamily = 'sans-serif' }: any) {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) throw new Error('Could not get 2d context')
|
||||||
|
|
||||||
|
const fontSize = 50
|
||||||
|
const padding = 5
|
||||||
|
ctx.font = `${fontSize}px ${fontFamily}`
|
||||||
|
|
||||||
|
const textWidth = ctx.measureText(username).width + padding * 2
|
||||||
|
|
||||||
|
canvas.width = textWidth
|
||||||
|
canvas.height = fontSize + padding * 2
|
||||||
|
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
ctx.font = `${fontSize}px ${fontFamily}`
|
||||||
|
ctx.fillStyle = 'white'
|
||||||
|
ctx.fillText(username, padding, fontSize)
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNametag = (entity, options, mesh) => {
|
||||||
|
if (entity.username !== undefined) {
|
||||||
|
if (mesh.children.some(c => c.name === 'nametag')) return // todo update
|
||||||
|
const canvas = getUsernameTexture(entity.username, options)
|
||||||
|
const tex = new THREE.Texture(canvas)
|
||||||
|
tex.needsUpdate = true
|
||||||
|
const spriteMat = new THREE.SpriteMaterial({ map: tex })
|
||||||
|
const sprite = new THREE.Sprite(spriteMat)
|
||||||
|
sprite.renderOrder = 1000
|
||||||
|
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
|
||||||
|
sprite.position.y += entity.height + 0.6
|
||||||
|
sprite.name = 'nametag'
|
||||||
|
|
||||||
|
mesh.add(sprite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo cleanup
|
||||||
|
const nametags = {}
|
||||||
|
|
||||||
|
function getEntityMesh (entity, scene, options, overrides) {
|
||||||
|
if (entity.name) {
|
||||||
|
try {
|
||||||
|
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
||||||
|
const entityName = entity.name.toLowerCase()
|
||||||
|
const e = new Entity.EntityMesh('1.16.4', entityName, scene, overrides)
|
||||||
|
|
||||||
|
if (e.mesh) {
|
||||||
|
addNametag(entity, options, e.mesh)
|
||||||
|
return e.mesh
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reportError?.(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return cube
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SceneEntity = THREE.Object3D & {
|
||||||
|
playerObject?: PlayerObject & {
|
||||||
|
animation?: PlayerAnimation
|
||||||
|
}
|
||||||
|
username?: string
|
||||||
|
additionalCleanup?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Entities extends EventEmitter {
|
||||||
|
entities = {} as Record<string, SceneEntity>
|
||||||
|
entitiesOptions: {
|
||||||
|
fontFamily?: string
|
||||||
|
} = {}
|
||||||
|
debugMode: string
|
||||||
|
onSkinUpdate: () => void
|
||||||
|
clock = new THREE.Clock()
|
||||||
|
rendering = true
|
||||||
|
itemsTexture: THREE.Texture | null = null
|
||||||
|
getItemUv: undefined | ((idOrName: number | string) => {
|
||||||
|
texture: THREE.Texture;
|
||||||
|
u: number;
|
||||||
|
v: number;
|
||||||
|
su?: number;
|
||||||
|
sv?: number;
|
||||||
|
size?: number;
|
||||||
|
})
|
||||||
|
|
||||||
|
constructor (public scene: THREE.Scene) {
|
||||||
|
super()
|
||||||
|
this.entitiesOptions = {}
|
||||||
|
this.debugMode = 'none'
|
||||||
|
this.onSkinUpdate = () => { }
|
||||||
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
for (const mesh of Object.values(this.entities)) {
|
||||||
|
this.scene.remove(mesh)
|
||||||
|
disposeObject(mesh)
|
||||||
|
}
|
||||||
|
this.entities = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.rendering = rendering
|
||||||
|
for (const ent of entity ? [entity] : Object.values(this.entities)) {
|
||||||
|
if (rendering) {
|
||||||
|
if (!this.scene.children.includes(ent)) this.scene.add(ent)
|
||||||
|
} else {
|
||||||
|
this.scene.remove(ent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const dt = this.clock.getDelta()
|
||||||
|
for (const entityId of Object.keys(this.entities)) {
|
||||||
|
const playerObject = this.getPlayerObject(entityId)
|
||||||
|
if (playerObject?.animation) {
|
||||||
|
playerObject.animation.update(playerObject, dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerObject (entityId: string | number) {
|
||||||
|
const playerObject = this.entities[entityId]?.playerObject as PlayerObjectType | undefined
|
||||||
|
return playerObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixme workaround
|
||||||
|
defaultSteveTexture
|
||||||
|
|
||||||
|
// true means use default skin url
|
||||||
|
updatePlayerSkin (entityId: string | number, username: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
||||||
|
let playerObject = this.getPlayerObject(entityId)
|
||||||
|
if (!playerObject) return
|
||||||
|
// const username = this.entities[entityId].username
|
||||||
|
// or https://mulv.vercel.app/
|
||||||
|
if (skinUrl === true) {
|
||||||
|
skinUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=skin`
|
||||||
|
if (!username) return
|
||||||
|
}
|
||||||
|
loadImage(skinUrl).then(image => {
|
||||||
|
playerObject = this.getPlayerObject(entityId)
|
||||||
|
if (!playerObject) return
|
||||||
|
/** @type {THREE.CanvasTexture} */
|
||||||
|
let skinTexture
|
||||||
|
if (skinUrl === stevePng && this.defaultSteveTexture) {
|
||||||
|
skinTexture = this.defaultSteveTexture
|
||||||
|
} else {
|
||||||
|
const skinCanvas = document.createElement('canvas')
|
||||||
|
loadSkinToCanvas(skinCanvas, image)
|
||||||
|
skinTexture = new THREE.CanvasTexture(skinCanvas)
|
||||||
|
if (skinUrl === stevePng) {
|
||||||
|
this.defaultSteveTexture = skinTexture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skinTexture.magFilter = THREE.NearestFilter
|
||||||
|
skinTexture.minFilter = THREE.NearestFilter
|
||||||
|
skinTexture.needsUpdate = true
|
||||||
|
playerObject.skin.map = skinTexture
|
||||||
|
playerObject.skin.modelType = inferModelType(skinTexture.image)
|
||||||
|
|
||||||
|
const earsCanvas = document.createElement('canvas')
|
||||||
|
loadEarsToCanvasFromSkin(earsCanvas, image)
|
||||||
|
if (isCanvasBlank(earsCanvas)) {
|
||||||
|
playerObject.ears.map = null
|
||||||
|
playerObject.ears.visible = false
|
||||||
|
} else {
|
||||||
|
const earsTexture = new THREE.CanvasTexture(earsCanvas)
|
||||||
|
earsTexture.magFilter = THREE.NearestFilter
|
||||||
|
earsTexture.minFilter = THREE.NearestFilter
|
||||||
|
earsTexture.needsUpdate = true
|
||||||
|
//@ts-expect-error
|
||||||
|
playerObject.ears.map = earsTexture
|
||||||
|
playerObject.ears.visible = true
|
||||||
|
}
|
||||||
|
this.onSkinUpdate?.()
|
||||||
|
if (capeUrl) {
|
||||||
|
if (capeUrl === true) capeUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=cape`
|
||||||
|
loadImage(capeUrl).then(capeImage => {
|
||||||
|
playerObject = this.getPlayerObject(entityId)
|
||||||
|
if (!playerObject) return
|
||||||
|
const capeCanvas = document.createElement('canvas')
|
||||||
|
loadCapeToCanvas(capeCanvas, capeImage)
|
||||||
|
|
||||||
|
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||||
|
capeTexture.magFilter = THREE.NearestFilter
|
||||||
|
capeTexture.minFilter = THREE.NearestFilter
|
||||||
|
capeTexture.needsUpdate = true
|
||||||
|
//@ts-expect-error
|
||||||
|
playerObject.cape.map = capeTexture
|
||||||
|
playerObject.cape.visible = true
|
||||||
|
//@ts-expect-error
|
||||||
|
playerObject.elytra.map = capeTexture
|
||||||
|
this.onSkinUpdate?.()
|
||||||
|
|
||||||
|
if (!playerObject.backEquipment) {
|
||||||
|
playerObject.backEquipment = 'cape'
|
||||||
|
}
|
||||||
|
}, () => { })
|
||||||
|
}
|
||||||
|
}, () => { })
|
||||||
|
|
||||||
|
|
||||||
|
playerObject.cape.visible = false
|
||||||
|
if (!capeUrl) {
|
||||||
|
playerObject.backEquipment = null
|
||||||
|
playerObject.elytra.map = null
|
||||||
|
if (playerObject.cape.map) {
|
||||||
|
playerObject.cape.map.dispose()
|
||||||
|
}
|
||||||
|
playerObject.cape.map = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCanvasBlank (canvas) {
|
||||||
|
return !canvas.getContext('2d')
|
||||||
|
.getImageData(0, 0, canvas.width, canvas.height).data
|
||||||
|
.some(channel => channel !== 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle') {
|
||||||
|
const playerObject = this.getPlayerObject(entityPlayerId)
|
||||||
|
if (!playerObject) return
|
||||||
|
|
||||||
|
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 !== 'idle'
|
||||||
|
playerObject.animation.isRunning = animation === 'running'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
parseEntityLabel (jsonLike) {
|
||||||
|
if (!jsonLike) return
|
||||||
|
try {
|
||||||
|
const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike)
|
||||||
|
const text = flat(parsed).map(x => x.text)
|
||||||
|
return text.join('')
|
||||||
|
} catch (err) {
|
||||||
|
return jsonLike
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemMesh (item) {
|
||||||
|
const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
|
||||||
|
if (textureUv) {
|
||||||
|
// todo use geometry buffer uv instead!
|
||||||
|
const { u, v, size, su, sv, texture } = textureUv
|
||||||
|
const itemsTexture = texture.clone()
|
||||||
|
itemsTexture.flipY = true
|
||||||
|
const sizeY = (sv ?? size)!
|
||||||
|
const sizeX = (su ?? size)!
|
||||||
|
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), [
|
||||||
|
// top left and right bottom are black box materials others are transparent
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||||
|
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||||
|
material, materialFlipped,
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
mesh,
|
||||||
|
itemsTexture,
|
||||||
|
itemsTextureFlipped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update (entity: import('prismarine-entity').Entity & { delete?; pos }, overrides) {
|
||||||
|
console.log('entity', entity)
|
||||||
|
const isPlayerModel = entity.name === 'player'
|
||||||
|
if (entity.name === 'zombie' || 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 (!this.entities[entity.id] && !entity.delete) {
|
||||||
|
const group = new THREE.Group()
|
||||||
|
let mesh
|
||||||
|
if (entity.name === 'item') {
|
||||||
|
const item = entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
||||||
|
if (item) {
|
||||||
|
const object = this.getItemMesh(item)
|
||||||
|
if (object) {
|
||||||
|
mesh = object.mesh
|
||||||
|
mesh.scale.set(0.5, 0.5, 0.5)
|
||||||
|
mesh.position.set(0, 0.2, 0)
|
||||||
|
// set faces
|
||||||
|
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||||
|
// viewer.scene.add(mesh)
|
||||||
|
const clock = new THREE.Clock()
|
||||||
|
mesh.onBeforeRender = () => {
|
||||||
|
const delta = clock.getDelta()
|
||||||
|
mesh.rotation.y += delta
|
||||||
|
}
|
||||||
|
//@ts-expect-error
|
||||||
|
group.additionalCleanup = () => {
|
||||||
|
// important: avoid texture memory leak and gpu slowdown
|
||||||
|
object.itemsTexture.dispose()
|
||||||
|
object.itemsTextureFlipped.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isPlayerModel) {
|
||||||
|
// CREATE NEW PLAYER ENTITY
|
||||||
|
const wrapper = new THREE.Group()
|
||||||
|
const playerObject = new PlayerObject() as PlayerObjectType
|
||||||
|
playerObject.position.set(0, 16, 0)
|
||||||
|
|
||||||
|
// fix issues with starfield
|
||||||
|
playerObject.traverse((obj) => {
|
||||||
|
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
||||||
|
obj.material.transparent = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//@ts-expect-error
|
||||||
|
wrapper.add(playerObject)
|
||||||
|
const scale = 1 / 16
|
||||||
|
wrapper.scale.set(scale, scale, scale)
|
||||||
|
|
||||||
|
if (entity.username) {
|
||||||
|
// todo proper colors
|
||||||
|
const nameTag = new NameTagObject(fromFormattedString(entity.username).text, {
|
||||||
|
font: `48px ${this.entitiesOptions.fontFamily}`,
|
||||||
|
})
|
||||||
|
nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
||||||
|
nameTag.renderOrder = 1000
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
wrapper.add(nameTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
group.playerObject = playerObject
|
||||||
|
wrapper.rotation.set(0, Math.PI, 0)
|
||||||
|
mesh = wrapper
|
||||||
|
playerObject.animation = new WalkingGeneralSwing()
|
||||||
|
//@ts-expect-error
|
||||||
|
playerObject.animation.isMoving = false
|
||||||
|
} else {
|
||||||
|
mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides)
|
||||||
|
}
|
||||||
|
if (!mesh) return
|
||||||
|
mesh.name = 'mesh'
|
||||||
|
// set initial position so there are no weird jumps update after
|
||||||
|
group.position.set(entity.pos.x, entity.pos.y, entity.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.scene.add(group)
|
||||||
|
|
||||||
|
this.entities[entity.id] = group
|
||||||
|
|
||||||
|
this.emit('add', entity)
|
||||||
|
|
||||||
|
if (isPlayerModel) {
|
||||||
|
this.updatePlayerSkin(entity.id, '', overrides?.texture || stevePng)
|
||||||
|
}
|
||||||
|
this.setDebugMode(this.debugMode, group)
|
||||||
|
this.setRendering(this.rendering, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
// set visibility
|
||||||
|
const isInvisible = entity.metadata?.[0] & 0x20
|
||||||
|
for (const child of this.entities[entity.id]?.children.find(c => c.name === 'mesh')?.children ?? []) {
|
||||||
|
if (child.name !== 'nametag') {
|
||||||
|
child.visible = !isInvisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---
|
||||||
|
// not player
|
||||||
|
const displayText = entity.metadata?.[3] && this.parseEntityLabel(entity.metadata[2])
|
||||||
|
if (entity.name !== 'player' && displayText) {
|
||||||
|
addNametag({ ...entity, username: displayText }, this.entitiesOptions, this.entities[entity.id].children.find(c => c.name === 'mesh'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo handle map, map_chunks events
|
||||||
|
// if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') {
|
||||||
|
// const example = {
|
||||||
|
// "present": true,
|
||||||
|
// "itemId": 847,
|
||||||
|
// "itemCount": 1,
|
||||||
|
// "nbtData": {
|
||||||
|
// "type": "compound",
|
||||||
|
// "name": "",
|
||||||
|
// "value": {
|
||||||
|
// "map": {
|
||||||
|
// "type": "int",
|
||||||
|
// "value": 2146483444
|
||||||
|
// },
|
||||||
|
// "interactiveboard": {
|
||||||
|
// "type": "byte",
|
||||||
|
// "value": 1
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// const item = entity.metadata?.[8]
|
||||||
|
// if (item.nbtData) {
|
||||||
|
// const nbt = nbt.simplify(item.nbtData)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
||||||
|
const e = this.entities[entity.id]
|
||||||
|
|
||||||
|
if (entity.username) {
|
||||||
|
e.username = entity.username
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e?.playerObject && overrides?.rotation?.head) {
|
||||||
|
const playerObject = e.playerObject as PlayerObjectType
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.delete && e) {
|
||||||
|
if (e.additionalCleanup) e.additionalCleanup()
|
||||||
|
this.emit('remove', entity)
|
||||||
|
this.scene.remove(e)
|
||||||
|
disposeObject(e)
|
||||||
|
// todo dispose textures as well ?
|
||||||
|
delete this.entities[entity.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.pos) {
|
||||||
|
new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, TWEEN_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 }, TWEEN_DURATION).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDamageEvent (entityId, damageAmount) {
|
||||||
|
const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
|
||||||
|
if (entityMesh) {
|
||||||
|
entityMesh.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
399
prismarine-viewer/viewer/lib/entity/EntityMesh.js
Normal file
399
prismarine-viewer/viewer/lib/entity/EntityMesh.js
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
//@ts-check
|
||||||
|
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 entities from './entities.json'
|
||||||
|
import { externalModels } from './objModels'
|
||||||
|
import externalTexturesJson from './externalTextures.json'
|
||||||
|
// import { loadTexture } from globalThis.isElectron ? '../utils.electron.js' : '../utils';
|
||||||
|
const { loadTexture } = globalThis.isElectron ? require('../utils.electron.js') : require('../utils')
|
||||||
|
|
||||||
|
const elemFaces = {
|
||||||
|
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, b) {
|
||||||
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
for (const pos of corners) {
|
||||||
|
const u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
|
||||||
|
const v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
|
||||||
|
|
||||||
|
const inflate = cube.inflate ?? 0
|
||||||
|
let vecPos = new THREE.Vector3(
|
||||||
|
cube.origin[0] + pos[0] * cube.size[0] + (pos[0] ? inflate : -inflate),
|
||||||
|
cube.origin[1] + pos[1] * cube.size[1] + (pos[1] ? inflate : -inflate),
|
||||||
|
cube.origin[2] + pos[2] * cube.size[2] + (pos[2] ? 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)
|
||||||
|
attr.uvs.push(u, v)
|
||||||
|
attr.skinIndices.push(boneId, 0, 0, 0)
|
||||||
|
attr.skinWeights.push(1, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMesh(texture, jsonModel, overrides = {}) {
|
||||||
|
const bones = {}
|
||||||
|
|
||||||
|
const 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) {
|
||||||
|
addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootBones = []
|
||||||
|
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)
|
||||||
|
|
||||||
|
loadTexture(texture, texture => {
|
||||||
|
if (material.map) {
|
||||||
|
// texture is already loaded
|
||||||
|
return
|
||||||
|
}
|
||||||
|
texture.magFilter = THREE.NearestFilter
|
||||||
|
texture.minFilter = THREE.NearestFilter
|
||||||
|
texture.flipY = false
|
||||||
|
texture.wrapS = THREE.RepeatWrapping
|
||||||
|
texture.wrapT = THREE.RepeatWrapping
|
||||||
|
material.map = texture
|
||||||
|
})
|
||||||
|
|
||||||
|
return mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
export const knownNotHandled = [
|
||||||
|
'area_effect_cloud', 'block_display',
|
||||||
|
'chest_boat', 'end_crystal',
|
||||||
|
'falling_block', 'furnace_minecart',
|
||||||
|
'giant', 'glow_item_frame',
|
||||||
|
'glow_squid', 'illusioner',
|
||||||
|
'interaction', 'item',
|
||||||
|
'item_display', 'item_frame',
|
||||||
|
'lightning_bolt', 'marker',
|
||||||
|
'painting', 'spawner_minecart',
|
||||||
|
'spectral_arrow', 'text_display',
|
||||||
|
'tnt', 'trader_llama', 'zombie_horse'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const temporaryMap = {
|
||||||
|
'furnace_minecart': 'minecart',
|
||||||
|
'spawner_minecart': 'minecart',
|
||||||
|
'chest_minecart': 'minecart',
|
||||||
|
'hopper_minecart': 'minecart',
|
||||||
|
'command_block_minecart': 'minecart',
|
||||||
|
'tnt_minecart': 'minecart',
|
||||||
|
'glow_squid': 'squid',
|
||||||
|
'trader_llama': 'llama',
|
||||||
|
'chest_boat': 'boat',
|
||||||
|
'spectral_arrow': 'arrow',
|
||||||
|
'husk': 'zombie',
|
||||||
|
'zombie_horse': 'horse',
|
||||||
|
'donkey': 'horse',
|
||||||
|
'skeleton_horse': 'horse',
|
||||||
|
'mule': 'horse',
|
||||||
|
'ocelot': 'cat',
|
||||||
|
// 'falling_block': 'block',
|
||||||
|
// 'lightning_bolt': 'lightning',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEntity = (name) => {
|
||||||
|
return entities[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// const externalModelsTextures = {
|
||||||
|
// allay: 'allay/allay',
|
||||||
|
// axolotl: 'axolotl/axolotl_blue',
|
||||||
|
// blaze: 'blaze',
|
||||||
|
// camel: 'camel/camel',
|
||||||
|
// cat: 'cat/black',
|
||||||
|
// chicken: 'chicken',
|
||||||
|
// cod: 'fish/cod',
|
||||||
|
// creeper: 'creeper/creeper',
|
||||||
|
// dolphin: 'dolphin',
|
||||||
|
// ender_dragon: 'enderdragon/dragon',
|
||||||
|
// enderman: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAABGdBTUEAALGPC/xhBQAAAY5JREFUaN7lWNESgzAI8yv8/z/tXjZPHSShYitb73rXedo1AQJ0WchY17WhudQZ7TS18Qb5AXtY/yUBO8tXIaCRqRNwXlcgwDJgmAALfBUP8AjYEdHnAZUIAGdvPy+CnobJIVw9DVIPEABawuEyyvYx1sMIMP8fAbUO7ukBImZmCCEP2AhglnRip8vio7MIxYEsaVkdeYNjYfbN/BBA1twP9AxpB0qlMwj48gBP5Ji1rXc8nfBImk6A5+KqShNwdTwgKy0xYRzdS4yoY651W8EDRwGVJEDVITGtjiEAaEBq3o4SwGqRVAKsdVYIsAzDCACV6VwCFMBCpqLvgudzQ6CnjL5afmeX4pdE0LIQuYCBzZbQfT4rC6COUQGn9B3MQ28pSIxDSDdNrKdQSZJ7lDurMeZm6iEjKVENh8cQgBowBFK5gEHhsO3xFA/oKXp6vg8RoHaD2QRkiaDnAYcZAcB+E6GTRVAhQCVJyVImKOUiBLW3KL4jzU2POHp64RIQ/ADO6D6Ry1gl9tlN1Xm+AK8s2jHadDijAAAAAElFTkSuQmCC',
|
||||||
|
// endermite: 'endermite',
|
||||||
|
// fox: 'fox/fox',
|
||||||
|
// frog: 'frog/cold_frog',
|
||||||
|
// ghast: 'ghast/ghast',
|
||||||
|
// goat: 'goat/goat',
|
||||||
|
// guardian: 'guardian',
|
||||||
|
// horse: 'horse/horse_brown',
|
||||||
|
// llama: 'llama/creamy',
|
||||||
|
// minecart: 'minecart',
|
||||||
|
// parrot: 'parrot/parrot_grey',
|
||||||
|
// piglin: 'piglin/piglin',
|
||||||
|
// pillager: 'illager/pillager',
|
||||||
|
// rabbit: 'rabbit/brown',
|
||||||
|
// sheep: 'sheep/sheep',
|
||||||
|
// shulker: 'shulker/shulker',
|
||||||
|
// sniffer: 'sniffer/sniffer',
|
||||||
|
// spider: 'spider/spider',
|
||||||
|
// tadpole: 'tadpole/tadpole',
|
||||||
|
// turtle: 'turtle/big_sea_turtle',
|
||||||
|
// vex: 'illager/vex',
|
||||||
|
// villager: 'villager/villager',
|
||||||
|
// warden: 'warden/warden',
|
||||||
|
// witch: 'witch',
|
||||||
|
// wolf: 'wolf/wolf',
|
||||||
|
// zombie_villager: 'zombie_villager/zombie_villager'
|
||||||
|
// }
|
||||||
|
|
||||||
|
const scaleEntity = {
|
||||||
|
zombie: 1.85,
|
||||||
|
husk: 1.85
|
||||||
|
}
|
||||||
|
const offsetEntity = {
|
||||||
|
zombie: new Vec3(0, 1, 0),
|
||||||
|
husk: new Vec3(0, 1, 0),
|
||||||
|
boat: new Vec3(0, -1, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||||
|
export class EntityMesh {
|
||||||
|
constructor(version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
|
||||||
|
const originalType = type
|
||||||
|
const mappedValue = temporaryMap[type]
|
||||||
|
if (mappedValue) type = mappedValue
|
||||||
|
|
||||||
|
if (externalModels[type]) {
|
||||||
|
const objLoader = new OBJLoader()
|
||||||
|
let texturePath = externalTexturesJson[type]
|
||||||
|
if (originalType === 'zombie_horse') {
|
||||||
|
texturePath = `textures/${version}/entity/horse/horse_zombie.png`
|
||||||
|
}
|
||||||
|
if (originalType === 'husk') {
|
||||||
|
texturePath = huskPng
|
||||||
|
}
|
||||||
|
if (originalType === 'skeleton_horse') {
|
||||||
|
texturePath = `textures/${version}/entity/horse/horse_skeleton.png`
|
||||||
|
}
|
||||||
|
if (originalType === 'donkey') {
|
||||||
|
texturePath = `textures/${version}/entity/horse/donkey.png`
|
||||||
|
}
|
||||||
|
if (originalType === 'mule') {
|
||||||
|
texturePath = `textures/${version}/entity/horse/mule.png`
|
||||||
|
}
|
||||||
|
if (originalType === 'ocelot') {
|
||||||
|
texturePath = `textures/${version}/entity/cat/ocelot.png`
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
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
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = getEntity(type)
|
||||||
|
if (!e) {
|
||||||
|
if (knownNotHandled.includes(type)) return
|
||||||
|
throw new Error(`Unknown entity ${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(texture + '.png', jsonModel, overrides)
|
||||||
|
mesh.name = `geometry_${name}`
|
||||||
|
this.mesh.add(mesh)
|
||||||
|
|
||||||
|
const skeletonHelper = new THREE.SkeletonHelper(mesh)
|
||||||
|
//@ts-expect-error
|
||||||
|
skeletonHelper.material.linewidth = 2
|
||||||
|
skeletonHelper.visible = false
|
||||||
|
this.mesh.add(skeletonHelper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStaticData(name) {
|
||||||
|
name = temporaryMap[name] || name
|
||||||
|
if (externalModels[name]) {
|
||||||
|
return {
|
||||||
|
boneNames: [] // todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const e = getEntity(name)
|
||||||
|
if (!e) throw new Error(`Unknown entity ${name}`)
|
||||||
|
return {
|
||||||
|
boneNames: Object.values(e.geometry).flatMap(x => x.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
prismarine-viewer/viewer/lib/entity/animations.js
Normal file
103
prismarine-viewer/viewer/lib/entity/animations.js
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { PlayerAnimation } from 'skinview3d'
|
||||||
|
|
||||||
|
export class WalkingGeneralSwing extends PlayerAnimation {
|
||||||
|
|
||||||
|
switchAnimationCallback
|
||||||
|
|
||||||
|
isRunning = false
|
||||||
|
isMoving = true
|
||||||
|
|
||||||
|
_startArmSwing
|
||||||
|
|
||||||
|
swingArm() {
|
||||||
|
this._startArmSwing = this.progress
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(player) {
|
||||||
|
// Multiply by animation's natural speed
|
||||||
|
let t
|
||||||
|
const updateT = () => {
|
||||||
|
if (!this.isMoving) {
|
||||||
|
t = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isRunning) {
|
||||||
|
t = this.progress * 10 + Math.PI * 0.5
|
||||||
|
} else {
|
||||||
|
t = this.progress * 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateT()
|
||||||
|
let reset = false
|
||||||
|
|
||||||
|
if ((this.isRunning ? Math.cos(t) : Math.sin(t)) < 0.01) {
|
||||||
|
if (this.switchAnimationCallback) {
|
||||||
|
reset = true
|
||||||
|
this.progress = 0
|
||||||
|
updateT()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRunning) {
|
||||||
|
// Leg swing with larger amplitude
|
||||||
|
player.skin.leftLeg.rotation.x = Math.cos(t + Math.PI) * 1.3
|
||||||
|
player.skin.rightLeg.rotation.x = Math.cos(t) * 1.3
|
||||||
|
} else {
|
||||||
|
// Leg swing
|
||||||
|
player.skin.leftLeg.rotation.x = Math.sin(t) * 0.5
|
||||||
|
player.skin.rightLeg.rotation.x = Math.sin(t + Math.PI) * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._startArmSwing) {
|
||||||
|
const tHand = (this.progress - this._startArmSwing) * 18 + Math.PI * 0.5
|
||||||
|
player.skin.rightArm.rotation.x = Math.cos(tHand) * 1.5
|
||||||
|
const basicArmRotationZ = Math.PI * 0.1
|
||||||
|
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.3 - basicArmRotationZ
|
||||||
|
|
||||||
|
if (tHand > Math.PI + Math.PI * 0.5) {
|
||||||
|
this._startArmSwing = null
|
||||||
|
player.skin.rightArm.rotation.z = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRunning) {
|
||||||
|
player.skin.leftArm.rotation.x = Math.cos(t) * 1.5
|
||||||
|
if (!this._startArmSwing) {
|
||||||
|
player.skin.rightArm.rotation.x = Math.cos(t + Math.PI) * 1.5
|
||||||
|
}
|
||||||
|
const basicArmRotationZ = Math.PI * 0.1
|
||||||
|
player.skin.leftArm.rotation.z = Math.cos(t) * 0.1 + basicArmRotationZ
|
||||||
|
if (!this._startArmSwing) {
|
||||||
|
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Arm swing
|
||||||
|
player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.5
|
||||||
|
if (!this._startArmSwing) {
|
||||||
|
player.skin.rightArm.rotation.x = Math.sin(t) * 0.5
|
||||||
|
}
|
||||||
|
const basicArmRotationZ = Math.PI * 0.02
|
||||||
|
player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ
|
||||||
|
if (!this._startArmSwing) {
|
||||||
|
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRunning) {
|
||||||
|
player.rotation.z = Math.cos(t + Math.PI) * 0.01
|
||||||
|
}
|
||||||
|
if (this.isRunning) {
|
||||||
|
const basicCapeRotationX = Math.PI * 0.3
|
||||||
|
player.cape.rotation.x = Math.sin(t * 2) * 0.1 + basicCapeRotationX
|
||||||
|
} else {
|
||||||
|
// Always add an angle for cape around the x axis
|
||||||
|
const basicCapeRotationX = Math.PI * 0.06
|
||||||
|
player.cape.rotation.x = Math.sin(t / 1.5) * 0.06 + basicCapeRotationX
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
this.switchAnimationCallback()
|
||||||
|
this.switchAnimationCallback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21476
prismarine-viewer/viewer/lib/entity/entities.json
Normal file
21476
prismarine-viewer/viewer/lib/entity/entities.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,8 +22,7 @@ export { default as parrot } from './models/parrot.obj'
|
||||||
export { default as piglin } from './models/piglin.obj'
|
export { default as piglin } from './models/piglin.obj'
|
||||||
export { default as pillager } from './models/pillager.obj'
|
export { default as pillager } from './models/pillager.obj'
|
||||||
export { default as rabbit } from './models/rabbit.obj'
|
export { default as rabbit } from './models/rabbit.obj'
|
||||||
export { default as sheep } from './models/sheep.obj'
|
// export { default as sheep } from './models/sheep.obj'
|
||||||
export { default as arrow } from './models/arrow.obj'
|
|
||||||
export { default as shulker } from './models/shulker.obj'
|
export { default as shulker } from './models/shulker.obj'
|
||||||
export { default as sniffer } from './models/sniffer.obj'
|
export { default as sniffer } from './models/sniffer.obj'
|
||||||
export { default as spider } from './models/spider.obj'
|
export { default as spider } from './models/spider.obj'
|
||||||
|
|
@ -35,4 +34,6 @@ export { default as warden } from './models/warden.obj'
|
||||||
export { default as witch } from './models/witch.obj'
|
export { default as witch } from './models/witch.obj'
|
||||||
export { default as wolf } from './models/wolf.obj'
|
export { default as wolf } from './models/wolf.obj'
|
||||||
export { default as zombie_villager } from './models/zombie_villager.obj'
|
export { default as zombie_villager } from './models/zombie_villager.obj'
|
||||||
|
export { default as zombie } from './models/zombie.obj'
|
||||||
|
export { default as husk } from './models/zombie.obj'
|
||||||
export { default as boat } from './models/boat.obj'
|
export { default as boat } from './models/boat.obj'
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue