diff --git a/.eslintrc.json b/.eslintrc.json index 8b2225b5..3552f6a7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -102,6 +102,7 @@ // "@stylistic/multiline-ternary": "error", // not needed // "@stylistic/newline-per-chained-call": "error", // not sure if needed "@stylistic/new-parens": "error", + "@typescript-eslint/class-literal-property-style": "off", "@stylistic/no-confusing-arrow": "error", "@stylistic/wrap-iife": "error", "@stylistic/space-before-blocks": "error", diff --git a/package.json b/package.json index d9d0b69e..4867f241 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", - "mc-assets": "^0.2.45", + "mc-assets": "^0.2.47", "mineflayer-mouse": "^0.1.7", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 537a391e..865fd09a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,8 +353,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.45 - version: 0.2.45 + specifier: ^0.2.47 + version: 0.2.47 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) @@ -6690,8 +6690,8 @@ packages: maxrects-packer@2.7.3: resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==} - mc-assets@0.2.45: - resolution: {integrity: sha512-M1qnX73h/fW+mj9+A+jGtPaWDlq449DzPziZJxeLxmS1nSh/TSydKkLkWX967LaevVA1YtxNpA/SJ1N4jaW48w==} + mc-assets@0.2.47: + resolution: {integrity: sha512-L9tOcYctDGTUaG4QKJESw9uOiOMl8F2RZ3h0cF4NRCjBvd4c9GUrGDaaVdTaAByQZNNITXgGspmm9hNAFMWcKg==} engines: {node: '>=18.0.0'} mcraft-fun-mineflayer@0.1.14: @@ -17588,7 +17588,7 @@ snapshots: maxrects-packer@2.7.3: {} - mc-assets@0.2.45: + mc-assets@0.2.47: dependencies: maxrects-packer: 2.7.3 zod: 3.24.1 diff --git a/renderer/viewer/three/appShared.ts b/renderer/viewer/three/appShared.ts index b41d32db..9afbe563 100644 --- a/renderer/viewer/three/appShared.ts +++ b/renderer/viewer/three/appShared.ts @@ -26,7 +26,7 @@ export const getItemUv = (item: Record, specificProps: ItemSpecific const model = getItemModelName({ ...item, name, - } as GeneralInputItem, specificProps) + } as GeneralInputItem, specificProps, resourcesManager) const renderInfo = renderSlot({ modelName: model, diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 45202608..35872deb 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -493,6 +493,7 @@ export class Entities { } getItemMesh (item, specificProps: ItemSpecificContextProperties, previousModel?: string) { + if (!item.nbt && item.nbtData) item.nbt = item.nbtData const textureUv = this.worldRenderer.getItemRenderData(item, specificProps) if (previousModel && previousModel === textureUv?.modelName) return undefined diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index a6cd4a3e..76c1c52d 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -250,7 +250,7 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal } const getItemName = (slot: Item | RenderItem | null) => { - const parsed = getItemNameRaw(slot) + const parsed = getItemNameRaw(slot, appViewer.resourcesManager) if (!parsed) return // todo display full text renderer from sign renderer const text = flat(parsed as MessageFormatPart).map(x => x.text) @@ -277,7 +277,7 @@ const mapSlots = (slots: Array, isJei = false) => { try { if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot - const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }) + const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager) const slotCustomProps = renderSlot({ modelName }, debugIsQuickbar) const itemCustomName = getItemName(slot) Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts index 8ddb476c..45638cd4 100644 --- a/src/mineflayer/items.ts +++ b/src/mineflayer/items.ts @@ -4,6 +4,7 @@ import { fromFormattedString } from '@xmcl/text-component' import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' import { MessageFormatPart } from '../chatUtils' +import { ResourcesManager } from '../resourcesManager' import { playerState } from './playerState' type RenderSlotComponent = { @@ -27,13 +28,21 @@ export type GeneralInputItem = Pick { +export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManager) => { let customText = undefined as string | any | undefined let customModel = undefined as string | undefined + + let itemId = item.name + if (!itemId.includes(':')) { + itemId = `minecraft:${itemId}` + } + const customModelDataDefinitions = resourcesManager.currentResources?.customItemModelNames[itemId] + if (item.components) { const componentMap = new Map() for (const component of item.components) { @@ -48,6 +57,15 @@ export const getItemMetadata = (item: GeneralInputItem) => { if (customModelComponent) { customModel = customModelComponent.data } + if (customModelDataDefinitions) { + const customModelDataComponent: any = componentMap.get('custom_model_data') + if (customModelDataComponent?.data && typeof customModelDataComponent.data === 'number') { + const customModelData = customModelDataComponent.data + if (customModelDataDefinitions[customModelData]) { + customModel = customModelDataDefinitions[customModelData] + } + } + } const loreComponent = componentMap.get('lore') if (loreComponent) { customText ??= item.displayName ?? item.name @@ -61,6 +79,9 @@ export const getItemMetadata = (item: GeneralInputItem) => { if (customName) { customText = customName } + if (customModelDataDefinitions && itemNbt.CustomModelData && customModelDataDefinitions[itemNbt.CustomModelData]) { + customModel = customModelDataDefinitions[itemNbt.CustomModelData] + } } return { @@ -70,8 +91,8 @@ export const getItemMetadata = (item: GeneralInputItem) => { } -export const getItemNameRaw = (item: Pick | null) => { - const { customText } = getItemMetadata(item as any) +export const getItemNameRaw = (item: Pick | null, resourcesManager: ResourcesManager) => { + const { customText } = getItemMetadata(item as any, resourcesManager) if (!customText) return try { if (typeof customText === 'object') { @@ -90,9 +111,9 @@ export const getItemNameRaw = (item: Pick } } -export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties) => { +export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager) => { let itemModelName = item.name - const { customModel } = getItemMetadata(item) + const { customModel } = getItemMetadata(item, resourcesManager) if (customModel) { itemModelName = customModel } diff --git a/src/react/HotbarRenderApp.tsx b/src/react/HotbarRenderApp.tsx index f0fadbfb..438f7e68 100644 --- a/src/react/HotbarRenderApp.tsx +++ b/src/react/HotbarRenderApp.tsx @@ -42,7 +42,7 @@ const ItemName = ({ itemKey }: { itemKey: string }) => { useEffect(() => { const item = bot.heldItem if (item) { - const customDisplay = getItemNameRaw(item) + const customDisplay = getItemNameRaw(item, appViewer.resourcesManager) if (customDisplay) { setItemName(customDisplay) } else { diff --git a/src/resourcePack.ts b/src/resourcePack.ts index afe97777..7c348cb2 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -313,6 +313,7 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) = const resources = appViewer.resourcesManager.currentResources! resources.customBlockStates = {} resources.customModels = {} + resources.customItemModelNames = {} const usedBlockTextures = new Set() const usedItemTextures = new Set() const basePath = await getActiveResourcepackBasePath() @@ -354,14 +355,62 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) = return jsons } + const readCustomModelData = async (path: string, namespaceDir: string) => { + if (!(await existsAsync(path))) return + const files = await fs.promises.readdir(path) + const customModelData = {} as Record + await Promise.all(files.map(async (file) => { + const filePath = `${path}/${file}` + if (file.endsWith('.json')) { + const contents = await fs.promises.readFile(filePath, 'utf8') + const name = file.replace('.json', '') + const parsed = JSON.parse(contents) + const entries: string[] = [] + if (path.endsWith('/items')) { // 1.21.4+ + // TODO: Support other properties too + if (parsed.model?.type === 'range_dispatch' && parsed.model?.property === 'custom_model_data') { + for (const entry of parsed.model?.entries ?? []) { + const threshold = entry.threshold ?? 0 + let modelPath = entry.model?.model + if (typeof modelPath !== 'string') continue + if (!modelPath.includes(':')) modelPath = `minecraft:${modelPath}` + entries[threshold] = modelPath + } + } + } else if (path.endsWith('/models/item')) { // pre 1.21.4 + for (const entry of parsed.overrides ?? []) { + if (entry.predicate?.custom_model_data && entry.model) { + let modelPath = entry.model + if (typeof modelPath !== 'string') continue + if (!modelPath.includes(':')) modelPath = `minecraft:${modelPath}` + entries[entry.predicate.custom_model_data] = modelPath + } + } + } + if (entries.length > 0) { + customModelData[`${namespaceDir}:${name}`] = entries + } + } + })) + return customModelData + } + const readData = async (namespaceDir: string) => { const blockstatesPath = `${basePath}/assets/${namespaceDir}/blockstates` const blockModelsPath = `${basePath}/assets/${namespaceDir}/models/block` + const itemsPath = `${basePath}/assets/${namespaceDir}/items` const itemModelsPath = `${basePath}/assets/${namespaceDir}/models/item` Object.assign(resources.customBlockStates!, await readModelData(blockstatesPath, 'blockstates', namespaceDir)) Object.assign(resources.customModels!, await readModelData(blockModelsPath, 'models', namespaceDir)) Object.assign(resources.customModels!, await readModelData(itemModelsPath, 'models', namespaceDir)) + + for (const [key, value] of Object.entries(await readCustomModelData(itemsPath, namespaceDir) ?? {})) { + resources.customItemModelNames[key] = value + } + for (const [key, value] of Object.entries(await readCustomModelData(itemModelsPath, namespaceDir) ?? {})) { + resources.customItemModelNames[key] = value + } } try { @@ -373,6 +422,7 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) = console.error('Failed to read some of resource pack blockstates and models', err) resources.customBlockStates = undefined resources.customModels = undefined + resources.customItemModelNames = {} } return { usedBlockTextures, diff --git a/src/resourcesManager.ts b/src/resourcesManager.ts index 64810165..5b3f403b 100644 --- a/src/resourcesManager.ts +++ b/src/resourcesManager.ts @@ -31,6 +31,8 @@ export class LoadedResources { // User data (specific to current resourcepack/version) customBlockStates?: Record customModels?: Record + /** array where the index represents the custom model data value, and the element at that index is the model path to use */ + customItemModelNames: Record = {} customTextures: { items?: { tileSize: number | undefined, textures: Record } blocks?: { tileSize: number | undefined, textures: Record }