diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d5f4b5b..ef430a1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14370,9 +14370,9 @@ packages: async: 2.6.4 dev: false - github.com/zardoy/minecraft-inventory-gui/c1331c91fb39bd562dc48eeb33321240d4870edd(@types/react@18.2.20)(react@18.2.0): - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/c1331c91fb39bd562dc48eeb33321240d4870edd} - id: github.com/zardoy/minecraft-inventory-gui/c1331c91fb39bd562dc48eeb33321240d4870edd + github.com/zardoy/minecraft-inventory-gui/babb4669ac26b14ccf47505fb40d2590b6882c60(@types/react@18.2.20)(react@18.2.0): + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/babb4669ac26b14ccf47505fb40d2590b6882c60} + id: github.com/zardoy/minecraft-inventory-gui/babb4669ac26b14ccf47505fb40d2590b6882c60 name: minecraft-inventory-gui version: 1.0.1 dependencies: diff --git a/prismarine-viewer/viewer/lib/worldrenderer.js b/prismarine-viewer/viewer/lib/worldrenderer.js index 60c52921..37ef542e 100644 --- a/prismarine-viewer/viewer/lib/worldrenderer.js +++ b/prismarine-viewer/viewer/lib/worldrenderer.js @@ -28,8 +28,10 @@ class WorldRenderer { this.loadedChunks = {} this.sectionsOutstanding = new Set() this.renderUpdateEmitter = new EventEmitter() - this.blockStatesData = undefined - this.texturesDataUrl = undefined + this.customBlockStatesData = undefined + this.customTexturesDataUrl = undefined + this.downloadedBlockStatesData = undefined + this.downloadedTextureImage = undefined this.material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) @@ -156,17 +158,23 @@ class WorldRenderer { } updateTexturesData () { - loadTexture(this.texturesDataUrl || `textures/${this.texturesVersion}.png`, texture => { + loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, texture => { texture.magFilter = THREE.NearestFilter texture.minFilter = THREE.NearestFilter texture.flipY = false this.material.map = texture + this.material.map.onUpdate = () => { + this.downloadedTextureImage = this.material.map.image + } }) const loadBlockStates = async () => { return new Promise(resolve => { - if (this.blockStatesData) return resolve(this.blockStatesData) - return loadJSON(`blocksStates/${this.texturesVersion}.json`, resolve) + if (this.customBlockStatesData) return resolve(this.customBlockStatesData) + return loadJSON(`blocksStates/${this.texturesVersion}.json`, (data) => { + this.downloadedBlockStatesData = data + resolve(data) + }) }) } loadBlockStates().then((blockStates) => { diff --git a/prismarine-viewer/viewer/prepare/atlas.ts b/prismarine-viewer/viewer/prepare/atlas.ts index e03b8a6c..7399b196 100644 --- a/prismarine-viewer/viewer/prepare/atlas.ts +++ b/prismarine-viewer/viewer/prepare/atlas.ts @@ -25,7 +25,7 @@ function readTexture (basePath, name) { return fs.readFileSync(path.join(basePath, name), 'base64') } -type JsonAtlas = { +export type JsonAtlas = { size: number, textures: { [file: string]: { @@ -37,7 +37,7 @@ type JsonAtlas = { } } -export const makeTextureAtlas = (input: string[], getInputData: (name) => {contents: string, tileWidthMult?: number}, tilesCount = input.length): { +export const makeTextureAtlas = (input: string[], getInputData: (name) => {contents: string, tileWidthMult?: number}, tilesCount = input.length, suSvOptimize: 'remove' | null = null): { image: Buffer, canvas: Canvas, json: JsonAtlas @@ -59,18 +59,34 @@ export const makeTextureAtlas = (input: string[], getInputData: (name) => {conte const y = Math.floor(pos / texSize) * tileSize const img = new Image() - const inputData = getInputData(input[i]); + const keyValue = input[i]; + const inputData = getInputData(keyValue); img.src = inputData.contents const renderWidth = tileSize * (inputData.tileWidthMult ?? 1) g.drawImage(img, 0, 0, renderWidth, tileSize, x, y, renderWidth, tileSize) - const cleanName = input[i].split('.')[0] - texturesIndex[cleanName] = { u: x / imgSize, v: y / imgSize, su: suSv, sv: suSv } + const cleanName = keyValue.split('.').slice(0, -1).join('.') || keyValue + texturesIndex[cleanName] = { + u: x / imgSize, + v: y / imgSize, + ...suSvOptimize === 'remove' ? {} : { + su: suSv, + sv: suSv + } + } } return { image: canvas.toBuffer(), canvas, json: { size: suSv, textures: texturesIndex } } } +export const writeCanvasStream = (canvas, path, onEnd) => { + const out = fs.createWriteStream(path) + const stream = (canvas as any).pngStream() + stream.on('data', (chunk) => out.write(chunk)) + if (onEnd) stream.on('end', onEnd) + return stream +} + export function makeBlockTextureAtlas (mcAssets: McAssets) { const blocksTexturePath = path.join(mcAssets.directory, '/blocks') const textureFiles = fs.readdirSync(blocksTexturePath).filter(file => file.endsWith('.png')) diff --git a/prismarine-viewer/viewer/prepare/genItemsAtlas.ts b/prismarine-viewer/viewer/prepare/genItemsAtlas.ts new file mode 100644 index 00000000..9334ae72 --- /dev/null +++ b/prismarine-viewer/viewer/prepare/genItemsAtlas.ts @@ -0,0 +1,152 @@ +//@ts-check +import fs from 'fs' +import McAssets from 'minecraft-assets' +import { join } from 'path' +import { filesize } from 'filesize' +import minecraftDataLoader from 'minecraft-data' +import BlockLoader from 'prismarine-block' +import { JsonAtlas, makeTextureAtlas, writeCanvasStream } from './atlas' +import looksSame from 'looks-same' // ensure after canvas import +import { Version as _Version } from 'minecraft-data' +import { versionToNumber } from './utils' + +//@ts-ignore +const Version = _Version + +// todo move it, remove it +const legacyInvsprite = JSON.parse(fs.readFileSync(join(__dirname, '../../../src/invsprite.json'), 'utf8')) + +//@ts-ignore +const latestMcAssetsVersion = McAssets.versions.at(-1) +// const latestVersion = minecraftDataLoader.supportedVersions.pc.at(-1) +const mcData = minecraftDataLoader(latestMcAssetsVersion) +const PBlock = BlockLoader(latestMcAssetsVersion) + +function isCube (name) { + const id = mcData.blocksByName[name]?.id + if (!id) return + const block = new PBlock(id, 0, 0) + const shape = block.shapes?.[0] + return block.shapes?.length === 1 && shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 +} + +const latestAssets = McAssets(latestMcAssetsVersion) +const latestItems = fs.readdirSync(join(latestAssets.directory, 'items')).map(f => f.split('.')[0]) + +// item - texture path +const toAddTextures = { + fromBlocks: {} as Record, + remapItems: {} as Record, // todo +} + +const getItemTextureOfBlock = (name: string) => { + const blockModel = latestAssets.blocksModels[name] + // const isPlainBlockDisplay = blockModel?.display?.gui?.rotation?.[0] === 0 && blockModel?.display?.gui?.rotation?.[1] === 0 && blockModel?.display?.gui?.rotation?.[2] === 0 + // it seems that information about cross blocks is hardcoded + if (blockModel?.parent?.endsWith('block/cross')) { + toAddTextures.fromBlocks[name] = `blocks/${blockModel.textures.cross.split('/')[1]}` + return true + } + + if (legacyInvsprite[name]) { + return true + } + + if (fs.existsSync(join(latestAssets.directory, 'blocks', name + '.png'))) { + // very last resort + toAddTextures.fromBlocks[name] = `blocks/${name}` + return true + } + if (name.endsWith('_spawn_egg')) { + // todo also color + toAddTextures.fromBlocks[name] = `items/spawn_egg` + } +} + +for (const item of mcData.itemsArray) { + if (latestItems.includes(item.name)) { + continue + } + // USE IN RUNTIME + if (isCube(item.name)) { + // console.log('cube', block.name) + } else if (!getItemTextureOfBlock(item.name)) { + console.warn('skipping item (not cube, no item texture)', item.name) + } +} + +export type ItemsAtlasesOutputJson = { + latest: JsonAtlas + legacy: JsonAtlas + legacyMap: [string, string[]][] +} + +export const generateItemsAtlases = async () => { + let fullItemsMap = {} as Record + + const itemsSizes = {} + let saving = 0 + let overallsize = 0 + let prevItemsDir + let prevVersion + for (const version of [...McAssets.versions].reverse()) { + const itemsDir = join(McAssets(version).directory, 'items') + for (const item of fs.readdirSync(itemsDir)) { + const prevItemPath = !prevItemsDir ? undefined : join(prevItemsDir, item) + const itemSize = fs.statSync(join(itemsDir, item)).size + if (prevItemPath && fs.existsSync(prevItemPath) && (await looksSame(join(itemsDir, item), prevItemPath, { strict: true })).equal) { + saving += itemSize + } else { + fullItemsMap[version] ??= [] + fullItemsMap[version].push(item) + } + overallsize += itemSize + } + prevItemsDir = itemsDir + prevVersion = version + } + + fullItemsMap = Object.fromEntries(Object.entries(fullItemsMap).map(([ver, items]) => [ver, items.filter(item => item.endsWith('.png'))])) + const latestVersionItems = fullItemsMap[latestMcAssetsVersion] + delete fullItemsMap[latestMcAssetsVersion] + const legacyItemsSortedEntries = Object.entries(fullItemsMap).sort(([a], [b]) => versionToNumber(a) - versionToNumber(b)).map(([key, value]) => [key, value.map(x => x.replace('.png', ''))] as [typeof key, typeof value]) + // const allItemsLength = Object.values(fullItemsMap).reduce((acc, x) => acc + x.length, 0) + // console.log(`Items to generate: ${allItemsLength} (latest version: ${latestVersionItems.length})`) + const fullLatestItemsObject = { + ...Object.fromEntries(latestVersionItems.map(item => [item, `items/${item.replace('.png', '')}`])), + ...toAddTextures.fromBlocks, + ...toAddTextures.remapItems + } + + const latestAtlas = makeTextureAtlas(Object.keys(fullLatestItemsObject), (name) => { + const contents = `data:image/png;base64,${fs.readFileSync(join(latestAssets.directory, `${fullLatestItemsObject[name]}.png`), 'base64')}` + return { + contents, + } + }, undefined, 'remove') + const texturesPath = join(__dirname, '../../public/textures') + writeCanvasStream(latestAtlas.canvas, join(texturesPath, 'items.png'), () => { + console.log('Generated latest items atlas') + }) + + const legacyItemsMap = legacyItemsSortedEntries.flatMap(([ver, items]) => items.map(item => `${ver}-${item}.png`)) + const legacyItemsAtlas = makeTextureAtlas(legacyItemsMap, (name) => { + const [ver, item] = name.split('-') + const contents = `data:image/png;base64,${fs.readFileSync(join(McAssets(ver).directory, `items/${item}`), 'base64')}` + return { + contents, + } + }, undefined, 'remove') + writeCanvasStream(legacyItemsAtlas.canvas, join(texturesPath, 'items-legacy.png'), () => { + console.log('Generated legacy items atlas') + }) + + const allItemsMaps: ItemsAtlasesOutputJson = { + latest: latestAtlas.json, + legacy: legacyItemsAtlas.json, + legacyMap: legacyItemsSortedEntries + } + fs.writeFileSync(join(texturesPath, 'items.json'), JSON.stringify(allItemsMaps), 'utf8') + + console.log(`Generated items! Input size: ${filesize(overallsize)}, saving: ~${filesize(saving)}`) +} diff --git a/prismarine-viewer/viewer/prepare/generateTextures.ts b/prismarine-viewer/viewer/prepare/generateTextures.ts index 380ac96a..2eaa64e5 100644 --- a/prismarine-viewer/viewer/prepare/generateTextures.ts +++ b/prismarine-viewer/viewer/prepare/generateTextures.ts @@ -1,9 +1,10 @@ import path from 'path' -import { makeTextureAtlas } from './atlas' +import { makeBlockTextureAtlas } from './atlas' import { McAssets, prepareBlocksStates } from './modelsBuilder' import mcAssets from 'minecraft-assets' import fs from 'fs-extra' import { prepareMoreGeneratedBlocks } from './moreGeneratedBlocks' +import { generateItemsAtlases } from './genItemsAtlas' const publicPath = path.resolve(__dirname, '../../public') @@ -19,17 +20,18 @@ fs.mkdirSync(blockStatesPath, { recursive: true }) const warnings = new Set() Promise.resolve().then(async () => { + generateItemsAtlases() console.time('generateTextures') - for (const version of mcAssets.versions as typeof mcAssets['versions']) { + for (const version of ['1.14.4'] as typeof mcAssets['versions']) { // for debugging (e.g. when above is overridden) if (!mcAssets.versions.includes(version)) { - throw new Error(`Version ${version} is not supported by minecraft-assets, skipping...`) + throw new Error(`Version ${version} is not supported by minecraft-assets`) } const assets = mcAssets(version) const { warnings: _warnings } = await prepareMoreGeneratedBlocks(assets) _warnings.forEach(x => warnings.add(x)) // #region texture atlas - const atlas = makeTextureAtlas(assets) + const atlas = makeBlockTextureAtlas(assets) const out = fs.createWriteStream(path.resolve(texturesPath, version + '.png')) const stream = (atlas.canvas as any).pngStream() stream.on('data', (chunk) => out.write(chunk)) diff --git a/prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts b/prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts index 67b024ce..863d6283 100644 --- a/prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts +++ b/prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts @@ -7,7 +7,7 @@ import fs from 'fs' import { fileURLToPath } from 'url' // todo refactor -const twoBlockTextures: string[] = [] +const twoTileTextures: string[] = [] let currentImage: Jimp let currentBlockName: string let currentMcAssets: McAssets @@ -228,8 +228,8 @@ const handleSign = async (dataBase: string, match: RegExpExecArray) => { ], } } - twoBlockTextures.push(blockTextures.face.texture) - twoBlockTextures.push(blockTextures.up.texture) + twoTileTextures.push(blockTextures.face.texture) + twoTileTextures.push(blockTextures.up.texture) } const chestModels = { @@ -472,5 +472,5 @@ export const prepareMoreGeneratedBlocks = async (mcAssets: McAssets) => { } export const getAdditionalTextures = () => { - return { generated: generatedImageTextures, twoBlockTextures } + return { generated: generatedImageTextures, twoTileTextures } } diff --git a/prismarine-viewer/viewer/prepare/utils.ts b/prismarine-viewer/viewer/prepare/utils.ts new file mode 100644 index 00000000..a33909a9 --- /dev/null +++ b/prismarine-viewer/viewer/prepare/utils.ts @@ -0,0 +1,4 @@ +export const versionToNumber = (ver: string) => { + const [x, y = '0', z = '0'] = ver.split('.') + return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` +} diff --git a/src/controls.ts b/src/controls.ts index 6f827058..9b7bae4a 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -6,9 +6,10 @@ import { proxy, subscribe } from 'valtio' import { ControMax } from 'contro-max/build/controMax' import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types' import { stringStartsWith } from 'contro-max/build/stringUtils' -import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal } from './globalState' +import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState } from './globalState' import { goFullscreen, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' +import { openPlayerInventory } from './inventory' // doesnt seem to work for now const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) @@ -167,7 +168,7 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { // im still not sure, maybe need to refactor to handle in inventory instead const alwaysHandledCommand = (command: Command) => { if (command === 'general.inventory') { - if (activeModalStack.at(-1)?.reactType === 'inventory') { + if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) { // todo? hideCurrentModal() } } @@ -199,7 +200,7 @@ contro.on('trigger', ({ command }) => { switch (command) { case 'general.inventory': document.exitPointerLock?.() - showModal({ reactType: 'inventory' }) + openPlayerInventory() break case 'general.drop': if (bot.heldItem) bot.tossStack(bot.heldItem) diff --git a/src/index.ts b/src/index.ts index ee09c606..9dd3108c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -188,23 +188,19 @@ function hideCurrentScreens () { insertActiveModalStack('', []) } -async function main () { +const connectSingleplayer = (serverOverrides = {}) => { + void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides }) +} +function listenGlobalEvents () { const menu = document.getElementById('play-screen') menu.addEventListener('connect', e => { const options = e.detail void connect(options) }) - const connectSingleplayer = (serverOverrides = {}) => { - void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides }) - } window.addEventListener('singleplayer', (e) => { //@ts-expect-error connectSingleplayer(e.detail) }) - const qs = new URLSearchParams(window.location.search) - if (qs.get('singleplayer') === '1') { - connectSingleplayer() - } } let listeners = [] @@ -491,6 +487,7 @@ async function connect (connectOptions: { // don't use spawn event, player can be dead bot.once('health', () => { + miscUiState.gameLoaded = true if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) const mcData = require('minecraft-data')(bot.version) @@ -707,8 +704,14 @@ async function connect (connectOptions: { }) } -watchValue(miscUiState, m => { - if (m.appLoaded) void main() +listenGlobalEvents() +watchValue(miscUiState, () => { + if (miscUiState.appLoaded) { // fs ready + const qs = new URLSearchParams(window.location.search) + if (qs.get('singleplayer') === '1') { + connectSingleplayer() + } + } }) downloadAndOpenFile().then((downloadAction) => { diff --git a/src/inventory.ts b/src/inventory.ts index 72b71219..50c658bb 100644 --- a/src/inventory.ts +++ b/src/inventory.ts @@ -1,67 +1,105 @@ import { subscribe } from 'valtio' import { showInventory } from 'minecraft-inventory-gui/web/ext.mjs' import InventoryGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/inventory.png' +import ChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/shulker_box.png' +import LargeChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/generic_54.png' +import FurnaceGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/furnace.png' +import CraftingTableGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/crafting_table.png' +import DispenserGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/dispenser.png' + import Dirt from 'minecraft-assets/minecraft-assets/data/1.17.1/blocks/dirt.png' import { subscribeKey } from 'valtio/utils' import MinecraftData from 'minecraft-data' import { getVersion } from 'prismarine-viewer/viewer/lib/version' +import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' +import itemsPng from 'prismarine-viewer/public/textures/items.png' +import itemsLegacyPng from 'prismarine-viewer/public/textures/items-legacy.png' +import _itemsAtlases from 'prismarine-viewer/public/textures/items.json' +import type { ItemsAtlasesOutputJson } from 'prismarine-viewer/viewer/prepare/genItemsAtlas' +import PrismarineBlockLoader from 'prismarine-block' +import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState' import invspriteJson from './invsprite.json' -import { activeModalStack, hideCurrentModal, miscUiState } from './globalState' +import { options } from './optionsStorage' -const loadedImages = new Map() +const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases +const loadedImagesCache = new Map() +const cleanLoadedImagesCache = () => { + loadedImagesCache.delete('blocks') +} export type BlockStates = Record> + }> }> -let blockStates: BlockStates -let lastInventory -let mcData -let version +let lastWindow +let version: string +let PrismarineBlock: typeof PrismarineBlockLoader.Block -subscribeKey(miscUiState, 'gameLoaded', async () => { - if (!miscUiState.gameLoaded) { - // loadedBlocksAtlas = null - return - } - - // on game load +export const onGameLoad = (onLoad) => { version = getVersion(bot.version) - blockStates = await fetch(`blocksStates/${version}.json`).then(async res => res.json()) - getImage({ path: 'blocks' } as any) - getImage({ path: 'invsprite' } as any) - mcData = MinecraftData(version) -}) + getImage({ path: 'invsprite' }) + getImage({ path: 'items' }, onLoad) + getImage({ path: 'items-legacy' }) + PrismarineBlock = PrismarineBlockLoader(version) -const findBlockStateTexturesAtlas = (name) => { + bot.on('windowOpen', (win) => { + if (implementedContainersGuiMap[win.type]) { + // todo also render title! + openWindow(implementedContainersGuiMap[win.type]) + } else if (options.unimplementedContainers) { + openWindow('ChestWin') + } else { + // todo format + bot._client.emit('chat', { + message: JSON.stringify({ + text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Items: ${win.slots.map(slot => slot?.name).join(', ')}` + }) + }) + bot.currentWindow['close']() + } + }) +} + +const findTextureInBlockStates = (name) => { + const blockStates: BlockStates = viewer.world.customBlockStatesData || viewer.world.downloadedBlockStatesData const vars = blockStates[name]?.variants if (!vars) return - const firstVar = Object.values(vars)[0] - if (!firstVar || !Array.isArray(firstVar)) return - return firstVar[0]?.model.textures + let firstVar = Object.values(vars)[0] + if (Array.isArray(firstVar)) firstVar = firstVar[0] + if (!firstVar) return + const elements = firstVar.model?.elements + if (elements?.length !== 1) return + return elements[0].faces +} + +const svSuToCoordinates = (path: string, u, v, su, sv = su) => { + const img = getImage({ path }) + if (!img.width) throw new Error(`Image ${path} is not loaded`) + return [u * img.width, v * img.height, su * img.width, sv * img.height] } const getBlockData = (name) => { - const blocksImg = loadedImages.get('blocks') - if (!blocksImg?.width) return - - const data = findBlockStateTexturesAtlas(name) + const data = findTextureInBlockStates(name) if (!data) return const getSpriteBlockSide = (side) => { - const d = data[side] + const d = data[side]?.texture if (!d) return - const spriteSide = [d.u * blocksImg.width, d.v * blocksImg.height, d.su * blocksImg.width, d.sv * blocksImg.height] + const spriteSide = svSuToCoordinates('blocks', d.u, d.v, d.su, d.sv) const blockSideData = { slice: spriteSide, path: 'blocks' @@ -77,8 +115,8 @@ const getBlockData = (name) => { } } -const getItemSlice = (name) => { - const invspriteImg = loadedImages.get('invsprite') +const getInvspriteSlice = (name) => { + const invspriteImg = loadedImagesCache.get('invsprite') if (!invspriteImg?.width) return const { x, y } = invspriteJson[name] ?? /* unknown item */ { x: 0, y: 0 } @@ -86,68 +124,203 @@ const getItemSlice = (name) => { return sprite } -const getImageSrc = (path) => { +const getImageSrc = (path): string | HTMLImageElement => { switch (path) { case 'gui/container/inventory': return InventoryGui - case 'blocks': return globalThis.texturePackDataUrl || `textures/${version}.png` + case 'blocks': return viewer.world.customTexturesDataUrl || viewer.world.downloadedTextureImage case 'invsprite': return `invsprite.png` + case 'items': return itemsPng + case 'items-legacy': return itemsLegacyPng + case 'gui/container/dispenser': return DispenserGui + case 'gui/container/furnace': return FurnaceGui + case 'gui/container/crafting_table': return CraftingTableGui + case 'gui/container/shulker_box': return ChestLikeGui + case 'gui/container/generic_54': return LargeChestLikeGui } return Dirt } -const getImage = ({ path, texture, blockData }) => { +const getImage = ({ path = undefined, texture = undefined, blockData = undefined }, onLoad = () => {}) => { const loadPath = blockData ? 'blocks' : path ?? texture - if (!loadedImages.has(loadPath)) { - const image = new Image() - // image.onload(() => {}) - image.src = getImageSrc(loadPath) - loadedImages.set(loadPath, image) + if (!loadedImagesCache.has(loadPath)) { + const imageSrc = getImageSrc(loadPath) + let image: HTMLImageElement + if (imageSrc instanceof Image) { + image = imageSrc + } else { + image = new Image() + image.src = imageSrc + } + image.onload = onLoad + loadedImagesCache.set(loadPath, image) } - return loadedImages.get(loadPath) + return loadedImagesCache.get(loadPath) } -const upInventory = () => { +const getItemVerToRender = (version: string, item: string, itemsMapSortedEntries: any[]) => { + const verNumber = versionToNumber(version) + for (const [itemsVer, items] of itemsMapSortedEntries) { + // 1.18 < 1.18.1 + // 1.13 < 1.13.2 + if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) { + return itemsVer as string + } + } +} + +const isFullBlock = (block: string) => { + const blockData = loadedData.blocksByName[block] + if (!blockData) return false + const pBlock = new PrismarineBlock(blockData.id, 0, 0) + if (pBlock.shapes?.length !== 1) return false + const shape = pBlock.shapes[0]! + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 +} + +const renderSlot = (slot: import('prismarine-item').Item, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } => { + const itemName = slot.name + const isItem = loadedData.itemsByName[itemName] + const fullBlock = isFullBlock(itemName) + + if (isItem) { + const legacyItemVersion = getItemVerToRender(version, itemName, itemsAtlases.legacyMap) + const vuToSlice = ({ u, v }, size) => [...svSuToCoordinates('items', u, v, size).slice(0, 2), 16, 16] // item size is fixed + if (legacyItemVersion) { + const textureData = itemsAtlases.legacy.textures[`${legacyItemVersion}-${itemName}`]! + return { + texture: 'items-legacy', + slice: vuToSlice(textureData, itemsAtlases.legacy.size) + } + } + const textureData = itemsAtlases.latest.textures[itemName] + if (textureData) { + return { + texture: 'items', + slice: vuToSlice(textureData, itemsAtlases.latest.size) + } + } + } + if (fullBlock && !skipBlock) { + const blockData = getBlockData(itemName) + if (blockData) { + return { + texture: 'blocks', + blockData + } + } + } + const invspriteSlice = getInvspriteSlice(itemName) + if (invspriteSlice) { + return { + texture: 'invsprite', + scale: 0.5, + slice: invspriteSlice + } + } +} + +export const renderSlotExternal = (slot) => { + const data = renderSlot(slot, true) + if (!data) return + return { + imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture }).src, + sprite: data.slice && data.texture !== 'invsprite' ? data.slice.map(x => x * 2) : data.slice + } +} + +const upInventory = (inventory: boolean) => { // inv.pwindow.inv.slots[2].displayName = 'test' // inv.pwindow.inv.slots[2].blockData = getBlockData('dirt') - const customSlots = bot.inventory.slots.map(slot => { + const updateSlots = (inventory ? bot.inventory : bot.currentWindow).slots.map(slot => { + // todo stateid if (!slot) return - // const itemName = slot.name - // const isItem = mcData.itemsByName[itemName] - // try get block data first, but ideally we should also have atlas from atlas/ folder - const blockData = getBlockData(slot.name) - if (blockData) { - slot['texture'] = 'blocks' - slot['blockData'] = blockData - } else { - slot['texture'] = 'invsprite' - slot['scale'] = 0.5 - slot['slice'] = getItemSlice(slot.name) + try { + const slotCustomProps = renderSlot(slot) + Object.assign(slot, slotCustomProps) + } catch (err) { + console.error(err) } - return slot }) - lastInventory.pwindow.setSlots(customSlots) + const customSlots = updateSlots + lastWindow.pwindow.setSlots(customSlots) } -subscribe(activeModalStack, () => { - const inventoryOpened = activeModalStack.at(-1)?.reactType === 'inventory' - if (inventoryOpened) { - const inv = showInventory(undefined, getImage, {}, bot) - inv.canvas.style.zIndex = '10' - inv.canvas.style.position = 'fixed' - inv.canvas.style.inset = '0' - // todo scaling - inv.canvasManager.setScale(window.innerHeight < 480 ? 2 : window.innerHeight < 700 ? 3 : 4) - inv.canvasManager.onClose = () => { - hideCurrentModal() - inv.canvasManager.destroy() +export const onModalClose = (callback: () => any) => { + const { length } = activeModalStack + const unsubscribe = subscribe(activeModalStack, () => { + if (activeModalStack.length < length) { + callback() + unsubscribe() } + }) +} - lastInventory = inv - upInventory() - } else if (lastInventory) { - lastInventory.destroy() - lastInventory = null +const implementedContainersGuiMap = { + // todo allow arbitrary size instead! + 'minecraft:generic_9x3': 'ChestWin', + 'minecraft:generic_9x6': 'LargeChestWin', + 'minecraft:generic_3x3': 'DropDispenseWin', + 'minecraft:furnace': 'FurnaceWin', + 'minecraft:smoker': 'FurnaceWin', + 'minecraft:crafting': 'CraftingWin' +} + +const openWindow = (type: string | undefined) => { + // if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) { + if (activeModalStack.length) { // game is not in foreground, don't close current modal + if (type) bot.currentWindow['close']() + return } -}) + showModal({ + reactType: `player_win:${type}`, + }) + onModalClose(() => { + if (type !== undefined) bot.currentWindow['close']() + lastWindow.destroy() + lastWindow = null + destroyFn() + }) + cleanLoadedImagesCache() + const inv = showInventory(type, getImage, {}, bot) + inv.canvas.style.zIndex = '10' + inv.canvas.style.position = 'fixed' + inv.canvas.style.inset = '0' + // todo scaling + inv.canvasManager.setScale(window.innerHeight < 480 ? 2 : window.innerHeight < 700 ? 3 : 4) + + inv.canvasManager.onClose = () => { + hideCurrentModal() + inv.canvasManager.destroy() + } + + lastWindow = inv + const upWindowItems = () => { + upInventory(type === undefined) + } + upWindowItems() + + if (type === undefined) { + // player inventory + bot.inventory.on('updateSlot', upWindowItems) + destroyFn = () => { + bot.inventory.off('updateSlot', upWindowItems) + } + } else { + bot.on('windowClose', () => { + // todo hide up to the window itself! + hideCurrentModal() + }) + //@ts-expect-error + bot.currentWindow.on('updateSlot', () => { + upWindowItems() + }) + } +} + +let destroyFn = () => { } + +export const openPlayerInventory = () => { + openWindow(undefined) +} diff --git a/src/menus/components/hotbar.js b/src/menus/components/hotbar.js index ee30a827..ba4f87de 100644 --- a/src/menus/components/hotbar.js +++ b/src/menus/components/hotbar.js @@ -4,6 +4,7 @@ const { subscribeKey } = require('valtio/utils') const invsprite = require('../../invsprite.json') const { isGameActive, miscUiState, showModal } = require('../../globalState') +const { openPlayerInventory, renderSlotExternal } = require('../../inventory') const { isProbablyIphone } = require('./common') class Hotbar extends LitElement { @@ -57,7 +58,6 @@ class Hotbar extends LitElement { height: 32px; transform-origin: top left; transform: scale(0.5); - background-image: url('invsprite.png'); background-size: 1024px auto; } @@ -103,8 +103,6 @@ class Hotbar extends LitElement { static get properties () { return { activeItemName: { type: String }, - bot: { type: Object }, - viewerVersion: { type: String } } } @@ -119,7 +117,7 @@ class Hotbar extends LitElement { updated (changedProperties) { if (changedProperties.has('bot')) { // inventory listener - this.bot.once('spawn', () => { + bot.once('spawn', () => { this.init() }) } @@ -132,7 +130,7 @@ class Hotbar extends LitElement { document.addEventListener('wheel', (e) => { if (!isGameActive(true)) return e.preventDefault() - const newSlot = ((this.bot.quickBarSlot + Math.sign(e.deltaY)) % 9 + 9) % 9 + const newSlot = ((bot.quickBarSlot + Math.sign(e.deltaY)) % 9 + 9) % 9 this.reloadHotbarSelected(newSlot) }, { passive: false, @@ -145,38 +143,41 @@ class Hotbar extends LitElement { this.reloadHotbarSelected(numPressed - 1) }) - this.bot.inventory.on('updateSlot', (slot, oldItem, newItem) => { - if (slot >= this.bot.inventory.hotbarStart + 9) return - if (slot < this.bot.inventory.hotbarStart) return + bot.inventory.on('updateSlot', (slot, oldItem, newItem) => { + if (slot >= bot.inventory.hotbarStart + 9) return + if (slot < bot.inventory.hotbarStart) return - const sprite = newItem ? invsprite[newItem.name] ?? { x: 0, y: 0 } : invsprite.air - const slotEl = this.shadowRoot.getElementById('hotbar-' + (slot - this.bot.inventory.hotbarStart)) - const slotIcon = slotEl.children[0] - const slotStack = slotEl.children[1] - slotIcon.style['background-position-x'] = `-${sprite.x}px` - slotIcon.style['background-position-y'] = `-${sprite.y}px` - slotStack.textContent = newItem?.count > 1 ? newItem.count : '' + this.reloadHotbar(slot - bot.inventory.hotbarStart) }) } - async reloadHotbar () { + reloadHotbar (onlySlot = undefined) { for (let i = 0; i < 9; i++) { - const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + i] - const sprite = item ? invsprite[item.name] ?? { x: 0, y: 0 } : invsprite.air + if (onlySlot !== undefined && onlySlot !== i) continue + const item = bot.inventory.slots[bot.inventory.hotbarStart + i] const slotEl = this.shadowRoot.getElementById('hotbar-' + i) const slotIcon = slotEl.children[0] const slotStack = slotEl.children[1] - slotIcon.style['background-position-x'] = `-${sprite.x}px` - slotIcon.style['background-position-y'] = `-${sprite.y}px` + const data = item ? renderSlotExternal(item) : { sprite: [invsprite.air.x, invsprite.air.y] } + if (data?.imageDataUrl) { + slotIcon.style['background-image'] = `url('${data.imageDataUrl}')` + } else { + slotIcon.style['background-image'] = `url('invsprite.png')` + } + if (data?.sprite) { + const [x, y] = data.sprite + slotIcon.style['background-position-x'] = `-${x}px` + slotIcon.style['background-position-y'] = `-${y}px` + } slotStack.textContent = item?.count > 1 ? item.count : '' } } async reloadHotbarSelected (slot) { - const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + slot] + const item = bot.inventory.slots[bot.inventory.hotbarStart + slot] const newLeftPos = (-1 + 20 * slot) + 'px' this.shadowRoot.getElementById('hotbar-selected').style.left = newLeftPos - this.bot.setQuickBarSlot(slot) + bot.setQuickBarSlot(slot) this.activeItemName = item?.displayName ?? '' const name = this.shadowRoot.getElementById('hotbar-item-name') name.classList.remove('hotbar-item-name-fader') @@ -202,7 +203,7 @@ class Hotbar extends LitElement { `)} ${miscUiState.currentTouch ? html`
{ - showModal({ reactType: 'inventory' }) + openPlayerInventory() }}>` : undefined}
diff --git a/src/menus/hud.js b/src/menus/hud.js index 97f98c06..d92b0a11 100644 --- a/src/menus/hud.js +++ b/src/menus/hud.js @@ -137,7 +137,6 @@ class Hud extends LitElement { const xpLabel = this.shadowRoot.querySelector('#xp-label') this.bot = bot - hotbar.bot = bot debugMenu.bot = bot hotbar.init() diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 5ac7cf97..e3440a6a 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -28,6 +28,7 @@ const defaultOptions = { highPerformanceGpu: false, /** @unstable */ disableAssets: false, + unimplementedContainers: false, showChunkBorders: false, frameLimit: false as number | false, diff --git a/src/texturePack.ts b/src/texturePack.ts index e60bc1d0..d96c096c 100644 --- a/src/texturePack.ts +++ b/src/texturePack.ts @@ -271,8 +271,8 @@ export const genTexturePackTextures = async (version: string) => { export const watchTexturepackInViewer = (viewer: Viewer) => { subscribeKey(resourcePackState, 'currentTexturesDataUrl', () => { console.log('applying resourcepack world data') - viewer.world.texturesDataUrl = resourcePackState.currentTexturesDataUrl - viewer.world.blockStatesData = resourcePackState.currentTexturesBlockStates + viewer.world.customTexturesDataUrl = resourcePackState.currentTexturesDataUrl + viewer.world.customBlockStatesData = resourcePackState.currentTexturesBlockStates if (!viewer?.world.active) return viewer.world.updateTexturesData() }) diff --git a/src/utils.ts b/src/utils.ts index e3fc763c..7289388c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -153,7 +153,7 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul export const disconnect = async () => { - if (window.localServer) { + if (localServer) { await saveServer() localServer.quit() }