UNPKG

@ultrapowa/sc-tools

Version:

A tool to unpack, repack, edit and play 2d animations from Supercell games

196 lines (189 loc) 7.74 kB
/* eslint-disable no-console, no-param-reassign */ import { basename, join } from 'path'; import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import glob from 'glob'; import belongsToPolygon from 'point-in-polygon'; import gl from 'gl-constants'; import upng from '../upng.mjs'; import colors from '../gl-color.mjs'; import { getPixelInfo } from '../pixel-info.mjs'; import SupercellSC from '../supercell-sc/supercell-sc.mjs'; import SwfTexture from '../supercell-sc/tags/swf-texture.mjs'; import ShapeOriginal from '../supercell-sc/tags/shape-original.mjs'; import ShapeDrawBitmapCommand from '../supercell-sc/tags/shape-draw-bitmap-command.mjs'; class PngShapeBuilder { static build(inputProjectDirectory, outputDirectory) { const projectName = basename(inputProjectDirectory, '.conf'); console.info(`building ${projectName} png shapes...`); if (!outputDirectory) { outputDirectory = inputProjectDirectory; } this.scp2sc(inputProjectDirectory, outputDirectory); console.info('done\n'); } static scp2sc(inputProjectDirectory, outputDirectory) { const name = basename(outputDirectory, '.conf'); const sc = new SupercellSC(); // load textures const textureDirectory = join(inputProjectDirectory, 'textures'); const texturePaths = glob.sync(join(textureDirectory, '*.png')); texturePaths.forEach((texturePath) => { const texture = new SwfTexture(); const texName = basename(texturePath, '.png'); const texConfig = JSON.parse( readFileSync(join(textureDirectory, `${texName}.conf`)) ); texture.tagSignature = texConfig.signature; const png = upng.decode(readFileSync(texturePath)); texture.width = png.width; texture.height = png.height; const image = Uint8Array.from(png.data); const pixelInfo = getPixelInfo(texConfig.pixelCode); texture.pixelCode = pixelInfo.pixelCode; texture.image = image; texture.pixelInfo = pixelInfo; for (let i = 0; i < image.length; i += pixelInfo.bytesPerPixel) { texture.pixels.push( colors.encode( image.slice(i, i + pixelInfo.bytesPerPixel), pixelInfo.pixelType, pixelInfo.pixelFormat ) ); } sc.textures[texConfig.index] = texture; }); // load shapes const shapesDirectory = join(inputProjectDirectory, 'shapes'); const shapePaths = glob.sync(join(shapesDirectory, '*.conf')); sc.shapes = shapePaths .map((shapePath) => { const shape = new ShapeOriginal(); const shapeName = basename(shapePath, '.conf'); const shapeConfig = JSON.parse( readFileSync(join(shapesDirectory, `${shapeName}.conf`)) ); shape.tagSignature = shapeConfig.signature; shape.exportId = shapeConfig.exportId; shape.totalVertexCount = shapeConfig.totalVertexCount; shape.shapeDrawBitmapCommands = shapeConfig.shapeDrawBitmapCommands.map( (commandConfig) => { const shapeDrawBitmapCommand = new ShapeDrawBitmapCommand(); shapeDrawBitmapCommand.tagSignature = commandConfig.signature; shapeDrawBitmapCommand.textureIndex = commandConfig.textureIndex; const { positions: normalizedXYs, texcoords: normalizedUVs } = commandConfig; while (normalizedXYs.length) { shapeDrawBitmapCommand.normalizedXY.push( normalizedXYs.splice(0, 2) ); } while (normalizedUVs.length) { shapeDrawBitmapCommand.normalizedUV.push( normalizedUVs.splice(0, 2) ); } shapeDrawBitmapCommand.vertexCount = shapeDrawBitmapCommand.normalizedXY.length; return shapeDrawBitmapCommand; } ); return shape; }) .sort((a, b) => a.exportId - b.exportId); // arrange shapes in increasing order of exportIds // export commands const pngShapesDirectory = join(outputDirectory, 'png-shapes'); mkdirSync(pngShapesDirectory, { recursive: true }); sc.shapes.forEach((shape) => { shape.shapeDrawBitmapCommands.forEach((command, commandIndex) => { const texture = sc.textures[command.textureIndex]; const { pixelInfo, image: sourceImage } = texture; const vertices = []; // round 1, denormalize for (let i = 0; i < command.vertexCount; i += 1) { vertices.push({ x: command.normalizedXY[i][0] * 0.05, y: command.normalizedXY[i][1] * 0.05, u: command.tagSignature === 22 ? Math.round( (command.normalizedUV[i][0] * texture.width) / 0xffff ) : command.normalizedUV[i][0], v: command.tagSignature === 22 ? Math.round( (command.normalizedUV[i][1] * texture.height) / 0xffff ) : command.normalizedUV[i][1], }); } // round 2, get width and height in both texture and final command const uMax = vertices.reduce( (max, vertex) => (vertex.u > max ? vertex.u : max), vertices[0].u ); const uMin = vertices.reduce( (min, vertex) => (vertex.u < min ? vertex.u : min), vertices[0].u ); const width = uMax - uMin < 1 ? 1 : uMax - uMin; const vMax = vertices.reduce( (max, vertex) => (vertex.v > max ? vertex.v : max), vertices[0].v ); const vMin = vertices.reduce( (min, vertex) => (vertex.v < min ? vertex.v : min), vertices[0].v ); const height = vMax - vMin < 1 ? 1 : vMax - vMin; for (let i = 0; i < command.vertexCount; i += 1) { vertices[i].x /= width < 1 ? 1 : width; vertices[i].y /= height < 1 ? 1 : height; } // round 3, copy pixels to final image const polygon = vertices.map((vertex) => [vertex.u, vertex.v]); const image = new Uint8Array(width * height * pixelInfo.bytesPerPixel); for (let x = uMin; x < uMin + width; x += 1) { for (let y = vMin; y < vMax + height; y += 1) { const targetIndex = (width * (y - vMin) + (x - uMin)) * pixelInfo.bytesPerPixel; if (belongsToPolygon([x, y], polygon)) { const sourceIndex = (texture.width * y + x) * pixelInfo.bytesPerPixel; for (let i = 0; i < pixelInfo.bytesPerPixel; i += 1) { image[targetIndex + i] = sourceImage[sourceIndex + i]; } } else if ( pixelInfo.pixelFormat === gl.RGB || pixelInfo.pixelFormat === gl.RGBA ) { image[targetIndex] = 0; image[targetIndex + 1] = 0; image[targetIndex + 2] = 0; if (pixelInfo.pixelFormat === gl.RGBA) { image[targetIndex + 3] = 0; } } } } const png = upng.encodeLL( [image], width, height, pixelInfo.cc, pixelInfo.ac, 8 ); writeFileSync( join( pngShapesDirectory, `${name}_shape_${shape.exportId}_${commandIndex}.png` ), Buffer.from(png) ); }); }); } } export default PngShapeBuilder;