def from_json(cls, value: dict, game: RandovaniaGame) -> "MajorItemsConfiguration": item_database = default_database.item_database_for_game(game) items_state = {} for name, item in item_database.major_items.items(): if name in value["items_state"]: state = MajorItemState.from_json(value["items_state"][name]) else: state = MajorItemState() items_state[item] = state default_items = {} for category, options in item_database.default_items.items(): default_items[category] = item_database.major_items[ value["default_items"][category.value]] if default_items[category] not in options: raise ValueError( f"Category {category} has {default_items[category]} as default item, " f"but that's not a valid option.") return cls( items_state=items_state, default_items=default_items, minimum_random_starting_items=value[ "minimum_random_starting_items"], maximum_random_starting_items=value[ "maximum_random_starting_items"], )
def from_json(cls, value: dict, game: RandovaniaGame) -> "AmmoConfiguration": item_database = default_database.item_database_for_game(game) return cls(items_state={ item_database.ammo[name]: AmmoState.from_json(state) for name, state in value["items_state"].items() }, )
def from_json(cls, value: dict, game: RandovaniaGame) -> "MajorItemsConfiguration": item_database = default_database.item_database_for_game(game) items_state = {} for name, item in item_database.major_items.items(): if name in value["items_state"]: state = MajorItemState.from_json(value["items_state"][name]) else: state = MajorItemState() items_state[item] = state default_items = { category: item_database.major_items[value["default_items"][category.name]] for category, _ in item_database.default_items.items() } return cls( game=game, items_state=items_state, default_items=default_items, minimum_random_starting_items=value[ "minimum_random_starting_items"], maximum_random_starting_items=value[ "maximum_random_starting_items"], )
def __init__(self, editor: PresetEditor): super().__init__() self.setupUi(self) self._editor = editor size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.item_pool_layout.setAlignment(Qt.AlignTop) # Relevant Items self.game = editor.game self.game_description = default_database.game_description_for( self.game) item_database = default_database.item_database_for_game(self.game) self._energy_tank_item = item_database.major_items["Energy Tank"] self._register_random_starting_events() self._create_categories_boxes(item_database, size_policy) self._create_customizable_default_items(item_database) self._create_progressive_widgets(item_database) self._create_major_item_boxes(item_database, self.game_description.resource_database) self._create_energy_tank_box() self._create_split_ammo_widgets(item_database) self._create_ammo_pickup_boxes(size_policy, item_database)
def from_json(cls, json_dict: dict) -> "BaseConfiguration": game = cls.game_enum() item_database = default_database.item_database_for_game(game) kwargs = {} for field in dataclasses.fields(cls): try: arg = json_dict[field.name] except KeyError as e: raise KeyError(f"Missing field {e}") from e try: if issubclass(field.type, Enum): arg = field.type(arg) elif hasattr(field.type, "from_json"): extra_args = [] if field.name in ("trick_level", "starting_location"): extra_args.append(game) if field.name in ("major_items_configuration", "ammo_configuration"): extra_args.append(item_database) arg = field.type.from_json(arg, *extra_args) except ValueError as e: raise ValueError(f"Error in field {field.name}: {e}") from e kwargs[field.name] = arg return cls(**kwargs)
def _add_minimal_logic_initial_resources( self, resources: CurrentResources, game: GameDescription, major_items: MajorItemsConfiguration, ) -> None: resource_database = game.resource_database if game.minimal_logic is None: raise ValueError( f"Minimal logic enabled, but {game.game} doesn't have support for it." ) item_db = default_database.item_database_for_game(game.game) items_to_skip = set() for it in game.minimal_logic.items_to_exclude: if it.reason is None or major_items.items_state[ item_db.major_items[it.reason]].num_shuffled_pickups != 0: items_to_skip.add(it.name) custom_item_count = game.minimal_logic.custom_item_amount events_to_skip = { it.name for it in game.minimal_logic.events_to_exclude } for event in resource_database.event: if event.short_name not in events_to_skip: resources[event] = 1 for item in resource_database.item: if item.short_name not in items_to_skip: resources[item] = custom_item_count.get(item.short_name, 1)
def _add_minimal_logic_initial_resources( resources: CurrentResources, resource_database: ResourceDatabase, major_items: MajorItemsConfiguration, ) -> None: # TODO: this function assumes we're talking about Echoes for event in resource_database.event: # Ignoring these events: # Dark Samus 3 and 4 (93), otherwise we're done automatically (TODO: get this from database) # Chykka (28), otherwise we can't collect Dark Visor if event.index not in {28, 93}: resources[event] = 1 item_db = default_database.item_database_for_game(RandovaniaGame.PRIME2) items_to_skip = copy.copy(_items_to_not_add_in_minimal_logic) if major_items.items_state[item_db.major_items[ "Progressive Grapple"]].num_shuffled_pickups == 0: items_to_skip.remove(23) if major_items.items_state[ item_db.major_items["Progressive Suit"]].num_shuffled_pickups == 0: items_to_skip.remove(13) for item in resource_database.item: if item.index not in items_to_skip: resources[item] = _minimal_logic_custom_item_count.get( item.index, 1)
def _create_config_for(game: RandovaniaGame, replace: dict): with get_data_path().joinpath("item_database", game.value, "default_state", "major-items.json").open() as open_file: default_data = json.load(open_file) default_data["minimum_random_starting_items"] = 0 default_data["maximum_random_starting_items"] = 0 data = copy.deepcopy(default_data) for field, value in replace.items(): for key, inner_value in value.items(): data[field][key] = inner_value return ( MajorItemsConfiguration.from_json( default_data, default_database.item_database_for_game(game)), MajorItemsConfiguration.from_json( data, default_database.item_database_for_game(game)), )
def _config_with_data(request): game: RandovaniaGame = request.param["game"] with get_data_path().joinpath("item_database", game.value, "default_state", "ammo.json").open() as open_file: default_data = json.load(open_file) default = AmmoConfiguration.from_json( default_data, default_database.item_database_for_game(game)) data = copy.deepcopy(default_data) for key, value in request.param.get("items_state", {}).items(): data["items_state"][key] = value for key, value in request.param.get("maximum_ammo", {}).items(): data["maximum_ammo"][key] = value config = AmmoConfiguration.from_json( data, default_database.item_database_for_game(game)) return request.param["encoded"], config, default
def __init__(self, editor: PresetEditor): super().__init__(editor) item_database = default_database.item_database_for_game(self.game) game_description = default_database.game_description_for(self.game) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) self._energy_tank_item = item_database.major_items["Energy Tank"] self._create_energy_tank_box( game_description.resource_database.energy_tank) self._create_pickup_style_box(size_policy)
def __init__(self, editor: PresetEditor): super().__init__(editor) item_database = default_database.item_database_for_game(self.game) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) self._energy_item_to_starting_spinbox = {} self._energy_item_to_shuffled_spinbox = {} self._energy_tank_item = item_database.major_items["Energy Tank"] self._energy_part_item = item_database.major_items["Energy Part"] self._create_energy_box() self._create_pickup_style_box(size_policy)
def update_hints_text( game: RandovaniaGame, hint_item_names_tree_widget: QtWidgets.QTableWidget, ): item_database = default_database.item_database_for_game(game) rows = [] for item in item_database.major_items.values(): rows.append(( item.name, item.item_category.hint_details[1], item.item_category.general_details[1], item.broad_category.hint_details[1], )) if game == RandovaniaGame.PRIME2: for dark_temple_key in DARK_TEMPLE_KEY_NAMES: rows.append(( dark_temple_key.format("").strip(), ItemCategory.TEMPLE_KEY.hint_details[1], ItemCategory.TEMPLE_KEY.general_details[1], ItemCategory.KEY.hint_details[1], )) rows.append(( "Sky Temple Key", ItemCategory.SKY_TEMPLE_KEY.hint_details[1], ItemCategory.SKY_TEMPLE_KEY.general_details[1], ItemCategory.KEY.hint_details[1], )) for item in item_database.ammo.values(): rows.append(( item.name, ItemCategory.EXPANSION.hint_details[1], ItemCategory.EXPANSION.general_details[1], item.broad_category.hint_details[1], )) hint_item_names_tree_widget.setRowCount(len(rows)) for i, elements in enumerate(rows): for j, element in enumerate(elements): hint_item_names_tree_widget.setItem( i, j, QtWidgets.QTableWidgetItem(element)) for i in range(4): hint_item_names_tree_widget.resizeColumnToContents(i)
def __init__(self, description: LayoutDescription, players_config: PlayersConfiguration, cosmetic_patches: BaseCosmeticPatches): self.description = description self.players_config = players_config self.cosmetic_patches = cosmetic_patches self.item_db = default_database.item_database_for_game( self.game_enum()) self.patches = description.all_patches[players_config.player_index] self.configuration = description.get_preset( players_config.player_index).configuration self.rng = Random( description.get_seed_for_player(players_config.player_index)) self.game = filtered_database.game_description_for_layout( self.configuration)
def update_hints_text( game: RandovaniaGame, hint_item_names_tree_widget: QtWidgets.QTableWidget, ): item_database = default_database.item_database_for_game(game) rows = [] for item in item_database.major_items.values(): rows.append(( item.name, item.item_category.hint_details[1], item.item_category.general_details[1], item.broad_category.hint_details[1], )) for name, item_category, broad_category in _GAME_SPECIFIC.get( game, lambda: [])(): rows.append(( name, item_category.hint_details[1], item_category.general_details[1], broad_category.hint_details[1], )) for ammo in item_database.ammo.values(): rows.append(( ammo.name, ammo.item_category.hint_details[1], ammo.item_category.general_details[1], ammo.broad_category.hint_details[1], )) hint_item_names_tree_widget.setSortingEnabled(False) hint_item_names_tree_widget.setRowCount(len(rows)) for i, elements in enumerate(rows): for j, element in enumerate(elements): hint_item_names_tree_widget.setItem( i, j, QtWidgets.QTableWidgetItem(element)) for i in range(4): hint_item_names_tree_widget.resizeColumnToContents(i) hint_item_names_tree_widget.setSortingEnabled(True)
def __post_init__(self): item_database = default_database.item_database_for_game(self.game) _check_matching_items(self.items_state.keys(), item_database.major_items.values()) _check_matching_items(self.default_items.keys(), item_database.default_items.keys()) for item, state in self.items_state.items(): state.check_consistency(item) for category, options in item_database.default_items.items(): if category not in self.default_items: raise ValueError(f"Category {category} is missing an item.") if self.default_items[category] not in options: raise ValueError( f"Category {category} has {self.default_items[category]} as default item, " f"but that's not a valid option.")
def refresh_all_logic(args): from randovania.game_description import pretty_print from randovania.game_description import data_reader, data_writer from randovania.game_description import integrity_check gd_per_game = {} path_per_game = {} idb_per_game = {} for game in iterate_enum(RandovaniaGame): logging.info("Reading %s", game.long_name) path, data = default_data.read_json_then_binary(game) path_per_game[game] = path gd = data_reader.decode_data(data) gd_per_game[game] = gd idb = default_database.item_database_for_game(game) idb_per_game[game] = idb should_stop = False if args.integrity_check: for game, gd in gd_per_game.items(): errors = integrity_check.find_database_errors(gd) if errors: logging.warning("Integrity errors for %s:\n%s", game.long_name, "\n".join(errors)) if game.data.development_state.is_stable: should_stop = True if should_stop: return for game, gd in gd_per_game.items(): path = path_per_game[game] logging.info("Writing %s", game.long_name) new_data = data_writer.write_game_description(gd) data_writer.write_as_split_files(new_data, path) path.with_suffix("").mkdir(parents=True, exist_ok=True) pretty_print.write_human_readable_game(gd, path.with_suffix("")) default_database.write_item_database_for_game(idb_per_game[game], game)
def __init__(self, editor: PresetEditor): super().__init__(editor) item_database = default_database.item_database_for_game(self.game) self._create_split_ammo_widgets(item_database)
def _echoes_format_params( configuration: EchoesConfiguration ) -> Tuple[Dict[str, List[str]], dict]: major_items = configuration.major_items_configuration item_database = default_database.item_database_for_game(configuration.game) template_strings = copy.deepcopy(_ECHOES_TEMPLATE_STRINGS) format_params = {} # Items inventory_changes = [] if has_shuffled_item(major_items, "Progressive Suit"): inventory_changes.append("Progressive Suit") if has_shuffled_item(major_items, "Progressive Grapple"): inventory_changes.append("Progressive Grapple") unified_ammo = configuration.ammo_configuration.items_state[ item_database.ammo["Beam Ammo Expansion"]] if unified_ammo.pickup_count == 0: inventory_changes.append("Split beam ammo") if inventory_changes: template_strings["Items"].append(", ".join(inventory_changes)) # Difficulty if (configuration.varia_suit_damage, configuration.dark_suit_damage) != (6, 1.2): template_strings["Difficulty"].append( "Dark Aether: {:.2f} dmg/s Varia, {:.2f} dmg/s Dark".format( configuration.varia_suit_damage, configuration.dark_suit_damage)) if configuration.energy_per_tank != 100: template_strings["Difficulty"].append( f"Energy Tank: {configuration.energy_per_tank} energy") if configuration.safe_zone.heal_per_second != 1: template_strings["Difficulty"].append( f"Safe Zone: {configuration.safe_zone.heal_per_second:.2f} energy/s" ) if configuration.dangerous_energy_tank: template_strings["Difficulty"].append("1-HP Mode") # Gameplay translator_gates = "Custom" translator_configurations = [ (configuration.translator_configuration.with_vanilla_actual(), "Vanilla (Actual)"), (configuration.translator_configuration.with_vanilla_colors(), "Vanilla (Colors)"), (configuration.translator_configuration.with_full_random(), "Random"), ] for translator_config, name in translator_configurations: if translator_config == configuration.translator_configuration: translator_gates = name break format_params["translator_gates"] = translator_gates format_params["hints"] = "Yes" if configuration.elevators != LayoutElevators.VANILLA: template_strings["Gameplay"].append( f"Elevators: {configuration.elevators.long_name}") # Game Changes missile_launcher_required = True main_pb_required = True for ammo, state in configuration.ammo_configuration.items_state.items(): if ammo.name == "Missile Expansion": missile_launcher_required = state.requires_major_item elif ammo.name == "Power Bomb Expansion": main_pb_required = state.requires_major_item required_messages = [] if missile_launcher_required: required_messages.append("Missiles needs Launcher") if main_pb_required: required_messages.append("Power Bomb needs Main") if required_messages: template_strings["Game Changes"].append(", ".join(required_messages)) qol_changes = [] if configuration.warp_to_start: qol_changes.append("Can warp to start") if configuration.menu_mod: qol_changes.append("Menu Mod included") if configuration.skip_final_bosses: qol_changes.append("Final bosses removed") if qol_changes: template_strings["Game Changes"].append(", ".join(qol_changes)) if not template_strings["Game Changes"]: template_strings.pop("Game Changes") # Sky Temple Keys if configuration.sky_temple_keys.num_keys == LayoutSkyTempleKeyMode.ALL_BOSSES: template_strings["Items"].append("Sky Temple Keys at all bosses") elif configuration.sky_temple_keys.num_keys == LayoutSkyTempleKeyMode.ALL_GUARDIANS: template_strings["Items"].append("Sky Temple Keys at all guardians") else: template_strings["Items"].append( f"{configuration.sky_temple_keys.num_keys} Sky Temple Keys shuffled" ) # Item Model if configuration.pickup_model_style != PickupModelStyle.ALL_VISIBLE: template_strings["Difficulty"].append( f"Pickup: {configuration.pickup_model_style.long_name} " f"({configuration.pickup_model_data_source.long_name})") return template_strings, format_params
def _echoes_format_params(configuration: EchoesConfiguration) -> dict: major_items = configuration.major_items_configuration item_database = default_database.item_database_for_game(configuration.game) format_params = {} # Items unified_ammo = configuration.ammo_configuration.items_state[ item_database.ammo["Beam Ammo Expansion"]] format_params["progressive_suit"] = _bool_to_str( has_shuffled_item(major_items, "Progressive Suit")) format_params["progressive_grapple"] = _bool_to_str( has_shuffled_item(major_items, "Progressive Grapple")) format_params["split_beam_ammo"] = _bool_to_str( unified_ammo.pickup_count == 0) # Difficulty if configuration.varia_suit_damage == 6 and configuration.dark_suit_damage == 1.2: dark_aether_suit_damage = "Normal" else: dark_aether_suit_damage = "Custom" format_params["energy_tank"] = f"{configuration.energy_per_tank} energy" format_params["dangerous_energy_tank"] = _bool_to_str( configuration.dangerous_energy_tank) format_params[ "safe_zone"] = f"{configuration.safe_zone.heal_per_second} energy/s" format_params["dark_aether_suit_damage"] = dark_aether_suit_damage # Gameplay translator_gates = "Custom" translator_configurations = [ (configuration.translator_configuration.with_vanilla_actual(), "Vanilla (Actual)"), (configuration.translator_configuration.with_vanilla_colors(), "Vanilla (Colors)"), (configuration.translator_configuration.with_full_random(), "Random"), ] for translator_config, name in translator_configurations: if translator_config == configuration.translator_configuration: translator_gates = name break format_params["translator_gates"] = translator_gates format_params["elevators"] = configuration.elevators.value format_params["hints"] = "Yes" # Game Changes missile_launcher_required = True main_pb_required = True for ammo, state in configuration.ammo_configuration.items_state.items(): if ammo.name == "Missile Expansion": missile_launcher_required = state.requires_major_item elif ammo.name == "Power Bomb Expansion": main_pb_required = state.requires_major_item format_params["missile_launcher_required"] = _bool_to_str( missile_launcher_required) format_params["main_pb_required"] = _bool_to_str(main_pb_required) format_params["warp_to_start"] = _bool_to_str(configuration.warp_to_start) format_params["generic_patches"] = "Some" format_params["menu_mod"] = _bool_to_str(configuration.menu_mod) format_params["include_final_bosses"] = _bool_to_str( not configuration.skip_final_bosses) # Sky Temple Keys if configuration.sky_temple_keys.num_keys == LayoutSkyTempleKeyMode.ALL_BOSSES: stk_location = "Bosses" elif configuration.sky_temple_keys.num_keys == LayoutSkyTempleKeyMode.ALL_GUARDIANS: stk_location = "Guardians" else: stk_location = "Random" format_params["target"] = "{0} of {0}".format( configuration.sky_temple_keys.num_keys) format_params["location"] = stk_location return format_params
def format_params( self, configuration: BaseConfiguration) -> dict[str, list[str]]: assert isinstance(configuration, EchoesConfiguration) major_items = configuration.major_items_configuration item_database = default_database.item_database_for_game( configuration.game) template_strings = super().format_params(configuration) unified_ammo = configuration.ammo_configuration.items_state[ item_database.ammo["Beam Ammo Expansion"]] # Difficulty if (configuration.varia_suit_damage, configuration.dark_suit_damage) != (6, 1.2): template_strings["Difficulty"].append( "Dark Aether: {:.2f} dmg/s Varia, {:.2f} dmg/s Dark".format( configuration.varia_suit_damage, configuration.dark_suit_damage)) if configuration.energy_per_tank != 100: template_strings["Difficulty"].append( f"Energy Tank: {configuration.energy_per_tank} energy") if configuration.safe_zone.heal_per_second != 1: template_strings["Difficulty"].append( f"Safe Zone: {configuration.safe_zone.heal_per_second:.2f} energy/s" ) extra_message_tree = { "Item Pool": [{ "Progressive Suit": has_shuffled_item(major_items, "Progressive Suit"), "Progressive Grapple": has_shuffled_item(major_items, "Progressive Grapple"), "Split beam ammo": unified_ammo.pickup_count == 0, }], "Difficulty": [ { "1-HP Mode": configuration.dangerous_energy_tank }, ], "Gameplay": [ { f"Translator Gates: {configuration.translator_configuration.description()}": True }, { f"Elevators: {configuration.elevators.description()}": not configuration.elevators.is_vanilla }, ], "Game Changes": [ message_for_required_mains( configuration.ammo_configuration, { "Missiles needs Launcher": "Missile Expansion", "Power Bomb needs Main": "Power Bomb Expansion", }), { "Warp to start": configuration.warp_to_start, "Menu Mod": configuration.menu_mod, "Final bosses removed": configuration.elevators.skip_final_bosses }, *create_beam_configuration_description( configuration.beam_configuration), ] } fill_template_strings_from_tree(template_strings, extra_message_tree) # Sky Temple Keys if configuration.sky_temple_keys == LayoutSkyTempleKeyMode.ALL_BOSSES: template_strings["Item Pool"].append( "Sky Temple Keys at all bosses") elif configuration.sky_temple_keys == LayoutSkyTempleKeyMode.ALL_GUARDIANS: template_strings["Item Pool"].append( "Sky Temple Keys at all guardians") else: template_strings["Item Pool"].append( f"{configuration.sky_temple_keys.num_keys} Sky Temple Keys") return template_strings
def echoes_item_database() -> ItemDatabase: return default_database.item_database_for_game(RandovaniaGame.METROID_PRIME_ECHOES)
def _prime_format_params( configuration: PrimeConfiguration ) -> Tuple[Dict[str, List[str]], dict]: major_items = configuration.major_items_configuration item_database = default_database.item_database_for_game(configuration.game) template_strings = copy.deepcopy(_BASE_TEMPLATE_STRINGS) format_params = {} # Difficulty if configuration.heat_damage != 10.0: template_strings["Difficulty"].append( "Heat Damage: {:.2f} dmg/s".format(configuration.heat_damage)) if configuration.energy_per_tank != 100: template_strings["Difficulty"].append( f"Energy Tank: {configuration.energy_per_tank} energy") # Gameplay if not configuration.elevators.is_vanilla: template_strings["Gameplay"].append( f"Elevators: {configuration.elevators.description()}") # Game Changes missile_launcher_required = True main_pb_required = True for ammo, state in configuration.ammo_configuration.items_state.items(): if ammo.name == "Missile Expansion": missile_launcher_required = state.requires_major_item elif ammo.name == "Power Bomb Expansion": main_pb_required = state.requires_major_item required_messages = [] if missile_launcher_required: required_messages.append("Missiles needs Launcher") if main_pb_required: required_messages.append("Power Bomb needs Main") if configuration.heat_protection_only_varia: required_messages.append("Varia-only heat protection") if configuration.progressive_damage_reduction: required_messages.append("Progressive suit damage reduction") if required_messages: template_strings["Game Changes"].append(", ".join(required_messages)) qol_changes = [] for flag, message in ( (configuration.qol_logical, "Logical QOL"), (configuration.qol_game_breaking, "Game Breaking QOL"), (configuration.qol_minor_cutscenes, "Minor Cutscenes QOL"), (configuration.qol_major_cutscenes, "Major Cutscenes QOL"), (configuration.elevators.skip_final_bosses, "Final bosses removed"), ): if flag: qol_changes.append(message) if qol_changes: template_strings["Game Changes"].append(", ".join(qol_changes)) if not template_strings["Game Changes"]: template_strings.pop("Game Changes") # Artifacts template_strings["Items"].append( f"{configuration.artifacts.num_artifacts} Artifacts shuffled") # Item Model if configuration.pickup_model_style != PickupModelStyle.ALL_VISIBLE: template_strings["Difficulty"].append( f"Pickup: {configuration.pickup_model_style.long_name} " f"({configuration.pickup_model_data_source.long_name})") return template_strings, format_params
def echoes_item_database() -> ItemDatabase: return default_database.item_database_for_game(RandovaniaGame.PRIME2)
def __init__(self): super().__init__() self.setupUi(self) common_qt_lib.set_default_window_icon(self) self.game_description = default_database.game_description_for(RandovaniaGame.METROID_PRIME_CORRUPTION) item_database = default_database.item_database_for_game(RandovaniaGame.METROID_PRIME_CORRUPTION) world_list = self.game_description.world_list self._index_to_combo = {} columns = [] for i in range(2): columns.append(QtWidgets.QVBoxLayout(self.scroll_area_contents)) self.scroll_area_layout.addLayout(columns[-1]) ids_to_merge = [5406397194789083955, # Phaaze 16039522250714156185, 10717625015048596485, 14806081023590793725, ] nodes_to_merge = [] world_count = 0 for i, world in enumerate(world_list.worlds): if world.extra['asset_id'] in ids_to_merge: nodes_to_merge.extend( node for area in world.areas for node in area.nodes if isinstance(node, PickupNode) ) continue group = QtWidgets.QGroupBox(self.scroll_area_contents) group.setTitle(world.name) layout = QtWidgets.QGridLayout(group) area_count = 0 for area in world.areas: for node in area.nodes: if not isinstance(node, PickupNode): continue node_label = QtWidgets.QLabel(world_list.node_name(node), group) layout.addWidget(node_label, area_count, 0) node_combo = QtWidgets.QComboBox(group) _fill_combo(item_database, node_combo) node_combo.currentIndexChanged.connect(self.update_layout_string) layout.addWidget(node_combo, area_count, 1) self._index_to_combo[node.pickup_index] = node_combo area_count += 1 columns[world_count % len(columns)].addWidget(group) world_count += 1 group = QtWidgets.QGroupBox(self.scroll_area_contents) group.setTitle("Seeds") layout = QtWidgets.QGridLayout(group) area_count = 0 for node in nodes_to_merge: if not isinstance(node, PickupNode): continue node_label = QtWidgets.QLabel(world_list.node_name(node), group) layout.addWidget(node_label, area_count, 0) node_combo = QtWidgets.QComboBox(group) _fill_combo(item_database, node_combo) node_combo.currentIndexChanged.connect(self.update_layout_string) layout.addWidget(node_combo, area_count, 1) self._index_to_combo[node.pickup_index] = node_combo area_count += 1 columns[0].addWidget(group) # world_count += 1 self.update_layout_string()