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
|
|
@ -1 +1,4 @@
|
|||
node_modules
|
||||
rsbuild.config.ts
|
||||
*.module.css.d.ts
|
||||
generated
|
||||
|
|
|
|||
4
.vscode/launch.json
vendored
|
|
@ -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*"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 859 KiB After Width: | Height: | Size: 859 KiB |
|
Before Width: | Height: | Size: 952 KiB After Width: | Height: | Size: 952 KiB |
|
Before Width: | Height: | Size: 704 KiB After Width: | Height: | Size: 704 KiB |
|
Before Width: | Height: | Size: 684 KiB After Width: | Height: | Size: 684 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
BIN
assets/destroy_stage_0.png
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
assets/destroy_stage_1.png
Normal file
|
After Width: | Height: | Size: 115 B |
BIN
assets/destroy_stage_2.png
Normal file
|
After Width: | Height: | Size: 123 B |
BIN
assets/destroy_stage_3.png
Normal file
|
After Width: | Height: | Size: 145 B |
BIN
assets/destroy_stage_4.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
assets/destroy_stage_5.png
Normal file
|
After Width: | Height: | Size: 169 B |
BIN
assets/destroy_stage_6.png
Normal file
|
After Width: | Height: | Size: 177 B |
BIN
assets/destroy_stage_7.png
Normal file
|
After Width: | Height: | Size: 190 B |
BIN
assets/destroy_stage_8.png
Normal file
|
After Width: | Height: | Size: 211 B |
BIN
assets/destroy_stage_9.png
Normal file
|
After Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
140
esbuild.mjs
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
42
index.html
|
|
@ -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'>
|
||||
|
|
|
|||
37
package.json
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
16
patches/mineflayer-item-map-downloader@1.2.0.patch
Normal 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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
|||
1
prismarine-viewer/examples/examples/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as rotation } from './rotation'
|
||||
9
prismarine-viewer/examples/examples/rotation.ts
Normal 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
|
||||
6
prismarine-viewer/examples/examples/type.ts
Normal 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
|
||||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
38
prismarine-viewer/index.d.ts
vendored
|
|
@ -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';
|
||||
|
|
@ -3,5 +3,4 @@ module.exports = {
|
|||
standalone: require('./lib/standalone'),
|
||||
headless: require('./lib/headless'),
|
||||
viewer: require('./viewer'),
|
||||
supportedVersions: require('./viewer/supportedVersions.json')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
launch: {
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
module.exports = {
|
||||
preset: 'jest-puppeteer',
|
||||
testRegex: './*\\.test\\.js$'
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
142
prismarine-viewer/viewer/lib/mesher/modelsGeometryCommon.ts
Normal 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]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
273
prismarine-viewer/viewer/lib/mesher/standaloneRenderer.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
@ -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')
|
||||
})
|
||||
|
Before Width: | Height: | Size: 339 B |
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
|
@ -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',
|
||||
// },
|
||||
},
|
||||
})
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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')
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
30
server.js
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
99
src/index.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
@ -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 }} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
8
src/react/DiveTransition.module.css.d.ts
vendored
Normal 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;
|
||||
7
src/react/GamepadUiCursor.module.css.d.ts
vendored
Normal 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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
27
src/react/KeybindingsScreen.module.css.d.ts
vendored
Normal 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;
|
||||