UNPKG

@ultrapowa/sc-tools

Version:

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

386 lines (368 loc) 16.7 kB
/* eslint-disable no-console, no-param-reassign */ import { basename, join, dirname } from 'path'; import { writeFileSync, readFileSync, mkdirSync } from 'fs'; import glob from 'glob'; import * as scCompression from 'sc-compression'; import upng from '../upng.mjs'; import { SmartBuffer } from '../smart-buffer.mjs'; import colors from '../gl-color.mjs'; import { getPixelInfo } from '../pixel-info.mjs'; import SupercellSC from '../supercell-sc/supercell-sc.mjs'; import SupercellTexSC from '../supercell-sc/supercell-tex-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'; import TextFieldOriginal from '../supercell-sc/tags/text-field-original.mjs'; import Matrix2x3 from '../supercell-sc/tags/matrix-2x3.mjs'; import ColorTransformation from '../supercell-sc/tags/color-transformation.mjs'; import MovieClipOriginal from '../supercell-sc/tags/movie-clip-original.mjs'; import MovieClipFrame from '../supercell-sc/tags/movie-clip-frame.mjs'; import ScalingGrid from '../supercell-sc/tags/scaling-grid.mjs'; import Tag41 from '../supercell-sc/tags/tag-41.mjs'; import Timeline from '../supercell-sc/tags/timeline.mjs'; import MovieClipModifierOriginal from '../supercell-sc/tags/movie-clip-modifier-original.mjs'; import Tag38 from '../supercell-sc/tags/tag-38.mjs'; import Tag42 from '../supercell-sc/tags/tag-42.mjs'; class Builder { static async build(inputProjectDirectory, outputDirectory) { const projectName = basename(inputProjectDirectory, '.conf'); console.info(`building ${projectName}...`); if (!outputDirectory) { outputDirectory = inputProjectDirectory; } await this.scp2sc(inputProjectDirectory, outputDirectory); console.info('done\n'); } static async scp2sc(inputProjectDirectory, outputDirectory) { const name = basename(outputDirectory, '.conf'); const projectConfig = JSON.parse( readFileSync(join(inputProjectDirectory, `${name}.conf`)) ); const sc = new SupercellSC(); sc.header.header_7 = projectConfig.header.header_7; sc.header.header_8 = projectConfig.header.header_8; sc.header.header_9 = projectConfig.header.header_9; sc.exports = projectConfig.exports; sc.textureFileFlag = projectConfig.useTexFiles; sc.lowresFlag = true; // export textures const externalTextures = []; 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); if (png.ctype !== pixelInfo.colorType) { console.warn( `the given colorType (${png.ctype}) doesn't match with the desired colorType (${pixelInfo.colorType})` ); console.warn('the .sc file may not work as intended'); console.warn( 'either convert the image format to desired colorType or change the pixelCode in .conf file in order to match the desired colorType' ); } texture.pixelCode = pixelInfo.pixelCode; 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 ) ); } if (projectConfig.useTexFiles) { sc.textureFileFlag = !!projectConfig.useTexFiles; externalTextures[texConfig.index] = texture; const placeholder = new SwfTexture(); placeholder.width = texture.width; placeholder.height = texture.height; placeholder.pixelCode = texture.pixelCode; placeholder.tagSignature = texConfig.originalSignature; sc.textures[texConfig.index] = placeholder; } else { sc.textures[texConfig.index] = texture; } }); // export tex textures if (projectConfig.useTexFiles) { const texSc = new SupercellTexSC(); externalTextures.forEach((texture) => texSc.textures.push(texture)); const buffer = new SmartBuffer(); console.info('encoding external texture file data...'); texSc.encode(buffer); console.info('writing external texture file...'); await this.writeScFile( join(outputDirectory, 'build', `${name}_tex.sc`), buffer.toBuffer(), projectConfig.compression ); } // export 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 textFields const textFieldsDirectory = join(inputProjectDirectory, 'text_fields'); const textFieldPaths = glob.sync(join(textFieldsDirectory, '*.conf')); sc.textFields = textFieldPaths .map((textFieldPath) => { const textField = new TextFieldOriginal(); const textFieldName = basename(textFieldPath, '.conf'); const textFieldConfig = JSON.parse( readFileSync(join(textFieldsDirectory, `${textFieldName}.conf`)) ); textField.tagSignature = textFieldConfig.signature; textField.exportId = textFieldConfig.exportId; textField.fontName = textFieldConfig.fontName; textField.color = textFieldConfig.color; textField.textField_4 = textFieldConfig.textField_4; textField.textField_5 = textFieldConfig.textField_5; textField.multiLineFlag = textFieldConfig.multiLineFlag; textField.textField_7 = textFieldConfig.textField_7; textField.fontAlign = textFieldConfig.fontAlign; textField.fontSize = textFieldConfig.fontSize; textField.pointX = textFieldConfig.pointX; textField.pointY = textFieldConfig.pointY; textField.pointU = textFieldConfig.pointU; textField.pointV = textFieldConfig.pointV; textField.textField_14 = textFieldConfig.textField_14; textField.textField_15 = textFieldConfig.textField_15; textField.textField_16 = textFieldConfig.textField_16; textField.textField_17 = textFieldConfig.textField_17; textField.textField_18 = textFieldConfig.textField_18; textField.textField_19 = textFieldConfig.textField_19; textField.textField_20 = textFieldConfig.textField_20; textField.textField_21 = textFieldConfig.textField_21; return textField; }) .sort((a, b) => a.exportId - b.exportId); // arrange textFields in increasing order of exportIds // export matrices const matricesFile = join( inputProjectDirectory, `${basename(inputProjectDirectory)}_matrices.conf` ); const matricesConfig = JSON.parse(readFileSync(matricesFile)); sc.matrices = matricesConfig.map((matrixConfig) => { const matrix2x3 = new Matrix2x3(); matrix2x3.index = matrixConfig.index; matrix2x3.tagSignature = matrixConfig.signature; matrix2x3.normalizedScalars = matrixConfig.normalizedScalars; return matrix2x3; }); // export colorTransformations const colorTransformationsDirectory = join( inputProjectDirectory, 'color_transformations' ); const colorTransformationPaths = glob.sync( join(colorTransformationsDirectory, '*.conf') ); sc.colorTransformations = colorTransformationPaths .map((colorTransformationPath) => { const colorTransformation = new ColorTransformation(); const colorTransformationName = basename( colorTransformationPath, '.conf' ); const colorTransformationConfig = JSON.parse( readFileSync( join( colorTransformationsDirectory, `${colorTransformationName}.conf` ) ) ); colorTransformation.index = colorTransformationConfig.index; colorTransformation.tagSignature = colorTransformationConfig.signature; colorTransformation.ra = colorTransformationConfig.ra; colorTransformation.ga = colorTransformationConfig.ga; colorTransformation.ba = colorTransformationConfig.ba; colorTransformation.am = colorTransformationConfig.am; colorTransformation.rm = colorTransformationConfig.rm; colorTransformation.gm = colorTransformationConfig.gm; colorTransformation.bm = colorTransformationConfig.bm; return colorTransformation; }) .sort((a, b) => a.index - b.index); // arrange in increasing order // export movieClips const movieClipsDirectory = join(inputProjectDirectory, 'movie_clips'); const movieClipPaths = glob.sync(join(movieClipsDirectory, '*.conf')); sc.movieClips = movieClipPaths .map((movieClipPath) => { const movieClip = new MovieClipOriginal(); const movieClipName = basename(movieClipPath, '.conf'); const movieClipConfig = JSON.parse( readFileSync(join(movieClipsDirectory, `${movieClipName}.conf`)) ); movieClip.tagSignature = movieClipConfig.signature; movieClip.exportId = movieClipConfig.exportId; movieClip.fps = movieClipConfig.fps; movieClip.frameCount = movieClipConfig.frames.length; movieClip.frameData = movieClipConfig.frameData; movieClip.frameDataLength = movieClip.tagSignature === 14 ? 14 : movieClip.frameData.length; movieClip.displayObjectIds = movieClipConfig.displayObjectIds; movieClip.opacities = movieClipConfig.opacities; movieClip.asciis = movieClipConfig.asciis; movieClip.frames = movieClipConfig.frames.map((frameConfig) => { const movieClipFrame = new MovieClipFrame(); movieClipFrame.tagSignature = frameConfig.signature; movieClipFrame.displayObjectCount = frameConfig.displayObjectCount; movieClipFrame.label = frameConfig.label; return movieClipFrame; }); movieClip.scalingGrids = movieClipConfig.scalingGrids.map( (scalingGridConfig) => { const scalingGrid = new ScalingGrid(); scalingGrid.tagSignature = scalingGridConfig.signature; scalingGrid.normalizedScalar0 = scalingGridConfig.normalizedScalar0; scalingGrid.normalizedScalar1 = scalingGridConfig.normalizedScalar1; scalingGrid.normalizedScalar2 = scalingGridConfig.normalizedScalar2; scalingGrid.normalizedScalar3 = scalingGridConfig.normalizedScalar3; return scalingGrid; } ); movieClip.tag41s = movieClipConfig.tag41s.map((tag41Config) => { const tag41 = new Tag41(); tag41.tagSignature = tag41Config.signature; tag41.tag41_1 = tag41Config.tag41_1; return tag41; }); return movieClip; }) .sort((a, b) => a.exportId - b.exportId); // arrange movieClips in increasing order of exportIds; // updating header sc.header.shapeCount = sc.shapes.length; sc.header.movieClipCount = sc.movieClips.length; sc.header.textureCount = sc.textures.length; sc.header.textFieldCount = sc.textFields.length; sc.header.matrixCount = sc.matrices.length; sc.header.colorTransformationCount = sc.colorTransformations.length; // export timelines const timelinesDirectory = join(inputProjectDirectory, 'timelines'); const timelinePaths = glob.sync(join(timelinesDirectory, '*.conf')); sc.timelines = timelinePaths .map((timelinePath) => { const timeline = new Timeline(); const timelineName = basename(timelinePath, '.conf'); const timelineConfig = JSON.parse( readFileSync(join(timelinesDirectory, `${timelineName}.conf`)) ); timeline.index = timelineConfig.index; timeline.tagSignature = timelineConfig.signature; timeline.indices = timelineConfig.indices; return timeline; }) .sort((a, b) => a.index - b.index); // arrange in increasing order // export movie clip modifiers const modifiersDirectory = join( inputProjectDirectory, 'movie_clip_modifiers' ); const modifierPaths = glob.sync(join(modifiersDirectory, '*.conf')); sc.movieClipModifiers = modifierPaths .map((modifierPath) => { const modifier = new MovieClipModifierOriginal(); const modifierName = basename(modifierPath, '.conf'); const modifierConfig = JSON.parse( readFileSync(join(modifiersDirectory, `${modifierName}.conf`)) ); modifier.index = modifierConfig.index; modifier.tagSignature = modifierConfig.signature; return modifier; }) .sort((a, b) => a.index - b.index); // arrange in increasing order // export tag38s const tag38sDirectory = join(inputProjectDirectory, 'tag38s'); const tag38Paths = glob.sync(join(tag38sDirectory, '*.conf')); sc.tag38s = tag38Paths .map((timelinePath) => { const tag38 = new Tag38(); const tag38Name = basename(timelinePath, '.conf'); const tag38Config = JSON.parse( readFileSync(join(tag38sDirectory, `${tag38Name}.conf`)) ); tag38.index = tag38Config.index; tag38.tagSignature = tag38Config.signature; return tag38; }) .sort((a, b) => a.index - b.index); // arrange in increasing order // export tag42s const tag42sDirectory = join(inputProjectDirectory, 'tag42s'); const tag42Paths = glob.sync(join(timelinesDirectory, '*.conf')); sc.tag42s = tag42Paths .map((timelinePath) => { const tag42 = new Tag42(); const tag42Name = basename(timelinePath, '.conf'); const tag42Config = JSON.parse( readFileSync(join(tag42sDirectory, `${tag42Name}.conf`)) ); tag42.index = tag42Config.index; tag42.tagSignature = tag42Config.signature; return tag42; }) .sort((a, b) => a.index - b.index); // arrange in increasing order // export sc file const buffer = new SmartBuffer(); console.info('encoding file data...'); sc.encode(buffer); console.info('writing file...'); await this.writeScFile( join(outputDirectory, 'build', `${name}.sc`), buffer.toBuffer(), projectConfig.compression ); } static async writeScFile(filePath, buffer, compression) { console.info(' compressing file'); buffer = await scCompression.compress(buffer, compression); console.info(' writing to disk'); mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, buffer); } } export default Builder;