├── MANIFEST.in ├── requirements.txt ├── setup.py ├── README.md └── bin └── ccbup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dimensions==0.0.2 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from setuptools import setup 3 | 4 | setup( 5 | name = 'CCBUpgrade', 6 | version = '0.2.0', 7 | author = 'Sidebolt Studios', 8 | author_email = 'support@sidebolt.com', 9 | scripts = ['bin/ccbup.py'], 10 | url = 'http://sidebolt.com/', 11 | install_requires = ['dimensions==0.0.2'], 12 | description = 'Converts CocosBuilder 3 files to the SpriteBuilder 1.0 format.' 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CCBUpgrade 2 | 3 | This script upgrades CocosBuilder 3 files to the SpriteBuilder 1.0 format. 4 | 5 | ## Installation 6 | 7 | ``` 8 | sudo pip install CCBUpgrade 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | usage: ccbup.py [-h] [--destructive] project file [file ...] 15 | 16 | positional arguments: 17 | project A CocosBuilder CCB project file 18 | file A CocosBuilder CCB file to process 19 | 20 | optional arguments: 21 | -h, --help show this help message and exit 22 | --destructive, -d Modify files in-place. 23 | ``` 24 | 25 | ## What it does 26 | 27 | - Converts CCLayer to CCNode 28 | - Converts CCLayerColor to CCNodeColor 29 | - Converts CCLayerGradient to CCNodeGradient 30 | - Converts CCMenu to CCNode 31 | - Converts CCMenuItemImage to CCButton 32 | - Converts CCParticleSystemQuad to CCParticleSystem 33 | - Strips tags 34 | - Renames displayFrame to spriteFrame 35 | - Upgrades size, position, color, and opacity properties to the new format 36 | - Fixes the callback and sound channels 37 | - Removes the "ignoreAnchorPointForPosition" property and attempts to offset nodes to compensate (may or may not work) 38 | 39 | ## License 40 | MIT 41 | -------------------------------------------------------------------------------- /bin/ccbup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import plistlib 4 | import argparse 5 | import logging 6 | import copy 7 | import os 8 | 9 | try: 10 | import dimensions 11 | except ImportError: 12 | logging.critical('Please install the \'dimensions\' Python package: sudo pip install dimensions') 13 | exit(1) 14 | 15 | kCCBSizeTypeAbsolute = 0 16 | kCCBSizeTypePercent = 1 17 | kCCBSizeTypeRelativeContainer = 2 18 | kCCBSizeTypeHorizontalPercent = 3 19 | kCCBSizeTypeVerticalPercent = 4 20 | kCCBSizeTypeMultiplyResolution = 5 21 | 22 | CCSizeUnitPoints = 0 23 | CCSizeUnitUIPoints = 1 24 | CCSizeUnitNormalized = 2 25 | CCSizeUnitInsetPoints = 3 26 | CCSizeUnitInsetUIPoints = 4 27 | 28 | # Figure out the absolute position of a node from the bottom left 29 | def absolutePosition(node, parentSize): 30 | positionProp = None 31 | for prop in node['properties']: 32 | if prop['type'] == 'Position': 33 | positionProp = prop 34 | break 35 | 36 | if positionProp is None: 37 | return [0, 0] 38 | 39 | pt = positionProp['value'] 40 | posType = pt[2] 41 | 42 | absPt = [0, 0] 43 | 44 | if posType == kCCBPositionTypeRelativeBottomLeft or posType == kCCBPositionTypeMultiplyResolution: 45 | absPt = pt 46 | elif posType == kCCBPositionTypeRelativeTopLeft: 47 | absPt[0] = pt[0] 48 | absPt[1] = parentSize[1] - pt[1] 49 | elif posType == kCCBPositionTypeRelativeTopRight: 50 | absPt[0] = parentSize[0] - pt[0] 51 | absPt[1] = parentSize[1] - pt[1] 52 | elif posType == kCCBPositionTypeRelativeBottomRight: 53 | absPt[0] = parentSize[0] - pt[0] 54 | absPt[1] = pt[1] 55 | elif posType == kCCBPositionTypePercent: 56 | absPt[0] = int(parentSize[0] * pt[0] / 100.0) 57 | absPt[1] = int(parentSize[1] * pt[1] / 100.0) 58 | 59 | return absPt 60 | 61 | # Try to offset the absolute position of a node. This may or may not work. 62 | def offsetAbsolutePosition(positionProp, parentSize, offset): 63 | posType = positionProp['value'][2] 64 | 65 | pos = [positionProp['value'][0] + offset[0], positionProp['value'][1] + offset[1]] 66 | 67 | finalPos = [0, 0] 68 | 69 | if posType == kCCBPositionTypeRelativeBottomLeft or posType == kCCBPositionTypeMultiplyResolution: 70 | finalPos = pos 71 | elif posType == kCCBPositionTypeRelativeTopLeft: 72 | finalPos[0] = pos[0] 73 | finalPos[1] = parentSize[1] - pos[1] 74 | elif posType == kCCBPositionTypeRelativeTopRight: 75 | finalPos[0] = parentSize[0] - pos[0] 76 | finalPos[1] = parentSize[1] - pos[1] 77 | elif posType == kCCBPositionTypeRelativeBottomRight: 78 | finalPos[0] = parentSize[0] - pos[0] 79 | finalPos[1] = pos[1] 80 | elif posType == kCCBPositionTypePercent: 81 | if parentSize[0] == 0: 82 | finalPos[0] = pos[0] 83 | else: 84 | finalPos[0] = pos[0] * 100.0 / parentSize[0] 85 | if parentSize[1] == 0: 86 | finalPos[1] = pos[1] 87 | else: 88 | finalPos[1] = pos[1] * 100.0 / parentSize[1] 89 | 90 | positionProp['value'][0] = finalPos[0] 91 | positionProp['value'][1] = finalPos[1] 92 | 93 | # Convert CCLayer to CCNode 94 | # Convert CCLayerColor to CCNodeColor 95 | # Convert CCLayerGradient to CCNodeGradient 96 | # Remove deprecated properties 97 | def stripCCLayer(node): 98 | isColorLayer = node['baseClass'] == 'CCLayerColor' 99 | isGradientLayer = node['baseClass'] == 'CCLayerGradient' 100 | if node['baseClass'] == 'CCLayer' or isColorLayer or isGradientLayer: 101 | if isColorLayer: 102 | node['baseClass'] = 'CCNodeColor' 103 | elif isGradientLayer: 104 | node['baseClass'] = 'CCNodeGradient' 105 | else: 106 | node['baseClass'] = 'CCNode' 107 | 108 | # Strip invalid properties 109 | stripProps = ['touchEnabled', 'mouseEnabled'] 110 | props = node['properties'] 111 | for prop in list(props): 112 | if prop['name'] in stripProps: 113 | props.remove(prop) 114 | 115 | # Remove tag property 116 | def stripTag(node): 117 | props = node['properties'] 118 | for prop in list(props): 119 | if prop['name'] == 'tag': 120 | props.remove(prop) 121 | 122 | # Calculate the size of an image reference 123 | def imageSize(project, imagePath): 124 | for resourcePath in project['resourcePaths']: 125 | path = os.path.join(project['location'], resourcePath['path']) 126 | 127 | finalPath = os.path.join(path, imagePath) 128 | if os.path.isfile(finalPath): 129 | return list(dimensions.dimensions(finalPath))[:2] 130 | 131 | basename = os.path.basename(imagePath) 132 | dirname = os.path.dirname(imagePath) 133 | 134 | finalPath = os.path.join(path, dirname, 'resources-auto', basename) 135 | if os.path.isfile(finalPath): 136 | d = dimensions.dimensions(finalPath) 137 | return [int(d[0] * 0.25), int(d[1] * 0.25)] 138 | 139 | finalPath = os.path.join(path, basename, 'resources-iphone', basename) 140 | if os.path.isfile(finalPath): 141 | return list(dimensions.dimensions(finalPath))[:2] 142 | 143 | logging.warning('Failed to determine size of image \'%s\'' % imagePath) 144 | return [0, 0] 145 | 146 | # Convert CCMenu to CCNode 147 | # Convert CCMenuItemImage to CCButton 148 | def convertCCMenu(project, node): 149 | if node['baseClass'] == 'CCMenu': 150 | node['baseClass'] = 'CCNode' 151 | 152 | if node['baseClass'] == 'CCMenuItemImage': 153 | node['baseClass'] = 'CCButton' 154 | normalSpriteFrame = None 155 | for prop in node['properties']: 156 | if prop['name'] == 'normalSpriteFrame': 157 | prop['name'] = 'backgroundSpriteFrame|Normal' 158 | normalSpriteFrame = prop 159 | if prop['name'] == 'selectedSpriteFrame': 160 | prop['name'] = 'backgroundSpriteFrame|Highlighted' 161 | if prop['name'] == 'disabledSpriteFrame': 162 | prop['name'] = 'backgroundSpriteFrame|Disabled' 163 | if prop['name'] == 'isEnabled': 164 | prop['name'] = 'userInteractionEnabled' 165 | 166 | node['properties'].append({ 167 | 'name': 'title', 168 | 'type': 'String', 169 | 'value': '', 170 | }) 171 | 172 | if normalSpriteFrame is not None: 173 | selectedSpriteFrame = copy.deepcopy(normalSpriteFrame) 174 | selectedSpriteFrame['name'] = 'backgroundSpriteFrame|Selected' 175 | node['properties'].append(selectedSpriteFrame) 176 | 177 | size = imageSize(project, normalSpriteFrame['value'][1]) 178 | node['properties'].append({ 179 | 'name': 'preferredSize', 180 | 'type': 'Size', 181 | 'value': [ 182 | size[0], 183 | size[1], 184 | 0, 185 | 0, 186 | ], 187 | }) 188 | 189 | # Set the type of a sequence channel and all its keyframes 190 | def setChannelType(channel, code): 191 | channel['type'] = code 192 | for keyframe in channel['keyframes']: 193 | keyframe['type'] = code 194 | 195 | # Change callbackChannel type to 12 196 | # Change soundChannel type to 11 197 | def convertCallbacks(root): 198 | for sequence in root['sequences']: 199 | setChannelType(sequence['callbackChannel'], 12) 200 | setChannelType(sequence['soundChannel'], 11) 201 | 202 | # Convert opacity from (0-255) range to (0-1) range 203 | # Change opacity keyframe type to 10 204 | def convertOpacity(node): 205 | for prop in node['properties']: 206 | if prop['name'] == 'opacity': 207 | prop['type'] = 'Float' 208 | prop['value'] /= 255.0 209 | 210 | value = prop.get('baseValue') 211 | if value is not None: 212 | prop['baseValue'] = value / 255.0 213 | 214 | if 'animatedProperties' in node: 215 | for index, prop in node['animatedProperties'].iteritems(): 216 | if 'opacity' in prop: 217 | prop['opacity']['type'] = 10 218 | for keyframe in prop['opacity']['keyframes']: 219 | keyframe['value'] /= 255.0 220 | keyframe['type'] = 10 221 | 222 | # Convert CCParticleSystemQuad to CCParticleSystem 223 | def convertParticleSystem(node): 224 | if node['baseClass'] == 'CCParticleSystemQuad': 225 | node['baseClass'] = 'CCParticleSystem' 226 | 227 | # Remove ignoreAnchorPointForPosition property and attempt to offset the node to compensate 228 | def convertAndStripIgnoreAnchorPointForPosition(parent, parentSize, absSize, node): 229 | props = node['properties'] 230 | convert = False 231 | anchorProperty = None 232 | positionProperty = None 233 | for prop in list(props): 234 | if prop['type'] == 'Position': 235 | positionProperty = prop 236 | if prop['name'] == 'ignoreAnchorPointForPosition': 237 | props.remove(prop) 238 | convert = convert or prop['value'] 239 | if prop['name'] == 'anchorPoint': 240 | anchorProperty = prop 241 | 242 | if parent is None: 243 | anchorProperty['value'] = [0, 0] 244 | 245 | if positionProperty is None and 'animatedProperties' in node: 246 | # No position property. That means it's animated. Oh no. 247 | for index, prop in node['animatedProperties'].iteritems(): 248 | if 'position' in prop: 249 | positionProperty = prop['position'] 250 | break 251 | 252 | if positionProperty is not None and convert: 253 | offset = [anchorProperty['value'][0] * absSize[0], anchorProperty['value'][1] * absSize[1]] 254 | if 'keyframes' in positionProperty: 255 | for keyframe in positionProperty['keyframes']: 256 | offsetAbsolutePosition(keyframe, parentSize, offset) 257 | else: 258 | offsetAbsolutePosition(positionProperty, parentSize, offset) 259 | 260 | CCPositionUnitPoints = 0 261 | CCPositionUnitUIPoints = 1 262 | CCPositionUnitNormalized = 2 263 | 264 | CCPositionReferenceCornerBottomLeft = 0 265 | CCPositionReferenceCornerTopLeft = 1 266 | CCPositionReferenceCornerTopRight = 2 267 | CCPositionReferenceCornerBottomRight = 3 268 | 269 | kCCBPositionTypeRelativeBottomLeft = 0 270 | kCCBPositionTypeRelativeTopLeft = 1 271 | kCCBPositionTypeRelativeTopRight = 2 272 | kCCBPositionTypeRelativeBottomRight = 3 273 | kCCBPositionTypePercent = 4 274 | kCCBPositionTypeMultiplyResolution = 5 275 | 276 | # Convert to new position format 277 | # Convert percentage values from (0-100) range to (0-1) range 278 | def convertPosition(node): 279 | posType = -1 280 | positionProp = None 281 | for prop in node['properties']: 282 | if prop['type'] == 'Position': 283 | value = prop['value'] 284 | if len(value) < 5: 285 | posType = value[2] 286 | 287 | while len(value) < 5: 288 | value.append(0) 289 | 290 | if posType == kCCBPositionTypeRelativeBottomLeft: 291 | value[2] = CCPositionReferenceCornerBottomLeft 292 | value[3] = value[4] = CCPositionUnitPoints 293 | elif posType == kCCBPositionTypeMultiplyResolution: 294 | value[2] = CCPositionReferenceCornerBottomLeft 295 | value[3] = value[4] = CCPositionUnitUIPoints 296 | elif posType == kCCBPositionTypeRelativeTopLeft: 297 | value[2] = CCPositionReferenceCornerTopLeft 298 | value[3] = value[4] = CCPositionUnitPoints 299 | elif posType == kCCBPositionTypeRelativeTopRight: 300 | value[2] = CCPositionReferenceCornerTopRight 301 | value[3] = value[4] = CCPositionUnitPoints 302 | elif posType == kCCBPositionTypeRelativeBottomRight: 303 | value[2] = CCPositionReferenceCornerBottomRight 304 | value[3] = value[4] = CCPositionUnitPoints 305 | elif posType == kCCBPositionTypePercent: 306 | value[0] /= 100.0 307 | value[1] /= 100.0 308 | value[2] = CCPositionReferenceCornerBottomLeft 309 | value[3] = value[4] = CCPositionUnitNormalized 310 | positionProp = prop 311 | break 312 | 313 | if posType == kCCBPositionTypePercent: 314 | if 'animatedProperties' in node: 315 | for index, prop in node['animatedProperties'].iteritems(): 316 | if 'position' in prop: 317 | if positionProp is not None: 318 | positionProp['baseValue'] = copy.deepcopy(positionProp['value']) 319 | for keyframe in prop['position']['keyframes']: 320 | keyframe['value'][0] /= 100.0 321 | keyframe['value'][1] /= 100.0 322 | 323 | # Change displayFrame property to spriteFrame 324 | def convertSpriteFrames(node): 325 | spriteFrameProp = None 326 | for prop in node['properties']: 327 | if prop['name'] == 'displayFrame': 328 | prop['name'] = 'spriteFrame' 329 | spriteFrameProp = prop 330 | break 331 | 332 | if 'animatedProperties' in node: 333 | if spriteFrameProp is not None: 334 | spriteFrameProp['baseValue'] = [ 335 | spriteFrameProp['value'][1], 336 | 'Use regular file', 337 | ] 338 | for index, prop in node['animatedProperties'].iteritems(): 339 | if 'displayFrame' in prop: 340 | displayFrame = prop['displayFrame'] 341 | del prop['displayFrame'] 342 | prop['spriteFrame'] = displayFrame 343 | for keyframe in prop['spriteFrame']['keyframes']: 344 | keyframe['name'] = 'spriteFrame' 345 | 346 | # Convert to new size format 347 | def convertSize(node): 348 | for prop in node['properties']: 349 | if prop['type'] == 'Size': 350 | value = prop['value'] 351 | 352 | if value[2] == kCCBSizeTypeAbsolute: 353 | value[2] = CCSizeUnitUIPoints 354 | if len(value) < 4: 355 | value.append(CCSizeUnitUIPoints) 356 | else: 357 | value[3] = CCSizeUnitUIPoints 358 | 359 | elif value[2] == kCCBSizeTypePercent: 360 | value[0] /= 100.0 361 | value[1] /= 100.0 362 | value[2] = CCSizeUnitNormalized 363 | if len(value) < 4: 364 | value.append(CCSizeUnitNormalized) 365 | else: 366 | value[3] = CCSizeUnitNormalized 367 | 368 | elif value[2] == kCCBSizeTypeRelativeContainer: 369 | value[2] = CCSizeUnitInsetUIPoints 370 | if len(value) < 4: 371 | value.append(CCSizeUnitInsetUIPoints) 372 | else: 373 | value[3] = CCSizeUnitInsetUIPoints 374 | 375 | elif value[2] == kCCBSizeTypeHorizontalPercent: 376 | value[0] /= 100.0 377 | value[2] = CCSizeUnitNormalized 378 | if len(value) < 4: 379 | value.append(CCSizeUnitUIPoints) 380 | else: 381 | value[3] = CCSizeUnitUIPoints 382 | 383 | elif value[2] == kCCBSizeTypeVerticalPercent: 384 | value[1] /= 100.0 385 | value[2] = CCSizeUnitUIPoints 386 | if len(value) < 4: 387 | value.append(CCSizeUnitNormalized) 388 | else: 389 | value[3] = CCSizeUnitNormalized 390 | 391 | elif value[2] == kCCBSizeTypeMultiplyResolution: 392 | value[2] = CCSizeUnitPoints 393 | if len(value) < 4: 394 | value.append(CCSizeUnitPoints) 395 | else: 396 | value[3] = CCSizeUnitPoints 397 | 398 | # Convert RGB values from (0-255) range to (0-1) range 399 | def convertColor3(node): 400 | for prop in node['properties']: 401 | if prop['type'] == 'Color3': 402 | value = prop['value'] 403 | for i in xrange(len(value)): 404 | value[i] /= 255.0 405 | while len(value) < 4: 406 | value.append(1) 407 | 408 | value = prop.get('baseValue') 409 | if value is not None: 410 | for i in xrange(len(value)): 411 | value[i] /= 255.0 412 | while len(value) < 4: 413 | value.append(1) 414 | 415 | if 'animatedProperties' in node: 416 | for index, prop in node['animatedProperties'].iteritems(): 417 | if 'color' in prop: 418 | for keyframe in prop['color']['keyframes']: 419 | value = keyframe['value'] 420 | for i in xrange(len(value)): 421 | value[i] /= 255.0 422 | while len(value) < 4: 423 | value.append(1) 424 | 425 | # Calculate absolute size of a node 426 | def absoluteSize(project, node, parentSize): 427 | sizeProp = None 428 | for prop in node['properties']: 429 | if prop['name'] == 'contentSize': 430 | sizeProp = prop 431 | break 432 | 433 | if sizeProp is None: 434 | for prop in node['properties']: 435 | if prop['type'] == 'SpriteFrame': 436 | return imageSize(project, prop['value'][1]) 437 | 438 | return [0, 0] 439 | 440 | absSize = [0, 0] 441 | size = sizeProp['value'] 442 | sizeType = sizeProp['value'][2] 443 | if sizeType == kCCBSizeTypeAbsolute or sizeType == kCCBSizeTypeMultiplyResolution: 444 | absSize = size[:2] 445 | elif sizeType == kCCBSizeTypeRelativeContainer: 446 | absSize[0] = parentSize[0] - size[0] 447 | absSize[1] = parentSize[1] - size[1] 448 | elif sizeType == kCCBSizeTypePercent: 449 | absSize[0] = int(parentSize[0] * size[0] / 100.0) 450 | absSize[1] = int(parentSize[1] * size[1] / 100.0) 451 | elif sizeType == kCCBSizeTypeHorizontalPercent: 452 | absSize[0] = int(parentSize[0] * size[0] / 100.0) 453 | absSize[1] = size[1] 454 | elif sizeType == kCCBSizeTypeVerticalPercent: 455 | absSize[0] = size[0] 456 | absSize[2] = int(parentSize[1] * size[1] / 100.0) 457 | 458 | return absSize 459 | 460 | # Build a new path to write to in case we are doing this non-destructively 461 | def nonDestructivePath(f): 462 | fileNameParts = os.path.splitext(f) 463 | return fileNameParts[0] + '-new' + fileNameParts[1] 464 | 465 | # Fix CCBFile references if we are doing this non-destructively 466 | def fixCCBPaths(node): 467 | for prop in node['properties']: 468 | if prop['type'] == 'CCBFile': 469 | prop['value'] = nonDestructivePath(prop['value']) 470 | 471 | trace = [] 472 | 473 | # Process a CCNode 474 | def process(project, parent, parentSize, node, args): 475 | try: 476 | absSize = absoluteSize(project, node, parentSize) 477 | 478 | convertSize(node) 479 | 480 | stripCCLayer(node) 481 | 482 | stripTag(node) 483 | 484 | convertCCMenu(project, node) 485 | 486 | convertParticleSystem(node) 487 | 488 | convertAndStripIgnoreAnchorPointForPosition(parent, parentSize, absSize, node) 489 | 490 | if not args.destructive: 491 | fixCCBPaths(node) 492 | 493 | convertSpriteFrames(node) 494 | 495 | convertPosition(node) 496 | 497 | convertColor3(node) 498 | 499 | convertOpacity(node) 500 | 501 | for child in node['children']: 502 | process(project, node, absSize, child, args) 503 | 504 | except Exception: 505 | klass = node.get('customClass') 506 | if klass is None or klass == '': 507 | klass = node.get('baseClass') 508 | error = '> "%s" of type %s' % (node.get('displayName'), klass) 509 | if parent is None: 510 | logging.critical('Node hierarchy:\n' + ', parent of\n'.join(reversed(trace))) 511 | else: 512 | trace.append(error) 513 | raise 514 | 515 | if __name__ == '__main__': 516 | parser = argparse.ArgumentParser(description = 'Converts CocosBuilder 3 files to the SpriteBuilder 1.0 format.' + 517 | ' Visit https://github.com/sidebolt/CCBUpgrade for more info.') 518 | parser.add_argument('project', metavar = 'project', type = str, help = 'A CocosBuilder CCB project file') 519 | parser.add_argument('files', metavar = 'file', type = str, nargs='+', help = 'A CocosBuilder CCB file to process') 520 | parser.add_argument('--destructive', '-d', dest = 'destructive', action = 'store_true', default = False, help = 'Modify files in-place.') 521 | args = parser.parse_args() 522 | 523 | logging.basicConfig(level = logging.DEBUG) 524 | 525 | project = plistlib.readPlist(args.project) 526 | project['location'] = os.path.abspath(os.path.dirname(args.project)) 527 | 528 | for f in args.files: 529 | logging.info('Processing %s...' % f) 530 | doc = plistlib.readPlist(f) 531 | 532 | convertCallbacks(doc) 533 | 534 | process(project, None, [480, 320], doc['nodeGraph'], args) 535 | 536 | if args.destructive: 537 | newFile = f 538 | else: 539 | newFile = nonDestructivePath(f) 540 | 541 | plistlib.writePlist(doc, newFile) 542 | 543 | logging.info('Wrote %s' % newFile) 544 | --------------------------------------------------------------------------------