Update loot table preview, item display and tooltips to 1.21

This commit is contained in:
Misode
2024-09-11 02:31:17 +02:00
parent 337b7d9b0a
commit fd6de2ac85
15 changed files with 2073 additions and 408 deletions

View File

@@ -120,6 +120,31 @@ export async function fetchBlockStates(versionId: VersionId) {
return result
}
export async function fetchItemComponents(versionId: VersionId) {
console.debug(`[fetchItemComponents] ${versionId}`)
const version = config.versions.find(v => v.id === versionId)!
const result = new Map<string, Map<string, unknown>>()
try {
const data = await cachedFetch<Record<string, Record<string, unknown>>>(`${mcmeta(version, 'summary')}/item_components/data.min.json`)
for (const [id, components] of Object.entries(data)) {
const base = new Map<string, unknown>()
if (Array.isArray(components)) { // syntax before 1.21
for (const entry of components) {
base.set(entry.type, entry.value)
}
} else {
for (const [key, value] of Object.entries(components)) {
base.set(key, value)
}
}
result.set('minecraft:' + id, base)
}
} catch (e) {
console.warn('Error occurred while fetching item components:', message(e))
}
return result
}
export async function fetchPreset(versionId: VersionId, registry: string, id: string) {
console.debug(`[fetchPreset] ${versionId} ${registry} ${id}`)
const version = config.versions.find(v => v.id === versionId)!

View File

@@ -0,0 +1,194 @@
import type { NbtTag } from 'deepslate'
import { Identifier, ItemStack } from 'deepslate'
export class ResolvedItem extends ItemStack {
constructor(
item: ItemStack,
public base: ReadonlyMap<string, NbtTag>,
) {
super(item.id, item.count, item.components)
}
public static create(id: string | Identifier, count: number, components: Map<string, NbtTag>, baseGetter: (id: string) => ReadonlyMap<string, NbtTag>) {
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)) {
return this.components.get(key)
}
if (this.components.has(`!${key}`)) {
return undefined
}
return this.base.get(key)
}
public get<T>(key: string, reader: (tag: NbtTag) => T) {
const tag = this.getTag(key)
return tag === undefined ? undefined : reader(tag)
}
public has(key: string) {
return this.getTag(key) !== undefined
}
public set(key: string, tag: NbtTag) {
key = Identifier.parse(key).toString()
this.components.set(key, tag)
}
public getSize() {
const keys = new Set(this.base.keys())
for (const key of this.components.keys()) {
if (key.startsWith('!')) {
keys.delete(key.slice(1))
} else {
keys.add(key)
}
}
return keys.size
}
public getMaxDamage() {
return this.get('max_damage', tag => tag.getAsNumber()) ?? 0
}
public getDamage() {
return this.get('damage', tag => tag.getAsNumber()) ?? 0
}
public isDamageable() {
return this.has('max_damage') && this.has('damage') && !this.has('unbreakable')
}
public isDamaged() {
return this.isDamageable() && this.getDamage() > 0
}
public getMaxStackSize() {
return this.get('max_stack_size', tag => tag.getAsNumber()) ?? 1
}
public isStackable() {
return this.getMaxStackSize() > 1 && (!this.isDamageable() || !this.isDamaged())
}
public isEnchanted() {
return this.get('enchantments', tag => {
return tag.isCompound() ? tag.has('levels') ? tag.getCompound('levels').size > 0 : tag.size > 0 : false
}) ?? false
}
public hasFoil() {
return this.has('enchantment_glint_override') || this.isEnchanted()
}
public getLore() {
return this.get('lore', tag => {
return tag.isList() ? tag.map(e => e.getAsString()) : []
}) ?? []
}
public showInTooltip(key: string) {
return this.get(key, tag => {
return tag.isCompound() && tag.has('show_in_tooltip') ? tag.getBoolean('show_in_tooltip') !== false : true
}) ?? false
}
public getRarity() {
const rarity = this.get('rarity', tag => tag.isString() ? tag.getAsString() : undefined) ?? 'common'
if (!this.isEnchanted()) {
return rarity
}
if (rarity === 'common' || rarity === 'uncommon') {
return 'rare'
}
if (rarity === 'rare') {
return 'epic'
}
return rarity
}
public getRarityColor() {
const rarity = this.getRarity()
if (rarity === 'epic') {
return 'light_purple'
} else if (rarity === 'rare') {
return 'aqua'
} else if (rarity === 'uncommon') {
return 'yellow'
} else {
return 'white'
}
}
public getHoverName() {
const customName = this.get('custom_name', tag => tag.isString() ? tag.getAsString() : undefined)
if (customName) {
try {
return JSON.parse(customName)
} catch (e) {
return '(invalid custom name)'
}
}
const bookTitle = this.get('written_book_content', tag => tag.isCompound() ? (tag.hasCompound('title') ? tag.getCompound('title').getString('raw') : tag.getString('title')) : undefined)
if (bookTitle && bookTitle.length > 0) {
return { text: bookTitle }
}
const itemName = this.get('item_name', tag => tag.isString() ? tag.getAsString() : undefined)
try {
if (itemName) {
return JSON.parse(itemName)
}
} catch (e) {}
const guess = this.id.path
.replace(/[_\/]/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
return { text: guess }
}
public getStyledHoverName() {
return { text: '', extra: [this.getHoverName()], color: this.getRarityColor(), italic: this.has('custom_name') }
}
public getDisplayName() {
// Does not use translation key "chat.square_brackets" due to limitations of TextComponent
return { text: '[', extra: [this.getStyledHoverName(), ']'], color: this.getRarityColor() }
}
public getChargedProjectile() {
return this.get('charged_projectiles', tag => {
if (!tag.isList() || tag.length === 0) {
return undefined
}
return ItemStack.fromNbt(tag.getCompound(0))
})
}
}

View File

@@ -0,0 +1,248 @@
import type { BlockDefinitionProvider, BlockFlagsProvider, BlockModelProvider, BlockPropertiesProvider, ItemStack, TextureAtlasProvider, UV } from 'deepslate-1.20.4/render'
import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, upperPowerOfTwo } from 'deepslate-1.20.4/render'
import config from '../Config.js'
import { message } from '../Utils.js'
import { fetchLanguage, fetchResources } from './DataFetcher.js'
import type { VersionId } from './Schemas.js'
const Resources: Record<string, ResourceManager | Promise<ResourceManager>> = {}
export async function getResources(version: VersionId) {
if (!Resources[version]) {
Resources[version] = (async () => {
try {
const { blockDefinitions, models, uvMapping, atlas} = await fetchResources(version)
Resources[version] = new ResourceManager(blockDefinitions, models, uvMapping, atlas)
return Resources[version]
} catch (e) {
console.error('Error: ', e)
throw new Error(`Cannot get resources for version ${version}: ${message(e)}`)
}
})()
return Resources[version]
}
return Resources[version]
}
const RENDER_SIZE = 128
const ItemRenderCache = new Map<string, Promise<string>>()
export async function renderItem(version: VersionId, item: ItemStack) {
const cache_key = `${version} ${item.toString()}`
const cached = ItemRenderCache.get(cache_key)
if (cached !== undefined) {
return cached
}
const promise = (async () => {
const canvas = document.createElement('canvas')
canvas.width = RENDER_SIZE
canvas.height = RENDER_SIZE
const resources = await getResources(version)
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
if (!gl) {
throw new Error('Cannot get WebGL2 context')
}
const renderer = new ItemRenderer(gl, item, resources)
console.log('Rendering', item.toString())
renderer.drawItem()
return canvas.toDataURL()
})()
ItemRenderCache.set(cache_key, promise)
return promise
}
interface Resources extends BlockDefinitionProvider, BlockModelProvider, TextureAtlasProvider, BlockFlagsProvider, BlockPropertiesProvider {}
export class ResourceManager implements Resources {
private readonly blockDefinitions: { [id: string]: BlockDefinition }
private readonly blockModels: { [id: string]: BlockModel }
private textureAtlas: TextureAtlas
constructor(blockDefinitions: Map<string, unknown>, models: Map<string, unknown>, uvMapping: any, textureAtlas: HTMLImageElement) {
this.blockDefinitions = {}
this.blockModels = {}
this.textureAtlas = TextureAtlas.empty()
this.loadBlockDefinitions(blockDefinitions)
this.loadBlockModels(models)
this.loadBlockAtlas(textureAtlas, uvMapping)
}
public getBlockDefinition(id: Identifier) {
return this.blockDefinitions[id.toString()]
}
public getBlockModel(id: Identifier) {
return this.blockModels[id.toString()]
}
public getTextureUV(id: Identifier) {
return this.textureAtlas.getTextureUV(id)
}
public getTextureAtlas() {
return this.textureAtlas.getTextureAtlas()
}
public getBlockFlags() {
return { opaque: false }
}
public getBlockProperties() {
return null
}
public getDefaultBlockProperties() {
return null
}
private loadBlockModels(models: Map<string, unknown>) {
[...models.entries()].forEach(([id, model]) => {
this.blockModels[Identifier.create(id).toString()] = BlockModel.fromJson(id, model)
})
Object.values(this.blockModels).forEach(m => m.flatten(this))
}
private loadBlockDefinitions(definitions: Map<string, unknown>) {
[...definitions.entries()].forEach(([id, definition]) => {
this.blockDefinitions[Identifier.create(id).toString()] = BlockDefinition.fromJson(id, definition)
})
}
private loadBlockAtlas(image: HTMLImageElement, textures: any) {
const atlasCanvas = document.createElement('canvas')
const w = upperPowerOfTwo(image.width)
const h = upperPowerOfTwo(image.height)
atlasCanvas.width = w
atlasCanvas.height = h
const ctx = atlasCanvas.getContext('2d')!
ctx.drawImage(image, 0, 0)
const imageData = ctx.getImageData(0, 0, w, h)
const idMap: Record<string, UV> = {}
Object.keys(textures).forEach(id => {
const [u, v, du, dv] = textures[id]
const dv2 = (du !== dv && id.startsWith('block/')) ? du : dv
idMap[Identifier.create(id).toString()] = [u / w, v / h, (u + du) / w, (v + dv2) / h]
})
this.textureAtlas = new TextureAtlas(imageData, idMap)
}
}
export class ResourceWrapper implements Resources {
constructor(
private readonly wrapped: Resources,
private readonly overrides: Partial<Resources>,
) {}
public getBlockDefinition(id: Identifier) {
return this.overrides.getBlockDefinition?.(id) ?? this.wrapped.getBlockDefinition(id)
}
public getBlockModel(id: Identifier) {
return this.overrides.getBlockModel?.(id) ?? this.wrapped.getBlockModel(id)
}
public getTextureUV(texture: Identifier) {
return this.overrides.getTextureUV?.(texture) ?? this.wrapped.getTextureUV(texture)
}
public getTextureAtlas() {
return this.overrides.getTextureAtlas?.() ?? this.wrapped.getTextureAtlas()
}
public getBlockFlags(id: Identifier) {
return this.overrides.getBlockFlags?.(id) ?? this.wrapped.getBlockFlags(id)
}
public getBlockProperties(id: Identifier) {
return this.overrides.getBlockProperties?.(id) ?? this.wrapped.getBlockProperties(id)
}
public getDefaultBlockProperties(id: Identifier) {
return this.overrides.getDefaultBlockProperties?.(id) ?? this.wrapped.getDefaultBlockProperties(id)
}
}
export type Language = Record<string, string>
const Languages: Record<string, Language | Promise<Language>> = {}
export async function getLanguage(version: VersionId, lang: string = 'en') {
const mcLang = config.languages.find(l => l.code === lang)?.mc ?? 'en_us'
const cacheKey = `${version}_${mcLang}`
if (!Languages[cacheKey]) {
Languages[cacheKey] = (async () => {
try {
Languages[cacheKey] = await fetchLanguage(version, mcLang)
return Languages[cacheKey]
} catch (e) {
console.error('Error: ', e)
throw new Error(`Cannot get language '${mcLang}' for version ${version}: ${message(e)}`)
}
})()
return Languages[cacheKey]
}
return Languages[cacheKey]
}
export function getTranslation(lang: Language, key: string, params?: string[]) {
const str = lang[key]
if (!str) return undefined
return replaceTranslation(str, params)
}
export function replaceTranslation(src: string, params?: string[]) {
let out = ''
let i = 0
let p = 0
while (i < src.length) {
const c0 = src[i++]
if (c0 === '%') { // percent character
if (i >= src.length) { // INVALID: %<end>
out += c0
break
}
let c1 = src[i++]
if (c1 === '%') { // escape
out += '%'
} else if (c1 === 's' || c1 === 'd') { // short form %s
out += params?.[p++] ?? ''
} else if (c1 >= '0' && c1 <= '9') {
if (i >= src.length) { // INVALID: %2<end>
out += c0 + c1
break
}
let num = ''
do {
num += c1
c1 = src[i++]
} while (i < src.length && c1 >= '0' && c1 <= '9')
if (c1 === '$') {
if (i >= src.length) { // INVALID: %2$<end>
out += c0 + num + c1
break
}
const c2 = src[i++]
if (c2 === 's' || c2 === 'd') { // long form %2$s
const pos = parseInt(num) - 1
if (!params || isNaN(pos) || pos < 0 || pos >= params.length) {
out += ''
} else {
out += params[pos]
}
} else { // INVALID: %2$...
out += c0 + num + c1
}
} else { // INVALID: %2...
out += c0 + num
}
} else { // INVALID: %...
out += c0
}
} else { // normal character
out += c0
}
}
return out
}

View File

@@ -1,6 +1,7 @@
import { NbtByte, NbtCompound, NbtDouble, NbtInt, NbtList, NbtString, NbtTag } from 'deepslate'
import { NbtTag } from 'deepslate'
import yaml from 'js-yaml'
import { Store } from '../Store.js'
import { jsonToNbt } from '../Utils.js'
const INDENTS: Record<string, number | string | undefined> = {
'2_spaces': 2,
@@ -40,28 +41,6 @@ const FORMATS: Record<string, {
},
}
function jsonToNbt(value: unknown): NbtTag {
if (typeof value === 'string') {
return new NbtString(value)
}
if (typeof value === 'number') {
return Number.isInteger(value) ? new NbtInt(value) : new NbtDouble(value)
}
if (typeof value === 'boolean') {
return new NbtByte(value)
}
if (Array.isArray(value)) {
return new NbtList(value.map(jsonToNbt))
}
if (typeof value === 'object' && value !== null) {
return new NbtCompound(
new Map(Object.entries(value ?? {})
.map(([k, v]) => [k, jsonToNbt(v)]))
)
}
throw new Error(`Could not convert ${value} to NBT`)
}
export function stringifySource(data: unknown, format?: string, indent?: string) {
return FORMATS[format ?? Store.getFormat()].stringify(data, INDENTS[indent ?? Store.getIndent()])
}