250 lines
8.4 KiB
TypeScript
250 lines
8.4 KiB
TypeScript
import { setLoadingScreenStatus } from './utils'
|
|
import blocksFileNames from '../generated/blocks.json'
|
|
import JSZip from 'jszip'
|
|
import { join, dirname } from 'path'
|
|
import fs from 'fs'
|
|
import type { BlockStates } from './inventory'
|
|
import type { Viewer } from 'prismarine-viewer/viewer/lib/viewer'
|
|
import { removeFileRecursiveAsync } from './browserfs'
|
|
import { miscUiState } from './globalState'
|
|
import { subscribeKey } from 'valtio/utils'
|
|
|
|
function nextPowerOfTwo(n) {
|
|
if (n === 0) return 1
|
|
n--
|
|
n |= n >> 1
|
|
n |= n >> 2
|
|
n |= n >> 4
|
|
n |= n >> 8
|
|
n |= n >> 16
|
|
return n + 1
|
|
}
|
|
|
|
const mkdirRecursive = async (path) => {
|
|
const parts = path.split('/')
|
|
let current = ''
|
|
for (const part of parts) {
|
|
current += part + '/'
|
|
try {
|
|
await fs.promises.mkdir(current)
|
|
} catch (err) {
|
|
}
|
|
}
|
|
}
|
|
|
|
const texturePackBasePath = '/userData/resourcePacks/default'
|
|
export const uninstallTexturePack = async () => {
|
|
await removeFileRecursiveAsync(texturePackBasePath)
|
|
setCustomTexturePackData(undefined, undefined)
|
|
}
|
|
|
|
export const getResourcePackName = async () => {
|
|
// temp
|
|
try {
|
|
return await fs.promises.readFile(join(texturePackBasePath, 'name.txt'), 'utf8')
|
|
} catch (err) {
|
|
return '???'
|
|
}
|
|
}
|
|
|
|
export const fromTexturePackPath = (path) => {
|
|
return join(texturePackBasePath, path)
|
|
}
|
|
|
|
export const updateTexturePackInstalledState = async () => {
|
|
try {
|
|
miscUiState.resourcePackInstalled = await existsAsync(texturePackBasePath)
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
export const installTexturePack = async (file: File | ArrayBuffer) => {
|
|
try {
|
|
await uninstallTexturePack()
|
|
} catch (err) {
|
|
}
|
|
const status = 'Installing resource pack: copying all files';
|
|
setLoadingScreenStatus(status)
|
|
// extract the zip and write to fs every file in it
|
|
const zip = new JSZip()
|
|
const zipFile = await zip.loadAsync(file)
|
|
if (!zipFile.file('pack.mcmeta')) throw new Error('Not a resource pack: missing pack.mcmeta')
|
|
await mkdirRecursive(texturePackBasePath)
|
|
|
|
const allFilesArr = Object.entries(zipFile.files);
|
|
let i = 0
|
|
for (const [path, file] of allFilesArr) {
|
|
const writePath = join(texturePackBasePath, path);
|
|
if (path.endsWith('/')) continue
|
|
await mkdirRecursive(dirname(writePath))
|
|
await fs.promises.writeFile(writePath, Buffer.from(await file.async('arraybuffer')))
|
|
setLoadingScreenStatus(`${status} ${Math.round(++i / allFilesArr.length * 100)}%`)
|
|
}
|
|
await fs.promises.writeFile(join(texturePackBasePath, 'name.txt'), file['name'] ?? '??', 'utf8')
|
|
|
|
if (viewer?.world.active) {
|
|
await genTexturePackTextures(viewer.version)
|
|
}
|
|
setLoadingScreenStatus(undefined)
|
|
}
|
|
|
|
const existsAsync = async (path) => {
|
|
try {
|
|
await fs.promises.stat(path)
|
|
return true
|
|
} catch (err) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
type TextureResolvedData = {
|
|
blockSize: number
|
|
// itemsUrlContent: string
|
|
}
|
|
|
|
const arrEqual = (a: any[], b: any[]) => a.length === b.length && a.every((x) => b.includes(x))
|
|
|
|
const applyTexturePackData = async (version: string, { blockSize }: TextureResolvedData, blocksUrlContent: string) => {
|
|
const result = await fetch(`blocksStates/${version}.json`)
|
|
const blockStates: BlockStates = await result.json()
|
|
const factor = blockSize / 16
|
|
|
|
// this will be refactored with prerender refactor
|
|
const processObj = (x) => {
|
|
if (typeof x !== 'object' || !x) return
|
|
if (Array.isArray(x)) {
|
|
for (const v of x) {
|
|
processObj(v)
|
|
}
|
|
return
|
|
} else {
|
|
const actual = Object.keys(x)
|
|
const needed = ['u', 'v', 'su', 'sv']
|
|
|
|
if (!arrEqual(actual, needed)) {
|
|
for (const v of Object.values(x)) {
|
|
processObj(v)
|
|
}
|
|
return
|
|
}
|
|
for (const k of needed) {
|
|
x[k] *= factor
|
|
}
|
|
}
|
|
}
|
|
processObj(blockStates)
|
|
setCustomTexturePackData(blocksUrlContent, blockStates)
|
|
}
|
|
|
|
const setCustomTexturePackData = (blockTextures, blockStates) => {
|
|
globalThis.texturePackDataBlockStates = blockStates
|
|
globalThis.texturePackDataUrl = blockTextures
|
|
miscUiState.resourcePackInstalled = blockTextures !== undefined
|
|
}
|
|
|
|
const getSizeFromImage = async (filePath: string) => {
|
|
const probeImg = new Image()
|
|
const file = await fs.promises.readFile(filePath, 'base64');
|
|
probeImg.src = `data:image/png;base64,${file}`
|
|
await new Promise((resolve, reject) => {
|
|
probeImg.onload = resolve
|
|
})
|
|
if (probeImg.width !== probeImg.height) throw new Error(`Probe texture ${filePath} is not square`)
|
|
return probeImg.width
|
|
}
|
|
|
|
export const genTexturePackTextures = async (version: string) => {
|
|
setCustomTexturePackData(undefined, undefined)
|
|
const blocksBasePath = '/userData/resourcePacks/default/assets/minecraft/textures/blocks';
|
|
const blocksGenereatedPath = `/userData/resourcePacks/default/${version}.png`
|
|
const genereatedPathData = `/userData/resourcePacks/default/${version}.json`
|
|
if (await existsAsync(blocksBasePath) === false) {
|
|
return
|
|
}
|
|
if (await existsAsync(blocksGenereatedPath) === true) {
|
|
applyTexturePackData(version, JSON.parse(await fs.promises.readFile(genereatedPathData, 'utf8')), await fs.promises.readFile(blocksGenereatedPath, 'utf8'))
|
|
return
|
|
}
|
|
|
|
setLoadingScreenStatus('Generating custom textures')
|
|
|
|
const textureFiles = blocksFileNames.indexes[version].map(k => blocksFileNames.blockNames[k])
|
|
textureFiles.unshift('missing_texture.png')
|
|
|
|
const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length)))
|
|
const originalTileSize = 16
|
|
|
|
const firstBlockFile = (await fs.promises.readdir(blocksBasePath)).find(f => f.endsWith('.png'))
|
|
if (!firstBlockFile) {
|
|
return
|
|
}
|
|
|
|
// we get the size of image from the first block file, which is not ideal but works in 99% cases
|
|
const tileSize = await getSizeFromImage(join(blocksBasePath, firstBlockFile))
|
|
|
|
const imgSize = texSize * tileSize
|
|
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = imgSize
|
|
canvas.height = imgSize
|
|
const src = `textures/${version}.png`
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.imageSmoothingEnabled = false
|
|
const img = new Image()
|
|
img.src = src
|
|
await new Promise((resolve, reject) => {
|
|
img.onerror = reject
|
|
img.onload = resolve
|
|
})
|
|
for (const [i, fileName] of textureFiles.entries()) {
|
|
const x = (i % texSize) * tileSize
|
|
const y = Math.floor(i / texSize) * tileSize
|
|
const xOrig = (i % texSize) * originalTileSize
|
|
const yOrig = Math.floor(i / texSize) * originalTileSize
|
|
let imgCustom: HTMLImageElement
|
|
try {
|
|
const fileBase64 = await fs.promises.readFile(join(blocksBasePath, fileName), 'base64')
|
|
const _imgCustom = new Image()
|
|
await new Promise<void>(resolve => {
|
|
_imgCustom.onload = () => {
|
|
imgCustom = _imgCustom
|
|
resolve();
|
|
}
|
|
_imgCustom.onerror = () => {
|
|
console.log('Skipping issued texture', fileName)
|
|
resolve()
|
|
}
|
|
_imgCustom.src = `data:image/png;base64,${fileBase64}`
|
|
})
|
|
} catch {
|
|
console.log('Skipping not found texture', fileName)
|
|
}
|
|
|
|
if (imgCustom) {
|
|
ctx.drawImage(imgCustom, x, y, tileSize, tileSize)
|
|
} else {
|
|
// todo this involves incorrect mappings for existing textures when the size is different
|
|
ctx.drawImage(img, xOrig, yOrig, originalTileSize, originalTileSize, x, y, tileSize, tileSize)
|
|
}
|
|
}
|
|
const blockDataUrl = canvas.toDataURL('image/png');
|
|
const newData: TextureResolvedData = {
|
|
blockSize: tileSize,
|
|
};
|
|
await fs.promises.writeFile(genereatedPathData, JSON.stringify(newData), 'utf8')
|
|
await fs.promises.writeFile(blocksGenereatedPath, blockDataUrl, 'utf8')
|
|
await applyTexturePackData(version, newData, blockDataUrl)
|
|
|
|
// const a = document.createElement('a')
|
|
// a.href = dataUrl
|
|
// a.download = 'pack.png'
|
|
// a.click()
|
|
}
|
|
|
|
export const watchTexturepackInViewer = (viewer: Viewer) => {
|
|
subscribeKey(miscUiState, 'resourcePackInstalled', () => {
|
|
if (!viewer?.world.active) return
|
|
console.log('reloading world data')
|
|
viewer.world.updateData()
|
|
})
|
|
}
|