import os import typing import settings import base64 import logging from BaseClasses import Item, Region, Tutorial, ItemClassification from .items import CVCotMItem, FILLER_ITEM_NAMES, ACTION_CARDS, ATTRIBUTE_CARDS, cvcotm_item_info, \ get_item_names_to_ids, get_item_counts from .locations import CVCotMLocation, get_location_names_to_ids, BASE_ID, get_named_locations_data, \ get_location_name_groups from .options import cvcotm_option_groups, CVCotMOptions, SubWeaponShuffle, IronMaidenBehavior, RequiredSkirmishes, \ CompletionGoal, EarlyEscapeItem from .regions import get_region_info, get_all_region_names from .rules import CVCotMRules from .data import iname, lname from .presets import cvcotm_options_presets from worlds.AutoWorld import WebWorld, World from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \ get_start_inventory_data from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \ CVCOTM_VC_US_HASH from .client import CastlevaniaCotMClient class CVCotMSettings(settings.Group): class RomFile(settings.UserFilePath): """File name of the Castlevania CotM US rom""" copy_to = "Castlevania - Circle of the Moon (USA).gba" description = "Castlevania CotM (US) ROM File" md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH] rom_file: RomFile = RomFile(RomFile.copy_to) class CVCotMWeb(WebWorld): theme = "stone" options_presets = cvcotm_options_presets tutorials = [Tutorial( "Multiworld Setup Guide", "A guide to setting up the Archipleago Castlevania: Circle of the Moon randomizer on your computer and " "connecting it to a multiworld.", "English", "setup_en.md", "setup/en", ["Liquid Cat"] )] option_groups = cvcotm_option_groups class CVCotMWorld(World): """ Castlevania: Circle of the Moon is a launch title for the Game Boy Advance and the first of three Castlevania games released for the handheld in the "Metroidvania" format. As Nathan Graves, wielding the Hunter Whip and utilizing the Dual Set-Up System for new possibilities, you must battle your way through Camilla's castle and rescue your master from a demonic ritual to restore the Count's power... """ game = "Castlevania - Circle of the Moon" item_name_groups = { "DSS": ACTION_CARDS.union(ATTRIBUTE_CARDS), "Card": ACTION_CARDS.union(ATTRIBUTE_CARDS), "Action": ACTION_CARDS, "Action Card": ACTION_CARDS, "Attribute": ATTRIBUTE_CARDS, "Attribute Card": ATTRIBUTE_CARDS, "Freeze": {iname.serpent, iname.cockatrice, iname.mercury, iname.mars}, "Freeze Action": {iname.mercury, iname.mars}, "Freeze Attribute": {iname.serpent, iname.cockatrice} } location_name_groups = get_location_name_groups() options_dataclass = CVCotMOptions options: CVCotMOptions settings: typing.ClassVar[CVCotMSettings] origin_region_name = "Catacomb" hint_blacklist = frozenset({lname.ba24}) # The Battle Arena reward, if it's put in, will always be a Last Key. item_name_to_id = {name: cvcotm_item_info[name].code + BASE_ID for name in cvcotm_item_info if cvcotm_item_info[name].code is not None} location_name_to_id = get_location_names_to_ids() # Default values to possibly be updated in generate_early total_last_keys: int = 0 required_last_keys: int = 0 auth: bytearray web = CVCotMWeb() def generate_early(self) -> None: # Generate the player's unique authentication self.auth = bytearray(self.random.getrandbits(8) for _ in range(16)) # If Required Skirmishes are on, force the Required and Available Last Keys to 8 or 9 depending on which option # was chosen. if self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses: self.options.required_last_keys.value = 8 self.options.available_last_keys.value = 8 elif self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena: self.options.required_last_keys.value = 9 self.options.available_last_keys.value = 9 self.total_last_keys = self.options.available_last_keys.value self.required_last_keys = self.options.required_last_keys.value # If there are more Last Keys required than there are Last Keys in total, drop the required Last Keys to # the total Last Keys. if self.required_last_keys > self.total_last_keys: self.required_last_keys = self.total_last_keys logging.warning(f"[{self.player_name}] The Required Last Keys " f"({self.options.required_last_keys.value}) is higher than the Available Last Keys " f"({self.options.available_last_keys.value}). Lowering the required number to: " f"{self.required_last_keys}") self.options.required_last_keys.value = self.required_last_keys # Place the Double or Roc Wing in local_early_items if the Early Escape option is being used. if self.options.early_escape_item == EarlyEscapeItem.option_double: self.multiworld.local_early_items[self.player][iname.double] = 1 elif self.options.early_escape_item == EarlyEscapeItem.option_roc_wing: self.multiworld.local_early_items[self.player][iname.roc_wing] = 1 elif self.options.early_escape_item == EarlyEscapeItem.option_double_or_roc_wing: self.multiworld.local_early_items[self.player][self.random.choice([iname.double, iname.roc_wing])] = 1 def create_regions(self) -> None: # Create every Region object. created_regions = [Region(name, self.player, self.multiworld) for name in get_all_region_names()] # Attach the Regions to the Multiworld. self.multiworld.regions.extend(created_regions) for reg in created_regions: # Add the Entrances to all the Regions. ent_destinations_and_names = get_region_info(reg.name, "entrances") if ent_destinations_and_names is not None: reg.add_exits(ent_destinations_and_names) # Add the Locations to all the Regions. loc_names = get_region_info(reg.name, "locations") if loc_names is None: continue locations_with_ids, locked_pairs = get_named_locations_data(loc_names, self.options) reg.add_locations(locations_with_ids, CVCotMLocation) # Place locked Items on all of their associated Locations. for locked_loc, locked_item in locked_pairs.items(): self.get_location(locked_loc).place_locked_item(self.create_item(locked_item, ItemClassification.progression)) def create_item(self, name: str, force_classification: typing.Optional[ItemClassification] = None) -> Item: if force_classification is not None: classification = force_classification else: classification = cvcotm_item_info[name].default_classification code = cvcotm_item_info[name].code if code is not None: code += BASE_ID created_item = CVCotMItem(name, classification, code, self.player) return created_item def create_items(self) -> None: item_counts = get_item_counts(self) # Set up the items correctly self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item in item_counts[classification] for _ in range(item_counts[classification][item])] def set_rules(self) -> None: # Set all the Entrance and Location rules properly. CVCotMRules(self).set_cvcotm_rules() def generate_output(self, output_directory: str) -> None: # Get out all the Locations that are not Events. Only take the Iron Maiden switch if the Maiden Detonator is in # the item pool. active_locations = [loc for loc in self.multiworld.get_locations(self.player) if loc.address is not None and (loc.name != lname.ct21 or self.options.iron_maiden_behavior == IronMaidenBehavior.option_detonator_in_pool)] # Location data offset_data = get_location_data(self, active_locations) # Sub-weapons if self.options.sub_weapon_shuffle: offset_data.update(shuffle_sub_weapons(self)) # Item drop randomization if self.options.item_drop_randomization: offset_data.update(populate_enemy_drops(self)) # Countdown if self.options.countdown: offset_data.update(get_countdown_flags(self, active_locations)) # Start Inventory start_inventory_data = get_start_inventory_data(self) offset_data.update(start_inventory_data[0]) patch = CVCotMProcedurePatch(player=self.player, player_name=self.player_name) patch_rom(self, patch, offset_data, start_inventory_data[1]) rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" f"{patch.patch_file_ending}") patch.write(rom_path) def fill_slot_data(self) -> dict: return {"death_link": self.options.death_link.value, "iron_maiden_behavior": self.options.iron_maiden_behavior.value, "ignore_cleansing": self.options.ignore_cleansing.value, "skip_tutorials": self.options.skip_tutorials.value, "required_last_keys": self.required_last_keys, "completion_goal": self.options.completion_goal.value} def get_filler_item_name(self) -> str: return self.random.choice(FILLER_ITEM_NAMES) def modify_multidata(self, multidata: typing.Dict[str, typing.Any]): # Put the player's unique authentication in connect_names. multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \ multidata["connect_names"][self.player_name]