def __init__(self, graphic_set_number): if not self.GRAPHIC_SET_BG_PAGE_1: self.GRAPHIC_SET_BG_PAGE_1 = ROM().bulk_read( BG_PAGE_COUNT, Level_BG_Pages1) self.GRAPHIC_SET_BG_PAGE_2 = ROM().bulk_read( BG_PAGE_COUNT, Level_BG_Pages2) self.data = bytearray() self.number = graphic_set_number segments = [] if graphic_set_number == WORLD_MAP: segments = [0x14, 0x16, 0x20, 0x21, 0x22, 0x23] if graphic_set_number not in range(BG_PAGE_COUNT): self._read_in([graphic_set_number, graphic_set_number + 2]) else: gfx_index = self.GRAPHIC_SET_BG_PAGE_1[graphic_set_number] common_index = self.GRAPHIC_SET_BG_PAGE_2[graphic_set_number] segments.append(gfx_index) segments.append(common_index) if graphic_set_number == SPADE_ROULETTE: segments.extend([0x20, 0x21, 0x22, 0x23]) elif graphic_set_number == N_SPADE: segments.extend([0x28, 0x29, 0x5A, 0x31]) elif graphic_set_number == VS_2P: segments.extend([0x04, 0x05, 0x06, 0x07]) else: segments.extend([0x00, 0x00, 0x00, 0x00]) self._read_in(segments)
def _save_current_changes_to_file(self, pathname: str, set_new_path): for offset, data in self.level_ref.to_bytes(): ROM().bulk_write(data, offset) try: ROM().save_to_file(pathname, set_new_path) except IOError as exp: QMessageBox.warning(self, f"{type(exp).__name__}", f"Cannot save ROM data to file '{pathname}'.")
def __init__(self, level_name: str, layout_address: int, enemy_data_offset: int, object_set_number: int): super(Level, self).__init__(object_set_number, layout_address) self._signal_emitter = LevelSignaller() self.attached_to_rom = True self.object_set = ObjectSet(object_set_number) self.undo_stack = UndoStack() self.name = level_name self.header_offset = layout_address self.enemy_offset = enemy_data_offset self.objects: List[LevelObject] = [] self.jumps: List[Jump] = [] self.enemies: List[EnemyObject] = [] rom = ROM() self.header_bytes = rom.bulk_read(Level.HEADER_LENGTH, self.header_offset) self._parse_header() self.object_offset = self.header_offset + Level.HEADER_LENGTH object_data = ROM.rom_data[self.object_offset:] enemy_data = ROM.rom_data[self.enemy_offset:] self._load_level_data(object_data, enemy_data) self.changed = False
def load_palette_group(object_set: int, palette_group_index: int) -> PaletteGroup: """ Basically does, what the Setup_PalData routine does. :param object_set: Level_Tileset in the disassembly. :param palette_group_index: Palette_By_Tileset. Defined in the level header. :return: A list of 4 groups of 4 colors. """ rom = ROM() palette_offset_position = PALETTE_OFFSET_LIST + (object_set * PALETTE_OFFSET_SIZE) palette_offset = rom.little_endian(palette_offset_position) palette_address = PALETTE_BASE_ADDRESS + palette_offset palette_address += palette_group_index * PALETTES_PER_PALETTES_GROUP * COLORS_PER_PALETTE palettes = [] for _ in range(PALETTES_PER_PALETTES_GROUP): palettes.append(rom.read(palette_address, COLORS_PER_PALETTE)) palette_address += COLORS_PER_PALETTE return palettes
def gen_object_factories(): ROM(root_dir.joinpath("SMB3.nes")) for object_set in range(MAX_OBJECT_SET + 1): if object_set in [WORLD_MAP_OBJECT_SET, MUSHROOM_OBJECT_SET, SPADE_BONUS_OBJECT_SET]: continue yield LevelObjectFactory(object_set, object_set, 0, [], False)
def get_block(block_index, palette_group, graphics_set, tsa_data): if block_index > 0xFF: rom_block_index = ROM().get_byte(block_index) # block_index is an offset into the graphic memory block = Block(rom_block_index, palette_group, graphics_set, tsa_data) else: block = Block(block_index, palette_group, graphics_set, tsa_data) return block
def _draw_floor(self, painter: QPainter, level: Level): floor_level = (GROUND - 1) * self.block_length floor_block_index = 86 palette_group = load_palette(level.object_set_number, level.header.object_palette_index) pattern_table = PatternTable(level.header.graphic_set_index) tsa_data = ROM().get_tsa_data(level.object_set_number) floor_block = Block(floor_block_index, palette_group, pattern_table, tsa_data) for x in range(level.width): floor_block.draw(painter, x * self.block_length, floor_level, self.block_length)
def _block_from_index(block_index: int, level: Level) -> Block: """ Returns the block at the given index, from the TSA table for the given level. :param block_index: :param level: :return: """ palette_group = load_palette_group(level.object_set_number, level.header.object_palette_index) graphics_set = GraphicsSet(level.header.graphic_set_index) tsa_data = ROM().get_tsa_data(level.object_set_number) return Block(block_index, palette_group, graphics_set, tsa_data)
def __init__(self, world: int, level: int, layout_address: int, enemy_data_offset: int, object_set_number: int): super(Level, self).__init__(object_set_number, layout_address) self._signal_emitter = LevelSignaller() self.attached_to_rom = True self.object_set = ObjectSet(object_set_number) self.undo_stack = UndoStack() level_index = Level.world_indexes[world] + level level_data: Mario3Level = Level.offsets[level_index] if world == 0: self.name = level_data.name else: self.name = f"Level {world}-{level}, '{level_data.name}'" # TODO get rid of this; only used for naming and M3L header self.world = world self.level_number = level self.header_offset = layout_address self.enemy_offset = enemy_data_offset self.objects: List[LevelObject] = [] self.jumps: List[Jump] = [] self.enemies: List[EnemyObject] = [] rom = ROM() self.header_bytes = rom.bulk_read(Level.HEADER_LENGTH, self.header_offset) self._parse_header() self.object_offset = self.header_offset + Level.HEADER_LENGTH object_data = ROM.rom_data[self.object_offset:] enemy_data = ROM.rom_data[self.enemy_offset:] self._load_level_data(object_data, enemy_data) self.changed = False
def __init__(self, auto_scroll_row: int, level: Level): self.auto_scroll_row = auto_scroll_row self.level = level self.current_pos = QPointF() self.horizontal_speed = 0 self.vertical_speed = 0 self.rom = ROM() self.pixel_length = 1 self.acceleration_pen = Qt.NoPen self.acceleration_brush = Qt.NoBrush self.scroll_pen = Qt.NoPen self.scroll_brush = Qt.NoBrush self.screen_polygon = QPolygonF()
def on_play(self): """ Copies the ROM, including the current level, to a temporary directory, saves the current level as level 1-1 and opens the rom in an emulator. """ temp_dir = pathlib.Path(tempfile.gettempdir()) / "smb3foundry" temp_dir.mkdir(parents=True, exist_ok=True) path_to_temp_rom = temp_dir / "instaplay.rom" ROM().save_to(path_to_temp_rom) if not self._put_current_level_to_level_1_1(path_to_temp_rom): return if not self._set_default_powerup(path_to_temp_rom): return arguments = SETTINGS["instaplay_arguments"].replace( "%f", str(path_to_temp_rom)) arguments = shlex.split(arguments, posix=False) emu_path = pathlib.Path(SETTINGS["instaplay_emulator"]) if emu_path.is_absolute(): if emu_path.exists(): emulator = str(emu_path) else: QMessageBox.critical( self, "Emulator not found", f"Check it under File > Settings.\nFile {emu_path} not found." ) return else: emulator = SETTINGS["instaplay_emulator"] try: subprocess.run([emulator, *arguments]) except Exception as e: QMessageBox.critical(self, "Emulator command failed.", f"Check it under File > Settings.\n{str(e)}")
def __init__(self, world_index): self._internal_world_map = _WorldMap.from_world_number(ROM(), world_index) super(WorldMap, self).__init__(0, self._internal_world_map.layout_address) self.name = f"World {world_index} - Overworld" self.pattern_table = PatternTable(OVERWORLD_GRAPHIC_SET) self.palette_group = load_palette(WORLD_MAP_OBJECT_SET, 0) self.object_set = WORLD_MAP_OBJECT_SET self.tsa_data = ROM.get_tsa_data(self.object_set) self.world = 0 self.level_number = 0 self.objects = [] self._load_objects() self._calc_size()
def _draw_block(self, painter: QPainter, block_index, x, y, block_length, transparent): if block_index not in self.block_cache: if block_index > 0xFF: rom_block_index = ROM().get_byte( block_index ) # block_index is an offset into the graphic memory block = Block(rom_block_index, self.palette_group, self.pattern_table, self.tsa_data) else: block = Block(block_index, self.palette_group, self.pattern_table, self.tsa_data) self.block_cache[block_index] = block self.block_cache[block_index].draw( painter, x * block_length, y * block_length, block_length=block_length, selected=self.selected, transparent=transparent, )
def _save_auto_rom(): ROM().save_to_file(auto_save_rom_path, set_new_path=False)
def rom(): rom_path = root_dir.joinpath("SMB3.nes") rom = ROM(rom_path) yield rom
def save_rom(self, is_save_as): safe_to_save, reason, additional_info = self.level_view.level_safe_to_save( ) if not safe_to_save: answer = QMessageBox.warning( self, reason, f"{additional_info}\n\nDo you want to proceed?", QMessageBox.No | QMessageBox.Yes, QMessageBox.No, ) if answer == QMessageBox.No: return if not self.level_ref.attached_to_rom: QMessageBox.information( self, "Importing M3L into ROM", "Please select the positions in the ROM you want the level objects and enemies/items to be stored.", QMessageBox.Ok, ) level_selector = LevelSelector(self) answer = level_selector.exec_() if answer == QMessageBox.Accepted: self.level_view.level_ref.attach_to_rom( level_selector.object_data_offset, level_selector.enemy_data_offset) if is_save_as: # if we save to another rom, don't consider the level # attached (to the current rom) self.level_view.level_ref.attached_to_rom = False else: return if is_save_as: pathname, _ = QFileDialog.getSaveFileName(self, caption="Save ROM as", filter=ROM_FILE_FILTER) if not pathname: return # the user changed their mind else: pathname = ROM.path level = self.level_ref.level for offset, data in level.to_bytes(): ROM().bulk_write(data, offset) try: ROM().save_to_file(pathname) except IOError as exp: QMessageBox.warning(self, f"{type(exp).__name__}", f"Cannot save ROM data to file '{pathname}'.") self.update_title() if not is_save_as: level.changed = False
def _render(self): self.rendered_base_x = base_x = self.x_position self.rendered_base_y = base_y = self.y_position self.rendered_width = new_width = self.width self.rendered_height = new_height = self.height try: self.index_in_level = self.objects_ref.index(self) except ValueError: # the object has not been added yet, so stick with the one given in the constructor pass blocks_to_draw = [] if self.orientation == TO_THE_SKY: base_x = self.x_position base_y = SKY for _ in range(self.y_position): blocks_to_draw.extend(self.blocks[0:self.width]) blocks_to_draw.extend(self.blocks[-self.width:]) elif self.orientation == DESERT_PIPE_BOX: # segments are the horizontal sections, which are 8 blocks long # two of those are drawn per length bit # rows are the 4 block high rows Mario can walk in is_pipe_box_type_b = self.obj_index // 0x10 == 4 rows_per_box = self.height lines_per_row = 4 segment_width = self.width segments = (self.length + 1) * 2 box_height = lines_per_row * rows_per_box new_width = segments * segment_width new_height = box_height for row_number in range(rows_per_box): for line in range(lines_per_row): if is_pipe_box_type_b and row_number > 0 and line == 0: # in pipebox type b we do not repeat the horizontal beams line += 1 start = line * segment_width stop = start + segment_width for segment_number in range(segments): blocks_to_draw.extend(self.blocks[start:stop]) if is_pipe_box_type_b: # draw another open row start = segment_width else: # draw the first row again to close the box start = 0 stop = start + segment_width for segment_number in range(segments): blocks_to_draw.extend(self.blocks[start:stop]) elif self.orientation in [ DIAG_DOWN_LEFT, DIAG_DOWN_RIGHT, DIAG_UP_RIGHT, DIAG_WEIRD ]: if self.ending == UNIFORM: new_height = (self.length + 1) * self.height new_width = (self.length + 1) * self.width left = [BLANK] right = [BLANK] slopes = self.blocks elif self.ending == END_ON_TOP_OR_LEFT: new_height = (self.length + 1) * self.height new_width = (self.length + 1) * (self.width - 1 ) # without fill block if self.orientation in [DIAG_DOWN_RIGHT, DIAG_UP_RIGHT]: fill_block = self.blocks[0:1] slopes = self.blocks[1:] left = fill_block right = [BLANK] elif self.orientation == DIAG_DOWN_LEFT: fill_block = self.blocks[-1:] slopes = self.blocks[0:-1] right = fill_block left = [BLANK] else: fill_block = self.blocks[0:1] slopes = self.blocks[1:] right = [BLANK] left = fill_block elif self.ending == END_ON_BOTTOM_OR_RIGHT: new_height = (self.length + 1) * self.height new_width = (self.length + 1) * (self.width - 1 ) # without fill block fill_block = self.blocks[-1:] slopes = self.blocks[0:-1] left = [BLANK] right = fill_block else: # todo other two ends not used with diagonals? print(self.description) self.rendered_blocks = [] return rows = [] if self.height > self.width: slope_width = self.width else: slope_width = len(slopes) for y in range(new_height): amount_right = (y // self.height) * slope_width amount_left = new_width - slope_width - amount_right offset = y % self.height rows.append(amount_left * left + slopes[offset:offset + slope_width] + amount_right * right) if self.orientation in [DIAG_UP_RIGHT]: for row in rows: row.reverse() if self.orientation in [DIAG_DOWN_RIGHT, DIAG_UP_RIGHT]: if not self.height > self.width: # special case for 60 degree platform wire down right rows.reverse() if self.orientation in [DIAG_UP_RIGHT]: base_y -= new_height - 1 if self.orientation in [DIAG_DOWN_LEFT]: base_x -= new_width - slope_width for row in rows: blocks_to_draw.extend(row) elif self.orientation in [PYRAMID_TO_GROUND, PYRAMID_2]: # since pyramids grow horizontally in both directions when extending # we need to check for new ground every time it grows base_x += 1 # set the new base_x to the tip of the pyramid for y in range(base_y, self.ground_level): new_height = y - base_y new_width = 2 * new_height bottom_row = QRect(base_x, y, new_width, 1) if any([ bottom_row.intersects(obj.get_rect()) and y == obj.get_rect().top() for obj in self.objects_ref[0:self.index_in_level] ]): break base_x = base_x - (new_width // 2) blank = self.blocks[0] left_slope = self.blocks[1] left_fill = self.blocks[2] right_fill = self.blocks[3] right_slope = self.blocks[4] for y in range(new_height): blank_blocks = (new_width // 2) - (y + 1) middle_blocks = y # times two blocks_to_draw.extend(blank_blocks * [blank]) blocks_to_draw.append(left_slope) blocks_to_draw.extend(middle_blocks * [left_fill] + middle_blocks * [right_fill]) blocks_to_draw.append(right_slope) blocks_to_draw.extend(blank_blocks * [blank]) elif self.orientation == ENDING: page_width = 16 page_limit = page_width - self.x_position % page_width new_width = page_width + page_limit + 1 new_height = (GROUND - 1) - SKY for y in range(SKY, GROUND - 1): blocks_to_draw.append(self.blocks[0]) blocks_to_draw.extend([self.blocks[1]] * (new_width - 1)) # todo magic number # ending graphics rom_offset = ENDING_OBJECT_OFFSET + self.object_set.get_ending_offset( ) * 0x60 rom = ROM() ending_graphic_height = 6 floor_height = 1 y_offset = GROUND - floor_height - ending_graphic_height for y in range(ending_graphic_height): for x in range(page_width): block_index = rom.get_byte(rom_offset + y * page_width + x - 1) block_position = (y_offset + y) * new_width + x + page_limit + 1 blocks_to_draw[block_position] = block_index # Mushroom/Fire flower/Star is categorized as an enemy elif self.orientation == VERTICAL: new_height = self.length + 1 new_width = self.width if self.ending == UNIFORM: if self.is_4byte: # there is one VERTICAL 4-byte object: Vertically oriented X-blocks # the width is the primary expansion new_width = (self.obj_index & 0x0F) + 1 for _ in range(new_height): for x in range(new_width): for y in range(self.height): blocks_to_draw.append(self.blocks[x % self.width]) elif self.ending == END_ON_TOP_OR_LEFT: # in case the drawn object is smaller than its actual size for y in range(min(self.height, new_height)): offset = y * self.width blocks_to_draw.extend(self.blocks[offset:offset + self.width]) additional_rows = new_height - self.height # assume only the last row needs to repeat # todo true for giant blocks? if additional_rows > 0: last_row = self.blocks[-self.width:] for _ in range(additional_rows): blocks_to_draw.extend(last_row) elif self.ending == END_ON_BOTTOM_OR_RIGHT: additional_rows = new_height - self.height # assume only the first row needs to repeat # todo true for giant blocks? if additional_rows > 0: last_row = self.blocks[0:self.width] for _ in range(additional_rows): blocks_to_draw.extend(last_row) # in case the drawn object is smaller than its actual size for y in range(min(self.height, new_height)): offset = y * self.width blocks_to_draw.extend(self.blocks[offset:offset + self.width]) elif self.ending == TWO_ENDS: # object exists on ships top_row = self.blocks[0:self.width] bottom_row = self.blocks[-self.width:] blocks_to_draw.extend(top_row) additional_rows = new_height - 2 # repeat second to last row if additional_rows > 0: for _ in range(additional_rows): blocks_to_draw.extend( self.blocks[-2 * self.width:-self.width]) if new_height > 1: blocks_to_draw.extend(bottom_row) elif self.orientation in [HORIZONTAL, HORIZ_TO_GROUND, HORIZONTAL_2]: new_width = self.length + 1 if self.orientation == HORIZ_TO_GROUND: # to the ground only, until it hits something for y in range(base_y, self.ground_level): bottom_row = QRect(base_x, y, new_width, 1) if any([ bottom_row.intersects(obj.get_rect()) and y == obj.get_rect().top() for obj in self.objects_ref[0:self.index_in_level] ]): new_height = y - base_y break else: # nothing underneath this object, extend to the ground new_height = self.ground_level - base_y if self.is_single_block: new_width = self.length elif self.orientation == HORIZONTAL_2 and self.ending == TWO_ENDS: # floating platforms seem to just be one shorter for some reason new_width -= 1 else: new_height = self.height + self.secondary_length if self.ending == UNIFORM and not self.is_4byte: for y in range(new_height): offset = (y % self.height) * self.width for _ in range(0, new_width): blocks_to_draw.extend(self.blocks[offset:offset + self.width]) # in case of giant blocks new_width *= self.width elif self.ending == UNIFORM and self.is_4byte: # 4 byte objects top = self.blocks[0:1] bottom = self.blocks[-1:] new_height = self.height + self.secondary_length # ceilings are one shorter than normal if self.height > self.width: new_height -= 1 blocks_to_draw.extend(new_width * top) for _ in range(1, new_height): blocks_to_draw.extend(new_width * bottom) elif self.ending == END_ON_TOP_OR_LEFT: for y in range(new_height): offset = y * self.width blocks_to_draw.append(self.blocks[offset]) for x in range(1, new_width): blocks_to_draw.append(self.blocks[offset + 1]) elif self.ending == END_ON_BOTTOM_OR_RIGHT: for y in range(new_height): offset = y * self.width for x in range(new_width - 1): blocks_to_draw.append(self.blocks[offset]) blocks_to_draw.append(self.blocks[offset + self.width - 1]) elif self.ending == TWO_ENDS: top_and_bottom_line = 2 for y in range(self.height): offset = y * self.width left, *middle, right = self.blocks[offset:offset + self.width] blocks_to_draw.append(left) blocks_to_draw.extend(middle * (new_width - top_and_bottom_line)) blocks_to_draw.append(right) if not len(blocks_to_draw) % self.height == 0: print( f"Blocks to draw are not divisible by height. {self}") new_width = int(len(blocks_to_draw) / self.height) top_row = blocks_to_draw[0:new_width] bottom_row = blocks_to_draw[-new_width:] middle_blocks = blocks_to_draw[new_width:-new_width] new_rows = new_height - top_and_bottom_line if new_rows >= 0: blocks_to_draw = top_row + middle_blocks * new_rows + bottom_row else: if not self.orientation == SINGLE_BLOCK_OBJECT: print(f"Didn't render {self.description}") # breakpoint() # for not yet implemented objects and single block objects if blocks_to_draw: self.rendered_blocks = blocks_to_draw else: self.rendered_blocks = self.blocks self.rendered_width = new_width self.rendered_height = new_height self.rendered_base_x = base_x self.rendered_base_y = base_y if new_width and not self.rendered_height == len( self.rendered_blocks) / new_width: print( f"Not enough Blocks for calculated height: {self.description}. " f"Blocks for height: {len(self.rendered_blocks) / new_width}. Rendered height: {self.rendered_height}" ) self.rendered_height = len(self.rendered_blocks) / new_width elif new_width == 0: print( f"Calculated Width is 0, setting to 1: {self.description}. " f"Blocks to draw: {len(self.rendered_blocks)}. Rendered height: {self.rendered_height}" ) self.rendered_width = 1 self.rect = QRect(self.rendered_base_x, self.rendered_base_y, self.rendered_width, self.rendered_height)
def _read_in_chr_rom_segment(self, index): offset = CHR_ROM_OFFSET + index * CHR_ROM_SEGMENT_SIZE chr_rom_data = ROM().bulk_read(2 * CHR_ROM_SEGMENT_SIZE, offset) self.data.extend(chr_rom_data)
def rom(): rom = ROM(test_rom_path) yield rom