diff --git a/.github/workflows/build-zip.yml b/.github/workflows/build-zip.yml new file mode 100644 index 00000000..cc472476 --- /dev/null +++ b/.github/workflows/build-zip.yml @@ -0,0 +1,43 @@ +name: Make Self Host Zip + +on: + workflow_dispatch: + +jobs: + build-and-bundle: + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout repository + uses: actions/checkout@master + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + + - name: Build project + run: pnpm build + + - name: Bundle server.js + run: | + pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'" + + - name: Create distribution package + run: | + mkdir -p package + cp -r dist package/ + cp bundled-server.js package/server.js + cd package + zip -r ../self-host.zip . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: self-host + path: self-host.zip diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index 665abb30..042302a4 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -32,6 +32,8 @@ jobs: echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 18c80e8c..6408c86a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -61,6 +61,8 @@ jobs: echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eb765648..5af8abab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,6 +30,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | @@ -43,19 +45,36 @@ jobs: with: run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod id: deploy - - run: | - pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # has possible output: tag - id: release - # has output + # publish to github - run: cp vercel.json .vercel/output/static/vercel.json - uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .vercel/output/static force_orphan: true + + - name: Build self-host version + run: pnpm build + - name: Bundle server.js + run: | + pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'" + + - name: Create zip package + run: | + mkdir -p package + cp -r dist package/ + cp bundled-server.js package/server.js + cd package + zip -r ../self-host.zip . + + - run: | + pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # has possible output: tag + id: release + + # has output - name: Set publishing config run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" env: diff --git a/Dockerfile b/Dockerfile index 4769141f..34641353 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN npm i -g pnpm@9.0.4 # Build arguments ARG DOWNLOAD_SOUNDS=false ARG DISABLE_SERVICE_WORKER=false +ARG CONFIG_JSON_SOURCE=REMOTE # TODO need flat --no-root-optional RUN node ./scripts/dockerPrepare.mjs RUN pnpm i @@ -22,8 +23,8 @@ RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs # ENTRYPOINT ["pnpm", "run", "run-all"] # only for prod -RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client \ - DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ +RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ + CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \ pnpm run build # ---- Run Stage ---- diff --git a/config.json b/config.json index 6aa86397..e48d758b 100644 --- a/config.json +++ b/config.json @@ -6,6 +6,10 @@ "peerJsServer": "", "peerJsServerFallback": "https://p2p.mcraft.fun", "promoteServers": [ + { + "ip": "ws://mcraft.ryzyn.xyz", + "version": "1.19.4" + }, { "ip": "ws://play.mcraft.fun" }, diff --git a/package.json b/package.json index 7cacae56..1a195884 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "web", "client" ], + "release": { + "attachReleaseFiles": "self-host.zip" + }, "publish": { "preset": { "publishOnlyIfChanged": true, @@ -145,8 +148,8 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", + "mc-assets": "^0.2.42", "mineflayer-mouse": "^0.0.5", - "mc-assets": "^0.2.37", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer", "mineflayer-pathfinder": "^2.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbbac60e..d5c2446f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,8 +350,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.37 - version: 0.2.37 + specifier: ^0.2.42 + version: 0.2.42 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8(@types/react@18.2.20)(react@18.2.0) @@ -3589,9 +3589,6 @@ packages: resolution: {integrity: sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==} engines: {node: '>=8'} - apl-image-packer@1.1.0: - resolution: {integrity: sha512-Pb1Q76cp8xpY8X4OqVrnk5v1/tB5kOtCzwgOnx8IxMNeekFh/eNUiUKeX5fvGNViZiLmuKAAQc5ICuBDspZ4cA==} - app-root-dir@1.0.2: resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} @@ -6571,8 +6568,11 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mc-assets@0.2.37: - resolution: {integrity: sha512-49tk3shwxsDoV0PrrbORZEKg613vUQPULgusWjXNl8JEma5y41LEo57D6q4aHliXsV3Gb9ThrkFf6hIb0WlY1Q==} + maxrects-packer@2.7.3: + resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==} + + mc-assets@0.2.42: + resolution: {integrity: sha512-j2D1RNYtB5Z9gFu9MVjyDBbiALI0mWZ3xW/A3PPefVAHm3HJ2T1vH+1XBov1spBGPl7u+Zo7mRXza3X0egbeOg==} engines: {node: '>=18.0.0'} mcraft-fun-mineflayer@0.1.8: @@ -13609,8 +13609,6 @@ snapshots: apache-md5@1.1.8: {} - apl-image-packer@1.1.0: {} - app-root-dir@1.0.2: {} aproba@2.0.0: @@ -17367,9 +17365,11 @@ snapshots: math-intrinsics@1.1.0: {} - mc-assets@0.2.37: + maxrects-packer@2.7.3: {} + + mc-assets@0.2.42: dependencies: - apl-image-packer: 1.1.0 + maxrects-packer: 2.7.3 zod: 3.24.1 mcraft-fun-mineflayer@0.1.8(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)): diff --git a/renderer/viewer/lib/guiRenderer.ts b/renderer/viewer/lib/guiRenderer.ts new file mode 100644 index 00000000..b197040e --- /dev/null +++ b/renderer/viewer/lib/guiRenderer.ts @@ -0,0 +1,275 @@ +// Import placeholders - replace with actual imports for your environment +import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate' +import { mat4, vec3 } from 'gl-matrix' +import { AssetsParser } from 'mc-assets/dist/assetsParser' +import { getLoadedImage } from 'mc-assets/dist/utils' +import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets' +import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores' +import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator' +import { proxy, ref } from 'valtio' +import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' + +export const activeGuiAtlas = proxy({ + atlas: null as null | { json, image }, +}) + +export const getNonFullBlocksModels = () => { + const version = viewer.world.texturesVersion ?? 'latest' + const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest + const blockModelsResolved = {} as Record + const itemsModelsResolved = {} as Record + const fullBlocksWithNonStandardDisplay = [] as string[] + const handledItemsWithDefinitions = new Set() + const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels)) + + const standardGuiDisplay = { + 'rotation': [ + 30, + 225, + 0 + ], + 'translation': [ + 0, + 0, + 0 + ], + 'scale': [ + 0.625, + 0.625, + 0.625 + ] + } + + const arrEqual = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i]) + const addModelIfNotFullblock = (name: string, model: BlockModelMcAssets) => { + if (blockModelsResolved[name]) return + if (!model?.elements?.length) return + const isFullBlock = model.elements.length === 1 && arrEqual(model.elements[0].from, [0, 0, 0]) && arrEqual(model.elements[0].to, [16, 16, 16]) + if (isFullBlock) return + model['display'] ??= {} + model['display']['gui'] ??= standardGuiDisplay + blockModelsResolved[name] = model + } + + for (const [name, definition] of Object.entries(itemsDefinitions)) { + const item = getItemDefinition(viewer.world.itemsDefinitionsStore, { + version, + name, + properties: { + 'minecraft:display_context': 'gui', + }, + }) + if (item) { + const { resolvedModel } = assetsParser.getResolvedModelsByModel((item.special ? name : item.model).replace('minecraft:', '')) ?? {} + if (resolvedModel) { + handledItemsWithDefinitions.add(name) + } + if (resolvedModel?.elements) { + + let hasStandardDisplay = true + if (resolvedModel['display']?.gui) { + hasStandardDisplay = + arrEqual(resolvedModel['display'].gui.rotation, standardGuiDisplay.rotation) + && arrEqual(resolvedModel['display'].gui.translation, standardGuiDisplay.translation) + && arrEqual(resolvedModel['display'].gui.scale, standardGuiDisplay.scale) + } + + addModelIfNotFullblock(name, resolvedModel) + + if (!blockModelsResolved[name] && !hasStandardDisplay) { + fullBlocksWithNonStandardDisplay.push(name) + } + const notSideLight = resolvedModel['gui_light'] && resolvedModel['gui_light'] !== 'side' + if (!hasStandardDisplay || notSideLight) { + blockModelsResolved[name] = resolvedModel + } + } + if (!blockModelsResolved[name] && item.tints && resolvedModel) { + resolvedModel['tints'] = item.tints + if (resolvedModel.elements) { + blockModelsResolved[name] = resolvedModel + } else { + itemsModelsResolved[name] = resolvedModel + } + } + } + } + + for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) { + if (handledItemsWithDefinitions.has(name)) { + continue + } + const resolvedModel = assetsParser.getResolvedModelFirst({ name: name.replace('minecraft:', ''), properties: {} }, true) + if (resolvedModel) { + addModelIfNotFullblock(name, resolvedModel[0]) + } + } + + return { + blockModelsResolved, + itemsModelsResolved + } +} + +// customEvents.on('gameLoaded', () => { +// const res = getNonFullBlocksModels() +// }) + +const RENDER_SIZE = 64 + +const generateItemsGui = async (models: Record, isItems = false) => { + const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage) + const canvasTemp = document.createElement('canvas') + canvasTemp.width = img.width + canvasTemp.height = img.height + canvasTemp.style.imageRendering = 'pixelated' + const ctx = canvasTemp.getContext('2d')! + ctx.imageSmoothingEnabled = false + ctx.drawImage(img, 0, 0) + + const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser! + const textureAtlas = new TextureAtlas( + ctx.getImageData(0, 0, img.width, img.height), + Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => { + return [key, [ + value.u, + value.v, + (value.u + (value.su ?? atlasParser.atlas.latest.suSv)), + (value.v + (value.sv ?? atlasParser.atlas.latest.suSv)), + ]] as [string, [number, number, number, number]] + })) + ) + + const PREVIEW_ID = Identifier.parse('preview:preview') + const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() } }, undefined) + + let modelData: any + let currentModelName: string | undefined + const resources: ItemRendererResources = { + getBlockModel (id) { + if (id.equals(PREVIEW_ID)) { + return BlockModel.fromJson(modelData ?? {}) + } + return null + }, + getTextureUV (texture) { + return textureAtlas.getTextureUV(texture.toString().slice(1).split('/').slice(1).join('/') as any) + }, + getTextureAtlas () { + return textureAtlas.getTextureAtlas() + }, + getItemComponents (id) { + return new Map() + }, + getItemModel (id) { + // const isSpecial = currentModelName === 'shield' || currentModelName === 'conduit' || currentModelName === 'trident' + const isSpecial = false + if (id.equals(PREVIEW_ID)) { + return ItemModel.fromJson({ + type: isSpecial ? 'minecraft:special' : 'minecraft:model', + model: isSpecial ? { + type: currentModelName, + } : PREVIEW_ID.toString(), + base: PREVIEW_ID.toString(), + tints: modelData?.tints, + }) + } + return null + }, + } + + const canvas = document.createElement('canvas') + canvas.width = RENDER_SIZE + canvas.height = RENDER_SIZE + const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true }) + if (!gl) { + throw new Error('Cannot get WebGL2 context') + } + + function resetGLContext (gl) { + gl.clearColor(0, 0, 0, 0) + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } + + // const includeOnly = ['powered_repeater', 'wooden_door'] + const includeOnly = [] as string[] + + const images: Record = {} + const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({ + 'minecraft:item_model': new NbtString(PREVIEW_ID.toString()), + }))) + const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' }) + const missingTextures = new Set() + for (const [modelName, model] of Object.entries(models)) { + if (includeOnly.length && !includeOnly.includes(modelName)) continue + + const patchMissingTextures = () => { + for (const element of model.elements ?? []) { + for (const [faceName, face] of Object.entries(element.faces)) { + if (face.texture.startsWith('#')) { + missingTextures.add(`${modelName} ${faceName}: ${face.texture}`) + face.texture = 'block/unknown' + } + } + } + } + patchMissingTextures() + // TODO eggs + + modelData = model + currentModelName = modelName + resetGLContext(gl) + if (!modelData) continue + renderer.setItem(item, { display_context: 'gui' }) + renderer.drawItem() + const url = canvas.toDataURL() + // eslint-disable-next-line no-await-in-loop + const img = await getLoadedImage(url) + images[modelName] = img + } + + if (missingTextures.size) { + console.warn(`[guiRenderer] Missing textures in ${[...missingTextures].join(', ')}`) + } + + return images +} + +const generateAtlas = async (images: Record) => { + const atlas = makeTextureAtlas({ + input: Object.keys(images), + tileSize: RENDER_SIZE, + getLoadedImage (name) { + return { + image: images[name], + } + }, + }) + + // const atlasParser = new AtlasParser({ latest: atlas.json }, atlas.canvas.toDataURL()) + // const a = document.createElement('a') + // a.href = await atlasParser.createDebugImage(true) + // a.download = 'blocks_atlas.png' + // a.click() + + activeGuiAtlas.atlas = { + json: atlas.json, + image: ref(await getLoadedImage(atlas.canvas.toDataURL())), + } + + return atlas +} + +export const generateGuiAtlas = async () => { + const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels() + + // Generate blocks atlas + console.time('generate blocks gui atlas') + const blockImages = await generateItemsGui(blockModelsResolved, false) + console.timeEnd('generate blocks gui atlas') + console.time('generate items gui atlas') + const itemImages = await generateItemsGui(itemsModelsResolved, true) + console.timeEnd('generate items gui atlas') + await generateAtlas({ ...blockImages, ...itemImages }) + // await generateAtlas(blockImages) +} diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index aeba4d4c..7cfd6e7e 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -15,6 +15,7 @@ import { buildCleanupDecorator } from './cleanupDecorator' import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo } from './mesher/shared' import { chunkPos } from './simpleUtils' import { updateStatText } from './ui/newStats' +import { generateGuiAtlas } from './guiRenderer' const appViewer = undefined @@ -332,6 +333,10 @@ export abstract class WorldRendererCommon } } + async generateGuiTextures () { + await generateGuiAtlas() + } + async updateAssetsData () { const texture = await new THREE.TextureLoader().loadAsync(this.resourcesManager.blocksAtlasParser!.latestImage) texture.magFilter = THREE.NearestFilter @@ -365,11 +370,15 @@ export abstract class WorldRendererCommon config: this.mesherConfig, }) } - const itemsTexture = await new THREE.TextureLoader().loadAsync(this.resourcesManager.itemsAtlasParser!.latestImage) + const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage) itemsTexture.magFilter = THREE.NearestFilter itemsTexture.minFilter = THREE.NearestFilter itemsTexture.flipY = false viewer.entities.itemsTexture = itemsTexture + + this.renderUpdateEmitter.emit('textureDownloaded') + this.renderUpdateEmitter.emit('itemsTextureDownloaded') + console.log('textures loaded') } async downloadDebugAtlas (isItems = false) { diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 4147ce7d..875d0e0c 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -25,12 +25,14 @@ const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true' let releaseTag let releaseLink let releaseChangelog +let githubRepositoryFallback if (fs.existsSync('./assets/release.json')) { const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8')) releaseTag = releaseJson.latestTag releaseLink = releaseJson.isCommit ? `/commit/${releaseJson.latestTag}` : `/releases/${releaseJson.latestTag}` releaseChangelog = releaseJson.changelog?.replace(//, '') + githubRepositoryFallback = releaseJson.repository } const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8')) @@ -41,6 +43,8 @@ if (dev) { configJson.defaultProxy = ':8080' } +const configSource = process.env.CONFIG_JSON_SOURCE || 'REMOTE' + // base options are in ./renderer/rsbuildSharedConfig.ts const appConfig = defineConfig({ html: { @@ -66,13 +70,13 @@ const appConfig = defineConfig({ 'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'), 'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS), '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}`}`), + 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({}), 'process.env.RELEASE_TAG': JSON.stringify(releaseTag), 'process.env.RELEASE_LINK': JSON.stringify(releaseLink), 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), - 'process.env.INLINED_APP_CONFIG': JSON.stringify(configJson), + 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), }, }, server: { @@ -109,7 +113,9 @@ const appConfig = defineConfig({ fs.copyFileSync('./assets/release.json', './dist/release.json') } - fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8') + if (configSource === 'REMOTE') { + fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8') + } if (fs.existsSync('./generated/sounds.js')) { fs.copyFileSync('./generated/sounds.js', './dist/sounds.js') } diff --git a/scripts/dockerPrepare.mjs b/scripts/dockerPrepare.mjs index 37e57d01..62a4f5e4 100644 --- a/scripts/dockerPrepare.mjs +++ b/scripts/dockerPrepare.mjs @@ -4,9 +4,27 @@ import path from 'path' import { fileURLToPath } from 'url' import { execSync } from 'child_process' -// write release tag +// Get repository from git config +const getGitRepository = () => { + try { + const gitConfig = fs.readFileSync('.git/config', 'utf8') + const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m) + if (originUrlMatch) { + return originUrlMatch[1] + } + } catch (err) { + console.warn('Failed to read git repository from config:', err) + } + return null +} + +// write release tag and repository info const commitShort = execSync('git rev-parse --short HEAD').toString().trim() -fs.writeFileSync('./assets/release.json', JSON.stringify({ latestTag: `${commitShort} (docker)` }), 'utf8') +const repository = getGitRepository() +fs.writeFileSync('./assets/release.json', JSON.stringify({ + latestTag: `${commitShort} (docker)`, + repository +}), 'utf8') const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')) delete packageJson.optionalDependencies diff --git a/server.js b/server.js index 2dbb05b3..20e66051 100644 --- a/server.js +++ b/server.js @@ -15,7 +15,7 @@ try { // Create our app const app = express() -const isProd = process.argv.includes('--prod') +const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production' app.use(compression()) app.use(cors()) app.use(netApi({ allowOrigin: '*' })) diff --git a/src/appConfig.ts b/src/appConfig.ts new file mode 100644 index 00000000..3d6d8f93 --- /dev/null +++ b/src/appConfig.ts @@ -0,0 +1,59 @@ +import { disabledSettings, options, qsOptions } from './optionsStorage' +import { miscUiState } from './globalState' +import { setLoadingScreenStatus } from './appStatus' + +export type AppConfig = { + // defaultHost?: string + // defaultHostSave?: string + defaultProxy?: string + // defaultProxySave?: string + // defaultVersion?: string + peerJsServer?: string + peerJsServerFallback?: string + promoteServers?: Array<{ ip, description, version? }> + mapsProvider?: string + + appParams?: Record // query string params + + defaultSettings?: Record + forceSettings?: Record + // hideSettings?: Record + allowAutoConnect?: boolean + pauseLinks?: Array>> +} + +export const loadAppConfig = (appConfig: AppConfig) => { + if (miscUiState.appConfig) { + Object.assign(miscUiState.appConfig, appConfig) + } else { + miscUiState.appConfig = appConfig + } + + if (appConfig.forceSettings) { + for (const [key, value] of Object.entries(appConfig.forceSettings)) { + if (value) { + disabledSettings.value.add(key) + // since the setting is forced, we need to set it to that value + if (appConfig.defaultSettings?.[key] && !qsOptions[key]) { + options[key] = appConfig.defaultSettings[key] + } + } else { + disabledSettings.value.delete(key) + } + } + } +} + +export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG + +if (isBundledConfigUsed) { + loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {}) +} else { + void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { + // console.warn('Failed to load optional app config.json', error) + // return {} + setLoadingScreenStatus('Failed to load app config.json', true) + }).then((config: AppConfig) => { + loadAppConfig(config) + }) +} diff --git a/src/appParams.ts b/src/appParams.ts index 962eb168..98d6ff62 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from './globalState' +import type { AppConfig } from './appConfig' const qsParams = new URLSearchParams(window.location?.search ?? '') diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 6c2b5f4f..40428c6b 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -17,17 +17,30 @@ const convertedSounds = [] as string[] export async function loadSound (path: string, contents = path) { if (loadingSounds.includes(path)) return true loadingSounds.push(path) - const res = await window.fetch(contents) - if (!res.ok) { - const error = `Failed to load sound ${path}` - if (isCypress()) throw new Error(error) - else console.warn(error) - return - } - const data = await res.arrayBuffer() - sounds[path] = data - loadingSounds.splice(loadingSounds.indexOf(path), 1) + try { + audioContext ??= new window.AudioContext() + + const res = await window.fetch(contents) + if (!res.ok) { + const error = `Failed to load sound ${path}` + if (isCypress()) throw new Error(error) + else console.warn(error) + return + } + const arrayBuffer = await res.arrayBuffer() + + // Decode the audio data immediately + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) + sounds[path] = audioBuffer + convertedSounds.push(path) // Mark as converted immediately + + loadingSounds.splice(loadingSounds.indexOf(path), 1) + } catch (err) { + console.warn(`Failed to load sound ${path}:`, err) + loadingSounds.splice(loadingSounds.indexOf(path), 1) + if (isCypress()) throw err + } } export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => { @@ -53,13 +66,6 @@ export async function playSound (url, soundVolume = 1) { return } - for (const [soundName, sound] of Object.entries(sounds)) { - if (convertedSounds.includes(soundName)) continue - // eslint-disable-next-line no-await-in-loop - sounds[soundName] = await audioContext.decodeAudioData(sound) - convertedSounds.push(soundName) - } - const soundBuffer = sounds[url] if (!soundBuffer) { console.warn(`Sound ${url} not loaded yet`) diff --git a/src/browserfs.ts b/src/browserfs.ts index 41608e30..0f4579b8 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -41,13 +41,13 @@ browserfs.configure({ throw e2 } showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true) - miscUiState.appLoaded = true + miscUiState.fsReady = true miscUiState.singleplayerAvailable = false }) return } await updateTexturePackInstalledState() - miscUiState.appLoaded = true + miscUiState.fsReady = true miscUiState.singleplayerAvailable = true }) diff --git a/src/core/progressReporter.ts b/src/core/progressReporter.ts index bc6f9ab7..f4e4e701 100644 --- a/src/core/progressReporter.ts +++ b/src/core/progressReporter.ts @@ -13,12 +13,14 @@ export interface ProgressReporter { setMessage (message: string): void - end (): void + end(): void + error(message: string): void } interface ReporterDisplayImplementation { setMessage (message: string): void end (): void + error(message: string): void } interface StageInfo { @@ -124,6 +126,10 @@ const createProgressReporter = (implementation: ReporterDisplayImplementation): get currentMessage () { return currentMessage + }, + + error (message: string): void { + implementation.error(message) } } @@ -145,6 +151,11 @@ export const createFullScreenProgressReporter = (): ProgressReporter => { } else { setLoadingScreenStatus(fullScreenReporters.at(-1)!.currentMessage) } + }, + + error (message: string): void { + if (appStatusState.isError) return + setLoadingScreenStatus(message, true) } }) fullScreenReporters.push(reporter) @@ -162,6 +173,10 @@ export const createNotificationProgressReporter = (endMessage?: string): Progres } else { hideNotification() } + }, + + error (message: string): void { + showNotification(message, '', true, '', undefined, true) } }) } @@ -173,6 +188,10 @@ export const createConsoleLogProgressReporter = (group?: string): ProgressReport }, end () { console.log(group ? `[${group}] done` : 'done') + }, + + error (message: string): void { + console.error(message) } }) } @@ -191,6 +210,10 @@ export const createWrappedProgressReporter = (reporter: ProgressReporter, messag if (message) { reporter.endStage(stage) } + }, + + error (message: string): void { + reporter.error(message) } }) } @@ -200,6 +223,8 @@ export const createNullProgressReporter = (): ProgressReporter => { setMessage (message: string) { }, end () { + }, + error (message: string) { } }) } diff --git a/src/customChannels.ts b/src/customChannels.ts index 35922101..ff0f8a32 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -85,15 +85,15 @@ const registeredJeiChannel = () => { [ { name: 'id', - type: 'pstring', + type: ['pstring', { countType: 'i16' }] }, { name: 'categoryTitle', - type: 'pstring', + type: ['pstring', { countType: 'i16' }] }, { name: 'items', - type: 'pstring', + type: ['pstring', { countType: 'i16' }] }, ] ] diff --git a/src/globalState.ts b/src/globalState.ts index 74e6c3ff..0ee8671d 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -5,6 +5,7 @@ import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps' import type { OptionsGroupType } from './optionsGuiScheme' import { appQueryParams } from './appParams' import { options, disabledSettings } from './optionsStorage' +import { AppConfig } from './appConfig' // todo: refactor structure with support of hideNext=false @@ -110,26 +111,6 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY }) // --- -export type AppConfig = { - // defaultHost?: string - // defaultHostSave?: string - defaultProxy?: string - // defaultProxySave?: string - // defaultVersion?: string - peerJsServer?: string - peerJsServerFallback?: string - promoteServers?: Array<{ ip, description, version? }> - mapsProvider?: string - - appParams?: Record // query string params - - defaultSettings?: Record - forceSettings?: Record - // hideSettings?: Record - allowAutoConnect?: boolean - pauseLinks?: Array>> -} - export const miscUiState = proxy({ currentDisplayQr: null as string | null, currentTouch: null as boolean | null, @@ -144,7 +125,7 @@ export const miscUiState = proxy({ loadedServerIndex: '', /** currently trying to load or loaded mc version, after all data is loaded */ loadedDataVersion: null as string | null, - appLoaded: false, + fsReady: false, singleplayerAvailable: false, usingGamepadInput: false, appConfig: null as AppConfig | null, @@ -152,24 +133,6 @@ export const miscUiState = proxy({ displayFullmap: false }) -export const loadAppConfig = (appConfig: AppConfig) => { - if (miscUiState.appConfig) { - Object.assign(miscUiState.appConfig, appConfig) - } else { - miscUiState.appConfig = appConfig - } - - if (appConfig.forceSettings) { - for (const [key, value] of Object.entries(appConfig.forceSettings)) { - if (value) { - disabledSettings.value.delete(key) - } else { - disabledSettings.value.add(key) - } - } - } -} - export const isGameActive = (foregroundCheck: boolean) => { if (foregroundCheck && activeModalStack.length) return false return miscUiState.gameLoaded diff --git a/src/index.ts b/src/index.ts index 884f8594..3a40b905 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import './mineflayer/cameraShake' import './shims/patchShims' import './mineflayer/java-tester/index' import './external' +import './appConfig' import { getServerInfo } from './mineflayer/mc-protocol' import { onGameLoad, renderSlot } from './inventoryWindows' import { GeneralInputItem } from './mineflayer/items' @@ -46,7 +47,6 @@ import initializePacketsReplay from './packetsReplay/packetsReplayLegacy' import { initVR } from './vr' import { - AppConfig, activeModalStack, activeModalStacks, hideModal, @@ -55,7 +55,6 @@ import { miscUiState, showModal, gameAdditionalState, - loadAppConfig } from './globalState' import { parseServerAddress } from './parseServerAddress' @@ -836,8 +835,9 @@ export async function connect (connectOptions: ConnectOptions) { const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined listenGlobalEvents() -watchValue(miscUiState, async s => { - if (s.appLoaded) { // fs ready +const unsubscribe = watchValue(miscUiState, async s => { + if (s.fsReady && s.appConfig) { + unsubscribe() if (reconnectOptions) { sessionStorage.removeItem('reconnectOptions') if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) { @@ -898,15 +898,6 @@ document.body.addEventListener('touchstart', (e) => { }, { passive: false }) // #endregion -loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {}) -// load maybe updated config on the server with updated params (just in case) -void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { - console.warn('Failed to load optional app config.json', error) - return {} -}).then((config: AppConfig | {}) => { - loadAppConfig(config) -}) - // qs open actions if (!reconnectOptions) { downloadAndOpenFile().then((downloadAction) => { diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 353426cf..e11ce59c 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -10,6 +10,7 @@ import { versionToNumber } from 'renderer/viewer/common/utils' import { getRenamedData } from 'flying-squid/dist/blockRenames' import PrismarineChatLoader from 'prismarine-chat' import { BlockModel } from 'mc-assets' +import { activeGuiAtlas } from 'renderer/viewer/lib/guiRenderer' import Generic95 from '../assets/generic_95.png' import { appReplacableResources } from './generated/resources' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' @@ -155,7 +156,10 @@ const getImageSrc = (path): string | HTMLImageElement => { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' } -const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => { }) => { +const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any, image = undefined as HTMLImageElement | undefined }, onLoad = () => { }) => { + if (image) { + return image + } if (!path && !texture) throw new Error('Either pass path or texture') const loadPath = (blockData ? 'blocks' : path ?? texture)! if (loadedImagesCache.has(loadPath)) { @@ -184,7 +188,8 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal blockData?: Record & { resolvedModel: BlockModel }, scale?: number, slice?: number[], - modelName?: string + modelName?: string, + image?: HTMLImageElement } | undefined => { let itemModelName = model.modelName const originalItemName = itemModelName @@ -196,6 +201,23 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal let itemTexture + + if (!fullBlockModelSupport) { + const atlas = activeGuiAtlas.atlas?.json + // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) + const item = atlas?.textures[itemModelName.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '').replace('_bottom', '')] + if (item) { + const x = item.u * atlas.width + const y = item.v * atlas.height + return { + texture: 'gui', + image: activeGuiAtlas.atlas!.image, + slice: [x, y, atlas.tileSize, atlas.tileSize], + scale: 0.25, + } + } + } + try { assertDefined(viewer.world.itemsRenderer) itemTexture = @@ -205,6 +227,8 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`) itemTexture = viewer.world.itemsRenderer!.getItemTexture('block/errored')! } + + if ('type' in itemTexture) { // is item return { @@ -230,13 +254,13 @@ const getItemName = (slot: Item | RenderItem | null) => { return text.join('') } -const mapSlots = (slots: Array) => { +const mapSlots = (slots: Array, isJei = false) => { return slots.map((slot, i) => { // todo stateid if (!slot) return try { - const debugIsQuickbar = i === bot.inventory.hotbarStart + bot.quickBarSlot + const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }) const slotCustomProps = renderSlot({ modelName }, debugIsQuickbar) const itemCustomName = getItemName(slot) @@ -305,7 +329,7 @@ const upJei = (search: string) => { return new PrismarineItem(x.id, 1) }).filter(a => a !== null) lastWindow.pwindow.win.jeiSlotsPage = 0 - lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots) + lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots, true) } export const openItemsCanvas = (type, _bot = bot as typeof bot | null) => { diff --git a/src/mineflayer/minecraft-protocol-extra.ts b/src/mineflayer/minecraft-protocol-extra.ts index ef1bfbf2..e8216a00 100644 --- a/src/mineflayer/minecraft-protocol-extra.ts +++ b/src/mineflayer/minecraft-protocol-extra.ts @@ -13,7 +13,7 @@ export const pingServerVersion = async (ip: string, port?: number, mergeOptions: ...mergeOptions, } let latency = 0 - let fullInfo = null + let fullInfo: any = null fakeClient.autoVersionHooks = [(res) => { latency = res.latency fullInfo = res diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 904b0a29..79748679 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -5,7 +5,7 @@ import { proxy, subscribe } from 'valtio/vanilla' import { subscribeKey } from 'valtio/utils' import { omitObj } from '@zardoy/utils' import { appQueryParamsArray } from './appParams' -import type { AppConfig } from './globalState' +import type { AppConfig } from './appConfig' const isDev = process.env.NODE_ENV === 'development' const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {} @@ -188,7 +188,7 @@ subscribe(options, () => { localStorage.options = JSON.stringify(saveOptions) }) -type WatchValue = >(proxy: T, callback: (p: T, isChanged: boolean) => void) => void +type WatchValue = >(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void export const watchValue: WatchValue = (proxy, callback) => { const watchedProps = new Set() @@ -198,10 +198,19 @@ export const watchValue: WatchValue = (proxy, callback) => { return Reflect.get(target, p, receiver) }, }), false) + const unsubscribes = [] as Array<() => void> for (const prop of watchedProps) { - subscribeKey(proxy, prop, () => { - callback(proxy, true) - }) + unsubscribes.push( + subscribeKey(proxy, prop, () => { + callback(proxy, true) + }) + ) + } + + return () => { + for (const unsubscribe of unsubscribes) { + unsubscribe() + } } } diff --git a/src/react/EnterFullscreenButton.tsx b/src/react/EnterFullscreenButton.tsx index 3901b9ae..0bc41632 100644 --- a/src/react/EnterFullscreenButton.tsx +++ b/src/react/EnterFullscreenButton.tsx @@ -4,6 +4,7 @@ import { activeModalStack, miscUiState } from '../globalState' import Button from './Button' import { useUsingTouch } from './utilsApp' import { pixelartIcons } from './PixelartIcon' +import { showNotification } from './NotificationProvider' const hideOnModals = new Set(['chat']) @@ -33,8 +34,12 @@ export default () => { left: inMainMenu ? 35 : 5, width: 22, }} - onClick={() => { - void document.documentElement.requestFullscreen() + onClick={async () => { + try { + await document.documentElement.requestFullscreen() + } catch (err) { + showNotification(`${err.message ?? err}`, undefined, true) + } }} /> } diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index e06ced52..30fd3047 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -75,12 +75,12 @@ export const mainMenuState = proxy({ let disableAnimation = false export default () => { const haveModals = useSnapshot(activeModalStack).length - const { gameLoaded, appLoaded, appConfig, singleplayerAvailable } = useSnapshot(miscUiState) + const { gameLoaded, fsReady, appConfig, singleplayerAvailable } = useSnapshot(miscUiState) - const noDisplay = haveModals || gameLoaded || !appLoaded + const noDisplay = haveModals || gameLoaded || !fsReady useEffect(() => { - if (noDisplay && appLoaded) disableAnimation = true + if (noDisplay && fsReady) disableAnimation = true }, [noDisplay]) const [versionStatus, setVersionStatus] = useState('') diff --git a/src/react/NotificationProvider.tsx b/src/react/NotificationProvider.tsx index 4c9661e0..6460c0e9 100644 --- a/src/react/NotificationProvider.tsx +++ b/src/react/NotificationProvider.tsx @@ -60,10 +60,13 @@ export default () => { // }, []) const scale = useAppScale() - return
+ return
void onInteraction?(interaction: 'enter' | 'space') elemRef?: React.Ref offline?: boolean } -const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef, offline }: WorldProps & { ref?: React.Ref }) => { +const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, worldNameRightGrayed, elemRef, offline }: WorldProps & { ref?: React.Ref }) => { const timeRelativeFormatted = useMemo(() => { if (!lastPlayed) return '' const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) @@ -63,6 +64,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
{title}
+ {worldNameRightGrayed && {worldNameRightGrayed}} {offline ? ( diff --git a/src/react/singleplayer.module.css b/src/react/singleplayer.module.css index 0e69ea46..0de91735 100644 --- a/src/react/singleplayer.module.css +++ b/src/react/singleplayer.module.css @@ -36,6 +36,9 @@ .world_title_right { color: #999; font-size: 9px; + display: flex; + align-items: end; + gap: 1px; } .world_info { margin-left: 3px; diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 701c734a..9702891a 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -220,6 +220,7 @@ const App = () => { +
@@ -227,7 +228,6 @@ const App = () => {
-
diff --git a/src/resourcePack.ts b/src/resourcePack.ts index a5921be9..a72e7857 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -393,7 +393,7 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres const response = await fetch(url).catch((err) => { console.log(`Ensure server on ${url} support CORS which is not required for regular client, but is required for the web client`) console.error(err) - showNotification('Failed to download resource pack: ' + err.message) + progressReporter.error('Failed to download resource pack: ' + err.message) }) console.timeEnd('downloadServerResourcePack') if (!response) return @@ -426,6 +426,8 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres console.error(err) showNotification('Failed to install resource pack: ' + err.message) }) + } catch (err) { + progressReporter.error('Could not install resource pack: ' + err.message) } finally { progressReporter.endStage('download-resource-pack') resourcePackState.isServerInstalling = false diff --git a/src/resourcesManager.ts b/src/resourcesManager.ts index fd92e58b..43c3882e 100644 --- a/src/resourcesManager.ts +++ b/src/resourcesManager.ts @@ -14,10 +14,11 @@ export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpec const itemSelector = playerState.getItemSelector({ ...specificProps }) - const model = getItemDefinition(viewer.world.itemsDefinitionsStore, { + const modelFromDef = getItemDefinition(viewer.world.itemsDefinitionsStore, { name: itemModelName, version: viewer.world.texturesVersion!, properties: itemSelector - })?.model ?? itemModelName + })?.model + const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName return model } diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 0520a361..86764203 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -95,7 +95,7 @@ export const watchOptionsAfterViewerInit = () => { appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting }) - customEvents.on('gameLoaded', () => { + customEvents.on('mineflayerBotCreated', () => { appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting })