import os import settings import typing import threading from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, Region, Entrance, \ LocationProgressType from worlds.AutoWorld import WebWorld, World from .Rom import MMBN3DeltaPatch, LocalRom, get_base_rom_path from .Items import MMBN3Item, ItemData, item_table, all_items, item_frequencies, items_by_id, ItemType from .Locations import Location, MMBN3Location, all_locations, location_table, location_data_table, \ always_excluded_locations, jobs from .Options import MMBN3Options from .Regions import regions, RegionName from .Names.ItemName import ItemName from .Names.LocationName import LocationName from worlds.generic.Rules import add_item_rule class MMBN3Settings(settings.Group): class RomFile(settings.UserFilePath): """File name of the MMBN3 Blue US rom""" copy_to = "Mega Man Battle Network 3 - Blue Version (USA).gba" description = "MMBN3 ROM File" md5s = [MMBN3DeltaPatch.hash] rom_file: RomFile = RomFile(RomFile.copy_to) rom_start: bool = True class MMBN3Web(WebWorld): theme = "ice" setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the MegaMan Battle Network 3 Randomizer connected to an Archipelago Multiworld.", "English", "setup_en.md", "setup/en", ["digiholic"] ) tutorials = [setup_en] class MMBN3World(World): """ Play as Lan and MegaMan to stop the evil organization WWW led by the nefarious Dr. Wily in their plans to take over the Net! Collect BattleChips, Customize your Navi, and utilize powerful Style Changes to grow strong enough to take on the greatest threat the Internet has ever faced! """ game = "MegaMan Battle Network 3" options_dataclass = MMBN3Options options: MMBN3Options settings: typing.ClassVar[MMBN3Settings] topology_present = False item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} excluded_locations: typing.List[str] item_frequencies: typing.Dict[str, int] web = MMBN3Web() def generate_early(self) -> None: """ called per player before any items or locations are created. You can set properties on your world here. Already has access to player options and RNG. """ self.item_frequencies = item_frequencies.copy() if self.options.extra_ranks > 0: self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.options.extra_ranks if not self.options.include_jobs: self.excluded_locations = always_excluded_locations + [job.name for job in jobs] else: self.excluded_locations = always_excluded_locations def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done during generate_early or basic as well. """ name_to_region = {} for region_info in regions: region = Region(region_info.name, self.player, self.multiworld) name_to_region[region_info.name] = region for location in region_info.locations: loc = MMBN3Location(self.player, location, self.location_name_to_id.get(location, None), region) if location in self.excluded_locations: loc.progress_type = LocationProgressType.EXCLUDED # Do not place any progression items on WWW Island if region_info.name == RegionName.WWW_Island: add_item_rule(loc, lambda item: not item.advancement) region.locations.append(loc) self.multiworld.regions.append(region) # Regions which contribute to explore score when accessible. explore_score_region_names = ( RegionName.WWW_Island, RegionName.SciLab_Overworld, RegionName.SciLab_Cyberworld, RegionName.Yoka_Overworld, RegionName.Yoka_Cyberworld, RegionName.Beach_Overworld, RegionName.Beach_Cyberworld, RegionName.Undernet, RegionName.Deep_Undernet, RegionName.Secret_Area, ) explore_score_regions = [self.get_region(region_name) for region_name in explore_score_region_names] # Entrances which use explore score in their logic need to register all the explore score regions as indirect # conditions. def register_explore_score_indirect_conditions(entrance): for explore_score_region in explore_score_regions: self.multiworld.register_indirect_condition(explore_score_region, entrance) for region_info in regions: region = name_to_region[region_info.name] for connection in region_info.connections: entrance = region.connect(name_to_region[connection]) # ACDC Pending with Start Randomizer # if connection == RegionName.ACDC_Overworld: # entrance.access_rule = lambda state: state.has(ItemName.Parasol, self.player) if connection == RegionName.SciLab_Overworld: entrance.access_rule = lambda state: state.has(ItemName.SubPET, self.player) if connection == RegionName.Yoka_Overworld: entrance.access_rule = lambda state: state.has(ItemName.Needle, self.player) if connection == RegionName.Beach_Overworld: entrance.access_rule = lambda state: state.has(ItemName.PETCase, self.player) # ACDC Pending with Start Randomizer # if connection == RegionName.ACDC_Cyberworld: # entrance.access_rule = lambda state: state.has(ItemName.CACDCPas, self.player) if connection == RegionName.SciLab_Cyberworld: entrance.access_rule = lambda state: \ state.has(ItemName.CSciPas, self.player) or \ state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Yoka_Cyberworld: entrance.access_rule = lambda state: \ state.has(ItemName.CYokaPas, self.player) or \ ( state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and state.has(ItemName.Press, self.player) ) self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Beach_Cyberworld: entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.register_indirect_condition(self.get_region(RegionName.Yoka_Overworld), entrance) if connection == RegionName.Undernet: entrance.access_rule = lambda state: self.explore_score(state) > 8 and\ state.has(ItemName.Press, self.player) register_explore_score_indirect_conditions(entrance) if connection == RegionName.Secret_Area: entrance.access_rule = lambda state: self.explore_score(state) > 12 and\ state.has(ItemName.Hammer, self.player) register_explore_score_indirect_conditions(entrance) if connection == RegionName.WWW_Island: entrance.access_rule = lambda state:\ state.has(ItemName.Progressive_Undernet_Rank, self.player, 8) def create_items(self) -> None: # First add in all progression and useful items required_items = [] for item in all_items: if item.progression != ItemClassification.filler: freq = self.item_frequencies.get(item.itemName, 1) required_items += [item.itemName for _ in range(freq)] for itemName in required_items: self.multiworld.itempool.append(self.create_item(itemName)) # Then, get a random amount of fillers until we have as many items as we have locations filler_items = [] for item in all_items: if item.progression == ItemClassification.filler: freq = self.item_frequencies.get(item.itemName, 1) filler_items += [item.itemName for _ in range(freq)] remaining = len(all_locations) - len(required_items) for i in range(remaining): filler_item_name = self.random.choice(filler_items) item = self.create_item(filler_item_name) self.multiworld.itempool.append(item) filler_items.remove(filler_item_name) def set_rules(self) -> None: """ called to set access and item rules on locations and entrances. """ # Set WWW ID requirements def has_www_id(state): return state.has(ItemName.WWW_ID, self.player) self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player).access_rule = has_www_id self.multiworld.get_location(LocationName.SciLab_1_WWW_BMD, self.player).access_rule = has_www_id self.multiworld.get_location(LocationName.Yoka_1_WWW_BMD, self.player).access_rule = has_www_id self.multiworld.get_location(LocationName.Undernet_1_WWW_BMD, self.player).access_rule = has_www_id # Set Press Program requirements def has_press(state): return state.has(ItemName.Press, self.player) self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player).access_rule = has_press self.multiworld.get_location(LocationName.Yoka_2_Upper_BMD, self.player).access_rule = has_press self.multiworld.get_location(LocationName.Beach_2_East_BMD, self.player).access_rule = has_press self.multiworld.get_location(LocationName.Hades_South_BMD, self.player).access_rule = has_press self.multiworld.get_location(LocationName.Secret_3_BugFrag_BMD, self.player).access_rule = has_press self.multiworld.get_location(LocationName.Secret_3_Island_BMD, self.player).access_rule = has_press # Set Job additional area access self.multiworld.get_location(LocationName.Please_deliver_this, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \ state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.My_Navi_is_sick, self.player).access_rule =\ lambda state: \ state.has(ItemName.Recov30_star, self.player) self.multiworld.get_location(LocationName.Help_me_with_my_son, self.player).access_rule =\ lambda state:\ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \ state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Transmission_error, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Chip_Prices, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) and \ state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Im_broke, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Rare_chips_for_cheap, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Be_my_boyfriend, self.player).access_rule =\ lambda state: \ state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Will_you_deliver, self.player).access_rule=\ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \ state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Somebody_please_help, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Looking_for_condor, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Help_with_rehab, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Old_Master, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Catching_gang_members, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) and \ state.has(ItemName.Press, self.player) self.multiworld.get_location(LocationName.Please_adopt_a_virus, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Legendary_Tomes, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Undernet, "Region", self.player) and \ state.can_reach(RegionName.Deep_Undernet, "Region", self.player) and \ state.has_all({ItemName.Press, ItemName.Magnum1_A}, self.player) self.multiworld.get_location(LocationName.Legendary_Tomes_Treasure, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \ state.can_reach(LocationName.Legendary_Tomes, "Location", self.player) self.multiworld.get_location(LocationName.Hide_and_seek_First_Child, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Hide_and_seek_Second_Child, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Hide_and_seek_Third_Child, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Hide_and_seek_Fourth_Child, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Hide_and_seek_Completion, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Finding_the_blue_Navi, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Undernet, "Region", self.player) self.multiworld.get_location(LocationName.Give_your_support, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) self.multiworld.get_location(LocationName.Stamp_collecting, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \ state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) and \ state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player) and \ state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) and \ state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player) self.multiworld.get_location(LocationName.Help_with_a_will, self.player).access_rule = \ lambda state: \ state.can_reach(RegionName.ACDC_Overworld, "Region", self.player) and \ state.can_reach(RegionName.ACDC_Cyberworld, "Region", self.player) and \ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player) and \ state.can_reach(RegionName.Beach_Overworld, "Region", self.player) and \ state.can_reach(RegionName.Undernet, "Region", self.player) # Set Trade quests self.multiworld.get_location(LocationName.ACDC_SonicWav_W_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.SonicWav_W, self.player) self.multiworld.get_location(LocationName.ACDC_Bubbler_C_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.Bubbler_C, self.player) self.multiworld.get_location(LocationName.ACDC_Recov120_S_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.Recov120_S, self.player) self.multiworld.get_location(LocationName.SciLab_Shake1_S_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.Shake1_S, self.player) self.multiworld.get_location(LocationName.Yoka_FireSwrd_P_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.FireSwrd_P, self.player) self.multiworld.get_location(LocationName.Hospital_DynaWav_V_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.DynaWave_V, self.player) self.multiworld.get_location(LocationName.Beach_DNN_WideSwrd_C_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.WideSwrd_C, self.player) self.multiworld.get_location(LocationName.Beach_DNN_HoleMetr_H_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.HoleMetr_H, self.player) self.multiworld.get_location(LocationName.Beach_DNN_Shadow_J_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.Shadow_J, self.player) self.multiworld.get_location(LocationName.Hades_GrabBack_K_Trade, self.player).access_rule =\ lambda state: state.has(ItemName.GrabBack_K, self.player) # Set Number Traders # The first 8 are considered cheap enough to grind for in ACDC. Protip: Try grinding in the tank self.multiworld.get_location(LocationName.Numberman_Code_09, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_10, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_11, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_12, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_13, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_14, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_15, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_16, self.player).access_rule = \ lambda state: self.explore_score(state) > 2 self.multiworld.get_location(LocationName.Numberman_Code_17, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_18, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_19, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_20, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_21, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_22, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_23, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_24, self.player).access_rule =\ lambda state: self.explore_score(state) > 4 self.multiworld.get_location(LocationName.Numberman_Code_25, self.player).access_rule =\ lambda state: self.explore_score(state) > 8 self.multiworld.get_location(LocationName.Numberman_Code_26, self.player).access_rule =\ lambda state: self.explore_score(state) > 8 self.multiworld.get_location(LocationName.Numberman_Code_27, self.player).access_rule =\ lambda state: self.explore_score(state) > 8 self.multiworld.get_location(LocationName.Numberman_Code_28, self.player).access_rule =\ lambda state: self.explore_score(state) > 8 self.multiworld.get_location(LocationName.Numberman_Code_29, self.player).access_rule =\ lambda state: self.explore_score(state) > 10 self.multiworld.get_location(LocationName.Numberman_Code_30, self.player).access_rule =\ lambda state: self.explore_score(state) > 10 self.multiworld.get_location(LocationName.Numberman_Code_31, self.player).access_rule =\ lambda state: self.explore_score(state) > 10 def not_undernet(item): return item.code != item_table[ItemName.Progressive_Undernet_Rank].code or item.player != self.player self.multiworld.get_location(LocationName.WWW_1_Central_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_1_East_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_2_East_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_2_Northwest_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_3_East_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_3_North_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_4_Northwest_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_4_Central_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_Wall_BMD, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_Control_Room_1_Screen, self.player).item_rule = not_undernet self.multiworld.get_location(LocationName.WWW_Wilys_Desk, self.player).item_rule = not_undernet # place "Victory" at "Final Boss" and set collection as win condition self.multiworld.get_location(LocationName.Alpha_Defeated, self.player) \ .place_locked_item(self.create_event(ItemName.Victory)) self.multiworld.completion_condition[self.player] = \ lambda state: state.has(ItemName.Victory, self.player) def generate_output(self, output_directory: str) -> None: rompath: str = "" try: world = self.multiworld player = self.player rom = LocalRom(get_base_rom_path()) for location_name in location_table.keys(): location = world.get_location(location_name, player) ap_item = location.item item_id = ap_item.code if item_id is not None: if ap_item.player != player or item_id not in items_by_id: item = ItemData(item_id, ap_item.name, ap_item.classification, ItemType.External) item = item._replace(recipient=self.multiworld.player_name[ap_item.player]) else: item = items_by_id[item_id] location_data = location_data_table[location_name] # print("Placing item "+item.itemName+" at location "+location_data.name) rom.replace_item(location_data, item) if location_data.inject_name: item_name_text = "Item" long_item_text = "" # No item hinting if self.options.trade_quest_hinting == 0: item_name_text = "Check" # Partial item hinting elif self.options.trade_quest_hinting == 1: if item.progression == ItemClassification.progression \ or item.progression == ItemClassification.progression_skip_balancing: item_name_text = "Progress" elif item.progression == ItemClassification.useful \ or item.progression == ItemClassification.trap: item_name_text = "Item" else: item_name_text = "Garbage" if item.recipient == 'Myself': item_name_text = "Your " + item_name_text else: item_name_text = item.recipient + "'s " + item_name_text # Full item hinting else: owners_name = "Your" if item.recipient == 'Myself' else item.recipient + "'s" long_item_text = f"It's {owners_name} \n\"{item.itemName}\"!!" rom.insert_hint_text(location_data, item_name_text, long_item_text) rom.inject_name(world.player_name[player]) rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gba") rom.write_changed_rom() rom.write_to_file(rompath) patch = MMBN3DeltaPatch(os.path.splitext(rompath)[0]+MMBN3DeltaPatch.patch_file_ending, player=player, player_name=world.player_name[player], patched_path=rompath) patch.write() except: raise finally: if os.path.exists(rompath): os.unlink(rompath) @classmethod def stage_assert_generate(cls, multiworld: "MultiWorld") -> None: rom_file = get_base_rom_path() if not os.path.exists(rom_file): raise FileNotFoundError(rom_file) def create_item(self, name: str) -> "Item": item = item_table[name] return MMBN3Item(item.itemName, item.progression, item.code, self.player) def create_event(self, event: str): # while we are at it, we can also add a helper to create events return MMBN3Item(event, ItemClassification.progression, None, self.player) def fill_slot_data(self): return self.options.as_dict("extra_ranks", "include_jobs", "trade_quest_hinting") def explore_score(self, state): """ Determine roughly how much of the game you can explore to make certain checks not restrict much movement """ score = 0 if state.can_reach(RegionName.WWW_Island, "Region", self.player): return 999 if state.can_reach(RegionName.SciLab_Overworld, "Region", self.player): score += 3 if state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player): score += 1 if state.can_reach(RegionName.Yoka_Overworld, "Region", self.player): score += 2 if state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player): score += 1 if state.can_reach(RegionName.Beach_Overworld, "Region", self.player): score += 3 if state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player): score += 1 if state.can_reach(RegionName.Undernet, "Region", self.player): score += 2 if state.can_reach(RegionName.Deep_Undernet, "Region", self.player): score += 1 if state.can_reach(RegionName.Secret_Area, "Region", self.player): score += 1 return score