pages235/prismarine-viewer/viewer/prepare/genItemsAtlas.ts
Vitaly d546fa8f41 enable strict null types which improves quality of codebase a lot!
todo add types to worker & world renderer
2023-11-01 03:07:07 +03:00

148 lines
5.8 KiB
TypeScript

import fs from 'fs'
import McAssets from 'minecraft-assets'
import { join } from 'path'
import { filesize } from 'filesize'
import minecraftDataLoader from 'minecraft-data'
import BlockLoader from 'prismarine-block'
import { JsonAtlas, makeTextureAtlas, writeCanvasStream } from './atlas'
import looksSame from 'looks-same' // ensure after canvas import
import { Version as _Version } from 'minecraft-data'
import { versionToNumber } from './utils'
// todo move it, remove it
const legacyInvsprite = JSON.parse(fs.readFileSync(join(__dirname, '../../../src/invsprite.json'), 'utf8'))
//@ts-ignore
const latestMcAssetsVersion = McAssets.versions.at(-1)!
// const latestVersion = minecraftDataLoader.supportedVersions.pc.at(-1)
const mcData = minecraftDataLoader(latestMcAssetsVersion)
const PBlock = BlockLoader(latestMcAssetsVersion)
function isCube (name) {
const id = mcData.blocksByName[name]?.id
if (!id) return
const block = new PBlock(id, 0, 0)
const shape = block.shapes?.[0]
return block.shapes?.length === 1 && shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
export type ItemsAtlasesOutputJson = {
latest: JsonAtlas
legacy: JsonAtlas
legacyMap: [string, string[]][]
}
export const generateItemsAtlases = async () => {
const latestAssets = McAssets(latestMcAssetsVersion)
const latestItems = fs.readdirSync(join(latestAssets.directory, 'items')).map(f => f.split('.')[0])
// item - texture path
const toAddTextures = {
fromBlocks: {} as Record<string, string>,
remapItems: {} as Record<string, string>, // todo
}
const getItemTextureOfBlock = (name: string) => {
const blockModel = latestAssets.blocksModels[name]
// const isPlainBlockDisplay = blockModel?.display?.gui?.rotation?.[0] === 0 && blockModel?.display?.gui?.rotation?.[1] === 0 && blockModel?.display?.gui?.rotation?.[2] === 0
// it seems that information about cross blocks is hardcoded
if (blockModel?.parent?.endsWith('block/cross')) {
toAddTextures.fromBlocks[name] = `blocks/${blockModel.textures.cross.split('/')[1]}`
return true
}
if (legacyInvsprite[name]) {
return true
}
if (fs.existsSync(join(latestAssets.directory, 'blocks', name + '.png'))) {
// very last resort
toAddTextures.fromBlocks[name] = `blocks/${name}`
return true
}
if (name.endsWith('_spawn_egg')) {
// todo also color
toAddTextures.fromBlocks[name] = `items/spawn_egg`
}
}
for (const item of mcData.itemsArray) {
if (latestItems.includes(item.name)) {
continue
}
// USE IN RUNTIME
if (isCube(item.name)) {
// console.log('cube', block.name)
} else if (!getItemTextureOfBlock(item.name)) {
console.warn('skipping item (not cube, no item texture)', item.name)
}
}
let fullItemsMap = {} as Record<string, string[]>
const itemsSizes = {}
let saving = 0
let overallsize = 0
let prevItemsDir
let prevVersion
for (const version of [...McAssets.versions].reverse()) {
const itemsDir = join(McAssets(version).directory, 'items')
for (const item of fs.readdirSync(itemsDir)) {
const prevItemPath = !prevItemsDir ? undefined : join(prevItemsDir, item)
const itemSize = fs.statSync(join(itemsDir, item)).size
if (prevItemPath && fs.existsSync(prevItemPath) && (await looksSame(join(itemsDir, item), prevItemPath, { strict: true })).equal) {
saving += itemSize
} else {
fullItemsMap[version] ??= []
fullItemsMap[version].push(item)
}
overallsize += itemSize
}
prevItemsDir = itemsDir
prevVersion = version
}
fullItemsMap = Object.fromEntries(Object.entries(fullItemsMap).map(([ver, items]) => [ver, items.filter(item => item.endsWith('.png'))]))
const latestVersionItems = fullItemsMap[latestMcAssetsVersion]
delete fullItemsMap[latestMcAssetsVersion]
const legacyItemsSortedEntries = Object.entries(fullItemsMap).sort(([a], [b]) => versionToNumber(a) - versionToNumber(b)).map(([key, value]) => [key, value.map(x => x.replace('.png', ''))] as [typeof key, typeof value])
// const allItemsLength = Object.values(fullItemsMap).reduce((acc, x) => acc + x.length, 0)
// console.log(`Items to generate: ${allItemsLength} (latest version: ${latestVersionItems.length})`)
const fullLatestItemsObject = {
...Object.fromEntries(latestVersionItems.map(item => [item, `items/${item.replace('.png', '')}`])),
...toAddTextures.fromBlocks,
...toAddTextures.remapItems
}
const latestAtlas = makeTextureAtlas(Object.keys(fullLatestItemsObject), (name) => {
const contents = `data:image/png;base64,${fs.readFileSync(join(latestAssets.directory, `${fullLatestItemsObject[name]}.png`), 'base64')}`
return {
contents,
}
}, undefined, 'remove')
const texturesPath = join(__dirname, '../../public/textures')
writeCanvasStream(latestAtlas.canvas, join(texturesPath, 'items.png'), () => {
console.log('Generated latest items atlas')
})
const legacyItemsMap = legacyItemsSortedEntries.flatMap(([ver, items]) => items.map(item => `${ver}-${item}.png`))
const legacyItemsAtlas = makeTextureAtlas(legacyItemsMap, (name) => {
const [ver, item] = name.split('-')
const contents = `data:image/png;base64,${fs.readFileSync(join(McAssets(ver).directory, `items/${item}`), 'base64')}`
return {
contents,
}
}, undefined, 'remove')
writeCanvasStream(legacyItemsAtlas.canvas, join(texturesPath, 'items-legacy.png'), () => {
console.log('Generated legacy items atlas')
})
const allItemsMaps: ItemsAtlasesOutputJson = {
latest: latestAtlas.json,
legacy: legacyItemsAtlas.json,
legacyMap: legacyItemsSortedEntries
}
fs.writeFileSync(join(texturesPath, 'items.json'), JSON.stringify(allItemsMaps), 'utf8')
console.log(`Generated items! Input size: ${filesize(overallsize)}, saving: ~${filesize(saving)}`)
}