Files
misode.github.io/src/app/services/Resources1204.ts

249 lines
7.5 KiB
TypeScript

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 './Versions.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
}