feat: experimental namespaces support in resource packs

This commit is contained in:
Vitaly Turovsky 2025-01-22 20:53:30 +03:00
commit 61659d82b4
5 changed files with 169 additions and 73 deletions

View file

@ -142,7 +142,7 @@
"http-browserify": "^1.7.0",
"http-server": "^14.1.1",
"https-browserify": "^1.0.0",
"mc-assets": "^0.2.26",
"mc-assets": "^0.2.27",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer",
"mineflayer-pathfinder": "^2.4.4",

30
pnpm-lock.yaml generated
View file

@ -346,8 +346,8 @@ importers:
specifier: ^1.0.0
version: 1.0.0
mc-assets:
specifier: ^0.2.26
version: 0.2.26
specifier: ^0.2.27
version: 0.2.27
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)
@ -6220,8 +6220,8 @@ packages:
peerDependencies:
react: ^18.2.0
mc-assets@0.2.26:
resolution: {integrity: sha512-BDrdD/kAMuVvD18nnvukE9StddL1VokParxSlFSRQdAAQmqTuYZlC19rho/SjYb+dBGZSVxwC+e0hZnSuyP9hA==}
mc-assets@0.2.27:
resolution: {integrity: sha512-9VUM89lRIhclj/+CvSXeum6frA7UWXTQR2TUC/qtXJp7DJ6Jf4Mmo/vM/m6Ztev1jG5ZgKNwL6rrP9lwt0YybA==}
engines: {node: '>=18.0.0'}
md5-file@4.0.0:
@ -7132,6 +7132,11 @@ packages:
version: 1.38.0
engines: {node: '>=14'}
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374:
resolution: {tarball: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374}
version: 1.38.1
engines: {node: '>=14'}
prismarine-entity@2.3.1:
resolution: {integrity: sha512-HOv8l7IetHNf4hwZ7V/W4vM3GNl+e6VCtKDkH9h02TRq7jWngsggKtJV+VanCce/sNwtJUhJDjORGs728ep4MA==}
@ -14148,7 +14153,7 @@ snapshots:
diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71:
dependencies:
minecraft-data: 3.83.1
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/cc4d8a232e33e946fa0929dceabe92df4d405734(minecraft-data@3.83.1)
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
prismarine-registry: 1.10.0
random-seed: 0.3.0
vec3: 0.1.8
@ -16523,7 +16528,7 @@ snapshots:
dependencies:
react: 18.2.0
mc-assets@0.2.26: {}
mc-assets@0.2.27: {}
md5-file@4.0.0: {}
@ -17706,6 +17711,19 @@ snapshots:
transitivePeerDependencies:
- minecraft-data
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1):
dependencies:
prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.10.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/5fac7911d8b9a648f08466de3af4283b2de763c3
prismarine-nbt: 2.5.0
prismarine-registry: 1.10.0
smart-buffer: 4.2.0
uint4: 0.1.2
vec3: 0.1.8
xxhash-wasm: 0.4.2
transitivePeerDependencies:
- minecraft-data
prismarine-entity@2.3.1:
dependencies:
prismarine-chat: 1.10.1

View file

@ -324,7 +324,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
Object.assign(blockTexturesChanges, christmasPack)
}
const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {}).filter(x => x.includes('/'))
const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {})
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
const texture = this.customTextures?.blocks?.textures[textureName]
return blockTexturesChanges[textureName] ?? texture

View file

@ -45,9 +45,13 @@ export const onGameLoad = (onLoad) => {
if (!viewer.world.itemsAtlasParser) return
itemsRenderer = new ItemsRenderer(bot.version, viewer.world.blockstatesModels, viewer.world.itemsAtlasParser, viewer.world.blocksAtlasParser)
globalThis.itemsRenderer = itemsRenderer
if (allImagesLoadedState.value) return
onLoad?.()
allImagesLoadedState.value = true
if (!allImagesLoadedState.value) {
onLoad?.()
}
allImagesLoadedState.value = false
setTimeout(() => {
allImagesLoadedState.value = true
}, 0)
}
viewer.world.renderUpdateEmitter.on('textureDownloaded', checkIfLoaded)
checkIfLoaded()
@ -190,7 +194,7 @@ const renderSlot = (slot: RenderSlot, skipBlock = false): {
itemTexture = itemsRenderer.getItemTexture(itemName) ?? itemsRenderer.getItemTexture('item/missing_texture')!
} catch (err) {
itemTexture = itemsRenderer.getItemTexture('block/errored')!
inGameError(`Failed to render item ${itemName} on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.message}`)
inGameError(`Failed to render item ${itemName} on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
}
if ('type' in itemTexture) {
// is item

View file

@ -1,7 +1,9 @@
/* eslint-disable no-await-in-loop */
import { join, dirname, basename } from 'path'
import fs from 'fs'
import JSZip from 'jszip'
import { proxy, subscribe } from 'valtio'
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
import { mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
import { setLoadingScreenStatus } from './utils'
import { showNotification } from './react/NotificationProvider'
@ -9,7 +11,8 @@ import { options } from './optionsStorage'
import { showOptionsModal } from './react/SelectOption'
import { appStatusState } from './react/AppStatusProvider'
import { appReplacableResources, resourcesContentOriginal } from './generated/resources'
import { loadedGameState } from './globalState'
import { loadedGameState, miscUiState } from './globalState'
import { watchUnloadForCleanup } from './gameUnload'
export const resourcePackState = proxy({
resourcePackInstalled: false,
@ -169,64 +172,106 @@ export const getActiveTexturepackBasePath = async () => {
return null
}
const isDirSafe = async (filePath: string) => {
try {
return await fs.promises.stat(filePath).then(stat => stat.isDirectory()).catch(() => false)
} catch (err) {
return false
}
}
const getFilesMapFromDir = async (dir: string) => {
const files = [] as string[]
const scan = async (dir) => {
const dirFiles = await fs.promises.readdir(dir)
for (const file of dirFiles) {
const filePath = join(dir, file)
if (await isDirSafe(filePath)) {
await scan(filePath)
} else {
files.push(filePath)
}
}
}
await scan(dir)
return files
}
export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTextures: string[]) => {
const basePath = await getActiveTexturepackBasePath()
if (!basePath) return
const texturesCommonBasePath = `${basePath}/assets/minecraft/textures`
let texturesBasePath = `${texturesCommonBasePath}/${type === 'blocks' ? 'block' : 'item'}`
const texturesBasePathAlt = `${texturesCommonBasePath}/${type === 'blocks' ? 'blocks' : 'items'}`
if (!(await existsAsync(texturesBasePath))) {
if (await existsAsync(texturesBasePathAlt)) {
texturesBasePath = texturesBasePathAlt
}
}
const allInterestedPaths = existingTextures.map(tex => {
if (tex.includes('/')) {
return join(`${texturesCommonBasePath}/${tex}`)
}
return join(texturesBasePath, tex)
})
const allInterestedPathsPerDir = new Map<string, string[]>()
for (const path of allInterestedPaths) {
const dir = dirname(path)
if (!allInterestedPathsPerDir.has(dir)) {
allInterestedPathsPerDir.set(dir, [])
}
const file = basename(path)
allInterestedPathsPerDir.get(dir)!.push(file)
}
// filter out by readdir each dir
const allInterestedImages = [] as string[]
for (const [dir, paths] of allInterestedPathsPerDir) {
if (!await existsAsync(dir)) {
continue
}
const dirImages = (await fs.promises.readdir(dir)).filter(f => f.endsWith('.png')).map(f => f.replace('.png', ''))
allInterestedImages.push(...dirImages.filter(image => paths.includes(image)).map(image => `${dir}/${image}`))
}
if (allInterestedImages.length === 0) {
return
}
let firstTextureSize: number | undefined
const namespaces = await fs.promises.readdir(join(basePath, 'assets'))
if (appStatusState.status) {
setLoadingScreenStatus(`Generating atlas texture for ${type}`)
}
const textures = {} as Record<string, HTMLImageElement>
for (const namespace of namespaces) {
const texturesCommonBasePath = `${basePath}/assets/${namespace}/textures`
const isMinecraftNamespace = namespace === 'minecraft'
let texturesBasePath = `${texturesCommonBasePath}/${type === 'blocks' ? 'block' : 'item'}`
const texturesBasePathAlt = `${texturesCommonBasePath}/${type === 'blocks' ? 'blocks' : 'items'}`
if (!(await existsAsync(texturesBasePath))) {
if (await existsAsync(texturesBasePathAlt)) {
texturesBasePath = texturesBasePathAlt
}
}
const allInterestedPaths = new Set(
existingTextures
.filter(tex => (isMinecraftNamespace && !tex.includes(':')) || (tex.includes(':') && tex.split(':')[0] === namespace))
.map(tex => {
tex = tex.split(':')[1] ?? tex
if (tex.includes('/')) {
return join(`${texturesCommonBasePath}/${tex}`)
}
return join(texturesBasePath, tex)
})
)
// add all files from texturesCommonBasePath
// if (!isMinecraftNamespace) {
// const commonBasePathFiles = await getFilesMapFromDir(texturesCommonBasePath)
// for (const file of commonBasePathFiles) {
// allInterestedPaths.add(file)
// }
// }
const allInterestedPathsPerDir = new Map<string, string[]>()
for (const path of allInterestedPaths) {
const dir = dirname(path)
if (!allInterestedPathsPerDir.has(dir)) {
allInterestedPathsPerDir.set(dir, [])
}
const file = basename(path)
allInterestedPathsPerDir.get(dir)!.push(file)
}
// filter out by readdir each dir
const allInterestedImages = [] as string[]
for (const [dir, paths] of allInterestedPathsPerDir) {
if (!await existsAsync(dir)) {
continue
}
const dirImages = (await fs.promises.readdir(dir)).filter(f => f.endsWith('.png')).map(f => f.replace('.png', ''))
allInterestedImages.push(...dirImages.filter(image => paths.includes(image)).map(image => `${dir}/${image}`))
}
const firstImageFile = allInterestedImages[0]!
if (allInterestedImages.length === 0) {
continue
}
let firstTextureSize: number | undefined
try {
// todo compare sizes from atlas
firstTextureSize = await getSizeFromImage(`${firstImageFile}.png`)
} catch (err) { }
const textures = Object.fromEntries(await Promise.all(allInterestedImages.map(async (image) => {
const imagePath = `${image}.png`
const contents = await fs.promises.readFile(imagePath, 'base64')
const img = await getLoadedImage(`data:image/png;base64,${contents}`)
const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '')
return [imageRelative, img]
})))
const firstImageFile = allInterestedImages[0]!
try {
// todo check all sizes from atlas
firstTextureSize ??= await getSizeFromImage(`${firstImageFile}.png`)
} catch (err) { }
const newTextures = Object.fromEntries(await Promise.all(allInterestedImages.map(async (image) => {
const imagePath = `${image}.png`
const contents = await fs.promises.readFile(imagePath, 'base64')
const img = await getLoadedImage(`data:image/png;base64,${contents}`)
const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '')
const textureName = isMinecraftNamespace ? imageRelative : `${namespace}:${imageRelative}`
return [textureName, img]
})))
Object.assign(textures, newTextures) as any
}
return {
firstTextureSize,
textures
@ -234,8 +279,9 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTex
}
const prepareBlockstatesAndModels = async () => {
viewer.world.customBlockStates = undefined
viewer.world.customModels = undefined
viewer.world.customBlockStates = {}
viewer.world.customModels = {}
const usedTextures = new Set<string>()
const basePath = await getActiveTexturepackBasePath()
if (!basePath) return
if (appStatusState.status) {
@ -256,16 +302,25 @@ const prepareBlockstatesAndModels = async () => {
if (type === 'models') {
name = `block/${name}`
}
if (namespaceDir !== 'minecraft') {
name = `${namespaceDir}:${name}`
const parsed = JSON.parse(contents)
if (namespaceDir === 'minecraft') {
jsons[name] = parsed
}
jsons[`${namespaceDir}:${name}`] = parsed
if (type === 'models') {
for (let texturePath of Object.values(parsed.textures ?? {})) {
if (typeof texturePath !== 'string') continue
if (texturePath.startsWith('#')) continue
if (!texturePath.includes(':')) texturePath = `minecraft:${texturePath}`
usedTextures.add(texturePath as string)
}
}
jsons[name] = JSON.parse(contents)
}
}))
return jsons
}
viewer.world.customBlockStates = await getAllJson(blockstatesPath, 'blockstates')
viewer.world.customModels = await getAllJson(modelsPath, 'models')
Object.assign(viewer.world.customBlockStates!, await getAllJson(blockstatesPath, 'blockstates'))
Object.assign(viewer.world.customModels!, await getAllJson(modelsPath, 'models'))
}
try {
const assetsDirs = await fs.promises.readdir(join(basePath, 'assets'))
@ -277,6 +332,7 @@ const prepareBlockstatesAndModels = async () => {
viewer.world.customBlockStates = undefined
viewer.world.customModels = undefined
}
return { usedTextures }
}
const downloadAndUseResourcePack = async (url: string): Promise<void> => {
@ -290,6 +346,17 @@ const downloadAndUseResourcePack = async (url: string): Promise<void> => {
})
}
const waitForGameEvent = async () => {
if (miscUiState.gameLoaded) return
await new Promise<void>(resolve => {
const listener = () => resolve()
customEvents.once('gameLoaded', listener)
watchUnloadForCleanup(() => {
customEvents.removeListener('gameLoaded', listener)
})
})
}
export const onAppLoad = () => {
customEvents.on('mineflayerBotCreated', () => {
// todo also handle resourcePack
@ -307,8 +374,12 @@ export const onAppLoad = () => {
minecraftJsonMessage: promptMessagePacket,
})
if (!choice) return
await new Promise(resolve => {
setTimeout(resolve, 500)
})
console.log('accepting resource pack')
bot.acceptResourcePack()
if (choice === 'Download & Install (recommended)') {
if (choice === true || choice === 'Download & Install (recommended)') {
await downloadAndUseResourcePack(packet.url).catch((err) => {
console.error(err)
showNotification('Failed to download resource pack: ' + err.message)
@ -349,10 +420,10 @@ const updateAllReplacableTextures = async () => {
for (const [key, { cssVar, cssVarRepeat, resourcePackPath }] of vars) {
const resPath = `${basePath}/assets/${resourcePackPath}`
if (cssVar) {
// eslint-disable-next-line no-await-in-loop
await setCustomCss(resPath, cssVar, cssVarRepeat ?? 1)
} else {
// eslint-disable-next-line no-await-in-loop
await setCustomPicture(key, resPath)
}
}
@ -363,10 +434,10 @@ const repeatArr = (arr, i) => Array.from({ length: i }, () => arr)
const updateTextures = async () => {
const blocksFiles = Object.keys(viewer.world.blocksAtlases.latest.textures)
const itemsFiles = Object.keys(viewer.world.itemsAtlases.latest.textures)
const blocksData = await getResourcepackTiles('blocks', blocksFiles)
const { usedTextures: extraBlockTextures = new Set<string>() } = await prepareBlockstatesAndModels() ?? {}
const blocksData = await getResourcepackTiles('blocks', [...blocksFiles, ...extraBlockTextures])
const itemsData = await getResourcepackTiles('items', itemsFiles)
await updateAllReplacableTextures()
await prepareBlockstatesAndModels()
viewer.world.customTextures = {}
if (blocksData) {
viewer.world.customTextures.blocks = {
@ -382,6 +453,9 @@ const updateTextures = async () => {
}
if (viewer.world.active) {
await viewer.world.updateTexturesData()
if (viewer.world instanceof WorldRendererThree) {
viewer.world.rerenderAllChunks?.()
}
}
}