feat: experimental namespaces support in resource packs
This commit is contained in:
parent
4ce95de03f
commit
61659d82b4
5 changed files with 169 additions and 73 deletions
|
|
@ -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
30
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue