Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    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()
Esempio n. 4
0
    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()
Esempio n. 5
0
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)
Esempio n. 6
0
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}"
Esempio n. 7
0
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}"