mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-24 23:56:51 +00:00
Update loot table preview, item display and tooltips to 1.21
This commit is contained in:
@@ -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)!
|
||||
|
||||
194
src/app/services/ResolvedItem.ts
Normal file
194
src/app/services/ResolvedItem.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
248
src/app/services/Resources1204.ts
Normal file
248
src/app/services/Resources1204.ts
Normal 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
|
||||
}
|
||||
@@ -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()])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user