Skip to content

Commit

Permalink
support having a level open while rearranging levels
Browse files Browse the repository at this point in the history
  • Loading branch information
mchlnix committed Mar 11, 2023
1 parent 8dfcd6c commit eb1095d
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 63 deletions.
20 changes: 8 additions & 12 deletions foundry/game/additional_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ def __init__(self, rom: Rom):
work with this.
"""

self.found_level_information: list[FoundLevel] = []
self.found_levels: list[FoundLevel] = []

def __str__(self) -> str:
return json.dumps(
{
"managed_level_positions": self.managed_level_positions,
"found_level_information": [found_level.to_dict() for found_level in self.found_level_information],
"found_levels": [found_level.to_dict() for found_level in self.found_levels],
}
)

Expand All @@ -56,21 +56,19 @@ def from_str(string_data: str, rom: Rom) -> "AdditionalData":
data_dict = json.loads(string_data)

data_obj.managed_level_positions = data_dict.get("managed_level_positions", None)
data_obj.found_level_information = [
FoundLevel.from_dict(data) for data in data_dict.get("found_level_information", [])
]
data_obj.found_levels = [FoundLevel.from_dict(data) for data in data_dict.get("found_levels", [])]

return data_obj

def __bool__(self):
return bool(self.managed_level_positions is not None or self.found_level_information)
return bool(self.managed_level_positions is not None or self.found_levels)

def free_space_for_object_set(self, object_set_number: int):
prg_banks_by_object_set = self.rom.read(PAGE_A000_ByTileset, 16)

levels_by_bank: dict[int, list[FoundLevel]] = defaultdict(list)

for level in self.found_level_information:
for level in self.found_levels:
levels_by_bank[prg_banks_by_object_set[level.object_set_number]].append(
MovableLevel.from_found_level(level)
)
Expand All @@ -84,7 +82,7 @@ def free_space_for_object_set(self, object_set_number: int):
return free_space_left

def free_space_for_enemies(self):
level_with_last_enemy_data = max(self.found_level_information, key=attrgetter("enemy_offset"))
level_with_last_enemy_data = max(self.found_levels, key=attrgetter("enemy_offset"))

end_of_enemy_data = (
level_with_last_enemy_data.enemy_offset
Expand All @@ -97,7 +95,7 @@ def free_space_for_enemies(self):

def clear(self):
self.managed_level_positions = None
self.found_level_information.clear()
self.found_levels.clear()


class MovableLevel(FoundLevel):
Expand Down Expand Up @@ -270,7 +268,7 @@ def _separate_levels_by_banks(self):
MovableLevel.from_found_level(level)
)

def rearrange_enemies(self) -> dict[EnemyItemAddress, EnemyItemAddress]:
def rearrange_enemies(self):
# 1. Sort levels based on their enemy offset (filter out enemy offsets, that aren't real/mean something else)
sorted_levels = self._sort_levels_by_enemy_address()

Expand All @@ -288,8 +286,6 @@ def rearrange_enemies(self) -> dict[EnemyItemAddress, EnemyItemAddress]:
# 3.2 Save enemy data to new position
self._update_enemy_address_and_copy_data(old_enemy_data_sets, sorted_levels)

return self.old_enemy_address_to_new

def _update_enemy_address_and_copy_data(self, old_enemy_data_sets, sorted_levels):
already_copied = []

Expand Down
76 changes: 44 additions & 32 deletions foundry/game/level/Level.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from foundry.game.File import ROM
from foundry.game.ObjectSet import ObjectSet
from foundry.game.additional_data import LevelOrganizer
from foundry.game.additional_data import LEVEL_DATA_DELIMITER_COUNT, LevelOrganizer
from foundry.game.gfx.objects import EnemyItem, EnemyItemFactory, Jump, LevelObject, LevelObjectFactory
from foundry.game.gfx.objects.in_level.in_level_object import InLevelObject
from foundry.game.gfx.objects.object_like import ObjectLike
Expand All @@ -14,8 +14,9 @@
from smb3parse import OFFSET_BY_OBJECT_SET_A000
from smb3parse.constants import BASE_OFFSET, ENEMY_SIZE, OFFSET_SIZE
from smb3parse.data_points import Position
from smb3parse.levels import HEADER_LENGTH, WORLD_MAP_LAYOUT_DELIMITER
from smb3parse.levels import HEADER_LENGTH
from smb3parse.levels.level_header import LevelHeader
from smb3parse.util.parser import FoundLevel

TIME_INF = -1

Expand Down Expand Up @@ -256,7 +257,7 @@ def was_saved(self):
@property
def objects_end(self):
return (
self.header_offset + HEADER_LENGTH + self.current_object_size() + len(WORLD_MAP_LAYOUT_DELIMITER)
self.header_offset + HEADER_LENGTH + self.current_object_size() + LEVEL_DATA_DELIMITER_COUNT
) # the delimiter

@property
Expand All @@ -267,6 +268,7 @@ def enemies_end(self):
def next_area_objects(self):
return self.header.jump_level_address

# TODO: Rename to from Next Area to Jump (Destination)
@next_area_objects.setter
def next_area_objects(self, value):
if value == self.header.jump_level_address:
Expand Down Expand Up @@ -759,7 +761,7 @@ def from_m3l(self, m3l_bytes: bytearray):

# figure out how many bytes are the objects
self._load_objects(m3l_bytes)
object_size = self.current_object_size() + len(WORLD_MAP_LAYOUT_DELIMITER) # delimiter
object_size = self.current_object_size() + LEVEL_DATA_DELIMITER_COUNT # delimiter

object_bytes = m3l_bytes[:object_size]
enemy_bytes = m3l_bytes[object_size:]
Expand All @@ -779,46 +781,56 @@ def from_asm(self, object_set_number: int, object_bytes: bytearray):
self.level_changed.emit()

def save_to_rom(self) -> None:
(level_address, level_data), (enemy_address, enemy_data) = self.to_bytes()

if ROM().additional_data.managed_level_positions:
if not self.attached_to_rom:
raise ValueError("This level is not attached to the ROM. Please place it somewhere on a world map.")

current_level = next(
filter(
lambda level: level.level_offset == level_address, ROM().additional_data.found_level_information
),
None,
)

if current_level is None:
raise LookupError(f"Current Level {level_address:x} could not be found in ROM. Attach it first.")
current_level = self._find_corresponding_level()

current_level.object_data_length = self.current_object_size()
current_level.enemy_data_length = self.current_enemies_size()
self._update_level_and_enemy_size(current_level)

lo = LevelOrganizer(ROM(), ROM().additional_data.found_level_information)
lo = LevelOrganizer(ROM(), ROM().additional_data.found_levels)

lo.rearrange_levels()
lo.rearrange_enemies()

assert current_level.level_offset in lo.old_level_address_to_new, (
hex(current_level.level_offset),
lo.old_level_address_to_new,
)
current_level.level_offset = lo.old_level_address_to_new[current_level.level_offset]
self._update_level_and_enemy_addresses(current_level, lo)

address_changes = lo.rearrange_enemies()
self.set_addresses(current_level.level_offset, current_level.enemy_offset)
self.next_area_objects = lo.old_level_address_to_new[self.header.jump_level_address]
self.next_area_enemies = lo.old_enemy_address_to_new[self.header.jump_enemy_address]

assert current_level.enemy_offset in address_changes
current_level.enemy_offset = address_changes[current_level.enemy_offset]

level_address = current_level.level_offset
enemy_address = current_level.enemy_offset
(level_address, level_data), (enemy_address, enemy_data) = self.to_bytes()

ROM().write(level_address, level_data)
ROM().write(enemy_address, enemy_data)

@staticmethod
def _update_level_and_enemy_addresses(current_level: FoundLevel, lo: LevelOrganizer):
assert current_level.level_offset in lo.old_level_address_to_new, (
hex(current_level.level_offset),
lo.old_level_address_to_new,
)
assert current_level.enemy_offset in lo.old_enemy_address_to_new

current_level.level_offset = lo.old_level_address_to_new[current_level.level_offset]
current_level.enemy_offset = lo.old_enemy_address_to_new[current_level.enemy_offset]

def _update_level_and_enemy_size(self, current_level: FoundLevel):
current_level.object_data_length = HEADER_LENGTH + self.current_object_size()
current_level.enemy_data_length = self.current_enemies_size()

def _find_corresponding_level(self) -> FoundLevel:
if not self.attached_to_rom:
raise ValueError("This level is not attached to the ROM. Please place it somewhere on a world map.")

current_level = next(
filter(lambda level: level.level_offset == self.header_offset, ROM().additional_data.found_levels),
None,
)

if current_level is None:
raise LookupError(f"Current Level {self.header_offset:x} could not be found in ROM. Attach it first.")

return current_level

def to_bytes(self) -> LevelByteData:
data = bytearray()

Expand Down
3 changes: 3 additions & 0 deletions foundry/game/test_additional_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ def test_rearrange_levels_consistency(level_organizer):

# THEN the result will not change a second time
assert first_result == second_result
assert all(
old_address == new_address for old_address, new_address in level_organizer.old_level_address_to_new.items()
)


def test_rearrange_levels_with_larger_data(mock_rom):
Expand Down
2 changes: 1 addition & 1 deletion foundry/game/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_load_additional_data_from_rom(rom):
assert not rom.additional_data

rom.additional_data.managed_level_positions = True
rom.additional_data.found_level_information.append(FoundLevel([1], [2], 3, 4, 5, 6, 7, 8, True, True, True))
rom.additional_data.found_levels.append(FoundLevel([1], [2], 3, 4, 5, 6, 7, 8, True, True, True))

assert rom.additional_data

Expand Down
4 changes: 2 additions & 2 deletions foundry/gui/FoundryMainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,11 +670,11 @@ def _ask_for_level_management(self):
ROM.additional_data.managed_level_positions = None
return

ROM.additional_data.found_level_information = [
ROM.additional_data.found_levels = [
pd.levels_by_address[key] for key in sorted(pd.levels_by_address.keys())
]

lo = LevelOrganizer(ROM(), ROM().additional_data.found_level_information)
lo = LevelOrganizer(ROM(), ROM().additional_data.found_levels)
lo.rearrange_levels()
lo.rearrange_enemies()

Expand Down
2 changes: 1 addition & 1 deletion foundry/gui/LevelSelector.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def __init__(self, parent):
if not ROM().additional_data.managed_level_positions:
self.on_world_click()
else:
first_level = ROM().additional_data.found_level_information[0]
first_level = ROM().additional_data.found_levels[0]
self._fill_in_data(first_level.object_set_number, first_level.level_offset, first_level.enemy_offset)

def keyPressEvent(self, key_event: QKeyEvent):
Expand Down
5 changes: 2 additions & 3 deletions foundry/gui/menus/rom_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ def _on_trigger(self, action: QAction):
if ROM.additional_data.managed_level_positions:
levels_per_object_set: dict[int, set[int]] = defaultdict(set)

for found_level in ROM.additional_data.found_level_information:
for found_level in ROM.additional_data.found_levels:
levels_per_object_set[found_level.object_set_number].add(found_level.level_offset)

levels_by_address = {
found_level.level_offset: found_level
for found_level in ROM.additional_data.found_level_information
found_level.level_offset: found_level for found_level in ROM.additional_data.found_levels
}

else:
Expand Down
22 changes: 11 additions & 11 deletions foundry/gui/rom_settings/managed_levels_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def update_level_info(self):
if was_enabled:
levels_per_object_set: dict[int, set[int]] = defaultdict(set)

for found_level in ROM.additional_data.found_level_information:
for found_level in ROM.additional_data.found_levels:
levels_per_object_set[found_level.object_set_number].add(found_level.level_offset)

else:
Expand All @@ -75,7 +75,7 @@ def update_level_info(self):

levels_per_object_set = pd.levels_per_object_set

ROM.additional_data.found_level_information = [
ROM.additional_data.found_levels = [
pd.levels_by_address[key] for key in sorted(pd.levels_by_address.keys())
]

Expand Down Expand Up @@ -128,34 +128,34 @@ def level(self) -> Level | None:
return None

def on_rearrange(self):
lo = LevelOrganizer(ROM(), ROM().additional_data.found_level_information)
lo = LevelOrganizer(ROM(), ROM().additional_data.found_levels)
lo.rearrange_levels()
lo.rearrange_enemies()

ROM().save_to_file(ROM.path)

if self.level and self.level.attached_to_rom:
new_level_address = lo.old_level_address_to_new[self.level.layout_address]
new_level_address = lo.old_level_address_to_new[self.level.header_offset]
new_enemy_address = lo.old_enemy_address_to_new[self.level.enemy_offset]

new_jump_level_address = lo.old_level_address_to_new[self.level.header.jump_level_address]
new_jump_enemy_address = lo.old_enemy_address_to_new[self.level.header.jump_enemy_address]

print(f"Level {self.level.layout_address:x} -> {new_level_address}")
print(f"Enemy {self.level.enemy_offset:x} -> {new_enemy_address}")
print(f"Level {self.level.layout_address:x} -> {new_level_address:x}")
print(f"Enemy {self.level.enemy_offset:x} -> {new_enemy_address:x}")

self.level.set_addresses(new_level_address, new_enemy_address)

print(f"Jump Level {self.level.header.jump_level_address:x} -> {new_jump_level_address}")
print(f"Jump Enemy {self.level.header.jump_enemy_address:x} -> {new_jump_enemy_address}")
print(f"Jump Level {self.level.header.jump_level_address:x} -> {new_jump_level_address:x}")
print(f"Jump Enemy {self.level.header.jump_enemy_address:x} -> {new_jump_enemy_address:x}")

self.level.header.jump_level_address = new_jump_level_address
self.level.header.jump_enemy_address = new_jump_enemy_address
self.level.next_area_objects = new_jump_level_address
self.level.next_area_enemies = new_jump_enemy_address

self.needs_gui_update.emit()

def closeEvent(self, event):
super().closeEvent(event)

if not self.enabled_checkbox.isChecked():
ROM.additional_data.found_level_information.clear()
ROM.additional_data.found_levels.clear()
10 changes: 9 additions & 1 deletion smb3parse/levels/level_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(self, rom: Rom, header_bytes: bytearray, object_set_number: int):
self.music_index = self.data[8] & 0b0000_1111

self.jump_level_offset = (self.data[1] << 8) + self.data[0]
self.jump_enemy_address = (self.data[3] << 8) + self.data[2] + ENEMY_BASE_OFFSET
self.jump_enemy_offset = (self.data[3] << 8) + self.data[2]

def mario_position(self):
x = MARIO_X_POSITIONS[self.start_x_index] >> 4
Expand All @@ -75,3 +75,11 @@ def jump_level_address(self):
@jump_level_address.setter
def jump_level_address(self, value):
self.jump_level_offset = value - self.jump_object_set.level_offset

@property
def jump_enemy_address(self):
return self.jump_enemy_offset + ENEMY_BASE_OFFSET

@jump_enemy_address.setter
def jump_enemy_address(self, value):
self.jump_enemy_offset = value - ENEMY_BASE_OFFSET

0 comments on commit eb1095d

Please sign in to comment.