pages235/prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts
Wolf2323 38e4efc79b
fix: improve signs models & text, added all missing sign types (#143)
Co-authored-by: Vitaly Turovsky <vital2580@icloud.com>
2024-06-11 21:14:58 +03:00

421 lines
15 KiB
TypeScript

import Jimp from 'jimp'
import minecraftData from 'minecraft-data'
import { McAssets } from './modelsBuilder'
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
import { versionToNumber } from './utils'
// todo refactor
const handledBlocks = ['water', 'lava', 'barrier']
const origSizeTextures: string[] = []
let currentImage: Jimp
let currentBlockName: string
let currentMcAssets: McAssets
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
type SidesType = {
"up": string
"north": string
"east": string
"south": string
"west": string
"down": string
}
const getBlockStates = (name: string) => {
const mcData = minecraftData(currentMcAssets.version)
return mcData.blocksByName[name]?.states
}
export const addBlockCustomSidesModel = (name: string, sides: SidesType) => {
currentMcAssets.blocksStates[name] = {
"variants": {
"": {
"model": name
}
}
}
currentMcAssets.blocksModels[name] = {
"parent": "block/cube",
"textures": sides
}
}
type TextureMap = [
x: number,
y: number,
width?: number,
height?: number,
]
const justCropUV = (x: number, y: number, x1, y1) => {
// input: 0-16, output: 0-currentImage.getWidth()
const width = Math.abs(x1 - x)
const height = Math.abs(y1 - y)
return currentImage.clone().crop(
x / 16 * currentImage.getWidth(),
y / 16 * currentImage.getHeight(),
width / 16 * currentImage.getWidth(),
height / 16 * currentImage.getHeight(),
)
}
const justCrop = (x: number, y: number, width = 16, height = 16) => {
return currentImage.clone().crop(x, y, width, height)
}
const combineTextures = (locations: TextureMap[]) => {
const resized: Jimp[] = []
for (const [x, y, height = 16, width = 16] of locations) {
resized.push(justCrop(x, y, width, height))
}
const combinedImage = new Jimp(locations[0]![2] ?? 16, locations[0]![3] ?? 16)
for (const image of resized) {
combinedImage.blit(image, 0, 0)
}
return combinedImage
}
const generatedImageTextures: { [blockName: string]: /* base64 */string } = {}
const getBlockTexturesFromJimp = async <T extends Record<string, Jimp>> (sides: T, withUv = false, textureNameBase = currentBlockName): Promise<Record<keyof T, any>> => {
const sidesTextures = {} as any
for (const [side, jimp] of Object.entries(sides)) {
const textureName = `${textureNameBase}_${side}`
const sideTexture = withUv ? { uv: [0, 0, jimp.getWidth(), jimp.getHeight()], texture: textureName } : textureName
const base64Url = await jimp.getBase64Async(jimp.getMIME())
if (side === 'side') {
sidesTextures['north'] = sideTexture
sidesTextures['east'] = sideTexture
sidesTextures['south'] = sideTexture
sidesTextures['west'] = sideTexture
} else {
sidesTextures[side] = sideTexture
}
generatedImageTextures[textureName] = base64Url
}
return sidesTextures
}
const addSimpleCubeWithSides = async (sides: Record<string, Jimp>) => {
const sidesTextures = await getBlockTexturesFromJimp(sides)
addBlockCustomSidesModel(currentBlockName, sidesTextures as any)
}
const handleShulkerBox = async (dataBase: string, match: RegExpExecArray) => {
const [, shulkerColor = ''] = match
currentImage = await Jimp.read(dataBase + `entity/shulker/shulker${shulkerColor && `_${shulkerColor}`}.png`)
const shulkerBoxTextures = {
// todo do all sides
side: combineTextures([
[0, 16], // top
[0, 36], // bottom
]),
up: justCrop(16, 0),
down: justCrop(32, 28)
}
await addSimpleCubeWithSides(shulkerBoxTextures)
}
// TODO! should not be there! move to data with signs!
const chestModels = {
chest: {
"parent": "block/block",
"textures": {
"particle": "#particles"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 10, 15],
"faces": {
"down": { "texture": "#chest", "uv": [3.5, 4.75, 7, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.5, 8.25, 14, 10.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 8.25, 3.5, 10.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 8.25, 7, 10.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7, 8.25, 10.5, 10.75], "rotation": 180 }
},
},
{
"from": [1, 10, 1],
"to": [15, 14, 15],
"faces": {
"up": { "texture": "#chest", "uv": [3.5, 4.75, 7, 8.25] },
"north": { "texture": "#chest", "uv": [10.5, 3.75, 14, 4.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 3.75, 3.5, 4.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 3.75, 7, 4.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7, 3.75, 10.5, 4.75], "rotation": 180 }
}
},
{
"from": [7, 7, 0],
"to": [9, 11, 1],
"faces": {
"down": { "texture": "#chest", "uv": [0.25, 0, 0.75, 0.25], "rotation": 180 },
"up": { "texture": "#chest", "uv": [0.75, 0, 1.25, 0.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [1, 0.25, 1.5, 1.25], "rotation": 180 },
"west": { "texture": "#chest", "uv": [0.75, 0.25, 1, 1.25], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 0.25, 0.25, 1.25], "rotation": 180 }
}
}
]
},
chest_left: {
"parent": "block/block",
"textures": {
"particle": "#particles"
},
"elements": [
{
"from": [1, 0, 1],
"to": [16, 10, 15],
"faces": {
"down": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 8.25, 14.5, 10.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 8.25, 7.25, 10.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7.25, 8.25, 10.75, 10.75], "rotation": 180 }
}
},
{
"from": [1, 10, 1],
"to": [16, 14, 15],
"faces": {
"up": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 3.75, 14.5, 4.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 3.75, 7.25, 4.75], "rotation": 180 },
"west": { "texture": "#chest", "uv": [7.25, 3.75, 10.75, 4.75], "rotation": 180 }
}
},
{
"from": [15, 7, 0],
"to": [16, 11, 1],
"faces": {
"down": { "texture": "#chest", "uv": [0.25, 0, 0.5, 0.25], "rotation": 180 },
"up": { "texture": "#chest", "uv": [0.5, 0, 0.75, 0.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [0.75, 0.25, 1, 1.25], "rotation": 180 },
"west": { "texture": "#chest", "uv": [0.5, 0.25, 0.75, 1.25], "rotation": 180 }
}
}
]
},
chest_right: {
"parent": "block/block",
"textures": {
"particle": "#particles"
},
"elements": [
{
"from": [0, 0, 1],
"to": [15, 10, 15],
"faces": {
"down": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 8.25, 14.5, 10.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 8.25, 3.5, 10.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 8.25, 7.25, 10.75], "rotation": 180 }
}
},
{
"from": [0, 10, 1],
"to": [15, 14, 15],
"faces": {
"up": { "texture": "#chest", "uv": [3.5, 4.75, 7.25, 8.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [10.75, 3.75, 14.5, 4.75], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0, 3.75, 3.5, 4.75], "rotation": 180 },
"south": { "texture": "#chest", "uv": [3.5, 3.75, 7.25, 4.75], "rotation": 180 }
}
},
{
"from": [0, 7, 0],
"to": [1, 11, 1],
"faces": {
"down": { "texture": "#chest", "uv": [0.25, 0, 0.5, 0.25], "rotation": 180 },
"up": { "texture": "#chest", "uv": [0.5, 0, 0.75, 0.25], "rotation": 180 },
"north": { "texture": "#chest", "uv": [0.75, 0.25, 1, 1.25], "rotation": 180 },
"east": { "texture": "#chest", "uv": [0.0, 0.25, 0.25, 1.25], "rotation": 180 }
}
}
]
}
}
// these blockStates / models copied from https://github.com/FakeDomi/FastChest/blob/master/src/main/resources/assets/minecraft/blockstates/
const chestBlockStatesMap = {
chest: JSON.parse(fs.readFileSync(path.join(__dirname, 'blockStates/chest.json'), 'utf-8')),
trapped_chest: JSON.parse(fs.readFileSync(path.join(__dirname, 'blockStates/trapped_chest.json'), 'utf-8')),
ender_chest: JSON.parse(fs.readFileSync(path.join(__dirname, 'blockStates/ender_chest.json'), 'utf-8')),
}
const handleChest = async (dataBase: string, match: RegExpExecArray) => {
const blockStates = structuredClone(chestBlockStatesMap[currentBlockName])
const particle = match[1] === 'ender' ? 'obsidian' : 'oak_planks'
const blockStatesVariants = Object.values(blockStates.variants) as { model }[]
const neededModels = [...new Set(blockStatesVariants.map((x) => x.model))]
for (const modelName of neededModels) {
let chestTextureName = {
chest: 'normal',
trapped_chest: 'trapped',
ender_chest: 'ender',
}[currentBlockName]
if (modelName.endsWith('_left')) chestTextureName = `${chestTextureName}_left`
if (modelName.endsWith('_right')) chestTextureName = `${chestTextureName}_right`
const texture = path.join(currentMcAssets.directory, `../1.19.1/entity/chest/${chestTextureName}.png`)
currentImage = await Jimp.read(texture)
const model = structuredClone(chestModels[modelName])
model.textures.particle = particle
const newModelName = `${currentBlockName}_${modelName}`
for (const variant of blockStatesVariants) {
if (variant.model !== modelName) continue
variant.model = newModelName
}
for (const [i, { faces }] of model.elements.entries()) {
for (const [faceName, face] of Object.entries(faces) as any) {
const { uv } = face
//@ts-ignore
const jimp = justCropUV(...uv)
const key = `${chestTextureName}_${modelName}_${i}_${faceName}`
const texture = await getBlockTexturesFromJimp({
[key]: jimp
}, true, key).then(a => a[key])
face.texture = texture.texture
face.uv = texture.uv
}
}
currentMcAssets.blocksModels[newModelName] = model
}
currentMcAssets.blocksStates[currentBlockName] = blockStates
}
async function loadBlockModelTextures (dataBase: string, blockModel: any) {
for (const key in blockModel.textures) {
let texture: string = blockModel.textures[key]
const useAssetsPath = !!texture.match(/^[0-9.]+\//)
blockModel.textures.particle = texture
generatedImageTextures[texture] = `data:image/png;base64,${fs.readFileSync(path.join(dataBase, useAssetsPath ? '..' : '', texture + '.png'), 'base64')}`
origSizeTextures[texture] = true
}
}
const handlers = [
[/(.+)_shulker_box$/, handleShulkerBox],
[/^shulker_box$/, handleShulkerBox],
[/^(?:(ender|trapped)_)?chest$/, handleChest],
// [/(^|(.+)_)bed$/, handleBed],
// no-op just suppress warning
[/(^light|^moving_piston$)/, true],
] as const
export const tryHandleBlockEntity = async (dataBase, blockName) => {
currentBlockName = blockName
for (const [regex, handler] of handlers) {
const match = regex.exec(blockName)
if (!match) continue
if (handler !== true) {
await handler(dataBase, match)
}
return true
}
}
async function readAllBlockStates (blockStatesDir: string) {
const files = fs.readdirSync(blockStatesDir)
for (const file of files) {
if (file.endsWith('.json')) {
const state = JSON.parse(fs.readFileSync(path.join(blockStatesDir, file), 'utf-8'))
const name = file.replace('.json', '')
currentMcAssets.blocksStates[name] = state
handledBlocks.push(name)
} else {
await readAllBlockStates(path.join(blockStatesDir, file))
}
}
}
async function readAllBlockModels (dataBase: string, blockModelsDir: string, completePath: string) {
const actualPath = completePath.length ? completePath + "/" : ""
const files = fs.readdirSync(blockModelsDir)
for (const file of files) {
if (file.endsWith('.json')) {
const model = JSON.parse(fs.readFileSync(path.join(blockModelsDir, file), 'utf-8'))
const name = actualPath + file.replace('.json', '')
currentMcAssets.blocksModels[name] = model
await loadBlockModelTextures(dataBase, model)
} else {
await readAllBlockModels(dataBase, path.join(blockModelsDir, file), actualPath + file)
}
}
}
const handleExternalData = async (assetsPathRoot: string, version: string) => {
const currentVersionNumber = versionToNumber(version)
const versions = fs.readdirSync(path.join(__dirname, 'data'), { withFileTypes: true })
.filter(x => x.isDirectory())
.map(x => x.name)
.sort((a, b) => versionToNumber(b) - versionToNumber(a))
const allAssetsVersions = fs.readdirSync(assetsPathRoot, { withFileTypes: true })
.filter(x => x.isDirectory())
.map(x => x.name)
.sort((a, b) => versionToNumber(b) - versionToNumber(a))
const getAssetsVersion = (version: string) => {
return allAssetsVersions[version] ?? allAssetsVersions.find(x => x.startsWith(version))
}
for (const curVer of versions) {
const baseDir = path.join(__dirname, 'data', curVer)
if (versionToNumber(curVer) > currentVersionNumber) continue
const assetsVersion = getAssetsVersion(curVer)
await readAllBlockStates(path.join(baseDir, 'blockStates'))
await readAllBlockModels(path.join(assetsPathRoot, assetsVersion), path.join(baseDir, 'blockModels'), "")
}
}
export const prepareMoreGeneratedBlocks = async (mcAssets: McAssets) => {
const mcData = minecraftData(mcAssets.version)
const allTheBlocks = mcData.blocksArray.map(x => x.name)
currentMcAssets = mcAssets
// todo
const ignoredBlocks = ['skull', 'structure_void', 'banner', 'bed', 'end_portal']
for (const theBlock of allTheBlocks) {
try {
if (await tryHandleBlockEntity(mcAssets.directory, theBlock)) {
handledBlocks.push(theBlock)
}
} catch (err) {
// todo remove when all warnings are resolved
console.warn(`[${mcAssets.version}] failed to generate block ${theBlock}`)
}
}
await handleExternalData(path.join(mcAssets.directory, '..'), mcAssets.version)
const warnings: string[] = []
for (const [name, model] of Object.entries(mcAssets.blocksModels)) {
if (Object.keys(model).length === 1 && model.textures) {
const keys = Object.keys(model.textures)
if (keys.length === 1 && keys[0] === 'particle') {
if (handledBlocks.includes(name) || ignoredBlocks.includes(name)) continue
warnings.push(`unhandled block ${name}`)
}
}
}
return { warnings }
}
export const getAdditionalTextures = () => {
return { generated: generatedImageTextures, origSizeTextures }
}