UNPKG

@ultrapowa/sc-tools

Version:

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

393 lines (374 loc) 12.9 kB
/* eslint-disable no-console, no-param-reassign */ import { basename, join, dirname } from 'path'; import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs'; import delaunator from 'delaunator'; 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'; const crypto = await import('crypto'); class Importer { static outputFileSync(filepath, ...args) { if (!existsSync(filepath)) { mkdirSync(dirname(filepath), { recursive: true }); } writeFileSync(filepath, ...args); } static async import(inputFilePath, outputDirectory, options) { console.info(`importing ${inputFilePath}...`); let filename = basename(inputFilePath); if (filename.endsWith('_tex.sc')) { const originalName = filename; filename = `${basename(filename, '_tex.sc')}.sc`; console.warn(`${originalName} submitted, ${filename} will be used`); inputFilePath = join(dirname(inputFilePath), `${filename}`); } const projectName = basename(filename, '.sc'); if (!outputDirectory) { outputDirectory = projectName; } console.info('reading file...'); const buffer = await this.readScFile( inputFilePath, join(outputDirectory, 'cache') ); console.info('decoding file data...'); const sc = SupercellSC.decode(SmartBuffer.fromBuffer(buffer)); let texFilePath = join( dirname(inputFilePath), `${projectName}_highres_tex.sc` ); if (!existsSync(texFilePath)) { texFilePath = join( dirname(inputFilePath), `${projectName}_lowres_tex.sc` ); } if (!existsSync(texFilePath)) { texFilePath = join(dirname(inputFilePath), `${projectName}_tex.sc`); } if (existsSync(texFilePath)) { console.info('reading external texture file...'); const texBuffer = await this.readScFile( texFilePath, join(outputDirectory, 'cache') ); console.info('decoding external texture file data...'); const tex = SupercellTexSC.decode(SmartBuffer.fromBuffer(texBuffer)); console.info('merging external texture file data with original data...'); this.mergeTex(sc, tex); } console.info(`generating project to ${outputDirectory}...`); this.sc2scp(outputDirectory, sc, options); console.info('done\n'); } static mergeTex(sc, tex) { tex.textures.forEach((texture) => { const index = sc.textures.findIndex( (scTexture) => !scTexture.pixels.length ); if (index === -1) { throw new Error('could not insert external texture into texture'); } texture.originalSignature = sc.textures[index].tagSignature; sc.textures[index] = texture; }); } static async readScFile(filePath, cacheDirectory) { let buffer = readFileSync(filePath); const hash = crypto.createHash('md5').update(buffer).digest('hex'); const cacheFilePath = join(cacheDirectory, `${hash}.cache`); if (!existsSync(cacheFilePath)) { console.info(' creating cache...'); buffer = await scCompression.decompress(buffer); this.outputFileSync(cacheFilePath, buffer); } else { console.info(' existing cache is being used...'); buffer = readFileSync(cacheFilePath); } return buffer; } static sc2scp(outputDirectory, sc, options) { options = { shapeOuterColor: { red: 0, green: 0, blue: 0, alpha: 0, ...options?.shapeOuterColor, }, flattenShapes: true, ...options, }; const name = basename(outputDirectory); // init config const projectConfig = { useTexFiles: !!sc.textureFileFlag, compression: scCompression.SC, header: { header_7: sc.header.header_7, header_8: sc.header.header_8, header_9: sc.header.header_9, }, exports: sc.exports, }; this.outputFileSync( join(outputDirectory, `${name}.conf`), JSON.stringify(projectConfig, null, 2) ); // import textures sc.textures.forEach((texture, textureIndex) => { const { pixels } = texture; if (!pixels.length) { throw new Error('missing texture data'); } const pixelInfo = getPixelInfo(texture.pixelCode); texture.pixelInfo = pixelInfo; const texConfig = { index: textureIndex, signature: texture.tagSignature, pixelCode: texture.pixelCode, }; if (texture.originalSignature) { texConfig.originalSignature = texture.originalSignature; } const image = new Uint8Array(pixels.length * pixelInfo.bytesPerPixel); texture.image = image; for (let i = 0; i < pixels.length; i += 1) { const color = colors.decode( pixels[i], pixelInfo.pixelType, pixelInfo.pixelFormat ); for (let j = 0; j < pixelInfo.bytesPerPixel; j += 1) { image[pixelInfo.bytesPerPixel * i + j] = color[j]; } } const png = upng.encodeLL( [image], texture.width, texture.height, pixelInfo.cc, pixelInfo.ac, 8 ); this.outputFileSync( join( outputDirectory, 'textures', `${name}_texture_${textureIndex}.png` ), Buffer.from(png) ); this.outputFileSync( join( outputDirectory, 'textures', `${name}_texture_${textureIndex}.conf` ), JSON.stringify(texConfig, null, 2) ); }); // import shapes sc.shapes.forEach((shape) => { let shapeDirectory = join(outputDirectory, 'shapes'); if (!options.flattenShapes) { shapeDirectory = join( shapeDirectory, `${name}_shape_${shape.exportId}` ); } const exportedCommands = shape.shapeDrawBitmapCommands.map((command) => { const exported = { signature: command.tagSignature, textureIndex: command.textureIndex, }; exported.triangles = Array.from( delaunator.from(command.normalizedXY).triangles ); exported.positions = command.normalizedXY.flat(); exported.texcoords = command.normalizedUV.flat(); return exported; }); const exported = { signature: shape.tagSignature, exportId: shape.exportId, totalVertexCount: shape.totalVertexCount, shapeDrawBitmapCommands: exportedCommands, }; this.outputFileSync( join(shapeDirectory, `${name}_shape_${shape.exportId}.conf`), JSON.stringify(exported, null, 2) ); }); // import textFields sc.textFields.forEach((textField) => { const textFieldDirectory = join(outputDirectory, 'text_fields'); const exported = { signature: textField.tagSignature, exportId: textField.exportId, fontName: textField.fontName, color: textField.color, textField_4: textField.textField_4, textField_5: textField.textField_5, multiLineFlag: textField.multiLineFlag, textField_7: textField.textField_7, fontAlign: textField.fontAlign, fontSize: textField.fontSize, pointX: textField.pointX, pointY: textField.pointY, pointU: textField.pointU, pointV: textField.pointV, textField_14: textField.textField_14, textField_15: textField.textField_15, }; if (textField.tagSignature !== 7) { exported.textField_16 = textField.textField_16; if ([21, 25].includes(textField.tagSignature)) { exported.textField_17 = textField.textField_17; } if ([33, 43, 44].includes(textField.tagSignature)) { exported.textField_17 = textField.textField_17; exported.textField_18 = textField.textField_18; exported.textField_19 = textField.textField_19; } if ([43, 44].includes(textField.tagSignature)) { exported.textField_20 = textField.textField_20; } if (textField.tagSignature === 44) { exported.textField_21 = textField.textField_21; } } this.outputFileSync( join( textFieldDirectory, `${name}_text_field_${textField.exportId}.conf` ), JSON.stringify(exported, null, 2) ); }); // import matrices const matricesExport = sc.matrices.map((matrix, matrixIndex) => ({ index: matrixIndex, signature: matrix.tagSignature, normalizedScalars: matrix.normalizedScalars, })); this.outputFileSync( join(outputDirectory, `${name}_matrices.conf`), JSON.stringify(matricesExport, null, 2) ); // import colorTransformation sc.colorTransformations.forEach( (colorTransformation, colorTransformationIndex) => { const colorTransformationDirectory = join( outputDirectory, 'color_transformations' ); const exported = { index: colorTransformationIndex, signature: colorTransformation.tagSignature, ra: colorTransformation.ra, ga: colorTransformation.ga, ba: colorTransformation.ba, am: colorTransformation.am, rm: colorTransformation.rm, gm: colorTransformation.gm, bm: colorTransformation.bm, }; this.outputFileSync( join( colorTransformationDirectory, `${name}_color_transformation_${colorTransformationIndex}.conf` ), JSON.stringify(exported, null, 2) ); } ); // import movieClips sc.movieClips.forEach((movieClip) => { const movieClipDirectory = join(outputDirectory, 'movie_clips'); const exported = { signature: movieClip.tagSignature, exportId: movieClip.exportId, fps: movieClip.fps, frameData: movieClip.frameData, displayObjectIds: movieClip.displayObjectIds, opacities: movieClip.opacities, asciis: movieClip.asciis, }; exported.frames = movieClip.frames.map((frame) => ({ signature: frame.tagSignature, displayObjectCount: frame.displayObjectCount, label: frame.label, })); exported.scalingGrids = movieClip.scalingGrids.map((scalingGrid) => ({ signature: scalingGrid.tagSignature, normalizedScalar0: scalingGrid.normalizedScalar0, normalizedScalar1: scalingGrid.normalizedScalar1, normalizedScalar2: scalingGrid.normalizedScalar2, normalizedScalar3: scalingGrid.normalizedScalar3, })); exported.tag41s = movieClip.tag41s.map((tag41) => ({ signature: tag41.tagSignature, tag41_1: tag41.tag41_1, })); this.outputFileSync( join( movieClipDirectory, `${name}_movie_clip_${movieClip.exportId}.conf` ), JSON.stringify(exported, null, 2) ); }); // import timelines sc.timelines.forEach((timeline, timelineIndex) => { const timelineDirectory = join(outputDirectory, 'timelines'); const exported = { index: timelineIndex, signature: timeline.tagSignature, indices: timeline.indices, }; this.outputFileSync( join(timelineDirectory, `${name}_timeline_${timelineIndex}.conf`), JSON.stringify(exported, null, 2) ); }); // import movie clip modifiers sc.movieClipModifiers.forEach((modifier, modifierIndex) => { const modifierDirectory = join(outputDirectory, 'movie_clip_modifiers'); const exported = { index: modifierIndex, signature: modifier.tagSignature, }; this.outputFileSync( join( modifierDirectory, `${name}_movie_clip_modifier_${modifierIndex}.conf` ), JSON.stringify(exported, null, 2) ); }); // import tag38s sc.tag38s.forEach((tag38, tag38Index) => { const tag38Directory = join(outputDirectory, 'tag38s'); const exported = { index: tag38Index, signature: tag38.tagSignature }; this.outputFileSync( join(tag38Directory, `${name}_tag38_${tag38Index}.conf`), JSON.stringify(exported, null, 2) ); }); // import tag45s sc.tag42s.forEach((tag42, tag42Index) => { const tag42Directory = join(outputDirectory, 'tag42s'); const exported = { index: tag42Index, signature: tag42.tagSignature }; this.outputFileSync( join(tag42Directory, `${name}_tag42_${tag42Index}.conf`), JSON.stringify(exported, null, 2) ); }); } } export default Importer;