Compare commits
18 commits
next
...
optimize-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79394d72b6 | ||
|
|
d3c592930f | ||
|
|
8e31499257 | ||
|
|
037b4b48e2 | ||
|
|
294c69f3b5 | ||
|
|
32da6f4ce5 | ||
|
|
2203a8a32b | ||
|
|
25cfbf4588 | ||
|
|
a6c288aa22 | ||
|
|
1e20f98d16 | ||
|
|
6fde5cac9b | ||
|
|
1d25593574 | ||
|
|
cd277169f4 | ||
|
|
7247a47e64 | ||
|
|
c90b7fdea6 | ||
|
|
7c68ead81b | ||
|
|
d1aa155b79 | ||
|
|
f7554676fd |
21 changed files with 1787 additions and 151 deletions
|
|
@ -14,7 +14,7 @@ const compareRenderedFlatWorld = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const testWorldLoad = () => {
|
const testWorldLoad = () => {
|
||||||
return cy.document().then({ timeout: 25_000 }, doc => {
|
return cy.document().then({ timeout: 35_000 }, doc => {
|
||||||
return new Cypress.Promise(resolve => {
|
return new Cypress.Promise(resolve => {
|
||||||
doc.addEventListener('cypress-world-ready', resolve)
|
doc.addEventListener('cypress-world-ready', resolve)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
1
experiments/decode.html
Normal file
1
experiments/decode.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<script src="decode.ts" type="module"></script>
|
||||||
26
experiments/decode.ts
Normal file
26
experiments/decode.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Include the pako library
|
||||||
|
import pako from 'pako';
|
||||||
|
import compressedJsRaw from './compressed.js?raw'
|
||||||
|
|
||||||
|
function decompressFromBase64(input) {
|
||||||
|
// Decode the Base64 string
|
||||||
|
const binaryString = atob(input);
|
||||||
|
const len = binaryString.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
|
||||||
|
// Convert the binary string to a byte array
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompress the byte array
|
||||||
|
const decompressedData = pako.inflate(bytes, { to: 'string' });
|
||||||
|
|
||||||
|
return decompressedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the function
|
||||||
|
console.time('decompress');
|
||||||
|
const decompressedData = decompressFromBase64(compressedJsRaw);
|
||||||
|
console.timeEnd('decompress')
|
||||||
|
console.log(decompressedData)
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"filesize": "^10.0.12",
|
"filesize": "^10.0.12",
|
||||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.35",
|
"flying-squid": "npm:@zardoy/flying-squid@^0.0.36",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
|
|
||||||
1021
pnpm-lock.yaml
generated
1021
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,20 +6,19 @@ import * as esbuild from 'esbuild'
|
||||||
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
||||||
import path, { dirname, join } from 'path'
|
import path, { dirname, join } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
import childProcess from 'child_process'
|
||||||
|
import supportedVersions from '../src/supportedVersions.mjs'
|
||||||
|
|
||||||
const dev = process.argv.includes('-w')
|
const dev = process.argv.includes('-w')
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
||||||
|
|
||||||
const mcDataPath = join(__dirname, '../dist/mc-data')
|
const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json')
|
||||||
if (!fs.existsSync(mcDataPath)) {
|
if (!fs.existsSync(mcDataPath)) {
|
||||||
// shouldn't it be in the viewer instead?
|
childProcess.execSync('tsx ../scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: __dirname })
|
||||||
await import('../scripts/prepareData.mjs')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.copyFileSync(join(__dirname, 'playground.html'), join(__dirname, 'public/index.html'))
|
fs.copyFileSync(join(__dirname, 'playground.html'), join(__dirname, 'public/index.html'))
|
||||||
fsExtra.copySync(mcDataPath, join(__dirname, 'public/mc-data'))
|
|
||||||
const availableVersions = fs.readdirSync(mcDataPath).map(ver => ver.replace('.js', ''))
|
|
||||||
|
|
||||||
/** @type {import('esbuild').BuildOptions} */
|
/** @type {import('esbuild').BuildOptions} */
|
||||||
const buildOptions = {
|
const buildOptions = {
|
||||||
|
|
@ -37,7 +36,7 @@ const buildOptions = {
|
||||||
],
|
],
|
||||||
keepNames: true,
|
keepNames: true,
|
||||||
banner: {
|
banner: {
|
||||||
js: `globalThis.global = globalThis;globalThis.includedVersions = ${JSON.stringify(availableVersions)};`,
|
js: `globalThis.global = globalThis;globalThis.includedVersions = ${JSON.stringify(supportedVersions)};`,
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
events: 'events',
|
events: 'events',
|
||||||
|
|
@ -63,13 +62,14 @@ const buildOptions = {
|
||||||
}, () => {
|
}, () => {
|
||||||
const defaultVersionsObj = {}
|
const defaultVersionsObj = {}
|
||||||
return {
|
return {
|
||||||
contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`,
|
contents: fs.readFileSync(join(__dirname, '../src/shims/minecraftData.ts'), 'utf8'),
|
||||||
loader: 'js',
|
loader: 'ts',
|
||||||
|
resolveDir: join(__dirname, '../src/shims'),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
build.onEnd((e) => {
|
build.onEnd((e) => {
|
||||||
if (e.errors.length) return
|
if (e.errors.length) return
|
||||||
// fs.writeFileSync(join(__dirname, 'dist/metafile.json'), JSON.stringify(e.metafile), 'utf8')
|
fs.writeFileSync(join(__dirname, './public/metafile.json'), JSON.stringify(e.metafile), 'utf8')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { loadScript } from '../viewer/lib/utils'
|
||||||
import { TWEEN_DURATION } from '../viewer/lib/entities'
|
import { TWEEN_DURATION } from '../viewer/lib/entities'
|
||||||
import { EntityMesh } from '../viewer/lib/entity/EntityMesh'
|
import { EntityMesh } from '../viewer/lib/entity/EntityMesh'
|
||||||
import { WorldDataEmitter, Viewer } from '../viewer'
|
import { WorldDataEmitter, Viewer } from '../viewer'
|
||||||
|
import '../../src/getCollisionShapes'
|
||||||
import { toMajorVersion } from '../../src/utils'
|
import { toMajorVersion } from '../../src/utils'
|
||||||
|
|
||||||
window.THREE = THREE
|
window.THREE = THREE
|
||||||
|
|
@ -65,21 +66,21 @@ async function main () {
|
||||||
let continuousRender = false
|
let continuousRender = false
|
||||||
|
|
||||||
const { version } = params
|
const { version } = params
|
||||||
|
await window._LOAD_MC_DATA()
|
||||||
// temporary solution until web worker is here, cache data for faster reloads
|
// temporary solution until web worker is here, cache data for faster reloads
|
||||||
const globalMcData = window['mcData']
|
// const globalMcData = window['mcData']
|
||||||
if (!globalMcData['version']) {
|
// if (!globalMcData['version']) {
|
||||||
const major = toMajorVersion(version)
|
// const major = toMajorVersion(version)
|
||||||
const sessionKey = `mcData-${major}`
|
// const sessionKey = `mcData-${major}`
|
||||||
if (sessionStorage[sessionKey]) {
|
// if (sessionStorage[sessionKey]) {
|
||||||
Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey]))
|
// Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey]))
|
||||||
} else {
|
// } else {
|
||||||
if (sessionStorage.length > 1) sessionStorage.clear()
|
// if (sessionStorage.length > 1) sessionStorage.clear()
|
||||||
await loadScript(`./mc-data/${major}.js`)
|
// try {
|
||||||
try {
|
// sessionStorage[sessionKey] = JSON.stringify(Object.fromEntries(Object.entries(globalMcData).filter(([ver]) => ver.startsWith(major))))
|
||||||
sessionStorage[sessionKey] = JSON.stringify(Object.fromEntries(Object.entries(globalMcData).filter(([ver]) => ver.startsWith(major))))
|
// } catch { }
|
||||||
} catch { }
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
const mcData: IndexedData = require('minecraft-data')(version)
|
const mcData: IndexedData = require('minecraft-data')(version)
|
||||||
window['loadedData'] = mcData
|
window['loadedData'] = mcData
|
||||||
|
|
|
||||||
|
|
@ -38,5 +38,8 @@
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"node-canvas-webgl": "^0.3.0"
|
"node-canvas-webgl": "^0.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"live-server": "^1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import mcDataRaw from 'minecraft-data/data.js' // handled correctly in esbuild plugin
|
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
|
||||||
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
|
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
|
||||||
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
|
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
|
||||||
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
||||||
|
|
@ -223,8 +223,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
|
|
||||||
sendMesherMcData () {
|
sendMesherMcData () {
|
||||||
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(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)))
|
const mcData = {
|
||||||
mcData.version = JSON.parse(JSON.stringify(mcData.version))
|
version: JSON.parse(JSON.stringify(allMcData.version))
|
||||||
|
}
|
||||||
|
for (const key of dynamicMcDataFiles) {
|
||||||
|
mcData[key] = allMcData[key]
|
||||||
|
}
|
||||||
|
|
||||||
for (const worker of this.workers) {
|
for (const worker of this.workers) {
|
||||||
worker.postMessage({ type: 'mcData', mcData, config: this.mesherConfig })
|
worker.postMessage({ type: 'mcData', mcData, config: this.mesherConfig })
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,10 @@ export default defineConfig({
|
||||||
const prep = async () => {
|
const prep = async () => {
|
||||||
console.time('total-prep')
|
console.time('total-prep')
|
||||||
fs.mkdirSync('./generated', { recursive: true })
|
fs.mkdirSync('./generated', { recursive: true })
|
||||||
if (!fs.existsSync('./generated/minecraft-data-data.js')) {
|
if (!fs.existsSync('./generated/minecraft-data-optimized.json') || require('./generated/minecraft-data-optimized.json').versionKey !== require('minecraft-data/package.json').version) {
|
||||||
childProcess.execSync('tsx ./scripts/genShims.ts', { stdio: 'inherit' })
|
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit' })
|
||||||
}
|
}
|
||||||
|
childProcess.execSync('tsx ./scripts/genShims.ts', { stdio: 'inherit' })
|
||||||
if (!fs.existsSync('./generated/latestBlockCollisionsShapes.json')) {
|
if (!fs.existsSync('./generated/latestBlockCollisionsShapes.json')) {
|
||||||
childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' })
|
childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' })
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +118,6 @@ export default defineConfig({
|
||||||
configJson.defaultProxy = ':8080'
|
configJson.defaultProxy = ':8080'
|
||||||
}
|
}
|
||||||
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
|
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('./scripts/prepareSounds.mjs', { stdio: 'inherit' })
|
||||||
// childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
|
// childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
|
||||||
// childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
|
// childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
|
||||||
|
|
@ -164,7 +164,7 @@ export default defineConfig({
|
||||||
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
||||||
}
|
}
|
||||||
if (absolute.endsWith('/minecraft-data/data.js')) {
|
if (absolute.endsWith('/minecraft-data/data.js')) {
|
||||||
resource.request = path.join(__dirname, './generated/minecraft-data-data.js')
|
resource.request = path.join(__dirname, './src/shims/minecraftData.ts')
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
addRules([
|
addRules([
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ exports.getSwAdditionalEntries = () => {
|
||||||
// need to be careful with this
|
// need to be careful with this
|
||||||
const filesToCachePatterns = [
|
const filesToCachePatterns = [
|
||||||
'index.html',
|
'index.html',
|
||||||
`mc-data/${defaultLocalServerOptions.versionMajor}.js`,
|
|
||||||
'background/**',
|
'background/**',
|
||||||
// todo-low copy from assets
|
// todo-low copy from assets
|
||||||
'*.mp3',
|
'*.mp3',
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,7 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import MinecraftData from 'minecraft-data'
|
|
||||||
import MCProtocol from 'minecraft-protocol'
|
|
||||||
import { appReplacableResources } from '../src/resourcesSource'
|
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.mkdirSync('./generated', { recursive: true })
|
fs.mkdirSync('./generated', { recursive: true })
|
||||||
fs.writeFileSync('./generated/minecraft-data-data.js', mcDataContents, 'utf8')
|
|
||||||
|
|
||||||
// app resources
|
// app resources
|
||||||
|
|
||||||
|
|
|
||||||
230
scripts/makeOptimizedMcData.mjs
Normal file
230
scripts/makeOptimizedMcData.mjs
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
//@ts-check
|
||||||
|
import { build } from 'esbuild'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import Module from "node:module"
|
||||||
|
import { dirname } from 'node:path'
|
||||||
|
import supportedVersions from '../src/supportedVersions.mjs'
|
||||||
|
import { gzipSizeFromFileSync } from 'gzip-size'
|
||||||
|
import fs from 'fs'
|
||||||
|
import {default as _JsonOptimizer} from '../src/optimizeJson'
|
||||||
|
import { gzipSync } from 'zlib';
|
||||||
|
import MinecraftData from 'minecraft-data'
|
||||||
|
import MCProtocol from 'minecraft-protocol'
|
||||||
|
|
||||||
|
/** @type {typeof _JsonOptimizer} */
|
||||||
|
//@ts-ignore
|
||||||
|
const JsonOptimizer = _JsonOptimizer.default
|
||||||
|
|
||||||
|
// console.log(a.diff_main(JSON.stringify({ a: 1 }), JSON.stringify({ a: 1, b: 2 })))
|
||||||
|
|
||||||
|
const require = Module.createRequire(import.meta.url)
|
||||||
|
|
||||||
|
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
|
||||||
|
|
||||||
|
function toMajor (version) {
|
||||||
|
const [a, b] = (version + '').split('.')
|
||||||
|
return `${a}.${b}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = {}
|
||||||
|
const dataTypes = new Set()
|
||||||
|
|
||||||
|
for (const [version, dataSet] of Object.entries(dataPaths.pc)) {
|
||||||
|
if (!supportedVersions.includes(version)) continue
|
||||||
|
for (const type of Object.keys(dataSet)) {
|
||||||
|
dataTypes.add(type)
|
||||||
|
}
|
||||||
|
versions[version] = dataSet
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionToNumber = (ver) => {
|
||||||
|
const [x, y = '0', z = '0'] = ver.split('.')
|
||||||
|
return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not included here (even as {}) will not be bundled & accessible!
|
||||||
|
const compressedOutput = false
|
||||||
|
// const dataTypeBundling = {
|
||||||
|
// protocol: {
|
||||||
|
// // ignoreRemoved: true,
|
||||||
|
// // ignoreChanges: true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
const dataTypeBundling = {
|
||||||
|
language: {
|
||||||
|
ignoreRemoved: true,
|
||||||
|
ignoreChanges: true
|
||||||
|
},
|
||||||
|
blocks: {
|
||||||
|
arrKey: 'name',
|
||||||
|
// ignoreRemoved: true,
|
||||||
|
// genChanges (source, diff) {
|
||||||
|
// const diffs = {}
|
||||||
|
// const newItems = {}
|
||||||
|
// for (const [key, val] of Object.entries(diff)) {
|
||||||
|
// const src = source[key]
|
||||||
|
// if (!src) {
|
||||||
|
// newItems[key] = val
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// const { minStateId, defaultState, maxStateId } = val
|
||||||
|
// if (defaultState === undefined || minStateId === src.minStateId || maxStateId === src.maxStateId || defaultState === src.defaultState) continue
|
||||||
|
// diffs[key] = [minStateId, defaultState, maxStateId]
|
||||||
|
// }
|
||||||
|
// return {
|
||||||
|
// stateChanges: diffs
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ignoreChanges: true
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
particles: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
enchantments: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
instruments: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
foods: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
entities: {
|
||||||
|
arrKey: 'id+type'
|
||||||
|
},
|
||||||
|
materials: {},
|
||||||
|
windows: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
raw: true
|
||||||
|
},
|
||||||
|
tints: {},
|
||||||
|
biomes: {
|
||||||
|
arrKey: 'name'
|
||||||
|
},
|
||||||
|
entityLoot: {
|
||||||
|
arrKey: 'entity'
|
||||||
|
},
|
||||||
|
blockLoot: {
|
||||||
|
arrKey: 'block'
|
||||||
|
},
|
||||||
|
recipes: {}, // todo we can do better
|
||||||
|
blockCollisionShapes: {},
|
||||||
|
loginPacket: {},
|
||||||
|
protocol: {
|
||||||
|
raw: true
|
||||||
|
},
|
||||||
|
sounds: {
|
||||||
|
arrKey: 'name'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notBundling = [...dataTypes.keys()].filter(x => !Object.keys(dataTypeBundling).includes(x))
|
||||||
|
console.log("Not bundling minecraft-data data:", notBundling)
|
||||||
|
|
||||||
|
let previousData = {}
|
||||||
|
// /** @type {Record<string, JsonOptimizer>} */
|
||||||
|
const diffSources = {}
|
||||||
|
const versionsArr = Object.entries(versions)
|
||||||
|
const sizePerDataType = {}
|
||||||
|
const rawDataVersions = {}
|
||||||
|
// const versionsArr = Object.entries(versions).slice(-1)
|
||||||
|
for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) {
|
||||||
|
for (const [dataType, dataPath] of Object.entries(dataSet)) {
|
||||||
|
const config = dataTypeBundling[dataType]
|
||||||
|
if (!config) continue
|
||||||
|
if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) {
|
||||||
|
// contents += ` get ${dataType} () { return window.globalGetCollisionShapes?.("${version}") },\n`
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const loc = `minecraft-data/data/${dataPath}/`
|
||||||
|
const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${dataType}`)
|
||||||
|
// const data = fs.readFileSync(dataPathAbsolute, 'utf8')
|
||||||
|
const dataRaw = require(dataPathAbsolute)
|
||||||
|
let injectCode = ''
|
||||||
|
let rawData = dataRaw
|
||||||
|
if (config.raw) {
|
||||||
|
rawDataVersions[dataType] ??= {}
|
||||||
|
rawDataVersions[dataType][version] = rawData
|
||||||
|
rawData = dataRaw
|
||||||
|
} else {
|
||||||
|
if (!diffSources[dataType]) {
|
||||||
|
diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
diffSources[dataType].recordDiff(version, dataRaw)
|
||||||
|
injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})`
|
||||||
|
} catch (err) {
|
||||||
|
const error = new Error(`Failed to diff ${dataType} for ${version}: ${err.message}`)
|
||||||
|
error.stack = err.stack
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sizePerDataType[dataType] ??= 0
|
||||||
|
sizePerDataType[dataType] += Buffer.byteLength(JSON.stringify(injectCode || rawData), 'utf8')
|
||||||
|
if (config.genChanges && previousData[dataType]) {
|
||||||
|
const changes = config.genChanges(previousData[dataType], dataRaw)
|
||||||
|
// Object.assign(data, changes)
|
||||||
|
}
|
||||||
|
previousData[dataType] = dataRaw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sources = Object.fromEntries(Object.entries(diffSources).map(x => {
|
||||||
|
const data = x[1].export()
|
||||||
|
// const data = {}
|
||||||
|
sizePerDataType[x[0]] += Buffer.byteLength(JSON.stringify(data), 'utf8')
|
||||||
|
return [x[0], data]
|
||||||
|
}))
|
||||||
|
Object.assign(sources, rawDataVersions)
|
||||||
|
sources.versionKey = require('minecraft-data/package.json').version
|
||||||
|
|
||||||
|
const totalSize = Object.values(sizePerDataType).reduce((acc, val) => acc + val, 0)
|
||||||
|
console.log('total size (mb)', totalSize / 1024 / 1024)
|
||||||
|
console.log(
|
||||||
|
'size per data type (mb, %)',
|
||||||
|
Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => {
|
||||||
|
return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]];
|
||||||
|
}).sort((a, b) => {
|
||||||
|
//@ts-ignore
|
||||||
|
return b[1][1] - a[1][1];
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
function compressToBase64(input) {
|
||||||
|
const buffer = gzipSync(input);
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = './generated/minecraft-data-optimized.json'
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(sources), 'utf8')
|
||||||
|
if (compressedOutput) {
|
||||||
|
const minizedCompressed = compressToBase64(fs.readFileSync(filePath))
|
||||||
|
console.log('size of compressed', Buffer.byteLength(minizedCompressed, 'utf8') / 1000 / 1000)
|
||||||
|
const compressedFilePath = './experiments/compressed.js'
|
||||||
|
fs.writeFileSync(compressedFilePath, minizedCompressed, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileSync(filePath) / 1000 / 1000)
|
||||||
|
|
||||||
|
// always bundled
|
||||||
|
|
||||||
|
const { defaultVersion } = MCProtocol
|
||||||
|
const data = MinecraftData(defaultVersion)
|
||||||
|
const initialMcData = {
|
||||||
|
[defaultVersion]: {
|
||||||
|
version: data.version,
|
||||||
|
protocol: data.protocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('./generated/minecraft-initial-data.json', JSON.stringify(initialMcData), 'utf8')
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
//@ts-check
|
|
||||||
import { build } from 'esbuild'
|
|
||||||
import { existsSync } from 'node:fs'
|
|
||||||
import Module from "node:module"
|
|
||||||
import { dirname } from 'node:path'
|
|
||||||
import supportedVersions from '../src/supportedVersions.mjs'
|
|
||||||
|
|
||||||
if (existsSync('dist/mc-data') && !process.argv.includes('-f')) {
|
|
||||||
console.log('using cached prepared data')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const require = Module.createRequire(import.meta.url)
|
|
||||||
|
|
||||||
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
|
|
||||||
|
|
||||||
function toMajor (version) {
|
|
||||||
const [a, b] = (version + '').split('.')
|
|
||||||
return `${a}.${b}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const grouped = {}
|
|
||||||
|
|
||||||
for (const [version, data] of Object.entries(dataPaths.pc)) {
|
|
||||||
if (!supportedVersions.includes(version)) continue
|
|
||||||
const major = toMajor(version)
|
|
||||||
grouped[major] ??= {}
|
|
||||||
grouped[major][version] = data
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionToNumber = (ver) => {
|
|
||||||
const [x, y = '0', z = '0'] = ver.split('.')
|
|
||||||
return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('preparing data')
|
|
||||||
console.time('data prepared')
|
|
||||||
let builds = []
|
|
||||||
for (const [major, versions] of Object.entries(grouped)) {
|
|
||||||
// if (major !== '1.19') continue
|
|
||||||
let contents = 'Object.assign(window.mcData, {\n'
|
|
||||||
for (const [version, dataSet] of Object.entries(versions)) {
|
|
||||||
contents += ` '${version}': {\n`
|
|
||||||
for (const [dataType, dataPath] of Object.entries(dataSet)) {
|
|
||||||
if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) {
|
|
||||||
contents += ` get ${dataType} () { return window.globalGetCollisionShapes?.("${version}") },\n`
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const loc = `minecraft-data/data/${dataPath}/`
|
|
||||||
contents += ` get ${dataType} () { return require("./${loc}${dataType}.json") },\n`
|
|
||||||
}
|
|
||||||
contents += ' },\n'
|
|
||||||
}
|
|
||||||
contents += '})'
|
|
||||||
|
|
||||||
const promise = build({
|
|
||||||
bundle: true,
|
|
||||||
outfile: `dist/mc-data/${major}.js`,
|
|
||||||
stdin: {
|
|
||||||
contents,
|
|
||||||
|
|
||||||
resolveDir: dirname(require.resolve('minecraft-data')),
|
|
||||||
sourcefile: `mcData${major}.js`,
|
|
||||||
loader: 'js',
|
|
||||||
},
|
|
||||||
metafile: true,
|
|
||||||
})
|
|
||||||
// require('fs').writeFileSync('dist/mc-data/metafile.json', JSON.stringify(promise.metafile), 'utf8')
|
|
||||||
builds.push(promise)
|
|
||||||
}
|
|
||||||
await Promise.all(builds)
|
|
||||||
console.timeEnd('data prepared')
|
|
||||||
99
scripts/testOptimizedMcdata.ts
Normal file
99
scripts/testOptimizedMcdata.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import JsonOptimizer from '../src/optimizeJson';
|
||||||
|
import fs from 'fs'
|
||||||
|
import minecraftData from 'minecraft-data'
|
||||||
|
|
||||||
|
const json = JSON.parse(fs.readFileSync('./generated/minecraft-data-optimized.json', 'utf8'))
|
||||||
|
|
||||||
|
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
|
||||||
|
|
||||||
|
const validateData = (ver, type) => {
|
||||||
|
const target = JsonOptimizer.restoreData(json[type], ver)
|
||||||
|
const arrKey = json[type].arrKey
|
||||||
|
const originalPath = dataPaths.pc[ver][type]
|
||||||
|
const original = require(`minecraft-data/minecraft-data/data/${originalPath}/${type}.json`)
|
||||||
|
if (arrKey) {
|
||||||
|
const originalKeys = original.map(a => JsonOptimizer.getByArrKey(a, arrKey)) as string[]
|
||||||
|
for (const [i, item] of originalKeys.entries()) {
|
||||||
|
if (originalKeys.indexOf(item) !== i) {
|
||||||
|
console.warn(`${type} ${ver} Incorrect source, duplicated arrKey (${arrKey}) ${item}. Ignoring!`) // todo should span instead
|
||||||
|
const index = originalKeys.indexOf(item);
|
||||||
|
original.splice(index, 1)
|
||||||
|
originalKeys.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (target.length !== originalKeys.length) {
|
||||||
|
// throw new Error(`wrong arr length: ${target.length} !== ${original.length}`)
|
||||||
|
// }
|
||||||
|
checkKeys(originalKeys, target.map(a => JsonOptimizer.getByArrKey(a, arrKey)))
|
||||||
|
for (const item of target as any[]) {
|
||||||
|
const keys = Object.entries(item).map(a => a[0])
|
||||||
|
const origItem = original.find(a => JsonOptimizer.getByArrKey(a, arrKey) === JsonOptimizer.getByArrKey(item, arrKey));
|
||||||
|
const keysSource = Object.entries(origItem).map(a => a[0])
|
||||||
|
checkKeys(keysSource, keys, true, 'prop keys', true)
|
||||||
|
checkObj(origItem, item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keysOriginal = Object.keys(original)
|
||||||
|
const keysTarget = Object.keys(target)
|
||||||
|
checkKeys(keysOriginal, keysTarget)
|
||||||
|
for (const key of keysTarget) {
|
||||||
|
checkObj(original[key], target[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkObj = (source, diffing) => {
|
||||||
|
checkKeys(Object.keys(source), Object.keys(diffing))
|
||||||
|
for (const [key, val] of Object.entries(source)) {
|
||||||
|
if (JSON.stringify(val) !== JSON.stringify(diffing[key])) {
|
||||||
|
throw new Error(`different value of ${key}: ${val} ${diffing[key]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkKeys = (source, diffing, isUniq = true, msg = '', redunantOk = false) => {
|
||||||
|
if (isUniq) {
|
||||||
|
for (const [i, item] of diffing.entries()) {
|
||||||
|
if (diffing.indexOf(item) !== i) {
|
||||||
|
throw new Error(`Duplicate: ${item}: ${i} ${diffing.indexOf(item)} ${msg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of source) {
|
||||||
|
if (!diffing.includes(key)) {
|
||||||
|
throw new Error(`Diffing does not include "${key}" (${msg})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!redunantOk) {
|
||||||
|
for (const key of diffing) {
|
||||||
|
if (!source.includes(key)) {
|
||||||
|
throw new Error(`Source does not include "${key}" (${msg})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const data = minecraftData('1.20.4')
|
||||||
|
const oldId = JsonOptimizer.restoreData(json['blocks'], '1.20').find(x => x.name === 'brown_stained_glass').id;
|
||||||
|
const newId = JsonOptimizer.restoreData(json['blocks'], '1.20.4').find(x => x.name === 'brown_stained_glass').id;
|
||||||
|
assert(oldId !== newId)
|
||||||
|
// test all types + all versions
|
||||||
|
|
||||||
|
for (const type of Object.keys(json)) {
|
||||||
|
if (!json[type].__IS_OPTIMIZED__) continue
|
||||||
|
if (type === 'language') continue // we have loose data for language for size reasons
|
||||||
|
console.log('validating', type)
|
||||||
|
const source = json[type]
|
||||||
|
let checkedVer = 0
|
||||||
|
for (const ver of Object.keys(source.diffs)) {
|
||||||
|
try {
|
||||||
|
validateData(ver, type)
|
||||||
|
} catch (err) {
|
||||||
|
err.message = `Failed to validate ${type} for ${ver}: ${err.message}`
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
checkedVer++
|
||||||
|
}
|
||||||
|
console.log('Checked versions:', checkedVer)
|
||||||
|
}
|
||||||
17
src/getCollisionInteractionShapes.ts
Normal file
17
src/getCollisionInteractionShapes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||||
|
import outputInteractionShapesJson from './interactionShapesGenerated.json'
|
||||||
|
import './getCollisionShapes'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
customEvents.on('gameLoaded', () => {
|
||||||
|
// todo also remap block states (e.g. redstone)!
|
||||||
|
const renamedBlocksInteraction = getRenamedData('blocks', Object.keys(outputInteractionShapesJson), '1.20.2', bot.version)
|
||||||
|
const interactionShapes = {
|
||||||
|
...outputInteractionShapesJson,
|
||||||
|
...Object.fromEntries(Object.entries(outputInteractionShapesJson).map(([block, shape], i) => [renamedBlocksInteraction[i], shape]))
|
||||||
|
}
|
||||||
|
interactionShapes[''] = interactionShapes['air']
|
||||||
|
// todo make earlier
|
||||||
|
window.interactionShapes = interactionShapes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||||
import collisionShapesInit from '../generated/latestBlockCollisionsShapes.json'
|
import collisionShapesInit from '../generated/latestBlockCollisionsShapes.json'
|
||||||
import outputInteractionShapesJson from './interactionShapesGenerated.json'
|
|
||||||
|
|
||||||
// defining globally to be used in loaded data, not sure of better workaround
|
// defining globally to be used in loaded data, not sure of better workaround
|
||||||
window.globalGetCollisionShapes = (version) => {
|
window.globalGetCollisionShapes = (version) => {
|
||||||
|
|
@ -13,17 +12,3 @@ window.globalGetCollisionShapes = (version) => {
|
||||||
}
|
}
|
||||||
return collisionShapes
|
return collisionShapes
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
|
||||||
customEvents.on('gameLoaded', () => {
|
|
||||||
// todo also remap block states (e.g. redstone)!
|
|
||||||
const renamedBlocksInteraction = getRenamedData('blocks', Object.keys(outputInteractionShapesJson), '1.20.2', bot.version)
|
|
||||||
const interactionShapes = {
|
|
||||||
...outputInteractionShapesJson,
|
|
||||||
...Object.fromEntries(Object.entries(outputInteractionShapesJson).map(([block, shape], i) => [renamedBlocksInteraction[i], shape]))
|
|
||||||
}
|
|
||||||
interactionShapes[''] = interactionShapes['air']
|
|
||||||
// todo make earlier
|
|
||||||
window.interactionShapes = interactionShapes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import './globals'
|
||||||
import './devtools'
|
import './devtools'
|
||||||
import './entities'
|
import './entities'
|
||||||
import './globalDomListeners'
|
import './globalDomListeners'
|
||||||
import initCollisionShapes from './getCollisionShapes'
|
import initCollisionShapes from './getCollisionInteractionShapes'
|
||||||
import { onGameLoad } from './inventoryWindows'
|
import { onGameLoad } from './inventoryWindows'
|
||||||
import { supportedVersions } from 'minecraft-protocol'
|
import { supportedVersions } from 'minecraft-protocol'
|
||||||
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
|
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
|
||||||
|
|
@ -380,6 +380,7 @@ async function connect (connectOptions: ConnectOptions) {
|
||||||
try {
|
try {
|
||||||
const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions)
|
const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions)
|
||||||
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
|
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
|
||||||
|
window._LOAD_MC_DATA() // start loading data (if not loaded yet)
|
||||||
const downloadMcData = async (version: string) => {
|
const downloadMcData = async (version: string) => {
|
||||||
if (connectOptions.authenticatedAccount && versionToNumber(version) < versionToNumber('1.19.4')) {
|
if (connectOptions.authenticatedAccount && versionToNumber(version) < versionToNumber('1.19.4')) {
|
||||||
// todo support it (just need to fix .export crash)
|
// todo support it (just need to fix .export crash)
|
||||||
|
|
@ -392,13 +393,13 @@ async function connect (connectOptions: ConnectOptions) {
|
||||||
// ignore cache hit
|
// ignore cache hit
|
||||||
versionsByMinecraftVersion.pc[lastVersion]!['dataVersion']!++
|
versionsByMinecraftVersion.pc[lastVersion]!['dataVersion']!++
|
||||||
}
|
}
|
||||||
|
setLoadingScreenStatus(`Loading data for ${version}`)
|
||||||
if (!document.fonts.check('1em mojangles')) {
|
if (!document.fonts.check('1em mojangles')) {
|
||||||
// todo instead re-render signs on load
|
// todo instead re-render signs on load
|
||||||
await document.fonts.load('1em mojangles').catch(() => { })
|
await document.fonts.load('1em mojangles').catch(() => { })
|
||||||
}
|
}
|
||||||
setLoadingScreenStatus(`Downloading data for ${version}`)
|
await window._MC_DATA_RESOLVER.promise // ensure data is loaded
|
||||||
await downloadSoundsIfNeeded()
|
await downloadSoundsIfNeeded()
|
||||||
await loadScript(`./mc-data/${toMajorVersion(version)}.js`)
|
|
||||||
miscUiState.loadedDataVersion = version
|
miscUiState.loadedDataVersion = version
|
||||||
try {
|
try {
|
||||||
await resourcepackReload(version)
|
await resourcepackReload(version)
|
||||||
|
|
|
||||||
264
src/optimizeJson.ts
Normal file
264
src/optimizeJson.ts
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||||
|
|
||||||
|
type IdMap = Record<string, number>
|
||||||
|
|
||||||
|
type DiffData = {
|
||||||
|
removed: number[],
|
||||||
|
changed: any[],
|
||||||
|
removedProps: Array<[number, number[]]>,
|
||||||
|
added
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceData = {
|
||||||
|
keys: IdMap,
|
||||||
|
properties: IdMap
|
||||||
|
source: Record<number, any>
|
||||||
|
diffs: Record<string, DiffData>
|
||||||
|
arrKey?
|
||||||
|
__IS_OPTIMIZED__: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class JsonOptimizer {
|
||||||
|
keys = {} as IdMap
|
||||||
|
idToKey = {} as Record<number, string>
|
||||||
|
properties = {} as IdMap
|
||||||
|
source = {}
|
||||||
|
previousKeys = [] as number[]
|
||||||
|
previousValues = {} as Record<number, any>
|
||||||
|
diffs = {} as Record<string, DiffData>
|
||||||
|
|
||||||
|
constructor (public arrKey?: string, public ignoreChanges = false, public ignoreRemoved = false) { }
|
||||||
|
|
||||||
|
export () {
|
||||||
|
const { keys, properties, source, arrKey, diffs } = this
|
||||||
|
return {
|
||||||
|
keys,
|
||||||
|
properties,
|
||||||
|
source,
|
||||||
|
arrKey,
|
||||||
|
diffs,
|
||||||
|
'__IS_OPTIMIZED__': true
|
||||||
|
} satisfies SourceData
|
||||||
|
}
|
||||||
|
|
||||||
|
diffObj (diffing): DiffData {
|
||||||
|
const removed = [] as number[]
|
||||||
|
const changed = [] as any[]
|
||||||
|
const removedProps = [] as any[]
|
||||||
|
const { arrKey, ignoreChanges, ignoreRemoved } = this
|
||||||
|
const added = [] as number[]
|
||||||
|
|
||||||
|
if (!diffing || typeof diffing !== 'object') throw new Error('diffing data is not object')
|
||||||
|
if (Array.isArray(diffing) && !arrKey) throw new Error('arrKey is required for arrays')
|
||||||
|
const diffingObj = Array.isArray(diffing) ? Object.fromEntries(diffing.map(x => {
|
||||||
|
const key = JsonOptimizer.getByArrKey(x, arrKey!)
|
||||||
|
return [key, x]
|
||||||
|
})) : diffing
|
||||||
|
|
||||||
|
const possiblyNewKeys = Object.keys(diffingObj)
|
||||||
|
this.keys ??= {}
|
||||||
|
this.properties ??= {}
|
||||||
|
let lastRootKeyId = Object.values(this.keys).length
|
||||||
|
let lastItemKeyId = Object.values(this.properties).length
|
||||||
|
for (const key of possiblyNewKeys) {
|
||||||
|
this.keys[key] ??= lastRootKeyId++
|
||||||
|
this.idToKey[this.keys[key]] = key
|
||||||
|
}
|
||||||
|
const DEBUG = false
|
||||||
|
|
||||||
|
const addDiff = (key, newVal, prevVal) => {
|
||||||
|
const valueMapped = [] as any[]
|
||||||
|
const isItemObj = typeof newVal === 'object' && newVal
|
||||||
|
const keyId = this.keys[key]
|
||||||
|
if (isItemObj) {
|
||||||
|
const removedPropsLocal = [] as any[]
|
||||||
|
for (const [prop, val] of Object.entries(newVal)) {
|
||||||
|
// mc-data: why push only changed props? eg for blocks only stateId are different between all versions so we skip a lot of duplicated data like block props
|
||||||
|
if (!isEqualStructured(newVal[prop], prevVal[prop])) {
|
||||||
|
let keyMapped = this.properties[prop]
|
||||||
|
if (keyMapped === undefined) {
|
||||||
|
this.properties[prop] = lastItemKeyId++
|
||||||
|
keyMapped = this.properties[prop]
|
||||||
|
}
|
||||||
|
valueMapped.push(DEBUG ? prop : keyMapped, newVal[prop])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// also add undefined for removed props
|
||||||
|
for (const prop of Object.keys(prevVal)) {
|
||||||
|
if (prop in newVal) continue
|
||||||
|
let keyMapped = this.properties[prop]
|
||||||
|
if (keyMapped === undefined) {
|
||||||
|
this.properties[prop] = lastItemKeyId++
|
||||||
|
keyMapped = this.properties[prop]
|
||||||
|
}
|
||||||
|
removedPropsLocal.push(DEBUG ? prop : keyMapped)
|
||||||
|
}
|
||||||
|
removedProps.push([keyId, removedPropsLocal])
|
||||||
|
}
|
||||||
|
changed.push(DEBUG ? key : keyId, isItemObj ? valueMapped : newVal)
|
||||||
|
}
|
||||||
|
for (const [id, sourceVal] of Object.entries(this.source)) {
|
||||||
|
const key = this.idToKey[id]
|
||||||
|
const diffVal = diffingObj[key]
|
||||||
|
if (!ignoreChanges && diffVal !== undefined) {
|
||||||
|
this.previousValues[id] ??= this.source[id]
|
||||||
|
const prevVal = this.previousValues[id]
|
||||||
|
if (!isEqualStructured(prevVal, diffVal)) {
|
||||||
|
addDiff(key, diffVal, prevVal)
|
||||||
|
}
|
||||||
|
this.previousValues[id] = diffVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [key, val] of Object.entries(diffingObj)) {
|
||||||
|
const id = this.keys[key]
|
||||||
|
if (!this.source[id]) {
|
||||||
|
this.source[id] = val
|
||||||
|
}
|
||||||
|
added.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const previousKey of this.previousKeys) {
|
||||||
|
const key = this.idToKey[previousKey]
|
||||||
|
if (diffingObj[key] === undefined && !ignoreRemoved) {
|
||||||
|
removed.push(previousKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const toRemove of removed) {
|
||||||
|
this.previousKeys.splice(this.previousKeys.indexOf(toRemove), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const previousKey of this.previousKeys) {
|
||||||
|
const index = added.indexOf(previousKey)
|
||||||
|
if (index === -1) continue
|
||||||
|
added.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousKeys = [...this.previousKeys, ...added]
|
||||||
|
|
||||||
|
return {
|
||||||
|
removed,
|
||||||
|
changed,
|
||||||
|
added,
|
||||||
|
removedProps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDiff (key: string, diffObj: string) {
|
||||||
|
const diff = this.diffObj(diffObj)
|
||||||
|
this.diffs[key] = diff
|
||||||
|
}
|
||||||
|
|
||||||
|
static isOptimizedChangeDiff (changePossiblyArrDiff) {
|
||||||
|
if (!Array.isArray(changePossiblyArrDiff)) return false
|
||||||
|
if (changePossiblyArrDiff.length % 2 !== 0) return false
|
||||||
|
for (let i = 0; i < changePossiblyArrDiff.length; i += 2) {
|
||||||
|
if (typeof changePossiblyArrDiff[i] !== 'number') return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static restoreData ({ keys, properties, source, arrKey, diffs }: SourceData, targetKey: string) {
|
||||||
|
// if (!diffs[targetKey]) throw new Error(`The requested data to restore with key ${targetKey} does not exist`)
|
||||||
|
source = structuredClone(source)
|
||||||
|
const keysById = Object.fromEntries(Object.entries(keys).map(x => [x[1], x[0]]))
|
||||||
|
const propertiesById = Object.fromEntries(Object.entries(properties).map(x => [x[1], x[0]]))
|
||||||
|
const dataByKeys = {} as Record<string, any>
|
||||||
|
for (const [versionKey, { added, changed, removed, removedProps }] of Object.entries(diffs)) {
|
||||||
|
for (const toAdd of added) {
|
||||||
|
dataByKeys[toAdd] = source[toAdd]
|
||||||
|
}
|
||||||
|
for (const toRemove of removed) {
|
||||||
|
delete dataByKeys[toRemove]
|
||||||
|
}
|
||||||
|
for (let i = 0; i < changed.length; i += 2) {
|
||||||
|
const key = changed[i]
|
||||||
|
const change = changed[i + 1]
|
||||||
|
const isOptimizedChange = JsonOptimizer.isOptimizedChangeDiff(change)
|
||||||
|
if (isOptimizedChange) {
|
||||||
|
// apply optimized diff
|
||||||
|
for (let k = 0; k < change.length; k += 2) {
|
||||||
|
const propId = change[k]
|
||||||
|
const newVal = change[k + 1]
|
||||||
|
const prop = propertiesById[propId]
|
||||||
|
// const prop = propId
|
||||||
|
if (prop === undefined) throw new Error(`Property id change is undefined: ${propId}`)
|
||||||
|
dataByKeys[key][prop] = newVal
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataByKeys[key] = change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [key, removePropsId] of removedProps) {
|
||||||
|
for (const removePropId of removePropsId) {
|
||||||
|
const removeProp = propertiesById[removePropId]
|
||||||
|
delete dataByKeys[key][removeProp]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (versionToNumber(versionKey) <= versionToNumber(targetKey)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arrKey) {
|
||||||
|
return Object.values(dataByKeys)
|
||||||
|
} else {
|
||||||
|
return Object.fromEntries(Object.entries(dataByKeys).map(([key, val]) => [keysById[key], val]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getByArrKey (item: any, arrKey: string) {
|
||||||
|
return arrKey.split('+').map(x => item[x]).join('+')
|
||||||
|
}
|
||||||
|
|
||||||
|
static resolveDefaults (arr) {
|
||||||
|
if (!Array.isArray(arr)) throw new Error('not an array')
|
||||||
|
const propsValueCount = {} as {
|
||||||
|
[key: string]: {
|
||||||
|
[val: string]: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const obj of arr) {
|
||||||
|
if (typeof obj !== 'object' || !obj) continue
|
||||||
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
|
const valJson = JSON.stringify(val)
|
||||||
|
propsValueCount[key] ??= {}
|
||||||
|
propsValueCount[key][valJson] ??= 0
|
||||||
|
propsValueCount[key][valJson] += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaults = Object.fromEntries(Object.entries(propsValueCount).map(([prop, values]) => {
|
||||||
|
const defaultValue = Object.entries(values).sort(([, count1], [, count2]) => count2 - count1)[0][0]
|
||||||
|
return [prop, defaultValue]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const newData = [] as any[]
|
||||||
|
const noData = {}
|
||||||
|
for (const [i, obj] of arr.entries()) {
|
||||||
|
if (typeof obj !== 'object' || !obj) {
|
||||||
|
newData.push(obj)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(defaults)) {
|
||||||
|
const val = obj[key]
|
||||||
|
if (!val) {
|
||||||
|
noData[key] ??= []
|
||||||
|
noData[key].push(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (defaults[key] === JSON.stringify(val)) {
|
||||||
|
delete obj[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newData.push(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: newData,
|
||||||
|
defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEqualStructured = (val1, val2) => {
|
||||||
|
return JSON.stringify(val1) === JSON.stringify(val2)
|
||||||
|
}
|
||||||
|
|
@ -45,8 +45,8 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
|
||||||
placeholder='World name'
|
placeholder='World name'
|
||||||
/>
|
/>
|
||||||
<SelectGameVersion
|
<SelectGameVersion
|
||||||
versions={versions.map((obj) => { return { value: obj.version, label: obj.version === defaultVersion ? obj.version + ' (available offline)' : obj.version } })}
|
versions={versions.map((obj) => { return { value: obj.version, label: obj.version } })}
|
||||||
selected={{ value: defaultVersion, label: defaultVersion + ' (available offline)' }}
|
selected={{ value: defaultVersion, label: defaultVersion }}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
creatingWorldState.version = value ?? defaultVersion
|
creatingWorldState.version = value ?? defaultVersion
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
92
src/shims/minecraftData.ts
Normal file
92
src/shims/minecraftData.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||||
|
import JsonOptimizer from '../optimizeJson'
|
||||||
|
import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json'
|
||||||
|
import { toMajorVersion } from '../utils'
|
||||||
|
|
||||||
|
const customResolver = () => {
|
||||||
|
const resolver = Promise.withResolvers()
|
||||||
|
let resolvedData
|
||||||
|
return {
|
||||||
|
...resolver,
|
||||||
|
get resolvedData () {
|
||||||
|
return resolvedData
|
||||||
|
},
|
||||||
|
resolve (data) {
|
||||||
|
resolver.resolve(data)
|
||||||
|
resolvedData = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const optimizedDataResolver = customResolver()
|
||||||
|
window._MC_DATA_RESOLVER = optimizedDataResolver
|
||||||
|
window._LOAD_MC_DATA = async () => {
|
||||||
|
if (optimizedDataResolver.resolvedData) return
|
||||||
|
optimizedDataResolver.resolve(await import('../../generated/minecraft-data-optimized.json'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 30 seconds
|
||||||
|
const cacheTtl = 30 * 1000
|
||||||
|
const cache = new Map<string, any>()
|
||||||
|
const cacheTime = new Map<string, number>()
|
||||||
|
const possiblyGetFromCache = (version: string) => {
|
||||||
|
if (minecraftInitialDataJson[version] && !optimizedDataResolver.resolvedData) {
|
||||||
|
return minecraftInitialDataJson[version]
|
||||||
|
}
|
||||||
|
if (cache.has(version)) {
|
||||||
|
return cache.get(version)
|
||||||
|
}
|
||||||
|
const inner = () => {
|
||||||
|
if (!optimizedDataResolver.resolvedData) {
|
||||||
|
throw new Error(`Data for ${version} is not ready yet`)
|
||||||
|
}
|
||||||
|
const dataTypes = Object.keys(optimizedDataResolver.resolvedData)
|
||||||
|
const allRestored = {}
|
||||||
|
for (const dataType of dataTypes) {
|
||||||
|
if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) {
|
||||||
|
const shapes = window.globalGetCollisionShapes?.(version)
|
||||||
|
if (shapes) {
|
||||||
|
allRestored[dataType] = shapes
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = optimizedDataResolver.resolvedData[dataType]
|
||||||
|
if (data.__IS_OPTIMIZED__) {
|
||||||
|
allRestored[dataType] = JsonOptimizer.restoreData(data, version)
|
||||||
|
} else {
|
||||||
|
allRestored[dataType] = data[version] ?? data[toMajorVersion(version)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allRestored
|
||||||
|
}
|
||||||
|
const data = inner()
|
||||||
|
cache.set(version, data)
|
||||||
|
cacheTime.set(version, Date.now())
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
window.allLoadedMcData = new Proxy({}, {
|
||||||
|
get (t, version: string) {
|
||||||
|
// special properties like $typeof
|
||||||
|
if (version.includes('$')) return
|
||||||
|
// todo enumerate all props
|
||||||
|
return new Proxy({}, {
|
||||||
|
get (target, prop) {
|
||||||
|
return possiblyGetFromCache(version)[prop]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [version, time] of cacheTime) {
|
||||||
|
if (now - time > cacheTtl) {
|
||||||
|
cache.delete(version)
|
||||||
|
cacheTime.delete(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
export const pc = window.allLoadedMcData
|
||||||
|
export default { pc }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue