def _externalize_anim(anim, animations_path_rel, base_path): ensure(anim.get('name', '') != '', 'empty animation name') anim_path_rel = '{}/{}.json'.format(animations_path_rel, anim['name']) anim_path = '{}/{}'.format(base_path, anim_path_rel) with open(anim_path, 'w') as anim_file: dict_to_json(anim, anim_file, base_path) return {'type': 'import', 'src': anim_path_rel}
def tile_subrasters(layer): """ Return a list of tiles position in a layer layer: A layer as parsed from an ORA file. Result type: [ {'x': int, 'y': int}, ... ] Each element in the list is a position, in pixels, from the top-left corner of the layer. Each returned position is the top-left pixel of an 8x8 tile. """ ensure( layer['raster'].size[0] % 8 == 0 and layer['raster'].size[1] % 8 == 0, 'partial tiles in raster of size {}x{}'.format( layer['raster'].size[0], layer['raster'].size[1])) tile_positions = [] raster_horizontal_tiles_count = layer['raster'].size[0] // 8 raster_vertical_tiles_count = layer['raster'].size[1] // 8 for tile_y_num_in_raster in range(raster_vertical_tiles_count): tile_y_pos_in_raster = tile_y_num_in_raster * 8 for tile_x_num_in_raster in range(raster_horizontal_tiles_count): tile_x_pos_in_raster = tile_x_num_in_raster * 8 tile_positions.append({ 'x': tile_x_pos_in_raster, 'y': tile_y_pos_in_raster }) return tile_positions
def check(self): # Self consistency check of characters for character in self.characters: character.check() # Check that no unique name is shared between characters character_names = [] animation_names = [] def ensure_unique_animation_name(anim): ensure(anim.name not in animation_names, 'multiple animations are named "{}"'.format(anim.name)) for character in self.characters: ensure(character.name not in character_names, 'multiple characters are named "{}"'.format(character.name)) character_names.append(character.name) ensure_unique_animation_name(character.victory_animation) ensure_unique_animation_name(character.defeat_animation) ensure_unique_animation_name(character.menu_select_animation) for anim in character.animations: ensure_unique_animation_name(anim) # Self consistency check of tilesets for tileset in self.tilesets: tileset.check() # Check that all tilesets are uniqueley named and not oversized tileset_names = [] for tileset_index in range(len(self.tilesets)): tileset = self.tilesets[tileset_index] ensure(tileset.name is not None, 'tileset #{} is not named'.format(tileset_index)) ensure(re.match('[a-z_][a-z0-9_]+', tileset.name) is not None, 'invalid tileset name "{}" (shall be lower case, numbers and underscores)'.format(tileset.name)) ensure(tileset.name not in tileset_names, 'multiple tilesets are named "{}"'.format(tileset.name)) ensure(len(tileset.tiles) <= 256) tileset_names.append(tileset.name)
def _uniq_transparent(color): ensure( isinstance(color, tuple) and len(color) == 4, 'expected RGBA images, but seems not to be the case') if color[3] == 0: return (0, 0, 0, 0) else: return color
def check(self): ensure(isinstance(self.input, int) or isinstance(self.input, str)) if isinstance(self.input, int): ensure(0 <= self.input and self.input <= 255) if isinstance(self.input, str): ensure(self.input[:17] == 'CONTROLLER_INPUT_') ensure(isinstance(self.duration, int)) ensure(0 <= self.duration and self.duration <= 255)
def place_in_tileset(tile, tileset, tilename): """ Get the name and attributes of a tile in a tileset, append the tile if not already present tile: The tile to search for tileset: The tileset to search in tilename: If the tile is added, it will be given this name return a tuple (name, attributes) name: The name of the tile found (may differ from tilename) attributes: a dictionary {'v': bool, 'h': bool} v: True if the returned tile needs to be flipped vertically to match the searched tile h: True if the returned tile needs to be flipped horizontally to match the seached tile """ found_tile_name = None flip = None for try_flip in [{ 'v': False, 'h': False }, { 'v': False, 'h': True }, { 'v': True, 'h': False }, { 'v': True, 'h': True }]: flipped = stblib.tiles.Tile( representation=copy.deepcopy(tile._representation)) if try_flip['v']: flipped.flip_v() if try_flip['h']: flipped.flip_h() try: found_tile_index = tileset.tiles.index(flipped) found_tile_name = tileset.tilenames[found_tile_index] flip = try_flip break except ValueError: pass if found_tile_name is None: found_tile_name = tilename flip = {'v': False, 'h': False} tileset.tiles.append(tile) tileset.tilenames.append(found_tile_name) ensure(found_tile_name is not None, 'internal error: place_in_tileset failed to determine a tilename') ensure( flip is not None, 'internal error: place_in_tileset failed to determine tile\'s attributes' ) return (found_tile_name, flip)
def _handle_tileset(obj, base_path): if obj.get('src') is not None: obj['tiles'] = extract_tiles_from_img('{}/{}'.format(base_path, obj['src'])) nb_tiles = len(obj['tiles']) nb_tilenames = len(obj['tilenames']) ensure(nb_tiles >= nb_tilenames, 'less tiles in image "{}" than names in the tileset'.format(obj['src'])) if nb_tiles > nb_tilenames: obj['tiles'] = obj['tiles'][:nb_tilenames] del obj['src'] return obj
def extract_params(qs, known_params, object_type, object_name): """ Extract parameters from a query string to a dict while ensuring there is no unknown parameter qs: the query string known_params: a list of known parameters, any unknown parameter found will fail an "ensure" check object_type: string containing the type of object to which the parameters relate (for error message purpose) object_name: string containing the name of the object to which the parameters relate (for error message purpose) """ params = urllib.parse.parse_qs(qs, strict_parsing = True) for param_name in params.keys(): ensure(param_name in known_params, 'unknown {} parameter "{}" for {} {}'.format(object_type, param_name, object_type, object_name)) ensure(len(params[param_name]) == 1, '{} parameter "{}" defined multiple times in {} {}'.format(object_type, param_name, object_type, object_name)) params[param_name] = params[param_name][0] return params
def parse_fileobj(f, on_listing=None, on_file=None): line_num = 1 for line in f: parsed = parse_line(line) ensure(parsed['type'] in ['listing', 'empty', 'filename']) if parsed['type'] == 'listing': if on_listing is not None: on_listing(line_num, parsed['parsed']) elif parsed['type'] == 'filename': if on_file is not None: on_file(line_num, parsed['file']) line_num += 1
def check(self): ensure(is_valid_label_name(self.name)) ensure(isinstance(self.steps, list)) ensure(len(self.steps) > 0, 'empty action') for step in self.steps: ensure(isinstance(step, AiActionStep)) step.check()
def check(self): ensure(isinstance(self.action, str)) ensure(isinstance(self.hitbox, AiHitbox)) self.hitbox.check() ensure(isinstance(self.constraints, int)) ensure( self.constraints <= AiAttack.ConstraintFlag.DIRECTION_LEFT + AiAttack.ConstraintFlag.DIRECTION_RIGHT, "unknown flag set in action's constraints") ensure( not (self.constraint_set(AiAttack.ConstraintFlag.DIRECTION_LEFT) and self.constraint_set( AiAttack.ConstraintFlag.DIRECTION_RIGHT)), "impossible constraints mix: right and left")
def tileset_to_img(tileset, img_with_in_tiles, img_height_in_tiles): # Normalize to accept a Tileset or a list of tiles as input tiles = None if isinstance(tileset, stblib.tiles.Tileset): tiles = tileset.tiles else: tiles = tileset ensure( len(tiles) <= img_with_in_tiles * img_height_in_tiles, 'too much tiles in the tileset: {} / {}'.format( len(tiles), img_with_in_tiles * img_height_in_tiles)) # Create empty image img_size = (img_with_in_tiles * 8, img_height_in_tiles * 8) img = PIL.Image.new('P', img_size, 0) img.putpalette([ 238, 130, 238, 0, 0, 0, 128, 128, 128, 255, 255, 255, ] + [0] * (256 * 3 - 4 * 3)) # Draw pixels from tiles data tile_num = 0 for tile in tiles: representation = None if isinstance(tile, stblib.tiles.Tile): representation = tile._representation else: representation = tile['representation'] for tile_y in range(8): img_y = (int(tile_num / img_with_in_tiles) * 8) + tile_y for tile_x in range(8): img_x = (tile_num % img_with_in_tiles) * 8 + tile_x img.putpixel((img_x, img_y), representation[tile_y][tile_x]) tile_num += 1 return img
def parse_listing_line(line): """ Parse a listing line. Warning: not a filename line nor an empty line, just an actual listing line """ if line[-1] == '\n': line = line[:-1] ensure(len(line) >= 39, 'listing line truncated "{}"'.format(line)) return { 'line': int(line[0:5]), 'segment': line[6], 'address': int(line[8:12], 16), 'data_repr': line[14:38], #TODO parse bytes in "data" field, and deprecate this one 'code': line[39:] }
def parse_tile(sprite_layer, pos_in_raster, palettes, sprite_container_name): """ Extract an stblib Tile from an ORA layer sprite_layer: The layer containing original sprite pos_in_raster: {x,y} pixel position of the sprite in the layer palettes: List of colors parsed from the palettes ORA layer sprite_container_name: Name of the sprite layer's parent (for error message purpose) return a tuple (tile, palette_number) tile: The parsed tile palette_number: The NES palette used to colorize that tile """ tile = stblib.tiles.Tile() palette_num = 0 for y in range(8): y_in_raster = pos_in_raster['y'] + y for x in range(8): x_in_raster = pos_in_raster['x'] + x # Convert pixel data to palette index try: color_index = palettes.index( _uniq_transparent(sprite_layer['raster'].getpixel( (x_in_raster, y_in_raster)))) except ValueError: ensure( False, 'a sprite in {} use a color not found in palettes: sprite "{}", position "{}x{}", color "{}", palettes: {}' .format( sprite_container_name, sprite_layer['name'], x_in_raster, y_in_raster, sprite_layer['raster'].getpixel( (x_in_raster, y_in_raster)), palettes)) # Store pixel in tile tile._representation[y][x] = color_index % 4 if color_index != 0: palette_num = int(color_index / 4) return (tile, palette_num)
def check(self): # Self consistency check of characters for character in self.characters: character.check() # Check that no unique name is shared between characters character_names = [] animation_names = [] def ensure_unique_animation_name(anim): ensure(anim.name not in animation_names, 'multiple animations are named "{}"'.format(anim.name)) for character in self.characters: ensure(character.name not in character_names, 'multiple characters are named "{}"'.format(character.name)) character_names.append(character.name) ensure_unique_animation_name(character.victory_animation) ensure_unique_animation_name(character.defeat_animation) ensure_unique_animation_name(character.menu_select_animation) for anim in character.animations: ensure_unique_animation_name(anim)
def main(): FIRST_AVAILABLE_BANK = 5 # Parse command line if len(sys.argv) < 3 or sys.argv[1] == '-h' or sys.argv[1] == '--help': print( 'Compile a game mod stored in JSON format to Super Tilt Bro. source files' ) print('') print('usage: {} game-mod-path super-tilt-bro-path') print('') return 1 mod_file = sys.argv[1] ensure(os.path.isfile(mod_file), 'file not found: "{}"'.format(mod_file)) game_dir = sys.argv[2] ensure(os.path.isdir(game_dir), 'directory not found: "{}"'.format(game_dir)) game_dir = os.path.abspath(game_dir) if os.path.basename(game_dir) == 'game': gamedir = os.path.dirname(gamedir) ensure( os.path.isdir('{}/game'.format(game_dir)), '"game/" folder not found in source directory "{}"'.format(game_dir)) # Parse mod with open(mod_file, 'r') as f: mod_dict = stblib.jsonformat.json_to_dict(f, os.path.dirname(mod_file)) mod = stblib.dictformat.import_from_dict(mod_dict) mod.check() # Generate characters char_to_bank = {} current_bank = FIRST_AVAILABLE_BANK for character in mod.characters: if character.name not in char_to_bank: char_to_bank[character.name] = current_bank current_bank += 1 generate_character(character, game_dir) # Generate shared character files generate_characters_index(mod.characters, game_dir) # Generate tilesets tileset_to_bank = {} current_bank = FIRST_AVAILABLE_BANK for tileset in mod.tilesets: tileset_to_bank[tileset.name] = current_bank current_bank += 1 generate_tileset(tileset, game_dir) # Generate bank files generate_banks(char_to_bank, tileset_to_bank, game_dir) return 0
def extract_tiles_from_img(image_file_path): tiles = [] # Open image file and do sanity checks img = PIL.Image.open(image_file_path) ensure(img.mode == 'P', 'image file "{}" is not in palette mode'.format(image_file_path)) width = img.size[0] height = img.size[1] ensure( width % 8 == 0, 'image file "{}" does not contain 8x8 tiles'.format(image_file_path)) ensure( height % 8 == 0, 'image file "{}" does not contain 8x8 tiles'.format(image_file_path)) # Compute useful values width_in_tiles = int(width / 8) height_in_tiles = int(height / 8) # Generate tiles for y_tile in range(height_in_tiles): for x_tile in range(width_in_tiles): tile = { 'type': 'tile', 'representation': [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], ], } for y in range(8): y_img = y_tile * 8 + y for x in range(8): x_img = x_tile * 8 + x tile['representation'][y][x] = img.getpixel( (x_img, y_img)) % 4 tiles.append(tile) return tiles
def ora_to_character(image_file, char_name): character = stblib.character.Character(name=char_name) # Read Open Raster file image = ora.read_ora(image_file) # Find interesting elements animations_stack = None extra_sprites_stack = None illustrations_stack = None origin_layer = None palettes_layer = None for child in image['root']['childs']: if child.get('name') == 'anims': animations_stack = child ensure(animations_stack['type'] == 'stack', '"anims" is not a stack') elif child.get('name') == 'extra_sprites': extra_sprites_stack = child ensure(extra_sprites_stack['type'] == 'stack', '"extra_sprites" is not a stack') elif child.get('name') == 'illustrations': illustrations_stack = child ensure(illustrations_stack['type'] == 'stack', '"illustrations" is not a stack') elif child.get('name') == 'origin': origin_layer = child ensure(origin_layer['type'] == 'layer', '"origin" is not a layer') elif child.get('name') == 'palettes': palettes_layer = child ensure(palettes_layer['type'] == 'layer', '"palettes" is not a layer') # Convert origin info to a usable form ensure(origin_layer is not None, 'No origin layer found') origin = {'x': origin_layer['x'], 'y': origin_layer['y']} # Convert palettes info to a usable form ensure(palettes_layer is not None, 'No palettes layer found') palettes = [] for color in palettes_layer['raster'].getdata(): palettes.append(_uniq_transparent(color)) # Extract illustrations ensure(illustrations_stack is not None, 'no illustrations stack found') for illustration_layer in illustrations_stack['childs']: if illustration_layer['name'] == 'illustrations.small': ensure( illustration_layer['raster'].size[0] == 16 and illustration_layer['raster'].size[1] == 16, 'unnexpected size of {}x{} for small illustration'.format( illustration_layer['raster'].size[0], illustration_layer['raster'].size[1])) illustration_id = 'SMALL' tileset = character.illustration_small elif illustration_layer['name'] == 'illustrations.token': ensure( illustration_layer['raster'].size[0] == 8 and illustration_layer['raster'].size[1] == 8, 'unnexpected size of {}x{} for small illustration'.format( illustration_layer['raster'].size[0], illustration_layer['raster'].size[1])) illustration_id = 'TOKEN' tileset = character.illustration_token else: ensure( False, 'uknown illustration "{}"'.format(illustration_layer['name'])) tileset.tiles = [] tileset.tilenames = [] for subraster in tile_subrasters(illustration_layer): tile, _ = parse_tile(illustration_layer, subraster, palettes, illustrations_stack['name']) tileset.tiles.append(tile) tileset.tilenames.append('{}_ILLUSTRATION_{}_{}'.format( character.name.upper(), illustration_id, len(tileset.tiles))) # Place extra sprites in tileset if extra_sprites_stack is not None: for sprite_layer in extra_sprites_stack['childs']: ensure( sprite_layer['type'] == 'layer', 'extra_sprites child "{}" is not a layer'.format( sprite_layer['name'])) ensure( re.match('^[A-Z][A-Z0-9_]*$', sprite_layer['name']), 'invalid extra sprite name "{}"'.format( extra_sprites_stack['childs'])) ensure( sprite_layer['raster'].size[0] == 8 and sprite_layer['raster'].size[1] == 8, 'unexpected sprite size of {}x{} in extra_sprites: "{}"'. format(sprite_layer['raster'].size[0], sprite_layer['raster'].size[1], sprite_layer['name'])) tile, _ = parse_tile(sprite_layer, { 'x': 0, 'y': 0 }, palettes, extra_sprites_stack['name']) place_in_tileset( tile, character.tileset, '{}_TILE_{}'.format(character.name.upper(), sprite_layer['name'])) # Construct animations and tileset for animation_stack in animations_stack['childs']: # Parse animation animation = stblib.animations.Animation() m = re.match('^anims\.(?P<anim>[a-z0-9_]+)$', animation_stack['name']) ensure( m is not None, 'invalid animation stack name "{}"'.format( animation_stack['name'])) anim_name = m.group('anim') animation.name = '{}_anim_{}'.format(character.name, anim_name) for frame_stack in animation_stack['childs']: frame = stblib.animations.Frame() m = re.match( '^anims\.(?P<anim>[a-z0-9_]+)\.frame(?P<frame>[0-9]+)(\?(?P<params>.*))?$', frame_stack['name']) ensure(m is not None, 'invalid frame stack name "{}"'.format(frame_stack['name'])) ensure( m.group('anim') == anim_name, 'frame stack "{}" is named after animation "{}" while in animation "{}"' .format(frame_stack['name'], m.group('anim'), anim_name)) params = {} if m.group('params') is not None: params = extract_params(m.group('params'), ['dur'], 'frame', frame_stack['name']) frame_id = m.group('frame') frame.duration = int(params.get('dur', '4')) for frame_child in frame_stack['childs']: if frame_child['name'] == 'anims.{}.frame{}.hurtbox'.format( anim_name, frame_id): # Parse hurtbox ensure(frame_child['type'] == 'layer', '{} is note a layer'.format(frame_child['name'])) hurtbox = stblib.animations.Hurtbox( left=frame_child['x'] - origin['x'], top=frame_child['y'] - origin['y']) hurtbox.right = hurtbox.left + frame_child['raster'].size[ 0] #TODO check if we should substract 1 (so that a 16 pixels wide box at 0 ends on pixel 15) hurtbox.bottom = hurtbox.top + frame_child['raster'].size[ 1] #TODO check if we should substract 1 frame.hurtbox = hurtbox elif remove_qs(frame_child['name'] ) == 'anims.{}.frame{}.hitbox'.format( anim_name, frame_id): # Parse hitbox ensure( frame.hitbox is None, 'multiple hitboxes defined for frame anims.{}.frame{}'. format(anim_name, frame_id)) hitbox_qs = urllib.parse.urlparse( frame_child['name']).query ensure( hitbox_qs != '', 'hitbox {} has no parameters'.format( frame_child['name'])) hitbox_params = extract_params( hitbox_qs, [ 'enabled', 'damages', 'base_h', 'force_h', 'base_v', 'force_v' ], 'hitbox', remove_qs(frame_child['name'])) hitbox = stblib.animations.Hitbox( enabled=hitbox_params.get('enabled', 'false') == 'true', damages=int(hitbox_params.get('damages', '0')), base_h=int(hitbox_params.get('base_h', '0')), base_v=int(hitbox_params.get('base_v', '0')), force_h=int(hitbox_params.get('force_h', '0')), force_v=int(hitbox_params.get('force_v', '0')), left=frame_child['x'] - origin['x'], top=frame_child['y'] - origin['y']) hitbox.right = hitbox.left + frame_child['raster'].size[ 0] #TODO check if we should substract 1 hitbox.bottom = hitbox.top + frame_child['raster'].size[ 1] #TODO check if we should substract 1 frame.hitbox = hitbox elif frame_child['name'] == 'anims.{}.frame{}.sprites'.format( anim_name, frame_id): # Parse sprites ensure(frame_child['type'] == 'stack', '{} is not a stack'.format(frame_child['name'])) # Collect sprite layers sprite_layers = [] for sprites_container_child in frame_child['childs']: if sprites_container_child['type'] == 'layer': sprite_layers.append({ 'foreground': False, 'layer': sprites_container_child }) elif sprites_container_child['type'] == 'stack': ensure( sprites_container_child['name'] == 'anims.{}.frame{}.sprites.foreground'.format( anim_name, frame_id), 'unexpected stack in {}: "{}"'.format( frame_child['name'], sprites_container_child['name'])) for foreground_sprite_layer in sprites_container_child[ 'childs']: ensure( foreground_sprite_layer['type'] == 'layer', 'unexpected non-layer in {}: "{}"'.format( sprites_container_child['name'], sprite_layer['name'])) sprite_layers.append({ 'foreground': True, 'layer': foreground_sprite_layer }) else: ensure( False, 'unexpected non-stack non-layer in {}: "{}"'. format(frame_child['name'], sprites_container_child['name'])) # Parse sprite layers for sprite_layer_info in sprite_layers: sprite_layer = sprite_layer_info['layer'] ensure( sprite_layer['type'] == 'layer', 'unexpected non-layer in {}: "{}"'.format( frame_child['name'], sprite_layer['name'])) ensure( sprite_layer['raster'].size[0] % 8 == 0 and sprite_layer['raster'].size[1] % 8 == 0, 'unexpected sprite size of {}x{} in {}: "{}"'. format(sprite_layer['raster'].size[0], sprite_layer['raster'].size[1], frame_child['name'], sprite_layer['name'])) for tile_pos_in_raster in tile_subrasters( sprite_layer): # Parse tile tile, palette_num = parse_tile( sprite_layer, tile_pos_in_raster, palettes, frame_child['name']) # Add tile to tileset if needed, get its name and attributes tile_name, flip = place_in_tileset( tile, character.tileset, '{}_TILE_{}'.format( character.name.upper(), len(character.tileset.tiles))) # Add sprite to frame numeric_attr = palette_num if flip['v']: numeric_attr += 0x80 if flip['h']: numeric_attr += 0x40 sprite = stblib.animations.Sprite( y=sprite_layer['y'] + tile_pos_in_raster['y'] - origin['y'], tile=tile_name, attr=numeric_attr, x=sprite_layer['x'] + tile_pos_in_raster['x'] - origin['x'], foreground=sprite_layer_info['foreground']) frame.sprites.append(sprite) else: # Refuse unknown children in a frame, it is certainly a naming error or something not yet supported ensure( False, 'unknown frame child in {}: "{}"'.format( frame_stack['name'], frame_child['name'])) animation.frames.append(frame) # Store parsed animation in character if anim_name == 'victory': character.victory_animation = animation elif anim_name == 'defeat': character.defeat_animation = animation elif anim_name == 'menu_select': character.menu_select_animation = animation else: character.animations.append(animation) character.animations = sorted(character.animations, key=lambda x: x.name) return character
def check(self): ensure(isinstance(self.primary_names, list)) for name in self.primary_names: ensure(isinstance(name, str)) ensure(len(name) >= 1) ensure(len(name) <= 8) ensure(isinstance(self.secondary_names, list)) for name in self.secondary_names: ensure(isinstance(name, str)) ensure(len(name) >= 1) ensure(len(name) <= 8) ensure(isinstance(self.primary_colors, list)) for colors in self.primary_colors: ensure(isinstance(colors, Palette)) colors.check() ensure(isinstance(self.alternate_colors, list)) for colors in self.alternate_colors: ensure(isinstance(colors, Palette)) colors.check() ensure(isinstance(self.secondary_colors, list)) for colors in self.secondary_colors: ensure(isinstance(colors, Palette)) colors.check() ensure(len(self.primary_names) == len(self.primary_colors)) ensure(len(self.alternate_colors) == len(self.primary_colors)) ensure(len(self.secondary_names) == len(self.secondary_colors))
def check(self): ensure(len(self.name) > 0) ensure(self.start_routine is None or len(self.start_routine) > 0) ensure(len(self.update_routine) > 0) ensure(len(self.offground_routine) > 0) ensure(len(self.onground_routine) > 0) ensure(len(self.input_routine) > 0) ensure(len(self.onhurt_routine) > 0)
# Parse command line perf_filename = sys.argv[1] listing_filename = sys.argv[2] # Gather statistics per instruction cycles_per_instr = {} cycles_total = 0 re_perf = re.compile( '^(?P<stack>([0-9a-f]{4};)*)?(?P<instr>[0-9a-f]{4}) (?P<cycles>[0-9]+)$') with open(perf_filename, 'r') as perf_file: for line in perf_file: if line[-1] == '\n': line = line[:-1] m = re_perf.match(line) ensure(m is not None, "invalid line in input file: '{}'".format(line)) instr = int(m.group('instr'), 16) cycles = int(m.group('cycles')) if instr >= 0xc000: cycles_per_instr[instr] = cycles_per_instr.get(instr, 0) + cycles cycles_total += cycles # Anotate listing file def o(m): print(m) def on_file(line_num, filename): o('')
def check(self): ensure(isinstance(self.tiles, list)) for tile in self.tiles: ensure(isinstance(tile, Tile)) tile.check() ensure(isinstance(self.tilenames, list)) for tilename in self.tilenames: ensure(isinstance(tilename, str)) ensure(len(tilename) > 0) ensure(len(self.tilenames) == len(self.tiles))
def check(self): ensure(isinstance(self._representation, list)) ensure(len(self._representation) == 8) for line in self._representation: ensure(isinstance(line, list)) ensure(len(line) == 8) for pixel in line: ensure(isinstance(pixel, int)) ensure(pixel >= 0) ensure(pixel <= 3)
def ora_to_character(image_file, char_name): character = stblib.character.Character(name = char_name) # Read Open Raster file image = ora.read_ora(image_file) # Find interesting elements animations_stack = None origin_layer = None palettes_layer = None for child in image['root']['childs']: if child.get('name') == 'anims': animations_stack = child ensure(animations_stack['type'] == 'stack', '"anims" is not a stack') elif child.get('name') == 'origin': origin_layer = child ensure(origin_layer['type'] == 'layer', '"origin" is not a layer') elif child.get('name') == 'palettes': palettes_layer = child ensure(palettes_layer['type'] == 'layer', '"palettes" is not a layer') # Convert origin info to a usable form ensure(origin_layer is not None, 'No origin layer found') origin = { 'x': origin_layer['x'], 'y': origin_layer['y'] } # Convert palettes info to a usable form ensure(palettes_layer is not None, 'No palettes layer found') palettes = [] for color in palettes_layer['raster'].getdata(): palettes.append(_uniq_transparent(color)) # Construct animations and tileset for animation_stack in animations_stack['childs']: # Parse animation animation = stblib.animations.Animation() m = re.match('^anims\.(?P<anim>[a-z0-9_]+)$', animation_stack['name']) ensure(m is not None, 'invalid animation stack name "{}"'.format(animation_stack['name'])) anim_name = m.group('anim') animation.name = '{}_anim_{}'.format(character.name, anim_name) for frame_stack in animation_stack['childs']: frame = stblib.animations.Frame() m = re.match('^anims\.(?P<anim>[a-z0-9_]+)\.frame(?P<frame>[0-9]+)(\?(?P<params>.*))?$', frame_stack['name']) ensure(m is not None, 'invalid frame stack name "{}"'.format(frame_stack['name'])) ensure(m.group('anim') == anim_name, 'frame stack "{}" is named after animation "{}" while in animation "{}"'.format(frame_stack['name'], m.group('anim'), anim_name)) params = {} if m.group('params') is not None: params = extract_params(m.group('params'), ['dur'], 'frame', frame_stack['name']) frame_id = m.group('frame') frame.duration = int(params.get('dur', '4')) for frame_child in frame_stack['childs']: if frame_child['name'] == 'anims.{}.frame{}.hurtbox'.format(anim_name, frame_id): # Parse hurtbox ensure(frame_child['type'] == 'layer', '{} is note a layer'.format(frame_child['name'])) hurtbox = stblib.animations.Hurtbox( left = frame_child['x'] - origin['x'], top = frame_child['y'] - origin['y'] ) hurtbox.right = hurtbox.left + frame_child['raster'].size[0] hurtbox.bottom = hurtbox.top + frame_child['raster'].size[1] frame.hurtbox = hurtbox elif remove_qs(frame_child['name']) == 'anims.{}.frame{}.hitbox'.format(anim_name, frame_id): # Parse hitbox ensure(frame.hitbox is None, 'multiple hitboxes defined for frame anims.{}.frame{}'.format(anim_name, frame_id)) hitbox_qs = urllib.parse.urlparse(frame_child['name']).query ensure(hitbox_qs != '', 'hitbox {} has no parameters'.format(frame_child['name'])) hitbox_params = extract_params( hitbox_qs, ['enabled', 'damages', 'base_h', 'force_h', 'base_v', 'force_v'], 'hitbox', remove_qs(frame_child['name']) ) hitbox = stblib.animations.Hitbox( enabled = hitbox_params.get('enabled', 'false') == 'true', damages = int(hitbox_params.get('damages', '0')), base_h = int(hitbox_params.get('base_h', '0')), base_v = int(hitbox_params.get('base_v', '0')), force_h = int(hitbox_params.get('force_h', '0')), force_v = int(hitbox_params.get('force_v', '0')), left = frame_child['x'] - origin['x'], top = frame_child['y'] - origin['y'] ) hitbox.right = hitbox.left + frame_child['raster'].size[0] hitbox.bottom = hitbox.top + frame_child['raster'].size[1] frame.hitbox = hitbox elif frame_child['name'] == 'anims.{}.frame{}.sprites'.format(anim_name, frame_id): # Parse sprites ensure(frame_child['type'] == 'stack', '{} is not a stack'.format(frame_child['name'])) # Collect sprite layers sprite_layers = [] for sprites_container_child in frame_child['childs']: if sprites_container_child['type'] == 'layer': sprite_layers.append({'foreground': False, 'layer': sprites_container_child}) elif sprites_container_child['type'] == 'stack': ensure(sprites_container_child['name'] == 'anims.{}.frame{}.sprites.foreground'.format(anim_name, frame_id), 'unexpected stack in {}: "{}"'.format(frame_child['name'], sprite_layer['name'])) for foreground_sprite_layer in sprites_container_child['childs']: ensure(foreground_sprite_layer['type'] == 'layer', 'unexpected non-layer in {}: "{}"'.format(sprites_container_child['name'], sprite_layer['name'])) sprite_layers.append({'foreground': True, 'layer': foreground_sprite_layer}) else: ensure(False, 'unexpected non-stack non-layer in {}: "{}"'.format(frame_child['name'], sprites_container_child['name'])) # Parse sprite layers for sprite_layer_info in sprite_layers: sprite_layer = sprite_layer_info['layer'] ensure(sprite_layer['type'] == 'layer', 'unexpected non-layer in {}: "{}"'.format(frame_child['name'], sprite_layer['name'])) ensure(sprite_layer['raster'].size[0] % 8 == 0 and sprite_layer['raster'].size[1] % 8 == 0, 'unexpected sprite size of {}x{} in {}: "{}"'.format( sprite_layer['raster'].size[0], sprite_layer['raster'].size[1], frame_child['name'], sprite_layer['name'] )) raster_horizontal_tiles_count = sprite_layer['raster'].size[0] // 8 raster_vertical_tiles_count = sprite_layer['raster'].size[1] // 8 for tile_y_num_in_raster in range(raster_vertical_tiles_count): tile_y_pos_in_raster = tile_y_num_in_raster * 8 for tile_x_num_in_raster in range(raster_horizontal_tiles_count): tile_x_pos_in_raster = tile_x_num_in_raster * 8 # Parse tile tile = stblib.tiles.Tile() palette_num = 0 for y in range(8): y_in_raster = tile_y_pos_in_raster + y for x in range(8): x_in_raster = tile_x_pos_in_raster + x # Convert pixel data to palette index try: color_index = palettes.index(_uniq_transparent(sprite_layer['raster'].getpixel((x_in_raster, y_in_raster)))) except ValueError: ensure(False, 'a sprite in {} use a color not found in palettes: sprite "{}", position "{}x{}", color "{}", palettes: {}'.format(frame_child['name'], sprite_layer['name'], x_in_raster, y_in_raster, sprite_layer['raster'].getpixel((x_in_raster, y_in_raster)), palettes)) # Store pixel in tile tile._representation[y][x] = color_index % 4 if color_index != 0: palette_num = int(color_index / 4) # Add tile to tileset if needed, get its name and attributes tile_name = None flip = None for try_flip in [{'v': False, 'h': False}, {'v': False, 'h': True}, {'v': True, 'h': False}, {'v': True, 'h': True}]: flipped = stblib.tiles.Tile(representation = copy.deepcopy(tile._representation)) if try_flip['v']: flipped.flip_v() if try_flip['h']: flipped.flip_h() try: tile_index = character.tileset.tiles.index(flipped) tile_name = character.tileset.tilenames[tile_index] flip = try_flip break except ValueError: pass if tile_name is None: tile_name = '{}_TILE_{}'.format(character.name.upper(), len(character.tileset.tiles)) flip = {'v': False, 'h': False} character.tileset.tiles.append(tile) character.tileset.tilenames.append(tile_name) # Add sprite to frame numeric_attr = palette_num if flip['v']: numeric_attr += 0x80 if flip['h']: numeric_attr += 0x40 sprite = stblib.animations.Sprite( y = sprite_layer['y'] + tile_y_pos_in_raster - origin['y'], tile = tile_name, attr = numeric_attr, x = sprite_layer['x'] + tile_x_pos_in_raster - origin['x'], foreground = sprite_layer_info['foreground'] ) frame.sprites.append(sprite) else: # Refuse unknown children in a frame, it is certainly a naming error or something not yet supported ensure(False, 'unknown frame child in {}: "{}"'.format(frame_stack['name'], frame_child['name'])) animation.frames.append(frame) # Store parsed animation in character if anim_name == 'victory': character.victory_animation = animation elif anim_name == 'defeat': character.defeat_animation = animation elif anim_name == 'menu_select': character.menu_select_animation = animation else: character.animations.append(animation) character.animations = sorted(character.animations, key = lambda x: x.name) return character
def check(self): ensure(isinstance(self.colors, list)) ensure(len(self.colors) == 3) for color in self.colors: ensure(color >= 0) ensure(color <= 255)
def ensure_unique_animation_name(anim): ensure(anim.name not in animation_names, 'multiple animations are named "{}"'.format(anim.name))
def _jsonify_character(character, base_path): character_path_rel = 'characters/{}'.format(character['name']) animations_path_rel = '{}/animations'.format(character_path_rel) illustrations_path_rel = '{}/illustrations'.format(character_path_rel) character_path = '{}/{}'.format(base_path, character_path_rel) animations_path = '{}/{}'.format(base_path, animations_path_rel) illustrations_path = '{}/{}'.format(base_path, illustrations_path_rel) os.makedirs(character_path, exist_ok=True) os.makedirs(animations_path, exist_ok=True) # Convert tilesets to gif based tileset tileset_src = '{}/tileset.gif'.format(character_path) tileset_img = tileset_to_img(character['tileset']['tiles'], 8, 12) tileset_img.save(tileset_src) del character['tileset']['tiles'] character['tileset']['src'] = '{}/tileset.gif'.format(character_path_rel) illustration_small_src = '{}/small.gif'.format(illustrations_path) illustration_small_img = tileset_to_img(character['illustration_small']['tiles'], 2, 2) illustration_small_img.save(illustration_small_src) del character['illustration_small']['tiles'] character['illustration_small']['src'] = '{}/small.gif'.format(illustrations_path_rel) illustration_token_src = '{}/token.gif'.format(illustrations_path) illustration_token_img = tileset_to_img(character['illustration_token']['tiles'], 1, 1) illustration_token_img.save(illustration_token_src) del character['illustration_token']['tiles'] character['illustration_token']['src'] = '{}/token.gif'.format(illustrations_path_rel) # Export animations in their own file def _externalize_anim(anim, animations_path_rel, base_path): ensure(anim.get('name', '') != '', 'empty animation name') anim_path_rel = '{}/{}.json'.format(animations_path_rel, anim['name']) anim_path = '{}/{}'.format(base_path, anim_path_rel) with open(anim_path, 'w') as anim_file: dict_to_json(anim, anim_file, base_path) return { 'type': 'import', 'src': anim_path_rel } for anim_index in range(len(character['animations'])): character['animations'][anim_index] = _externalize_anim(character['animations'][anim_index], animations_path_rel, base_path) character['victory_animation'] = _externalize_anim(character['victory_animation'], animations_path_rel, base_path) character['defeat_animation'] = _externalize_anim(character['defeat_animation'], animations_path_rel, base_path) character['menu_select_animation'] = _externalize_anim(character['menu_select_animation'], animations_path_rel, base_path) # Export sourcecode in its own file source_path_rel = '{}/states.asm'.format(character_path_rel) source_path = '{}/{}'.format(base_path, source_path_rel) with open(source_path, 'w') as source_file: source_file.write(character['sourcecode']) del character['sourcecode'] character['sourcecode_file'] = source_path_rel # Export AI's sourcecode in its own file ensure(isinstance(character.get('ai'), dict), 'no AI defined for character {}'.format(character['name'])) if character['ai'].get('sourcecode', '') == '': character['ai']['sourcecode_file'] = None else: source_path_rel = '{}/ai.asm'.format(character_path_rel) source_path = '{}/{}'.format(base_path, source_path_rel) with open(source_path, 'w') as source_file: source_file.write(character['ai']['sourcecode']) del character['ai']['sourcecode'] character['ai']['sourcecode_file'] = source_path_rel return character
def check(self): ensure(isinstance(self.name, str)) ensure(len(self.name) >= 1) ensure(len(self.name) <= 10) ensure(isinstance(self.weapon_name, str)) ensure(len(self.weapon_name) >= 1) ensure(len(self.weapon_name) <= 10) ensure(isinstance(self.sourcecode, str)) ensure(isinstance(self.victory_animation, stblib.animations.Animation)) for frame in self.victory_animation.frames: ensure(frame.hitbox is None) ensure(frame.hurtbox is None) ensure(isinstance(self.defeat_animation, stblib.animations.Animation)) for frame in self.defeat_animation.frames: ensure(frame.hitbox is None) ensure(frame.hurtbox is None) ensure(isinstance(self.menu_select_animation, stblib.animations.Animation)) for frame in self.menu_select_animation.frames: ensure(frame.hitbox is None) ensure(frame.hurtbox is None) ensure(isinstance(self.animations, list)) for animation in self.animations: ensure(isinstance(animation, stblib.animations.Animation)) ensure(isinstance(self.color_swaps, Colorswaps)) self.color_swaps.check() ensure(isinstance(self.states, list)) for state in self.states: ensure(isinstance(state, State)) state.check()
def check(self): ensure(-128 <= self.left and self.left <= 127) ensure(-128 <= self.right and self.right <= 127) ensure(-128 <= self.top and self.top <= 127) ensure(-128 <= self.bottom and self.bottom <= 127)
def generate_character(char, game_dir): name_upper = char.name.upper() # Create destination directories rel_char_dir = 'game/data/characters/{}'.format(char.name) rel_anim_dir = '{}/animations'.format(rel_char_dir) char_dir = '{}/{}'.format(game_dir, rel_char_dir) anim_dir = '{}/{}'.format(game_dir, rel_anim_dir) os.makedirs(char_dir) os.makedirs(anim_dir) # Label names that are used in multiple places tileset_label_name = '{}_chr_tiles'.format(char.name) illustrations_label_name = '{}_chr_illustrations'.format(char.name) primary_palettes_label_name = '{}_character_palettes'.format(char.name) alternate_palettes_label_name = '{}_character_alternate_palettes'.format(char.name) weapon_palettes_label_name = '{}_weapon_palettes'.format(char.name) character_names_label_name = '{}_character_names'.format(char.name) weapon_names_label_name = '{}_weapon_names'.format(char.name) properties_table_label_name = '{}_properties'.format(char.name) ai_attacks_table_label_name = '{}_ai_attacks'.format(char.name) ai_selectors_table_label_name = '{}_ai_selectors'.format(char.name) # Create character's master file master_file_path = '{}/{}.asm'.format(char_dir, char.name) with open(master_file_path, 'w') as master_file: master_file.write(textwrap.dedent("""\ {name_upper}_BANK_NUMBER = CURRENT_BANK_NUMBER #include "{rel_char_dir}/chr_tiles.asm" #include "{rel_char_dir}/chr_illustrations.asm" #include "{rel_char_dir}/animations/animations.asm" #include "{rel_char_dir}/character_colors.asm" #include "{rel_char_dir}/properties.asm" #include "{rel_char_dir}/state_events.asm" #include "{rel_char_dir}/player_states.asm" #include "{rel_char_dir}/ai_data.asm" #include "{rel_char_dir}/ai.asm" """.format_map(locals()))) # Tileset file chr_tiles_file_path = '{}/chr_tiles.asm'.format(char_dir) with open(chr_tiles_file_path, 'w') as chr_tiles_file: # Tileset label chr_tiles_file.write('{}:\n\n'.format(tileset_label_name)) # Tiles in binary form, each with a label containing its index index_expression = '(*-{})/16'.format(tileset_label_name) for tile_index in range(len(char.tileset.tilenames)): tile = char.tileset.tiles[tile_index] tile_name = char.tileset.tilenames[tile_index] # Label containing tile's index chr_tiles_file.write('{} = {}\n'.format(tile_name, index_expression)) # Tile data chr_tiles_file.write('{}\n\n'.format(stblib.asmformat.tiles.tile_to_asm(tile))) # Tileset footer chr_tiles_file.write(textwrap.dedent("""\ {name_upper}_SPRITE_TILES_NUMBER = {index_expression} #print {name_upper}_SPRITE_TILES_NUMBER #if {name_upper}_SPRITE_TILES_NUMBER > 96 #error too many sprites for character {name_upper} #endif """.format_map(locals()))) # Illustrations file chr_illustrations_file_path = '{}/chr_illustrations.asm'.format(char_dir) with open(chr_illustrations_file_path, 'w') as chr_illustrations_file: # Illustrations label chr_illustrations_file.write('{}:\n\n'.format(illustrations_label_name)) index_expression = '(*-{})/16'.format(illustrations_label_name) # Token illustration chr_illustrations_file.write(';\n; Token\n;\n\n') ensure(len(char.illustration_token.tilenames) == 1) ensure(len(char.illustration_token.tiles) == len(char.illustration_token.tilenames)) chr_illustrations_file.write('{}\n\n'.format(stblib.asmformat.tiles.tile_to_asm(char.illustration_token.tiles[0]))) # Small illustration chr_illustrations_file.write(';\n; Small\n;\n\n') ensure(len(char.illustration_small.tilenames) == 4) ensure(len(char.illustration_small.tiles) == len(char.illustration_small.tilenames)) for tile_index in range(len(char.illustration_small.tilenames)): tile = char.illustration_small.tiles[tile_index] chr_illustrations_file.write('{}\n\n'.format(stblib.asmformat.tiles.tile_to_asm(tile))) # Large illustration chr_illustrations_file.write(';\n; Large\n;\n\n') ensure(len(char.illustration_large.tilenames) == 48) ensure(len(char.illustration_large.tiles) == len(char.illustration_large.tilenames)) for tile_index in range(len(char.illustration_large.tilenames)): tile = char.illustration_large.tiles[tile_index] chr_illustrations_file.write('{}\n\n'.format(stblib.asmformat.tiles.tile_to_asm(tile))) # Illustration footer chr_illustrations_file.write(textwrap.dedent("""\ {name_upper}_ILLUSTRATION_TILES_NUMBER = {index_expression} #print {name_upper}_ILLUSTRATION_TILES_NUMBER #if {name_upper}_ILLUSTRATION_TILES_NUMBER <> 53 #error bad count of illustration tiles for character {name_upper} #endif """.format_map(locals()))) # Palettes file character_colors_file_path = '{}/character_colors.asm'.format(char_dir) with open(character_colors_file_path, 'w') as character_colors_file: def write_palettes_table(palettes, label_name, description): def _c(i): return stblib.utils.intasm8(palette.colors[i]) character_colors_file.write('; {}\n'.format(description)) character_colors_file.write('{}:\n'.format(label_name)) for palette in palettes: character_colors_file.write('.byt {}, {}, {}\n'.format(_c(0), _c(1), _c(2))) character_colors_file.write('\n') def write_palette_names_table(names, label_name, description): character_colors_file.write('; {}\n'.format(description)) character_colors_file.write('{}:\n'.format(label_name)) for name in names: character_colors_file.write('.byt {} ; {}\n'.format(text_asm(name, 8, 2), name)) character_colors_file.write('\n') # Primary palettes write_palettes_table( char.color_swaps.primary_colors, primary_palettes_label_name, 'Main palette for character' ) # Alternate palettes write_palettes_table( char.color_swaps.alternate_colors, alternate_palettes_label_name, 'Alternate palette to use to reflect special state' ) # Primary palettes names write_palette_names_table( char.color_swaps.primary_names, character_names_label_name, 'Character palette name' ) # Secondary palettes write_palettes_table( char.color_swaps.secondary_colors, weapon_palettes_label_name, 'Secondary palette for character' ) # Secondary palettes names write_palette_names_table( char.color_swaps.secondary_names, weapon_names_label_name, 'Weapon palette name' ) # Character properties properties_file_path = '{}/properties.asm'.format(char_dir) with open(properties_file_path, 'w') as properties_file: # Propeties table's label properties_file.write('{}:\n'.format(properties_table_label_name)) # Standard animations properties_file.write('VECTOR({})\n'.format(char.victory_animation.name)) properties_file.write('VECTOR({})\n'.format(char.defeat_animation.name)) properties_file.write('VECTOR({})\n'.format(char.menu_select_animation.name)) # Character name properties_file.write('.byt {} ; {}\n'.format(text_asm(char.name, 10, 2), char.name)) properties_file.write('.byt {} ; {}\n'.format(text_asm(char.weapon_name, 10, 2), char.weapon_name)) # Illustrations properties_file.write('VECTOR({}) ; Illustrations begining\n'.format(illustrations_label_name)) # AI properties_file.write('VECTOR({}) ; AI selectors\n'.format(ai_selectors_table_label_name)) properties_file.write('.byt {} ; Number of AI attacks\n'.format(len(char.ai.attacks))) properties_file.write('VECTOR({}) ; AI attacks\n'.format(ai_attacks_table_label_name)) # State events state_events_file_path = '{}/state_events.asm'.format(char_dir) with open(state_events_file_path, 'w') as state_events_file: # State count state_events_file.write('{}_NUM_STATES = {}\n\n'.format(name_upper, len(char.states))) # Routines tables for routine_type in ['start', 'update', 'offground', 'onground', 'input', 'onhurt']: state_events_file.write('{}_state_{}_routines:\n'.format(char.name, routine_type)) for state in char.states: routine_name = getattr(state, '{}_routine'.format(routine_type)) ensure(routine_name is not None or routine_type == 'start', 'in {}\'s state {}, missing {} routine'.format(char.name, state.name, routine_type)) if routine_name is not None: state_events_file.write('STATE_ROUTINE({}) ; {}\n'.format(routine_name, state.name)) state_events_file.write('\n') # Character's logic player_states_file_path = '{}/player_states.asm'.format(char_dir) with open(player_states_file_path, 'w') as player_states_file: player_states_file.write(char.sourcecode) # Animations rel_animations_path = [] def write_animation_file(anim): rel_anim_file_path = '{}/{}.asm'.format(rel_anim_dir, anim.name) anim_file_path = '{}/{}'.format(game_dir, rel_anim_file_path) with open(anim_file_path, 'w') as anim_file: anim_file.write(stblib.asmformat.animations.animation_to_asm(anim)) rel_animations_path.append(rel_anim_file_path) write_animation_file(char.victory_animation) write_animation_file(char.defeat_animation) write_animation_file(char.menu_select_animation) for anim in char.animations: write_animation_file(anim) master_animations_file_path = '{}/animations.asm'.format(anim_dir) with open(master_animations_file_path, 'w') as master_animations_file: for rel_anim_file_path in rel_animations_path: master_animations_file.write('#include "{}"\n'.format(rel_anim_file_path)) # AI custom_ai_file_path = '{}/ai.asm'.format(char_dir) with open(custom_ai_file_path, 'w') as custom_ai_file: custom_ai_file.write(char.ai.sourcecode) ai_data_file_path = '{}/ai_data.asm'.format(char_dir) with open(ai_data_file_path, 'w') as ai_data_file: # Attacks ai_data_file.write('{}:\n'.format(ai_attacks_table_label_name)) ai_data_file.write('; LSBs\n') for attack in char.ai.attacks: ai_data_file.write('AI_ATTACK_HITBOX({}, ${:02x}, ${:02x}, ${:02x}, ${:02x})\n'.format( stblib.utils.uint16lsb(attack.constraints), stblib.utils.int16lsb(attack.hitbox.left), stblib.utils.int16lsb(attack.hitbox.right), stblib.utils.int16lsb(attack.hitbox.top), stblib.utils.int16lsb(attack.hitbox.bottom) )) ai_data_file.write('.byt <{}\n'.format(attack.action)) ai_data_file.write('; MSBs\n') for attack in char.ai.attacks: ai_data_file.write('AI_ATTACK_HITBOX({}, ${:02x}, ${:02x}, ${:02x}, ${:02x})\n'.format( stblib.utils.uint16msb(attack.constraints), stblib.utils.int16msb(attack.hitbox.left), stblib.utils.int16msb(attack.hitbox.right), stblib.utils.int16msb(attack.hitbox.top), stblib.utils.int16msb(attack.hitbox.bottom) )) ai_data_file.write('.byt >{}\n'.format(attack.action)) ai_data_file.write('\n') # Selectors ai_data_file.write('{}:\n'.format(ai_selectors_table_label_name)) for selector in char.ai.action_selectors: ai_data_file.write('VECTOR({})\n'.format(selector)) ai_data_file.write('\n') # Actions for action in char.ai.actions: ai_data_file.write('{}_ai_action_{}:\n'.format(char.name, action.name)) for step in action.steps: ai_data_file.write('AI_ACTION_STEP({}, {})\n'.format(step.input, step.duration)) ai_data_file.write('AI_ACTION_END_STEPS\n')