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:
parent
cd9b796f16
commit
47be0ac865
10 changed files with 90 additions and 15 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
10
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue