├── README.md ├── Reader.py └── sc_decode.py /README.md: -------------------------------------------------------------------------------- 1 | # sc_decode 2 | 3 | Fixed this version for highres_tex , you can get all highres using my assets downloader (https://github.com/Galaxy1036/Sc-Assets-Downloader) 4 | Just rename *highres_tex.png to *_tex.png 5 | 6 | 7 | An effort to reverse-engineer .sc sprite maps. 8 | 9 | Usage: 10 | 11 | python sc_decode.py [-d] [-s] 12 | 13 | -d: dumps the raw data (split in blocks) to a text file. 14 | 15 | -s: dumps all the sprites mapped in to PNG files. 16 | 17 | the output is saved in folders named _out 18 | 19 | (structured data dump coming soon) 20 | 21 | Input must be a file extracted with QuickBMS from the .sc files. 22 | 23 | Known bugs: 24 | 25 | * some red/blue sprite components (Canon) are pasted slightly misaligned 26 | * some composite sprites (Barbarian's sword, Dark Prince's shield, Baby Dragon's tongue, etc) are pasted VERY misaligned 27 | 28 | 29 | Want to help? Check our wiki! 30 | 31 | [sc_decode wiki](https://github.com/umop-aplsdn/sc_decode/wiki) 32 | -------------------------------------------------------------------------------- /Reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def ReadByte(stream): 4 | return int.from_bytes(stream.read(1),'little') 5 | 6 | def ReadUint16(stream): 7 | return int.from_bytes(stream.read(2),'little') 8 | 9 | def ReadInt16(stream): 10 | return int.from_bytes(stream.read(2),'little',signed = True) 11 | 12 | def ReadUint32(stream): 13 | return int.from_bytes(stream.read(4),'little') 14 | 15 | def ReadInt32(stream): 16 | return int.from_bytes(stream.read(4),'little', signed = True) 17 | 18 | def ReadString(stream,length): 19 | return stream.read(length).decode('utf-8') -------------------------------------------------------------------------------- /sc_decode.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import BytesIO 3 | from Reader import * 4 | from PIL import Image, ImageDraw 5 | import os 6 | import argparse 7 | #Fixed version for both highres and lowres tex (also re-written for better support) 8 | #Thanks to umop for his script and Knobse for KosmoSc and help 9 | 10 | def WriteShape(spritedata,sheetdata,ShapeCount,TotalsTexture,filein): 11 | 12 | pathout = path_out(filein) 13 | maxLeft = 0 14 | maxRight = 0 15 | maxAbove = 0 16 | maxBelow = 0 17 | spriteglobals = {'SpriteWidth':0, 'SpriteHeight':0, 'GlobalZeroX':0, 'GlobalZeroY':0} 18 | for x in range(ShapeCount): 19 | for y in range(spritedata[x]['TotalRegions']): 20 | 21 | regionMinX = 32767 22 | regionMaxX = -32767 23 | regionMinY = 32767 24 | regionMaxY = -32767 25 | for z in range (spritedata[x]['Regions'][y]['NumPoints']): 26 | tmpX = spritedata[x]['Regions'][y]['ShapePoints'][z]['x'] 27 | tmpY = spritedata[x]['Regions'][y]['ShapePoints'][z]['y'] 28 | 29 | spritedata[x]['Regions'][y]['Top'] = tmpY if tmpY > spritedata[x]['Regions'][y]['Top'] else spritedata[x]['Regions'][y]['Top'] 30 | spritedata[x]['Regions'][y]['Left'] = tmpX if tmpX < spritedata[x]['Regions'][y]['Left'] else spritedata[x]['Regions'][y]['Left'] 31 | spritedata[x]['Regions'][y]['Bottom'] = tmpY if tmpY < spritedata[x]['Regions'][y]['Bottom'] else spritedata[x]['Regions'][y]['Bottom'] 32 | spritedata[x]['Regions'][y]['Right'] = tmpX if tmpX > spritedata[x]['Regions'][y]['Right'] else spritedata[x]['Regions'][y]['Right'] 33 | 34 | tmpX = spritedata[x]['Regions'][y]['SheetPoints'][z]['x'] 35 | tmpY = spritedata[x]['Regions'][y]['SheetPoints'][z]['y'] 36 | 37 | regionMinX = tmpX if tmpX < regionMinX else regionMinX 38 | regionMaxX = tmpX if tmpX > regionMaxX else regionMaxX 39 | regionMinY = tmpY if tmpY < regionMinY else regionMinY 40 | regionMaxY = tmpY if tmpY > regionMaxY else regionMaxY 41 | 42 | spritedata[x]['Regions'][y] = region_rotation(spritedata[x]['Regions'][y]) 43 | 44 | if (spritedata[x]['Regions'][y]['Rotation'] == 90 or spritedata[x]['Regions'][y]['Rotation'] == 270): 45 | spritedata[x]['Regions'][y]['SpriteWidth'] = regionMaxY - regionMinY 46 | spritedata[x]['Regions'][y]['SpriteHeight'] = regionMaxX - regionMinX 47 | else: 48 | spritedata[x]['Regions'][y]['SpriteWidth'] = regionMaxX - regionMinX 49 | spritedata[x]['Regions'][y]['SpriteHeight'] = regionMaxY - regionMinY 50 | 51 | tmpX = spritedata[x]['Regions'][y]['SpriteWidth'] 52 | tmpY = spritedata[x]['Regions'][y]['SpriteHeight'] 53 | 54 | #determine origin pixel (0,0) 55 | spritedata[x]['Regions'][y]['RegionZeroX'] = \ 56 | int(round(abs(spritedata[x]['Regions'][y]['Left']) * (tmpX/(spritedata[x]['Regions'][y]['Right'] - spritedata[x]['Regions'][y]['Left'])))) 57 | spritedata[x]['Regions'][y]['RegionZeroY'] = \ 58 | int(round(abs(spritedata[x]['Regions'][y]['Bottom']) * (tmpY/(spritedata[x]['Regions'][y]['Top'] - spritedata[x]['Regions'][y]['Bottom'])))) 59 | 60 | #sprite image dimensions 61 | #max sprite size is determined from the zero points 62 | #the higher the 0, more pixels to the left/top are required 63 | #the higher the diff between the 0 and the image width/height, more pixels to the right/bottom are required 64 | maxLeft = spritedata[x]['Regions'][y]['RegionZeroX'] if spritedata[x]['Regions'][y]['RegionZeroX'] > maxLeft else maxLeft 65 | maxAbove = spritedata[x]['Regions'][y]['RegionZeroY'] if spritedata[x]['Regions'][y]['RegionZeroY'] > maxAbove else maxAbove 66 | tmpX = spritedata[x]['Regions'][y]['SpriteWidth'] - spritedata[x]['Regions'][y]['RegionZeroX'] 67 | tmpY = spritedata[x]['Regions'][y]['SpriteHeight'] - spritedata[x]['Regions'][y]['RegionZeroY'] 68 | maxRight = tmpX if tmpX > maxRight else maxRight 69 | maxBelow = tmpY if tmpY > maxBelow else maxBelow 70 | 71 | spriteglobals['SpriteWidth'] = maxLeft + maxRight 72 | spriteglobals['SpriteHeight'] = maxAbove + maxBelow 73 | spriteglobals['GlobalZeroX'] = maxLeft 74 | spriteglobals['GlobalZeroY'] = maxAbove 75 | 76 | 77 | #seems like final sprite size takes into account the mask's line, so we add 2 to each dimension 78 | spriteglobals['SpriteWidth'] += 2 79 | spriteglobals['SpriteHeight'] += 2 80 | 81 | maxrange = len(str(ShapeCount)) 82 | 83 | #debug 84 | #print(spriteglobals) 85 | 86 | # 87 | #third: all data gathered, time to start cutting 88 | # 89 | 90 | sheetimage = [] 91 | for x in range(TotalsTexture): 92 | sheetimage.append(Image.open(filein + '_tex' + (x * '_') + '.png').convert('RGBA')) 93 | 94 | 95 | for x in range(ShapeCount): 96 | #debug 97 | #print ('sprite {}'.format(x)) 98 | #print (spritedata[x]) 99 | 100 | #credit goes to the author of http://stackoverflow.com/questions/22588074/polygon-crop-clip-using-python-pil 101 | #for how to clip sprites from the spritesheet using a polygon/mask 102 | 103 | outImage = Image.new('RGBA', (spriteglobals['SpriteWidth'], spriteglobals['SpriteHeight']), None) 104 | 105 | for y in range(spritedata[x]['TotalRegions']): 106 | polygon = [] 107 | for z in range(spritedata[x]['Regions'][y]['NumPoints']): 108 | polygon.append((spritedata[x]['Regions'][y]['SheetPoints'][z]['x'], spritedata[x]['Regions'][y]['SheetPoints'][z]['y'])) 109 | 110 | sheetID = spritedata[x]['Regions'][y]['SheetID'] 111 | imMask = Image.new('L', (sheetdata[sheetID]['x'], sheetdata[sheetID]['y']), 0) 112 | ImageDraw.Draw(imMask).polygon(polygon, fill=255) 113 | #bbox is the cut image size 114 | bbox = imMask.getbbox() 115 | regionsize = (bbox[2]-bbox[0], bbox[3]-bbox[1]) 116 | imMask = imMask.crop(bbox) 117 | 118 | tmpRegion = Image.new('RGBA', regionsize, None) 119 | 120 | tmpRegion.paste(sheetimage[sheetID].crop(bbox), None, imMask) 121 | if (spritedata[x]['Regions'][y]['Mirroring']==1): 122 | tmpRegion = tmpRegion.transform(regionsize, Image.EXTENT, (regionsize[0], 0, 0, regionsize[1])) 123 | 124 | tmpRegion = tmpRegion.rotate(spritedata[x]['Regions'][y]['Rotation'], expand=True) 125 | 126 | #debug: save sprite components 127 | #tmpRegion.save(pathout + '/' + filein + '_sprite_dbg_' + str(x) + '_' + str(y) + ' 128 | 129 | 130 | #align the zeroes 131 | pasteLeft = spriteglobals['GlobalZeroX'] - spritedata[x]['Regions'][y]['RegionZeroX'] 132 | pasteTop = spriteglobals['GlobalZeroY'] - spritedata[x]['Regions'][y]['RegionZeroY'] 133 | 134 | #debug: where image is pasted 135 | #print('paste sprite {} region {} on {},{}'.format(x, y, pasteLeft, pasteTop)) 136 | 137 | outImage.paste(tmpRegion, (pasteLeft, pasteTop), tmpRegion) 138 | outImage.save(pathout + '/' + filein + '_sprite_' + str(x).rjust(maxrange, '0') + '.png') 139 | 140 | print('done') 141 | 142 | 143 | def path_out(filein): 144 | pathout = os.getcwd() + '/' + filein + '_out' 145 | if not (os.path.exists(pathout)): 146 | os.makedirs(pathout) 147 | return pathout 148 | 149 | 150 | def region_rotation(region): 151 | #first: determine orientation and mirroring 152 | #ref: http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order 153 | sumSheet = 0 154 | sumShape = 0 155 | for z in range(region['NumPoints']): 156 | sumSheet += ((region['SheetPoints'][(z+1)%(region['NumPoints'])]['x'] - region['SheetPoints'][z]['x']) * 157 | (region['SheetPoints'][(z+1)%(region['NumPoints'])]['y'] + region['SheetPoints'][z]['y'])) 158 | sumShape += ((region['ShapePoints'][(z+1)%(region['NumPoints'])]['x'] - region['ShapePoints'][z]['x']) * 159 | (region['ShapePoints'][(z+1)%(region['NumPoints'])]['y'] + region['ShapePoints'][z]['y'])) 160 | 161 | sheetOrientation = -1 if (sumSheet<0) else 1 162 | shapeOrientation = -1 if (sumShape<0) else 1 163 | 164 | region['Mirroring'] = 0 if (shapeOrientation == sheetOrientation) else 1 165 | 166 | if (region['Mirroring'] == 1): 167 | #what, just horizontally mirror the points? 168 | for x in range(region['NumPoints']): 169 | region['ShapePoints'][x]['x'] *= -1 170 | 171 | #define region rotation 172 | #pX, qX mean "where in X is point 1, according to point 0" 173 | #pY, qY mean "where in Y is point 1, according to point 0" 174 | #possible values are "M"ore, "L"ess and "S"ame 175 | if (region['SheetPoints'][1]['x']>region['SheetPoints'][0]['x']): 176 | px = 'M' 177 | elif (region['SheetPoints'][1]['x']region['SheetPoints'][0]['y']): 185 | py = 'L' 186 | else: 187 | py = 'S' 188 | 189 | if (region['ShapePoints'][1]['x']>region['ShapePoints'][0]['x']): 190 | qx = 'M' 191 | elif (region['ShapePoints'][1]['x']region['ShapePoints'][0]['y']): 197 | qy = 'M' 198 | elif (region['ShapePoints'][1]['y']