feat: Item custom model data support (#318)

* feat: Item custom model data support

* rename prop, jsdoc for clarity

* explicit resource manager because it can be run in different threads, up mc-assets

* fix tsc

---------

Co-authored-by: Vitaly Turovsky <vital2580@icloud.com>
This commit is contained in:
Max Lee 2025-03-26 06:22:38 +01:00 committed by GitHub
commit 47be0ac865
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 90 additions and 15 deletions

View file

@ -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",

View file

@ -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",

10
pnpm-lock.yaml generated
View file

@ -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

View file

@ -26,7 +26,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
const model = getItemModelName({
...item,
name,
} as GeneralInputItem, specificProps)
} as GeneralInputItem, specificProps, resourcesManager)
const renderInfo = renderSlot({
modelName: model,

View file

@ -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

View file

@ -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<RenderItem | Item | null>, 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 })

View file

@ -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<import('prismarine-item').Item, 'name' | 'nb
type JsonString = string
type PossibleItemProps = {
CustomModelData?: number
Damage?: number
display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"}
}
export const getItemMetadata = (item: GeneralInputItem) => {
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<string, RenderSlotComponent>()
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<import('prismarine-item').Item, 'nbt'> | null) => {
const { customText } = getItemMetadata(item as any)
export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | 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<import('prismarine-item').Item, 'nbt'>
}
}
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
}

View file

@ -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 {

View file

@ -313,6 +313,7 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) =
const resources = appViewer.resourcesManager.currentResources!
resources.customBlockStates = {}
resources.customModels = {}
resources.customItemModelNames = {}
const usedBlockTextures = new Set<string>()
const usedItemTextures = new Set<string>()
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<string, string[]>
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,

View file

@ -31,6 +31,8 @@ export class LoadedResources {
// User data (specific to current resourcepack/version)
customBlockStates?: Record<string, any>
customModels?: Record<string, any>
/** array where the index represents the custom model data value, and the element at that index is the model path to use */
customItemModelNames: Record<string, string[]> = {}
customTextures: {
items?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
blocks?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }