feat: migrate to mc-assets & Rsbuild better resource pack support (#164)

The complete migration from `minecraft-assets` to [`mc-assets`](https://npmjs.com/mc-assets).

Now all block states & block models are processed dynamically! So it is now easily possible to implement custom models

- no post-install work anymore: the building is now 3x faster and 4x faster in docker
- drop 10x total deploy size
- display world ~1.5x faster
- fix snow & repeater state parser (they didn't render correctly)

rsbuild pipeline!

- the initial app load is faster ~1.2
- much fewer requests are made & cached
- dev reloads are fast now

Resource pack changes:

- now textures are reloaded much more quickly on the fly
- add hotkey to quickly reload textures (for debugging) assigned to F3+T (open dev widget is now assigned to F3+Y)
- add a way to disable resource pack instead of uninstalling it
- items render from resource pack are now support
- resource pack widgets & icons are now supported
This commit is contained in:
Vitaly 2024-07-26 13:12:28 +03:00 committed by GitHub
commit 9b72cdb8f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
137 changed files with 3330 additions and 3480 deletions

View file

@ -1 +1,4 @@
node_modules
rsbuild.config.ts
*.module.css.d.ts
generated

4
.vscode/launch.json vendored
View file

@ -29,7 +29,7 @@
"type": "chrome",
"name": "Launch Chrome",
"request": "launch",
"url": "http://localhost:8080/",
"url": "http://localhost:3000/",
"pathMapping": {
"/": "${workspaceFolder}/dist"
},
@ -50,7 +50,7 @@
"name": "Attach Firefox",
"request": "attach",
// comment if using webpack
"url": "http://localhost:8080/",
"url": "http://localhost:3000/",
"webRoot": "${workspaceFolder}/",
"skipFiles": [
// "<node_internals>/**/*vendors*"

View file

@ -1,12 +1,14 @@
# ---- Build Stage ----
FROM node:18-alpine AS build
# Without git installing the npm packages fails
RUN apk add --no-cache git python3 make g++ cairo-dev pango-dev jpeg-dev giflib-dev librsvg-dev
RUN apk add git
WORKDIR /app
COPY . /app
# install pnpm
RUN npm i -g pnpm@9.0.4
RUN pnpm install
# TODO need flat --no-root-optional
RUN node ./scripts/dockerPrepare.mjs
RUN pnpm i
# TODO for development
# EXPOSE 9090
@ -29,4 +31,4 @@ 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"]
ENTRYPOINT ["node", "server.js", "--prod"]

View file

@ -16,7 +16,7 @@ You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.
- Works offline
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
- First-class touch (mobile) & controller support
- Resource pack support
- FULL Resource pack support: Custom GUI, all textures & custom models! Server resource packs are also supported.
- Builtin JEI with recipes & guides for every item (also replaces creative inventory)
- even even more!

View file

@ -1,6 +1,6 @@
# Minecraft React
Minecraft UI components for React.
Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun) project.
```bash
pnpm i minecraft-react

View file

Before

Width:  |  Height:  |  Size: 859 KiB

After

Width:  |  Height:  |  Size: 859 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 952 KiB

After

Width:  |  Height:  |  Size: 952 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 704 KiB

After

Width:  |  Height:  |  Size: 704 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 684 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

BIN
assets/destroy_stage_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

BIN
assets/destroy_stage_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

BIN
assets/destroy_stage_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

BIN
assets/destroy_stage_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

BIN
assets/destroy_stage_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

BIN
assets/destroy_stage_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

BIN
assets/destroy_stage_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

BIN
assets/destroy_stage_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

BIN
assets/destroy_stage_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

BIN
assets/destroy_stage_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View file

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 433 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

View file

@ -1,140 +0,0 @@
//@ts-check
import * as esbuild from 'esbuild'
import fs from 'fs'
// import htmlPlugin from '@chialab/esbuild-plugin-html'
import server from './server.js'
import { clients, plugins, 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',
'yggdrasil': './src/yggdrasilReplacement.ts',
// 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',
'.woff': 'dataurl',
'.woff2': 'dataurl',
'.ttf': 'dataurl',
'.webp': 'dataurl',
},
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',
})
}
}

View file

@ -1,60 +0,0 @@
<!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>

View file

@ -6,7 +6,7 @@
</script>
<!-- // #region initial loader -->
<script async>
const loadingDiv = `
const loadingDiv = /* html */ `
<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);">
@ -18,22 +18,31 @@
</div>
</div>
`
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
const insertLoadingDiv = () => {
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
}
}
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
window.addEventListener('error', (e) => onError(e.message))
}
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
window.addEventListener('error', (e) => onError(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
newContainer.appendChild(loadingDivElem);
})
</script>
<script type="module" async>
const checkLoadEruda = () => {
@ -76,13 +85,12 @@
}
</script> -->
<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="2023-09-11" scheme="YYYY-MM-DD">
<meta name="date" content="2024-07-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'>

View file

@ -3,22 +3,26 @@
"version": "0.0.0-dev",
"description": "A minecraft client running in a browser",
"scripts": {
"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",
"dev-rsbuild": "rsbuild dev",
"dev-proxy": "node server.js",
"start": "run-p dev-rsbuild dev-proxy",
"start-watch-script": "nodemon -w rsbuild.config.ts --watch",
"build": "rsbuild build",
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build",
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
"test:cypress": "cypress run",
"test-unit": "vitest",
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
"prod-start": "node server.js",
"postinstall": "node scripts/gen-texturepack-files.mjs && tsx scripts/optimizeBlockCollisions.ts",
"prod-start": "node server.js --prod",
"postinstall": "tsx scripts/optimizeBlockCollisions.ts && pnpm build-mesher",
"test-mc-server": "tsx cypress/minecraft-server.mjs",
"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",
"watch-mesher": "node prismarine-viewer/buildMesherWorker.mjs -w",
"build-mesher": "node prismarine-viewer/buildMesherWorker.mjs",
"watch-mesher": "pnpm build-mesher -w",
"run-playground": "run-p watch-mesher watch-other-workers playground-server watch-playground",
"run-all": "run-p start run-playground",
"playground-server": "live-server --port=9090 prismarine-viewer/public",
@ -56,19 +60,18 @@
"classnames": "^2.5.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"cypress-plugin-snapshots": "^1.4.4",
"debug": "^4.3.4",
"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.33",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.34",
"fs-extra": "^11.1.1",
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"minecraft-assets": "^1.12.2",
"minecraft-data": "3.65.0",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
@ -104,11 +107,17 @@
"workbox-build": "^7.0.0"
},
"devDependencies": {
"@rsbuild/core": "1.0.1-beta.4",
"@rsbuild/plugin-node-polyfill": "^1.0.3",
"@rsbuild/plugin-type-check": "1.0.1-beta.4",
"@rsbuild/plugin-typed-css-modules": "^1.0.1",
"@rsbuild/plugin-react": "^1.0.1-beta.4",
"@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",
@ -122,7 +131,6 @@
"constants-browserify": "^1.0.0",
"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",
@ -131,6 +139,7 @@
"http-browserify": "^1.7.0",
"http-server": "^14.1.1",
"https-browserify": "^1.0.0",
"mc-assets": "^0.2.5",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer",
"mineflayer-pathfinder": "^2.4.4",
@ -150,10 +159,13 @@
"yaml": "^2.3.2"
},
"optionalDependencies": {
"cypress": "^10.11.0",
"cypress-plugin-snapshots": "^1.4.4",
"systeminformation": "^5.21.22"
},
"pnpm": {
"overrides": {
"buffer": "^6.0.3",
"@nxg-org/mineflayer-physics-util": "1.5.8",
"three": "0.154.0",
"diamond-square": "github:zardoy/diamond-square",
@ -171,7 +183,8 @@
"patchedDependencies": {
"minecraft-protocol@1.47.0": "patches/minecraft-protocol@1.47.0.patch",
"three@0.154.0": "patches/three@0.154.0.patch",
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch"
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch"
}
},
"packageManager": "pnpm@9.0.4"

View file

@ -3,7 +3,13 @@
"description": "A Minecraft-like React UI library",
"keywords": [
"minecraft",
"minecraft style"
"minecraft style",
"minecraft ui",
"minecraft components",
"minecraft react",
"minecraft library",
"minecraft web",
"minecraft browser"
],
"license": "MIT",
"sideEffects": false,

View file

@ -0,0 +1,16 @@
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:./"
}

1488
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -108,6 +108,7 @@ const buildOptions = {
})
build.onEnd(({ metafile, outputFiles }) => {
if (!metafile) return
fs.mkdirSync(path.join(__dirname, './public'), { recursive: true })
fs.writeFileSync(path.join(__dirname, './public/metafile.json'), JSON.stringify(metafile))
for (const outDir of ['../dist/', './public/']) {
for (const outputFile of outputFiles) {
@ -115,8 +116,9 @@ const buildOptions = {
// skip writing & browser loading sourcemap there, worker debugging should be done in playground
// continue
}
fs.mkdirSync(outDir, { recursive: true })
fs.writeFileSync(path.join(__dirname, outDir, path.basename(outputFile.path)), outputFile.text)
const writePath = path.join(__dirname, outDir, path.basename(outputFile.path))
fs.mkdirSync(path.dirname(writePath), { recursive: true })
fs.writeFileSync(writePath, outputFile.text)
}
}
})

View file

@ -1,7 +1,7 @@
//@ts-check
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'
@ -17,7 +17,6 @@ if (!fs.existsSync(mcDataPath)) {
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', ''))
@ -47,6 +46,7 @@ const buildOptions = {
http: 'http-browserify',
stream: 'stream-browserify',
net: 'net-browserify',
// 'mc-assets': '/Users/vitaly/Documents/mc-assets',
},
inject: [],
metafile: true,
@ -57,7 +57,7 @@ const buildOptions = {
plugins: [
{
name: 'minecraft-data',
setup (build) {
setup(build) {
build.onLoad({
filter: /minecraft-data[\/\\]data.js$/,
}, () => {
@ -69,7 +69,7 @@ const buildOptions = {
})
build.onEnd((e) => {
if (e.errors.length) return
fs.writeFileSync(join(__dirname, 'public/metafile.json'), JSON.stringify(e.metafile), 'utf8')
// fs.writeFileSync(join(__dirname, 'dist/metafile.json'), JSON.stringify(e.metafile), 'utf8')
})
}
},

View file

@ -0,0 +1 @@
export { default as rotation } from './rotation'

View file

@ -0,0 +1,9 @@
import { Vec3 } from 'vec3'
import { ExampleSetupFunction } from './type'
const setup: ExampleSetupFunction = (world, mcData, mesherConfig, setupParam) => {
mesherConfig.debugModelVariant = [3]
world.setBlockStateId(new Vec3(0, 0, 0), mcData.blocksByName.sand.defaultState)
}
export default setup

View file

@ -0,0 +1,6 @@
import { CustomWorld } from 'flying-squid/dist/lib/modules/world'
import { MesherConfig } from '../../viewer/lib/mesher/shared'
import { IndexedData } from 'minecraft-data'
type SetupParams = {}
export type ExampleSetupFunction = (world: CustomWorld, mcData: IndexedData, mesherConfig: MesherConfig, setupParam: SetupParams) => void

View file

@ -6,21 +6,22 @@ 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'
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
globalThis.THREE = THREE
//@ts-ignore
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { toMajorVersion } from '../../src/utils'
const gui = new GUI()
// initial values
const params = {
skip: '',
skipQs: '',
version: globalThis.includedVersions.sort((a, b) => {
const s = (x) => {
const parts = x.split('.')
@ -38,7 +39,8 @@ const params = {
entityRotate: false,
camera: '',
playSound () { },
blockIsomorphicRenderBundle () { }
blockIsomorphicRenderBundle () { },
modelVariant: 0
}
const qs = new URLSearchParams(window.location.search)
@ -49,7 +51,7 @@ qs.forEach((value, key) => {
const setQs = () => {
const newQs = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (!value || typeof value === 'function' || params.skip.includes(key)) continue
if (!value || typeof value === 'function' || params.skipQs.includes(key)) continue
//@ts-ignore
newQs.set(key, value)
}
@ -65,7 +67,7 @@ async function main () {
// temporary solution until web worker is here, cache data for faster reloads
const globalMcData = window['mcData']
if (!globalMcData['version']) {
const major = toMajor(version)
const major = toMajorVersion(version)
const sessionKey = `mcData-${major}`
if (sessionStorage[sessionKey]) {
Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey]))
@ -84,11 +86,12 @@ async function main () {
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, 'modelVariant')
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, 'skipQs')
gui.add(params, 'playSound')
gui.add(params, 'blockIsomorphicRenderBundle')
gui.open(false)
@ -133,7 +136,8 @@ async function main () {
document.body.appendChild(renderer.domElement)
// Create viewer
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false })
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false, })
viewer.world.blockstatesModels = blockstatesModels
viewer.entities.setDebugMode('basic')
viewer.setVersion(version)
viewer.entities.onSkinUpdate = () => {
@ -299,36 +303,45 @@ async function main () {
}
const onUpdate = {
version (initialUpdate) {
if (initialUpdate) return
// viewer.world.texturesVersion = params.version
// viewer.world.updateTexturesData()
// todo warning
},
block () {
blockProps = {}
metadataFolder.destroy()
const block = mcData.blocksByName[params.block]
if (!block) return
console.log('block', block.name)
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
let defaultValue: string | number | boolean
if (state.values) { // int, enum
defaultValue = state.values[0]
} else {
switch (state.type) {
case 'bool':
defaultValue = false
break
case 'int':
defaultValue = 0
break
case 'direction':
defaultValue = 'north'
break
default:
continue
default:
continue
}
}
blockProps[state.name] = defaultValue
if (state.type === 'enum') {
if (state.values) {
metadataFolder.add(blockProps, state.name, state.values)
} else {
metadataFolder.add(blockProps, state.name)
@ -340,6 +353,7 @@ async function main () {
metadataFolder.add(blockProps, name)
}
}
console.log('props', blockProps)
metadataFolder.open()
},
entity () {
@ -364,6 +378,9 @@ async function main () {
},
supportBlock () {
viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
},
modelVariant () {
viewer.world.mesherConfig.debugModelVariant = params.modelVariant === 0 ? undefined : [params.modelVariant]
}
}
@ -407,10 +424,14 @@ async function main () {
}
})
viewer.waitForChunksToRender().then(async () => {
// TODO!
await new Promise(resolve => {
setTimeout(resolve, 50)
})
for (const update of Object.values(onUpdate)) {
update()
update(true)
}
applyChanges(true)
applyChanges()
gui.openAnimated()
})

View file

@ -1,38 +0,0 @@
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';

View file

@ -3,5 +3,4 @@ module.exports = {
standalone: require('./lib/standalone'),
headless: require('./lib/headless'),
viewer: require('./viewer'),
supportedVersions: require('./viewer/supportedVersions.json')
}

View file

@ -1,5 +0,0 @@
module.exports = {
launch: {
args: ['--no-sandbox', '--disable-setuid-sandbox']
}
}

View file

@ -1,4 +0,0 @@
module.exports = {
preset: 'jest-puppeteer',
testRegex: './*\\.test\\.js$'
}

View file

@ -3,10 +3,7 @@
"version": "1.25.0",
"description": "Web based viewer",
"main": "index.js",
"scripts": {
"postinstall": "pnpm generate-textures && node buildMesherWorker.mjs",
"generate-textures": "tsx viewer/prepare/postinstall.ts"
},
"scripts": {},
"author": "PrismarineJS",
"license": "MIT",
"standard": {
@ -21,11 +18,9 @@
"@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",
@ -41,6 +36,7 @@
"vec3": "^0.1.7"
},
"optionalDependencies": {
"canvas": "^2.11.2",
"node-canvas-webgl": "^0.3.0"
}
}

View file

@ -1,10 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"strictNullChecks": true,
"experimentalDecorators": true
},
"files": [
"index.d.ts"
]
}

View file

@ -7,7 +7,7 @@ import EventEmitter from 'events'
import { PlayerObject, PlayerAnimation } from 'skinview3d'
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
// todo replace with url
import stevePng from 'minecraft-assets/minecraft-assets/data/1.20.2/entity/player/wide/steve.png'
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
import { WalkingGeneralSwing } from './entity/animations'
import { NameTagObject } from 'skinview3d/libs/nametag'
import { flat, fromFormattedString } from '@xmcl/text-component'
@ -17,7 +17,7 @@ import { disposeObject } from './threeJsUtils'
export const TWEEN_DURATION = 50 // todo should be 100
function getUsernameTexture(username, { fontFamily = 'sans-serif' }) {
function getUsernameTexture (username, { fontFamily = 'sans-serif' }) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Could not get 2d context')
@ -61,7 +61,7 @@ const addNametag = (entity, options, mesh) => {
// todo cleanup
const nametags = {}
function getEntityMesh(entity, scene, options, overrides) {
function getEntityMesh (entity, scene, options, overrides) {
if (entity.name) {
try {
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
@ -102,11 +102,12 @@ export class Entities extends EventEmitter {
this.onSkinUpdate = () => { }
this.clock = new THREE.Clock()
this.rendering = true
/** @type {THREE.Texture | null} */
this.itemsTexture = null
this.getItemUv = undefined
}
clear() {
clear () {
for (const mesh of Object.values(this.entities)) {
this.scene.remove(mesh)
disposeObject(mesh)
@ -114,7 +115,7 @@ export class Entities extends EventEmitter {
this.entities = {}
}
setDebugMode(mode, /** @type {THREE.Object3D?} */entity = null) {
setDebugMode (mode, /** @type {THREE.Object3D?} */entity = null) {
this.debugMode = mode
for (const mesh of entity ? [entity] : Object.values(this.entities)) {
const boxHelper = mesh.children.find(c => c.name === 'debug')
@ -126,7 +127,7 @@ export class Entities extends EventEmitter {
}
}
setRendering(rendering, /** @type {THREE.Object3D?} */entity = null) {
setRendering (rendering, /** @type {THREE.Object3D?} */entity = null) {
this.rendering = rendering
for (const ent of entity ? [entity] : Object.values(this.entities)) {
if (rendering) {
@ -137,7 +138,7 @@ export class Entities extends EventEmitter {
}
}
render() {
render () {
const dt = this.clock.getDelta()
for (const entityId of Object.keys(this.entities)) {
const playerObject = this.getPlayerObject(entityId)
@ -147,7 +148,7 @@ export class Entities extends EventEmitter {
}
}
getPlayerObject(entityId) {
getPlayerObject (entityId) {
/** @type {(PlayerObject & { animation?: PlayerAnimation }) | undefined} */
const playerObject = this.entities[entityId]?.playerObject
return playerObject
@ -157,7 +158,7 @@ export class Entities extends EventEmitter {
defaultSteveTexture
// true means use default skin url
updatePlayerSkin(entityId, username, /** @type {string | true} */skinUrl, /** @type {string | true | undefined} */capeUrl = undefined) {
updatePlayerSkin (entityId, username, /** @type {string | true} */skinUrl, /** @type {string | true | undefined} */capeUrl = undefined) {
let playerObject = this.getPlayerObject(entityId)
if (!playerObject) return
// const username = this.entities[entityId].username
@ -240,14 +241,14 @@ export class Entities extends EventEmitter {
playerObject.cape.map = null
}
function isCanvasBlank(canvas) {
function isCanvasBlank (canvas) {
return !canvas.getContext('2d')
.getImageData(0, 0, canvas.width, canvas.height).data
.some(channel => channel !== 0)
}
}
playAnimation(entityPlayerId, /** @type {'walking' | 'running' | 'oneSwing' | 'idle'} */animation) {
playAnimation (entityPlayerId, /** @type {'walking' | 'running' | 'oneSwing' | 'idle'} */animation) {
const playerObject = this.getPlayerObject(entityPlayerId)
if (!playerObject) return
@ -267,7 +268,7 @@ export class Entities extends EventEmitter {
}
parseEntityLabel(jsonLike) {
parseEntityLabel (jsonLike) {
if (!jsonLike) return
try {
const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike)
@ -278,7 +279,7 @@ export class Entities extends EventEmitter {
}
}
update(/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
update (/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
let isPlayerModel = entity.name === 'player'
if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
isPlayerModel = true
@ -407,7 +408,7 @@ export class Entities extends EventEmitter {
//@ts-ignore
// set visibility
const isInvisible = entity.metadata?.[0] & 0x20
for (const child of this.entities[entity.id].children.find(c => c.name === 'mesh').children) {
for (const child of this.entities[entity.id]?.children.find(c => c.name === 'mesh')?.children ?? []) {
if (child.name !== 'nametag') {
child.visible = !isInvisible
}

View file

@ -356,7 +356,7 @@ export class EntityMesh {
const texture = overrides.textures?.[name] ?? e.textures[name]
if (!texture) continue
// console.log(JSON.stringify(jsonModel, null, 2))
const mesh = getMesh(texture.replace('textures', 'textures/' + version) + '.png', jsonModel, overrides)
const mesh = getMesh(texture + '.png', jsonModel, overrides,)
mesh.name = `geometry_${name}`
this.mesh.add(mesh)

View file

@ -1,6 +1,6 @@
import { World } from './world'
import { Vec3 } from 'vec3'
import { getSectionGeometry, setBlockStatesData } from './models'
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
if (module.require) {
// If we are in a node environement, we need to fake some env variables
@ -13,7 +13,7 @@ if (module.require) {
let world: World
let dirtySections: Map<string, number> = new Map()
let blockStatesReady = false
let allDataReady = false
function sectionKey (x, y, z) {
return `${x},${y},${z}`
@ -74,14 +74,18 @@ const handleMessage = data => {
}
if (data.config) {
if (data.type === 'mesherData' && allDataReady) {
world = undefined as any // reset models
}
world ??= new World(data.config.version)
world.config = { ...world.config, ...data.config }
globalThis.world = world
}
if (data.type === 'mesherData') {
setBlockStatesData(data.json)
blockStatesReady = true
setMesherData(data.blockstatesModels, data.blocksAtlas)
allDataReady = true
} else if (data.type === 'dirty') {
const loc = new Vec3(data.x, data.y, data.z)
setSectionDirty(loc, data.value)
@ -99,7 +103,7 @@ const handleMessage = data => {
dirtySections = new Map()
// todo also remove cached
globalVar.mcData = null
blockStatesReady = false
allDataReady = false
}
}
@ -113,7 +117,7 @@ self.onmessage = ({ data }) => {
}
setInterval(() => {
if (world === null || !blockStatesReady) return
if (world === null || !allDataReady) return
if (dirtySections.size === 0) return
// console.log(sections.length + ' dirty sections')

View file

@ -1,12 +1,13 @@
import { Vec3 } from 'vec3'
import type { BlockStatesOutput } from '../../prepare/modelsBuilder'
import { World } from './world'
import { World, BlockModelPartsResolved } from './world'
import { WorldBlock as Block } from './world'
import legacyJson from '../../../../src/preflatMap.json'
import { versionToNumber } from '../../prepare/utils'
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
let blockProvider: WorldBlockProvider
const tints: any = {}
let blockStates: BlockStatesOutput
let needTiles = false
let tintsData
@ -35,6 +36,10 @@ function prepareTints (tints) {
})
}
function mod (x: number, n: number) {
return ((x % n) + n) % n
}
const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
export function preflatBlockCalculation (block: Block, world: World, position: Vec3) {
const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
@ -89,75 +94,6 @@ function tintToGl (tint) {
return [r / 255, g / 255, b / 255]
}
const elemFaces = {
up: {
dir: [0, 1, 0],
mask1: [1, 1, 0],
mask2: [0, 1, 1],
corners: [
[0, 1, 1, 0, 1],
[1, 1, 1, 1, 1],
[0, 1, 0, 0, 0],
[1, 1, 0, 1, 0]
]
},
down: {
dir: [0, -1, 0],
mask1: [1, 1, 0],
mask2: [0, 1, 1],
corners: [
[1, 0, 1, 0, 1],
[0, 0, 1, 1, 1],
[1, 0, 0, 0, 0],
[0, 0, 0, 1, 0]
]
},
east: {
dir: [1, 0, 0],
mask1: [1, 1, 0],
mask2: [1, 0, 1],
corners: [
[1, 1, 1, 0, 0],
[1, 0, 1, 0, 1],
[1, 1, 0, 1, 0],
[1, 0, 0, 1, 1]
]
},
west: {
dir: [-1, 0, 0],
mask1: [1, 1, 0],
mask2: [1, 0, 1],
corners: [
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 1, 1]
]
},
north: {
dir: [0, 0, -1],
mask1: [1, 0, 1],
mask2: [0, 1, 1],
corners: [
[1, 0, 0, 0, 1],
[0, 0, 0, 1, 1],
[1, 1, 0, 0, 0],
[0, 1, 0, 1, 0]
]
},
south: {
dir: [0, 0, 1],
mask1: [1, 0, 1],
mask2: [0, 1, 1],
corners: [
[0, 0, 1, 0, 1],
[1, 0, 1, 1, 1],
[0, 1, 1, 0, 0],
[1, 1, 1, 1, 0]
]
}
}
function getLiquidRenderHeight (world, block, type, pos) {
if (!block || block.type !== type) return 1 / 9
if (block.metadata === 0) { // source block
@ -177,8 +113,9 @@ const everyArray = (array, callback) => {
const isCube = (block) => {
if (!block || block.transparent) return false
if (block.isCube) return true
if (!block.variant) block.variant = getModelVariants(block)
if (!block.variant.length) return false
// TODO!
// if (!block.variant) block.variant = getModelVariants(block)
if (!block.variant?.length) return false
return block.variant.every(v => everyArray(v?.model?.elements, e => {
return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
}))
@ -249,78 +186,9 @@ function renderLiquid (world, cursor, texture, type, biome, water, attr) {
}
}
function vecadd3 (a, b) {
if (!b) return a
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
function vecsub3 (a, b) {
if (!b) return a
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
function matmul3 (matrix, vector): [number, number, number] {
if (!matrix) return vector
return [
matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2]
]
}
function matmulmat3 (a, b) {
const te = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
const a11 = a[0][0]; const a12 = a[1][0]; const a13 = a[2][0]
const a21 = a[0][1]; const a22 = a[1][1]; const a23 = a[2][1]
const a31 = a[0][2]; const a32 = a[1][2]; const a33 = a[2][2]
const b11 = b[0][0]; const b12 = b[1][0]; const b13 = b[2][0]
const b21 = b[0][1]; const b22 = b[1][1]; const b23 = b[2][1]
const b31 = b[0][2]; const b32 = b[1][2]; const b33 = b[2][2]
te[0][0] = a11 * b11 + a12 * b21 + a13 * b31
te[1][0] = a11 * b12 + a12 * b22 + a13 * b32
te[2][0] = a11 * b13 + a12 * b23 + a13 * b33
te[0][1] = a21 * b11 + a22 * b21 + a23 * b31
te[1][1] = a21 * b12 + a22 * b22 + a23 * b32
te[2][1] = a21 * b13 + a22 * b23 + a23 * b33
te[0][2] = a31 * b11 + a32 * b21 + a33 * b31
te[1][2] = a31 * b12 + a32 * b22 + a33 * b32
te[2][2] = a31 * b13 + a32 * b23 + a33 * b33
return te
}
function buildRotationMatrix (axis, degree) {
const radians = degree / 180 * Math.PI
const cos = Math.cos(radians)
const sin = Math.sin(radians)
const axis0 = { x: 0, y: 1, z: 2 }[axis]
const axis1 = (axis0 + 1) % 3
const axis2 = (axis0 + 2) % 3
const matrix = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
matrix[axis0][axis0] = 1
matrix[axis1][axis1] = cos
matrix[axis1][axis2] = -sin
matrix[axis2][axis1] = +sin
matrix[axis2][axis2] = cos
return matrix
}
let needRecompute = false
function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr, globalMatrix, globalShift, block: Block, biome) {
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record<string, any>, globalMatrix: any, globalShift: any, block: Block, biome: string) {
const position = cursor
// const key = `${position.x},${position.y},${position.z}`
// if (!globalThis.allowedBlocks.includes(key)) return
@ -349,10 +217,11 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr
const maxy = element.to[1]
const maxz = element.to[2]
const u = eFace.texture.u
const v = eFace.texture.v
const su = eFace.texture.su
const sv = eFace.texture.sv
const texture = eFace.texture as any
const u = texture.u
const v = texture.v
const su = texture.su
const sv = texture.sv
const ndx = Math.floor(attr.positions.length / 3)
@ -382,6 +251,7 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr
let localShift = null as any
if (element.rotation) {
// todo do we support rescale?
localMatrix = buildRotationMatrix(
element.rotation.axis,
element.rotation.angle
@ -485,6 +355,11 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr
}
}
const invisibleBlocks = ['air', 'cave_air', 'void_air', 'barrier']
const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true'
let unknownBlockModel: BlockModelPartsResolved
export function getSectionGeometry (sx, sy, sz, world: World) {
let delayedRender = [] as (() => void)[]
@ -511,6 +386,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
const block = world.getBlock(cursor)!
if (invisibleBlocks.includes(block.name)) continue
if (block.name.includes('_sign') || block.name === 'sign') {
const key = `${cursor.x},${cursor.y},${cursor.z}`
const props: any = block.getProperties()
@ -546,51 +422,71 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
block._originalProperties = undefined
}
}
if (block.variant === undefined || preflatRecomputeVariant) {
block.variant = getModelVariants(block)
const isWaterlogged = isBlockWaterlogged(block)
if (block.name === 'water' || isWaterlogged) {
const pos = cursor.clone()
delayedRender.push(() => {
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr)
})
} else if (block.name === 'lava') {
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr)
}
for (const variant of block.variant) {
if (!variant || !variant.model) continue
const isWaterlogged = block.getProperties().waterlogged
if (block.name === 'water' || isWaterlogged) {
const waterBlock = block.name === 'water' ? block : { name: 'water', metadata: 0 }
const variant = getModelVariants(waterBlock as any)[0]
const pos = cursor.clone()
delayedRender.push(() => {
renderLiquid(world, pos, variant.model.textures.particle, block.type, biome, true, attr)
})
} else if (block.name === 'lava') {
renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, false, attr)
if (block.name !== "water" && block.name !== "lava" && !invisibleBlocks.includes(block.name)) {
// cache
let models = block.models
if (block.models === undefined || preflatRecomputeVariant) {
try {
models = blockProvider.getAllResolvedModels0_1({
name: block.name,
properties: block.getProperties(),
})!
if (!models.length) models = null
} catch (err) {
console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(block.getProperties())}]: ` + err.message, err.stack)
}
}
if (block.name !== "water") {
block.models = models ?? null
models ??= unknownBlockModel
const firstForceVar = world.config.debugModelVariant?.[0]
let part = 0
for (const modelVars of models ?? []) {
const pos = cursor.clone()
// const variantRuntime = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), modelVars.length)
const variantRuntime = 0
const useVariant = world.config.debugModelVariant?.[part] ?? firstForceVar ?? variantRuntime
part++
const model = modelVars[useVariant] ?? modelVars[0]
if (!model) continue
let globalMatrix = null as any
let globalShift = null as any
for (const axis of ['x', 'y', 'z']) {
if (axis in variant) {
if (!globalMatrix) globalMatrix = buildRotationMatrix(axis, -variant[axis])
else globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -variant[axis]))
for (const axis of ['x', 'y', 'z'] as const) {
if (axis in model) {
if (!globalMatrix) globalMatrix = buildRotationMatrix(axis, -(model[axis] ?? 0))
else globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0)))
}
}
if (globalMatrix) {
globalShift = [8, 8, 8]
globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
}
for (const element of variant.model.elements) {
for (const element of model.elements ?? []) {
const ao = model.ao ?? true
if (block.transparent) {
const pos = cursor.clone()
delayedRender.push(() => {
renderElement(world, pos, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome)
renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome)
})
} else {
renderElement(world, cursor, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome)
renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome)
}
}
}
}
}
}
@ -631,65 +527,12 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
return attr
}
function parseProperties (properties) {
if (typeof properties === 'object') { return properties }
const json = {}
for (const prop of properties.split(',')) {
const [key, value] = prop.split('=')
json[key] = value
}
return json
}
function matchProperties (block: Block, /* to match against */properties: Record<string, string | boolean> & { OR }) {
if (!properties) { return true }
properties = parseProperties(properties)
const blockProps = block.getProperties()
if (properties.OR) {
return properties.OR.some((or) => matchProperties(block, or))
}
for (const prop in blockProps) {
if (properties[prop] === undefined) continue // unknown property, ignore
if (typeof properties[prop] !== 'string') properties[prop] = String(properties[prop])
if (!(properties[prop] as string).split('|').some((value) => value === String(blockProps[prop]))) {
return false
}
}
return true
}
function getModelVariants (block: Block) {
// air, cave_air, void_air and so on...
// full list of invisible & special blocks https://minecraft.wiki/w/Model#Blocks_and_fluids
if (block.name === '' || block.name === 'air' || block.name.endsWith('_air')) return []
if (block.name === 'barrier') return []
const matchedState = blockStates[block.name]
// if (!matchedState) currentWarnings.value.add(`Missing block ${block.name}`)
const state = matchedState ?? blockStates.missing_texture
if (!state) return []
if (state.variants) {
for (const [properties, variant] of Object.entries(state.variants)) {
if (!matchProperties(block, properties as any)) continue
if (variant instanceof Array) return [variant[0]]
return [variant]
}
}
if (state.multipart) {
const parts = state.multipart.filter(multipart => matchProperties(block, multipart.when))
let variants = [] as any[]
for (const part of parts) {
variants = [...variants, ...Array.isArray(part.apply) ? part.apply : [part.apply]]
}
return variants
export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true) => {
blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, 'latest')
globalThis.blockProvider = blockProvider
if (useUnknownBlockModel) {
unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} })
}
return []
}
export const setBlockStatesData = (_blockStates: BlockStatesOutput | null, _needTiles = false) => {
blockStates = _blockStates!
needTiles = _needTiles
}

View file

@ -0,0 +1,142 @@
import { BlockModelPartsResolved } from './world'
export type BlockElement = NonNullable<BlockModelPartsResolved[0][0]['elements']>[0]
export function buildRotationMatrix (axis, degree) {
const radians = degree / 180 * Math.PI
const cos = Math.cos(radians)
const sin = Math.sin(radians)
const axis0 = { x: 0, y: 1, z: 2 }[axis]
const axis1 = (axis0 + 1) % 3
const axis2 = (axis0 + 2) % 3
const matrix = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
matrix[axis0][axis0] = 1
matrix[axis1][axis1] = cos
matrix[axis1][axis2] = -sin
matrix[axis2][axis1] = +sin
matrix[axis2][axis2] = cos
return matrix
}
export function vecadd3 (a, b) {
if (!b) return a
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
export function vecsub3 (a, b) {
if (!b) return a
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
export function matmul3 (matrix, vector): [number, number, number] {
if (!matrix) return vector
return [
matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2]
]
}
export function matmulmat3 (a, b) {
const te = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
const a11 = a[0][0]; const a12 = a[1][0]; const a13 = a[2][0]
const a21 = a[0][1]; const a22 = a[1][1]; const a23 = a[2][1]
const a31 = a[0][2]; const a32 = a[1][2]; const a33 = a[2][2]
const b11 = b[0][0]; const b12 = b[1][0]; const b13 = b[2][0]
const b21 = b[0][1]; const b22 = b[1][1]; const b23 = b[2][1]
const b31 = b[0][2]; const b32 = b[1][2]; const b33 = b[2][2]
te[0][0] = a11 * b11 + a12 * b21 + a13 * b31
te[1][0] = a11 * b12 + a12 * b22 + a13 * b32
te[2][0] = a11 * b13 + a12 * b23 + a13 * b33
te[0][1] = a21 * b11 + a22 * b21 + a23 * b31
te[1][1] = a21 * b12 + a22 * b22 + a23 * b32
te[2][1] = a21 * b13 + a22 * b23 + a23 * b33
te[0][2] = a31 * b11 + a32 * b21 + a33 * b31
te[1][2] = a31 * b12 + a32 * b22 + a33 * b32
te[2][2] = a31 * b13 + a32 * b23 + a33 * b33
return te
}
export const elemFaces = {
up: {
dir: [0, 1, 0],
mask1: [1, 1, 0],
mask2: [0, 1, 1],
corners: [
[0, 1, 1, 0, 1],
[1, 1, 1, 1, 1],
[0, 1, 0, 0, 0],
[1, 1, 0, 1, 0]
]
},
down: {
dir: [0, -1, 0],
mask1: [1, 1, 0],
mask2: [0, 1, 1],
corners: [
[1, 0, 1, 0, 1],
[0, 0, 1, 1, 1],
[1, 0, 0, 0, 0],
[0, 0, 0, 1, 0]
]
},
east: {
dir: [1, 0, 0],
mask1: [1, 1, 0],
mask2: [1, 0, 1],
corners: [
[1, 1, 1, 0, 0],
[1, 0, 1, 0, 1],
[1, 1, 0, 1, 0],
[1, 0, 0, 1, 1]
]
},
west: {
dir: [-1, 0, 0],
mask1: [1, 1, 0],
mask2: [1, 0, 1],
corners: [
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 1, 1]
]
},
north: {
dir: [0, 0, -1],
mask1: [1, 0, 1],
mask2: [0, 1, 1],
corners: [
[1, 0, 0, 0, 1],
[0, 0, 0, 1, 1],
[1, 1, 0, 0, 0],
[0, 1, 0, 1, 0]
]
},
south: {
dir: [0, 0, 1],
mask1: [1, 0, 1],
mask2: [0, 1, 1],
corners: [
[0, 0, 1, 0, 1],
[1, 0, 1, 1, 1],
[0, 1, 1, 0, 0],
[1, 1, 1, 1, 0]
]
}
}

View file

@ -5,6 +5,7 @@ export const defaultMesherConfig = {
smoothLighting: true,
outputFormat: 'threeJs' as 'threeJs' | 'webgl',
textureSize: 1024, // for testing
debugModelVariant: undefined as undefined | number[]
}
export type MesherConfig = typeof defaultMesherConfig

View file

@ -0,0 +1,273 @@
import { Vec3 } from 'vec3'
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
import { Block } from 'prismarine-block'
import { BlockModelPartsResolved } from './world'
import { IndexedData } from 'minecraft-data'
import * as THREE from 'three'
type NeighborSide = 'up' | 'down' | 'east' | 'west' | 'north' | 'south'
function tintToGl (tint) {
const r = (tint >> 16) & 0xff
const g = (tint >> 8) & 0xff
const b = tint & 0xff
return [r / 255, g / 255, b / 255]
}
type Neighbors = Partial<Record<NeighborSide, boolean>>
function renderElement (element: BlockElement, doAO: boolean, attr, globalMatrix, globalShift, block: Block | undefined, biome: string, neighbors: Neighbors) {
const cursor = new Vec3(0, 0, 0)
// const key = `${position.x},${position.y},${position.z}`
// if (!globalThis.allowedBlocks.includes(key)) return
// const cullIfIdentical = block.name.indexOf('glass') >= 0
for (const face in element.faces) {
const eFace = element.faces[face]
const { corners, mask1, mask2 } = elemFaces[face]
const dir = matmul3(globalMatrix, elemFaces[face].dir)
if (eFace.cullface) {
if (neighbors[face]) continue
}
const minx = element.from[0]
const miny = element.from[1]
const minz = element.from[2]
const maxx = element.to[0]
const maxy = element.to[1]
const maxz = element.to[2]
const texture = eFace.texture as any
const u = texture.u
const v = texture.v
const su = texture.su
const sv = texture.sv
const ndx = Math.floor(attr.positions.length / 3)
let tint = [1, 1, 1]
if (eFace.tintindex !== undefined) {
if (eFace.tintindex === 0) {
// TODO
// if (block.name === 'redstone_wire') {
// tint = tints.redstone[`${block.getProperties().power}`]
// } else if (block.name === 'birch_leaves' ||
// block.name === 'spruce_leaves' ||
// block.name === 'lily_pad') {
// tint = tints.constant[block.name]
// } else if (block.name.includes('leaves') || block.name === 'vine') {
// tint = tints.foliage[biome]
// } else {
// tint = tints.grass[biome]
// }
}
}
// UV rotation
const r = eFace.rotation || 0
const uvcs = Math.cos(r * Math.PI / 180)
const uvsn = -Math.sin(r * Math.PI / 180)
let localMatrix = null as any
let localShift = null as any
if (element.rotation) {
// todo do we support rescale?
localMatrix = buildRotationMatrix(
element.rotation.axis,
element.rotation.angle
)
localShift = vecsub3(
element.rotation.origin,
matmul3(
localMatrix,
element.rotation.origin
)
)
}
const aos: number[] = []
// const neighborPos = position.plus(new Vec3(...dir))
// const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15
const baseLight = 1
for (const pos of corners) {
let vertex = [
(pos[0] ? maxx : minx),
(pos[1] ? maxy : miny),
(pos[2] ? maxz : minz)
]
vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
vertex = vertex.map(v => v / 16)
attr.positions.push(
vertex[0]/* + (cursor.x & 15) - 8 */,
vertex[1]/* + (cursor.y & 15) x */,
vertex[2]/* + (cursor.z & 15) - 8 */
)
attr.normals.push(...dir)
const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
attr.uvs.push(baseu * su + u, basev * sv + v)
let light = 1
if (doAO) {
const dx = pos[0] * 2 - 1
const dy = pos[1] * 2 - 1
const dz = pos[2] * 2 - 1
const cornerDir = matmul3(globalMatrix, [dx, dy, dz])
const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]])
const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]])
// const side1 = world.getBlock(cursor.offset(...side1Dir))
// const side2 = world.getBlock(cursor.offset(...side2Dir))
// const corner = world.getBlock(cursor.offset(...cornerDir))
let cornerLightResult = 15
// if (/* world.config.smoothLighting */false) { // todo fix
// const side1Light = world.getLight(cursor.plus(new Vec3(...side1Dir)), true)
// const side2Light = world.getLight(cursor.plus(new Vec3(...side2Dir)), true)
// const cornerLight = world.getLight(cursor.plus(new Vec3(...cornerDir)), true)
// // interpolate
// cornerLightResult = (side1Light + side2Light + cornerLight) / 3
// }
// const side1Block = world.shouldMakeAo(side1) ? 1 : 0
// const side2Block = world.shouldMakeAo(side2) ? 1 : 0
// const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0
const side1Block = 0
const side2Block = 0
const cornerBlock = 0
// TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block)
const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
// todo light should go upper on lower blocks
light = (ao + 1) / 4 * (cornerLightResult / 15)
aos.push(ao)
}
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
}
// if (needTiles) {
// attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
// block: block.name,
// faces: [],
// }
// attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
// face,
// neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
// light: baseLight
// // texture: eFace.texture.name,
// })
// }
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
attr.indices.push(
ndx, ndx + 3, ndx + 2,
ndx, ndx + 1, ndx + 3
)
} else {
attr.indices.push(
ndx, ndx + 1, ndx + 2,
ndx + 2, ndx + 1, ndx + 3
)
}
}
}
export const renderBlockThreeAttr = (models: BlockModelPartsResolved, block: Block | undefined, biome: string, mcData: IndexedData, variants = [], neighbors: Neighbors = {}) => {
const sx = 0
const sy = 0
const sz = 0
const attr = {
sx: sx + 0.5,
sy: sy + 0.5,
sz: sz + 0.5,
positions: [],
normals: [],
colors: [],
uvs: [],
t_positions: [],
t_normals: [],
t_colors: [],
t_uvs: [],
indices: [],
tiles: {},
} as Record<string, any>
for (const [i, modelVars] of models.entries()) {
const model = modelVars[variants[i]] ?? modelVars[0]
if (!model) continue
let globalMatrix = null as any
let globalShift = null as any
for (const axis of ['x', 'y', 'z'] as const) {
if (axis in model) {
if (!globalMatrix) globalMatrix = buildRotationMatrix(axis, -(model[axis] ?? 0))
else globalMatrix = matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0)))
}
}
if (globalMatrix) {
globalShift = [8, 8, 8]
globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
}
const ao = model.ao ?? true
for (const element of model.elements ?? []) {
renderElement(element, ao, attr, globalMatrix, globalShift, block, biome, neighbors)
}
}
let ndx = attr.positions.length / 3
for (let i = 0; i < attr.t_positions.length / 12; i++) {
attr.indices.push(
ndx, ndx + 1, ndx + 2,
ndx + 2, ndx + 1, ndx + 3,
// back face
ndx, ndx + 2, ndx + 1,
ndx + 2, ndx + 3, ndx + 1
)
ndx += 4
}
attr.positions.push(...attr.t_positions)
attr.normals.push(...attr.t_normals)
attr.colors.push(...attr.t_colors)
attr.uvs.push(...attr.t_uvs)
delete attr.t_positions
delete attr.t_normals
delete attr.t_colors
delete attr.t_uvs
attr.positions = new Float32Array(attr.positions) as any
attr.normals = new Float32Array(attr.normals) as any
attr.colors = new Float32Array(attr.colors) as any
attr.uvs = new Float32Array(attr.uvs) as any
return attr
}
export const renderBlockThree = (...args: Parameters<typeof renderBlockThreeAttr>) => {
const attr = renderBlockThreeAttr(...args)
const data = {
geometry: attr
}
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
geometry.setIndex(data.geometry.indices)
geometry.name = 'block-geometry'
return geometry
}

View file

@ -3,10 +3,11 @@ import { World as MesherWorld } from '../world'
import ChunkLoader, { PCChunk } from 'prismarine-chunk'
import { Vec3 } from 'vec3'
import MinecraftData from 'minecraft-data'
import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
export const setup = (version, initialBlocks: [number[], string][]) => {
const mcData = MinecraftData(version)
const blockStates = require(`../../../../public/blocksStates/${version}.json`)
const blockStatesModels = require(`mc-assets/dist/blockStatesModels.json`)
const mesherWorld = new MesherWorld(version)
const Chunk = ChunkLoader(version)
const chunk1 = new Chunk(undefined as any)
@ -31,7 +32,7 @@ export const setup = (version, initialBlocks: [number[], string][]) => {
}
}
setBlockStatesData(blockStates, true)
setBlockStatesData(blockStatesModels, blocksAtlasesJson, true, false)
const reload = () => {
mesherWorld.removeColumn(0, 0)
mesherWorld.addColumn(0, 0, chunk1.toJson())

View file

@ -1,9 +1,8 @@
import { test, expect } from 'vitest'
import { setup } from './mesherTester'
import minecraftData from 'minecraft-data'
import minecraftAssets from 'minecraft-assets'
import supportedVersions from '../../../../../src/supportedVersions.mjs'
const version = minecraftAssets.versions.at(-1)
const lastVersion = supportedVersions.at(-1)
const addPositions = [
// [[0, 0, 0], 'diamond_block'],
@ -16,7 +15,7 @@ const addPositions = [
] as const
test('Known blocks are not rendered', () => {
const { mesherWorld, getGeometry, pos, mcData } = setup(version, addPositions as any)
const { mesherWorld, getGeometry, pos, mcData } = setup(lastVersion, addPositions as any)
const ignoreAsExpected = ['air', 'cave_air', 'void_air', 'barrier', 'water', 'lava', 'moving_piston', 'light']
let time = 0
@ -45,96 +44,27 @@ test('Known blocks are not rendered', () => {
// should be fixed, but to avoid regressions & for visibility
expect(invalidBlocks).toMatchInlineSnapshot(`
{
"black_banner": true,
"black_bed": true,
"black_candle": true,
"black_wall_banner": true,
"blue_banner": true,
"blue_bed": true,
"blue_candle": true,
"blue_wall_banner": true,
"brown_banner": true,
"brown_bed": true,
"brown_candle": true,
"brown_wall_banner": true,
"black_glazed_terracotta": true,
"blue_glazed_terracotta": true,
"brown_glazed_terracotta": true,
"bubble_column": true,
"candle": true,
"creeper_head": true,
"creeper_wall_head": true,
"cyan_banner": true,
"cyan_bed": true,
"cyan_candle": true,
"cyan_wall_banner": true,
"dragon_head": true,
"dragon_wall_head": true,
"cyan_glazed_terracotta": true,
"end_gateway": true,
"end_portal": true,
"gray_banner": true,
"gray_bed": true,
"gray_candle": true,
"gray_wall_banner": true,
"green_banner": true,
"green_bed": true,
"green_candle": true,
"green_wall_banner": true,
"light_blue_banner": true,
"light_blue_bed": true,
"light_blue_candle": true,
"light_blue_wall_banner": true,
"light_gray_banner": true,
"light_gray_bed": true,
"light_gray_candle": true,
"light_gray_wall_banner": true,
"lime_banner": true,
"lime_bed": true,
"lime_candle": true,
"lime_wall_banner": true,
"magenta_banner": true,
"magenta_bed": true,
"magenta_candle": true,
"magenta_wall_banner": true,
"orange_banner": true,
"orange_bed": true,
"orange_candle": true,
"orange_wall_banner": true,
"piglin_head": true,
"piglin_wall_head": true,
"pink_banner": true,
"pink_bed": true,
"pink_candle": true,
"pink_petals": true,
"pink_wall_banner": true,
"player_head": true,
"player_wall_head": true,
"powder_snow_cauldron": true,
"purple_banner": true,
"purple_bed": true,
"purple_candle": true,
"purple_wall_banner": true,
"red_banner": true,
"red_bed": true,
"red_candle": true,
"red_wall_banner": true,
"repeater": true,
"sea_pickle": true,
"skeleton_skull": true,
"skeleton_wall_skull": true,
"snow": true,
"gray_glazed_terracotta": true,
"green_glazed_terracotta": true,
"light_blue_glazed_terracotta": true,
"light_gray_glazed_terracotta": true,
"lime_glazed_terracotta": true,
"magenta_glazed_terracotta": true,
"orange_glazed_terracotta": true,
"pink_glazed_terracotta": true,
"purple_glazed_terracotta": true,
"red_glazed_terracotta": true,
"structure_void": true,
"turtle_egg": true,
"water_cauldron": true,
"white_banner": true,
"white_bed": true,
"white_candle": true,
"white_wall_banner": true,
"wither_skeleton_skull": true,
"wither_skeleton_wall_skull": true,
"yellow_banner": true,
"yellow_bed": true,
"yellow_candle": true,
"yellow_wall_banner": true,
"zombie_head": true,
"zombie_wall_head": true,
"trial_spawner": true,
"white_glazed_terracotta": true,
"yellow_glazed_terracotta": true,
}
`)
})

View file

@ -5,6 +5,7 @@ import { Vec3 } from 'vec3'
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
import { defaultMesherConfig } from './shared'
import legacyJson from '../../../../src/preflatMap.json'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
@ -18,10 +19,13 @@ function isCube (shapes) {
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
export type BlockModelPartsResolved = ReturnType<WorldBlockProvider['getAllResolvedModels0_1']>
export type WorldBlock = Omit<Block, 'position'> & {
variant?: any
// todo
isCube: boolean
/** cache */
models?: BlockModelPartsResolved | null
}

View file

@ -1,30 +0,0 @@
const supportedVersions = require('../../public/supportedVersions.json')
const lastOfMajor = {}
for (const version of supportedVersions) {
const major = toMajor(version)
if (lastOfMajor[major]) {
if (minor(lastOfMajor[major]) < minor(version)) {
lastOfMajor[major] = version
}
} else {
lastOfMajor[major] = version
}
}
function toMajor (version) {
const [a, b] = (version + '').split('.')
return a + '.' + b
}
function minor (version) {
const [, , c] = (version + '.0').split('.')
return parseInt(c, 10)
}
function getVersion (version) {
if (supportedVersions.indexOf(version) !== -1) return version
return lastOfMajor[toMajor(version)] ?? Object.values(lastOfMajor).at(-1)
}
module.exports = { getVersion, toMajor }

View file

@ -2,12 +2,13 @@ import * as THREE from 'three'
import { Vec3 } from 'vec3'
import { Entities } from './entities'
import { Primitives } from './primitives'
import { getVersion } from './version'
import EventEmitter from 'events'
import { WorldRendererThree } from './worldrendererThree'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
import { versionToNumber } from '../prepare/utils'
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
import { renderBlockThree } from './mesher/standaloneRenderer'
export class Viewer {
scene: THREE.Scene
@ -76,11 +77,13 @@ export class Viewer {
// this.primitives.clear()
}
setVersion (userVersion: string) {
let texturesVersion = getVersion(userVersion)
if (versionToNumber(userVersion) < versionToNumber('1.13')) texturesVersion = '1.13.2' // we normalize to post-flatenning in mesher
setVersion (userVersion: string, texturesVersion = userVersion) {
console.log('[viewer] Using version:', userVersion, 'textures:', texturesVersion)
this.world.setVersion(userVersion, texturesVersion)
this.world.setVersion(userVersion, texturesVersion).then(() => {
return new THREE.TextureLoader().loadAsync(this.world.itemsAtlasParser!.latestImage)
}).then((texture) => {
this.entities.itemsTexture = texture
})
this.entities.clear()
// this.primitives.clear()
}
@ -97,6 +100,24 @@ export class Viewer {
this.world.setBlockStateId(pos, stateId)
}
demoModel () {
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlases, 'latest')
const models = blockProvider.getAllResolvedModels0_1({
name: 'item_frame',
properties: {
map: false
}
})
const geometry = renderBlockThree(models, undefined, 'plains', loadedData)
const material = this.world.material
// block material
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
const helper = new THREE.BoxHelper(mesh, 0xffff00)
mesh.add(helper)
this.scene.add(mesh)
}
updateEntity (e) {
this.entities.update(e, this.processEntityOverrides(e, {
rotation: {

View file

@ -5,10 +5,18 @@ import { loadTexture } from './utils.web'
import { EventEmitter } from 'events'
import mcDataRaw from 'minecraft-data/data.js' // handled correctly in esbuild plugin
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajor } from './version.js'
import { chunkPos } from './simpleUtils'
import { defaultMesherConfig } from './mesher/shared'
import { buildCleanupDecorator } from './cleanupDecorator'
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
import { AtlasParser } from 'mc-assets'
import { getResourcepackTiles } from '../../../src/resourcePack'
import { toMajorVersion } from '../../../src/utils'
function mod (x, n) {
return ((x % n) + n) % n
@ -23,6 +31,11 @@ export const defaultWorldRendererConfig = {
export type WorldRendererConfig = typeof defaultWorldRendererConfig
type CustomTexturesData = {
tileSize: number | undefined
textures: Record<string, HTMLImageElement>
}
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
worldConfig = { minY: 0, worldHeight: 256 }
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
@ -36,11 +49,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
finishedChunks = {} as Record<string, boolean>
@worldCleanup()
sectionsOutstanding = new Map<string, number>()
@worldCleanup()
renderUpdateEmitter = new EventEmitter()
customBlockStatesData = undefined as any
customTexturesDataUrl = undefined as string | undefined
downloadedBlockStatesData = undefined as any
downloadedTextureImage = undefined as any
currentTextureImage = undefined as any
workers: any[] = []
viewerPosition?: Vec3
lastCamUpdate = 0
@ -55,10 +67,22 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
handleResize = () => { }
mesherConfig = defaultMesherConfig
camera: THREE.PerspectiveCamera
blockstatesModels: any
customBlockStates: Record<string, any> | undefined
customModels: Record<string, any> | undefined
itemsAtlasParser: AtlasParser | undefined
blocksAtlasParser: AtlasParser | undefined
blocksAtlases = blocksAtlases
itemsAtlases = itemsAtlases
customTextures: {
items?: CustomTexturesData
blocks?: CustomTexturesData
} = {}
abstract outputFormat: 'threeJs' | 'webgl'
constructor(public config: WorldRendererConfig) {
constructor (public config: WorldRendererConfig) {
// this.initWorkers(1) // preload script on page load
this.snapshotInitialValues()
}
@ -161,10 +185,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
worker.terminate()
}
this.workers = []
this.currentTextureImage = undefined
this.blocksAtlasParser = undefined
this.itemsAtlasParser = undefined
}
// new game load happens here
setVersion (version, texturesVersion = version) {
async setVersion (version, texturesVersion = version) {
if (!this.blockstatesModels) throw new Error('Blockstates models is not loaded yet')
this.version = version
this.texturesVersion = texturesVersion
this.resetWorld()
@ -174,11 +202,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.mesherConfig.version = this.version!
this.sendMesherMcData()
this.updateTexturesData()
await this.updateTexturesData()
}
sendMesherMcData () {
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)]
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)]
const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key)))
mcData.version = JSON.parse(JSON.stringify(mcData.version))
@ -187,38 +215,56 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
}
updateTexturesData () {
loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, (texture: import('three').Texture) => {
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.flipY = false
this.material.map = texture
}, () => {
this.downloadedTextureImage = this.material.map!.image
const loadBlockStates = async () => {
return new Promise(resolve => {
if (this.customBlockStatesData) return resolve(this.customBlockStatesData)
return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => {
this.downloadedBlockStatesData = data
this.renderUpdateEmitter.emit('blockStatesDownloaded')
resolve(data)
})
})
}
loadBlockStates().then((blockStates) => {
this.mesherConfig.textureSize = this.material.map!.image.width
async updateTexturesData () {
const blocksAssetsParser = new AtlasParser(this.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
const itemsAssetsParser = new AtlasParser(this.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
const texture = this.customTextures?.blocks?.textures[textureName]
if (!texture) return
return texture
}, this.customTextures?.blocks?.tileSize)
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
const texture = this.customTextures?.items?.textures[textureName]
if (!texture) return
return texture
}, this.customTextures?.items?.tileSize)
this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
for (const worker of this.workers) {
worker.postMessage({
type: 'mesherData',
json: blockStates,
config: this.mesherConfig,
})
const texture = await new THREE.TextureLoader().loadAsync(this.blocksAtlasParser.latestImage)
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.flipY = false
this.material.map = texture
this.currentTextureImage = this.material.map!.image
this.mesherConfig.textureSize = this.material.map!.image.width
for (const worker of this.workers) {
const blockstatesModels = this.blockstatesModels
if (this.customBlockStates) {
// TODO! remove from other versions as well
blockstatesModels.blockstates.latest = {
...blockstatesModels.blockstates.latest,
...this.customBlockStates
}
this.renderUpdateEmitter.emit('textureDownloaded')
}
if (this.customModels) {
blockstatesModels.models.latest = {
...blockstatesModels.models.latest,
...this.customModels
}
}
worker.postMessage({
type: 'mesherData',
blocksAtlas: {
latest: blocksAtlas
},
blockstatesModels,
config: this.mesherConfig,
})
})
}
this.renderUpdateEmitter.emit('textureDownloaded')
console.log('texture loaded')
}
addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {

View file

@ -1,144 +0,0 @@
import fs from 'fs'
import path from 'path'
import { Canvas, Image } from 'canvas'
import { getAdditionalTextures } from './moreGeneratedBlocks'
import { McAssets } from './modelsBuilder'
function nextPowerOfTwo (n) {
if (n === 0) return 1
n--
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
return n + 1
}
const localTextures = ['missing_texture.png']
function readTexture (basePath, name) {
if (localTextures.includes(name)) {
// grab ./missing_texture.png
basePath = __dirname
}
return fs.readFileSync(path.join(basePath, name), 'base64')
}
export type JsonAtlas = {
size: number,
textures: {
[file: string]: {
u: number,
v: number,
}
}
}
export const makeTextureAtlas = (input: string[], getInputData: (name) => { contents: string, tileWidthMult?: number, origSizeTextures?}, tilesCount = input.length, suSvOptimize: 'remove' | null = null): {
image: Buffer,
canvas: Canvas,
json: JsonAtlas
} => {
const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(tilesCount)))
const tileSize = 16
const imgSize = texSize * tileSize
const canvas = new Canvas(imgSize, imgSize, 'png' as any)
const g = canvas.getContext('2d')
const texturesIndex = {}
let nextX = 0
let nextY = 0
let rowMaxY = 0
const goToNextRow = () => {
nextX = 0
nextY += rowMaxY
rowMaxY = 0
}
const suSv = tileSize / imgSize
for (const i in input) {
const img = new Image()
const keyValue = input[i]
const inputData = getInputData(keyValue)
img.src = inputData.contents
let su = suSv
let sv = suSv
let renderWidth = tileSize * (inputData.tileWidthMult ?? 1)
let renderHeight = tileSize
if (inputData.origSizeTextures?.[keyValue]) {
// todo check have enough space
renderWidth = Math.ceil(img.width / tileSize) * tileSize
renderHeight = Math.ceil(img.height / tileSize) * tileSize
su = renderWidth / imgSize
sv = renderHeight / imgSize
if (renderHeight > imgSize || renderWidth > imgSize) {
throw new Error('Texture ' + keyValue + ' is too big')
}
}
if (nextX + renderWidth > imgSize) {
goToNextRow()
}
const x = nextX
const y = nextY
nextX += renderWidth
rowMaxY = Math.max(rowMaxY, renderHeight)
if (nextX >= imgSize) {
goToNextRow()
}
g.drawImage(img, 0, 0, renderWidth, renderHeight, x, y, renderWidth, renderHeight)
const cleanName = keyValue.split('.').slice(0, -1).join('.') || keyValue
texturesIndex[cleanName] = {
u: x / imgSize,
v: y / imgSize,
...suSvOptimize === 'remove' ? {} : {
su: su,
sv: sv
}
}
}
return { image: canvas.toBuffer(), canvas, json: { size: suSv, textures: texturesIndex } }
}
export const writeCanvasStream = (canvas, path, onEnd) => {
const out = fs.createWriteStream(path)
const stream = (canvas as any).pngStream()
stream.on('data', (chunk) => out.write(chunk))
if (onEnd) stream.on('end', onEnd)
return stream
}
export function makeBlockTextureAtlas (mcAssets: McAssets) {
const blocksTexturePath = path.join(mcAssets.directory, '/blocks')
const textureFiles = fs.readdirSync(blocksTexturePath).filter(file => file.endsWith('.png'))
// const textureFiles = mostEncounteredBlocks.map(x => x + '.png')
textureFiles.unshift(...localTextures)
const { generated: additionalTextures, origSizeTextures } = getAdditionalTextures()
textureFiles.push(...Object.keys(additionalTextures))
const atlas = makeTextureAtlas(textureFiles, name => {
let contents: string
if (additionalTextures[name]) {
contents = additionalTextures[name]
} else {
contents = 'data:image/png;base64,' + readTexture(blocksTexturePath, name)
}
return {
contents,
// tileWidthMult: twoTileTextures.includes(name) ? 2 : undefined,
origSizeTextures
}
})
return atlas
}

View file

@ -1,148 +0,0 @@
import fs from 'fs'
import McAssets from 'minecraft-assets'
import { join } from 'path'
import { filesize } from 'filesize'
import minecraftDataLoader from 'minecraft-data'
import BlockLoader from 'prismarine-block'
import { JsonAtlas, makeTextureAtlas, writeCanvasStream } from './atlas'
import looksSame from 'looks-same' // ensure after canvas import
import { Version as _Version } from 'minecraft-data'
import { versionToNumber } from './utils'
// todo move it, remove it
const legacyInvsprite = JSON.parse(fs.readFileSync(join(__dirname, '../../../src/invsprite.json'), 'utf8'))
//@ts-ignore
const latestMcAssetsVersion = McAssets.versions.at(-1)!
// const latestVersion = minecraftDataLoader.supportedVersions.pc.at(-1)
const mcData = minecraftDataLoader(latestMcAssetsVersion)
const PBlock = BlockLoader(latestMcAssetsVersion)
function isCube (name) {
const id = mcData.blocksByName[name]?.id
if (!id) return
const block = new PBlock(id, 0, 0)
const shape = block.shapes?.[0]
return block.shapes?.length === 1 && shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
export type ItemsAtlasesOutputJson = {
latest: JsonAtlas
legacy: JsonAtlas
legacyMap: [string, string[]][]
}
export const generateItemsAtlases = async () => {
const latestAssets = McAssets(latestMcAssetsVersion)
const latestItems = fs.readdirSync(join(latestAssets.directory, 'items')).map(f => f.split('.')[0])
// item - texture path
const toAddTextures = {
fromBlocks: {} as Record<string, string>,
remapItems: {} as Record<string, string>, // todo
}
const getItemTextureOfBlock = (name: string) => {
const blockModel = latestAssets.blocksModels[name]
// const isPlainBlockDisplay = blockModel?.display?.gui?.rotation?.[0] === 0 && blockModel?.display?.gui?.rotation?.[1] === 0 && blockModel?.display?.gui?.rotation?.[2] === 0
// it seems that information about cross blocks is hardcoded
if (blockModel?.parent?.endsWith('block/cross')) {
toAddTextures.fromBlocks[name] = `blocks/${blockModel.textures.cross.split('/')[1]}`
return true
}
if (legacyInvsprite[name]) {
return true
}
if (fs.existsSync(join(latestAssets.directory, 'blocks', name + '.png'))) {
// very last resort
toAddTextures.fromBlocks[name] = `blocks/${name}`
return true
}
if (name.endsWith('_spawn_egg')) {
// todo also color
toAddTextures.fromBlocks[name] = `items/spawn_egg`
}
}
for (const item of mcData.itemsArray) {
if (latestItems.includes(item.name)) {
continue
}
// USE IN RUNTIME
if (isCube(item.name)) {
// console.log('cube', block.name)
} else if (!getItemTextureOfBlock(item.name)) {
console.warn('skipping item (not cube, no item texture)', item.name)
}
}
let fullItemsMap = {} as Record<string, string[]>
const itemsSizes = {}
let saving = 0
let overallsize = 0
let prevItemsDir
let prevVersion
for (const version of [...McAssets.versions].reverse()) {
const itemsDir = join(McAssets(version).directory, 'items')
for (const item of fs.readdirSync(itemsDir)) {
const prevItemPath = !prevItemsDir ? undefined : join(prevItemsDir, item)
const itemSize = fs.statSync(join(itemsDir, item)).size
if (prevItemPath && fs.existsSync(prevItemPath) && (await looksSame(join(itemsDir, item), prevItemPath, { strict: true })).equal) {
saving += itemSize
} else {
fullItemsMap[version] ??= []
fullItemsMap[version].push(item)
}
overallsize += itemSize
}
prevItemsDir = itemsDir
prevVersion = version
}
fullItemsMap = Object.fromEntries(Object.entries(fullItemsMap).map(([ver, items]) => [ver, items.filter(item => item.endsWith('.png'))]))
const latestVersionItems = fullItemsMap[latestMcAssetsVersion]
delete fullItemsMap[latestMcAssetsVersion]
const legacyItemsSortedEntries = Object.entries(fullItemsMap).sort(([a], [b]) => versionToNumber(a) - versionToNumber(b)).map(([key, value]) => [key, value.map(x => x.replace('.png', ''))] as [typeof key, typeof value])
// const allItemsLength = Object.values(fullItemsMap).reduce((acc, x) => acc + x.length, 0)
// console.log(`Items to generate: ${allItemsLength} (latest version: ${latestVersionItems.length})`)
const fullLatestItemsObject = {
...Object.fromEntries(latestVersionItems.map(item => [item, `items/${item.replace('.png', '')}`])),
...toAddTextures.fromBlocks,
...toAddTextures.remapItems
}
const latestAtlas = makeTextureAtlas(Object.keys(fullLatestItemsObject), (name) => {
const contents = `data:image/png;base64,${fs.readFileSync(join(latestAssets.directory, `${fullLatestItemsObject[name]}.png`), 'base64')}`
return {
contents,
}
}, undefined, 'remove')
const texturesPath = join(__dirname, '../../public/textures')
writeCanvasStream(latestAtlas.canvas, join(texturesPath, 'items.png'), () => {
console.log('Generated latest items atlas')
})
const legacyItemsMap = legacyItemsSortedEntries.flatMap(([ver, items]) => items.map(item => `${ver}-${item}.png`))
const legacyItemsAtlas = makeTextureAtlas(legacyItemsMap, (name) => {
const [ver, item] = name.split('-')
const contents = `data:image/png;base64,${fs.readFileSync(join(McAssets(ver).directory, `items/${item}`), 'base64')}`
return {
contents,
}
}, undefined, 'remove')
writeCanvasStream(legacyItemsAtlas.canvas, join(texturesPath, 'items-legacy.png'), () => {
console.log('Generated legacy items atlas')
})
const allItemsMaps: ItemsAtlasesOutputJson = {
latest: latestAtlas.json,
legacy: legacyItemsAtlas.json,
legacyMap: legacyItemsSortedEntries
}
fs.writeFileSync(join(texturesPath, 'items.json'), JSON.stringify(allItemsMaps), 'utf8')
console.log(`Generated items! Input size: ${filesize(overallsize)}, saving: ~${filesize(saving)}`)
}

View file

@ -1,52 +0,0 @@
import path from 'path'
import { makeBlockTextureAtlas } from './atlas'
import { prepareBlocksStates } from './modelsBuilder'
import mcAssets from 'minecraft-assets'
import fs from 'fs-extra'
import { prepareMoreGeneratedBlocks } from './moreGeneratedBlocks'
import { generateItemsAtlases } from './genItemsAtlas'
import { versionToNumber } from './utils'
const publicPath = path.resolve(__dirname, '../../public')
const texturesPath = path.join(publicPath, 'textures')
fs.mkdirSync(texturesPath, { recursive: true })
const blockStatesPath = path.join(publicPath, 'blocksStates')
fs.mkdirSync(blockStatesPath, { recursive: true })
const warnings = new Set<string>()
Promise.resolve().then(async () => {
generateItemsAtlases()
console.time('generateTextures')
const versions = process.argv.includes('-l') ? [mcAssets.versions.at(-1)!] : mcAssets.versions
for (const version of versions as typeof mcAssets['versions']) {
// for debugging (e.g. when above is overridden)
if (!versions.includes(version)) {
throw new Error(`Version ${version} is not supported by minecraft-assets`)
}
if (versionToNumber(version) < versionToNumber('1.13')) {
// we normalize data to 1.13 for pre 1.13 versions
continue
}
const assets = mcAssets(version)
const { warnings: _warnings } = await prepareMoreGeneratedBlocks(assets)
_warnings.forEach(x => warnings.add(x))
// #region texture atlas
const atlas = makeBlockTextureAtlas(assets)
const out = fs.createWriteStream(path.resolve(texturesPath, version + '.png'))
const stream = (atlas.canvas as any).pngStream()
stream.on('data', (chunk) => out.write(chunk))
stream.on('end', () => console.log('Generated textures/' + version + '.png'))
// #endregion
const blocksStates = JSON.stringify(prepareBlocksStates(assets, atlas))
fs.writeFileSync(path.resolve(blockStatesPath, version + '.json'), blocksStates)
fs.copySync(assets.directory, path.resolve(texturesPath, version), { overwrite: true })
}
fs.writeFileSync(path.join(publicPath, 'supportedVersions.json'), '[' + versions.map(v => `"${v}"`).toString() + ']')
warnings.forEach(x => console.warn(x))
console.timeEnd('generateTextures')
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

View file

@ -1,259 +0,0 @@
type ModelBasic = {
model: string
x?: number
y?: number
uvlock?: boolean
}
type BlockApplyModel = ModelBasic | (ModelBasic & { weight })[]
type BlockStateCondition = {
[name: string]: string | number
}
type BlockState = {
variants?: {
[name: string | ""]: BlockApplyModel
}
multipart?: {
when: {
[name: string]: string | number
} & {
OR?: BlockStateCondition[]
}
apply: BlockApplyModel
}[]
}
type BlockModel = {
parent?: string
textures?: {
[name: string]: string
}
elements?: {
from: number[]
to: number[]
faces: {
[name: string]: {
texture: string
uv?: number[]
cullface?: string
}
}
}[]
ambientocclusion?: boolean
x?: number
y?: number
z?: number
ao?: boolean
}
export type McAssets = {
blocksStates: {
[x: string]: BlockState
}
blocksModels: {
[x: string]: BlockModel
}
directory: string
version: string
}
export type BlockStatesOutput = {
// states: {
[blockName: string]: any/* ResolvedModel */
// }
// defaults: {
// su: number
// sv: number
// }
}
export type ResolvedModel = {
textures: {
[name: string]: {
u: number
v: number
su: number
sv: number
bu: number
bv: number
}
}
elements: {
from: number[]
to: number[]
faces: {
[name: string]: {
texture: {
u: number
v: number
su: number
sv: number
bu: number
bv: number
}
}
}
}[]
ao: boolean
x?: number
y?: number
z?: number
}
export const addBlockAllModel = (mcAssets: McAssets, name: string, texture = name) => {
mcAssets.blocksStates[name] = {
"variants": {
"": {
"model": name
}
}
}
mcAssets.blocksModels[name] = {
"parent": "block/cube_all",
"textures": {
"all": `blocks/${texture}`
}
}
}
function cleanupBlockName (name: string) {
if (name.startsWith('block') || name.startsWith('minecraft:block')) return name.split('/')[1]
return name
}
const objectAssignStrict = <T extends Record<string, any>> (target: T, source: Partial<T>) => Object.assign(target, source)
function getFinalModel (name: string, blocksModels: { [x: string]: BlockModel }) {
name = cleanupBlockName(name)
const input = blocksModels[name]
if (!input) {
return null
}
let out: BlockModel | null = {
textures: {},
elements: [],
ao: true,
x: input.x,
y: input.y,
z: input.z,
}
if (input.parent) {
out = getFinalModel(input.parent, blocksModels)
if (!out) return null
}
if (input.textures) {
Object.assign(out.textures!, deepCopy(input.textures))
}
if (input.elements) out.elements = deepCopy(input.elements)
if (input.ao !== undefined) out.ao = input.ao
return out
}
const deepCopy = (obj) => JSON.parse(JSON.stringify(obj))
const workerUsedTextures = ['particle']
function prepareModel (model: BlockModel, texturesJson) {
const newModel = {}
const getFinalTexture = (originalBlockName) => {
// texture name e.g. blocks/anvil_base
const cleanBlockName = cleanupBlockName(originalBlockName)
return { ...texturesJson[cleanBlockName], /* __debugName: cleanBlockName */ }
}
const finalTextures = []
// resolve texture names eg west: #all -> blocks/stone
for (const side in model.textures) {
let texture = model.textures[side]
while (texture.charAt(0) === '#') {
const textureName = texture.slice(1)
texture = model.textures[textureName]
if (texture === undefined) throw new Error(`Texture ${textureName} in ${JSON.stringify(model.textures)} not found`)
}
finalTextures[side] = getFinalTexture(texture)
if (workerUsedTextures.includes(side)) {
model.textures[side] = finalTextures[side]
}
}
for (const elem of model.elements!) {
for (const sideName of Object.keys(elem.faces)) {
const face = elem.faces[sideName]
const textureRaw = face.texture.charAt(0) === '#'
? finalTextures![face.texture.slice(1)]
: getFinalTexture(face.texture)
if (!textureRaw) throw new Error(`Texture ${face.texture} in ${JSON.stringify(model.textures)} not found`)
const finalTexture = deepCopy(
textureRaw
)
const _from = elem.from
const _to = elem.to
// taken from https://github.com/DragonDev1906/Minecraft-Overviewer/
const uv = face.uv || {
// default UVs
// format: [u1, v1, u2, v2] (u = x, v = y)
north: [_to[0], 16 - _to[1], _from[0], 16 - _from[1]],
east: [_from[2], 16 - _to[1], _to[2], 16 - _from[1]],
south: [_from[0], 16 - _to[1], _to[0], 16 - _from[1]],
west: [_from[2], 16 - _to[1], _to[2], 16 - _from[1]],
up: [_from[0], _from[2], _to[0], _to[2]],
down: [_to[0], _from[2], _from[0], _to[2]]
}[sideName]!
const su = (uv[2] - uv[0]) / 16 * finalTexture.su
const sv = (uv[3] - uv[1]) / 16 * finalTexture.sv
finalTexture.u += uv[0] / 16 * finalTexture.su
finalTexture.v += uv[1] / 16 * finalTexture.sv
finalTexture.su = su
finalTexture.sv = sv
face.texture = finalTexture
}
}
return model
}
function resolveModel (name, blocksModels, texturesJson) {
const model = getFinalModel(name, blocksModels)
return prepareModel(model, texturesJson.textures)
}
export function prepareBlocksStates (mcAssets: McAssets, atlas: { json: any }) {
addBlockAllModel(mcAssets, 'missing_texture')
const blocksStates = mcAssets.blocksStates
for (const block of Object.values(blocksStates)) {
if (!block) continue
if (block.variants) {
for (const variant of Object.values(block.variants)) {
if (variant instanceof Array) {
for (const v of variant) {
v.model = resolveModel(v.model, mcAssets.blocksModels, atlas.json) as any
}
} else {
variant.model = resolveModel(variant.model, mcAssets.blocksModels, atlas.json) as any
}
}
}
if (block.multipart) {
for (const variant of block.multipart) {
if (variant.apply instanceof Array) {
for (const v of variant.apply) {
v.model = resolveModel(v.model, mcAssets.blocksModels, atlas.json) as any
}
} else {
variant.apply.model = resolveModel(variant.apply.model, mcAssets.blocksModels, atlas.json) as any
}
}
}
}
return blocksStates
}

View file

@ -1,421 +0,0 @@
import Jimp from 'jimp'
import minecraftData from 'minecraft-data'
import { McAssets } from './modelsBuilder'
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
import { versionToNumber } from './utils'
// todo refactor
const handledBlocks = ['water', 'lava', 'barrier']
const origSizeTextures: string[] = []
let currentImage: Jimp
let currentBlockName: string
let currentMcAssets: McAssets
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
type SidesType = {
"up": string
"north": string
"east": string
"south": string
"west": string
"down": string
}
const getBlockStates = (name: string) => {
const mcData = minecraftData(currentMcAssets.version)
return mcData.blocksByName[name]?.states
}
export const addBlockCustomSidesModel = (name: string, sides: SidesType) => {
currentMcAssets.blocksStates[name] = {
"variants": {
"": {
"model": name
}
}
}
currentMcAssets.blocksModels[name] = {
"parent": "block/cube",
"textures": sides
}
}
type TextureMap = [
x: number,
y: number,
width?: number,
height?: number,
]
const justCropUV = (x: number, y: number, x1, y1) => {
// input: 0-16, output: 0-currentImage.getWidth()
const width = Math.abs(x1 - x)
const height = Math.abs(y1 - y)
return currentImage.clone().crop(
x / 16 * currentImage.getWidth(),
y / 16 * currentImage.getHeight(),
width / 16 * currentImage.getWidth(),
height / 16 * currentImage.getHeight(),
)
}
const justCrop = (x: number, y: number, width = 16, height = 16) => {
return currentImage.clone().crop(x, y, width, height)
}
const combineTextures = (locations: TextureMap[]) => {
const resized: Jimp[] = []
for (const [x, y, height = 16, width = 16] of locations) {
resized.push(justCrop(x, y, width, height))
}
const combinedImage = new Jimp(locations[0]![2] ?? 16, locations[0]![3] ?? 16)
for (const image of resized) {
combinedImage.blit(image, 0, 0)
}
return combinedImage
}
const generatedImageTextures: { [blockName: string]: /* base64 */string } = {}
const getBlockTexturesFromJimp = async <T extends Record<string, Jimp>> (sides: T, withUv = false, textureNameBase = currentBlockName): Promise<Record<keyof T, any>> => {
const sidesTextures = {} as any
for (const [side, jimp] of Object.entries(sides)) {
const textureName = `${textureNameBase}_${side}`
const sideTexture = withUv ? { uv: [0, 0, jimp.getWidth(), jimp.getHeight()], texture: textureName } : textureName
const base64Url = await jimp.getBase64Async(jimp.getMIME())
if (side === 'side') {
sidesTextures['north'] = sideTexture
sidesTextures['east'] = sideTexture
sidesTextures['south'] = sideTexture
sidesTextures['west'] = sideTexture
} else {
sidesTextures[side] = sideTexture
}
generatedImageTextures[textureName] = base64Url
}
return sidesTextures
}
const addSimpleCubeWithSides = async (sides: Record<string, Jimp>) => {
const sidesTextures = await getBlockTexturesFromJimp(sides)
addBlockCustomSidesModel(currentBlockName, sidesTextures as any)
}
const handleShulkerBox = async (dataBase: string, match: RegExpExecArray) => {
const [, shulkerColor = ''] = match
currentImage = await Jimp.read(dataBase + `entity/shulker/shulker${shulkerColor && `_${shulkerColor}`}.png`)
const shulkerBoxTextures = {
// todo do all sides
side: combineTextures([
[0, 16], // top
[0, 36], // bottom
]),
up: justCrop(16, 0),
down: justCrop(32, 28)
}
await addSimpleCubeWithSides(shulkerBoxTextures)
}
// TODO! should not be there! move to data with signs!
const chestModels = {
chest: {
"parent": "block/block",
"textures": {
"particle": "#particles"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 10, 15],
"faces": {
"down": { "texture": "#chest", "uv": [3.5, 4.75, 7, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.5, 8.25, 14, 10.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 8.25, 3.5, 10.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 8.25, 7, 10.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7, 8.25, 10.5, 10.75], "rotation": 180 }
},
},
{
"from": [1, 10, 1],
"to": [15, 14, 15],
"faces": {
"up": { "texture": "#chest", "uv": [3.5, 4.75, 7, 8.25] },
"north": { "texture": "#chest", "uv": [10.5, 3.75, 14, 4.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 3.75, 3.5, 4.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 3.75, 7, 4.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7, 3.75, 10.5, 4.75], "rotation": 180 }
}
},
{
"from": [7, 7, 0],
"to": [9, 11, 1],
"faces": {
"down": { "texture": "#chest", "uv": [0.25, 0, 0.75, 0.25], "rotation": 180 },
"up": { "texture": "#chest", "uv": [0.75, 0, 1.25, 0.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [1, 0.25, 1.5, 1.25], "rotation": 180 },
"west": { "texture": "#chest", "uv": [0.75, 0.25, 1, 1.25], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 0.25, 0.25, 1.25], "rotation": 180 }
}
}
]
},
chest_left: {
"parent": "block/block",
"textures": {
"particle": "#particles"
},
"elements": [
{
"from": [1, 0, 1],
"to": [16, 10, 15],
"faces": {
"down": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 8.25, 14.5, 10.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 8.25, 7.25, 10.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7.25, 8.25, 10.75, 10.75], "rotation": 180 }
}
},
{
"from": [1, 10, 1],
"to": [16, 14, 15],
"faces": {
"up": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 3.75, 14.5, 4.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 3.75, 7.25, 4.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7.25, 3.75, 10.75, 4.75], "rotation": 180 }
}
},
{
"from": [15, 7, 0],
"to": [16, 11, 1],
"faces": {
"down": { "texture": "#chest", "uv": [0.25, 0, 0.5, 0.25], "rotation": 180 },
"up": { "texture": "#chest", "uv": [0.5, 0, 0.75, 0.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [0.75, 0.25, 1, 1.25], "rotation": 180 },
"west": { "texture": "#chest", "uv": [0.5, 0.25, 0.75, 1.25], "rotation": 180 }
}
}
]
},
chest_right: {
"parent": "block/block",
"textures": {
"particle": "#particles"
},
"elements": [
{
"from": [0, 0, 1],
"to": [15, 10, 15],
"faces": {
"down": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 8.25, 14.5, 10.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 8.25, 3.5, 10.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 8.25, 7.25, 10.75], "rotation": 180 }
}
},
{
"from": [0, 10, 1],
"to": [15, 14, 15],
"faces": {
"up": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 3.75, 14.5, 4.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 3.75, 3.5, 4.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 3.75, 7.25, 4.75], "rotation": 180 }
}
},
{
"from": [0, 7, 0],
"to": [1, 11, 1],
"faces": {
"down": { "texture": "#chest", "uv": [0.25, 0, 0.5, 0.25], "rotation": 180 },
"up": { "texture": "#chest", "uv": [0.5, 0, 0.75, 0.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [0.75, 0.25, 1, 1.25], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0.0, 0.25, 0.25, 1.25], "rotation": 180 }
}
}
]
}
}
// these blockStates / models copied from https://github.com/FakeDomi/FastChest/blob/master/src/main/resources/assets/minecraft/blockstates/
const chestBlockStatesMap = {
chest: JSON.parse(fs.readFileSync(path.join(__dirname, 'blockStates/chest.json'), 'utf-8')),
trapped_chest: JSON.parse(fs.readFileSync(path.join(__dirname, 'blockStates/trapped_chest.json'), 'utf-8')),
ender_chest: JSON.parse(fs.readFileSync(path.join(__dirname, 'blockStates/ender_chest.json'), 'utf-8')),
}
const handleChest = async (dataBase: string, match: RegExpExecArray) => {
const blockStates = structuredClone(chestBlockStatesMap[currentBlockName])
const particle = match[1] === 'ender' ? 'obsidian' : 'oak_planks'
const blockStatesVariants = Object.values(blockStates.variants) as { model }[]
const neededModels = [...new Set(blockStatesVariants.map((x) => x.model))]
for (const modelName of neededModels) {
let chestTextureName = {
chest: 'normal',
trapped_chest: 'trapped',
ender_chest: 'ender',
}[currentBlockName]
if (modelName.endsWith('_left')) chestTextureName = `${chestTextureName}_left`
if (modelName.endsWith('_right')) chestTextureName = `${chestTextureName}_right`
const texture = path.join(currentMcAssets.directory, `../1.19.1/entity/chest/${chestTextureName}.png`)
currentImage = await Jimp.read(texture)
const model = structuredClone(chestModels[modelName])
model.textures.particle = particle
const newModelName = `${currentBlockName}_${modelName}`
for (const variant of blockStatesVariants) {
if (variant.model !== modelName) continue
variant.model = newModelName
}
for (const [i, { faces }] of model.elements.entries()) {
for (const [faceName, face] of Object.entries(faces) as any) {
const { uv } = face
//@ts-ignore
const jimp = justCropUV(...uv)
const key = `${chestTextureName}_${modelName}_${i}_${faceName}`
const texture = await getBlockTexturesFromJimp({
[key]: jimp
}, true, key).then(a => a[key])
face.texture = texture.texture
face.uv = texture.uv
}
}
currentMcAssets.blocksModels[newModelName] = model
}
currentMcAssets.blocksStates[currentBlockName] = blockStates
}
async function loadBlockModelTextures (dataBase: string, blockModel: any) {
for (const key in blockModel.textures) {
let texture: string = blockModel.textures[key]
const useAssetsPath = !!texture.match(/^[0-9.]+\//)
blockModel.textures.particle = texture
generatedImageTextures[texture] = `data:image/png;base64,${fs.readFileSync(path.join(dataBase, useAssetsPath ? '..' : '', texture + '.png'), 'base64')}`
origSizeTextures[texture] = true
}
}
const handlers = [
[/(.+)_shulker_box$/, handleShulkerBox],
[/^shulker_box$/, handleShulkerBox],
[/^(?:(ender|trapped)_)?chest$/, handleChest],
// [/(^|(.+)_)bed$/, handleBed],
// no-op just suppress warning
[/(^light|^moving_piston$)/, true],
] as const
export const tryHandleBlockEntity = async (dataBase, blockName) => {
currentBlockName = blockName
for (const [regex, handler] of handlers) {
const match = regex.exec(blockName)
if (!match) continue
if (handler !== true) {
await handler(dataBase, match)
}
return true
}
}
async function readAllBlockStates (blockStatesDir: string) {
const files = fs.readdirSync(blockStatesDir)
for (const file of files) {
if (file.endsWith('.json')) {
const state = JSON.parse(fs.readFileSync(path.join(blockStatesDir, file), 'utf-8'))
const name = file.replace('.json', '')
currentMcAssets.blocksStates[name] = state
handledBlocks.push(name)
} else {
await readAllBlockStates(path.join(blockStatesDir, file))
}
}
}
async function readAllBlockModels (dataBase: string, blockModelsDir: string, completePath: string) {
const actualPath = completePath.length ? completePath + "/" : ""
const files = fs.readdirSync(blockModelsDir)
for (const file of files) {
if (file.endsWith('.json')) {
const model = JSON.parse(fs.readFileSync(path.join(blockModelsDir, file), 'utf-8'))
const name = actualPath + file.replace('.json', '')
currentMcAssets.blocksModels[name] = model
await loadBlockModelTextures(dataBase, model)
} else {
await readAllBlockModels(dataBase, path.join(blockModelsDir, file), actualPath + file)
}
}
}
const handleExternalData = async (assetsPathRoot: string, version: string) => {
const currentVersionNumber = versionToNumber(version)
const versions = fs.readdirSync(path.join(__dirname, 'data'), { withFileTypes: true })
.filter(x => x.isDirectory())
.map(x => x.name)
.sort((a, b) => versionToNumber(b) - versionToNumber(a))
const allAssetsVersions = fs.readdirSync(assetsPathRoot, { withFileTypes: true })
.filter(x => x.isDirectory())
.map(x => x.name)
.sort((a, b) => versionToNumber(b) - versionToNumber(a))
const getAssetsVersion = (version: string) => {
return allAssetsVersions[version] ?? allAssetsVersions.find(x => x.startsWith(version))
}
for (const curVer of versions) {
const baseDir = path.join(__dirname, 'data', curVer)
if (versionToNumber(curVer) > currentVersionNumber) continue
const assetsVersion = getAssetsVersion(curVer)
await readAllBlockStates(path.join(baseDir, 'blockStates'))
await readAllBlockModels(path.join(assetsPathRoot, assetsVersion), path.join(baseDir, 'blockModels'), "")
}
}
export const prepareMoreGeneratedBlocks = async (mcAssets: McAssets) => {
const mcData = minecraftData(mcAssets.version)
const allTheBlocks = mcData.blocksArray.map(x => x.name)
currentMcAssets = mcAssets
// todo
const ignoredBlocks = ['skull', 'structure_void', 'banner', 'bed', 'end_portal']
for (const theBlock of allTheBlocks) {
try {
if (await tryHandleBlockEntity(mcAssets.directory, theBlock)) {
handledBlocks.push(theBlock)
}
} catch (err) {
// todo remove when all warnings are resolved
console.warn(`[${mcAssets.version}] failed to generate block ${theBlock}`)
}
}
await handleExternalData(path.join(mcAssets.directory, '..'), mcAssets.version)
const warnings: string[] = []
for (const [name, model] of Object.entries(mcAssets.blocksModels)) {
if (Object.keys(model).length === 1 && model.textures) {
const keys = Object.keys(model.textures)
if (keys.length === 1 && keys[0] === 'particle') {
if (handledBlocks.includes(name) || ignoredBlocks.includes(name)) continue
warnings.push(`unhandled block ${name}`)
}
}
}
return { warnings }
}
export const getAdditionalTextures = () => {
return { generated: generatedImageTextures, origSizeTextures }
}

View file

@ -1,12 +0,0 @@
import path from 'path'
import fs from 'fs'
const publicPath = path.resolve(__dirname, '../../public')
const texturesPath = path.join(publicPath, 'textures')
if (fs.existsSync(texturesPath) && !process.argv.includes('-f')) {
console.log('textures folder already exists, skipping...')
process.exit(0)
} else {
import('./generateTextures')
}

View file

@ -1 +0,0 @@
["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", "1.18.2"]

View file

@ -1,88 +0,0 @@
// eslint-disable-next-line no-unused-vars
const webpack = require('webpack')
const path = require('path')
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// Minify the index.js by removing unused minecraft data. Since the worker only needs to do meshing,
// we can remove all the other data unrelated to meshing.
const blockedIndexFiles = ['blocksB2J', 'blocksJ2B', 'blockMappings', 'steve', 'recipes']
const allowedWorkerFiles = ['blocks', 'blockCollisionShapes', 'tints', 'blockStates',
'biomes', 'features', 'version', 'legacy', 'versions', 'version', 'protocolVersions']
const indexConfig = {
entry: './lib/index.js',
mode: 'production',
output: {
path: path.resolve(__dirname, './public'),
filename: './index.js'
},
resolve: {
fallback: {
zlib: false
}
},
plugins: [
// fix "process is not defined" error:
new webpack.ProvidePlugin({
process: 'process/browser'
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer']
}),
new webpack.NormalModuleReplacementPlugin(
// eslint-disable-next-line
/viewer[\/|\\]lib[\/|\\]utils/,
'./utils.web.js'
)
// new BundleAnalyzerPlugin()
],
externals: [
function (req, cb) {
if (req.context.includes('minecraft-data') && req.request.endsWith('.json')) {
const fileName = req.request.split('/').pop().replace('.json', '')
if (blockedIndexFiles.includes(fileName)) {
cb(null, [])
return
}
}
cb()
}
]
}
const workerConfig = {
entry: './viewer/lib/worker.js',
mode: 'production',
output: {
path: path.join(__dirname, '/public'),
filename: './worker.js'
},
resolve: {
fallback: {
zlib: false
}
},
plugins: [
// fix "process is not defined" error:
new webpack.ProvidePlugin({
process: 'process/browser'
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer']
})
],
externals: [
function (req, cb) {
if (req.context.includes('minecraft-data') && req.request.endsWith('.json')) {
const fileName = req.request.split('/').pop().replace('.json', '')
if (!allowedWorkerFiles.includes(fileName)) {
cb(null, [])
return
}
}
cb()
}
]
}
module.exports = [indexConfig, workerConfig]

185
rsbuild.config.ts Normal file
View file

@ -0,0 +1,185 @@
import { defineConfig, RsbuildPluginAPI } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules'
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
import { pluginTypeCheck } from '@rsbuild/plugin-type-check'
import path from 'path'
import childProcess from 'child_process'
import fs from 'fs'
import fsExtra from 'fs-extra'
import { promisify } from 'util'
import { generateSW } from 'workbox-build'
import { getSwAdditionalEntries } from './scripts/build'
//@ts-ignore
try { require('./localSettings.js') } catch { }
const execAsync = promisify(childProcess.exec)
const buildingVersion = new Date().toISOString().split(':')[0]
const dev = process.env.NODE_ENV === 'development'
export default defineConfig({
dev: {
progressBar: true,
writeToDisk: true
},
html: {
template: './index.html',
},
output: {
polyfill: 'usage',
externals: [
'sharp'
],
sourceMap: {
js: 'source-map',
css: true,
},
// 50kb limit for data uri
dataUriLimit: 50 * 1024
},
source: {
alias: {
fs: './src/shims/fs.js',
http: 'http-browserify',
stream: 'stream-browserify',
net: 'net-browserify',
'minecraft-protocol$': 'minecraft-protocol/src/index.js',
'buffer$': 'buffer',
// avoid bundling, not used on client side
'prismarine-auth': './src/shims/empty.ts',
perf_hooks: './src/shims/perf_hooks_replacement.js',
crypto: './src/shims/crypto.js',
dns: './src/shims/dns.js',
yggdrasil: './src/shims/yggdrasilReplacement.ts',
},
entry: {
index: './src/index.ts',
},
// exclude: [
// /.woff$/
// ],
define: {
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
'process.platform': '"browser"',
'process.env.GITHUB_URL':
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`),
'process.env.DEPS_VERSIONS': JSON.stringify({})
},
decorators: {
version: 'legacy', // default is a lie
},
},
server: {
// strictPort: true,
htmlFallback: false,
publicDir: false,
// publicDir: {
// name: 'assets',
// },
headers: {
// enable shared array buffer
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
open: process.env.OPEN_BROWSER === 'true',
proxy: {
'/api': 'http://localhost:8080',
},
},
plugins: [
pluginReact(),
pluginTypedCSSModules(),
pluginNodePolyfill(),
{
name: 'test',
setup (build: RsbuildPluginAPI) {
const prep = async () => {
console.time('total-prep')
if (!fs.existsSync('./generated/minecraft-data-data.js')) {
childProcess.execSync('tsx ./scripts/genShims.ts', { stdio: 'inherit' })
}
fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity')
fsExtra.copySync('./assets/background', './dist/background')
fs.copyFileSync('./assets/favicon.png', './dist/favicon.png')
fs.copyFileSync('./assets/manifest.json', './dist/manifest.json')
fs.copyFileSync('./assets/loading-bg.jpg', './dist/loading-bg.jpg')
const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
if (dev) {
configJson.defaultProxy = ':8080'
}
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
childProcess.execSync('node ./scripts/prepareData.mjs', { stdio: 'inherit' })
// childProcess.execSync('./scripts/prepareSounds.mjs', { stdio: 'inherit' })
// childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
// childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
if (fs.existsSync('./prismarine-viewer/public/mesher.js')) {
// copy mesher
fs.copyFileSync('./prismarine-viewer/public/mesher.js', './dist/mesher.js')
} else {
await execAsync('pnpm run build-mesher')
}
fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8')
console.timeEnd('total-prep')
}
if (!dev) {
build.onBeforeBuild(async () => {
await prep()
})
build.onAfterBuild(async () => {
const { count, size, warnings } = await generateSW({
// dontCacheBustURLsMatching: [new RegExp('...')],
globDirectory: 'dist',
skipWaiting: true,
clientsClaim: true,
additionalManifestEntries: getSwAdditionalEntries(),
globPatterns: [],
swDest: './dist/service-worker.js',
})
})
}
build.onBeforeStartDevServer(prep)
},
},
],
tools: {
bundlerChain (chain, { CHAIN_ID }) {
},
rspack (config, { addRules, appendPlugins, rspack }) {
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data/, (resource) => {
let absolute: string
const request = resource.request.replaceAll('\\', '/')
absolute = path.join(resource.context, request).replaceAll('\\', '/')
if (request.includes('minecraft-data/data/pc/1.')) {
console.log('Error: incompatible resource', request, resource.contextInfo.issuer)
process.exit(1)
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
}
if (absolute.endsWith('/minecraft-data/data.js')) {
resource.request = path.join(__dirname, './generated/minecraft-data-data.js')
}
}))
addRules([
{
test: /\.obj$/,
type: 'asset/source',
},
{
test: /\.mp3$/,
type: 'asset/source',
}
])
config.ignoreWarnings = [
/the request of a dependency is an expression/,
/Unsupported pseudo class or element: xr-overlay/
]
}
},
performance: {
// bundleAnalyze: {
// analyzerMode: 'json',
// },
},
})

View file

@ -5,18 +5,15 @@ const glob = require('glob')
const fs = require('fs')
const crypto = require('crypto')
const path = require('path')
const McAssets = require('minecraft-assets')
const prismarineViewerBase = "./node_modules/prismarine-viewer"
const entityMcAssets = McAssets('1.16.4')
// these files could be copied at build time eg with copy plugin, but copy plugin slows down the config so we copy them there, alternative we could inline it in esbuild config
const filesToCopy = [
{ from: `${prismarineViewerBase}/public/blocksStates/`, to: 'dist/blocksStates/' },
{ from: `${prismarineViewerBase}/public/mesher.js`, to: 'dist/mesher.js' },
{ from: './assets/', to: './dist/' },
{ from: './config.json', to: 'dist/config.json' },
{ from: path.join(entityMcAssets.directory, 'entity'), to: 'dist/textures/1.16.4/entity' },
// { from: path.join(entityMcAssets.directory, 'entity'), to: 'dist/textures/1.16.4/entity' },
]
exports.filesToCopy = filesToCopy
exports.copyFiles = (dev = false) => {
@ -46,15 +43,10 @@ exports.copyFilesDev = () => {
exports.getSwAdditionalEntries = () => {
// need to be careful with this
const singlePlayerVersion = defaultLocalServerOptions.version
const filesToCachePatterns = [
'index.html',
'index.js',
'index.css',
'favicon.ico',
`mc-data/${defaultLocalServerOptions.versionMajor}.js`,
`blocksStates/${singlePlayerVersion}.json`,
'extra-textures/**',
'background/**',
// todo-low copy from assets
'*.mp3',
'*.ttf',
@ -62,13 +54,12 @@ exports.getSwAdditionalEntries = () => {
'*.woff',
'mesher.js',
'worldSaveWorker.js',
// todo-low preload entity atlas?
`textures/${singlePlayerVersion}.png`,
`textures/1.16.4/entity/squid.png`,
`textures/entity/squid/squid.png`,
// everything but not .map
'static/**/!(*.map)',
]
const filesNeedsCacheKey = [
'index.js',
'index.css',
'index.html',
'mesher.js',
'worldSaveWorker.js',
]
@ -89,6 +80,9 @@ exports.getSwAdditionalEntries = () => {
output.push({ url, revision })
}
}
if (output.length > 40) {
throw new Error(`SW: Ios has a limit of 40 urls to cache (now ${output.length})`)
}
console.log(`Got ${output.length} additional sw entries to cache`)
return output
}
@ -98,6 +92,16 @@ exports.moveStorybookFiles = () => {
fsExtra.copySync('dist/storybook', '.vercel/output/static/storybook')
}
exports.getSwFilesSize = () => {
const files = exports.getSwAdditionalEntries()
let size = 0
for (const { url } of files) {
const file = path.join(__dirname, '../dist', url)
size += fs.statSync(file).size
}
console.log('mb', size / 1024 / 1024)
}
const fn = require.main === module && exports[process.argv[2]]
if (fn) {

View file

@ -11,7 +11,7 @@ fs.promises.readdir(path.resolve(__dirname, '../src/react')).then(async (files)
const components = files
.filter((file) => {
if (file.startsWith('Concept')) return false
return file.endsWith('.stories.tsx');
return file.endsWith('.stories.tsx')
})
.map((file) => {
return file.replace('.stories.tsx', '')
@ -39,7 +39,7 @@ fs.promises.readdir(path.resolve(__dirname, '../src/react')).then(async (files)
version = version.replace(/^v/, '')
packageJson.version = version
const externalize = ['minecraft-assets', 'prismarine-viewer']
const externalize = ['prismarine-viewer', 'mc-assets']
const { metafile } = await build({
entryPoints: [path.resolve(__dirname, '../src/react/npmReactComponents.ts')],
bundle: true,

12
scripts/dockerPrepare.mjs Normal file
View file

@ -0,0 +1,12 @@
//@ts-check
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
delete packageJson.optionalDependencies
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2), 'utf8')
const packageJsonViewer = JSON.parse(fs.readFileSync('./prismarine-viewer/package.json', 'utf8'))
delete packageJsonViewer.optionalDependencies
fs.writeFileSync('./prismarine-viewer/package.json', JSON.stringify(packageJsonViewer, null, 2), 'utf8')

View file

@ -1,47 +1,16 @@
//@ts-check
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
import { join, dirname, basename } from 'path'
import { join, dirname } from 'path'
import * as fs from 'fs'
import { filesize } from 'filesize'
import MCProtocol from 'minecraft-protocol'
import MCData from 'minecraft-data'
import { throttle } from 'lodash-es'
import { fileURLToPath } from 'url'
import { gzipSizeFromFileSync } from 'gzip-size'
const __dirname = dirname(fileURLToPath(new URL(import.meta.url)))
const { supportedVersions } = MCProtocol
const prod = process.argv.includes('--prod')
let connectedClients = []
const writeToClients = (data) => {
connectedClients.forEach((res) => {
res.write(`data: ${JSON.stringify(data)}\n\n`)
res.flush()
})
}
export const startWatchingHmr = () => {
const eventsPerFile = {
'mesher.js': 'mesher',
// 'dist/webglRendererWorker.js': 'webglRendererWorker',
}
for (const name of Object.keys(eventsPerFile)) {
const file = join('dist', name)
if (!fs.existsSync(file)) console.warn(`[missing worker] File ${name} does not exist`)
fs.watchFile(file, () => {
writeToClients({ replace: { type: eventsPerFile[name] } })
})
}
}
/** @type {import('esbuild').Plugin[]} */
const mesherSharedPlugins = [
{
name: 'minecraft-data',
setup(build) {
setup (build) {
build.onLoad({
filter: /data[\/\\]pc[\/\\]common[\/\\]legacy.json$/,
}, async (args) => {
@ -55,311 +24,4 @@ const mesherSharedPlugins = [
}
]
/** @type {import('esbuild').Plugin[]} */
const plugins = [
...mesherSharedPlugins,
{
name: 'strict-aliases',
setup(build) {
build.onResolve({
filter: /^minecraft-protocol$/,
}, async ({ kind, resolveDir }) => {
return {
path: (await build.resolve('minecraft-protocol/src/index.js', { kind, resolveDir })).path,
}
})
build.onLoad({
filter: /minecraft-data[\/\\]data.js$/,
}, (args) => {
const version = supportedVersions.at(-1)
if (!version) throw new Error('unreachable')
const data = MCData(version)
const defaultVersionsObj = {
// default protocol data, needed for auto-version
[version]: {
version: data.version,
// protocol: JSON.parse(fs.readFileSync(join(args.path, '..', 'minecraft-data/data/pc/1.20/protocol.json'), 'utf8')),
protocol: data.protocol,
}
}
return {
contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`,
loader: 'js',
}
})
build.onResolve({
filter: /^minecraft-assets$/,
}, () => {
throw new Error('hit banned package')
})
build.onLoad({
filter: /^prismarine-auth/,
}, () => {
return {
contents: 'module.exports = {}',
}
})
build.onResolve({
filter: /^three$/,
}, async ({ kind, resolveDir }) => {
return {
path: (await build.resolve('three/src/Three.js', { kind, resolveDir })).path,
}
})
}
},
{
name: 'data-assets',
setup(build) {
build.onResolve({
filter: /.*/,
}, async ({ path, ...rest }) => {
if (['.woff', '.woff2', '.ttf'].some(ext => path.endsWith(ext)) || path.startsWith('extra-textures/')) {
return {
path,
namespace: 'assets',
external: true,
}
}
})
const removeNodeModulesSourcemaps = (map) => {
const doNotRemove = ['prismarine', 'mineflayer', 'flying-squid', '@jspm/core', 'minecraft', 'three']
map.sourcesContent.forEach((_, i) => {
if (map.sources[i].includes('node_modules') && !doNotRemove.some(x => map.sources[i].includes(x))) {
map.sourcesContent[i] = null
}
})
}
build.onEnd(async ({ metafile, outputFiles }) => {
// write outputFiles
//@ts-ignore
for (const file of outputFiles) {
let contents = file.text
if (file.path.endsWith('.map') && file.text && !process.env.PROD) {
const map = JSON.parse(file.text)
removeNodeModulesSourcemaps(map)
contents = JSON.stringify(map)
}
await fs.promises.writeFile(file.path, contents)
}
if (!prod) return
// const deps = Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).map(([x, { bytes }]) => [x, filesize(bytes)]).slice(0, 5)
//@ts-ignore
const sizeByExt = {}
//@ts-ignore
Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).forEach(([x, { bytes }]) => {
const ext = x.slice(x.lastIndexOf('.'))
sizeByExt[ext] ??= 0
sizeByExt[ext] += bytes
})
console.log('Input size by ext for dist/index.js:')
console.log(Object.fromEntries(Object.entries(sizeByExt).map(x => [x[0], filesize(x[1])])))
console.log('Gzip size for dist/index.js:', filesize(gzipSizeFromFileSync('dist/index.js')))
})
},
},
{
name: 'prevent-incorrect-linking',
setup(build) {
build.onResolve({
filter: /.+/,
}, async ({ resolveDir, path, importer, kind, pluginData }) => {
if (pluginData?.__internal) return
// not ideal as packages can have different version, on the other hand we should not have multiple versions of the same package of developing deps
const packageName = path.startsWith('@') ? path.split('/', 2).join('/') : path.split('/', 1)[0]
const localPackageToReuse = join('node_modules', packageName)
if (!resolveDir.startsWith(process.cwd()) && ['./', '../'].every(x => !path.startsWith(x)) && fs.existsSync(localPackageToReuse)) {
const redirected = await build.resolve(path, { kind: 'import-statement', resolveDir: process.cwd(), pluginData: { __internal: true }, })
return redirected
}
// disallow imports from outside the root directory to ensure modules are resolved from node_modules of this workspace
// if ([resolveDir, path].some(x => x.includes('node_modules')) && !resolveDir.startsWith(process.cwd())) {
// // why? ensure workspace dependency versions are used (we have overrides and need to dedupe so it doesn't grow in size)
// throw new Error(`Restricted package import from outside the root directory: ${resolveDir}`)
// }
return undefined
})
}
},
{
name: 'watch-notify',
setup(build) {
let count = 0
let time
let prevHash
build.onStart(() => {
time = Date.now()
})
build.onEnd(({ errors, outputFiles: _outputFiles, metafile, warnings }) => {
/** @type {import('esbuild').OutputFile[]} */
// @ts-ignore
const outputFiles = _outputFiles
const elapsed = Date.now() - time
outputFiles.find(outputFile => outputFile.path)
if (errors.length) {
writeToClients({ errors: errors.map(error => error.text) })
return
}
// write metafile to disk if needed to analyze
fs.writeFileSync('dist/meta.json', JSON.stringify(metafile, null, 2))
/** @type {import('esbuild').OutputFile} */
//@ts-ignore
const outputFile = outputFiles.find(x => x.path.endsWith('.js'))
if (outputFile.hash === prevHash) {
// todo also check workers and css
console.log('Ignoring reload as contents the same')
return
}
prevHash = outputFile.hash
let outputText = outputFile.text
//@ts-ignore
if (['inline', 'both'].includes(build.initialOptions.sourcemap)) {
outputText = outputText.slice(0, outputText.indexOf('//# sourceMappingURL=data:application/json;base64,'))
}
console.log(`Done in ${elapsed}ms. Size: ${filesize(outputText.length)} (${build.initialOptions.minify ? 'minified' : 'without minify'})`)
if (count++ === 0) {
return
}
writeToClients({ update: { time: elapsed } })
connectedClients.length = 0
})
}
},
{
name: 'esbuild-readdir',
setup(build) {
build.onResolve({
filter: /^esbuild-readdir:.+$/,
}, ({ resolveDir, path }) => {
return {
namespace: 'esbuild-readdir',
path,
pluginData: {
resolveDir: join(resolveDir, path.replace(/^esbuild-readdir:/, ''))
},
}
})
build.onLoad({
filter: /.+/,
namespace: 'esbuild-readdir',
}, async ({ pluginData }) => {
const { resolveDir } = pluginData
const files = await fs.promises.readdir(resolveDir)
return {
contents: `module.exports = ${JSON.stringify(files)}`,
resolveDir,
loader: 'js',
}
})
}
},
{
name: 'esbuild-import-glob',
setup(build) {
build.onResolve({
filter: /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/,
}, ({ resolveDir, path }) => {
return {
namespace: 'esbuild-import-glob',
path,
pluginData: {
resolveDir
},
}
})
build.onLoad({
filter: /.+/,
namespace: 'esbuild-import-glob',
}, async ({ pluginData, path }) => {
const { resolveDir } = pluginData
//@ts-ignore
const [, userPath, skipFiles] = /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/g.exec(path)
const files = (await fs.promises.readdir(join(resolveDir, userPath))).filter(f => !skipFiles.includes(f))
return {
contents: `module.exports = { ${files.map(f => `'${f}': require('./${join(userPath, f)}')`).join(',')} }`,
resolveDir,
loader: 'js',
}
})
}
},
{
name: 'fix-dynamic-require',
setup(build) {
build.onResolve({
filter: /1\.14\/chunk/,
}, async ({ resolveDir, path }) => {
if (!resolveDir.includes('prismarine-provider-anvil')) return
return {
namespace: 'fix-dynamic-require',
path,
pluginData: {
resolvedPath: `${join(resolveDir, path)}.js`,
resolveDir
},
}
})
build.onLoad({
filter: /.+/,
namespace: 'fix-dynamic-require',
}, async ({ pluginData: { resolvedPath, resolveDir } }) => {
const resolvedFile = await fs.promises.readFile(resolvedPath, 'utf8')
return {
contents: resolvedFile.replace("require(`prismarine-chunk/src/pc/common/BitArray${noSpan ? 'NoSpan' : ''}`)", "noSpan ? require(`prismarine-chunk/src/pc/common/BitArray`) : require(`prismarine-chunk/src/pc/common/BitArrayNoSpan`)"),
resolveDir,
loader: 'js',
}
})
}
},
{
name: 'react-displayname',
setup(build) {
build.onLoad({
filter: /.tsx$/,
}, async ({ path }) => {
let file = await fs.promises.readFile(path, 'utf8')
const fileName = basename(path, '.tsx')
let replaced = false
const varName = `__${fileName}_COMPONENT`
file = file.replace(/export default /, () => {
replaced = true
return `const ${varName} = `
})
if (replaced) {
file += `;${varName}.displayName = '${fileName}';export default ${varName};`
}
return {
contents: file,
loader: 'tsx',
}
})
}
},
polyfillNode({
polyfills: {
fs: false,
dns: false,
crypto: false,
events: false,
http: false,
stream: false,
buffer: false,
perf_hooks: false,
net: false,
assert: false,
},
})
]
export { plugins, connectedClients as clients, mesherSharedPlugins }
export { mesherSharedPlugins }

View file

@ -1,52 +0,0 @@
//@ts-check
import fs from 'fs'
import minecraftAssets from 'minecraft-assets'
// why store another data?
// 1. want to make it compatible (at least for now)
// 2. don't want to read generated blockStates as it might change in future, and the current way was faster to implement
const blockNames = []
const indexesPerVersion = {}
/** @type {Map<string, number>} */
const allBlocksMap = new Map()
const getBlockIndex = (block) => {
if (allBlocksMap.has(block)) {
return allBlocksMap.get(block)
}
const index = blockNames.length
allBlocksMap.set(block, index)
blockNames.push(block)
return index
}
// const blocksFull = []
// const allBlocks = []
// // we can even optimize it even futher by doing prev-step resolving
// const blocksDiff = {}
for (const [i, version] of minecraftAssets.versions.reverse().entries()) {
const assets = minecraftAssets(version)
const blocksDir = assets.directory + '/blocks'
const blocks = fs.readdirSync(blocksDir)
indexesPerVersion[version] = blocks.map(block => {
if (!block.endsWith('.png')) return undefined
return getBlockIndex(block)
}).filter(i => i !== undefined)
// if (!blocksFull.length) {
// // first iter
// blocksFull.push(...blocks)
// } else {
// const missing = blocksFull.map((b, i) => !blocks.includes(b) ? i : -1).filter(i => i !== -1)
// const added = blocks.filter(b => !blocksFull.includes(b))
// blocksDiff[version] = {
// missing,
// added
// }
// }
}
fs.mkdirSync('./generated', { recursive: true, })
fs.writeFileSync('./generated/blocks.json', JSON.stringify({ blockNames: blockNames, indexes: indexesPerVersion }))

43
scripts/genShims.ts Normal file
View file

@ -0,0 +1,43 @@
import fs from 'fs'
import MinecraftData from 'minecraft-data'
import MCProtocol from 'minecraft-protocol'
import { appReplacableResources } from '../src/resourcesSource'
const { supportedVersions, defaultVersion } = MCProtocol
// gen generated/minecraft-data-data.js
const data = MinecraftData(defaultVersion)
const defaultVersionObj = {
[defaultVersion]: {
version: data.version,
protocol: data.protocol,
}
}
const mcDataContents = `window.mcData ??= ${JSON.stringify(defaultVersionObj)};module.exports = { pc: window.mcData }`
fs.writeFileSync('./generated/minecraft-data-data.js', mcDataContents, 'utf8')
// app resources
let headerImports = ''
let resourcesContent = 'export const appReplacableResources: { [key: string]: { content: any, resourcePackPath: string, cssVar?: string, cssVarRepeat?: number } } = {\n'
for (const resource of appReplacableResources) {
const { path, ...rest } = resource
const name = path.split('/').slice(-4).join('_').replace('.png', '').replaceAll('-', '_').replaceAll('.', '_')
headerImports += `import ${name} from '${path.replace('../node_modules/', '')}'\n`
resourcesContent += `
'${name}': {
content: ${name},
resourcePackPath: 'minecraft/textures/${path.slice(path.indexOf('other-textures/') + 'other-textures/'.length).split('/').slice(1).join('/')}',
...${JSON.stringify(rest)}
},
`
}
resourcesContent += '}'
fs.mkdirSync('./src/generated', { recursive: true })
fs.writeFileSync('./src/generated/resources.ts', headerImports + '\n' + resourcesContent, 'utf8')

View file

@ -1,209 +0,0 @@
//@ts-check
import minecraftData from 'minecraft-data'
import minecraftAssets from 'minecraft-assets'
import fs from 'fs'
const latestVersion = minecraftData.versions.pc[0]
const latestData = minecraftData(latestVersion.minecraftVersion)
// dont touch, these are the ones that are already full box
const fullBoxInteractionShapes = [
'dead_bush',
'cave_vines_plant',
'grass',
'tall_seagrass',
'spruce_sapling',
'oak_sapling',
'dark_oak_sapling',
'birch_sapling',
'seagrass',
'nether_portal',
'tall_grass',
'lilac',
'cobweb'
]
const ignoreStates = [
'mangrove_propagule',
'moving_piston'
]
// const
// to fix
const fullBoxInteractionShapesTemp = [
'moving_piston',
'lime_wall_banner',
'gray_wall_banner',
'weeping_vines_plant',
'pumpkin_stem',
'red_wall_banner',
'crimson_wall_sign',
'magenta_wall_banner',
'melon_stem',
'gray_banner',
'spruce_sign',
'pink_wall_banner',
'purple_banner',
'bamboo_sapling',
'mangrove_sign',
'cyan_banner',
'blue_banner',
'green_wall_banner',
'yellow_banner',
'black_wall_banner',
'green_banner',
'oak_sign',
'jungle_sign',
'yellow_wall_banner',
'lime_banner',
'tube_coral',
'red_banner',
'magenta_banner',
'brown_wall_banner',
'white_wall_banner',
]
const shapes = latestData.blockCollisionShapes
const fullShape = shapes.shapes[1]
const outputJson = {}
let interestedBlocksNoStates = []
let interestedBlocksStates = []
const stateIgnoreStates = ['waterlogged']
const isNonInteractive = block => block.name.includes('air') || block.name.includes('water') || block.name.includes('lava') || block.name.includes('void')
const interestedBlocks = latestData.blocksArray.filter(block => {
const shapeId = shapes.blocks[block.name]
// console.log('shapeId', shapeId, block.name)
if (!shapeId) return true
const shape = typeof shapeId === 'number' ? shapes.shapes[shapeId] : shapeId
if (shape.length === 0) return true
// console.log(shape)
}).filter(b => !isNonInteractive(b)).filter(b => {
if (fullBoxInteractionShapes.includes(b.name)) {
outputJson[b.name] = fullShape
return false
}
if (!b.states?.length || ignoreStates.includes(b.name) || b.states.every(s => stateIgnoreStates.every(state => s.name === state))) {
interestedBlocksNoStates.push(b.name)
return false
} else {
interestedBlocksStates.push(b.name)
return false
}
}).map(d => d.name)
const { blocksStates, blocksModels } = minecraftAssets(latestData.version.minecraftVersion)
const getShapeFromModel = (block,) => {
const blockStates = JSON.parse(fs.readFileSync('./prismarine-viewer/public/blocksStates/1.19.1.json', 'utf8'))
const blockState = blockStates[block]
const perVariant = {}
for (const [key, variant] of Object.entries(blockState.variants)) {
// const shapes = (Array.isArray(variant) ? variant : [variant]).flatMap((v) => v.model?.elements).filter(Boolean).map(({ from, to }) => [...from, ...to]).reduce((acc, cur) => {
// return [
// Math.min(acc[0], cur[0]),
// Math.min(acc[1], cur[1]),
// Math.min(acc[2], cur[2]),
// Math.max(acc[3], cur[3]),
// Math.max(acc[4], cur[4]),
// Math.max(acc[5], cur[5])
// ]
// })
console.log(variant)
const shapes = (Array.isArray(variant) ? variant : [variant]).flatMap((v) => v.model?.elements).filter(Boolean).map(({ from, to }) => [...from, ...to])
perVariant[key] = shapes
break
}
return perVariant
}
// console.log(getShapeFromModel('oak_button'))
// const addShapeIf = {
// redstone: [
// ['east', 'up', shape]
// ]
// }
const needBlocksStated = {}
const groupedBlocksRules = {
// button: block => block.includes('button'),
// pressure_plate: block => block.includes('pressure_plate'),
// sign: block => block.includes('_sign'),
// sapling: block => block.includes('_sapling'),
}
const groupedBlocksOutput = {}
outer: for (const interestedBlock of [...interestedBlocksNoStates, ...interestedBlocksStates]) {
for (const [block, func] of Object.entries(groupedBlocksRules)) {
if (func(interestedBlock)) {
groupedBlocksOutput[block] ??= []
groupedBlocksOutput[block].push(interestedBlock)
continue outer
}
}
const hasStates = interestedBlocksStates.includes(interestedBlock)
if (hasStates) {
const states = blocksStates[interestedBlock]
if (!states) {
console.log('no states', interestedBlock)
continue
}
if (!states.variants) {
if (!states.multipart) {
console.log('no variants', interestedBlock)
continue
}
let outputStates = {}
for (const { when } of states.multipart) {
if (when) {
for (const [key, value] of Object.entries(when)) {
if (key === 'OR') {
for (const or of value) {
for (const [key, value] of Object.entries(or)) {
const str = `${key}=${value}`
outputStates[str] = true
}
}
continue
}
const str = `${key}=${value}`
outputStates[str] = true
}
}
}
needBlocksStated[interestedBlock] = outputStates
continue
}
if (Object.keys(states.variants).length === 1 && states.variants['']) {
needBlocksStated[interestedBlock] = false
} else {
needBlocksStated[interestedBlock] = Object.fromEntries(Object.entries(states.variants).map(([key, value]) => [key, true]))
}
} else {
needBlocksStated[interestedBlock] = false
}
// let vars = []
// Object.keys(variants).forEach(variant => {
// if (variant !== '') vars.push(variant)
// })
// needBlocksVariants.push({
// block: interestedBlock,
// variants: vars
// })
}
fs.writeFileSync('scripts/needBlocks.json', JSON.stringify(needBlocksStated))
// console.log(interestedBlocks.includes('lever'))
// read latest block states
// read block model elements & combine

View file

@ -49,6 +49,7 @@ for (const version of [...supportedVersions].reverse()) {
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
data.version = version
processData(data)
fs.mkdirSync('./generated', { recursive: true })
fs.writeFileSync('./generated/latestBlockCollisionsShapes.json', JSON.stringify(data), 'utf8')
break
}

View file

@ -1,16 +0,0 @@
import fs from 'fs'
import minecraftAssets from 'minecraft-assets'
const gen = JSON.parse(fs.readFileSync('./blocks.json', 'utf8'))
const version = '1.8.8'
const { blockNames, indexes } = gen
const blocksActual = indexes[version].map((i) => blockNames[i])
const blocksExpected = fs.readdirSync(minecraftAssets(version).directory + '/blocks')
for (const [i, item] of blocksActual.entries()) {
if (item !== blocksExpected[i]) {
console.log('not equal at', i)
}
}

View file

@ -19,9 +19,6 @@ const isProd = process.argv.includes('--prod')
app.use(compression())
app.use(netApi({ allowOrigin: '*' }))
if (!isProd) {
app.use('/blocksStates', express.static(path.join(__dirname, './prismarine-viewer/public/blocksStates')))
app.use('/textures', express.static(path.join(__dirname, './prismarine-viewer/public/textures')))
app.use('/sounds', express.static(path.join(__dirname, './generated/sounds/')))
}
// patch config
@ -40,23 +37,24 @@ app.get('/config.json', (req, res, next) => {
'defaultProxy': '', // use current url (this server)
})
})
// add headers to enable shared array buffer
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
next()
})
app.use(express.static(path.join(__dirname, './dist')))
if (isProd) {
// add headers to enable shared array buffer
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
next()
})
app.use(express.static(path.join(__dirname, './dist')))
}
const portArg = process.argv.indexOf('--port')
const port = (require.main === module ? process.argv[2] : portArg !== -1 ? process.argv[portArg + 1] : undefined) || 8080
const numArg = process.argv.find(x => x.match(/^\d+$/))
const port = (require.main === module ? numArg : undefined) || 8080
// Start the server
const server = isProd ?
undefined :
const server =
app.listen(port, async function () {
console.log('Server listening on port ' + server.address().port)
if (siModule) {
console.log('Proxy server listening on port ' + server.address().port)
if (siModule && isProd) {
const _interfaces = await siModule.networkInterfaces()
const interfaces = Array.isArray(_interfaces) ? _interfaces : [_interfaces]
let netInterface = interfaces.find(int => int.default)

View file

@ -8,10 +8,10 @@ const sounds: Record<string, any> = {}
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
const loadingSounds = [] as string[]
const convertedSounds = [] as string[]
export async function loadSound (path: string) {
export async function loadSound (path: string, contents = path) {
if (loadingSounds.includes(path)) return true
loadingSounds.push(path)
const res = await window.fetch(path)
const res = await window.fetch(contents)
if (!res.ok) {
const error = `Failed to load sound ${path}`
if (isCypress()) throw new Error(error)

View file

@ -7,7 +7,7 @@ import * as browserfs from 'browserfs'
import { options, resetOptions } from './optionsStorage'
import { fsState, loadSave } from './loadSave'
import { installTexturePack, installTexturePackFromHandle, updateTexturePackInstalledState } from './texturePack'
import { installTexturePack, installTexturePackFromHandle, updateTexturePackInstalledState } from './resourcePack'
import { miscUiState } from './globalState'
import { setLoadingScreenStatus } from './utils'
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') // disable type checking
@ -531,7 +531,9 @@ export const openFilePicker = (specificCase?: 'resourcepack') => {
if (!doContinue) return
}
if (specificCase === 'resourcepack') {
void installTexturePack(file)
void installTexturePack(file).catch((err) => {
setLoadingScreenStatus(err.message, true)
})
} else {
void openWorldZip(file)
}

View file

@ -7,19 +7,20 @@ import { ControMax } from 'contro-max/build/controMax'
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
import { stringStartsWith } from 'contro-max/build/stringUtils'
import { UserOverrideCommand, UserOverridesConfig } from 'contro-max/build/types/store'
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState } from './globalState'
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, loadedGameState } from './globalState'
import { goFullscreen, pointerLock, reloadChunks } from './utils'
import { options } from './optionsStorage'
import { openPlayerInventory } from './inventoryWindows'
import { chatInputValueGlobal } from './react/Chat'
import { fsState } from './loadSave'
import { customCommandsConfig } from './customCommands'
import { CustomCommand } from './react/KeybindingsCustom'
import type { CustomCommand } from './react/KeybindingsCustom'
import { showOptionsModal } from './react/SelectOption'
import widgets from './react/widgets'
import { getItemFromBlock } from './botUtils'
import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor'
import { updateBinds } from './react/KeybindingsScreenProvider'
import { completeTexturePackInstall, resourcePackState } from './resourcePack'
import { showNotification } from './react/NotificationProvider'
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig
@ -454,7 +455,7 @@ export const f3Keybinds = [
mobileTitle: 'Toggle chunk borders',
},
{
key: 'KeyT',
key: 'KeyY',
async action () {
// waypoints
const widgetNames = widgets.map(widget => widget.name)
@ -463,6 +464,17 @@ export const f3Keybinds = [
showModal({ reactType: `widget-${widget}` })
},
mobileTitle: 'Open Widget'
},
{
key: 'KeyT',
async action () {
// TODO!
if (resourcePackState.resourcePackInstalled || loadedGameState.usingServerResourcePack) {
showNotification('Reloading textures...')
await completeTexturePackInstall('default', 'default')
}
},
mobileTitle: 'Open Widget'
}
]
@ -687,3 +699,30 @@ window.addEventListener('keydown', (e) => {
}
})
// #endregion
export function updateBinds (commands: any) {
contro.inputSchema.commands.custom = Object.fromEntries(Object.entries(commands?.custom ?? {}).map(([key, value]) => {
return [key, {
keys: [],
gamepad: [],
type: '',
inputs: []
}]
}))
for (const [group, actions] of Object.entries(commands)) {
contro.userConfig![group] = Object.fromEntries(Object.entries(actions).map(([key, value]) => {
const newValue = {
keys: value?.keys ?? undefined,
gamepad: value?.gamepad ?? undefined,
}
if (group === 'custom') {
newValue['type'] = (value).type
newValue['inputs'] = (value).inputs
}
return [key, newValue]
}))
}
}

View file

@ -2,44 +2,11 @@ import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils'
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
if (process.env.NODE_ENV === 'development') {
if (sessionStorage.lastReload) {
const [rebuild, reloadStart] = sessionStorage.lastReload.split(',')
const now = Date.now()
console.log(`rebuild + reload:`, `${+rebuild} + ${now - reloadStart} = ${((+rebuild + (now - reloadStart)) / 1000).toFixed(1)}s`)
sessionStorage.lastReload = ''
}
const autoRefresh = () => {
window.noAutoReload ??= false
new EventSource('/esbuild').onmessage = async ({ data: _data }) => {
if (!_data) return
const data = JSON.parse(_data)
if (data.update) {
console.log('[esbuild] Page is outdated')
document.title = `[O] ${document.title}`
if (window.noAutoReload || localStorage.noAutoReload) return
if (localStorage.autoReloadVisible && document.visibilityState !== 'visible') return
sessionStorage.lastReload = `${data.update.time},${Date.now()}`
location.reload()
}
if (data.replace) {
console.log('[esbuild hmr] Reloading', data.replace.type, data.replace.path)
switch (data.replace.type) {
case 'mesher': {
if (!worldView || !viewer.world.version || !(viewer.world instanceof WorldRendererThree)) return
void viewer.world.doHmr()
}
}
}
}
}
autoRefresh()
// mobile devtools
if (isMobile()) {
// can be changed to require('eruda')
//@ts-expect-error
void import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => eruda.init())
console.log('JS Loaded in', Date.now() - window.startLoad)
}
}
console.log('JS Loaded in', Date.now() - window.startLoad)

View file

@ -1,6 +1,6 @@
import prettyBytes from 'pretty-bytes'
import { openWorldZip } from './browserfs'
import { getResourcePackName, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './texturePack'
import { getResourcePackNames, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './resourcePack'
import { setLoadingScreenStatus } from './utils'
export const getFixedFilesize = (bytes: number) => {
@ -18,7 +18,7 @@ const inner = async () => {
if (texturepack) {
await updateTexturePackInstalledState()
if (resourcePackState.resourcePackInstalled) {
if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackName()} Continue?`)) return
if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackNames()[0]} Continue?`)) return
}
}
const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25)

View file

@ -65,6 +65,8 @@ async function handleDroppedFile (file: File) {
let versionDetected = false
for (const [i, _] of Array.from({ length: 32 }).entries()) {
for (const [k, _] of Array.from({ length: 32 }).entries()) {
// todo, may use faster reading, but features is not commonly used
// eslint-disable-next-line no-await-in-loop
const nbt = await region.read(i, k)
chunks[`${i},${k}`] = nbt
if (nbt && !versionDetected) {

View file

@ -130,8 +130,6 @@ export type AppConfig = {
export const miscUiState = proxy({
currentDisplayQr: null as string | null,
currentTouch: null as boolean | null,
serverIp: null as string | null,
username: '',
hasErrors: false,
singleplayer: false,
flyingSquid: false,
@ -148,6 +146,12 @@ export const miscUiState = proxy({
displaySearchInput: false,
})
export const loadedGameState = proxy({
username: '',
serverIp: '' as string | null,
usingServerResourcePack: false,
})
export const isGameActive = (foregroundCheck: boolean) => {
if (foregroundCheck && activeModalStack.length) return false
return miscUiState.gameLoaded

View file

@ -6,7 +6,7 @@ import './devtools'
import './entities'
import './globalDomListeners'
import initCollisionShapes from './getCollisionShapes'
import { itemsAtlases, onGameLoad } from './inventoryWindows'
import { onGameLoad } from './inventoryWindows'
import { supportedVersions } from 'minecraft-protocol'
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
import microsoftAuthflow from './microsoftAuthflow'
@ -21,7 +21,7 @@ import PrismarineBlock from 'prismarine-block'
import PrismarineItem from 'prismarine-item'
import { options, watchValue } from './optionsStorage'
import './reactUi.jsx'
import './reactUi'
import { contro, onBotCreate } from './controls'
import './dragndrop'
import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs'
@ -51,6 +51,7 @@ import {
hideModal,
insertActiveModalStack,
isGameActive,
loadedGameState,
miscUiState,
showModal
} from './globalState'
@ -71,7 +72,7 @@ import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalS
import defaultServerOptions from './defaultLocalServerOptions'
import dayCycle from './dayCycle'
import { genTexturePackTextures, watchTexturepackInViewer } from './texturePack'
import { onAppLoad, resourcepackOnWorldLoad } from './resourcePack'
import { connectToPeer } from './localServerMultiplayer'
import CustomChannelClient from './customClient'
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
@ -98,6 +99,9 @@ import { signInMessageState } from './react/SignInMessageProvider'
import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider'
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
import packetsPatcher from './packetsPatcher'
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
import { mainMenuState } from './react/MainMenuRenderApp'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
window.debug = debug
window.THREE = THREE
@ -106,11 +110,14 @@ window.beforeRenderFrame = []
// ACTUAL CODE
void registerServiceWorker()
void registerServiceWorker().then(() => {
mainMenuState.serviceWorkerLoaded = true
})
watchFov()
initCollisionShapes()
initializePacketsReplay()
packetsPatcher()
onAppLoad()
// Create three.js context, add to page
let renderer: THREE.WebGLRenderer
@ -149,46 +156,45 @@ if (isIphone) {
// Create viewer
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer)
window.viewer = viewer
new THREE.TextureLoader().load(itemsPng, (texture) => {
viewer.entities.itemsTexture = texture
// todo unify
viewer.entities.getItemUv = (id) => {
try {
const name = loadedData.items[id]?.name
const uv = itemsAtlases.latest.textures[name]
if (!uv) {
const variant = viewer.world.downloadedBlockStatesData[name]?.variants?.['']
if (!variant) return
const faces = (Array.isArray(variant) ? variant[0] : variant).model?.elements?.[0]?.faces
const uvBlock = faces?.north?.texture ?? faces?.up?.texture ?? faces?.down?.texture ?? faces?.west?.texture ?? faces?.east?.texture ?? faces?.south?.texture
if (!uvBlock) return
return {
...uvBlock,
size: Math.abs(uvBlock.su),
texture: viewer.world.material.map
}
}
return {
...uv,
size: itemsAtlases.latest.size,
texture: viewer.entities.itemsTexture
}
} catch (err) {
reportError?.(err)
return {
u: 0,
v: 0,
size: 16 / viewer.world.material.map!.image.width,
texture: viewer.world.material.map
}
// todo unify
viewer.entities.getItemUv = (idOrName: number | string) => {
try {
const name = typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
// TODO
if (!viewer.world.itemsAtlasParser) throw new Error('itemsAtlasParser not loaded yet')
const itemsRenderer = new ItemsRenderer('latest', viewer.world.blockstatesModels, viewer.world.itemsAtlasParser, viewer.world.blocksAtlasParser)
const textureInfo = itemsRenderer.getItemTexture(name)
if (!textureInfo) throw new Error(`Texture not found for item ${name}`)
const tex = 'type' in textureInfo ? textureInfo : textureInfo.left
const [x, y, w, h] = tex.slice
const textureThree = tex.type === 'blocks' ? viewer.world.material.map! : viewer.entities.itemsTexture!
const img = textureThree.image
const [u, v, su, sv] = [x / img.width, y / img.height, (w / img.width), (h / img.height)]
const uvInfo = {
u,
v,
su,
sv
}
return {
...uvInfo,
texture: textureThree
}
} catch (err) {
reportError?.(err)
return {
u: 0,
v: 0,
size: 16 / viewer.world.material.map!.image.width,
texture: viewer.world.material.map
}
}
})
}
viewer.entities.entitiesOptions = {
fontFamily: 'mojangles'
}
watchOptionsAfterViewerInit()
watchTexturepackInViewer(viewer)
let mouseMovePostHandle = (e) => { }
let lastMouseMove: number
@ -282,6 +288,9 @@ async function connect (connectOptions: ConnectOptions) {
miscUiState.flyingSquid = singleplayer || p2pMultiplayer
const { renderDistance: renderDistanceSingleplayer, multiplayerRenderDistance } = options
const server = cleanConnectIp(connectOptions.server, '25565')
if (connectOptions.proxy?.startsWith(':')) {
connectOptions.proxy = `${location.protocol}//${location.hostname}${connectOptions.proxy}`
}
const proxy = cleanConnectIp(connectOptions.proxy, undefined)
let { username } = connectOptions
@ -393,7 +402,7 @@ async function connect (connectOptions: ConnectOptions) {
await loadScript(`./mc-data/${toMajorVersion(version)}.js`)
miscUiState.loadedDataVersion = version
try {
await genTexturePackTextures(version)
await resourcepackOnWorldLoad(version)
} catch (err) {
console.error(err)
const doContinue = confirm('Failed to apply texture pack. See errors in the console. Continue?')
@ -401,7 +410,8 @@ async function connect (connectOptions: ConnectOptions) {
throw err
}
}
viewer.setVersion(version)
viewer.world.blockstatesModels = blockstatesModels
viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
}
const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
@ -851,13 +861,8 @@ async function connect (connectOptions: ConnectOptions) {
// todo
onGameLoad(async () => {
if (!viewer.world.downloadedBlockStatesData && !viewer.world.customBlockStatesData) {
await new Promise<void>(resolve => {
viewer.world.renderUpdateEmitter.once('blockStatesDownloaded', () => resolve())
})
}
miscUiState.serverIp = server.host as string | null
miscUiState.username = username
loadedGameState.serverIp = server.host ?? null
loadedGameState.username = username
})
if (appStatusState.isError) return

View file

@ -1,32 +1,29 @@
import { proxy, subscribe } from 'valtio'
import { showInventory } from 'minecraft-inventory-gui/web/ext.mjs'
import InventoryGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/inventory.png'
import ChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/shulker_box.png'
import LargeChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/generic_54.png'
import FurnaceGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/furnace.png'
import CraftingTableGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/crafting_table.png'
import DispenserGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/dispenser.png'
import HopperGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/hopper.png'
import HorseGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/horse.png'
import VillagerGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/villager2.png'
import EnchantingGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/enchanting_table.png'
import AnvilGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/anvil.png'
import BeaconGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/beacon.png'
import WidgetsGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png'
import InventoryGui from 'mc-assets/dist/other-textures/latest/gui/container/inventory.png'
import ChestLikeGui from 'mc-assets/dist/other-textures/latest/gui/container/shulker_box.png'
import LargeChestLikeGui from 'mc-assets/dist/other-textures/latest/gui/container/generic_54.png'
import FurnaceGui from 'mc-assets/dist/other-textures/latest/gui/container/furnace.png'
import CraftingTableGui from 'mc-assets/dist/other-textures/latest/gui/container/crafting_table.png'
import DispenserGui from 'mc-assets/dist/other-textures/latest/gui/container/dispenser.png'
import HopperGui from 'mc-assets/dist/other-textures/latest/gui/container/hopper.png'
import HorseGui from 'mc-assets/dist/other-textures/latest/gui/container/horse.png'
import VillagerGui from 'mc-assets/dist/other-textures/latest/gui/container/villager2.png'
import EnchantingGui from 'mc-assets/dist/other-textures/latest/gui/container/enchanting_table.png'
import AnvilGui from 'mc-assets/dist/other-textures/latest/gui/container/anvil.png'
import BeaconGui from 'mc-assets/dist/other-textures/latest/gui/container/beacon.png'
import WidgetsGui from 'mc-assets/dist/other-textures/latest/gui/widgets.png'
import Dirt from 'minecraft-assets/minecraft-assets/data/1.17.1/blocks/dirt.png'
// import Dirt from 'mc-assets/dist/other-textures/latest/blocks/dirt.png'
import { RecipeItem } from 'minecraft-data'
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
import itemsPng from 'prismarine-viewer/public/textures/items.png'
import itemsLegacyPng from 'prismarine-viewer/public/textures/items-legacy.png'
import _itemsAtlases from 'prismarine-viewer/public/textures/items.json'
import type { ItemsAtlasesOutputJson } from 'prismarine-viewer/viewer/prepare/genItemsAtlas'
import PrismarineBlockLoader from 'prismarine-block'
import { flat } from '@xmcl/text-component'
import mojangson from 'mojangson'
import nbt from 'prismarine-nbt'
import { splitEvery, equals } from 'rambda'
import PItem, { Item } from 'prismarine-item'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import Generic95 from '../assets/generic_95.png'
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
import invspriteJson from './invsprite.json'
@ -36,7 +33,6 @@ import { MessageFormatPart } from './botUtils'
import { currentScaling } from './scaleInterface'
import { descriptionGenerators, getItemDescription } from './itemsDescriptions'
export const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases
const loadedImagesCache = new Map<string, HTMLImageElement>()
const cleanLoadedImagesCache = () => {
loadedImagesCache.delete('blocks')
@ -63,28 +59,28 @@ export type BlockStates = Record<string, null | {
let lastWindow: ReturnType<typeof showInventory>
/** bot version */
let version: string
let PrismarineBlock: typeof PrismarineBlockLoader.Block
let PrismarineItem: typeof Item
export const allImagesLoadedState = proxy({
value: false
})
let itemsRenderer: ItemsRenderer
export const onGameLoad = (onLoad) => {
allImagesLoadedState.value = false
let loaded = 0
const onImageLoaded = () => {
loaded++
if (loaded === 3) {
onLoad?.()
allImagesLoadedState.value = true
}
}
version = bot.version
getImage({ path: 'invsprite' }, onImageLoaded)
getImage({ path: 'items' }, onImageLoaded)
getImage({ path: 'items-legacy' }, onImageLoaded)
PrismarineBlock = PrismarineBlockLoader(version)
const checkIfLoaded = () => {
if (!viewer.world.itemsAtlasParser) return
itemsRenderer = new ItemsRenderer('latest', viewer.world.blockstatesModels, viewer.world.itemsAtlasParser, viewer.world.blocksAtlasParser)
globalThis.itemsRenderer = itemsRenderer
if (allImagesLoadedState.value) return
onLoad?.()
allImagesLoadedState.value = true
}
viewer.world.renderUpdateEmitter.on('textureDownloaded', checkIfLoaded)
checkIfLoaded()
PrismarineItem = PItem(version)
bot.on('windowOpen', (win) => {
@ -144,65 +140,12 @@ export const onGameLoad = (onLoad) => {
})
}
const findTextureInBlockStates = (name) => {
assertDefined(viewer)
const blockStates: BlockStates = viewer.world.customBlockStatesData || viewer.world.downloadedBlockStatesData
const vars = blockStates[name]?.variants
if (!vars) return
let firstVar = Object.values(vars)[0]
if (Array.isArray(firstVar)) firstVar = firstVar[0]
if (!firstVar) return
const elements = firstVar.model?.elements
if (elements?.length !== 1) return
return elements[0].faces
}
const svSuToCoordinates = (path: string, u, v, su, sv = su) => {
const img = getImage({ path })!
if (!img.width) throw new Error(`Image ${path} is not loaded`)
return [u * img.width, v * img.height, su * img.width, sv * img.height]
}
const getBlockData = (name) => {
const data = findTextureInBlockStates(name)
if (!data) return
const getSpriteBlockSide = (side) => {
const d = data[side]?.texture
if (!d) return
const spriteSide = svSuToCoordinates('blocks', d.u, d.v, d.su, d.sv)
const blockSideData = {
slice: spriteSide,
path: 'blocks'
}
return blockSideData
}
return {
// todo look at grass bug
top: getSpriteBlockSide('up') || getSpriteBlockSide('top'),
left: getSpriteBlockSide('east') || getSpriteBlockSide('side'),
right: getSpriteBlockSide('north') || getSpriteBlockSide('side'),
}
}
const getInvspriteSlice = (name) => {
const invspriteImg = loadedImagesCache.get('invsprite')
if (!invspriteImg?.width) return
const { x, y } = invspriteJson[name] ?? /* unknown item */ { x: 0, y: 0 }
const sprite = [x, y, 32, 32]
return sprite
}
const getImageSrc = (path): string | HTMLImageElement => {
assertDefined(viewer)
switch (path) {
case 'gui/container/inventory': return InventoryGui
case 'blocks': return viewer.world.customTexturesDataUrl || viewer.world.downloadedTextureImage
case 'invsprite': return `invsprite.png`
case 'items': return itemsPng
case 'items-legacy': return itemsLegacyPng
case 'blocks': return viewer.world.blocksAtlasParser!.latestImage
case 'items': return viewer.world.itemsAtlasParser!.latestImage
case 'gui/container/dispenser': return DispenserGui
case 'gui/container/furnace': return FurnaceGui
case 'gui/container/crafting_table': return CraftingTableGui
@ -217,7 +160,8 @@ const getImageSrc = (path): string | HTMLImageElement => {
case 'gui/container/beacon': return BeaconGui
case 'gui/widgets': return WidgetsGui
}
return Dirt
// empty texture
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
}
const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => { }) => {
@ -240,72 +184,28 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined
return loadedImagesCache.get(loadPath)
}
const getItemVerToRender = (version: string, item: string, itemsMapSortedEntries: any[]) => {
const verNumber = versionToNumber(version)
for (const [itemsVer, items] of itemsMapSortedEntries) {
// 1.18 < 1.18.1
// 1.13 < 1.13.2
if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
return itemsVer as string
}
}
}
const isFullBlock = (block: string) => {
const blockData = loadedData.blocksByName[block]
if (!blockData) return false
const pBlock = new PrismarineBlock(blockData.id, 0, 0)
if (pBlock.shapes?.length !== 1) return false
const shape = pBlock.shapes[0]!
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
type RenderSlot = Pick<import('prismarine-item').Item, 'name' | 'displayName' | 'durabilityUsed' | 'maxDurability' | 'enchants'>
const renderSlot = (slot: RenderSlot, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => {
const renderSlot = (slot: RenderSlot, skipBlock = false): {
texture: string,
blockData?: Record<string, { slice, path }>,
scale?: number,
slice?: number[]
} | undefined => {
const itemName = slot.name
const isItem = loadedData.itemsByName[itemName]
const fullBlock = isFullBlock(itemName)
if (isItem) {
const legacyItemVersion = getItemVerToRender(version, itemName, itemsAtlases.legacyMap)
const vuToSlice = ({ u, v }, size) => [...svSuToCoordinates('items', u, v, size).slice(0, 2), 16, 16] // item size is fixed
if (legacyItemVersion) {
const textureData = itemsAtlases.legacy.textures[`${legacyItemVersion}-${itemName}`]!
return {
texture: 'items-legacy',
slice: vuToSlice(textureData, itemsAtlases.legacy.size)
}
}
const textureData = itemsAtlases.latest.textures[itemName]
if (textureData) {
return {
texture: 'items',
slice: vuToSlice(textureData, itemsAtlases.latest.size)
}
}
}
if (fullBlock && !skipBlock) {
const blockData = getBlockData(itemName)
if (blockData) {
return {
texture: 'blocks',
blockData
}
}
}
const invspriteSlice = getInvspriteSlice(itemName)
if (invspriteSlice) {
const itemTexture = itemsRenderer.getItemTexture(itemName) ?? itemsRenderer.getItemTexture('item/missing_texture')!
if ('type' in itemTexture) {
// is item
return {
texture: 'invsprite',
scale: 0.5,
slice: invspriteSlice
texture: 'items',
slice: itemTexture.slice
}
}
console.warn(`No render data for ${itemName}`)
if (isItem) {
} else {
// is block
return {
texture: 'blocks',
slice: [0, 0, 16, 16]
blockData: itemTexture
}
}
}

View file

@ -159,6 +159,7 @@ export const loadSave = async (root = '/world') => {
// improve compatibility with community saves
const rootRemapFiles = ['Warp files']
for (const rootRemapFile of rootRemapFiles) {
// eslint-disable-next-line no-await-in-loop
if (await existsViaStats(path.join(root, '..', rootRemapFile))) {
forceRedirectPaths[path.join(root, rootRemapFile)] = path.join(root, '..', rootRemapFile)
}

View file

@ -1,15 +1,16 @@
import { useState } from 'react'
import { useSnapshot } from 'valtio'
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
import { miscUiState, openOptionsMenu, showModal } from './globalState'
import { loadedGameState, miscUiState, openOptionsMenu, showModal } from './globalState'
import { AppOptions, options } from './optionsStorage'
import Button from './react/Button'
import { OptionMeta, OptionSlider } from './react/OptionsItems'
import Slider from './react/Slider'
import { getScreenRefreshRate, setLoadingScreenStatus } from './utils'
import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs'
import { getResourcePackName, resourcePackState, uninstallTexturePack } from './texturePack'
import { completeTexturePackInstall, getResourcePackNames, resourcePackState, uninstallTexturePack } from './resourcePack'
import { downloadPacketsReplay, packetsReplaceSessionState } from './packetsReplay'
import { showOptionsModal } from './react/SelectOption'
export const guiOptionsScheme: {
@ -119,12 +120,30 @@ export const guiOptionsScheme: {
{
custom () {
const { resourcePackInstalled } = useSnapshot(resourcePackState)
return <Button label={`Resource Pack... ${resourcePackInstalled ? 'ON' : 'OFF'}`} inScreen onClick={async () => {
const { usingServerResourcePack } = useSnapshot(loadedGameState)
const { enabledResourcepack } = useSnapshot(options)
return <Button label={`Resource Pack: ${usingServerResourcePack ? 'SERVER ON' : resourcePackInstalled ? enabledResourcepack ? 'ON' : 'OFF' : 'NO'}`} inScreen onClick={async () => {
if (resourcePackState.resourcePackInstalled) {
const resourcePackName = await getResourcePackName()
if (confirm(`Uninstall ${resourcePackName} resource pack?`)) {
const names = Object.keys(await getResourcePackNames())
const name = names[0]
const choices = [
options.enabledResourcepack ? 'Disable' : 'Enable',
'Uninstall',
]
const choice = await showOptionsModal(`Resource Pack ${name} action`, choices)
if (!choice) return
if (choice === 'Disable') {
options.enabledResourcepack = null
return
}
if (choice === 'Enable') {
options.enabledResourcepack = name
await completeTexturePackInstall(name, name)
return
}
if (choice === 'Uninstall') {
// todo make hidable
setLoadingScreenStatus('Uninstalling texturepack...')
setLoadingScreenStatus('Uninstalling texturepack')
await uninstallTexturePack()
setLoadingScreenStatus(undefined)
}

View file

@ -43,6 +43,10 @@ const defaultOptions = {
loadPlayerSkins: true,
lowMemoryMode: false,
starfieldRendering: true,
enabledResourcepack: null as string | null,
useVersionsTextures: 'latest',
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
// antiAliasing: false,
showChunkBorders: false, // todo rename option

View file

@ -5,7 +5,7 @@ import fs from 'fs'
import * as THREE from 'three'
import { subscribeKey } from 'valtio/utils'
import { EntityMesh } from 'prismarine-viewer/viewer/lib/entity/EntityMesh'
import { fromTexturePackPath, resourcePackState } from './texturePack'
import { fromTexturePackPath, resourcePackState } from './resourcePack'
import { options, watchValue } from './optionsStorage'
import { miscUiState } from './globalState'
@ -27,18 +27,20 @@ const possiblyLoadPanoramaFromResourcePack = async (file) => {
let base64Texture
if (panoramaUsesResourcePack) {
try {
base64Texture = await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, file)), 'base64')
// TODO!
// base64Texture = await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, file)), 'base64')
} catch (err) {
panoramaUsesResourcePack = false
}
}
if (base64Texture) return `data:image/png;base64,${base64Texture}`
else return join('extra-textures/background', file)
else return join('background', file)
}
const updateResourcePackSupportPanorama = async () => {
try {
await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, panoramaFiles[0])), 'base64')
// TODO!
// await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, panoramaFiles[0])), 'base64')
panoramaUsesResourcePack = true
} catch (err) {
panoramaUsesResourcePack = false

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import icons from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/icons.png'
import icons from 'mc-assets/dist/other-textures/latest/gui/icons.png'
import ArmorBar from './ArmorBar'

38
src/react/Book.module.css.d.ts vendored Normal file
View file

@ -0,0 +1,38 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
actions: string;
blink: string;
bookContainer: string;
bookImages: string;
bookWrapper: string;
controlNext: string;
controlPrev: string;
hidden: string;
inside: string;
insideAnimation: string;
insideAnimationReverse: string;
insideHalfIcon: string;
insideIcon: string;
messageFormattedString: string;
outSide: string;
page: string;
pageAnimation: string;
pageAnimationReverse: string;
pageButtonAnimationReverse: string;
pageSecondTextAnimation: string;
pageSecondTextAnimationReverse: string;
pageTextAnimation: string;
pageTextAnimationReverse: string;
text: string;
textArea: string;
titleAnimation: string;
titleAnimationReverse: string;
titleContent: string;
titleContentAnimation: string;
titleContentAnimationReverse: string;
titleIcon: string;
uneditable: string;
}
declare const cssExports: CssExports;
export default cssExports;

View file

@ -197,7 +197,7 @@ const Book: React.FC<BookProps> = ({ textPages, editable, onSign, onEdit, onClos
maxLength={500}
/>
) : (
<div className={getAnimationClass(animatePageIcon, styles.pageText)}>
<div className={getAnimationClass(animatePageIcon, '')}>
<MessageFormattedString message={pages[index]} fallbackColor='black' className={styles.messageFormattedString} />
</div>
)}
@ -286,7 +286,7 @@ const Book: React.FC<BookProps> = ({ textPages, editable, onSign, onEdit, onClos
}}>
<input
ref={inputRef}
className={`${styles.inputTitle}`}
className={''}
/>
{/* for some reason this is needed to make Enter work on android chrome */}
<button type='submit' style={{ visibility: 'hidden', height: 0, width: 0 }} />

View file

@ -18,7 +18,7 @@
color: #fff;
}
.bossbar {
background-image: url("minecraft-assets/minecraft-assets/data/1.18.1/gui/bars.png");
background-image: var(--bars-gui-atlas);
width: 182px;
height: 5px;
position: relative;
@ -30,5 +30,5 @@
left: 0;
height: 5px;
width: 0;
background-image: url("minecraft-assets/minecraft-assets/data/1.18.1/gui/bars.png");
background-image: var(--bars-gui-atlas);
}

View file

@ -1,7 +1,8 @@
import { loadSound, playSound } from '../basicSounds'
import buttonClickMp3 from '../../assets/button_click.mp3'
import { ButtonProvider } from './Button'
void loadSound('button_click.mp3')
void loadSound('button_click.mp3', buttonClickMp3)
export default ({ children }) => {
return <ButtonProvider onClick={() => {

View file

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { useSnapshot } from 'valtio'
import { formatMessage } from '../botUtils'
import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands'
import { hideCurrentModal, miscUiState } from '../globalState'
import { hideCurrentModal, loadedGameState, miscUiState } from '../globalState'
import { options } from '../optionsStorage'
import Chat, { Message, fadeMessage } from './Chat'
import { useIsModalActive } from './utilsApp'
@ -53,7 +53,7 @@ export default () => {
updateLoadedServerData((server) => {
server.autoLogin ??= {}
const password = message.split(' ')[1]
server.autoLogin[miscUiState.username] = password
server.autoLogin[loadedGameState.username] = password
return server
})
hideNotification()

11
src/react/DebugOverlay.module.css.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'debug-left-side': string;
'debug-right-side': string;
debugLeftSide: string;
debugRightSide: string;
empty: string;
}
declare const cssExports: CssExports;
export default cssExports;

View file

@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
container: string;
main: string;
}
declare const cssExports: CssExports;
export default cssExports;

View file

@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
crosshair: string;
}
declare const cssExports: CssExports;
export default cssExports;

View file

@ -111,7 +111,7 @@ export default () => {
inv.canvas.style.pointerEvents = 'auto'
container.current.appendChild(inv.canvas)
const upHotbarItems = () => {
if (!viewer.world.downloadedTextureImage || !viewer.world.downloadedBlockStatesData || !allImagesLoadedState.value) return
if (!viewer.world.currentTextureImage || !allImagesLoadedState.value) return
upInventoryItems(true, inv)
}
@ -126,7 +126,6 @@ export default () => {
upHotbarItems()
bot.inventory.on('updateSlot', upHotbarItems)
viewer.world.renderUpdateEmitter.on('textureDownloaded', upHotbarItems)
viewer.world.renderUpdateEmitter.on('blockStatesDownloaded', upHotbarItems)
const unsub2 = subscribe(allImagesLoadedState, () => {
upHotbarItems()
})
@ -200,7 +199,6 @@ export default () => {
controller.abort()
unsub2()
viewer.world.renderUpdateEmitter.off('textureDownloaded', upHotbarItems)
viewer.world.renderUpdateEmitter.off('blockStatesDownloaded', upHotbarItems)
}
}, [])

View file

@ -0,0 +1,27 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
actionBinds: string;
actionName: string;
button: string;
'chat-command': string;
chatCommand: string;
container: string;
group: string;
'group-category': string;
groupCategory: string;
'margin-left': string;
marginLeft: string;
'matched-bind': string;
'matched-bind-warning': string;
matchedBind: string;
matchedBindWarning: string;
'undo-gamepad': string;
'undo-keyboard': string;
undoGamepad: string;
undoKeyboard: string;
'warning-container': string;
warningContainer: string;
}
declare const cssExports: CssExports;
export default cssExports;

Some files were not shown because too many files have changed in this diff Show more