 3192799bbf
			
		
	
	3192799bbf
	
	
	
		
			
			* Comment out VC ROM hash usages and clarify that it's unsupported. * Update worlds/cvcotm/docs/en_Castlevania - Circle of the Moon.md Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Update worlds/cvcotm/docs/setup_en.md Co-authored-by: Scipio Wright <scipiowright@gmail.com> --------- Co-authored-by: Scipio Wright <scipiowright@gmail.com>
		
			
				
	
	
		
			223 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			223 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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]
 | |
|         md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_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]
 |