Merge remote-tracking branch 'origin/next' into worker-rewrite
This commit is contained in:
commit
c228b91d2d
32 changed files with 6387 additions and 4979 deletions
31
.github/workflows/build-single-file.yml
vendored
Normal file
31
.github/workflows/build-single-file.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
name: build-single-file
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-bundle:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build single-file version - minecraft.html
|
||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: minecraft.html
|
||||
path: minecraft.html
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
|
@ -53,6 +53,8 @@ jobs:
|
|||
publish_dir: .vercel/output/static
|
||||
force_orphan: true
|
||||
|
||||
- name: Build single-file version - minecraft.html
|
||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
||||
- name: Build self-host version
|
||||
run: pnpm build
|
||||
- name: Bundle server.js
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Prismarine Web Client",
|
||||
"short_name": "Prismarine Web Client",
|
||||
"name": "Minecraft Web Client",
|
||||
"short_name": "Minecraft Web Client",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
|
|
|
|||
15
config.json
15
config.json
|
|
@ -1,18 +1,29 @@
|
|||
{
|
||||
"version": 1,
|
||||
"defaultHost": "<from-proxy>",
|
||||
"defaultProxy": "proxy.mcraft.fun",
|
||||
"defaultProxy": "https://proxy.mcraft.fun",
|
||||
"mapsProvider": "https://maps.mcraft.fun/",
|
||||
"peerJsServer": "",
|
||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||
"promoteServers": [
|
||||
{
|
||||
"ip": "ws://mcraft.ryzyn.xyz",
|
||||
"ip": "wss://mcraft.ryzyn.xyz",
|
||||
"version": "1.19.4"
|
||||
},
|
||||
{
|
||||
"ip": "grim.mcraft.fun",
|
||||
"version": "1.19.4"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play2.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"version": "1.20.3",
|
||||
"description": "Very nice a polite server. Must try for everyone!"
|
||||
}
|
||||
],
|
||||
"pauseLinks": [
|
||||
|
|
|
|||
10
index.html
10
index.html
|
|
@ -128,21 +128,17 @@
|
|||
window.loadedPlugins[pluginName] = await import(script)
|
||||
}
|
||||
</script> -->
|
||||
<title>Prismarine Web Client</title>
|
||||
<link rel="favicon" href="favicon.png">
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="canonical" href="https://mcraft.fun">
|
||||
<title>Minecraft Web Client</title>
|
||||
<!-- <link rel="canonical" href="https://mcraft.fun"> -->
|
||||
<meta name="description" content="Minecraft web client running in your browser">
|
||||
<meta name="keywords" content="Play, Minecraft, Online, Web, Java, Server, Single player, Javascript, PrismarineJS, Voxel, WebGL, Three.js">
|
||||
<meta name="date" content="2024-07-11" scheme="YYYY-MM-DD">
|
||||
<meta name="language" content="English">
|
||||
<meta name="theme-color" content="#349474">
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'>
|
||||
<meta property="og:title" content="Prismarine Web Client" />
|
||||
<meta property="og:title" content="Minecraft Web Client" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="favicon.png" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root"></div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
"start2": "run-p dev-rsbuild watch-mesher",
|
||||
"build": "pnpm build-other-workers && rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
|
||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
||||
"prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts",
|
||||
"check-build": "pnpm prepare-project && tsc && pnpm build",
|
||||
"test:cypress": "cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"test-unit": "vitest",
|
||||
|
|
@ -36,7 +38,7 @@
|
|||
"client"
|
||||
],
|
||||
"release": {
|
||||
"attachReleaseFiles": "self-host.zip"
|
||||
"attachReleaseFiles": "{self-host.zip,minecraft.html}"
|
||||
},
|
||||
"publish": {
|
||||
"preset": {
|
||||
|
|
@ -149,7 +151,7 @@
|
|||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.42",
|
||||
"mineflayer-mouse": "^0.0.7",
|
||||
"mineflayer-mouse": "^0.0.8",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:GenerelSchwerz/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
|
|
@ -171,6 +173,7 @@
|
|||
"optionalDependencies": {
|
||||
"cypress": "^10.11.0",
|
||||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"sharp": "^0.33.5",
|
||||
"systeminformation": "^5.21.22"
|
||||
},
|
||||
"browserslist": {
|
||||
|
|
|
|||
10609
pnpm-lock.yaml
generated
10609
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -78,6 +78,12 @@ export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }:
|
|||
if (absolute.endsWith('/minecraft-data/data.js')) {
|
||||
resource.request = path.join(__dirname, `../src/shims/minecraftData.ts`)
|
||||
}
|
||||
if (absolute.endsWith('/minecraft-data/data/bedrock/common/legacy.json')) {
|
||||
resource.request = path.join(__dirname, `../src/shims/empty.ts`)
|
||||
}
|
||||
if (absolute.endsWith('/minecraft-data/data/pc/common/legacy.json')) {
|
||||
resource.request = path.join(__dirname, `../src/preflatMap.json`)
|
||||
}
|
||||
}))
|
||||
addRules([
|
||||
{
|
||||
|
|
@ -91,11 +97,17 @@ export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }:
|
|||
{
|
||||
test: /\.mp3$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.txt$/,
|
||||
type: 'asset/source',
|
||||
}
|
||||
])
|
||||
config.ignoreWarnings = [
|
||||
/the request of a dependency is an expression/,
|
||||
/Unsupported pseudo class or element: xr-overlay/
|
||||
]
|
||||
|
||||
if (process.env.SINGLE_FILE_BUILD === 'true') {
|
||||
config.module!.parser!.javascript!.dynamicImportMode = 'eager'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
// eslint-disable-next-line node/no-path-concat
|
||||
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
|
||||
|
||||
const worker: any = new Worker(src)
|
||||
let worker: any
|
||||
if (process.env.SINGLE_FILE_BUILD) {
|
||||
const workerCode = document.getElementById('mesher-worker-code')!.textContent!
|
||||
const blob = new Blob([workerCode], { type: 'text/javascript' })
|
||||
worker = new Worker(window.URL.createObjectURL(blob))
|
||||
} else {
|
||||
worker = new Worker(src)
|
||||
}
|
||||
const handleMessage = (data) => {
|
||||
if (!this.active) return
|
||||
if (data.type !== 'geometry' || !this.debugStopGeometryUpdate) {
|
||||
|
|
@ -331,6 +338,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.version = version
|
||||
this.texturesVersion = texturesVersion
|
||||
this.resetWorld()
|
||||
|
||||
// for workers in single file build
|
||||
if (document.readyState === 'loading') {
|
||||
await new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', resolve)
|
||||
})
|
||||
}
|
||||
|
||||
this.initWorkers()
|
||||
this.active = true
|
||||
this.mesherConfig.outputFormat = this.outputFormat
|
||||
|
|
|
|||
|
|
@ -11,6 +11,17 @@ import { promisify } from 'util'
|
|||
import { generateSW } from 'workbox-build'
|
||||
import { getSwAdditionalEntries } from './scripts/build'
|
||||
import { appAndRendererSharedConfig } from './renderer/rsbuildSharedConfig'
|
||||
import { genLargeDataAliases } from './scripts/genLargeDataAliases'
|
||||
import sharp from 'sharp'
|
||||
import supportedVersions from './src/supportedVersions.mjs'
|
||||
|
||||
const SINGLE_FILE_BUILD = process.env.SINGLE_FILE_BUILD === 'true'
|
||||
|
||||
if (SINGLE_FILE_BUILD) {
|
||||
const patchCssFile = 'node_modules/pixelarticons/fonts/pixelart-icons-font.css'
|
||||
const text = fs.readFileSync(patchCssFile, 'utf8')
|
||||
fs.writeFileSync(patchCssFile, text.replaceAll("url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'),", ""), 'utf8')
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
try { require('./localSettings.js') } catch { }
|
||||
|
|
@ -43,12 +54,54 @@ if (dev) {
|
|||
configJson.defaultProxy = ':8080'
|
||||
}
|
||||
|
||||
const configSource = process.env.CONFIG_JSON_SOURCE || 'REMOTE'
|
||||
const configSource = (SINGLE_FILE_BUILD ? 'BUNDLED' : (process.env.CONFIG_JSON_SOURCE || 'REMOTE')) as 'BUNDLED' | 'REMOTE'
|
||||
|
||||
const faviconPath = 'favicon.png'
|
||||
|
||||
// base options are in ./renderer/rsbuildSharedConfig.ts
|
||||
const appConfig = defineConfig({
|
||||
html: {
|
||||
template: './index.html',
|
||||
inject: 'body',
|
||||
tags: [
|
||||
...SINGLE_FILE_BUILD ? [] : [
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'manifest',
|
||||
crossorigin: 'use-credentials',
|
||||
href: 'manifest.json'
|
||||
},
|
||||
}
|
||||
],
|
||||
// <link rel="favicon" href="favicon.png">
|
||||
// <link rel="icon" type="image/png" href="favicon.png" />
|
||||
// <meta property="og:image" content="favicon.png" />
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'favicon',
|
||||
href: faviconPath
|
||||
}
|
||||
},
|
||||
...SINGLE_FILE_BUILD ? [] : [
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: faviconPath
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'meta',
|
||||
attrs: {
|
||||
property: 'og:image',
|
||||
content: faviconPath
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
output: {
|
||||
externals: [
|
||||
|
|
@ -58,6 +111,13 @@ const appConfig = defineConfig({
|
|||
js: 'source-map',
|
||||
css: true,
|
||||
},
|
||||
distPath: SINGLE_FILE_BUILD ? {
|
||||
html: './single',
|
||||
} : undefined,
|
||||
inlineScripts: SINGLE_FILE_BUILD,
|
||||
inlineStyles: SINGLE_FILE_BUILD,
|
||||
// 50kb limit for data uri
|
||||
dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024
|
||||
},
|
||||
source: {
|
||||
entry: {
|
||||
|
|
@ -69,6 +129,9 @@ const appConfig = defineConfig({
|
|||
define: {
|
||||
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
|
||||
'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS),
|
||||
'process.env.SINGLE_FILE_BUILD': JSON.stringify(process.env.SINGLE_FILE_BUILD),
|
||||
'process.env.SINGLE_FILE_BUILD_MODE': JSON.stringify(process.env.SINGLE_FILE_BUILD),
|
||||
'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}` || githubRepositoryFallback}`),
|
||||
'process.env.DEPS_VERSIONS': JSON.stringify({}),
|
||||
|
|
@ -92,17 +155,19 @@ const appConfig = defineConfig({
|
|||
pluginTypedCSSModules(),
|
||||
{
|
||||
name: 'test',
|
||||
setup (build: RsbuildPluginAPI) {
|
||||
setup(build: RsbuildPluginAPI) {
|
||||
const prep = async () => {
|
||||
console.time('total-prep')
|
||||
fs.mkdirSync('./generated', { recursive: true })
|
||||
if (!fs.existsSync('./generated/minecraft-data-optimized.json') || require('./generated/minecraft-data-optimized.json').versionKey !== require('minecraft-data/package.json').version) {
|
||||
if (!fs.existsSync('./generated/minecraft-data-optimized.json') || !fs.existsSync('./generated/mc-assets-compressed.js') || require('./generated/minecraft-data-optimized.json').versionKey !== require('minecraft-data/package.json').version) {
|
||||
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit' })
|
||||
}
|
||||
childProcess.execSync('tsx ./scripts/genShims.ts', { stdio: 'inherit' })
|
||||
if (!fs.existsSync('./generated/latestBlockCollisionsShapes.json') || require('./generated/latestBlockCollisionsShapes.json').versionKey !== require('minecraft-data/package.json').version) {
|
||||
childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' })
|
||||
}
|
||||
// childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' })
|
||||
genLargeDataAliases(SINGLE_FILE_BUILD)
|
||||
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')
|
||||
|
|
@ -137,16 +202,33 @@ const appConfig = defineConfig({
|
|||
prep()
|
||||
})
|
||||
build.onAfterBuild(async () => {
|
||||
if (!disableServiceWorker) {
|
||||
if (SINGLE_FILE_BUILD) {
|
||||
// process index.html
|
||||
const singleBuildHtml = './dist/single/index.html'
|
||||
let html = fs.readFileSync(singleBuildHtml, 'utf8')
|
||||
const verToMajor = (ver: string) => ver.split('.').slice(0, 2).join('.')
|
||||
const supportedMajorVersions = [...new Set(supportedVersions.map(a => verToMajor(a)))].join(', ')
|
||||
html = `<!DOCTYPE html><!-- MINECRAFT WEB CLIENT ${releaseTag ?? ''} -->\n<!-- A true SINGLE FILE BUILD with built-in server -->\n<!-- All textures, assets and Minecraft data for ${supportedMajorVersions} inlined into one file. -->\n${html}`
|
||||
|
||||
const resizedImage = (await (sharp('./assets/favicon.png') as any).resize(64).toBuffer()).toString('base64')
|
||||
html = html.replace('favicon.png', `data:image/png;base64,${resizedImage}`)
|
||||
html = html.replace('src="./loading-bg.jpg"', `src="data:image/png;base64,${fs.readFileSync('./assets/loading-bg.jpg', 'base64')}"`)
|
||||
html += '<script id="mesher-worker-code">' + fs.readFileSync('./dist/mesher.js', 'utf8') + '</script>'
|
||||
fs.writeFileSync(singleBuildHtml, html, 'utf8')
|
||||
// write output file size
|
||||
console.log('single file size', (fs.statSync(singleBuildHtml).size / 1024 / 1024).toFixed(2), 'mb')
|
||||
} else {
|
||||
if (!disableServiceWorker) {
|
||||
const { count, size, warnings } = await generateSW({
|
||||
// dontCacheBustURLsMatching: [new RegExp('...')],
|
||||
globDirectory: 'dist',
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
additionalManifestEntries: getSwAdditionalEntries(),
|
||||
globPatterns: [],
|
||||
swDest: './dist/service-worker.js',
|
||||
})
|
||||
// dontCacheBustURLsMatching: [new RegExp('...')],
|
||||
globDirectory: 'dist',
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
additionalManifestEntries: getSwAdditionalEntries(),
|
||||
globPatterns: [],
|
||||
swDest: './dist/service-worker.js',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
59
scripts/genLargeDataAliases.ts
Normal file
59
scripts/genLargeDataAliases.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import * as fs from 'fs'
|
||||
|
||||
export const genLargeDataAliases = async (isCompressed: boolean) => {
|
||||
const modules = {
|
||||
mcData: {
|
||||
raw: '../generated/minecraft-data-optimized.json',
|
||||
compressed: '../generated/mc-data-compressed.js',
|
||||
},
|
||||
blockStatesModels: {
|
||||
raw: 'mc-assets/dist/blockStatesModels.json',
|
||||
compressed: '../generated/mc-assets-compressed.js',
|
||||
}
|
||||
}
|
||||
|
||||
const OUT_FILE = './generated/large-data-aliases.ts'
|
||||
|
||||
let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n`
|
||||
for (const [module, { compressed, raw }] of Object.entries(modules)) {
|
||||
let importCode = `(await import('${isCompressed ? compressed : raw}')).default`;
|
||||
if (isCompressed) {
|
||||
importCode = `JSON.parse(decompressFromBase64(${importCode}))`
|
||||
}
|
||||
str += ` if (mod === '${module}') return ${importCode}\n`
|
||||
}
|
||||
str += `}\n`
|
||||
|
||||
fs.writeFileSync(OUT_FILE, str, 'utf8')
|
||||
}
|
||||
|
||||
const decoderCode = /* ts */ `
|
||||
import pako from 'pako';
|
||||
|
||||
function decompressFromBase64(input) {
|
||||
console.time('decompressFromBase64')
|
||||
// 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' });
|
||||
|
||||
console.timeEnd('decompressFromBase64')
|
||||
return decompressedData;
|
||||
}
|
||||
`
|
||||
|
||||
// execute if run directly
|
||||
if (require.main === module) {
|
||||
console.log('running...')
|
||||
const isCompressed = process.argv.includes('--compressed')
|
||||
genLargeDataAliases(isCompressed)
|
||||
console.log('done generating large data aliases')
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ for (const resource of appReplacableResources) {
|
|||
const name = path.split('/').slice(-4).join('_').replace('.png', '').replaceAll('-', '_').replaceAll('.', '_')
|
||||
keys.push(name)
|
||||
headerImports += `import ${name} from '${path.replace('../node_modules/', '')}'\n`
|
||||
|
||||
resourcesContent += `
|
||||
'${name}': {
|
||||
content: ${name},
|
||||
|
|
|
|||
|
|
@ -42,14 +42,20 @@ const versionToNumber = (ver) => {
|
|||
return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const compressedOutput = false
|
||||
// if not included here (even as {}) will not be bundled & accessible!
|
||||
// const dataTypeBundling = {
|
||||
// protocol: {
|
||||
// // ignoreRemoved: true,
|
||||
// // ignoreChanges: true
|
||||
// }
|
||||
// }
|
||||
// const compressedOutput = !!process.env.SINGLE_FILE_BUILD
|
||||
const compressedOutput = true
|
||||
const dataTypeBundling2 = {
|
||||
blocks: {
|
||||
arrKey: 'name',
|
||||
},
|
||||
items: {
|
||||
arrKey: 'name',
|
||||
},
|
||||
recipes: {
|
||||
processData: processRecipes
|
||||
}
|
||||
}
|
||||
const dataTypeBundling = {
|
||||
language: {
|
||||
ignoreRemoved: true,
|
||||
|
|
@ -132,7 +138,8 @@ const dataTypeBundling = {
|
|||
},
|
||||
recipes: {
|
||||
raw: true
|
||||
}, // todo we can do better
|
||||
// processData: processRecipes
|
||||
},
|
||||
blockCollisionShapes: {},
|
||||
loginPacket: {},
|
||||
protocol: {
|
||||
|
|
@ -143,6 +150,84 @@ const dataTypeBundling = {
|
|||
// }
|
||||
}
|
||||
|
||||
function processRecipes (current, prev, getData, version) {
|
||||
// can require the same multiple times per different versions
|
||||
if (current._proccessed) return
|
||||
const items = getData('items')
|
||||
const blocks = getData('blocks')
|
||||
const itemsIdsMap = Object.fromEntries(items.map((b) => [b.id, b.name]))
|
||||
const blocksIdsMap = Object.fromEntries(blocks.map((b) => [b.id, b.name]))
|
||||
for (const key of Object.keys(current)) {
|
||||
const mapId = (id) => {
|
||||
if (typeof id !== 'string' && typeof id !== 'number') throw new Error('Incorrect type')
|
||||
const mapped = itemsIdsMap[id] ?? blocksIdsMap[id]
|
||||
if (!mapped) {
|
||||
throw new Error(`No item/block name with id ${id}`)
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
const processRecipe = (obj) => {
|
||||
// if (!obj) return
|
||||
// if (Array.isArray(obj)) {
|
||||
// obj.forEach((id, i) => {
|
||||
// obj[i] = mapId(obj[id])
|
||||
// })
|
||||
// } else if (obj && typeof obj === 'object') {
|
||||
// if (!'count metadata id'.split(' ').every(x => x in obj)) {
|
||||
// throw new Error(`process error: Unknown deep object pattern: ${JSON.stringify(obj)}`)
|
||||
// }
|
||||
// obj.id = mapId(obj.id)
|
||||
// } else {
|
||||
// throw new Error('unknown type')
|
||||
// }
|
||||
const parseRecipeItem = (item) => {
|
||||
if (typeof item === 'number') return mapId(item)
|
||||
if (Array.isArray(item)) return [mapId(item), ...item.slice(1)]
|
||||
if (!item) {
|
||||
return item
|
||||
}
|
||||
if ('id' in item) {
|
||||
item.id = mapId(item.id)
|
||||
return item
|
||||
}
|
||||
throw new Error('unhandled')
|
||||
}
|
||||
const maybeProccessShape = (shape) => {
|
||||
if (!shape) return
|
||||
for (const shapeRow of shape) {
|
||||
for (const [i, item] of shapeRow.entries()) {
|
||||
shapeRow[i] = parseRecipeItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj.result) obj.result = parseRecipeItem(obj.result)
|
||||
maybeProccessShape(obj.inShape)
|
||||
maybeProccessShape(obj.outShape)
|
||||
if (obj.ingredients) {
|
||||
for (const [i, ingredient] of obj.ingredients.entries()) {
|
||||
obj.ingredients[i] = parseRecipeItem(ingredient)
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
const name = mapId(key)
|
||||
for (const [i, recipe] of current[key].entries()) {
|
||||
try {
|
||||
processRecipe(recipe)
|
||||
} catch (err) {
|
||||
console.warn(`${version} [warn] Removing incorrect recipe: ${err}`)
|
||||
delete current[i]
|
||||
}
|
||||
}
|
||||
current[name] = current[key]
|
||||
} catch (err) {
|
||||
console.warn(`${version} [warn] Removing incorrect recipe: ${err}`)
|
||||
}
|
||||
delete current[key]
|
||||
}
|
||||
current._proccessed = true
|
||||
}
|
||||
|
||||
const notBundling = [...dataTypes.keys()].filter(x => !Object.keys(dataTypeBundling).includes(x))
|
||||
console.log("Not bundling minecraft-data data:", notBundling)
|
||||
|
||||
|
|
@ -161,11 +246,15 @@ for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) {
|
|||
// 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 = ''
|
||||
const getData = (type) => {
|
||||
const loc = `minecraft-data/data/${dataSet[type]}/`
|
||||
const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`)
|
||||
// const data = fs.readFileSync(dataPathAbsolute, 'utf8')
|
||||
const dataRaw = require(dataPathAbsolute)
|
||||
return dataRaw
|
||||
}
|
||||
const dataRaw = getData(dataType)
|
||||
let rawData = dataRaw
|
||||
if (config.raw) {
|
||||
rawDataVersions[dataType] ??= {}
|
||||
|
|
@ -176,7 +265,7 @@ for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) {
|
|||
diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved)
|
||||
}
|
||||
try {
|
||||
config.processData?.(dataRaw, previousData[dataType])
|
||||
config.processData?.(dataRaw, previousData[dataType], getData, version)
|
||||
diffSources[dataType].recordDiff(version, dataRaw)
|
||||
injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})`
|
||||
} catch (err) {
|
||||
|
|
@ -225,8 +314,14 @@ 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')
|
||||
const compressedFilePath = './generated/mc-data-compressed.js'
|
||||
fs.writeFileSync(compressedFilePath, `export default ${JSON.stringify(minizedCompressed)}`, 'utf8')
|
||||
|
||||
const mcAssets = JSON.stringify(require('mc-assets/dist/blockStatesModels.json'))
|
||||
fs.writeFileSync('./generated/mc-assets-compressed.js', `export default ${JSON.stringify(compressToBase64(mcAssets))}`, 'utf8')
|
||||
|
||||
// const modelsObj = fs.readFileSync('./prismarine-renderer/viewer/lib/entity/exportedModels.js')
|
||||
// const models =
|
||||
}
|
||||
|
||||
console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileSync(filePath) / 1000 / 1000)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import assert from 'assert'
|
||||
import JsonOptimizer from '../src/optimizeJson';
|
||||
import JsonOptimizer, { restoreMinecraftData } from '../src/optimizeJson';
|
||||
import fs from 'fs'
|
||||
import minecraftData from 'minecraft-data'
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ const json = JSON.parse(fs.readFileSync('./generated/minecraft-data-optimized.js
|
|||
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
|
||||
|
||||
const validateData = (ver, type) => {
|
||||
const target = JsonOptimizer.restoreData(structuredClone(json[type]), ver)
|
||||
const target = restoreMinecraftData(structuredClone(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`)
|
||||
|
|
@ -92,8 +92,8 @@ const checkKeys = (source, diffing, isUniq = true, msg = '', redundantIsOk = fal
|
|||
}
|
||||
|
||||
// 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;
|
||||
const oldId = JsonOptimizer.restoreData(json['blocks'], '1.20', undefined).find(x => x.name === 'brown_stained_glass').id;
|
||||
const newId = JsonOptimizer.restoreData(json['blocks'], '1.20.4', undefined).find(x => x.name === 'brown_stained_glass').id;
|
||||
assert(oldId !== newId)
|
||||
// test all types + all versions
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
// import { versionsByMinecraftVersion } from 'minecraft-data'
|
||||
// import minecraftInitialDataJson from '../generated/minecraft-initial-data.json'
|
||||
import { AuthenticatedAccount } from './react/serversStorage'
|
||||
import { downloadSoundsIfNeeded } from './sounds/botSoundSystem'
|
||||
import { options } from './optionsStorage'
|
||||
import MinecraftData from 'minecraft-data'
|
||||
import PrismarineBlock from 'prismarine-block'
|
||||
import PrismarineItem from 'prismarine-item'
|
||||
import pathfinder from 'mineflayer-pathfinder'
|
||||
import { importLargeData } from '../generated/large-data-aliases'
|
||||
import { miscUiState } from './globalState'
|
||||
import supportedVersions from './supportedVersions.mjs'
|
||||
import { options } from './optionsStorage'
|
||||
import { downloadSoundsIfNeeded } from './sounds/botSoundSystem'
|
||||
import { AuthenticatedAccount } from './react/serversStorage'
|
||||
|
||||
export type ConnectOptions = {
|
||||
server?: string
|
||||
|
|
@ -30,7 +36,7 @@ export type ConnectOptions = {
|
|||
|
||||
export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVersionSelect) => {
|
||||
if (autoVersionSelect === 'auto') {
|
||||
return '1.20.4'
|
||||
return '1.19.4'
|
||||
}
|
||||
if (autoVersionSelect === 'latest') {
|
||||
return supportedVersions.at(-1)!
|
||||
|
|
@ -38,7 +44,8 @@ export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVers
|
|||
return autoVersionSelect
|
||||
}
|
||||
|
||||
export const downloadMcDataOnConnect = async (version: string) => {
|
||||
export const loadMinecraftData = async (version: string, importBlockstatesModels = false) => {
|
||||
await window._LOAD_MC_DATA()
|
||||
// setLoadingScreenStatus(`Loading data for ${version}`)
|
||||
// // todo expose cache
|
||||
// // const initialDataVersion = Object.keys(minecraftInitialDataJson)[0]!
|
||||
|
|
@ -47,8 +54,16 @@ export const downloadMcDataOnConnect = async (version: string) => {
|
|||
// // versionsByMinecraftVersion.pc[initialDataVersion]!.dataVersion!++
|
||||
// // }
|
||||
|
||||
// await window._MC_DATA_RESOLVER.promise // ensure data is loaded
|
||||
// miscUiState.loadedDataVersion = version
|
||||
const mcData = MinecraftData(version)
|
||||
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
|
||||
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
||||
window.loadedData = mcData
|
||||
window.pathfinder = pathfinder
|
||||
miscUiState.loadedDataVersion = version
|
||||
|
||||
if (importBlockstatesModels) {
|
||||
viewer.world.blockstatesModels = await importLargeData('blockStatesModels')
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadAllMinecraftData = async () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
import fs from 'fs'
|
||||
import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree'
|
||||
import { enable, disable, enabled } from 'debug'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
||||
window.Vec3 = Vec3
|
||||
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
||||
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
||||
if (!newPos) return
|
||||
|
|
|
|||
19
src/index.ts
19
src/index.ts
|
|
@ -21,8 +21,6 @@ import { Duplex } from 'stream'
|
|||
|
||||
import './scaleInterface'
|
||||
import { initWithRenderer } from './topRightStats'
|
||||
import PrismarineBlock from 'prismarine-block'
|
||||
import PrismarineItem from 'prismarine-item'
|
||||
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import './reactUi'
|
||||
|
|
@ -37,7 +35,6 @@ import net from 'net'
|
|||
import mineflayer from 'mineflayer'
|
||||
import { WorldDataEmitter, Viewer } from 'renderer/viewer'
|
||||
import pathfinder from 'mineflayer-pathfinder'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
||||
import * as THREE from 'three'
|
||||
import MinecraftData from 'minecraft-data'
|
||||
|
|
@ -87,7 +84,7 @@ import { saveToBrowserMemory } from './react/PauseScreen'
|
|||
import { ViewerWrapper } from 'renderer/viewer/lib/viewerWrapper'
|
||||
import './devReload'
|
||||
import './water'
|
||||
import { ConnectOptions, downloadMcDataOnConnect, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect'
|
||||
import { ConnectOptions, loadMinecraftData, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect'
|
||||
import { subscribe } from 'valtio'
|
||||
import { signInMessageState } from './react/SignInMessageProvider'
|
||||
import { updateServerConnectionHistory } from './react/serversStorage'
|
||||
|
|
@ -109,6 +106,7 @@ import { localRelayServerPlugin } from './mineflayer/plugins/packetsRecording'
|
|||
import { createFullScreenProgressReporter } from './core/progressReporter'
|
||||
import { getItemModelName } from './resourcesManager'
|
||||
import { getProtocolClientGetter } from './protocolWorker/protocolMain'
|
||||
import { importLargeData } from '../generated/large-data-aliases'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
|
@ -410,7 +408,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
await progress.executeWithMessage(
|
||||
'Applying user-installed resource pack',
|
||||
async () => {
|
||||
await downloadMcDataOnConnect(version)
|
||||
await loadMinecraftData(version)
|
||||
try {
|
||||
await resourcepackReload(version)
|
||||
} catch (err) {
|
||||
|
|
@ -426,7 +424,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
await progress.executeWithMessage(
|
||||
'Loading minecraft models',
|
||||
async () => {
|
||||
viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json')
|
||||
viewer.world.blockstatesModels = await importLargeData('blockStatesModels')
|
||||
void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
miscUiState.loadedDataVersion = version
|
||||
}
|
||||
|
|
@ -693,13 +691,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
|
||||
bot.once('login', () => {
|
||||
setLoadingScreenStatus('Loading world')
|
||||
|
||||
const mcData = MinecraftData(bot.version)
|
||||
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
|
||||
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
||||
window.loadedData = mcData
|
||||
window.Vec3 = Vec3
|
||||
window.pathfinder = pathfinder
|
||||
})
|
||||
|
||||
const start = Date.now()
|
||||
|
|
@ -826,8 +817,8 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
miscUiState.gameLoaded = true
|
||||
miscUiState.loadedServerIndex = connectOptions.serverIndex ?? ''
|
||||
customEvents.emit('gameLoaded')
|
||||
setLoadingScreenStatus(undefined)
|
||||
progress.end()
|
||||
setLoadingScreenStatus(undefined)
|
||||
})
|
||||
|
||||
if (singleplayer && connectOptions.serverOverrides.worldFolder) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,102 @@ type SourceData = {
|
|||
__IS_OPTIMIZED__: true
|
||||
}
|
||||
|
||||
function getRecipesProcessorProcessRecipes (items, blocks) {
|
||||
return (current) => {
|
||||
// can require the same multiple times per different versions
|
||||
const itemsIdsMap = Object.fromEntries(items.map((b) => [b.name, b.id]))
|
||||
const blocksIdsMap = Object.fromEntries(blocks.map((b) => [b.name, b.id]))
|
||||
const keys = Object.keys(current)
|
||||
for (const key of keys) {
|
||||
if (key === '_proccessed') {
|
||||
delete current[key]
|
||||
continue
|
||||
}
|
||||
const mapId = (id) => {
|
||||
if (typeof id !== 'string' && typeof id !== 'number') throw new Error('Incorrect type')
|
||||
const mapped = itemsIdsMap[id] ?? blocksIdsMap[id]
|
||||
if (!mapped) {
|
||||
throw new Error(`No item/block name with id ${id}`)
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
const processRecipe = (obj) => {
|
||||
// if (!obj) return
|
||||
// if (Array.isArray(obj)) {
|
||||
// obj.forEach((id, i) => {
|
||||
// obj[i] = mapId(obj[id])
|
||||
// })
|
||||
// } else if (obj && typeof obj === 'object') {
|
||||
// if (!'count metadata id'.split(' ').every(x => x in obj)) {
|
||||
// throw new Error(`process error: Unknown deep object pattern: ${JSON.stringify(obj)}`)
|
||||
// }
|
||||
// obj.id = mapId(obj.id)
|
||||
// } else {
|
||||
// throw new Error('unknown type')
|
||||
// }
|
||||
const parseRecipeItem = (item) => {
|
||||
if (typeof item === 'number' || typeof item === 'string') return mapId(item)
|
||||
if (Array.isArray(item)) return [mapId(item), ...item.slice(1)]
|
||||
if (!item) {
|
||||
return item
|
||||
}
|
||||
if ('id' in item) {
|
||||
item.id = mapId(item.id)
|
||||
return item
|
||||
}
|
||||
throw new Error('unhandled')
|
||||
}
|
||||
const maybeProccessShape = (shape) => {
|
||||
if (!shape) return
|
||||
for (const shapeRow of shape) {
|
||||
for (const [i, item] of shapeRow.entries()) {
|
||||
shapeRow[i] = parseRecipeItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj.result) obj.result = parseRecipeItem(obj.result)
|
||||
maybeProccessShape(obj.inShape)
|
||||
maybeProccessShape(obj.outShape)
|
||||
if (obj.ingredients) {
|
||||
for (const [i, ingredient] of obj.ingredients.entries()) {
|
||||
obj.ingredients[i] = parseRecipeItem(ingredient)
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
const name = mapId(key)
|
||||
for (const [i, recipe] of current[key].entries()) {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
processRecipe(recipe)
|
||||
} catch (err) {
|
||||
// console.warn(`${version} [warn] Removing incorrect recipe: ${err}`)
|
||||
// delete current[i]
|
||||
throw err
|
||||
}
|
||||
}
|
||||
current[name] = current[key]
|
||||
} catch (err) {
|
||||
// console.warn(`${version} [warn] Removing incorrect recipe: ${err}`)
|
||||
throw err
|
||||
}
|
||||
delete current[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const restoreMinecraftData = (allVersionData: any, type: string, version: string) => {
|
||||
let restorer
|
||||
if (type === 'recipes') {
|
||||
restorer = getRecipesProcessorProcessRecipes(
|
||||
JsonOptimizer.restoreData(allVersionData.items, version, undefined),
|
||||
JsonOptimizer.restoreData(allVersionData.blocks, version, undefined),
|
||||
)
|
||||
}
|
||||
return JsonOptimizer.restoreData(allVersionData[type], version, restorer)
|
||||
}
|
||||
|
||||
export default class JsonOptimizer {
|
||||
keys = {} as IdMap
|
||||
idToKey = {} as Record<number, string>
|
||||
|
|
@ -146,6 +242,7 @@ export default class JsonOptimizer {
|
|||
|
||||
recordDiff (key: string, diffObj: string) {
|
||||
const diff = this.diffObj(diffObj)
|
||||
// problem is that 274 key 10.20.6 no removed keys in diff created
|
||||
this.diffs[key] = diff
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +255,7 @@ export default class JsonOptimizer {
|
|||
return true
|
||||
}
|
||||
|
||||
static restoreData ({ keys, properties, source, arrKey, diffs }: SourceData, targetKey: string) {
|
||||
static restoreData ({ keys, properties, source, arrKey, diffs }: SourceData, targetKey: string, dataRestorer: ((data) => void) | undefined) {
|
||||
// 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]]))
|
||||
|
|
@ -204,11 +301,14 @@ export default class JsonOptimizer {
|
|||
break
|
||||
}
|
||||
}
|
||||
let data
|
||||
if (arrKey) {
|
||||
return Object.values(dataByKeys)
|
||||
data = Object.values(dataByKeys)
|
||||
} else {
|
||||
return Object.fromEntries(Object.entries(dataByKeys).map(([key, val]) => [keysById[key], val]))
|
||||
data = Object.fromEntries(Object.entries(dataByKeys).map(([key, val]) => [keysById[key], val]))
|
||||
}
|
||||
dataRestorer?.(data)
|
||||
return data
|
||||
}
|
||||
|
||||
static getByArrKey (item: any, arrKey: string) {
|
||||
|
|
|
|||
|
|
@ -163,10 +163,12 @@ const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
|
|||
|
||||
export type AppOptions = typeof defaultOptions
|
||||
|
||||
// when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts
|
||||
const localStorageKey = process.env.SINGLE_FILE_BUILD ? 'minecraftWebClientOptions' : 'options'
|
||||
export const options: AppOptions = proxy({
|
||||
...defaultOptions,
|
||||
...initialAppConfig.defaultSettings,
|
||||
...migrateOptions(JSON.parse(localStorage.options || '{}')),
|
||||
...migrateOptions(JSON.parse(localStorage[localStorageKey] || '{}')),
|
||||
...qsOptions
|
||||
})
|
||||
|
||||
|
|
@ -185,7 +187,7 @@ Object.defineProperty(window, 'debugChangedOptions', {
|
|||
subscribe(options, () => {
|
||||
// Don't save disabled settings to localStorage
|
||||
const saveOptions = omitObj(options, [...disabledSettings.value] as any)
|
||||
localStorage.options = JSON.stringify(saveOptions)
|
||||
localStorage[localStorageKey] = JSON.stringify(saveOptions)
|
||||
})
|
||||
|
||||
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void
|
||||
|
|
|
|||
180
src/panorama.ts
180
src/panorama.ts
|
|
@ -1,78 +1,47 @@
|
|||
//@ts-check
|
||||
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import * as THREE from 'three'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { EntityMesh } from 'renderer/viewer/lib/entity/EntityMesh'
|
||||
import { fromTexturePackPath, resourcePackState } from './resourcePack'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { WorldDataEmitter } from 'renderer/viewer'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { getSyncWorld } from 'renderer/playground/shared'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { miscUiState } from './globalState'
|
||||
import { loadMinecraftData } from './connect'
|
||||
|
||||
let panoramaCubeMap
|
||||
let shouldDisplayPanorama = false
|
||||
let panoramaUsesResourcePack = null as boolean | null
|
||||
|
||||
const panoramaFiles = [
|
||||
'panorama_1.png', // WS
|
||||
'panorama_3.png', // ES
|
||||
'panorama_4.png', // Up
|
||||
'panorama_5.png', // Down
|
||||
'panorama_0.png', // NS
|
||||
'panorama_2.png' // SS
|
||||
'panorama_3.png', // right (+x)
|
||||
'panorama_1.png', // left (-x)
|
||||
'panorama_4.png', // top (+y)
|
||||
'panorama_5.png', // bottom (-y)
|
||||
'panorama_0.png', // front (+z)
|
||||
'panorama_2.png', // back (-z)
|
||||
]
|
||||
|
||||
const panoramaResourcePackPath = 'assets/minecraft/textures/gui/title/background'
|
||||
const possiblyLoadPanoramaFromResourcePack = async (file) => {
|
||||
let base64Texture
|
||||
if (panoramaUsesResourcePack) {
|
||||
try {
|
||||
// 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('background', file)
|
||||
}
|
||||
|
||||
const updateResourcePackSupportPanorama = async () => {
|
||||
try {
|
||||
// TODO!
|
||||
// await fs.promises.readFile(fromTexturePackPath(join(panoramaResourcePackPath, panoramaFiles[0])), 'base64')
|
||||
panoramaUsesResourcePack = true
|
||||
} catch (err) {
|
||||
panoramaUsesResourcePack = false
|
||||
}
|
||||
}
|
||||
|
||||
watchValue(miscUiState, m => {
|
||||
if (m.fsReady) {
|
||||
// Also adds panorama on app load here
|
||||
watchValue(resourcePackState, async (s) => {
|
||||
const oldState = panoramaUsesResourcePack
|
||||
const newState = s.resourcePackInstalled && (await updateResourcePackSupportPanorama(), panoramaUsesResourcePack)
|
||||
if (newState === oldState) return
|
||||
removePanorama()
|
||||
void addPanoramaCubeMap()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
subscribeKey(miscUiState, 'loadedDataVersion', () => {
|
||||
if (miscUiState.loadedDataVersion) removePanorama()
|
||||
else void addPanoramaCubeMap()
|
||||
})
|
||||
let unloadPanoramaCallbacks = [] as Array<() => void>
|
||||
|
||||
// Menu panorama background
|
||||
// TODO-low use abort controller
|
||||
export async function addPanoramaCubeMap () {
|
||||
if (panoramaCubeMap || miscUiState.loadedDataVersion || options.disableAssets) return
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0) // wait for viewer to be initialized
|
||||
})
|
||||
viewer.camera.fov = 85
|
||||
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
||||
void initDemoWorld()
|
||||
return
|
||||
}
|
||||
|
||||
shouldDisplayPanorama = true
|
||||
|
||||
let time = 0
|
||||
viewer.camera.fov = 85
|
||||
viewer.camera.near = 0.05
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.camera.position.set(0, 0, 0)
|
||||
|
|
@ -81,12 +50,25 @@ export async function addPanoramaCubeMap () {
|
|||
|
||||
const loader = new THREE.TextureLoader()
|
||||
const panorMaterials = [] as THREE.MeshBasicMaterial[]
|
||||
await updateResourcePackSupportPanorama()
|
||||
for (const file of panoramaFiles) {
|
||||
const texture = loader.load(join('background', file))
|
||||
|
||||
// Instead of using repeat/offset to flip, we'll use the texture matrix
|
||||
texture.matrixAutoUpdate = false
|
||||
texture.matrix.set(
|
||||
-1, 0, 1, 0, 1, 0, 0, 0, 1
|
||||
)
|
||||
|
||||
texture.wrapS = THREE.ClampToEdgeWrapping // Changed from RepeatWrapping
|
||||
texture.wrapT = THREE.ClampToEdgeWrapping // Changed from RepeatWrapping
|
||||
texture.minFilter = THREE.LinearFilter
|
||||
texture.magFilter = THREE.LinearFilter
|
||||
|
||||
panorMaterials.push(new THREE.MeshBasicMaterial({
|
||||
map: loader.load(await possiblyLoadPanoramaFromResourcePack(file)),
|
||||
map: texture,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -120,12 +102,94 @@ export async function addPanoramaCubeMap () {
|
|||
panoramaCubeMap = group
|
||||
}
|
||||
|
||||
subscribeKey(miscUiState, 'fsReady', () => {
|
||||
if (miscUiState.fsReady) {
|
||||
// don't do it earlier to load fs and display menu faster
|
||||
void addPanoramaCubeMap()
|
||||
}
|
||||
})
|
||||
|
||||
export function removePanorama () {
|
||||
for (const unloadPanoramaCallback of unloadPanoramaCallbacks) {
|
||||
unloadPanoramaCallback()
|
||||
}
|
||||
unloadPanoramaCallbacks = []
|
||||
viewer.camera.fov = options.fov
|
||||
shouldDisplayPanorama = false
|
||||
if (!panoramaCubeMap) return
|
||||
viewer.camera.fov = options.fov
|
||||
viewer.camera.near = 0.1
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.scene.remove(panoramaCubeMap)
|
||||
panoramaCubeMap = null
|
||||
}
|
||||
|
||||
const initDemoWorld = async () => {
|
||||
const abortController = new AbortController()
|
||||
unloadPanoramaCallbacks.push(() => {
|
||||
abortController.abort()
|
||||
})
|
||||
const version = '1.21.4'
|
||||
console.time(`load ${version} mc-data`)
|
||||
await loadMinecraftData(version, true)
|
||||
console.timeEnd(`load ${version} mc-data`)
|
||||
if (abortController.signal.aborted) return
|
||||
console.time('load scene')
|
||||
const world = getSyncWorld(version)
|
||||
const PrismarineBlock = require('prismarine-block')
|
||||
const Block = PrismarineBlock(version)
|
||||
const fullBlocks = loadedData.blocksArray.filter(block => {
|
||||
// if (block.name.includes('leaves')) return false
|
||||
if (/* !block.name.includes('wool') && */!block.name.includes('stained_glass')/* && !block.name.includes('terracotta') */) return false
|
||||
const b = Block.fromStateId(block.defaultState, 0)
|
||||
if (b.shapes?.length !== 1) return false
|
||||
const shape = b.shapes[0]
|
||||
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
||||
})
|
||||
const Z = -15
|
||||
const sizeX = 100
|
||||
const sizeY = 100
|
||||
for (let x = -sizeX; x < sizeX; x++) {
|
||||
for (let y = -sizeY; y < sizeY; y++) {
|
||||
const block = fullBlocks[Math.floor(Math.random() * fullBlocks.length)]
|
||||
world.setBlockStateId(new Vec3(x, y, Z), block.defaultState)
|
||||
}
|
||||
}
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.camera.position.set(0.5, sizeY / 2 + 0.5, 0.5)
|
||||
viewer.camera.rotation.set(0, 0, 0)
|
||||
const initPos = new Vec3(...viewer.camera.position.toArray())
|
||||
const worldView = new WorldDataEmitter(world, 2, initPos)
|
||||
// worldView.addWaitTime = 0
|
||||
await viewer.world.setVersion(version)
|
||||
if (abortController.signal.aborted) return
|
||||
viewer.connect(worldView)
|
||||
void worldView.init(initPos)
|
||||
await viewer.world.waitForChunksToRender()
|
||||
if (abortController.signal.aborted) return
|
||||
// add small camera rotation to side on mouse move depending on absolute position of the cursor
|
||||
const { camera } = viewer
|
||||
const initX = camera.position.x
|
||||
const initY = camera.position.y
|
||||
let prevTwin: tweenJs.Tween<THREE.Vector3> | undefined
|
||||
document.body.addEventListener('pointermove', (e) => {
|
||||
if (e.pointerType !== 'mouse') return
|
||||
const pos = new THREE.Vector2(e.clientX, e.clientY)
|
||||
const SCALE = 0.2
|
||||
/* -0.5 - 0.5 */
|
||||
const xRel = pos.x / window.innerWidth - 0.5
|
||||
const yRel = -(pos.y / window.innerHeight - 0.5)
|
||||
prevTwin?.stop()
|
||||
const to = {
|
||||
x: initX + (xRel * SCALE),
|
||||
y: initY + (yRel * SCALE)
|
||||
}
|
||||
prevTwin = new tweenJs.Tween(camera.position).to(to, 0) // todo use the number depending on diff // todo use the number depending on diff
|
||||
// prevTwin.easing(tweenJs.Easing.Exponential.InOut)
|
||||
prevTwin.start()
|
||||
camera.updateProjectionMatrix()
|
||||
}, {
|
||||
signal: abortController.signal
|
||||
})
|
||||
|
||||
console.timeEnd('load scene')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,16 @@ export default ({
|
|||
<>
|
||||
{showReconnect && onReconnect && <Button label="Reconnect" onClick={onReconnect} />}
|
||||
{actionsSlot}
|
||||
<Button onClick={() => window.location.reload()} label="Reset App (recommended)" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (location.search) {
|
||||
location.search = ''
|
||||
} else {
|
||||
window.location.reload()
|
||||
}
|
||||
}}
|
||||
label="Reset App (recommended)"
|
||||
/>
|
||||
{!lockConnect && backAction && <Button label="Back" onClick={backAction} />}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ export default () => {
|
|||
const [versionTitle, setVersionTitle] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
||||
setVersionStatus('(single file build)')
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
setVersionStatus('(dev)')
|
||||
} else {
|
||||
fetch('./version.txt').then(async (f) => {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ export default () => {
|
|||
|
||||
useEffect(() => {
|
||||
function requestUpdate () {
|
||||
// Placeholder for requestUpdate logic
|
||||
setPlayers(bot.players)
|
||||
setPlayers(bot?.players ?? {})
|
||||
}
|
||||
|
||||
bot.on('playerUpdated', () => requestUpdate())
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { useState } from 'react'
|
||||
import AddServerOrConnect from './AddServerOrConnect'
|
||||
import ServersList from './ServersList'
|
||||
|
||||
const meta: Meta<typeof ServersList> = {
|
||||
component: ServersList,
|
||||
render (args) {
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [username, setUsername] = useState('')
|
||||
|
||||
return addOpen ?
|
||||
<AddServerOrConnect
|
||||
onBack={() => {
|
||||
setAddOpen(false)
|
||||
}}
|
||||
accounts={['testting']}
|
||||
onConfirm={(info) => {
|
||||
console.log('add server', info)
|
||||
}}
|
||||
/> :
|
||||
<ServersList
|
||||
worldData={[{
|
||||
name: 'test',
|
||||
title: 'Server',
|
||||
formattedTextOverride: 'play yes',
|
||||
}]}
|
||||
joinServer={(ip) => {
|
||||
console.log('joinServer', ip)
|
||||
}}
|
||||
initialProxies={{
|
||||
proxies: ['localhost', 'mc.hypixel.net'],
|
||||
selected: 'localhost',
|
||||
}}
|
||||
updateProxies={newData => {
|
||||
console.log('setProxies', newData)
|
||||
}}
|
||||
onWorldAction={() => { }}
|
||||
onGeneralAction={(action) => {
|
||||
if (action === 'create') {
|
||||
setAddOpen(true)
|
||||
}
|
||||
}}
|
||||
username={username}
|
||||
setUsername={setUsername}
|
||||
/>
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof ServersList>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
},
|
||||
}
|
||||
|
|
@ -86,6 +86,35 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => (customServersList ? [] : getInitialServersList()))
|
||||
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
|
||||
// Add keyboard handler for moving servers
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (['input', 'textarea', 'select'].includes((e.target as HTMLElement)?.tagName?.toLowerCase())) return
|
||||
if (!e.shiftKey || selectedIndex === undefined) return
|
||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
const newIndex = e.key === 'ArrowUp'
|
||||
? Math.max(0, selectedIndex - 1)
|
||||
: Math.min(serversList.length - 1, selectedIndex + 1)
|
||||
|
||||
if (newIndex === selectedIndex) return
|
||||
|
||||
// Move server in the list
|
||||
const newList = [...serversList]
|
||||
const oldItem = newList[selectedIndex]
|
||||
newList[selectedIndex] = newList[newIndex]
|
||||
newList[newIndex] = oldItem
|
||||
|
||||
setServersList(newList)
|
||||
setSelectedIndex(newIndex)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [selectedIndex, serversList])
|
||||
|
||||
useEffect(() => {
|
||||
if (customServersList) {
|
||||
setServersList(customServersList.map(row => {
|
||||
|
|
@ -105,10 +134,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
setNewServersList(serversList)
|
||||
}, [serversList])
|
||||
|
||||
const serversListSorted = useMemo(() => serversList.map((server, index) => ({ ...server, index })), [serversList])
|
||||
// by lastJoined
|
||||
const serversListSorted = useMemo(() => {
|
||||
return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
||||
}, [serversList])
|
||||
// const serversListSorted = useMemo(() => {
|
||||
// return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
||||
// }, [serversList])
|
||||
|
||||
const isEditScreenModal = useIsModalActive('editServer')
|
||||
|
||||
|
|
@ -313,6 +343,13 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
// find and update
|
||||
const server = serversList.find(s => s.ip === ip)
|
||||
if (server) {
|
||||
// move to top
|
||||
const newList = [...serversList]
|
||||
const index = newList.indexOf(server)
|
||||
const thisItem = newList[index]
|
||||
newList.splice(index, 1)
|
||||
newList.unshift(thisItem)
|
||||
|
||||
server.lastJoined = Date.now()
|
||||
server.numConnects = (server.numConnects || 0) + 1
|
||||
setNewServersList(serversList)
|
||||
|
|
@ -369,7 +406,8 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
worldNameRight: additional?.textNameRight ?? '',
|
||||
worldNameRightGrayed: additional?.textNameRightGrayed ?? '',
|
||||
iconSrc: additional?.icon,
|
||||
offline: additional?.offline
|
||||
offline: additional?.offline,
|
||||
group: 'Custom Servers'
|
||||
}
|
||||
})}
|
||||
initialProxies={{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { ProseMirrorView } from './prosemirror-markdown'
|
||||
|
||||
import SignEditor from './SignEditor'
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ type Story = StoryObj<typeof SignEditor>
|
|||
export const Primary: Story = {
|
||||
args: {
|
||||
handleInput () {},
|
||||
isWysiwyg: false
|
||||
ProseMirrorView
|
||||
},
|
||||
parameters: {
|
||||
noScaling: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { focusable } from 'tabbable'
|
||||
import markdownToFormattedText from '../markdownToFormattedText'
|
||||
import { ProseMirrorView } from './prosemirror-markdown'
|
||||
import type { ProseMirrorView } from './prosemirror-markdown'
|
||||
import Button from './Button'
|
||||
import 'prosemirror-view/style/prosemirror.css'
|
||||
import 'prosemirror-menu/style/menu.css'
|
||||
|
|
@ -12,7 +12,7 @@ const imageSource = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAMCAYAA
|
|||
|
||||
type Props = {
|
||||
handleInput: (target: HTMLInputElement) => void,
|
||||
isWysiwyg: boolean,
|
||||
ProseMirrorView: typeof ProseMirrorView,
|
||||
handleClick?: (view: ResultType) => void
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +22,8 @@ export type ResultType = {
|
|||
dataText: string[]
|
||||
}
|
||||
|
||||
export default ({ handleInput, isWysiwyg, handleClick }: Props) => {
|
||||
export default ({ handleInput, ProseMirrorView, handleClick }: Props) => {
|
||||
const isWysiwyg = !!ProseMirrorView
|
||||
const prosemirrorContainer = useRef(null)
|
||||
const editorView = useRef<ProseMirrorView | null>(null)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export default () => {
|
|||
const text = useRef<string[]>(['', '', '', ''])
|
||||
const [enableWysiwyg, setEnableWysiwyg] = useState(false)
|
||||
const isModalActive = useIsModalActive('signs-editor-screen')
|
||||
const [proseMirrorView, setProseMirrorView] = useState(null as any)
|
||||
|
||||
const handleClick = (result: ResultType) => {
|
||||
hideModal({ reactType: 'signs-editor-screen' })
|
||||
|
|
@ -72,8 +73,14 @@ export default () => {
|
|||
setEnableWysiwyg(false)
|
||||
}
|
||||
})
|
||||
|
||||
if (!process.env.SINGLE_FILE_BUILD) {
|
||||
void import('./prosemirror-markdown').then(({ ProseMirrorView }) => {
|
||||
setProseMirrorView(() => ProseMirrorView)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!isModalActive) return null
|
||||
return <SignEditor isWysiwyg={enableWysiwyg} handleInput={handleInput} handleClick={handleClick} />
|
||||
return <SignEditor ProseMirrorView={enableWysiwyg ? proseMirrorView : undefined} handleInput={handleInput} handleClick={handleClick} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'classnames'
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
// todo optimize size
|
||||
import missingWorldPreview from 'mc-assets/dist/other-textures/latest/gui/presets/isles.png'
|
||||
|
|
@ -29,6 +29,21 @@ export interface WorldProps {
|
|||
onInteraction?(interaction: 'enter' | 'space')
|
||||
elemRef?: React.Ref<HTMLDivElement>
|
||||
offline?: boolean
|
||||
group?: string
|
||||
}
|
||||
|
||||
const GroupHeader = ({ name, count, expanded, onToggle }: { name: string, count: number, expanded: boolean, onToggle: () => void }) => {
|
||||
return <div
|
||||
className={styles.world_root}
|
||||
style={{ background: 'none', cursor: 'pointer', height: 'auto', fontSize: '8px' }}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', color: '#bcbcbc' }}>
|
||||
<span>{expanded ? '▼' : '▶'}</span>
|
||||
<span>{name}</span>
|
||||
<span>({count})</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, worldNameRightGrayed, elemRef, offline }: WorldProps & { ref?: React.Ref<HTMLDivElement> }) => {
|
||||
|
|
@ -159,6 +174,7 @@ export default ({
|
|||
|
||||
const [search, setSearch] = useState('')
|
||||
const [focusedWorld, setFocusedWorld] = useState(defaultSelectedRow === undefined ? '' : worldData?.[defaultSelectedRow]?.name ?? '')
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({})
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedWorld('')
|
||||
|
|
@ -179,6 +195,13 @@ export default ({
|
|||
}
|
||||
const isSmallWidth = useIsSmallWidth()
|
||||
|
||||
const toggleGroup = (groupName: string) => {
|
||||
setExpandedGroups(prev => ({
|
||||
...prev,
|
||||
[groupName]: prev[groupName] === undefined ? false : !prev[groupName]
|
||||
}))
|
||||
}
|
||||
|
||||
return <div ref={containerRef} hidden={hidden}>
|
||||
<div className="dirt-bg" />
|
||||
<div className={classNames('fullscreen', styles.root)}>
|
||||
|
|
@ -214,22 +237,42 @@ export default ({
|
|||
}
|
||||
{
|
||||
worldData
|
||||
? worldData.filter(data => data.title.toLowerCase().includes(search.toLowerCase())).map(({ name, size, detail, ...rest }, index) => (
|
||||
<World
|
||||
{...rest}
|
||||
size={size}
|
||||
name={name}
|
||||
elemRef={el => { worldRefs.current[name] = el }}
|
||||
onFocus={row => onRowSelectHandler(row, index)}
|
||||
isFocused={focusedWorld === name}
|
||||
key={name}
|
||||
onInteraction={(interaction) => {
|
||||
if (interaction === 'enter') onWorldAction('load', name)
|
||||
else if (interaction === 'space') firstButton.current?.focus()
|
||||
}}
|
||||
detail={detail}
|
||||
/>
|
||||
))
|
||||
? (() => {
|
||||
const filtered = worldData.filter(data => data.title.toLowerCase().includes(search.toLowerCase()))
|
||||
const groups = filtered.reduce<Record<string, WorldProps[]>>((acc, world) => {
|
||||
const group = world.group || ''
|
||||
if (!acc[group]) acc[group] = []
|
||||
acc[group].push(world)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return Object.entries(groups).map(([groupName, worlds]) => (
|
||||
<React.Fragment key={groupName}>
|
||||
<GroupHeader
|
||||
name={groupName}
|
||||
count={worlds.length}
|
||||
expanded={expandedGroups[groupName] ?? true}
|
||||
onToggle={() => toggleGroup(groupName)}
|
||||
/>
|
||||
{(expandedGroups[groupName] ?? true) && worlds.map(({ name, size, detail, ...rest }, index) => (
|
||||
<World
|
||||
{...rest}
|
||||
size={size}
|
||||
name={name}
|
||||
elemRef={el => { worldRefs.current[name] = el }}
|
||||
onFocus={row => onRowSelectHandler(row, index)}
|
||||
isFocused={focusedWorld === name}
|
||||
key={name}
|
||||
onInteraction={(interaction) => {
|
||||
if (interaction === 'enter') onWorldAction('load', name)
|
||||
else if (interaction === 'space') firstButton.current?.focus()
|
||||
}}
|
||||
detail={detail}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))
|
||||
})()
|
||||
: <div style={{
|
||||
fontSize: 10,
|
||||
color: error ? 'red' : 'lightgray',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const readWorlds = (abortController: AbortController) => {
|
|||
iconSrc: iconBase64 ? `data:image/png;base64,${iconBase64}` : undefined,
|
||||
size,
|
||||
lastModified: levelDatStat.mtimeMs,
|
||||
group: 'IndexedDB Memory Worlds'
|
||||
} satisfies WorldProps & { lastModified?: number }
|
||||
}))).filter((x, i) => {
|
||||
if (x.status === 'rejected') {
|
||||
|
|
@ -283,10 +284,10 @@ const Inner = () => {
|
|||
return <Singleplayer
|
||||
error={error}
|
||||
isReadonly={selectedProvider === 'google' && (googleDriveReadonly || !isGoogleProviderReady || !selectedGoogleId)}
|
||||
providers={{
|
||||
local: 'Local',
|
||||
google: 'Google Drive',
|
||||
}}
|
||||
// providers={{
|
||||
// local: 'Local',
|
||||
// google: 'Google Drive',
|
||||
// }}
|
||||
disabledProviders={[...isGoogleDriveAvailable() ? [] : ['google']]}
|
||||
worldData={worlds}
|
||||
providerActions={providerActions}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { isCypress } from './standaloneUtils'
|
|||
|
||||
// might not resolve at all
|
||||
export const registerServiceWorker = async () => {
|
||||
if (!('serviceWorker' in navigator) || process.env.SINGLE_FILE_BUILD) return
|
||||
if (process.env.DISABLE_SERVICE_WORKER) return
|
||||
if (!('serviceWorker' in navigator)) return
|
||||
if (!isCypress() && process.env.NODE_ENV !== 'development') {
|
||||
return new Promise<void>(resolve => {
|
||||
window.addEventListener('load', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { toMajorVersion } from 'renderer/viewer/lib/simpleUtils'
|
||||
import JsonOptimizer from '../optimizeJson'
|
||||
import { restoreMinecraftData } from '../optimizeJson'
|
||||
// import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json'
|
||||
|
||||
const customResolver = () => {
|
||||
|
|
@ -22,7 +22,7 @@ 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'))
|
||||
optimizedDataResolver.resolve(await importLargeData('mcData'))
|
||||
}
|
||||
|
||||
// 30 seconds
|
||||
|
|
@ -53,7 +53,7 @@ const possiblyGetFromCache = (version: string) => {
|
|||
|
||||
const data = optimizedDataResolver.resolvedData[dataType]
|
||||
if (data.__IS_OPTIMIZED__) {
|
||||
allRestored[dataType] = JsonOptimizer.restoreData(data, version)
|
||||
allRestored[dataType] = restoreMinecraftData(optimizedDataResolver.resolvedData, dataType, version)
|
||||
} else {
|
||||
allRestored[dataType] = data[version] ?? data[toMajorVersion(version)]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue