def from_m3l(self, m3l_bytes): self.world, self.level, self.object_set_number = m3l_bytes[:3] self.object_set = ObjectSet(self.object_set_number) self.object_offset = self.enemy_offset = 0 # update the level_object_factory self._load_level(b"", b"") m3l_bytes = m3l_bytes[3:] self.header = m3l_bytes[:Level.HEADER_LENGTH] self._parse_header() m3l_bytes = m3l_bytes[Level.HEADER_LENGTH:] # figure out how many bytes are the objects self._load_objects(m3l_bytes) object_size = self._calc_objects_size() + len(b"\xFF") # delimiter object_bytes = m3l_bytes[:object_size] enemy_bytes = m3l_bytes[object_size:] self._load_level(object_bytes, enemy_bytes) self.attached_to_rom = False
def __init__(self, world, level, object_data_offset, enemy_data_offset, object_set): super(Level, self).__init__(world, level, object_set) self.attached_to_rom = True self.object_set_number = object_set self.object_set = ObjectSet(object_set) level_index = Level.world_indexes[world - 1] + 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}'" self.object_offset = object_data_offset self.enemy_offset = enemy_data_offset + 1 self.objects = [] self.jumps = [] self.enemies = [] print( f"Loading {self.name} @ {hex(self.object_offset)}/{hex(self.enemy_offset)}" ) rom = ROM() self.header = rom.bulk_read(Level.HEADER_LENGTH, self.object_offset) self._parse_header() object_offset = self.object_offset + Level.HEADER_LENGTH object_data = ROM.rom_data[object_offset:] enemy_data = ROM.rom_data[self.enemy_offset:] self._load_level(object_data, enemy_data) self.changed = False
def __init__(self, data, png_data, palette_group): super(EnemyObject, self).__init__() self.is_4byte = False self.obj_index = data[0] self.x_position = data[1] self.y_position = data[2] self.pattern_table = PatternTable(0x4C) self.palette_group = palette_group self.object_set = ObjectSet(ENEMY_OBJECT_SET) self.bg_color = NESPalette[palette_group[0][0]] self.png_data = png_data self.selected = False self._setup()
def __init__( self, data, object_set, object_definitions, palette_group, pattern_table, objects_ref, is_vertical, index, size_minimal=False, ): self.object_set = ObjectSet(object_set) self.pattern_table = pattern_table self.tsa_data = ROM.get_tsa_data(object_set) self.x_position = 0 self.y_position = 0 self.rendered_base_x = 0 self.rendered_base_y = 0 self.palette_group = palette_group self.index = index self.objects_ref = objects_ref self.vertical_level = is_vertical self.data = data self.selected = False self.size_minimal = size_minimal self._setup()
class Level(LevelLike): MIN_LENGTH = 0x10 offsets, world_indexes = _load_level_offsets() WORLDS = len(world_indexes) HEADER_LENGTH = 9 # bytes palettes = [] def __init__(self, world, level, object_data_offset, enemy_data_offset, object_set): super(Level, self).__init__(world, level, object_set) self.attached_to_rom = True self.object_set_number = object_set self.object_set = ObjectSet(object_set) level_index = Level.world_indexes[world - 1] + 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}'" self.object_offset = object_data_offset self.enemy_offset = enemy_data_offset + 1 self.objects = [] self.jumps = [] self.enemies = [] print( f"Loading {self.name} @ {hex(self.object_offset)}/{hex(self.enemy_offset)}" ) rom = ROM() self.header = rom.bulk_read(Level.HEADER_LENGTH, self.object_offset) self._parse_header() object_offset = self.object_offset + Level.HEADER_LENGTH object_data = ROM.rom_data[object_offset:] enemy_data = ROM.rom_data[self.enemy_offset:] self._load_level(object_data, enemy_data) self.changed = False def _load_level(self, object_data, enemy_data): self.object_factory = LevelObjectFactory( self.object_set_number, self._graphic_set_index, self._object_palette_index, self.objects, self._is_vertical, ) self.enemy_item_factory = EnemyItemFactory(self.object_set_number, self._enemy_palette_index) self._load_objects(object_data) self._load_enemies(enemy_data) self.object_size_on_disk = self._calc_objects_size() self.enemy_size_on_disk = len(self.enemies) * ENEMY_SIZE def reload(self): header_and_object_data, enemy_data = self.to_bytes() object_data = header_and_object_data[1][Level.HEADER_LENGTH:] self._load_level(object_data, enemy_data[1]) def _calc_objects_size(self): size = 0 for obj in self.objects: if obj.is_4byte: size += 4 else: size += 3 size += Jump.SIZE * len(self.jumps) return size def _parse_header(self): self._start_y_index = (self.header[4] & 0b1110_0000) >> 5 self._length = Level.MIN_LENGTH + (self.header[4] & 0b0000_1111) * 0x10 self.width = self._length self.height = LEVEL_DEFAULT_HEIGHT self._start_x_index = (self.header[5] & 0b0110_0000) >> 5 self._enemy_palette_index = (self.header[5] & 0b0001_1000) >> 3 self._object_palette_index = self.header[5] & 0b0000_0111 self._pipe_ends_level = not (self.header[6] & 0b1000_0000) self._scroll_type_index = (self.header[6] & 0b0110_0000) >> 5 self._is_vertical = self.header[6] & 0b0001_0000 if self._is_vertical: self.height = self._length self.width = LEVEL_DEFAULT_WIDTH # todo isn't that the object set for the "next area"? self._next_area_object_set = (self.header[6] & 0b0000_1111 ) # for indexing purposes self._start_action = (self.header[7] & 0b1110_0000) >> 5 self._graphic_set_index = self.header[7] & 0b0001_1111 self._time_index = (self.header[8] & 0b1100_0000) >> 6 self._music_index = self.header[8] & 0b0000_1111 # if there is a bonus area or other secondary level, this pointer points to it self.object_set_pointer = object_set_pointers[self.object_set_number] self._level_pointer = ((self.header[1] << 8) + self.header[0] + LEVEL_POINTER_OFFSET + self.object_set_pointer.type) self._enemy_pointer = ((self.header[3] << 8) + self.header[2] + ENEMY_POINTER_OFFSET) self.has_bonus_area = (self.object_set_pointer.min <= self._level_pointer <= self.object_set_pointer.max) self.size = self.width, self.height self.changed = True def _load_enemies(self, data): self.enemies.clear() def data_left(_data): # the commented out code seems to hold for the stock ROM, but if the ROM was already edited with another # editor, it might not, since they only wrote the 0xFF to end the enemy data return _data and not _data[ 0] == 0xFF # and _data[1] in [0x00, 0x01] enemy_data, data = data[0:ENEMY_SIZE], data[ENEMY_SIZE:] while data_left(enemy_data): enemy = self.enemy_item_factory.make_object(enemy_data, 0) self.enemies.append(enemy) enemy_data, data = data[0:ENEMY_SIZE], data[ENEMY_SIZE:] def _load_objects(self, data): self.objects.clear() self.jumps.clear() if not data or data[0] == 0xFF: return while True: obj_data, data = data[0:3], data[3:] domain = (obj_data[0] & 0b1110_0000) >> 5 obj_id = obj_data[2] has_length_byte = (self.object_set.get_object_byte_length( domain, obj_id) == 4) if has_length_byte: fourth_byte, data = data[0], data[1:] obj_data.append(fourth_byte) level_object = self.object_factory.from_data( obj_data, len(self.objects)) if isinstance(level_object, LevelObject): self.objects.append(level_object) elif isinstance(level_object, Jump): self.jumps.append(level_object) if data[0] == 0xFF: break @property def next_area_objects(self): return self._level_pointer @next_area_objects.setter def next_area_objects(self, value): if value == self._level_pointer: return value -= LEVEL_POINTER_OFFSET + self.object_set_pointer.type self.header[0] = 0x00FF & value self.header[1] = value >> 8 self._parse_header() @property def next_area_enemies(self): return self._enemy_pointer @next_area_enemies.setter def next_area_enemies(self, value): if value == self._enemy_pointer: return value -= ENEMY_POINTER_OFFSET self.header[2] = 0x00FF & value self.header[3] = value >> 8 self._parse_header() @property def start_y_index(self): return self._start_y_index @start_y_index.setter def start_y_index(self, index): if index == self._start_y_index: return self.header[4] &= 0b0001_1111 self.header[4] |= index << 5 self._parse_header() # bit 4 unused @property def length(self): return self._length @length.setter def length(self, length): """ Sets the length of the level in "screens". :param length: amount of screens the level should have :return: """ # internally the level has length + 1 screens if length + 1 == self._length: return self.header[4] &= 0b1111_0000 self.header[4] |= length // 0x10 self._parse_header() # bit 1 unused @property def start_x_index(self): return self._start_x_index @start_x_index.setter def start_x_index(self, index): if index == self._start_x_index: return self.header[5] &= 0b1001_1111 self.header[5] |= index << 5 self._parse_header() @property def enemy_palette_index(self): return self._enemy_palette_index @enemy_palette_index.setter def enemy_palette_index(self, index): if index == self._enemy_palette_index: return self.header[5] &= 0b1110_0111 self.header[5] |= index << 3 self._parse_header() @property def object_palette_index(self): return self._object_palette_index @object_palette_index.setter def object_palette_index(self, index): if index == self._object_palette_index: return self.header[5] &= 0b1111_1000 self.header[5] |= index self._parse_header() @property def pipe_ends_level(self): return self._pipe_ends_level @pipe_ends_level.setter def pipe_ends_level(self, truth_value): if truth_value == self._pipe_ends_level: return self.header[6] &= 0b0111_1111 self.header[6] |= int(not truth_value) << 7 self._parse_header() @property def scroll_type(self): return self._scroll_type_index @scroll_type.setter def scroll_type(self, index): if index == self._scroll_type_index: return self.header[6] &= 0b1001_1111 self.header[6] |= index << 5 self._parse_header() @property def is_vertical(self): return self._is_vertical @is_vertical.setter def is_vertical(self, truth_value): if truth_value == self._is_vertical: return self.header[6] &= 0b1110_1111 self.header[6] |= int(truth_value) << 4 self._parse_header() @property def next_area_object_set(self): return self._next_area_object_set @next_area_object_set.setter def next_area_object_set(self, index): if index == self._next_area_object_set: return self.header[6] &= 0b1111_0000 self.header[6] |= index self._parse_header() @property def start_action(self): return self._start_action @start_action.setter def start_action(self, index): if index == self._start_action: return self.header[7] &= 0b0001_1111 self.header[7] |= index << 5 self._parse_header() @property def graphic_set(self): return self._graphic_set_index @graphic_set.setter def graphic_set(self, index): if index == self._graphic_set_index: return self.header[7] &= 0b1110_0000 self.header[7] |= index self._parse_header() @property def time_index(self): return self._time_index @time_index.setter def time_index(self, index): if index == self._time_index: return self.header[8] &= 0b0011_1111 self.header[8] |= index << 6 self._parse_header() # bit 3 and 4 unused @property def music_index(self): return self._music_index @music_index.setter def music_index(self, index): if index == self._music_index: return self.header[8] &= 0b1111_0000 self.header[8] |= index self._parse_header() def is_too_big(self): too_many_enemies = self.enemy_size_on_disk < len( self.enemies) * ENEMY_SIZE too_many_objects = self._calc_objects_size() > self.object_size_on_disk return too_many_enemies or too_many_objects def get_all_objects(self): return self.objects + self.enemies def get_object_names(self): return [obj.description for obj in self.objects + self.enemies] def object_at(self, x, y): for obj in reversed(self.objects + self.enemies): if (x, y) in obj: return obj else: return None def draw(self, dc, block_length, transparency): bg_color = get_bg_color_for(self.object_set_number, self._object_palette_index) dc.SetBackground(wx.Brush(wx.Colour(bg_color))) dc.SetPen(wx.Pen(wx.Colour(0x00, 0x00, 0x00, 0x80), width=1)) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.Clear() if self.object_set_number == 9: # desert self._draw_floor(dc, block_length) for level_object in self.objects: level_object.render() level_object.draw(dc, block_length, transparency) if level_object.selected: x, y, w, h = level_object.get_rect().Get() x *= block_length w *= block_length y *= block_length h *= block_length dc.DrawRectangle(wx.Rect(x, y, w, h)) for enemy in self.enemies: enemy.draw(dc, block_length, transparency) if enemy.selected: x, y, w, h = enemy.get_rect().Get() x *= block_length w *= block_length y *= block_length h *= block_length dc.DrawRectangle(wx.Rect(x, y, w, h)) def _draw_floor(self, dc, block_length): floor_level = 26 floor_block_index = 86 palette_group = load_palette(self.object_set_number, self._object_palette_index) pattern_table = PatternTable(self._graphic_set_index) tsa_data = ROM().get_tsa_data(self.object_set_number) floor_block = Block(floor_block_index, palette_group, pattern_table, tsa_data) for x in range(self.width): floor_block.draw(dc, x * block_length, floor_level * block_length, block_length) def paste_object_at(self, x, y, obj): if isinstance(obj, EnemyObject): return self.add_enemy(obj.obj_index, x, y) elif isinstance(obj, LevelObject): if obj.is_4byte: length = obj.data[3] else: length = None return self.add_object(obj.domain, obj.obj_index, x, y, length) def create_object_at(self, x, y, domain=0, object_index=0): self.add_object(domain, object_index, x, y, None, len(self.objects)) def create_enemy_at(self, x, y): # goomba to have something to display self.add_enemy(0x72, x, y, len(self.enemies)) def add_object(self, domain, object_index, x, y, length, index=-1): if index == -1: index = len(self.objects) obj = self.object_factory.from_properties(domain, object_index, x, y, length, index) self.objects.insert(index, obj) self.changed = True return obj def add_enemy(self, object_index, x, y, index=-1): if index == -1: index = len(self.enemies) else: index %= len(self.objects) enemy = self.enemy_item_factory.make_object([object_index, x, y], -1) self.enemies.insert(index, enemy) self.changed = True return enemy def add_jump(self): self.jumps.append(Jump.from_properties(0, 0, 0, 0)) def index_of(self, obj): if obj in self.objects: return self.objects.index(obj) else: return len(self.objects) + self.enemies.index(obj) def get_object(self, index): if index < len(self.objects): return self.objects[index] else: return self.enemies[index % len(self.objects)] def remove_object(self, obj): if obj is None: return try: self.objects.remove(obj) except ValueError: self.enemies.remove(obj) self.changed = True def to_m3l(self): m3l_bytes = bytearray() m3l_bytes.append(self.world) m3l_bytes.append(self.level) m3l_bytes.append(self.object_set_number) m3l_bytes.extend(self.header) for obj in self.objects + self.jumps: m3l_bytes.extend(obj.to_bytes()) # only write 0xFF, even though the stock ROM would use 0xFF00 or 0xFF01 # this is done to keep compatibility to older editors m3l_bytes.append(0xFF) for enemy in sorted(self.enemies, key=lambda _enemy: _enemy.x_position): m3l_bytes.extend(enemy.to_bytes()) m3l_bytes.append(0xFF) return m3l_bytes def from_m3l(self, m3l_bytes): self.world, self.level, self.object_set_number = m3l_bytes[:3] self.object_set = ObjectSet(self.object_set_number) self.object_offset = self.enemy_offset = 0 # update the level_object_factory self._load_level(b"", b"") m3l_bytes = m3l_bytes[3:] self.header = m3l_bytes[:Level.HEADER_LENGTH] self._parse_header() m3l_bytes = m3l_bytes[Level.HEADER_LENGTH:] # figure out how many bytes are the objects self._load_objects(m3l_bytes) object_size = self._calc_objects_size() + len(b"\xFF") # delimiter object_bytes = m3l_bytes[:object_size] enemy_bytes = m3l_bytes[object_size:] self._load_level(object_bytes, enemy_bytes) self.attached_to_rom = False def to_bytes(self): data = bytearray() data.extend(self.header) for obj in self.objects: data.extend(obj.to_bytes()) for jump in self.jumps: data.extend(jump.to_bytes()) data.append(0xFF) enemies = bytearray() for enemy in sorted(self.enemies, key=lambda _enemy: _enemy.x_position): enemies.extend(enemy.to_bytes()) enemies.append(0xFF) return [(self.object_offset, data), (self.enemy_offset, enemies)] def from_bytes(self, object_data, enemy_data): self.object_offset, object_bytes = object_data self.enemy_offset, enemies = enemy_data self.header = object_bytes[0:Level.HEADER_LENGTH] objects = object_bytes[Level.HEADER_LENGTH:] self._parse_header() self._load_level(objects, enemies)
class LevelObject(ObjectLike): def __init__( self, data, object_set, object_definitions, palette_group, pattern_table, objects_ref, is_vertical, index, size_minimal=False, ): self.object_set = ObjectSet(object_set) self.pattern_table = pattern_table self.tsa_data = ROM.get_tsa_data(object_set) self.x_position = 0 self.y_position = 0 self.rendered_base_x = 0 self.rendered_base_y = 0 self.palette_group = palette_group self.index = index self.objects_ref = objects_ref self.vertical_level = is_vertical self.data = data self.selected = False self.size_minimal = size_minimal self._setup() def _setup(self): data = self.data # where to look for the graphic data? self.domain = (data[0] & 0b1110_0000) >> 5 # position relative to the start of the level (top) self.original_y = data[0] & 0b0001_1111 self.y_position = self.original_y # position relative to the start of the level (left) self.original_x = data[1] self.x_position = self.original_x if self.vertical_level: offset = (self.x_position // SCREEN_WIDTH) * SCREEN_HEIGHT self.y_position += offset self.x_position %= SCREEN_WIDTH # describes what object it is self.obj_index = data[2] self.is_single_block = self.obj_index <= 0x0F domain_offset = self.domain * 0x1F if self.is_single_block: self.type = self.obj_index + domain_offset else: self.type = (self.obj_index >> 4) + domain_offset + 16 - 1 object_data = self.object_set.get_definition_of(self.type) self.width = object_data.bmp_width self.height = object_data.bmp_height self.orientation = object_data.orientation self.ending = object_data.ending self.description = object_data.description self.blocks = [int(block) for block in object_data.rom_object_design] self.block_cache = {} self.is_4byte = object_data.is_4byte if self.is_4byte and len(self.data) == 3: self.data.append(0) elif not self.is_4byte and len(data) == 4: del self.data[3] self.secondary_length = 0 self._calculate_lengths() self.rect = wx.Rect() self._render() def _calculate_lengths(self): if self.is_single_block: self.length = 1 else: self.length = self.obj_index & 0b0000_1111 if self.is_4byte: self.secondary_length = self.length self.length = self.data[3] def render(self): self._render() def _render(self): try: self.index = self.objects_ref.index(self) except ValueError: # the object has not been added yet, so stick with the one given in the constructor pass base_x = self.x_position base_y = self.y_position new_width = self.width new_height = self.height 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]: 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] else: fill_block = self.blocks[-1:] slopes = self.blocks[0:-1] left = [BLANK] right = 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): r = (y // self.height) * slope_width l = new_width - slope_width - r offset = y % self.height rows.append( l * left + slopes[offset : offset + slope_width] + r * 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 if not self.size_minimal: for y in range(base_y, GROUND): new_height = y - base_y new_width = 2 * new_height bottom_row = wx.Rect(base_x, y, new_width, 1) if any( [ bottom_row.Intersects(obj.get_rect()) and y == obj.get_rect().GetTop() for obj in self.objects_ref[0 : self.index] ] ): 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: for _ in range(new_height): for x in range(self.width): for y in range(self.height): blocks_to_draw.append(self.blocks[x]) 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: if not self.size_minimal: # to the ground only, until it hits something for y in range(base_y, GROUND): bottom_row = wx.Rect(base_x, y, new_width, 1) if any( [ bottom_row.Intersects(obj.get_rect()) and y == obj.get_rect().GetTop() for obj in self.objects_ref[0 : self.index] ] ): new_height = y - base_y break else: # nothing underneath this object, extend to the ground new_height = GROUND - 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 = wx.Rect( self.rendered_base_x, self.rendered_base_y, self.rendered_width, self.rendered_height, ) def draw(self, dc, block_length, transparent): for index, block_index in enumerate(self.rendered_blocks): if block_index == BLANK: continue x = self.rendered_base_x + index % self.rendered_width y = self.rendered_base_y + index // self.rendered_width self._draw_block(dc, block_index, x, y, block_length, transparent) def _draw_block(self, dc, 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( dc, x * block_length, y * block_length, block_length=block_length, selected=self.selected, transparent=transparent, ) def set_position(self, x, y): # todo also check for the upper bounds x = max(0, x) y = max(0, y) x_diff = self.x_position - self.rendered_base_x y_diff = self.y_position - self.rendered_base_y self.rendered_base_x = int(x) self.rendered_base_y = int(y) self.x_position = self.rendered_base_x + x_diff self.y_position = self.rendered_base_y + y_diff self._render() def move_by(self, dx, dy): new_x = self.rendered_base_x + dx new_y = self.rendered_base_y + dy self.set_position(new_x, new_y) def get_position(self): return self.x_position, self.y_position def resize_to(self, x, y): if not self.is_single_block: if self.is_4byte: max_width = 0xFF else: max_width = 0x0F # don't get negative width = max(0, x - self.x_position) # stay under maximum width width = min(width, max_width) if self.is_4byte: self.data[3] = width else: base_index = (self.obj_index // 0x10) * 0x10 self.obj_index = base_index + width self.data[2] = self.obj_index self._calculate_lengths() self._render() def resize_by(self, dx, dy): new_x = self.x_position + dx new_y = self.y_position + dy self.resize_to(new_x, new_y) def increment_type(self): self.change_type(True) def decrement_type(self): self.change_type(False) def change_type(self, increment): if self.obj_index < 0x10 or self.obj_index == 0x10 and not increment: value = 1 else: self.obj_index = self.obj_index // 0x10 * 0x10 value = 0x10 if not increment: value *= -1 new_type = self.obj_index + value if new_type < 0 and self.domain > 0: new_domain = self.domain - 1 new_type = 0xF0 elif new_type > 0xFF and self.domain < 7: new_domain = self.domain + 1 new_type = 0x00 else: new_type = min(0xFF, new_type) new_type = max(0, new_type) new_domain = self.domain self.data[0] &= 0b0001_1111 self.data[0] |= new_domain << 5 self.data[2] = new_type self._setup() def __contains__(self, item): x, y = item return self.point_in(x, y) def point_in(self, x, y): return self.rect.Contains(x, y) def get_status_info(self): return [ ("x", self.rendered_base_x), ("y", self.rendered_base_y), ("Width", self.rendered_width), ("Height", self.rendered_height), ("Orientation", ORIENTATION_TO_STR[self.orientation]), ("Ending", ENDING_STR[self.ending]), ] def get_rect(self): return self.rect def as_bitmap(self): """ Creates a Bitmap of the level object for use in the GUI (for menus or dropdowns). Shouldn't be used for Levelobjects, that are used in the LevelView. The Bitmap has to be resized, if necessary. :return: A wx.Bitmap with a version of this LevelObject. :rtype: wx.Bitmap """ assert self.rendered_base_x == 0 assert self.rendered_base_y == 0 dc = wx.MemoryDC() bitmap = wx.Bitmap( width=self.rendered_width * Block.SIDE_LENGTH, height=self.rendered_height * Block.SIDE_LENGTH, ) dc.SelectObject(bitmap) bg_color = get_bg_color_for(self.object_set.number, 0) dc.SetBackground(wx.Brush(wx.Colour(bg_color))) dc.Clear() self.draw(dc, Block.SIDE_LENGTH, True) dc.SelectObject(wx.NullBitmap) return bitmap def to_bytes(self): data = bytearray() if self.vertical_level: # todo from vertical to non-vertical is bugged, because it # seems like you can't convert the coordinates 1:1 # there seems to be ambiguity offset = self.y_position // SCREEN_HEIGHT x_position = self.x_position + offset * SCREEN_WIDTH y_position = self.y_position % SCREEN_HEIGHT else: x_position = self.x_position y_position = self.y_position if self.orientation in [PYRAMID_TO_GROUND, PYRAMID_2]: x_position = self.rendered_base_x - 1 + self.rendered_width // 2 data.append((self.domain << 5) | y_position) data.append(x_position) data.append(self.obj_index) if self.is_4byte: data.append(self.length) return data def __repr__(self): return f"LevelObject {self.description} at {self.x_position}, {self.y_position}"
class EnemyObject(ObjectLike): def __init__(self, data, png_data, palette_group): super(EnemyObject, self).__init__() self.is_4byte = False self.obj_index = data[0] self.x_position = data[1] self.y_position = data[2] self.pattern_table = PatternTable(0x4C) self.palette_group = palette_group self.object_set = ObjectSet(ENEMY_OBJECT_SET) self.bg_color = NESPalette[palette_group[0][0]] self.png_data = png_data self.selected = False self._setup() def _setup(self): obj_def = self.object_set.get_definition_of(self.obj_index) self.description = obj_def.description self.width = obj_def.bmp_width self.height = obj_def.bmp_height self.rect = wx.Rect(self.x_position, self.y_position, self.width, self.height) self._render(obj_def) def _render(self, obj_def): self.blocks = [] block_ids = obj_def.object_design for block_id in block_ids: x = (block_id % 64) * Block.WIDTH y = (block_id // 64) * Block.WIDTH self.blocks.append( self.png_data.GetSubImage( wx.Rect(x, y, Block.WIDTH, Block.HEIGHT))) def render(self): # nothing to re-render since enemies are just copied over pass def draw(self, dc, block_length, transparent): for i, image in enumerate(self.blocks): x = self.x_position + (i % self.width) y = self.y_position + (i // self.width) x_offset = int(enemy_handle_x[self.obj_index]) y_offset = int(enemy_handle_y[self.obj_index]) x += x_offset y += y_offset block = image.Copy() block.SetMaskColour(*MASK_COLOR) if not transparent: block.Replace(*MASK_COLOR, *self.bg_color) # todo better effect if self.selected: block = block.ConvertToDisabled(127) if block_length != Block.SIDE_LENGTH: block.Rescale(block_length, block_length, quality=wx.IMAGE_QUALITY_NEAREST) dc.DrawBitmap( block.ConvertToBitmap(), x * block_length, y * block_length, useMask=transparent, ) def get_status_info(self): return [ ("Name", self.description), ("X", self.x_position), ("Y", self.y_position), ] def __contains__(self, item): x, y = item return self.point_in(x, y) def point_in(self, x, y): return self.rect.Contains(x, y) def set_position(self, x, y): # todo also check for the upper bounds x = max(0, x) y = max(0, y) self.x_position = x self.y_position = y self.rect = wx.Rect(self.x_position, self.y_position, self.width, self.height) def move_by(self, dx, dy): new_x = self.x_position + dx new_y = self.y_position + dy self.set_position(new_x, new_y) def get_position(self): return self.x_position, self.y_position def resize_to(self, _, __): pass def resize_by(self, dx, dy): new_x = self.x_position + dx new_y = self.y_position + dy self.resize_to(new_x, new_y) def change_type(self, new_type): self.obj_index = new_type self._setup() def get_rect(self): return self.rect def increment_type(self): self.obj_index = min(0xFF, self.obj_index + 1) self._setup() def decrement_type(self): self.obj_index = max(0, self.obj_index - 1) self._setup() def to_bytes(self): return bytearray([self.obj_index, self.x_position, self.y_position]) def __repr__(self): return f"EnemyObject {self.description} at {self.x_position}, {self.y_position}"