Compare commits
1 commit
next
...
demo-emula
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d2358ea27 |
|
|
@ -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.
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# we dont want default config to be loaded in the dockerfile, but rather using a volume
|
||||
config.json
|
||||
# build stuff
|
||||
node_modules
|
||||
public
|
||||
|
|
@ -1,9 +1 @@
|
|||
node_modules
|
||||
rsbuild.config.ts
|
||||
*.module.css.d.ts
|
||||
*.generated.ts
|
||||
generated
|
||||
dist
|
||||
public
|
||||
**/*/rsbuildSharedConfig.ts
|
||||
src/mcDataTypes.ts
|
||||
144
.eslintrc.json
|
|
@ -1,76 +1,32 @@
|
|||
{
|
||||
"extends": [
|
||||
"zardoy",
|
||||
"plugin:@stylistic/disable-legacy"
|
||||
],
|
||||
"extends": "zardoy",
|
||||
"ignorePatterns": [
|
||||
"!*.js"
|
||||
],
|
||||
"plugins": [
|
||||
"@stylistic"
|
||||
"!*.js",
|
||||
"prismarine-viewer/"
|
||||
],
|
||||
"rules": {
|
||||
// style
|
||||
"@stylistic/space-infix-ops": "error",
|
||||
"@stylistic/no-multi-spaces": "error",
|
||||
"@stylistic/no-trailing-spaces": "error",
|
||||
"@stylistic/space-before-function-paren": "error",
|
||||
"@stylistic/array-bracket-spacing": "error",
|
||||
// would be great to have but breaks TS code like (url?) => ...
|
||||
// "@stylistic/arrow-parens": [
|
||||
// "error",
|
||||
// "as-needed"
|
||||
// ],
|
||||
"@stylistic/arrow-spacing": "error",
|
||||
"@stylistic/block-spacing": "error",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@stylistic/brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
{
|
||||
"allowSingleLine": true
|
||||
}
|
||||
],
|
||||
// too annoying to be forced to multi-line, probably should be enforced to never
|
||||
// "@stylistic/comma-dangle": [
|
||||
// "error",
|
||||
// "always-multiline"
|
||||
// ],
|
||||
"@stylistic/computed-property-spacing": "error",
|
||||
"@stylistic/dot-location": [
|
||||
"error",
|
||||
"property"
|
||||
],
|
||||
"@stylistic/eol-last": "error",
|
||||
"@stylistic/function-call-spacing": "error",
|
||||
"@stylistic/function-paren-newline": [
|
||||
"error",
|
||||
"consistent"
|
||||
],
|
||||
"@stylistic/generator-star-spacing": "error",
|
||||
"@stylistic/implicit-arrow-linebreak": "error",
|
||||
"@stylistic/indent-binary-ops": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"@stylistic/function-call-argument-newline": [
|
||||
"error",
|
||||
"consistent"
|
||||
],
|
||||
"@stylistic/space-in-parens": [
|
||||
"space-infix-ops": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"space-before-function-paren": "error",
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/object-curly-spacing": [
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"@stylistic/comma-spacing": "error",
|
||||
"@stylistic/semi": [
|
||||
"comma-spacing": "error",
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/indent": [
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
// todo maybe "always-multiline"?
|
||||
"only-multiline"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
|
|
@ -80,72 +36,13 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"@stylistic/quotes": [
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@stylistic/key-spacing": "error",
|
||||
"@stylistic/keyword-spacing": "error",
|
||||
// "@stylistic/line-comment-position": "error", // not needed
|
||||
// "@stylistic/lines-around-comment": "error", // also not sure if needed
|
||||
// "@stylistic/max-len": "error", // also not sure if needed
|
||||
// "@stylistic/linebreak-style": "error", // let git decide
|
||||
"@stylistic/max-statements-per-line": [
|
||||
"error",
|
||||
{
|
||||
"max": 5
|
||||
}
|
||||
],
|
||||
// "@stylistic/member-delimiter-style": "error",
|
||||
// "@stylistic/multiline-ternary": "error", // not needed
|
||||
// "@stylistic/newline-per-chained-call": "error", // not sure if needed
|
||||
"@stylistic/new-parens": "error",
|
||||
"@typescript-eslint/class-literal-property-style": "off",
|
||||
"@stylistic/no-confusing-arrow": "error",
|
||||
"@stylistic/wrap-iife": "error",
|
||||
"@stylistic/space-before-blocks": "error",
|
||||
"@stylistic/type-generic-spacing": "error",
|
||||
"@stylistic/template-tag-spacing": "error",
|
||||
"@stylistic/template-curly-spacing": "error",
|
||||
"@stylistic/type-annotation-spacing": "error",
|
||||
"@stylistic/jsx-child-element-spacing": "error",
|
||||
// buggy
|
||||
// "@stylistic/jsx-closing-bracket-location": "error",
|
||||
// "@stylistic/jsx-closing-tag-location": "error",
|
||||
"@stylistic/jsx-curly-brace-presence": "error",
|
||||
"@stylistic/jsx-curly-newline": "error",
|
||||
"@stylistic/jsx-curly-spacing": "error",
|
||||
"@stylistic/jsx-equals-spacing": "error",
|
||||
"@stylistic/jsx-first-prop-new-line": "error",
|
||||
"@stylistic/jsx-function-call-newline": "error",
|
||||
"@stylistic/jsx-max-props-per-line": [
|
||||
"error",
|
||||
{
|
||||
"maximum": 7
|
||||
}
|
||||
],
|
||||
"@stylistic/jsx-pascal-case": "error",
|
||||
"@stylistic/jsx-props-no-multi-spaces": "error",
|
||||
"@stylistic/jsx-self-closing-comp": "error",
|
||||
// "@stylistic/jsx-sort-props": [
|
||||
// "error",
|
||||
// {
|
||||
// "callbacksLast": false,
|
||||
// "shorthandFirst": true,
|
||||
// "shorthandLast": false,
|
||||
// "multiline": "ignore",
|
||||
// "ignoreCase": true,
|
||||
// "noSortAlphabetically": true,
|
||||
// "reservedFirst": [
|
||||
// "key",
|
||||
// "className"
|
||||
// ],
|
||||
// "locale": "auto"
|
||||
// }
|
||||
// ],
|
||||
// perf
|
||||
"import/no-deprecated": "off",
|
||||
// ---
|
||||
|
|
@ -155,7 +52,6 @@
|
|||
// intentional: improve readability in some cases
|
||||
"no-else-return": "off",
|
||||
"@typescript-eslint/padding-line-between-statements": "off",
|
||||
"@typescript-eslint/no-dynamic-delete": "off",
|
||||
"arrow-body-style": "off",
|
||||
"unicorn/prefer-ternary": "off",
|
||||
"unicorn/switch-case-braces": "off",
|
||||
|
|
@ -192,15 +88,13 @@
|
|||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
"unicorn/no-empty-file": "off",
|
||||
"unicorn/prefer-event-target": "off",
|
||||
"@typescript-eslint/member-ordering": "off",
|
||||
// needs to be fixed actually
|
||||
"complexity": "off",
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"no-async-promise-executor": "off",
|
||||
"no-bitwise": "off",
|
||||
"unicorn/filename-case": "off",
|
||||
"max-depth": "off",
|
||||
"unicorn/no-typeof-undefined": "off"
|
||||
"max-depth": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
@ -208,7 +102,7 @@
|
|||
"*.js"
|
||||
],
|
||||
"rules": {
|
||||
"@stylistic/space-before-function-paren": [
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"anonymous": "always",
|
||||
|
|
|
|||
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
|
|
@ -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
|
|
@ -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
|
||||
157
.github/workflows/ci.yml
vendored
|
|
@ -13,165 +13,28 @@ jobs:
|
|||
with:
|
||||
java-version: 17
|
||||
java-package: jre
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm@9.0.4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 18
|
||||
# cache: "pnpm"
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- run: pnpm install
|
||||
- run: pnpm build-single-file
|
||||
- name: Store minecraft.html size
|
||||
run: |
|
||||
SIZE_BYTES=$(du -s dist/single/minecraft.html 2>/dev/null | cut -f1)
|
||||
echo "SIZE_BYTES=$SIZE_BYTES" >> $GITHUB_ENV
|
||||
- run: pnpm check-build
|
||||
- name: Create zip package for size comparison
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
- run: pnpm build-playground
|
||||
# - run: pnpm build-storybook
|
||||
- run: pnpm test-unit
|
||||
- run: pnpm lint
|
||||
|
||||
- name: Parse Bundle Stats
|
||||
run: |
|
||||
GZIP_BYTES=$(du -s self-host.zip 2>/dev/null | cut -f1)
|
||||
SIZE=$(echo "scale=2; $SIZE_BYTES/1024/1024" | bc)
|
||||
GZIP_SIZE=$(echo "scale=2; $GZIP_BYTES/1024/1024" | bc)
|
||||
echo "{\"total\": ${SIZE}, \"gzipped\": ${GZIP_SIZE}}" > /tmp/bundle-stats.json
|
||||
|
||||
# - name: Compare Bundle Stats
|
||||
# id: compare
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
||||
# with:
|
||||
# script: |
|
||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
||||
|
||||
# async function getGistContent() {
|
||||
# const { data } = await github.rest.gists.get({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# }
|
||||
# });
|
||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
||||
# }
|
||||
|
||||
# const content = await getGistContent();
|
||||
# const baseStats = content['${{ github.event.pull_request.base.ref }}'];
|
||||
# const newStats = require('/tmp/bundle-stats.json');
|
||||
|
||||
# const comparison = `minecraft.html (normal build gzip)\n${baseStats.total}MB (${baseStats.gzipped}MB compressed) -> ${newStats.total}MB (${newStats.gzipped}MB compressed)`;
|
||||
# core.setOutput('stats', comparison);
|
||||
|
||||
# - run: pnpm tsx scripts/buildNpmReact.ts
|
||||
- run: pnpm tsx scripts/buildNpmReact.ts
|
||||
- run: nohup pnpm prod-start &
|
||||
- run: nohup pnpm test-mc-server &
|
||||
- uses: cypress-io/github-action@v5
|
||||
with:
|
||||
install: false
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-images
|
||||
path: cypress/screenshots/
|
||||
# - run: node scripts/outdatedGitPackages.mjs
|
||||
# if: ${{ github.event.pull_request.base.ref == 'release' }}
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Store Bundle Stats
|
||||
# if: github.event.pull_request.base.ref == 'next'
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
||||
# with:
|
||||
# script: |
|
||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
||||
|
||||
# async function getGistContent() {
|
||||
# const { data } = await github.rest.gists.get({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# }
|
||||
# });
|
||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
||||
# }
|
||||
|
||||
# async function updateGistContent(content) {
|
||||
# await github.rest.gists.update({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# },
|
||||
# files: {
|
||||
# 'bundle-stats.json': {
|
||||
# content: JSON.stringify(content, null, 2)
|
||||
# }
|
||||
# }
|
||||
# });
|
||||
# }
|
||||
|
||||
# const stats = require('/tmp/bundle-stats.json');
|
||||
# const content = await getGistContent();
|
||||
# content['${{ github.event.pull_request.base.ref }}'] = stats;
|
||||
# await updateGistContent(content);
|
||||
|
||||
# - name: Update PR Description
|
||||
# uses: actions/github-script@v6
|
||||
# with:
|
||||
# script: |
|
||||
# const { data: pr } = await github.rest.pulls.get({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# pull_number: context.issue.number
|
||||
# });
|
||||
|
||||
# let body = pr.body || '';
|
||||
# const statsMarker = '### Bundle Size';
|
||||
# const comparison = '${{ steps.compare.outputs.stats }}';
|
||||
|
||||
# if (body.includes(statsMarker)) {
|
||||
# body = body.replace(
|
||||
# new RegExp(`${statsMarker}[^\n]*\n[^\n]*`),
|
||||
# `${statsMarker}\n${comparison}`
|
||||
# );
|
||||
# } else {
|
||||
# body += `\n\n${statsMarker}\n${comparison}`;
|
||||
# }
|
||||
|
||||
# await github.rest.pulls.update({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# pull_number: context.issue.number,
|
||||
# body
|
||||
# });
|
||||
# dedupe-check:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event.pull_request.head.ref == 'next'
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
|
||||
# - name: Install pnpm
|
||||
# run: npm install -g pnpm@9.0.4
|
||||
|
||||
# - name: Run pnpm dedupe
|
||||
# run: pnpm dedupe
|
||||
|
||||
# - name: Check for changes
|
||||
# run: |
|
||||
# if ! git diff --exit-code --quiet pnpm-lock.yaml; then
|
||||
# echo "pnpm dedupe introduced changes:"
|
||||
# git diff --color=always pnpm-lock.yaml
|
||||
# exit 1
|
||||
# else
|
||||
# echo "No changes detected after pnpm dedupe in pnpm-lock.yaml"
|
||||
# fi
|
||||
path: cypress/integration/__image_snapshots__/
|
||||
- run: node scripts/outdatedGitPackages.mjs
|
||||
if: github.ref == 'refs/heads/next'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
29
.github/workflows/fix-lint.yml
vendored
|
|
@ -1,29 +0,0 @@
|
|||
name: Fix Lint Command
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.issue.pull_request != '' &&
|
||||
(
|
||||
contains(github.event.comment.body, '/fix')
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- run: pnpm install
|
||||
- run: pnpm lint --fix
|
||||
- name: Push Changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
28
.github/workflows/merge-next.yml
vendored
|
|
@ -1,28 +0,0 @@
|
|||
name: Update Base Branch Command
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.issue.pull_request != '' &&
|
||||
(
|
||||
contains(github.event.comment.body, '/update')
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history so we can merge branches
|
||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||
- name: Fetch All Branches
|
||||
run: git fetch --all
|
||||
# - name: Checkout PR
|
||||
# run: git checkout ${{ github.event.issue.pull_request.head.ref }}
|
||||
- name: Merge From Next
|
||||
run: git merge origin/next --strategy-option=theirs
|
||||
- name: Push Changes
|
||||
run: git push
|
||||
74
.github/workflows/next-deploy.yml
vendored
|
|
@ -3,7 +3,6 @@ env:
|
|||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
ALIASES: ${{ vars.ALIASES }}
|
||||
MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }}
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -16,76 +15,25 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Install Global Dependencies
|
||||
run: pnpm add -g vercel
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
run: npm install --global vercel pnpm@9.0.4
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||
id: deploy
|
||||
- name: Start servers for testing
|
||||
run: |
|
||||
nohup pnpm prod-start &
|
||||
nohup pnpm test-mc-server &
|
||||
- name: Run Cypress smoke tests
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
install: false
|
||||
spec: cypress/e2e/smoke.spec.ts
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-smoke-test-screenshots
|
||||
path: cypress/screenshots/
|
||||
- name: Set deployment aliases
|
||||
run: |
|
||||
for alias in $(echo ${{ secrets.TEST_PREVIEW_DOMAIN }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:next`,
|
||||
base: 'release',
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (pulls.length === 0) {
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Release',
|
||||
head: 'next',
|
||||
base: 'release',
|
||||
body: 'PR was created automatically by the release workflow, hope you release it as soon as possible!',
|
||||
});
|
||||
}
|
||||
- name: Set deployment alias
|
||||
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ secrets.TEST_PREVIEW_DOMAIN }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
# - uses: mshick/add-pr-comment@v2
|
||||
# with:
|
||||
# message: |
|
||||
# Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||
|
|
|
|||
92
.github/workflows/preview.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Vercel PR Deploy (Preview)
|
||||
name: Vercel Deploy Preview
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
|
@ -6,109 +6,57 @@ env:
|
|||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
# todo skip already created deploys on that commit
|
||||
if: >-
|
||||
github.event.issue.pull_request != '' &&
|
||||
(
|
||||
(
|
||||
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)
|
||||
)
|
||||
contains(github.event.comment.body, '/deploy')
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout Base To Temp
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: temp-base-repo
|
||||
- name: Get deployment alias
|
||||
run: node temp-base-repo/scripts/githubActions.mjs getAlias
|
||||
id: alias
|
||||
env:
|
||||
ALIASES: ${{ env.ALIASES }}
|
||||
PULL_URL: ${{ github.event.issue.pull_request.url || github.event.pull_request.url }}
|
||||
- name: Checkout PR (comment)
|
||||
uses: actions/checkout@v2
|
||||
if: github.event_name == 'issue_comment'
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||
- name: Checkout PR (pull_request)
|
||||
uses: actions/checkout@v2
|
||||
if: github.event_name == 'pull_request_target'
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/head
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- run: npm i -g pnpm@9.0.4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
- name: Update deployAlwaysUpdate packages
|
||||
run: |
|
||||
if [ -f package.json ]; then
|
||||
PACKAGES=$(node -e "const pkg = require('./package.json'); if (pkg.deployAlwaysUpdate) console.log(pkg.deployAlwaysUpdate.join(' '))")
|
||||
if [ ! -z "$PACKAGES" ]; then
|
||||
echo "Updating packages: $PACKAGES"
|
||||
pnpm up -L $PACKAGES
|
||||
else
|
||||
echo "No deployAlwaysUpdate packages found in package.json"
|
||||
fi
|
||||
else
|
||||
echo "package.json not found"
|
||||
fi
|
||||
- name: Install Global Dependencies
|
||||
run: pnpm add -g vercel
|
||||
run: npm install --global vercel
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
- name: Write pr redirect index.html
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/pr
|
||||
echo "<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
|
||||
run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||
id: deploy
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
# if: github.event_name == 'issue_comment'
|
||||
with:
|
||||
allow-repeats: true
|
||||
message: |
|
||||
Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||
[Playground](${{ steps.deploy.outputs.stdout }}/playground/)
|
||||
[Playground](${{ steps.deploy.outputs.stdout }}/playground.html)
|
||||
[Storybook](${{ steps.deploy.outputs.stdout }}/storybook/)
|
||||
# - 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
|
||||
if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }}
|
||||
run: |
|
||||
for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ steps.alias.outputs.alias }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
|
|
|
|||
48
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Deploy to GitHub pages
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
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 }}
|
||||
# will install + build to .vercel/output/static
|
||||
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
id: deploy
|
||||
- run: |
|
||||
pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 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
|
|
@ -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
|
|
@ -10,7 +10,7 @@ localSettings.mjs
|
|||
dist*
|
||||
.DS_Store
|
||||
.idea/
|
||||
/world
|
||||
world
|
||||
data*.json
|
||||
out
|
||||
*.iml
|
||||
|
|
@ -18,7 +18,5 @@ out
|
|||
generated
|
||||
storybook-static
|
||||
server-jar
|
||||
config.local.json
|
||||
logs/
|
||||
|
||||
src/react/npmReactComponents.ts
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
import type { Preview } from "@storybook/react"
|
||||
import type { Preview } from "@storybook/react";
|
||||
|
||||
import './storybook.css'
|
||||
import '../src/styles.css'
|
||||
import '../src/scaleInterface'
|
||||
import './storybook.css'
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
|
|
@ -12,7 +11,7 @@ const preview: Preview = {
|
|||
const noScaling = c.parameters.noScaling
|
||||
return <div id={noScaling ? '' : 'ui-root'}>
|
||||
<Story />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
|
|
@ -24,6 +23,6 @@ const preview: Preview = {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default preview
|
||||
export default preview;
|
||||
|
|
|
|||
5
.vscode/launch.json
vendored
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"configurations": [
|
||||
// UPDATED: all configs below are misconfigured and will crash vscode, open dist/index.html and use live preview debug instead
|
||||
// recommended as much faster
|
||||
{
|
||||
// to launch "C:\Program Files\Google\Chrome Beta\Application\chrome.exe" --remote-debugging-port=9222
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
"type": "chrome",
|
||||
"name": "Launch Chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000/",
|
||||
"url": "http://localhost:8080/",
|
||||
"pathMapping": {
|
||||
"/": "${workspaceFolder}/dist"
|
||||
},
|
||||
|
|
@ -49,7 +50,7 @@
|
|||
"name": "Attach Firefox",
|
||||
"request": "attach",
|
||||
// comment if using webpack
|
||||
"url": "http://localhost:3000/",
|
||||
"url": "http://localhost:8080/",
|
||||
"webRoot": "${workspaceFolder}/",
|
||||
"skipFiles": [
|
||||
// "<node_internals>/**/*vendors*"
|
||||
|
|
|
|||
168
CONTRIBUTING.md
|
|
@ -2,84 +2,26 @@
|
|||
|
||||
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`
|
||||
2. Start the project in development mode: `pnpm start` or build the project for production: `pnpm build`
|
||||
3. Read the [Tasks Categories](#tasks-categories) and [Workflow](#workflow) sections below
|
||||
4. Let us know if you are working on something and be sure to open a PR if you got any changes. Happy coding!
|
||||
|
||||
*(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 -->
|
||||
2. Start the project in development mode: `pnpm start`
|
||||
|
||||
## Project Structure
|
||||
|
||||
There are 3 main parts of the project:
|
||||
|
||||
### Core (`src`)
|
||||
|
||||
This is the main app source code which reuses all the other parts of the project.
|
||||
|
||||
> The first version used Webpack, then was migrated to Esbuild and now is using Rsbuild!
|
||||
|
||||
- Scripts:
|
||||
- Start: `pnpm start`, `pnpm dev-rsbuild` (if you don't need proxy server also running)
|
||||
- Build: `pnpm build` (note that `build` script builds only the core app, not the whole project!)
|
||||
|
||||
Paths:
|
||||
|
||||
- `src` - main app source code
|
||||
- `src/react` - React components - almost all UI is in this folder. Almost every component has its base (reused in app and storybook) and `Provider` - which is a component that provides context to its children. Consider looking at DeathScreen component to see how it's used.
|
||||
- `src/menus` - Old Lit Element GUI. In the process of migration to React.
|
||||
|
||||
### Renderer: Playground & Mesher (`renderer`)
|
||||
|
||||
- Playground Scripts:
|
||||
- Start: `pnpm run-playground` (playground, mesher + server) or `pnpm watch-playground`
|
||||
- Build: `pnpm build-playground` or `node renderer/esbuild.mjs`
|
||||
|
||||
- Mesher Scripts:
|
||||
- Start: `pnpm watch-mesher`
|
||||
- Build: `pnpm build-mesher`
|
||||
|
||||
Paths:
|
||||
|
||||
- `renderer` - Improved and refactored version of <https://github.com/PrismarineJS/prismarine-viewer>. Here is everything related to rendering the game world itself (no ui at all). Two most important parts here are:
|
||||
- `renderer/viewer/lib/worldrenderer.ts` - adding new objects to three.js happens here (sections)
|
||||
- `renderer/viewer/lib/models.ts` - preparing data for rendering (blocks) - happens in worker: out file - `worker.js`, building - `renderer/buildWorker.mjs`
|
||||
- `renderer/playground/playground.ts` - Playground (source of <mcraft.fun/playground.html>) Use this for testing any rendering changes. You can also modify the playground code.
|
||||
|
||||
### Storybook (`.storybook`)
|
||||
|
||||
Storybook is a tool for easier developing and testing React components.
|
||||
Path of all Storybook stories is `src/react/**/*.stories.tsx`.
|
||||
|
||||
- Scripts:
|
||||
- Start: `pnpm storybook`
|
||||
- Build: `pnpm build-storybook`
|
||||
|
||||
## Core-related
|
||||
- `prismarine-viewer` - Improved 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/viewer/lib/worldrenderer.ts` - adding new objects to three.js happens here (sections)
|
||||
- `prismarine-viewer/viewer/lib/models.ts` - preparing data for rendering (blocks) - happens in worker: out file - `worker.js`, building - `prismarine-viewer/buildWorker.mjs`
|
||||
- `prismarine-viewer/examples/playground.ts` - Playground (source of <mcraft.fun/playground.html>) Use this for testing render changes. You can also modify playground code.
|
||||
|
||||
How different modules are used:
|
||||
|
||||
- `mineflayer` - provider `bot` variable and as mineflayer states it is a wrapper for the `node-minecraft-protocol` module and is used to connect and interact with real Java Minecraft servers. However not all events & properties are exposed and sometimes you have to use `bot._client.on('packet_name', data => ...)` to handle packets that are not handled via mineflayer API. Also you can use almost any mineflayer plugin.
|
||||
|
||||
## Running Main App + Playground
|
||||
|
||||
To start the main web app and playground, run `pnpm run-all`. Note is doesn't start storybook and tests.
|
||||
|
||||
## Cypress Tests (E2E)
|
||||
|
||||
Cypress tests are located in `cypress` folder. To run them, run `pnpm test-mc-server` and then `pnpm test:cypress` when the `pnpm prod-start` is running (or change the port to 3000 to test with the dev server). Usually you don't need to run these until you get issues on the CI.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
There are not many unit tests for now (which we are trying to improve).
|
||||
Location of unit tests: `**/*.test.ts` files in `src` folder and `renderer` folder.
|
||||
Start them with `pnpm test-unit`.
|
||||
|
||||
## Making protocol-related changes
|
||||
## Making protocol changes
|
||||
|
||||
You can get a description of packets for the latest protocol version from <https://wiki.vg/Protocol> and for previous protocol versions from <https://wiki.vg/Protocol_version_numbers> (look for *Page* links that have *Protocol* in URL).
|
||||
|
||||
|
|
@ -95,100 +37,6 @@ Also there are [src/generatedClientPackets.ts](src/generatedClientPackets.ts) an
|
|||
- Some data are cached between restarts. If you see something doesn't work after upgrading dependencies, try to clear the by simply removing the `dist` folder.
|
||||
- The same folder `dist` is used for both development and production builds, so be careful when deploying the project.
|
||||
- Use `start-prod` script to start the project in production mode after running the `build` script to build the project.
|
||||
- If CI is failing on the next branch for some reason, feel free to use the latest commit for release branch. We will update the base branch asap. Please, always make sure to allow maintainers do changes when opening PRs.
|
||||
|
||||
## Tasks Categories
|
||||
|
||||
(most important for now are on top).
|
||||
|
||||
## 1. Client-side Logic (most important right now)
|
||||
|
||||
Everything related to the client side packets. Investigate issues when something goes wrong with some server. It's much easier to work on these types of tasks when you have experience in Java with Minecraft, a deep understanding of the original client, and know how to debug it (which is not hard actually). Right now the client is easily detectable by anti-cheat plugins, and the main goal is to fix it (mostly because of wrong physics implementation).
|
||||
|
||||
Priority tasks:
|
||||
|
||||
- Rewrite or fix the physics logic (Botcraft or Grim can be used as a reference as well)
|
||||
- Implement basic minecart / boat / horse riding
|
||||
- Fix auto jump module (false triggers, performance issues)
|
||||
- Investigate connection issues to some servers
|
||||
- Setup a platform for automatic cron testing against the latest version of the anti-cheat plugins
|
||||
- ...
|
||||
|
||||
Goals:
|
||||
|
||||
- Make more servers playable. Right now on hypixel-like servers (servers with minigames), only tnt run (and probably ) is fully playable.
|
||||
|
||||
Notes:
|
||||
|
||||
- You can see the incoming/outgoing packets in the console (F12 in Chrome) by enabling `options.debugLogNotFrequentPackets = true`. However, if you need a FULL log of all packets, you can start recording the packets by going into `Settings` > `Advanced` > `Enable Packets Replay` and then you can download the file and use it to replay the packets.
|
||||
- You can use mcraft-e2e studio to send the same packets over and over again (which is useful for testing) or use the packets replayer (which is useful for debugging).
|
||||
|
||||
## 2. Three.js Renderer
|
||||
|
||||
Example tasks:
|
||||
|
||||
- Improve / fix entity rendering
|
||||
- Better update entities on specific packets
|
||||
- Investigate performance issues under different conditions (instructions provided)
|
||||
- Work on the playground code
|
||||
|
||||
Goals:
|
||||
|
||||
- Fix a lot of entity rendering issues (including position updates)
|
||||
- Implement switching camera mode (first person, third person, etc)
|
||||
- Animated blocks
|
||||
- Armor rendering
|
||||
- ...
|
||||
|
||||
Note:
|
||||
|
||||
- It's useful to know how to use helpers & additional cameras (e.g. setScissor)
|
||||
|
||||
## 3. Server-side Logic
|
||||
|
||||
Flying squid fork (space-squid).
|
||||
Example tasks:
|
||||
|
||||
- Add missing commands (e.g. /scoreboard)
|
||||
- Basic physics (player fall damage, falling blocks & entities)
|
||||
- Basic entities AI (spawning, attacking)
|
||||
- Pvp
|
||||
- Emit more packets on some specific events (e.g. when a player uses an item)
|
||||
- Make more maps playable (e.g. fix when something is not implemented in both server and client and blocking map interaction)
|
||||
- ...
|
||||
|
||||
Long Term Goals:
|
||||
|
||||
- Make most adventure maps playable
|
||||
- Make a way to complete the game from the scratch (crafting, different dimensions, terrain generation, etc)
|
||||
- Make bedwars playable!
|
||||
Most of the tasks are straightforward to implement, just be sure to use a debugger ;). If you feel you are stuck, ask for help on Discord. Absolutely any tests / refactor suggestions are welcome!
|
||||
|
||||
## 4. Frontend
|
||||
|
||||
New React components, improve UI (including mobile support).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Locate the problem on the public test server & make an easily reproducible environment (you can also use local packets replay server or your custom server setup). Dm me for details on public test server / replay server
|
||||
2. Debug the code, find an issue in the code, isolate the problem
|
||||
3. Develop, try to fix and test. Finally we should find a way to fix it. It's ideal to have an automatic test but it's not necessary for now
|
||||
3. Repeat step 1 to make sure the task is done and the problem is fixed (or the feature is implemented)
|
||||
|
||||
## Updating Dependencies
|
||||
|
||||
1. Use `pnpm update-git-deps` to check and update git dependencies (like mineflayer fork, prismarine packages etc). The script will:
|
||||
- Show which git dependencies have updates available
|
||||
- Ask if you want to update them
|
||||
- Skip dependencies listed in `pnpm.updateConfig.ignoreDependencies`
|
||||
|
||||
2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ...
|
||||
|
||||
3. If `minecraft-protocol` patch fails, do this:
|
||||
1. Remove the patch from `patchedDependencies` in `package.json`
|
||||
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
||||
3. Apply the patch manually in this directory: `patch -p1 < minecraft-protocol@<version>.patch`
|
||||
4. Run the suggested command from `pnpm patch ...` (previous step) to update the patch
|
||||
|
||||
### Would be useful to have
|
||||
|
||||
|
|
|
|||
44
Dockerfile
|
|
@ -1,43 +1,9 @@
|
|||
# ---- Build Stage ----
|
||||
FROM node:18-alpine AS build
|
||||
FROM node:14-alpine
|
||||
# Without git installing the npm packages fails
|
||||
RUN apk add git
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
# install pnpm with corepack
|
||||
RUN corepack enable
|
||||
# Build arguments
|
||||
ARG DOWNLOAD_SOUNDS=false
|
||||
ARG DISABLE_SERVICE_WORKER=false
|
||||
ARG CONFIG_JSON_SOURCE=REMOTE
|
||||
# TODO need flat --no-root-optional
|
||||
RUN node ./scripts/dockerPrepare.mjs
|
||||
RUN pnpm i
|
||||
# Download sounds if flag is enabled
|
||||
RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs ; fi
|
||||
|
||||
# TODO for development
|
||||
# EXPOSE 9090
|
||||
# VOLUME /app/src
|
||||
# VOLUME /app/renderer
|
||||
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
||||
|
||||
# only for prod
|
||||
RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
|
||||
CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \
|
||||
pnpm run build
|
||||
|
||||
# ---- Run Stage ----
|
||||
FROM node:18-alpine
|
||||
RUN apk add git
|
||||
WORKDIR /app
|
||||
# Copy build artifacts from the build stage
|
||||
COPY --from=build /app/dist /app/dist
|
||||
COPY server.js /app/server.js
|
||||
# Install express
|
||||
RUN npm i -g pnpm@10.8.0
|
||||
RUN npm init -yp
|
||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||
EXPOSE 8080
|
||||
VOLUME /app/public
|
||||
ENTRYPOINT ["node", "server.js", "--prod"]
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
ENTRYPOINT ["npm", "run", "prod-start"]
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
# ---- Run Stage ----
|
||||
FROM node:18-alpine
|
||||
RUN apk add git
|
||||
WORKDIR /app
|
||||
COPY server.js /app/server.js
|
||||
# Install server dependencies
|
||||
RUN npm i -g pnpm@9.0.4
|
||||
RUN npm init -yp
|
||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["node", "server.js"]
|
||||
182
README.MD
|
|
@ -2,65 +2,32 @@
|
|||
|
||||

|
||||
|
||||
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 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.
|
||||
If you encounter any bugs or usability issues, please report them!
|
||||
|
||||
> For Turkey/Russia use [ru.mcraft.fun](https://ru.mcraft.fun/) (since Cloudflare is blocked)
|
||||
|
||||
Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, meanwhile this project is aimed for *device-compatiiblity* and better performance so it feels portable, flexible and lightweight. It's also a very strong example on how to build true HTML games for the web at scale entirely with the JS ecosystem. Have fun!
|
||||
|
||||
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft).
|
||||
|
||||
> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere)
|
||||
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.
|
||||
|
||||
### Big Features
|
||||
|
||||
- Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely.
|
||||
- Open any zip world file or even folder in read-write mode!
|
||||
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
|
||||
- Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc)
|
||||
- Connect to cracked servers* (it's possible because of proxy servers, see below)
|
||||
- Singleplayer mode with simple world generations!
|
||||
- Works offline
|
||||
- First-class touch (mobile) & controller support
|
||||
- First-class keybindings configuration
|
||||
- Advanced Resource pack support: Custom GUI, all textures. Server resource packs are supported with proper CORS configuration.
|
||||
- Builtin JEI with recipes & descriptions for almost every item (JEI is creative inventory replacement)
|
||||
- Custom protocol channel extensions (eg for custom block models in the world)
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- ~~Google Drive support for reading / saving worlds back to the cloud~~
|
||||
- Support for custom rendering 3D engines. Modular architecture.
|
||||
- First-class touch (mobile) & controller support
|
||||
- Resource pack support
|
||||
- Builtin JEI with recipes & guides for every item (also replaces creative inventory)
|
||||
- even even more!
|
||||
|
||||
All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||
|
||||
### Recommended Settings
|
||||
|
||||
- Controls -> **Raw Input** -> **On** - This will make the controls more precise
|
||||
- Controls -> **Touch Controls Type** -> **Joystick**
|
||||
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
||||
- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?)
|
||||
- Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default)
|
||||
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)
|
||||
|
||||
### Browser Notes
|
||||
|
||||
This project is tested with BrowserStack. Special thanks to [BrowserStack](https://www.browserstack.com/) for providing testing infrastructure!
|
||||
|
||||
Howerver, it's known that these browsers have issues:
|
||||
|
||||
**Opera Mini**: Disable *mouse gestures* in browsre settings to avoid opening new tab on right click hold
|
||||
|
||||
**Vivaldi**: Disable Controls -> *Raw Input* in game settings if experiencing issues
|
||||
|
||||
### Versions Support
|
||||
|
||||
Server versions 1.8 - 1.21.5 are supported.
|
||||
First class versions (most of the features are tested on these versions):
|
||||
|
||||
- 1.19.4
|
||||
- 1.21.4
|
||||
|
||||
Versions below 1.13 are not tested currently and may not work correctly.
|
||||
- Interface -> **Chat Select** -> **On** - To select chat messages
|
||||
|
||||
### World Loading
|
||||
|
||||
|
|
@ -70,37 +37,12 @@ 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 server, supporting offline connections.
|
||||
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:
|
||||
|
||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||
|
||||
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
||||
|
||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Web App - Client] --> C[Proxy Server]
|
||||
C --> B[Minecraft Server]
|
||||
style A fill:#f9d,stroke:#333,stroke-width:2px
|
||||
style B fill:#fc0,stroke:#333,stroke-width:2px
|
||||
style C fill:#fff,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
So if the server is located in Europe and you are connecting from Europe, you will have ~40ms ping (~180ms with residential proxy version), however if you are in the US and connecting to the server located in US, you will have >200ms ping, which is the worst case scenario.
|
||||
|
||||
Again, the proxy server is not a part of the client, it is a separate service that you can host yourself.
|
||||
|
||||
### Docker Files
|
||||
|
||||
- [Main Dockerfile](./Dockerfile) - for production build & offline/private usage. Includes full web-app + proxy server for connecting to Minecraft servers.
|
||||
- [Proxy Dockerfile](./Dockerfile.proxy) - for proxy server only - that one you would be able to specify in the proxy field on the client (`docker build . -f Dockerfile.proxy -t minecraft-web-proxy`)
|
||||
|
||||
In case of using docker, you don't have to follow preparation steps from CONTRIBUTING.MD, like installing Node.js, pnpm, etc.
|
||||
There is a builtin proxy, but you can also host a your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field.
|
||||
MS account authentication will be supported soon.
|
||||
|
||||
### Rendering
|
||||
|
||||
|
|
@ -111,37 +53,42 @@ In case of using docker, you don't have to follow preparation steps from CONTRIB
|
|||
- Supports resource packs
|
||||
- Doesn't support occlusion culling
|
||||
|
||||
<!-- TODO proxy server communication graph -->
|
||||
|
||||
### Things that are not planned yet
|
||||
|
||||
- Mods, plugins (basically JARs) support, shaders - since they all are related to specific game pipelines
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
There are many many settings, that are not exposed in the UI yet. You can find or change them by opening the browser console and typing `options`. You can also change them by typing `options.<setting_name> = <value>`.
|
||||
|
||||
### Console
|
||||
|
||||
To open the console, press `F12`, or if you are on mobile, you can type `#dev` in the URL (browser address bar), it wont't reload the page, but you will see a button to open the console. This way you can change advanced settings and see all errors or warnings. Also this way you can access global variables (described below).
|
||||
To open the console, press `F12`, or if you are on mobile, you can type `#debug` in the URL (browser address bar), it wont't reload the page, but you will see a button to open the console. This way you can change advanced settings and see all errors or warnings. Also this way you can access global variables (described below).
|
||||
|
||||
### Development, Debugging & Contributing
|
||||
### Debugging
|
||||
|
||||
It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info. Also you can look at Dockerfile for reference.
|
||||
It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info.
|
||||
|
||||
There is world renderer playground ([link](https://mcon.vercel.app/playground/)).
|
||||
There is world renderer playground ([link](https://mcon.vercel.app/playground.html)).
|
||||
|
||||
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 version. You can access some global variables in the console and useful examples:
|
||||
|
||||
- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam.
|
||||
- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam.
|
||||
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
||||
|
||||
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
||||
- `world` - Three.js world instance, basically does all the rendering (part of renderer backend).
|
||||
- `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||
- `viewer` - Three.js viewer instance, basically does all the rendering.
|
||||
- `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
||||
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
||||
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
||||
- `localServer.overworld.storageProvider.regions` - See ALL LOADED region files with all raw data.
|
||||
- `localServer.levelData.LevelName = 'test'; localServer.writeLevelDat()` - Change name of the world
|
||||
|
||||
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
|
||||
|
||||
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `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"/>
|
||||
|
||||
|
|
@ -160,62 +107,19 @@ world chunks have a *yellow* border, hostile mobs have a *red* outline, passive
|
|||
|
||||
Press `Y` to set query parameters to url of your current game state.
|
||||
|
||||
There are some parameters you can set in the url to archive some specific behaviors:
|
||||
|
||||
General:
|
||||
|
||||
- **`?setting=<setting_name>:<setting_value>`** - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4`
|
||||
- `?modal=<modal>` - Open specific modal on page load eg `keybindings`. Very useful on UI changes testing during dev. For path use `,` as separator. To get currently opened modal type this in the console: `activeModalStack.at(-1).reactType`
|
||||
- `?replayFileUrl=<url>` - Load and start a packet replay session from a URL with a integrated server. For debugging / previewing recorded sessions. The file must be CORS enabled.
|
||||
|
||||
Server specific:
|
||||
|
||||
- `?ip=<server_address>` - Display connect screen to the server on load with predefined server ip. `:<port>` is optional and can be added to the ip.
|
||||
- `?name=<name>` - Set the server name for saving to the server list
|
||||
- `?version=<version>` - Set the version for the server
|
||||
- `?proxy=<proxy_address>` - Set the proxy server address to use for the server
|
||||
- `?username=<username>` - Set the username for the server
|
||||
- `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes.
|
||||
- `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes.
|
||||
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.
|
||||
- `?addPing=<ping>` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping.
|
||||
|
||||
Single player specific:
|
||||
|
||||
- `?ip=<server_address>` - Display connect screen to the server on load
|
||||
- `?username=<username>` - Set the username for server
|
||||
- `?proxy=<proxy_address>` - Set the proxy server address to use for server
|
||||
- `?version=<version>` - Set the version for server
|
||||
- `?lockConnect=true` - Disable cancel / save buttons, useful for integrates iframes
|
||||
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
|
||||
<!-- - `?password=<password>` - Set the password on load -->
|
||||
- `?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
|
||||
- `?version=<version>` - Set the version for the singleplayer world (when used with `?singleplayer=1`)
|
||||
- `?noSave=true` - Disable auto save on unload / disconnect / export whenever a world is loaded. Only manual save with `/save` command will work.
|
||||
- `?serverSetting=<key>:<value>` - Set local server [options](https://github.com/zardoy/space-squid/tree/everything/src/modules.ts#L51). For example `?serverSetting=noInitialChunksSend:true` will disable initial chunks loading on the loading screen.
|
||||
- `?map=<map_url>` - Load the map from ZIP. You can use any url, but it must be **CORS enabled**.
|
||||
- `?mapDir=<index_file_url>` - Load the map from a file descriptor. It's recommended and the fastest way to load world but requires additional setup. The file must be in the following format:
|
||||
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
|
||||
- `?noSave=true` - Disable auto save on unload / disconnect / export. Only manual save with `/save` command will work
|
||||
|
||||
```json
|
||||
{
|
||||
"baseUrl": "<url>",
|
||||
"index": {
|
||||
"level.dat": null,
|
||||
"region": {
|
||||
"r.-1.-1.mca": null,
|
||||
"r.-1.0.mca": null,
|
||||
"r.0.-1.mca": null,
|
||||
"r.0.0.mca": null,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that `mapDir` also accepts base64 encoded JSON like so:
|
||||
`?mapDir=data:application/json;base64,...` where `...` is the base64 encoded JSON of the index file.
|
||||
In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the files from index.
|
||||
|
||||
- `?mapDirBaseUrl` - See above.
|
||||
|
||||
Only during development:
|
||||
|
||||
- `?reconnect=true` - Reconnect to the server on page reloads. Very useful on server testing.
|
||||
|
||||
<!-- - `?mapDirGuess=<base_url>` - Load the map from the provided URL and paths will be guessed with a few additional fetch requests. -->
|
||||
- `?map=<map_url>` - Load the map from ZIP. You can use any url, but it must be CORS enabled.
|
||||
- `?setting=<setting_name>:<setting_value>` - Set the 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
|
||||
|
||||
|
|
@ -227,12 +131,6 @@ Only during development:
|
|||
- [Peer.js](https://peerjs.com/) - P2P networking (when you open to wan)
|
||||
- [Three.js](https://threejs.org/) - Helping in 3D rendering
|
||||
|
||||
### Things that are not planned yet
|
||||
|
||||
- Mods, plugins (basically JARs) support, shaders - since they all are related to specific game pipelines
|
||||
|
||||
### Alternatives
|
||||
|
||||
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
|
||||
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
|
||||
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
# Minecraft React
|
||||
|
||||
Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun) project.
|
||||
|
||||
```bash
|
||||
pnpm i minecraft-react
|
||||
yarn add minecraft-react
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
|
|
|
|||
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>
|
||||
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 115 B |
|
Before Width: | Height: | Size: 123 B |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 169 B |
|
Before Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 190 B |
|
Before Width: | Height: | Size: 211 B |
|
Before Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 859 KiB After Width: | Height: | Size: 859 KiB |
|
Before Width: | Height: | Size: 952 KiB After Width: | Height: | Size: 952 KiB |
|
Before Width: | Height: | Size: 704 KiB After Width: | Height: | Size: 704 KiB |
|
Before Width: | Height: | Size: 684 KiB After Width: | Height: | Size: 684 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
BIN
assets/extra-textures/loading.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 513 KiB After Width: | Height: | Size: 589 KiB |
BIN
assets/invsprite.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Minecraft Web Client",
|
||||
"short_name": "Minecraft Web Client",
|
||||
"name": "Prismarine Web Client",
|
||||
"short_name": "Prismarine Web Client",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
<!-- just redirect to /playground/ -->
|
||||
<script>
|
||||
window.location.href = `/playground/${window.location.search}`
|
||||
</script>
|
||||
75
config.json
|
|
@ -1,80 +1,23 @@
|
|||
{
|
||||
"version": 1,
|
||||
"defaultHost": "<from-proxy>",
|
||||
"defaultProxy": "https://proxy.mcraft.fun",
|
||||
"defaultProxy": "proxy.mcraft.fun",
|
||||
"mapsProvider": "https://maps.mcraft.fun/",
|
||||
"skinTexturesProxy": "",
|
||||
"peerJsServer": "",
|
||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||
"promoteServers": [
|
||||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.webmc.fun",
|
||||
"name": "WebMC"
|
||||
},
|
||||
{
|
||||
"ip": "wss://ws.fuchsmc.net"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play2.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play-creative.mcraft.fun",
|
||||
"description": "Might be available soon, stay tuned!"
|
||||
},
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"version": "1.20.3",
|
||||
"description": "Very nice a polite server. Must try for everyone!"
|
||||
}
|
||||
],
|
||||
"rightSideText": "A Minecraft client clone in the browser!",
|
||||
"splashText": "The sunset is coming!",
|
||||
"splashTextFallback": "Welcome!",
|
||||
"pauseLinks": [
|
||||
[
|
||||
{
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"type": "discord"
|
||||
}
|
||||
]
|
||||
],
|
||||
"defaultUsername": "mcrafter{0-9999}",
|
||||
"mobileButtons": [
|
||||
{
|
||||
"action": "general.drop",
|
||||
"actionHold": "general.dropStack",
|
||||
"label": "Q"
|
||||
"version": "1.18.2",
|
||||
"description": "Chaos and destruction server. Free for everyone."
|
||||
},
|
||||
{
|
||||
"action": "general.selectItem",
|
||||
"actionHold": "",
|
||||
"label": "S"
|
||||
"ip": "go.mineberry.org",
|
||||
"version": "1.18.2",
|
||||
"description": "One of the best servers here. Join now!"
|
||||
},
|
||||
{
|
||||
"action": "general.debugOverlay",
|
||||
"actionHold": "general.debugOverlayHelpMenu",
|
||||
"label": "F3"
|
||||
},
|
||||
{
|
||||
"action": "general.playersList",
|
||||
"actionHold": "",
|
||||
"icon": "pixelarticons:users",
|
||||
"label": "TAB"
|
||||
},
|
||||
{
|
||||
"action": "general.chat",
|
||||
"actionHold": "",
|
||||
"label": ""
|
||||
},
|
||||
{
|
||||
"action": "ui.pauseMenu",
|
||||
"actionHold": "",
|
||||
"label": ""
|
||||
"ip": "sus.shhnowisnottheti.me",
|
||||
"version": "1.18.2",
|
||||
"description": "Creative, your own 'boxes' (islands)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"alwaysReconnectButton": true,
|
||||
"reportBugButtonWithReconnect": true,
|
||||
"allowAutoConnect": true
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true'
|
||||
|
||||
export default defineConfig({
|
||||
video: false,
|
||||
chromeWebSecurity: false,
|
||||
screenshotOnRunFailure: true, // Enable screenshots on test failures
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
|
|
@ -34,7 +31,7 @@ export default defineConfig({
|
|||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:8080',
|
||||
specPattern: !isPerformanceTest ? 'cypress/e2e/smoke.spec.ts' : 'cypress/e2e/rendering_performance.spec.ts',
|
||||
specPattern: 'cypress/e2e/**/*.spec.ts',
|
||||
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const compareRenderedFlatWorld = () => {
|
|||
}
|
||||
|
||||
const testWorldLoad = () => {
|
||||
return cy.document().then({ timeout: 35_000 }, doc => {
|
||||
return cy.document().then({ timeout: 20_000 }, doc => {
|
||||
return new Cypress.Promise(resolve => {
|
||||
doc.addEventListener('cypress-world-ready', resolve)
|
||||
})
|
||||
|
|
@ -38,18 +38,18 @@ it('Loads & renders singleplayer', () => {
|
|||
testWorldLoad()
|
||||
})
|
||||
|
||||
it.skip('Joins to local flying-squid server', () => {
|
||||
it('Joins to local flying-squid server', () => {
|
||||
visit('/?ip=localhost&version=1.16.1')
|
||||
window.localStorage.version = ''
|
||||
// todo replace with data-test
|
||||
// cy.get('[data-test-id="servers-screen-button"]').click()
|
||||
// cy.get('[data-test-id="server-ip"]').clear().focus().type('localhost')
|
||||
// cy.get('[data-test-id="version"]').clear().focus().type('1.16.1') // todo needs to fix autoversion
|
||||
cy.get('[data-test-id="connect-qs"]').click() // todo! cypress sometimes doesn't click
|
||||
cy.get('[data-test-id="connect-qs"]').click()
|
||||
testWorldLoad()
|
||||
})
|
||||
|
||||
it.skip('Joins to local latest Java vanilla server', () => {
|
||||
it('Joins to local latest Java vanilla server', () => {
|
||||
const version = supportedVersions.at(-1)!
|
||||
cy.task('startServer', [version, 25_590]).then(() => {
|
||||
visit('/?ip=localhost:25590&username=bot')
|
||||
|
|
@ -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
|
||||
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} */
|
||||
const serverOptions = {
|
||||
|
|
|
|||
|
|
@ -1,169 +0,0 @@
|
|||
# Handled Packets
|
||||
|
||||
## Server -> Client
|
||||
|
||||
❌ statistics
|
||||
❌ advancements
|
||||
❌ face_player
|
||||
❌ nbt_query_response
|
||||
❌ chat_suggestions
|
||||
❌ trade_list
|
||||
❌ vehicle_move
|
||||
❌ open_book
|
||||
❌ craft_recipe_response
|
||||
❌ end_combat_event
|
||||
❌ enter_combat_event
|
||||
❌ unlock_recipes
|
||||
❌ camera
|
||||
❌ update_view_position
|
||||
❌ update_view_distance
|
||||
❌ entity_sound_effect
|
||||
❌ stop_sound
|
||||
❌ feature_flags
|
||||
❌ select_advancement_tab
|
||||
❌ declare_recipes
|
||||
❌ tags
|
||||
❌ acknowledge_player_digging
|
||||
❌ initialize_world_border
|
||||
❌ world_border_center
|
||||
❌ world_border_lerp_size
|
||||
❌ world_border_size
|
||||
❌ world_border_warning_delay
|
||||
❌ world_border_warning_reach
|
||||
❌ simulation_distance
|
||||
❌ chunk_biomes
|
||||
❌ hurt_animation
|
||||
✅ damage_event
|
||||
✅ spawn_entity
|
||||
✅ spawn_entity_experience_orb
|
||||
✅ named_entity_spawn
|
||||
✅ animation
|
||||
✅ block_break_animation
|
||||
✅ tile_entity_data
|
||||
✅ block_action
|
||||
✅ block_change
|
||||
✅ boss_bar
|
||||
✅ difficulty
|
||||
✅ tab_complete
|
||||
✅ declare_commands
|
||||
✅ multi_block_change
|
||||
✅ close_window
|
||||
✅ open_window
|
||||
✅ window_items
|
||||
✅ craft_progress_bar
|
||||
✅ set_slot
|
||||
✅ set_cooldown
|
||||
✅ custom_payload
|
||||
✅ hide_message
|
||||
✅ kick_disconnect
|
||||
✅ profileless_chat
|
||||
✅ entity_status
|
||||
✅ explosion
|
||||
✅ unload_chunk
|
||||
✅ game_state_change
|
||||
✅ open_horse_window
|
||||
✅ keep_alive
|
||||
✅ map_chunk
|
||||
✅ world_event
|
||||
✅ world_particles
|
||||
✅ update_light
|
||||
✅ login
|
||||
✅ map
|
||||
✅ rel_entity_move
|
||||
✅ entity_move_look
|
||||
✅ entity_look
|
||||
✅ open_sign_entity
|
||||
✅ abilities
|
||||
✅ player_chat
|
||||
✅ death_combat_event
|
||||
✅ player_remove
|
||||
✅ player_info
|
||||
✅ position
|
||||
✅ entity_destroy
|
||||
✅ remove_entity_effect
|
||||
✅ resource_pack_send
|
||||
✅ respawn
|
||||
✅ entity_head_rotation
|
||||
✅ held_item_slot
|
||||
✅ scoreboard_display_objective
|
||||
✅ entity_metadata
|
||||
✅ attach_entity
|
||||
✅ entity_velocity
|
||||
✅ entity_equipment
|
||||
✅ experience
|
||||
✅ update_health
|
||||
✅ scoreboard_objective
|
||||
✅ set_passengers
|
||||
✅ teams
|
||||
✅ scoreboard_score
|
||||
✅ spawn_position
|
||||
✅ update_time
|
||||
✅ sound_effect
|
||||
✅ system_chat
|
||||
✅ playerlist_header
|
||||
✅ collect
|
||||
✅ entity_teleport
|
||||
✅ entity_update_attributes
|
||||
✅ entity_effect
|
||||
✅ server_data
|
||||
✅ clear_titles
|
||||
✅ action_bar
|
||||
✅ ping
|
||||
✅ set_title_subtitle
|
||||
✅ set_title_text
|
||||
✅ set_title_time
|
||||
✅ packet
|
||||
|
||||
## Client -> Server
|
||||
|
||||
❌ query_block_nbt
|
||||
❌ set_difficulty
|
||||
❌ query_entity_nbt
|
||||
❌ pick_item
|
||||
❌ set_beacon_effect
|
||||
❌ update_command_block_minecart
|
||||
❌ update_structure_block
|
||||
❌ generate_structure
|
||||
❌ lock_difficulty
|
||||
❌ craft_recipe_request
|
||||
❌ displayed_recipe
|
||||
❌ recipe_book
|
||||
❌ update_jigsaw_block
|
||||
❌ spectate
|
||||
❌ advancement_tab
|
||||
✅ teleport_confirm
|
||||
✅ chat_command
|
||||
✅ chat_message
|
||||
✅ message_acknowledgement
|
||||
✅ edit_book
|
||||
✅ name_item
|
||||
✅ select_trade
|
||||
✅ update_command_block
|
||||
✅ tab_complete
|
||||
✅ client_command
|
||||
✅ settings
|
||||
✅ enchant_item
|
||||
✅ window_click
|
||||
✅ close_window
|
||||
✅ custom_payload
|
||||
✅ use_entity
|
||||
✅ keep_alive
|
||||
✅ position
|
||||
✅ position_look
|
||||
✅ look
|
||||
✅ flying
|
||||
✅ vehicle_move
|
||||
✅ steer_boat
|
||||
✅ abilities
|
||||
✅ block_dig
|
||||
✅ entity_action
|
||||
✅ steer_vehicle
|
||||
✅ resource_pack_receive
|
||||
✅ held_item_slot
|
||||
✅ set_creative_slot
|
||||
✅ update_sign
|
||||
✅ arm_animation
|
||||
✅ block_place
|
||||
✅ use_item
|
||||
✅ pong
|
||||
✅ chat_session_update
|
||||
|
Before Width: | Height: | Size: 96 KiB |
135
esbuild.mjs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//@ts-check
|
||||
import * as esbuild from 'esbuild'
|
||||
import fs from 'fs'
|
||||
// import htmlPlugin from '@chialab/esbuild-plugin-html'
|
||||
import server from './server.js'
|
||||
import { clients, plugins, startWatchingHmr } from './scripts/esbuildPlugins.mjs'
|
||||
import { generateSW } from 'workbox-build'
|
||||
import { getSwAdditionalEntries } from './scripts/build.js'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
//@ts-ignore
|
||||
try { await import('./localSettings.mjs') } catch { }
|
||||
|
||||
const entrypoint = 'index.ts'
|
||||
|
||||
fs.writeFileSync('dist/index.html', fs.readFileSync('index.html', 'utf8').replace('<!-- inject script -->', `<script src="${entrypoint.replace(/\.tsx?/, '.js')}"></script>`), 'utf8')
|
||||
|
||||
const watch = process.argv.includes('--watch') || process.argv.includes('-w')
|
||||
const prod = process.argv.includes('--prod')
|
||||
if (prod) process.env.PROD = 'true'
|
||||
const dev = !prod
|
||||
|
||||
const banner = [
|
||||
'window.global = globalThis;',
|
||||
]
|
||||
|
||||
const buildingVersion = new Date().toISOString().split(':')[0]
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const buildOptions = {
|
||||
bundle: true,
|
||||
entryPoints: [`src/${entrypoint}`],
|
||||
target: ['es2020'],
|
||||
jsx: 'automatic',
|
||||
jsxDev: dev,
|
||||
// logLevel: 'debug',
|
||||
logLevel: 'info',
|
||||
platform: 'browser',
|
||||
sourcemap: prod ? true : 'linked',
|
||||
outdir: 'dist',
|
||||
mainFields: [
|
||||
'browser', 'module', 'main'
|
||||
],
|
||||
keepNames: true,
|
||||
banner: {
|
||||
// using \n breaks sourcemaps!
|
||||
js: banner.join(';'),
|
||||
},
|
||||
external: [
|
||||
'sharp'
|
||||
],
|
||||
alias: {
|
||||
events: 'events', // make explicit
|
||||
buffer: 'buffer',
|
||||
'fs': 'browserfs/dist/shims/fs.js',
|
||||
http: 'http-browserify',
|
||||
perf_hooks: './src/perf_hooks_replacement.js',
|
||||
crypto: './src/crypto.js',
|
||||
stream: 'stream-browserify',
|
||||
net: 'net-browserify',
|
||||
assert: 'assert',
|
||||
dns: './src/dns.js',
|
||||
// todo write advancedAliases plugin
|
||||
},
|
||||
inject: [
|
||||
'./src/shims.js'
|
||||
],
|
||||
metafile: true,
|
||||
plugins,
|
||||
sourcesContent: !process.argv.includes('--no-sources'),
|
||||
minify: process.argv.includes('--minify'),
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
|
||||
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
|
||||
'process.env.GITHUB_URL':
|
||||
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`),
|
||||
'process.env.DEPS_VERSIONS': JSON.stringify({})
|
||||
},
|
||||
loader: {
|
||||
// todo use external or resolve issues with duplicating
|
||||
'.png': 'dataurl',
|
||||
'.svg': 'dataurl',
|
||||
'.map': 'empty',
|
||||
'.vert': 'text',
|
||||
'.frag': 'text',
|
||||
'.obj': 'text',
|
||||
},
|
||||
write: false,
|
||||
// todo would be better to enable?
|
||||
// preserveSymlinks: true,
|
||||
}
|
||||
|
||||
if (watch) {
|
||||
const ctx = await esbuild.context(buildOptions)
|
||||
await ctx.watch()
|
||||
startWatchingHmr()
|
||||
server.app.get('/esbuild', (req, res, next) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
// Send a comment to keep the connection alive
|
||||
res.write(': ping\n\n')
|
||||
|
||||
// Add the client response to the clients array
|
||||
clients.push(res)
|
||||
|
||||
// Handle any client disconnection logic
|
||||
res.on('close', () => {
|
||||
const index = clients.indexOf(res)
|
||||
if (index !== -1) {
|
||||
clients.splice(index, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const result = await build(buildOptions)
|
||||
// console.log(await esbuild.analyzeMetafile(result.metafile))
|
||||
|
||||
if (prod) {
|
||||
fs.writeFileSync('dist/version.txt', buildingVersion, 'utf-8')
|
||||
|
||||
const { count, size, warnings } = await generateSW({
|
||||
// dontCacheBustURLsMatching: [new RegExp('...')],
|
||||
globDirectory: 'dist',
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
additionalManifestEntries: getSwAdditionalEntries(),
|
||||
globPatterns: [],
|
||||
swDest: 'dist/service-worker.js',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<script src="decode.ts" type="module"></script>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// Include the pako library
|
||||
import pako from 'pako';
|
||||
import compressedJsRaw from './compressed.js?raw'
|
||||
|
||||
function decompressFromBase64(input) {
|
||||
// Decode the Base64 string
|
||||
const binaryString = atob(input);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
// Convert the binary string to a byte array
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Decompress the byte array
|
||||
const decompressedData = pako.inflate(bytes, { to: 'string' });
|
||||
|
||||
return decompressedData;
|
||||
}
|
||||
|
||||
// Use the function
|
||||
console.time('decompress');
|
||||
const decompressedData = decompressFromBase64(compressedJsRaw);
|
||||
console.timeEnd('decompress')
|
||||
console.log(decompressedData)
|
||||
|
|
@ -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()
|
||||
60
experiments/texture-render.html
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="module">
|
||||
//@ts-check
|
||||
import blockImg from '../prismarine-viewer/public/textures/1.16.1.png'
|
||||
import blocksStates from '../prismarine-viewer/public/blocksStates/1.16.1.json'
|
||||
|
||||
// const block = {
|
||||
// name: 'oak_sign',
|
||||
// variant: 0,
|
||||
// elem: 1,
|
||||
// face: 'up'
|
||||
// }
|
||||
const block = {
|
||||
name: 'light_gray_stained_glass',
|
||||
variant: 0,
|
||||
elem: 0,
|
||||
face: 'north'
|
||||
}
|
||||
//@ts-ignore
|
||||
const model = Object.entries(blocksStates[block.name].variants).find((a, i) => typeof block.variant === 'number' ? i === block.variant : a === block.variant)[1].model.elements[block.elem]
|
||||
console.log(model)
|
||||
const textureUv = model.faces[block.face].texture
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.style.imageRendering = 'pixelated'
|
||||
document.body.appendChild(canvas)
|
||||
const factor = 50
|
||||
const modelWidth = model.to[0] - model.from[0]
|
||||
const modelHeight = model.to[1] - model.from[1]
|
||||
canvas.width = modelWidth * factor
|
||||
canvas.height = modelHeight * factor
|
||||
// canvas.width = 16 * factor
|
||||
// canvas.height = 16 * factor * 2
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
//@ts-ignore
|
||||
ctx.imageSmoothingEnabled = false
|
||||
const img = new Image()
|
||||
const img2 = new Image()
|
||||
img.src = blockImg
|
||||
img.onload = () => {
|
||||
//@ts-ignore
|
||||
ctx.drawImage(img, img.width * textureUv.u, img.height * textureUv.v, img.width * textureUv.su, img.height * textureUv.sv, 0, 0, canvas.width, canvas.height)
|
||||
// ctx.drawImage(img, 0, 0, canvas.width, canvas.height / 2)
|
||||
console.log('width;height texture', img.width * textureUv.su, img.height * textureUv.sv)
|
||||
console.log('base su=sv', 16 / img.width)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -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 +0,0 @@
|
|||
<script type="module" src="three.ts"></script>
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
// Create scene, camera and renderer
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Position camera
|
||||
camera.position.z = 5
|
||||
|
||||
// Create a canvas with some content
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
scene.background = new THREE.Color(0x444444)
|
||||
|
||||
// Draw something on the canvas
|
||||
ctx.fillStyle = '#444444'
|
||||
// ctx.fillRect(0, 0, 256, 256)
|
||||
ctx.fillStyle = 'red'
|
||||
ctx.font = '48px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText('Hello!', 128, 128)
|
||||
|
||||
// Create bitmap and texture
|
||||
async function createTexturedBox() {
|
||||
const canvas2 = new OffscreenCanvas(256, 256)
|
||||
const ctx2 = canvas2.getContext('2d')!
|
||||
ctx2.drawImage(canvas, 0, 0)
|
||||
const texture = new THREE.Texture(canvas2)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.needsUpdate = true
|
||||
texture.flipY = false
|
||||
|
||||
// Create box with texture
|
||||
const geometry = new THREE.BoxGeometry(2, 2, 2)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide,
|
||||
premultipliedAlpha: false,
|
||||
})
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
scene.add(cube)
|
||||
}
|
||||
|
||||
// Create the textured box
|
||||
createTexturedBox()
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
animate()
|
||||
117
index.html
|
|
@ -1,22 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="darkreader-lock">
|
||||
<script>
|
||||
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>
|
||||
<!-- // #region initial loader -->
|
||||
<script async>
|
||||
const loadingDiv = /* html */ `
|
||||
const loadingDiv = `
|
||||
<div class="initial-loader" style="position: fixed;transition:opacity 0.2s;inset: 0;background:black;display: flex;justify-content: center;align-items: center;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, Helvetica, sans-serif;gap: 15px;" ontransitionend="this.remove()">
|
||||
<div>
|
||||
<img src="./loading-bg.jpg" alt="Prismarine Web Client" style="position:fixed;inset:0;width:100%;height:100%;z-index: -2;object-fit: cover;filter: blur(3px);">
|
||||
|
|
@ -25,68 +15,25 @@
|
|||
<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>
|
||||
<!-- 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>
|
||||
`
|
||||
const insertLoadingDiv = () => {
|
||||
const loadingDivElem = document.createElement('div')
|
||||
loadingDivElem.innerHTML = loadingDiv
|
||||
if (!window.pageLoaded) {
|
||||
document.documentElement.appendChild(loadingDivElem)
|
||||
const loadingDivElem = document.createElement('div')
|
||||
loadingDivElem.innerHTML = loadingDiv
|
||||
if (!window.pageLoaded) {
|
||||
document.documentElement.appendChild(loadingDivElem)
|
||||
}
|
||||
// load error handling
|
||||
const onError = (message) => {
|
||||
console.log(message)
|
||||
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('.subtitle').textContent = message
|
||||
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
||||
}
|
||||
|
||||
// iOS version detection
|
||||
const getIOSVersion = () => {
|
||||
const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
}
|
||||
|
||||
// load error handling
|
||||
const onError = (errorOrMessage, log = false) => {
|
||||
let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage
|
||||
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') {
|
||||
document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error'
|
||||
const [errorMessage, ...errorStack] = message.split('\n')
|
||||
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage
|
||||
document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n')
|
||||
|
||||
// Show iOS warning if applicable
|
||||
const iosVersion = getIOSVersion();
|
||||
if (iosVersion !== null && iosVersion < 15) {
|
||||
document.querySelector('.initial-loader').querySelector('.ios-warning').style.display = 'block';
|
||||
}
|
||||
|
||||
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
||||
// unregister all sw
|
||||
if (window.navigator.serviceWorker && document.querySelector('.initial-loader').style.opacity !== 0) {
|
||||
console.log('got worker')
|
||||
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('error', (e) => onError(e.error ?? e.message))
|
||||
}
|
||||
insertLoadingDiv()
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// move loading div to the end of body
|
||||
const loadingDivElem = document.querySelector('.initial-loader');
|
||||
const newContainer = document.body; // replace with your new container
|
||||
if (loadingDivElem) newContainer.appendChild(loadingDivElem);
|
||||
})
|
||||
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
|
||||
window.addEventListener('error', (e) => onError(e.message))
|
||||
</script>
|
||||
<script type="module" async>
|
||||
const checkLoadEruda = () => {
|
||||
|
|
@ -95,25 +42,6 @@
|
|||
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
|
||||
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()
|
||||
|
|
@ -147,17 +75,22 @@
|
|||
window.loadedPlugins[pluginName] = await import(script)
|
||||
}
|
||||
</script> -->
|
||||
<title>Minecraft Web Client</title>
|
||||
<!-- <link rel="canonical" href="https://mcraft.fun"> -->
|
||||
<meta name="description" content="Minecraft Java Edition Client in Browser — Full Multiplayer Support, Server Connect, Offline Play — Join real Minecraft servers">
|
||||
<title>Prismarine Web Client</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<link rel="favicon" href="favicon.png">
|
||||
<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="date" content="2024-07-11" scheme="YYYY-MM-DD">
|
||||
<meta name="date" content="2023-09-11" scheme="YYYY-MM-DD">
|
||||
<meta name="language" content="English">
|
||||
<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 property="og:title" content="Minecraft Web Client" />
|
||||
<meta property="og:title" content="Prismarine Web Client" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="favicon.png" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root"></div>
|
||||
|
|
|
|||
142
package.json
|
|
@ -3,47 +3,32 @@
|
|||
"version": "0.0.0-dev",
|
||||
"description": "A minecraft client running in a browser",
|
||||
"scripts": {
|
||||
"dev-rsbuild": "rsbuild dev",
|
||||
"dev-proxy": "node server.js",
|
||||
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
||||
"start2": "run-p dev-rsbuild watch-mesher",
|
||||
"start-metrics": "ENABLE_METRICS=true rsbuild dev",
|
||||
"build": "pnpm build-other-workers && rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
||||
"prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts",
|
||||
"check-build": "pnpm prepare-project && tsc && pnpm build",
|
||||
"start": "node scripts/build.js copyFilesDev && node scripts/prepareData.mjs && node esbuild.mjs --watch",
|
||||
"start-watch-script": "nodemon -w esbuild.mjs --watch",
|
||||
"build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod",
|
||||
"check-build": "tsc && pnpm build",
|
||||
"test:cypress": "cypress run",
|
||||
"test:benchmark": "PERFORMANCE_TEST=true cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"test-unit": "vitest",
|
||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||
"prod-start": "node server.js --prod",
|
||||
"prod-start": "node server.js",
|
||||
"postinstall": "node scripts/gen-texturepack-files.mjs && tsx scripts/optimizeBlockCollisions.ts",
|
||||
"test-mc-server": "tsx cypress/minecraft-server.mjs",
|
||||
"lint": "eslint \"{src,cypress,renderer}/**/*.{ts,js,jsx,tsx}\"",
|
||||
"lint-fix": "pnpm lint --fix",
|
||||
"lint": "eslint \"{src,cypress}/**/*.{ts,js,jsx,tsx}\"",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
||||
"start-experiments": "vite --config experiments/vite.config.ts --host",
|
||||
"watch-other-workers": "echo NOT IMPLEMENTED",
|
||||
"build-other-workers": "echo NOT IMPLEMENTED",
|
||||
"build-mesher": "node renderer/buildMesherWorker.mjs",
|
||||
"watch-mesher": "pnpm build-mesher -w",
|
||||
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
||||
"watch-mesher": "node prismarine-viewer/buildMesherWorker.mjs -w",
|
||||
"run-playground": "run-p watch-mesher watch-other-workers playground-server watch-playground",
|
||||
"run-all": "run-p start run-playground",
|
||||
"build-playground": "rsbuild build --config renderer/rsbuild.config.ts",
|
||||
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts",
|
||||
"update-git-deps": "tsx scripts/updateGitDeps.ts",
|
||||
"request-data": "tsx scripts/requestData.ts"
|
||||
"playground-server": "live-server --port=9090 prismarine-viewer/public",
|
||||
"watch-playground": "node prismarine-viewer/esbuild.mjs -w"
|
||||
},
|
||||
"keywords": [
|
||||
"prismarine",
|
||||
"web",
|
||||
"client"
|
||||
],
|
||||
"release": {
|
||||
"attachReleaseFiles": "{self-host.zip,minecraft.html}"
|
||||
},
|
||||
"publish": {
|
||||
"preset": {
|
||||
"publishOnlyIfChanged": true,
|
||||
|
|
@ -54,17 +39,16 @@
|
|||
"dependencies": {
|
||||
"@dimaka/interface": "0.0.3-alpha.0",
|
||||
"@floating-ui/react": "^0.26.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
|
||||
"@nxg-org/mineflayer-tracker": "1.3.0",
|
||||
"@mui/base": "5.0.0-beta.40",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.7",
|
||||
"@nxg-org/mineflayer-tracker": "^1.2.1",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/wicg-file-system-access": "^2023.10.2",
|
||||
"@xmcl/text-component": "^2.1.3",
|
||||
"@zardoy/react-util": "^0.2.4",
|
||||
"@zardoy/react-util": "^0.2.0",
|
||||
"@zardoy/utils": "^0.0.11",
|
||||
"adm-zip": "^0.5.12",
|
||||
"browserfs": "github:zardoy/browserfs#build",
|
||||
|
|
@ -72,29 +56,27 @@
|
|||
"classnames": "^2.5.1",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"debug": "^4.3.4",
|
||||
"deepslate": "^0.23.5",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"eruda": "^3.0.1",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"express": "^4.18.2",
|
||||
"filesize": "^10.0.12",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104",
|
||||
"framer-motion": "^12.9.2",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.29",
|
||||
"fs-extra": "^11.1.1",
|
||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"iconify-icon": "^1.0.8",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mcraft-fun-mineflayer": "^0.1.23",
|
||||
"minecraft-data": "3.98.0",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"minecraft-assets": "^1.12.2",
|
||||
"minecraft-data": "3.65.0",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
"mojangson": "^2.0.4",
|
||||
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
|
||||
"node-gzip": "^1.1.2",
|
||||
"peerjs": "^1.5.0",
|
||||
"pixelarticons": "^1.8.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"prosemirror-example-setup": "^1.2.2",
|
||||
|
|
@ -105,8 +87,7 @@
|
|||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"remark": "^15.0.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"skinview3d": "^3.0.1",
|
||||
|
|
@ -123,18 +104,13 @@
|
|||
"workbox-build": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "1.3.5",
|
||||
"@rsbuild/plugin-node-polyfill": "1.3.0",
|
||||
"@rsbuild/plugin-react": "1.2.0",
|
||||
"@rsbuild/plugin-type-check": "1.2.1",
|
||||
"@rsbuild/plugin-typed-css-modules": "1.0.2",
|
||||
"@storybook/addon-essentials": "^7.4.6",
|
||||
"@storybook/addon-links": "^7.4.6",
|
||||
"@storybook/blocks": "^7.4.6",
|
||||
"@storybook/react": "^7.4.6",
|
||||
"@storybook/react-vite": "^7.4.6",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/react-transition-group": "^4.4.7",
|
||||
"@types/stats.js": "^0.17.1",
|
||||
"@types/three": "0.154.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
|
|
@ -144,99 +120,57 @@
|
|||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"constants-browserify": "^1.0.0",
|
||||
"contro-max": "^0.1.9",
|
||||
"contro-max": "^0.1.8",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"cypress": "^10.11.0",
|
||||
"cypress-esbuild-preprocessor": "^1.0.2",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-config-zardoy": "^0.2.17",
|
||||
"events": "^3.3.0",
|
||||
"gzip-size": "^7.0.0",
|
||||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.62",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||
"mineflayer-mouse": "^0.1.21",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"os-browserify": "^0.3.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-exists-cli": "^2.0.0",
|
||||
"prismarine-viewer": "link:prismarine-viewer",
|
||||
"process": "github:PrismarineJS/node-process",
|
||||
"renderer": "link:renderer",
|
||||
"rimraf": "^5.0.1",
|
||||
"storybook": "^7.4.6",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"three": "0.154.0",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"typescript": "5.5.4",
|
||||
"typescript": "5.5.0-beta",
|
||||
"vitest": "^0.34.6",
|
||||
"yaml": "^2.3.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cypress": "^10.11.0",
|
||||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"sharp": "^0.33.5",
|
||||
"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": {
|
||||
"overrides": {
|
||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||
"@nxg-org/mineflayer-physics-util": "1.8.10",
|
||||
"buffer": "^6.0.3",
|
||||
"vec3": "0.1.10",
|
||||
"@nxg-org/mineflayer-physics-util": "1.5.8",
|
||||
"three": "0.154.0",
|
||||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.98.0",
|
||||
"minecraft-data": "3.65.0",
|
||||
"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",
|
||||
"react": "^18.2.0",
|
||||
"prismarine-chunk": "github:zardoy/prismarine-chunk#master",
|
||||
"prismarine-item": "latest"
|
||||
"prismarine-chunk": "github:zardoy/prismarine-chunk"
|
||||
},
|
||||
"updateConfig": {
|
||||
"ignoreDependencies": [
|
||||
"browserfs",
|
||||
"google-drive-browserfs"
|
||||
]
|
||||
"ignoreDependencies": []
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
||||
"minecraft-protocol": "patches/minecraft-protocol.patch"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"canvas",
|
||||
"core-js",
|
||||
"gl"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp",
|
||||
"cypress",
|
||||
"esbuild",
|
||||
"fsevents"
|
||||
],
|
||||
"ignorePatchFailures": false,
|
||||
"allowUnusedPatches": false
|
||||
"minecraft-protocol@1.47.0": "patches/minecraft-protocol@1.47.0.patch",
|
||||
"three@0.154.0": "patches/three@0.154.0.patch"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
|
||||
"packageManager": "pnpm@9.0.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,7 @@
|
|||
"description": "A Minecraft-like React UI library",
|
||||
"keywords": [
|
||||
"minecraft",
|
||||
"minecraft style",
|
||||
"minecraft ui",
|
||||
"minecraft components",
|
||||
"minecraft react",
|
||||
"minecraft library",
|
||||
"minecraft web",
|
||||
"minecraft browser"
|
||||
"minecraft style"
|
||||
],
|
||||
"license": "MIT",
|
||||
"sideEffects": false,
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
130
patches/minecraft-protocol@1.47.0.patch
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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/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 c89375e32babbf3559655b1e95f6441b9a30796f..f24cd5dc8fa9a0a4000b184fb3c79590a3ad8b8a 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
|
||||
+ }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -166,7 +174,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()
|
||||
}
|
||||
|
||||
@@ -195,6 +206,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()
|
||||
}
|
||||
@@ -236,8 +249,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 0a5821c32d735e11205a280aa5a503c13533dc14..94a49f661d922478b940d853169b6087e6ec3df5 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -121,6 +121,7 @@ declare module 'minecraft-protocol' {
|
||||
sessionServer?: string
|
||||
keepAlive?: boolean
|
||||
closeTimeout?: number
|
||||
+ closeTimeout?: number
|
||||
noPongTimeout?: number
|
||||
checkTimeoutInterval?: number
|
||||
version?: string
|
||||
@@ -141,6 +142,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,16 +0,0 @@
|
|||
diff --git a/package.json b/package.json
|
||||
index 2a7aff75a9f1c7fe4eebb657002e58f4581dad0e..cd3490983353336efeb13f24f0af69c6c1d16444 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -9,10 +9,7 @@
|
||||
"keywords": [],
|
||||
"author": "Ic3Tank",
|
||||
"license": "ISC",
|
||||
- "dependencies": {
|
||||
- "mineflayer": "^4.3.0",
|
||||
- "sharp": "^0.30.6"
|
||||
- },
|
||||
+ "dependencies": {},
|
||||
"devDependencies": {
|
||||
"mineflayer-item-map-downloader": "file:./"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f69979165ab4 100644
|
||||
--- a/fonts/pixelart-icons-font.css
|
||||
+++ b/fonts/pixelart-icons-font.css
|
||||
@@ -1,16 +1,13 @@
|
||||
@font-face {
|
||||
font-family: "pixelart-icons-font";
|
||||
- src: url('pixelart-icons-font.eot?t=1711815892278'); /* IE9*/
|
||||
- src: url('pixelart-icons-font.eot?t=1711815892278#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
+ src:
|
||||
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
||||
url("pixelart-icons-font.woff?t=1711815892278") format("woff"),
|
||||
- url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
- url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */
|
||||
+ url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
}
|
||||
|
||||
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
||||
font-family: 'pixelart-icons-font' !important;
|
||||
- font-size:24px;
|
||||
font-style:normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -503,4 +500,3 @@
|
||||
.pixelart-icons-font-zap:before { content: "\ebe4"; }
|
||||
.pixelart-icons-font-zoom-in:before { content: "\ebe5"; }
|
||||
.pixelart-icons-font-zoom-out:before { content: "\ebe6"; }
|
||||
-
|
||||
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 {
|
||||
14486
pnpm-lock.yaml
generated
|
|
@ -1,4 +1,4 @@
|
|||
packages:
|
||||
- "."
|
||||
- "renderer"
|
||||
- "renderer/viewer/sign-renderer/"
|
||||
- "prismarine-viewer"
|
||||
- "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',
|
||||
entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')],
|
||||
minify: !watch,
|
||||
minify: true,
|
||||
logLevel: 'info',
|
||||
drop: !watch ? [
|
||||
'debugger'
|
||||
] : [],
|
||||
sourcemap: 'linked',
|
||||
target: watch ? undefined : ['ios14'],
|
||||
write: false,
|
||||
metafile: true,
|
||||
outdir: path.join(__dirname, './dist'),
|
||||
outdir: path.join(__dirname, './public'),
|
||||
define: {
|
||||
'process.env.BROWSER': '"true"',
|
||||
},
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
'.obj': 'text'
|
||||
},
|
||||
plugins: [
|
||||
...mesherSharedPlugins,
|
||||
{
|
||||
name: 'external-json',
|
||||
setup(build) {
|
||||
setup (build) {
|
||||
build.onResolve({ filter: /\.json$/ }, args => {
|
||||
const fileName = args.path.split('/').pop().replace('.json', '')
|
||||
if (args.resolveDir.includes('minecraft-data')) {
|
||||
|
|
@ -113,17 +108,15 @@ const buildOptions = {
|
|||
})
|
||||
build.onEnd(({ metafile, outputFiles }) => {
|
||||
if (!metafile) return
|
||||
fs.mkdirSync(path.join(__dirname, './dist'), { recursive: true })
|
||||
fs.writeFileSync(path.join(__dirname, './dist/metafile.json'), JSON.stringify(metafile))
|
||||
for (const outDir of ['../dist/', './dist/']) {
|
||||
fs.writeFileSync(path.join(__dirname, './public/metafile.json'), JSON.stringify(metafile))
|
||||
for (const outDir of ['../dist/', './public/']) {
|
||||
for (const outputFile of outputFiles) {
|
||||
if (outDir === '../dist/' && outputFile.path.endsWith('.map')) {
|
||||
// skip writing & browser loading sourcemap there, worker debugging should be done in playground
|
||||
// continue
|
||||
}
|
||||
const writePath = path.join(__dirname, outDir, path.basename(outputFile.path))
|
||||
fs.mkdirSync(path.dirname(writePath), { recursive: true })
|
||||
fs.writeFileSync(writePath, outputFile.text)
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(__dirname, outDir, path.basename(outputFile.path)), outputFile.text)
|
||||
}
|
||||
}
|
||||
})
|
||||
96
prismarine-viewer/esbuild.mjs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import * as fs from 'fs'
|
||||
import fsExtra from 'fs-extra'
|
||||
|
||||
//@ts-check
|
||||
import * as esbuild from 'esbuild'
|
||||
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
||||
import path, { dirname, join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const dev = process.argv.includes('-w')
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
||||
|
||||
const mcDataPath = join(__dirname, '../dist/mc-data')
|
||||
if (!fs.existsSync(mcDataPath)) {
|
||||
// shouldn't it be in the viewer instead?
|
||||
await import('../scripts/prepareData.mjs')
|
||||
}
|
||||
|
||||
fs.mkdirSync(join(__dirname, 'public'), { recursive: true })
|
||||
fs.copyFileSync(join(__dirname, 'playground.html'), join(__dirname, 'public/index.html'))
|
||||
fsExtra.copySync(mcDataPath, join(__dirname, 'public/mc-data'))
|
||||
const availableVersions = fs.readdirSync(mcDataPath).map(ver => ver.replace('.js', ''))
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const buildOptions = {
|
||||
bundle: true,
|
||||
entryPoints: [join(__dirname, './examples/playground.ts')],
|
||||
// target: ['es2020'],
|
||||
// logLevel: 'debug',
|
||||
logLevel: 'info',
|
||||
platform: 'browser',
|
||||
sourcemap: dev ? 'inline' : false,
|
||||
minify: !dev,
|
||||
outfile: join(__dirname, 'public/playground.js'),
|
||||
mainFields: [
|
||||
'browser', 'module', 'main'
|
||||
],
|
||||
keepNames: true,
|
||||
banner: {
|
||||
js: `globalThis.global = globalThis;globalThis.includedVersions = ${JSON.stringify(availableVersions)};`,
|
||||
},
|
||||
alias: {
|
||||
events: 'events',
|
||||
buffer: 'buffer',
|
||||
'fs': 'browserfs/dist/shims/fs.js',
|
||||
http: 'http-browserify',
|
||||
stream: 'stream-browserify',
|
||||
net: 'net-browserify',
|
||||
},
|
||||
inject: [],
|
||||
metafile: true,
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
'.obj': 'text',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'minecraft-data',
|
||||
setup (build) {
|
||||
build.onLoad({
|
||||
filter: /minecraft-data[\/\\]data.js$/,
|
||||
}, () => {
|
||||
const defaultVersionsObj = {}
|
||||
return {
|
||||
contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`,
|
||||
loader: 'js',
|
||||
}
|
||||
})
|
||||
build.onEnd((e) => {
|
||||
if (e.errors.length) return
|
||||
fs.writeFileSync(join(__dirname, 'public/metafile.json'), JSON.stringify(e.metafile), 'utf8')
|
||||
})
|
||||
}
|
||||
},
|
||||
polyfillNode({
|
||||
polyfills: {
|
||||
fs: false,
|
||||
crypto: false,
|
||||
events: false,
|
||||
http: false,
|
||||
stream: false,
|
||||
buffer: false,
|
||||
perf_hooks: false,
|
||||
net: false,
|
||||
},
|
||||
})
|
||||
],
|
||||
}
|
||||
if (dev) {
|
||||
(await esbuild.context(buildOptions)).watch()
|
||||
} else {
|
||||
await esbuild.build(buildOptions)
|
||||
}
|
||||
|
||||
// await ctx.rebuild()
|
||||
478
prismarine-viewer/examples/playground.ts
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
import _ from 'lodash'
|
||||
import { WorldDataEmitter, Viewer } from '../viewer'
|
||||
import { Vec3 } from 'vec3'
|
||||
import BlockLoader from 'prismarine-block'
|
||||
import ChunkLoader from 'prismarine-chunk'
|
||||
import WorldLoader from 'prismarine-world'
|
||||
import * as THREE from 'three'
|
||||
import { GUI } from 'lil-gui'
|
||||
import { toMajor } from '../viewer/lib/version'
|
||||
import { loadScript } from '../viewer/lib/utils'
|
||||
import JSZip from 'jszip'
|
||||
import { TWEEN_DURATION } from '../viewer/lib/entities'
|
||||
import { EntityMesh } from '../viewer/lib/entity/EntityMesh'
|
||||
|
||||
globalThis.THREE = THREE
|
||||
//@ts-ignore
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
|
||||
const gui = new GUI()
|
||||
|
||||
// initial values
|
||||
const params = {
|
||||
skip: '',
|
||||
version: globalThis.includedVersions.sort((a, b) => {
|
||||
const s = (x) => {
|
||||
const parts = x.split('.')
|
||||
return +parts[0] + (+parts[1])
|
||||
}
|
||||
return s(a) - s(b)
|
||||
}).at(-1),
|
||||
block: '',
|
||||
metadata: 0,
|
||||
supportBlock: false,
|
||||
entity: '',
|
||||
removeEntity () {
|
||||
this.entity = ''
|
||||
},
|
||||
entityRotate: false,
|
||||
camera: '',
|
||||
playSound () { },
|
||||
blockIsomorphicRenderBundle () { }
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
qs.forEach((value, key) => {
|
||||
const parsed = value.match(/^-?\d+$/) ? parseInt(value) : value === 'true' ? true : value === 'false' ? false : value
|
||||
params[key] = parsed
|
||||
})
|
||||
const setQs = () => {
|
||||
const newQs = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (!value || typeof value === 'function' || params.skip.includes(key)) continue
|
||||
//@ts-ignore
|
||||
newQs.set(key, value)
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`)
|
||||
}
|
||||
|
||||
let ignoreResize = false
|
||||
|
||||
async function main () {
|
||||
let continuousRender = false
|
||||
|
||||
const { version } = params
|
||||
// temporary solution until web worker is here, cache data for faster reloads
|
||||
const globalMcData = window['mcData']
|
||||
if (!globalMcData['version']) {
|
||||
const major = toMajor(version)
|
||||
const sessionKey = `mcData-${major}`
|
||||
if (sessionStorage[sessionKey]) {
|
||||
Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey]))
|
||||
} else {
|
||||
if (sessionStorage.length > 1) sessionStorage.clear()
|
||||
await loadScript(`./mc-data/${major}.js`)
|
||||
try {
|
||||
sessionStorage[sessionKey] = JSON.stringify(Object.fromEntries(Object.entries(globalMcData).filter(([ver]) => ver.startsWith(major))))
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
const mcData = require('minecraft-data')(version)
|
||||
window['loadedData'] = mcData
|
||||
|
||||
gui.add(params, 'version', globalThis.includedVersions)
|
||||
gui.add(params, 'block', mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b)))
|
||||
const metadataGui = gui.add(params, 'metadata')
|
||||
gui.add(params, 'supportBlock')
|
||||
gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b))).listen()
|
||||
gui.add(params, 'removeEntity')
|
||||
gui.add(params, 'entityRotate')
|
||||
gui.add(params, 'skip')
|
||||
gui.add(params, 'playSound')
|
||||
gui.add(params, 'blockIsomorphicRenderBundle')
|
||||
gui.open(false)
|
||||
let metadataFolder = gui.addFolder('metadata')
|
||||
// let entityRotationFolder = gui.addFolder('entity metadata')
|
||||
|
||||
const Chunk = ChunkLoader(version)
|
||||
const Block = BlockLoader(version)
|
||||
// const data = await fetch('smallhouse1.schem').then(r => r.arrayBuffer())
|
||||
// const schem = await Schematic.read(Buffer.from(data), version)
|
||||
|
||||
const viewDistance = 0
|
||||
const targetPos = new Vec3(2, 90, 2)
|
||||
|
||||
const World = WorldLoader(version)
|
||||
|
||||
// const diamondSquare = require('diamond-square')({ version, seed: Math.floor(Math.random() * Math.pow(2, 31)) })
|
||||
|
||||
//@ts-ignore
|
||||
const chunk1 = new Chunk()
|
||||
//@ts-ignore
|
||||
const chunk2 = new Chunk()
|
||||
chunk1.setBlockStateId(targetPos, 34)
|
||||
chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34)
|
||||
const world = new World((chunkX, chunkZ) => {
|
||||
// if (chunkX === 0 && chunkZ === 0) return chunk1
|
||||
// if (chunkX === 1 && chunkZ === 0) return chunk2
|
||||
//@ts-ignore
|
||||
const chunk = new Chunk()
|
||||
return chunk
|
||||
})
|
||||
|
||||
// await schem.paste(world, new Vec3(0, 60, 0))
|
||||
|
||||
const worldView = new WorldDataEmitter(world, viewDistance, targetPos)
|
||||
|
||||
// Create three.js context, add to page
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] })
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1)
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Create viewer
|
||||
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false })
|
||||
viewer.entities.setDebugMode('basic')
|
||||
viewer.setVersion(version)
|
||||
viewer.entities.onSkinUpdate = () => {
|
||||
viewer.render()
|
||||
}
|
||||
viewer.world.mesherConfig.enableLighting = false
|
||||
|
||||
viewer.listen(worldView)
|
||||
// Load chunks
|
||||
await worldView.init(targetPos)
|
||||
window['worldView'] = worldView
|
||||
window['viewer'] = viewer
|
||||
|
||||
params.blockIsomorphicRenderBundle = () => {
|
||||
const canvas = renderer.domElement
|
||||
const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one')
|
||||
const sizeRaw = prompt('Size', '512')
|
||||
if (!sizeRaw) return
|
||||
const size = parseInt(sizeRaw)
|
||||
// const size = 512
|
||||
|
||||
ignoreResize = true
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
renderer.setSize(size, size)
|
||||
|
||||
//@ts-ignore
|
||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||
viewer.scene.background = null
|
||||
|
||||
const rad = THREE.MathUtils.degToRad(-120)
|
||||
viewer.directionalLight.position.set(
|
||||
Math.cos(rad),
|
||||
Math.sin(rad),
|
||||
0.2
|
||||
).normalize()
|
||||
viewer.directionalLight.intensity = 1
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-30)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
// viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1)
|
||||
|
||||
const allBlocks = mcData.blocksArray.map(b => b.name)
|
||||
// const allBlocks = ['stone', 'warped_slab']
|
||||
|
||||
let blockCount = 1
|
||||
let blockName = allBlocks[0]
|
||||
|
||||
const updateBlock = () => {
|
||||
|
||||
//@ts-ignore
|
||||
// viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId)
|
||||
params.block = blockName
|
||||
// todo cleanup (introduce getDefaultState)
|
||||
onUpdate.block()
|
||||
applyChanges(false, true)
|
||||
}
|
||||
viewer.waitForChunksToRender().then(async () => {
|
||||
// wait for next macro task
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (onlyCurrent) {
|
||||
viewer.render()
|
||||
onWorldUpdate()
|
||||
} else {
|
||||
// will be called on every render update
|
||||
viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate)
|
||||
updateBlock()
|
||||
}
|
||||
})
|
||||
|
||||
const zip = new JSZip()
|
||||
zip.file('description.txt', 'Generated with prismarine-viewer')
|
||||
|
||||
const end = async () => {
|
||||
// download zip file
|
||||
|
||||
const a = document.createElement('a')
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const dataUrlZip = URL.createObjectURL(blob)
|
||||
a.href = dataUrlZip
|
||||
a.download = 'blocks_render.zip'
|
||||
a.click()
|
||||
URL.revokeObjectURL(dataUrlZip)
|
||||
console.log('end')
|
||||
|
||||
viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate)
|
||||
}
|
||||
|
||||
async function onWorldUpdate () {
|
||||
// await new Promise(resolve => {
|
||||
// setTimeout(resolve, 50)
|
||||
// })
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
|
||||
zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true })
|
||||
|
||||
if (onlyCurrent) {
|
||||
end()
|
||||
} else {
|
||||
nextBlock()
|
||||
}
|
||||
}
|
||||
const nextBlock = async () => {
|
||||
blockName = allBlocks[blockCount++]
|
||||
console.log(allBlocks.length, '/', blockCount, blockName)
|
||||
if (blockCount % 5 === 0) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
if (blockName) {
|
||||
updateBlock()
|
||||
} else {
|
||||
end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
const controls = new OrbitControls(viewer.camera, renderer.domElement)
|
||||
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-45)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
|
||||
controls.update()
|
||||
|
||||
let blockProps = {}
|
||||
let entityOverrides = {}
|
||||
const getBlock = () => {
|
||||
return mcData.blocksByName[params.block || 'air']
|
||||
}
|
||||
|
||||
const entityUpdateShared = () => {
|
||||
viewer.entities.clear()
|
||||
if (!params.entity) return
|
||||
worldView.emit('entity', {
|
||||
id: 'id', name: params.entity, pos: targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0
|
||||
})
|
||||
const enableSkeletonDebug = (obj) => {
|
||||
const { children, isSkeletonHelper } = obj
|
||||
if (!Array.isArray(children)) return
|
||||
if (isSkeletonHelper) {
|
||||
obj.visible = true
|
||||
return
|
||||
}
|
||||
for (const child of children) {
|
||||
if (typeof child === 'object') enableSkeletonDebug(child)
|
||||
}
|
||||
}
|
||||
enableSkeletonDebug(viewer.entities.entities['id'])
|
||||
setTimeout(() => {
|
||||
viewer.render()
|
||||
}, TWEEN_DURATION)
|
||||
}
|
||||
|
||||
const onUpdate = {
|
||||
block () {
|
||||
metadataFolder.destroy()
|
||||
const block = mcData.blocksByName[params.block]
|
||||
if (!block) return
|
||||
const props = new Block(block.id, 0, 0).getProperties()
|
||||
//@ts-ignore
|
||||
const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
|
||||
metadataFolder = gui.addFolder('metadata')
|
||||
if (states) {
|
||||
for (const state of states) {
|
||||
let defaultValue
|
||||
switch (state.type) {
|
||||
case 'enum':
|
||||
defaultValue = state.values[0]
|
||||
break
|
||||
case 'bool':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'int':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'direction':
|
||||
defaultValue = 'north'
|
||||
break
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
blockProps[state.name] = defaultValue
|
||||
if (state.type === 'enum') {
|
||||
metadataFolder.add(blockProps, state.name, state.values)
|
||||
} else {
|
||||
metadataFolder.add(blockProps, state.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [name, value] of Object.entries(props)) {
|
||||
blockProps[name] = value
|
||||
metadataFolder.add(blockProps, name)
|
||||
}
|
||||
}
|
||||
metadataFolder.open()
|
||||
},
|
||||
entity () {
|
||||
continuousRender = params.entity === 'player'
|
||||
entityUpdateShared()
|
||||
if (!params.entity) return
|
||||
if (params.entity === 'player') {
|
||||
viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, true, true)
|
||||
viewer.entities.playAnimation('id', 'running')
|
||||
}
|
||||
// let prev = false
|
||||
// setInterval(() => {
|
||||
// viewer.entities.playAnimation('id', prev ? 'running' : 'idle')
|
||||
// prev = !prev
|
||||
// }, 1000)
|
||||
|
||||
EntityMesh.getStaticData(params.entity)
|
||||
// entityRotationFolder.destroy()
|
||||
// entityRotationFolder = gui.addFolder('entity metadata')
|
||||
// entityRotationFolder.add(params, 'entityRotate')
|
||||
// entityRotationFolder.open()
|
||||
},
|
||||
supportBlock () {
|
||||
viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const applyChanges = (metadataUpdate = false, skipQs = false) => {
|
||||
const blockId = getBlock()?.id
|
||||
let block: BlockLoader.Block
|
||||
if (metadataUpdate) {
|
||||
block = new Block(blockId, 0, params.metadata)
|
||||
Object.assign(blockProps, block.getProperties())
|
||||
for (const _child of metadataFolder.children) {
|
||||
const child = _child as import('lil-gui').Controller
|
||||
child.updateDisplay()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
//@ts-ignore
|
||||
block = Block.fromProperties(blockId ?? -1, blockProps, 0)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
block = Block.fromStateId(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
viewer.setBlockStateId(targetPos, block.stateId)
|
||||
console.log('up stateId', block.stateId)
|
||||
params.metadata = block.metadata
|
||||
metadataGui.updateDisplay()
|
||||
if (!skipQs) {
|
||||
setQs()
|
||||
}
|
||||
}
|
||||
gui.onChange(({ property, object }) => {
|
||||
if (object === params) {
|
||||
if (property === 'camera') return
|
||||
onUpdate[property]?.()
|
||||
applyChanges(property === 'metadata')
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
})
|
||||
viewer.waitForChunksToRender().then(async () => {
|
||||
for (const update of Object.values(onUpdate)) {
|
||||
update()
|
||||
}
|
||||
applyChanges(true)
|
||||
gui.openAnimated()
|
||||
})
|
||||
|
||||
const animate = () => {
|
||||
// if (controls) controls.update()
|
||||
// worldView.updatePosition(controls.target)
|
||||
viewer.render()
|
||||
// window.requestAnimationFrame(animate)
|
||||
}
|
||||
viewer.world.renderUpdateEmitter.addListener('update', () => {
|
||||
animate()
|
||||
})
|
||||
animate()
|
||||
|
||||
// #region camera rotation param
|
||||
if (params.camera) {
|
||||
const [x, y] = params.camera.split(',')
|
||||
viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX')
|
||||
controls.update()
|
||||
console.log(viewer.camera.rotation.x, parseFloat(x))
|
||||
}
|
||||
const throttledCamQsUpdate = _.throttle(() => {
|
||||
const { camera } = viewer
|
||||
// params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}`
|
||||
setQs()
|
||||
}, 200)
|
||||
controls.addEventListener('change', () => {
|
||||
throttledCamQsUpdate()
|
||||
animate()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
const continuousUpdate = () => {
|
||||
if (continuousRender) {
|
||||
animate()
|
||||
}
|
||||
requestAnimationFrame(continuousUpdate)
|
||||
}
|
||||
continuousUpdate()
|
||||
|
||||
window.onresize = () => {
|
||||
if (ignoreResize) return
|
||||
// const vec3 = new THREE.Vector3()
|
||||
// vec3.set(-1, -1, -1).unproject(viewer.camera)
|
||||
// console.log(vec3)
|
||||
// box.position.set(vec3.x, vec3.y, vec3.z-1)
|
||||
|
||||
const { camera } = viewer
|
||||
viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
animate()
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
params.playSound = () => {
|
||||
viewer.playSound(targetPos, 'button_click.mp3')
|
||||
}
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.code === 'KeyE') {
|
||||
params.playSound()
|
||||
}
|
||||
}, { capture: true })
|
||||
}
|
||||
main()
|
||||
38
prismarine-viewer/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import {Bot} from "mineflayer";
|
||||
|
||||
export function mineflayer(bot: Bot, settings: {
|
||||
viewDistance?: number;
|
||||
firstPerson?: boolean;
|
||||
port?: number;
|
||||
prefix?: string;
|
||||
});
|
||||
|
||||
export function standalone(options: {
|
||||
version: versions;
|
||||
world: (x: number, y: number, z: number) => 0 | 1;
|
||||
center?: Vec3;
|
||||
viewDistance?: number;
|
||||
port?: number;
|
||||
prefix?: string;
|
||||
});
|
||||
|
||||
export function headless(bot: Bot, settings: {
|
||||
viewDistance?: number;
|
||||
output?: string;
|
||||
frames?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
logFFMPEG?: boolean;
|
||||
jpegOption: any;
|
||||
});
|
||||
|
||||
export const viewer: {
|
||||
Viewer: any;
|
||||
WorldDataEmitter: any;
|
||||
MapControls: any;
|
||||
Entitiy: any;
|
||||
getBufferFromStream: (stream: any) => Promise<Buffer>;
|
||||
};
|
||||
|
||||
export const supportedVersions: versions[];
|
||||
export type versions = '1.8.8' | '1.9.4' | '1.10.2' | '1.11.2' | '1.12.2' | '1.13.2' | '1.14.4' | '1.15.2' | '1.16.1' | '1.16.4' | '1.17.1' | '1.18.1';
|
||||
7
prismarine-viewer/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
mineflayer: require('./lib/mineflayer'),
|
||||
standalone: require('./lib/standalone'),
|
||||
headless: require('./lib/headless'),
|
||||
viewer: require('./viewer'),
|
||||
supportedVersions: require('./viewer/supportedVersions.json')
|
||||
}
|
||||
5
prismarine-viewer/jest-puppeteer.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
launch: {
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
}
|
||||
}
|
||||
4
prismarine-viewer/jest.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
preset: 'jest-puppeteer',
|
||||
testRegex: './*\\.test\\.js$'
|
||||
}
|
||||
12
prismarine-viewer/lib/common.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const path = require('path')
|
||||
const compression = require('compression')
|
||||
const express = require('express')
|
||||
|
||||
function setupRoutes (app, prefix = '') {
|
||||
app.use(compression())
|
||||
app.use(prefix + '/', express.static(path.join(__dirname, '../public')))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupRoutes
|
||||
}
|
||||
135
prismarine-viewer/lib/headless.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/* global THREE */
|
||||
function safeRequire (path) {
|
||||
try {
|
||||
return require(path)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const { spawn } = require('child_process')
|
||||
const net = require('net')
|
||||
global.THREE = require('three')
|
||||
global.Worker = require('worker_threads').Worker
|
||||
const { createCanvas } = safeRequire('node-canvas-webgl/lib')
|
||||
|
||||
const { WorldDataEmitter, Viewer, getBufferFromStream } = require('../viewer')
|
||||
|
||||
module.exports = (bot, { viewDistance = 6, output = 'output.mp4', frames = -1, width = 512, height = 512, logFFMPEG = false, jpegOptions }) => {
|
||||
const canvas = createCanvas(width, height)
|
||||
const renderer = new THREE.WebGLRenderer({ canvas })
|
||||
const viewer = new Viewer(renderer)
|
||||
|
||||
viewer.setVersion(bot.version)
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
|
||||
// Load world
|
||||
const worldView = new WorldDataEmitter(bot.world, viewDistance, bot.entity.position)
|
||||
viewer.listen(worldView)
|
||||
worldView.init(bot.entity.position)
|
||||
|
||||
function botPosition () {
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
worldView.updatePosition(bot.entity.position)
|
||||
}
|
||||
|
||||
// Render loop streaming
|
||||
const rtmpOutput = output.startsWith('rtmp://')
|
||||
const ffmpegOutput = output.endsWith('mp4')
|
||||
let client = null
|
||||
|
||||
if (rtmpOutput) {
|
||||
const fps = 20
|
||||
const gop = fps * 2
|
||||
const gopMin = fps
|
||||
const probesize = '42M'
|
||||
const cbr = '1000k'
|
||||
const threads = 4
|
||||
const args = `-y -r ${fps} -probesize ${probesize} -i pipe:0 -f flv -ac 2 -ar 44100 -vcodec libx264 -g ${gop} -keyint_min ${gopMin} -b:v ${cbr} -minrate ${cbr} -maxrate ${cbr} -pix_fmt yuv420p -s 1280x720 -preset ultrafast -tune film -threads ${threads} -strict normal -bufsize ${cbr} ${output}`.split(' ')
|
||||
client = spawn('ffmpeg', args)
|
||||
if (logFFMPEG) {
|
||||
client.stdout.on('data', (data) => {
|
||||
console.log(`stdout: ${data}`)
|
||||
})
|
||||
|
||||
client.stderr.on('data', (data) => {
|
||||
console.error(`stderr: ${data}`)
|
||||
})
|
||||
}
|
||||
update()
|
||||
} else if (ffmpegOutput) {
|
||||
client = spawn('ffmpeg', ['-y', '-i', 'pipe:0', output])
|
||||
if (logFFMPEG) {
|
||||
client.stdout.on('data', (data) => {
|
||||
console.log(`stdout: ${data}`)
|
||||
})
|
||||
|
||||
client.stderr.on('data', (data) => {
|
||||
console.error(`stderr: ${data}`)
|
||||
})
|
||||
}
|
||||
update()
|
||||
} else {
|
||||
const [host, port] = output.split(':')
|
||||
client = new net.Socket()
|
||||
client.connect(parseInt(port, 10), host, () => {
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
// Force end of stream
|
||||
bot.on('end', () => { frames = 0 })
|
||||
|
||||
let idx = 0
|
||||
function update () {
|
||||
viewer.update()
|
||||
renderer.render(viewer.scene, viewer.camera)
|
||||
|
||||
const imageStream = canvas.createJPEGStream({
|
||||
bufsize: 4096,
|
||||
quality: 1,
|
||||
progressive: false,
|
||||
...jpegOptions
|
||||
})
|
||||
|
||||
if (rtmpOutput || ffmpegOutput) {
|
||||
imageStream.on('data', (chunk) => {
|
||||
if (client.stdin.writable) {
|
||||
client.stdin.write(chunk)
|
||||
} else {
|
||||
console.log('Error: ffmpeg stdin closed!')
|
||||
}
|
||||
})
|
||||
imageStream.on('end', () => {
|
||||
idx++
|
||||
if (idx < frames || frames < 0) {
|
||||
setTimeout(update, 16)
|
||||
} else {
|
||||
console.log('done streaming')
|
||||
client.stdin.end()
|
||||
}
|
||||
})
|
||||
imageStream.on('error', () => { })
|
||||
} else {
|
||||
getBufferFromStream(imageStream).then((buffer) => {
|
||||
const sizebuff = new Uint8Array(4)
|
||||
const view = new DataView(sizebuff.buffer, 0)
|
||||
view.setUint32(0, buffer.length, true)
|
||||
client.write(sizebuff)
|
||||
client.write(buffer)
|
||||
|
||||
idx++
|
||||
if (idx < frames || frames < 0) {
|
||||
setTimeout(update, 16)
|
||||
} else {
|
||||
client.end()
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
// Register events
|
||||
bot.on('move', botPosition)
|
||||
worldView.listenToBot(bot)
|
||||
|
||||
return client
|
||||
}
|
||||
71
prismarine-viewer/lib/index.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/* global THREE */
|
||||
|
||||
global.THREE = require('three')
|
||||
const TWEEN = require('@tweenjs/tween.js')
|
||||
require('three/examples/js/controls/OrbitControls')
|
||||
|
||||
const { Viewer, Entity } = require('../viewer')
|
||||
|
||||
const io = require('socket.io-client')
|
||||
const socket = io()
|
||||
|
||||
let firstPositionUpdate = true
|
||||
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1)
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
const viewer = new Viewer(renderer)
|
||||
|
||||
let controls = new THREE.OrbitControls(viewer.camera, renderer.domElement)
|
||||
|
||||
function animate () {
|
||||
window.requestAnimationFrame(animate)
|
||||
if (controls) controls.update()
|
||||
viewer.update()
|
||||
renderer.render(viewer.scene, viewer.camera)
|
||||
}
|
||||
animate()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
|
||||
socket.on('version', (version) => {
|
||||
viewer.setVersion(version)
|
||||
|
||||
firstPositionUpdate = true
|
||||
viewer.listen(socket)
|
||||
|
||||
let botMesh
|
||||
socket.on('position', ({ pos, addMesh, yaw, pitch }) => {
|
||||
if (yaw !== undefined && pitch !== undefined) {
|
||||
if (controls) {
|
||||
controls.dispose()
|
||||
controls = null
|
||||
}
|
||||
viewer.setFirstPersonCamera(pos, yaw, pitch)
|
||||
return
|
||||
}
|
||||
if (pos.y > 0 && firstPositionUpdate) {
|
||||
controls.target.set(pos.x, pos.y, pos.z)
|
||||
viewer.camera.position.set(pos.x, pos.y + 20, pos.z + 20)
|
||||
controls.update()
|
||||
firstPositionUpdate = false
|
||||
}
|
||||
if (addMesh) {
|
||||
if (!botMesh) {
|
||||
botMesh = new Entity('1.16.4', 'player', viewer.scene).mesh
|
||||
viewer.scene.add(botMesh)
|
||||
}
|
||||
new TWEEN.Tween(botMesh.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
|
||||
const da = (yaw - botMesh.rotation.y) % (Math.PI * 2)
|
||||
const dy = 2 * da % (Math.PI * 2) - da
|
||||
new TWEEN.Tween(botMesh.rotation).to({ y: botMesh.rotation.y + dy }, 50).start()
|
||||
}
|
||||
})
|
||||
})
|
||||
91
prismarine-viewer/lib/mineflayer.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
const EventEmitter = require('events')
|
||||
const { WorldDataEmitter } = require('../viewer')
|
||||
|
||||
module.exports = (bot, { viewDistance = 6, firstPerson = false, port = 3000, prefix = '' }) => {
|
||||
const express = require('express')
|
||||
|
||||
const app = express()
|
||||
const http = require('http').createServer(app)
|
||||
|
||||
const io = require('socket.io')(http, { path: prefix + '/socket.io' })
|
||||
|
||||
const { setupRoutes } = require('./common')
|
||||
setupRoutes(app, prefix)
|
||||
|
||||
const sockets = []
|
||||
const primitives = {}
|
||||
|
||||
bot.viewer = new EventEmitter()
|
||||
|
||||
bot.viewer.erase = (id) => {
|
||||
delete primitives[id]
|
||||
for (const socket of sockets) {
|
||||
socket.emit('primitive', { id })
|
||||
}
|
||||
}
|
||||
|
||||
bot.viewer.drawBoxGrid = (id, start, end, color = 'aqua') => {
|
||||
primitives[id] = { type: 'boxgrid', id, start, end, color }
|
||||
for (const socket of sockets) {
|
||||
socket.emit('primitive', primitives[id])
|
||||
}
|
||||
}
|
||||
|
||||
bot.viewer.drawLine = (id, points, color = 0xff0000) => {
|
||||
primitives[id] = { type: 'line', id, points, color }
|
||||
for (const socket of sockets) {
|
||||
socket.emit('primitive', primitives[id])
|
||||
}
|
||||
}
|
||||
|
||||
bot.viewer.drawPoints = (id, points, color = 0xff0000, size = 5) => {
|
||||
primitives[id] = { type: 'points', id, points, color, size }
|
||||
for (const socket of sockets) {
|
||||
socket.emit('primitive', primitives[id])
|
||||
}
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.emit('version', bot.version)
|
||||
sockets.push(socket)
|
||||
|
||||
const worldView = new WorldDataEmitter(bot.world, viewDistance, bot.entity.position, socket)
|
||||
worldView.init(bot.entity.position)
|
||||
|
||||
worldView.on('blockClicked', (block, face, button) => {
|
||||
bot.viewer.emit('blockClicked', block, face, button)
|
||||
})
|
||||
|
||||
for (const id in primitives) {
|
||||
socket.emit('primitive', primitives[id])
|
||||
}
|
||||
|
||||
function botPosition () {
|
||||
const packet = { pos: bot.entity.position, yaw: bot.entity.yaw, addMesh: true }
|
||||
if (firstPerson) {
|
||||
packet.pitch = bot.entity.pitch
|
||||
}
|
||||
socket.emit('position', packet)
|
||||
worldView.updatePosition(bot.entity.position)
|
||||
}
|
||||
|
||||
bot.on('move', botPosition)
|
||||
worldView.listenToBot(bot)
|
||||
socket.on('disconnect', () => {
|
||||
bot.removeListener('move', botPosition)
|
||||
worldView.removeListenersFromBot(bot)
|
||||
sockets.splice(sockets.indexOf(socket), 1)
|
||||
})
|
||||
})
|
||||
|
||||
http.listen(port, () => {
|
||||
console.log(`Prismarine viewer web server running on *:${port}`)
|
||||
})
|
||||
|
||||
bot.viewer.close = () => {
|
||||
http.close()
|
||||
for (const socket of sockets) {
|
||||
socket.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
52
prismarine-viewer/lib/standalone.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = ({ version, world, center = new Vec3(0, 0, 0), viewDistance = 4, port = 3000, prefix = '' }) => {
|
||||
const express = require('express')
|
||||
|
||||
const app = express()
|
||||
const http = require('http').createServer(app)
|
||||
|
||||
const io = require('socket.io')(http)
|
||||
|
||||
const { setupRoutes } = require('./common')
|
||||
setupRoutes(app, prefix)
|
||||
|
||||
const sockets = []
|
||||
const viewer = { world }
|
||||
|
||||
async function sendChunks (sockets) {
|
||||
const cx = Math.floor(center.x / 16)
|
||||
const cz = Math.floor(center.z / 16)
|
||||
|
||||
for (let x = cx - viewDistance; x <= cx + viewDistance; x++) {
|
||||
for (let z = cz - viewDistance; z <= cz + viewDistance; z++) {
|
||||
const chunk = (await viewer.world.getColumn(x, z)).toJson()
|
||||
for (const socket of sockets) {
|
||||
socket.emit('loadChunk', { x: x * 16, z: z * 16, chunk })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewer.update = () => {
|
||||
sendChunks(sockets)
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.emit('version', version)
|
||||
sockets.push(socket)
|
||||
|
||||
sendChunks([socket])
|
||||
socket.emit('position', { pos: center, addMesh: false })
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
sockets.splice(sockets.indexOf(socket), 1)
|
||||
})
|
||||
})
|
||||
|
||||
http.listen(port, () => {
|
||||
console.log(`Prismarine viewer web server running on *:${port}`)
|
||||
})
|
||||
|
||||
return viewer
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
{
|
||||
"name": "renderer",
|
||||
"name": "prismarine-viewer",
|
||||
"version": "1.25.0",
|
||||
"description": "Web based viewer",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"postinstall": "pnpm generate-textures && node buildMesherWorker.mjs",
|
||||
"generate-textures": "tsx viewer/prepare/postinstall.ts"
|
||||
},
|
||||
"author": "PrismarineJS",
|
||||
"license": "MIT",
|
||||
"standard": {
|
||||
|
|
@ -18,15 +21,17 @@
|
|||
"@tweenjs/tween.js": "^20.0.3",
|
||||
"assert": "^2.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"canvas": "^2.11.2",
|
||||
"filesize": "^10.0.12",
|
||||
"fs-extra": "^11.0.0",
|
||||
"lil-gui": "^0.18.2",
|
||||
"looks-same": "^8.2.3",
|
||||
"minecraft-wrap": "^1.3.0",
|
||||
"minecrafthawkeye": "^1.3.6",
|
||||
"prismarine-block": "^1.7.3",
|
||||
"prismarine-chunk": "^1.22.0",
|
||||
"prismarine-schematic": "^1.2.0",
|
||||
"renderer": "link:./",
|
||||
"prismarine-viewer": "link:./",
|
||||
"process": "^0.11.10",
|
||||
"socket.io": "^4.0.0",
|
||||
"socket.io-client": "^4.0.0",
|
||||
|
|
@ -36,10 +41,6 @@
|
|||
"vec3": "^0.1.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvas": "^2.11.2",
|
||||
"node-canvas-webgl": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"live-server": "^1.2.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Renderer Playground</title>
|
||||
<title>Prismarine Viewer Playground</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'>
|
||||
<style type="text/css">
|
||||
html {
|
||||
|
|
@ -11,17 +11,11 @@
|
|||
|
||||
html, body {
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
@ -34,18 +28,9 @@
|
|||
font-family: mojangles;
|
||||
src: url(../../../assets/mojangles.ttf);
|
||||
}
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
if (window.location.pathname.endsWith('playground')) {
|
||||
// add trailing slash
|
||||
window.location.href = `${window.location.origin}${window.location.pathname}/${window.location.search}`
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/javascript" src="playground.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
prismarine-viewer/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts"
|
||||
]
|
||||
}
|
||||
7
prismarine-viewer/viewer/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
Viewer: require('./lib/viewer').Viewer,
|
||||
WorldDataEmitter: require('./lib/worldDataEmitter').WorldDataEmitter,
|
||||
MapControls: require('./lib/controls').MapControls,
|
||||
Entity: require('./lib/entity/EntityMesh'),
|
||||
getBufferFromStream: require('./lib/simpleUtils').getBufferFromStream
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export function buildCleanupDecorator (cleanupMethod: string) {
|
||||
return function () {
|
||||
return function (_target: { snapshotInitialValues }, propertyKey: string) {
|
||||
return function (_target: {snapshotInitialValues}, propertyKey: string) {
|
||||
const target = _target as any
|
||||
// Store the initial value of the property
|
||||
if (!target._snapshotMethodPatched) {
|
||||
|
|
@ -19,8 +19,7 @@ export function buildCleanupDecorator (cleanupMethod: string) {
|
|||
for (const key of target._toCleanup) {
|
||||
this[key] = this._initialValues[key]
|
||||
}
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
Reflect.apply(originalMethod, this, arguments)
|
||||
originalMethod.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
target._cleanupPatched = true
|
||||