malie 引擎 dzi 图片坐标合成工具

逆向工程maliedzi
浏览数 - 601发布于 - 2025-04-15 - 05:32
鲲

5317

一般来说 malie 引擎制作的游戏,例如 light 社的游戏(八命阵,神怒之日等),这种引擎在使用 GARbro 或者 土拔鼠的工具 拆包后解出的图片是被切片的,这不是完整的 CG 图片

image.png然后有若干 dzi 的图片坐标索引文件,所以我们要做的事情就是根据这个坐标文件来合成 tex 文件夹里面被切分的图片

image.png然后代码是这样的,嗯。。这是 TypeScript 写的,因为最近几年 Web 写的太多了,C / C++ / C# 什么的忘光了,什么你说 js 凭什么能写这些,为什么不行

 import fs from 'fs-extra'
 import path from 'path'
 import sharp from 'sharp'
 
 const EVENT_DIR = path.resolve(__dirname, 'event')
 const TEX_DIR = path.join(EVENT_DIR, 'tex')
 const DIST_DIR = path.join(EVENT_DIR, 'dist')
 
 // layer_1 is original size, layer_2 is original size / 0.5,layer_3 is original size / 0.25 ...
 const ENABLE_LOWER_LAYERS = true
 
 const parseDzi = async (filePath: string) => {
   const content = await fs.readFile(filePath, 'utf-8')
   const lines = content.trim().split(/\r?\n/)
 
   const [formatLine, sizeLine, ...restLines] = lines
   const [imgWidth, imgHeight] = sizeLine.split(',').map(Number)
 
   const layers: { tiles: string[][]; rows: number; cols: number }[] = []
 
   let i = 0
   while (i < restLines.length) {
     const [cols, rows] = restLines[i++].split(',').map(Number)
     const tileLines: string[][] = []
 
     for (let r = 0; r < rows; r++) {
       tileLines.push(restLines[i++].split(','))
     }
 
     layers.push({ tiles: tileLines, rows, cols })
   }
 
   return { width: imgWidth, height: imgHeight, layers }
 }
 
 const composeLayer = async (
   tiles: string[][],
   group: string,
   layerIndex: number,
   outputPath: string,
   finalWidth: number,
   finalHeight: number
 ) => {
   if (!tiles || tiles.length === 0 || tiles[0].length === 0) {
     return
   }
 
   const rows = tiles.length
   const cols = tiles[0].length
 
   const firstTilePath = path.join(TEX_DIR, tiles[0][0] + '.png')
   const { width: tileW, height: tileH } = await sharp(firstTilePath).metadata()
 
   if (!tileW || !tileH) {
     throw new Error(`Cannot get the tile size: ${firstTilePath}`)
   }
 
   const composedWidth = cols * tileW
   const composedHeight = rows * tileH
 
   const fullImg = sharp({
     create: {
       width: composedWidth,
       height: composedHeight,
       channels: 4,
       background: { r: 0, g: 0, b: 0, alpha: 0 },
     },
   })
 
   const composites: sharp.OverlayOptions[] = []
 
   for (let y = 0; y < rows; y++) {
     for (let x = 0; x < cols; x++) {
       const tileRelPath = tiles[y][x]
       if (!tileRelPath) {
         continue
       }
 
       const tileAbsPath = path.join(TEX_DIR, tileRelPath + '.png')
 
       const buffer = await fs.readFile(tileAbsPath)
       composites.push({
         input: buffer,
         left: x * tileW,
         top: y * tileH,
       })
     }
   }
 
   const layerOutputDir = path.join(outputPath, group)
   await fs.ensureDir(layerOutputDir)
 
   const outFile = path.join(layerOutputDir, `layer_${layerIndex}.png`)
 
   const cropWidth = Math.min(finalWidth, composedWidth)
   const cropHeight = Math.min(finalHeight, composedHeight)
 
   await fullImg
     .composite(composites)
     .extract({ left: 0, top: 0, width: cropWidth, height: cropHeight })
     .toFile(outFile)
 
   console.log(`Compose successfully: ${outFile}`)
 }
 
 const processAllDziFiles = async () => {
   const files = await fs.readdir(EVENT_DIR)
 
   if (fs.existsSync(DIST_DIR)) {
     await fs.rm(DIST_DIR, { recursive: true, force: true })
   }
 
   for (const file of files) {
     if (!file.endsWith('.dzi')) continue
 
     const filePath = path.join(EVENT_DIR, file)
     const groupName = path.basename(file, '.dzi')
 
     console.log(`Handling ${groupName} ...`)
     const {
       width: imgWidth,
       height: imgHeight,
       layers,
     } = await parseDzi(filePath)
 
     for (let i = 0; i < layers.length; i++) {
       if (i > 1 && !ENABLE_LOWER_LAYERS) {
         console.log(`Skip layer_${i} due to config`)
         continue
       }
 
       const { tiles } = layers[i]
       const scaleFactor = Math.pow(0.5, i - 1)
 
       const targetWidth = Math.round(imgWidth * scaleFactor)
       const targetHeight = Math.round(imgHeight * scaleFactor)
 
       await composeLayer(
         tiles,
         groupName,
         i,
         DIST_DIR,
         targetWidth,
         targetHeight
       )
     }
   }
 
   console.log('Assemble all cgs successfully!')
 }
 
 processAllDziFiles().catch((err) => {
   console.error('ERROR OCCURRENT', err)
 })

EVENT_DIR 是拆出来的图片文件夹,把这个 event 文件夹放在和这个脚本的同级目录下面

TEX_DIR 就是图中的那个 tex 文件夹,点进去有一堆图片碎片

DIST_DIR 是输出文件夹,合成后的图片会被输出到这个文件夹里面

ENABLE_LOWER_LAYERS 是一个配置项,不知道为什么每一张 CG 都被拆成了 0, 1, 2 三种大小,然后每一种大小都是在原分辨率的基础上长宽变成了 0.5 倍

ENABLE_LOWER_LAYERS 代表连同这些缩小过的 CG 一起合成,但是好像没什么用所以默认是关闭的

使用方法需要有 Node.js 环境,可以使用 pnpm i -g esno 来安装 esno, 然后使用 esno 运行这个脚本

这里还有一个,写了一半但是没有继续写的仓库,我想把这个东东封装成命令行,但是我真的懒得把它打包成可执行文件,哭了

https://github.com/KUN1007/assemble-malie-dzi

里面有一个测试的 Rust 程序,也写了一半,嗯,咕咕咕咕咕

为什么要写,因为今天实在是无聊,突然想到这个了,然后就写一写,为什么要玩这个游戏,因为有小只可爱软萌白毛红瞳女孩子!!!!!!!!!!!!啊啊啊啊啊啊啊啊啊啊啊啊这简直就是天使啊啊啊啊啊啊啊啊!身上有她在爬,看见就会滚来滚去的,嗯

鲲-1744695072332-sir02bxpng

重新编辑于 - 2025-04-15 - 05:38

kohaku