diff --git a/package-lock.json b/package-lock.json index a725d674..1dff26af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@zip.js/zip.js": "^2.4.5", "brace": "^0.11.1", "buffer": "^6.0.3", - "deepslate": "^0.22.3", + "deepslate": "^0.23.2", "deepslate-1.18": "npm:deepslate@0.9.0-beta.9", "deepslate-1.18.2": "npm:deepslate@0.9.0", "deepslate-1.20.4": "npm:deepslate@0.20.1", @@ -1749,9 +1749,9 @@ "dev": true }, "node_modules/deepslate": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.22.3.tgz", - "integrity": "sha512-Oxd3ha7hfUaO5wv1ibMG3D6eeQkzEKTPaukJBWu8mK1ETT0xnoYroCgmzI3OmoUi8ZYbDUHzjwbP4txISNZ0ZQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.23.2.tgz", + "integrity": "sha512-Adzy1W0rhJkT00rNp3PfnREC7JXc7BrWMo8h7xgmkfyk9GNLgEE/Zv0pUXvlIWc5yqeAnmqbPzgU5DXlNE5hBA==", "dependencies": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", @@ -6328,9 +6328,9 @@ "dev": true }, "deepslate": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.22.3.tgz", - "integrity": "sha512-Oxd3ha7hfUaO5wv1ibMG3D6eeQkzEKTPaukJBWu8mK1ETT0xnoYroCgmzI3OmoUi8ZYbDUHzjwbP4txISNZ0ZQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.23.2.tgz", + "integrity": "sha512-Adzy1W0rhJkT00rNp3PfnREC7JXc7BrWMo8h7xgmkfyk9GNLgEE/Zv0pUXvlIWc5yqeAnmqbPzgU5DXlNE5hBA==", "requires": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", diff --git a/package.json b/package.json index ad0cfaf9..a0820da9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@zip.js/zip.js": "^2.4.5", "brace": "^0.11.1", "buffer": "^6.0.3", - "deepslate": "^0.22.3", + "deepslate": "^0.23.2", "deepslate-1.18": "npm:deepslate@0.9.0-beta.9", "deepslate-1.18.2": "npm:deepslate@0.9.0", "deepslate-1.20.4": "npm:deepslate@0.20.1", diff --git a/src/app/components/ItemDisplay.tsx b/src/app/components/ItemDisplay.tsx index 0b46068e..dbe962fc 100644 --- a/src/app/components/ItemDisplay.tsx +++ b/src/app/components/ItemDisplay.tsx @@ -1,9 +1,8 @@ import type { ItemStack } from 'deepslate/core' -import { Identifier } from 'deepslate/core' import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' import { useVersion } from '../contexts/Version.jsx' import { useAsync } from '../hooks/useAsync.js' -import { fetchItemComponents, fetchRegistries } from '../services/index.js' +import { fetchItemComponents } from '../services/index.js' import { ResolvedItem } from '../services/ResolvedItem.js' import { renderItem } from '../services/Resources.js' import { jsonToNbt } from '../Utils.js' @@ -42,13 +41,13 @@ export function ItemDisplay({ item, slotDecoration, tooltip, advancedTooltip }: }, [baseComponents]) const resolvedItem = useMemo(() => { return itemResolver(item) - }, [item, baseComponents]) + }, [item, itemResolver]) const maxDamage = resolvedItem.getMaxDamage() const damage = resolvedItem.getDamage() return
- + {item.count !== 1 && <> {item.count} @@ -74,34 +73,16 @@ export function ItemDisplay({ item, slotDecoration, tooltip, advancedTooltip }: interface ResolvedProps extends Props { item: ResolvedItem + baseComponents: Map> | undefined } -function ItemItself({ item }: ResolvedProps) { +function RenderedItem({ item, baseComponents }: ResolvedProps) { const { version } = useVersion() - - if (item.id.namespace !== Identifier.DEFAULT_NAMESPACE) { - return Octicon.package - } - - const { value: allModels, loading: loadingModels } = useAsync(async () => { - const registries = await fetchRegistries(version) - return registries.get('model') - }, [version]) - - if (loadingModels || allModels === undefined) { - return null - } - - const modelPath = `item/${item.id.path}` - if (allModels && allModels.includes('minecraft:' + modelPath)) { - return - } - - return Octicon.package -} - -function RenderedItem({ item }: ResolvedProps) { - const { version } = useVersion() - const { value: src } = useAsync(() => renderItem(version, item.flatten()), [version, item]) + const { value: src } = useAsync(async () => { + if (!baseComponents) { + return undefined + } + return renderItem(version, item, baseComponents) + }, [version, item, baseComponents]) if (src) { return <> diff --git a/src/app/components/previews/BlockStatePreview.tsx b/src/app/components/previews/BlockStatePreview.tsx index 45fe000a..79c78ac0 100644 --- a/src/app/components/previews/BlockStatePreview.tsx +++ b/src/app/components/previews/BlockStatePreview.tsx @@ -18,7 +18,7 @@ export const BlockStatePreview = ({ docAndNode, shown }: PreviewProps) => { const { value: resources } = useAsync(async () => { if (!shown) return AsyncCancel - const resources = await getResources(version) + const resources = await getResources(version, new Map()) const definition = BlockDefinition.fromJson(safeJsonParse(text) ?? {}) const wrapper = new ResourceWrapper(resources, { getBlockDefinition(id) { diff --git a/src/app/components/previews/Deepslate.ts b/src/app/components/previews/Deepslate.ts index fd9ccbc6..9d0d17a8 100644 --- a/src/app/components/previews/Deepslate.ts +++ b/src/app/components/previews/Deepslate.ts @@ -315,11 +315,12 @@ export class Deepslate { finalDensity: this.d.DensityFunction.fromJson(state), }), }) + const levelHeight: deepslate19.LevelHeight = { minY: 0, height: 256 } const unknownBiome = this.d.Identifier.create('unknown') const randomState = new this.d.RandomState(settings, seed) const biomeSource = new this.d.FixedBiomeSource(unknownBiome) const chunkGenerator = new this.d.NoiseChunkGenerator(biomeSource, settings) - this.structureContextCache = { seed, settings, randomState, biomeSource, chunkGenerator } + this.structureContextCache = { seed, settings, randomState, biomeSource, chunkGenerator, levelHeight } class SimpleStructure extends this.d.WorldgenStructure { constructor(settings: deepslate19.WorldgenStructure.StructureSettings) { diff --git a/src/app/components/previews/LootTable.ts b/src/app/components/previews/LootTable.ts index 87ba95c5..b203e908 100644 --- a/src/app/components/previews/LootTable.ts +++ b/src/app/components/previews/LootTable.ts @@ -1,3 +1,4 @@ +import type { ItemComponentsProvider } from 'deepslate' import { NbtByte, NbtDouble, NbtLong } from 'deepslate' import type { Random } from 'deepslate/core' import { Identifier, ItemStack, LegacyRandom } from 'deepslate/core' @@ -20,7 +21,7 @@ const StackMixers = { type StackMixer = keyof typeof StackMixers -interface LootOptions { +interface LootOptions extends ItemComponentsProvider { version: VersionId, seed: bigint, luck: number, @@ -32,7 +33,6 @@ interface LootOptions { getPredicate(id: string): any, getEnchantments(): Map, getEnchantmentTag(id: string): string[], - getBaseComponents(id: string): Map, } interface LootContext extends LootOptions { @@ -235,12 +235,12 @@ function createItem(entry: any, consumer: ItemConsumer, ctx: LootContext) { switch (type) { case 'item': const id = Identifier.parse(entry.name) - entryConsumer(new ResolvedItem(new ItemStack(id, 1), ctx.getBaseComponents(id.toString()))) + entryConsumer(new ResolvedItem(new ItemStack(id, 1), ctx.getItemComponents(id))) break case 'tag': ctx.getItemTag(entry.name).forEach(tagEntry => { const id = Identifier.parse(tagEntry) - entryConsumer(new ResolvedItem(new ItemStack(id, 1), ctx.getBaseComponents(id.toString()))) + entryConsumer(new ResolvedItem(new ItemStack(id, 1), ctx.getItemComponents(id))) }) break case 'loot_table': @@ -303,7 +303,7 @@ const LootFunctions: Record LootFunction> = { const level = ctx.random.nextInt(maxLevel - 1) + 1 if (item.is('book')) { item.id = Identifier.create('enchanted_book') - item.base = ctx.getBaseComponents(item.id.toString()) + item.base = ctx.getItemComponents(item.id) } updateEnchantments(item, levels => { return levels.set(Identifier.parse(pick).toString(), level) @@ -314,7 +314,7 @@ const LootFunctions: Record LootFunction> = { const selected = selectEnchantments(item, computeInt(levels, ctx), allowed, ctx) if (item.is('book')) { item.id = Identifier.create('enchanted_book') - item.base = ctx.getBaseComponents(item.id.toString()) + item.base = ctx.getItemComponents(item.id) } updateEnchantments(item, levelsMap => { for (const { id, lvl } of selected) { @@ -437,7 +437,7 @@ const LootFunctions: Record LootFunction> = { } if (item.is('book')) { item.id = Identifier.create('enchanted_book') - item.base = ctx.getBaseComponents(item.id.toString()) + item.base = ctx.getItemComponents(item.id) } updateEnchantments(item, levels => { Object.entries(enchantments).forEach(([id, level]) => { @@ -463,7 +463,7 @@ const LootFunctions: Record LootFunction> = { set_item: ({ item: newId }) => (item, ctx) => { if (typeof newId !== 'string') return item.id = Identifier.parse(newId) - item.base = ctx.getBaseComponents(item.id.toString()) + item.base = ctx.getItemComponents(item.id) }, set_loot_table: ({ name, seed }) => (item) => { item.set('container_loot', new NbtCompound() diff --git a/src/app/components/previews/LootTablePreview.tsx b/src/app/components/previews/LootTablePreview.tsx index 0d0d575d..242e4194 100644 --- a/src/app/components/previews/LootTablePreview.tsx +++ b/src/app/components/previews/LootTablePreview.tsx @@ -1,4 +1,3 @@ -import { Identifier } from 'deepslate' import { useMemo, useRef, useState } from 'preact/hooks' import { useLocale, useVersion } from '../../contexts/index.js' import { useAsync } from '../../hooks/useAsync.js' @@ -58,7 +57,7 @@ export const LootTablePreview = ({ docAndNode }: PreviewProps) => { getPredicate: () => undefined, getEnchantments: () => enchantments ?? new Map(), getEnchantmentTag: (id) => (enchantmentTags?.get(id.replace(/^minecraft:/, '')) as any)?.values ?? [], - getBaseComponents: (id) => new Map([...(itemComponents?.get(Identifier.parse(id).toString()) ?? new Map()).entries()].map(([k, v]) => [k, jsonToNbt(v)])), + getItemComponents: (id) => new Map([...(itemComponents?.get(id.toString()) ?? new Map()).entries()].map(([k, v]) => [k, jsonToNbt(v)])), }) }, [version, seed, luck, daytime, weather, mixItems, text, dependencies, loading]) diff --git a/src/app/components/previews/ModelPreview.tsx b/src/app/components/previews/ModelPreview.tsx index 3f9ca064..d434c4d4 100644 --- a/src/app/components/previews/ModelPreview.tsx +++ b/src/app/components/previews/ModelPreview.tsx @@ -19,7 +19,7 @@ export const ModelPreview = ({ docAndNode, shown }: PreviewProps) => { const { value: resources } = useAsync(async () => { if (!shown) return AsyncCancel - const resources = await getResources(version) + const resources = await getResources(version, new Map()) const blockModel = BlockModel.fromJson(safeJsonParse(text) ?? {}) blockModel.flatten(resources) const wrapper = new ResourceWrapper(resources, { diff --git a/src/app/components/previews/StructureSetPreview.tsx b/src/app/components/previews/StructureSetPreview.tsx index 2d2d01c0..8550f7f1 100644 --- a/src/app/components/previews/StructureSetPreview.tsx +++ b/src/app/components/previews/StructureSetPreview.tsx @@ -52,7 +52,7 @@ export const StructureSetPreview = ({ docAndNode, shown }: PreviewProps) => { iterateWorld2D(imageData.current, transform, (x, y) => { const pos = ChunkPos.create(x, y) - const structure = computeIfAbsent(chunkStructures, `${pos[0]} ${pos[1]}`, () => structureSet?.getStructureInChunk(pos[0], pos[1], context)) + const structure = computeIfAbsent(chunkStructures, `${pos[0]} ${pos[1]}`, () => structureSet?.getStructureInChunk(pos[0], pos[1], context)?.id) return { structure, pos } }, ({ structure, pos }) => { if (structure !== undefined) { diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts index f02ae89a..eeaf77ea 100644 --- a/src/app/services/DataFetcher.ts +++ b/src/app/services/DataFetcher.ts @@ -209,15 +209,19 @@ export function getAssetUrl(versionId: VersionId, type: string, path: string): s export async function fetchResources(versionId: VersionId) { const version = config.versions.find(v => v.id === versionId)! + const needsItemModels = checkVersion(versionId, '1.20.5') + const hasItemModels = checkVersion(versionId, '1.21.4') await validateCache(version) try { - const [blockDefinitions, models, uvMapping, atlas] = await Promise.all([ + const [blockDefinitions, models, uvMapping, atlas, itemDefinitions] = await Promise.all([ fetchAllPresets(versionId, 'block_definition'), fetchAllPresets(versionId, 'model'), fetch(`${mcmeta(version, 'atlas')}/all/data.min.json`).then(r => r.json()), loadImage(`${mcmeta(version, 'atlas')}/all/atlas.png`), + // Always download the 1.21.4 item models for the version range 1.20.5 - 1.21.3 + needsItemModels ? fetchAllPresets(hasItemModels ? versionId : '1.21.4', 'item_definition') : new Map(), ]) - return { blockDefinitions, models, uvMapping, atlas } + return { blockDefinitions, models, uvMapping, atlas, itemDefinitions } } catch (e) { throw new Error(`Error occured while fetching resources: ${message(e)}`) } diff --git a/src/app/services/ResolvedItem.ts b/src/app/services/ResolvedItem.ts index fade600a..9299477d 100644 --- a/src/app/services/ResolvedItem.ts +++ b/src/app/services/ResolvedItem.ts @@ -11,30 +11,10 @@ export class ResolvedItem extends ItemStack { super(item.id, item.count, item.components) } - public static create(id: string | Identifier, count: number, components: Map, baseGetter: (id: string) => ReadonlyMap) { - if (typeof id === 'string') { - id = Identifier.parse(id) - } - const item = new ItemStack(id, count, components) - return new ResolvedItem(item, baseGetter(id.toString())) - } - public clone(): ResolvedItem { return new ResolvedItem(super.clone(), this.base) } - public flatten(): ItemStack { - const components = new Map(this.base) - for (const [key, value] of this.components) { - if (key.startsWith('!')) { - components.delete(key.slice(1)) - } else { - components.set(key, value) - } - } - return new ItemStack(this.id, this.count, components) - } - private getTag(key: string) { key = Identifier.parse(key).toString() if (this.components.has(key)) { diff --git a/src/app/services/Resources.ts b/src/app/services/Resources.ts index 8e200814..7770e53f 100644 --- a/src/app/services/Resources.ts +++ b/src/app/services/Resources.ts @@ -1,18 +1,22 @@ +import { isObject } from '@spyglassmc/core' +import type { ItemComponentsProvider, ItemModelProvider } from 'deepslate' +import { ItemModel, NbtString } from 'deepslate' import type { BlockDefinitionProvider, BlockFlagsProvider, BlockModelProvider, BlockPropertiesProvider, ItemStack, TextureAtlasProvider, UV } from 'deepslate/render' import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, upperPowerOfTwo } from 'deepslate/render' import config from '../Config.js' -import { message } from '../Utils.js' +import { jsonToNbt, message } from '../Utils.js' import { fetchLanguage, fetchResources } from './DataFetcher.js' import type { VersionId } from './Versions.js' +import { checkVersion } from './Versions.js' const Resources: Record> = {} -export async function getResources(version: VersionId) { +export async function getResources(version: VersionId, itemComponents: Map>) { if (!Resources[version]) { Resources[version] = (async () => { try { - const { blockDefinitions, models, uvMapping, atlas} = await fetchResources(version) - Resources[version] = new ResourceManager(blockDefinitions, models, uvMapping, atlas) + const { blockDefinitions, models, uvMapping, atlas, itemDefinitions } = await fetchResources(version) + Resources[version] = new ResourceManager(version, blockDefinitions, models, uvMapping, atlas, itemDefinitions, itemComponents) return Resources[version] } catch (e) { console.error('Error: ', e) @@ -27,7 +31,7 @@ export async function getResources(version: VersionId) { const RENDER_SIZE = 128 const ItemRenderCache = new Map>() -export async function renderItem(version: VersionId, item: ItemStack) { +export async function renderItem(version: VersionId, item: ItemStack, baseComponents: Map>) { const cache_key = `${version} ${item.toString()}` const cached = ItemRenderCache.get(cache_key) if (cached !== undefined) { @@ -38,12 +42,12 @@ export async function renderItem(version: VersionId, item: ItemStack) { const canvas = document.createElement('canvas') canvas.width = RENDER_SIZE canvas.height = RENDER_SIZE - const resources = await getResources(version) + const resources = await getResources(version, baseComponents) const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true }) if (!gl) { throw new Error('Cannot get WebGL2 context') } - const renderer = new ItemRenderer(gl, item, resources) + const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' }) renderer.drawItem() return canvas.toDataURL() })() @@ -51,20 +55,27 @@ export async function renderItem(version: VersionId, item: ItemStack) { return promise } -interface Resources extends BlockDefinitionProvider, BlockModelProvider, TextureAtlasProvider, BlockFlagsProvider, BlockPropertiesProvider {} +interface Resources extends BlockDefinitionProvider, BlockModelProvider, TextureAtlasProvider, BlockFlagsProvider, BlockPropertiesProvider, ItemModelProvider, ItemComponentsProvider {} export class ResourceManager implements Resources { + private readonly version: VersionId private readonly blockDefinitions: { [id: string]: BlockDefinition } private readonly blockModels: { [id: string]: BlockModel } + private readonly itemModels: { [id: string]: ItemModel } private textureAtlas: TextureAtlas + private readonly itemComponents: Map> - constructor(blockDefinitions: Map, models: Map, uvMapping: any, textureAtlas: HTMLImageElement) { + constructor(version: VersionId, blockDefinitions: Map, models: Map, uvMapping: any, textureAtlas: HTMLImageElement, itemDefinitions: Map, itemComponents: Map>) { + this.version = version this.blockDefinitions = {} this.blockModels = {} + this.itemModels = {} this.textureAtlas = TextureAtlas.empty() this.loadBlockDefinitions(blockDefinitions) this.loadBlockModels(models) this.loadBlockAtlas(textureAtlas, uvMapping) + this.loadItemModels(itemDefinitions) + this.itemComponents = itemComponents } public getBlockDefinition(id: Identifier) { @@ -95,6 +106,20 @@ export class ResourceManager implements Resources { return null } + public getItemModel(id: Identifier) { + return this.itemModels[id.toString()] + } + + public getItemComponents(id: Identifier) { + const components = this.itemComponents.get(id.toString()) ?? new Map() + const result = new Map([...components.entries()].map(([k, v]) => [k, jsonToNbt(v)])) + // Hack to make this version range work without needing another deepslate version + if (checkVersion(this.version, '1.20.5', '1.21.2')) { + result.set('minecraft:item_model', new NbtString(id.toString())) + } + return result + } + private loadBlockModels(models: Map) { [...models.entries()].forEach(([id, model]) => { this.blockModels[Identifier.create(id).toString()] = BlockModel.fromJson(model) @@ -108,6 +133,14 @@ export class ResourceManager implements Resources { }) } + private loadItemModels(definitions: Map) { + [...definitions.entries()].forEach(([id, definition]) => { + if (isObject(definition) && isObject((definition as any).model)) { + this.itemModels[Identifier.create(id).toString()] = ItemModel.fromJson((definition as any).model) + } + }) + } + private loadBlockAtlas(image: HTMLImageElement, textures: any) { const atlasCanvas = document.createElement('canvas') const w = upperPowerOfTwo(image.width) @@ -161,6 +194,14 @@ export class ResourceWrapper implements Resources { public getDefaultBlockProperties(id: Identifier) { return this.overrides.getDefaultBlockProperties?.(id) ?? this.wrapped.getDefaultBlockProperties(id) } + + public getItemModel(id: Identifier) { + return this.overrides.getItemModel?.(id) ?? this.wrapped.getItemModel(id) + } + + public getItemComponents(id: Identifier) { + return this.overrides.getItemComponents?.(id) ?? this.wrapped.getItemComponents(id) + } } export type Language = Record