| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | import base64 | 
					
						
							|  |  |  | import copy | 
					
						
							|  |  |  | import itertools | 
					
						
							|  |  |  | import math | 
					
						
							|  |  |  | import os | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  | import settings | 
					
						
							|  |  |  | import typing | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | from enum import IntFlag | 
					
						
							|  |  |  | from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \ | 
					
						
							|  |  |  |     LocationProgressType | 
					
						
							| 
									
										
										
										
											2023-05-20 19:21:39 +02:00
										 |  |  | from Utils import __version__ | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | from Options import AssembleOptions | 
					
						
							|  |  |  | from worlds.AutoWorld import WebWorld, World | 
					
						
							|  |  |  | from Fill import fill_restrictive | 
					
						
							|  |  |  | from worlds.generic.Rules import add_rule, set_rule | 
					
						
							|  |  |  | from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB | 
					
						
							|  |  |  | from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \ | 
					
						
							|  |  |  |     AdventureAutoCollectLocation | 
					
						
							|  |  |  | from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max | 
					
						
							|  |  |  | from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions | 
					
						
							|  |  |  | from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \ | 
					
						
							|  |  |  |     static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \ | 
					
						
							|  |  |  |     rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset | 
					
						
							|  |  |  | from .Regions import create_regions | 
					
						
							|  |  |  | from .Rules import set_rules | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from worlds.LauncherComponents import Component, components, SuffixIdentifier | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Adventure | 
					
						
							|  |  |  | components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn'))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  | class AdventureSettings(settings.Group): | 
					
						
							|  |  |  |     class RomFile(settings.UserFilePath): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         File name of the standard NTSC Adventure rom. | 
					
						
							|  |  |  |         The licensed "The 80 Classic Games" CD-ROM contains this. | 
					
						
							|  |  |  |         It may also have a .a26 extension | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         copy_to = "ADVNTURE.BIN" | 
					
						
							|  |  |  |         description = "Adventure ROM File" | 
					
						
							|  |  |  |         md5s = [AdventureDeltaPatch.hash] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     class RomStart(str): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Set this to false to never autostart a rom (such as after patching) | 
					
						
							|  |  |  |         True for operating system default program for '.a26' | 
					
						
							|  |  |  |         Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld) | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     class RomArgs(str): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Optional, additional args passed into rom_start before the .bin file | 
					
						
							|  |  |  |         For example, this can be used to autoload the connector script in BizHawk | 
					
						
							|  |  |  |         (see BizHawk --lua= option) | 
					
						
							|  |  |  |         Windows example: | 
					
						
							|  |  |  |         rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua" | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     class DisplayMsgs(settings.Bool): | 
					
						
							|  |  |  |         """Set this to true to display item received messages in EmuHawk""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     rom_file: RomFile = RomFile(RomFile.copy_to) | 
					
						
							|  |  |  |     rom_start: typing.Union[RomStart, bool] = True | 
					
						
							|  |  |  |     rom_args: Optional[RomArgs] = " " | 
					
						
							|  |  |  |     display_msgs: typing.Union[DisplayMsgs, bool] = True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | class AdventureWeb(WebWorld): | 
					
						
							| 
									
										
										
										
											2023-05-01 02:03:31 +02:00
										 |  |  |     theme = "dirt" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setup = Tutorial( | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |         "Multiworld Setup Guide", | 
					
						
							|  |  |  |         "A guide to setting up Adventure for MultiWorld.", | 
					
						
							|  |  |  |         "English", | 
					
						
							|  |  |  |         "setup_en.md", | 
					
						
							|  |  |  |         "setup/en", | 
					
						
							|  |  |  |         ["JusticePS"] | 
					
						
							| 
									
										
										
										
											2023-05-01 02:03:31 +02:00
										 |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setup_fr = Tutorial( | 
					
						
							|  |  |  |         "Guide de configuration Multimonde", | 
					
						
							|  |  |  |         "Un guide pour configurer Adventure MultiWorld", | 
					
						
							|  |  |  |         "Français", | 
					
						
							|  |  |  |         "setup_fr.md", | 
					
						
							|  |  |  |         "setup/fr", | 
					
						
							|  |  |  |         ["TheLynk"] | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     tutorials = [setup, setup_fr] | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_item_position_data_start(table_index: int): | 
					
						
							| 
									
										
										
										
											2023-05-20 14:40:51 +02:00
										 |  |  |     item_ram_address = item_ram_addresses[table_index] | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |     return item_position_table + item_ram_address - items_ram_start | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class AdventureWorld(World): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Adventure for the Atari 2600 is an early graphical adventure game. | 
					
						
							|  |  |  |     Find the enchanted chalice and return it to the yellow castle, | 
					
						
							|  |  |  |     using magic items to enter hidden rooms, retrieve out of | 
					
						
							|  |  |  |     reach items, or defeat the three dragons.  Beware the bat | 
					
						
							|  |  |  |     who likes to steal your equipment! | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     game: ClassVar[str] = "Adventure" | 
					
						
							|  |  |  |     web: ClassVar[WebWorld] = AdventureWeb() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions | 
					
						
							| 
									
										
										
										
											2023-07-05 22:39:35 +02:00
										 |  |  |     settings: ClassVar[AdventureSettings] | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |     item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} | 
					
						
							|  |  |  |     location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} | 
					
						
							|  |  |  |     data_version: ClassVar[int] = 1 | 
					
						
							|  |  |  |     required_client_version: Tuple[int, int, int] = (0, 3, 9) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, world: MultiWorld, player: int): | 
					
						
							|  |  |  |         super().__init__(world, player) | 
					
						
							|  |  |  |         self.rom_name: Optional[bytearray] = bytearray("", "utf8" ) | 
					
						
							|  |  |  |         self.dragon_rooms: [int] = [0x14, 0x19, 0x4] | 
					
						
							|  |  |  |         self.dragon_slay_check: Optional[int] = 0 | 
					
						
							|  |  |  |         self.connector_multi_slot: Optional[int] = 0 | 
					
						
							|  |  |  |         self.dragon_rando_type: Optional[int] = 0 | 
					
						
							|  |  |  |         self.yorgle_speed: Optional[int] = 2 | 
					
						
							|  |  |  |         self.yorgle_min_speed: Optional[int] = 2 | 
					
						
							|  |  |  |         self.grundle_speed: Optional[int] = 2 | 
					
						
							|  |  |  |         self.grundle_min_speed: Optional[int] = 2 | 
					
						
							|  |  |  |         self.rhindle_speed: Optional[int] = 3 | 
					
						
							|  |  |  |         self.rhindle_min_speed: Optional[int] = 3 | 
					
						
							|  |  |  |         self.difficulty_switch_a: Optional[int] = 0 | 
					
						
							|  |  |  |         self.difficulty_switch_b: Optional[int] = 0 | 
					
						
							|  |  |  |         self.start_castle: Optional[int] = 0 | 
					
						
							|  |  |  |         # dict of item names -> list of speed deltas | 
					
						
							|  |  |  |         self.dragon_speed_reducer_info: {} = {} | 
					
						
							|  |  |  |         self.created_items: int = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def stage_assert_generate(cls, _multiworld: MultiWorld) -> None: | 
					
						
							|  |  |  |         # don't need rom anymore | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def place_random_dragon(self, dragon_index: int): | 
					
						
							|  |  |  |         region_list = ["Overworld", "YellowCastle", "BlackCastle", "WhiteCastle"] | 
					
						
							|  |  |  |         self.dragon_rooms[dragon_index] = get_random_room_in_regions(region_list, self.multiworld.random) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def generate_early(self) -> None: | 
					
						
							|  |  |  |         self.rom_name = \ | 
					
						
							|  |  |  |             bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] | 
					
						
							|  |  |  |         self.rom_name.extend([0] * (21 - len(self.rom_name))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value | 
					
						
							|  |  |  |         self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value | 
					
						
							|  |  |  |         self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value | 
					
						
							|  |  |  |         self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value | 
					
						
							|  |  |  |         self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value | 
					
						
							|  |  |  |         self.grundle_speed = self.multiworld.grundle_speed[self.player].value | 
					
						
							|  |  |  |         self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value | 
					
						
							|  |  |  |         self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value | 
					
						
							|  |  |  |         self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value | 
					
						
							|  |  |  |         self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value | 
					
						
							|  |  |  |         self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value | 
					
						
							|  |  |  |         self.start_castle = self.multiworld.start_castle[self.player].value | 
					
						
							|  |  |  |         self.created_items = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.dragon_slay_check == 0: | 
					
						
							|  |  |  |             item_table["Sword"].classification = ItemClassification.useful | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             item_table["Sword"].classification = ItemClassification.progression | 
					
						
							|  |  |  |             if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: | 
					
						
							|  |  |  |                 item_table["Right Difficulty Switch"].classification = ItemClassification.progression | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.dragon_rando_type == DragonRandoType.option_shuffle: | 
					
						
							|  |  |  |             self.multiworld.random.shuffle(self.dragon_rooms) | 
					
						
							|  |  |  |         elif self.dragon_rando_type == DragonRandoType.option_overworldplus: | 
					
						
							|  |  |  |             dragon_indices = [0, 1, 2] | 
					
						
							|  |  |  |             overworld_forced_index = self.multiworld.random.choice(dragon_indices) | 
					
						
							|  |  |  |             dragon_indices.remove(overworld_forced_index) | 
					
						
							|  |  |  |             region_list = ["Overworld"] | 
					
						
							|  |  |  |             self.dragon_rooms[overworld_forced_index] = get_random_room_in_regions(region_list, self.multiworld.random) | 
					
						
							|  |  |  |             self.place_random_dragon(dragon_indices[0]) | 
					
						
							|  |  |  |             self.place_random_dragon(dragon_indices[1]) | 
					
						
							|  |  |  |         elif self.dragon_rando_type == DragonRandoType.option_randomized: | 
					
						
							|  |  |  |             self.place_random_dragon(0) | 
					
						
							|  |  |  |             self.place_random_dragon(1) | 
					
						
							|  |  |  |             self.place_random_dragon(2) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_items(self) -> None: | 
					
						
							|  |  |  |         for event in map(self.create_item, event_table): | 
					
						
							|  |  |  |             self.multiworld.itempool.append(event) | 
					
						
							|  |  |  |         exclude = [item for item in self.multiworld.precollected_items[self.player]] | 
					
						
							|  |  |  |         self.created_items = 0 | 
					
						
							|  |  |  |         for item in map(self.create_item, item_table): | 
					
						
							|  |  |  |             if item.code == nothing_item_id: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if item in exclude and item.code <= standard_item_max: | 
					
						
							|  |  |  |                 exclude.remove(item)  # this is destructive. create unique list above | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 if item.code <= standard_item_max: | 
					
						
							|  |  |  |                     self.multiworld.itempool.append(item) | 
					
						
							|  |  |  |                     self.created_items += 1 | 
					
						
							|  |  |  |         num_locations = len(location_table) - 1  # subtract out the chalice location | 
					
						
							|  |  |  |         if self.dragon_slay_check == 0: | 
					
						
							|  |  |  |             num_locations -= 3 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.difficulty_switch_a == DifficultySwitchA.option_hard_with_unlock_item: | 
					
						
							|  |  |  |             self.multiworld.itempool.append(self.create_item("Left Difficulty Switch")) | 
					
						
							|  |  |  |             self.created_items += 1 | 
					
						
							|  |  |  |         if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: | 
					
						
							|  |  |  |             self.multiworld.itempool.append(self.create_item("Right Difficulty Switch")) | 
					
						
							|  |  |  |             self.created_items += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         extra_filler_count = num_locations - self.created_items | 
					
						
							|  |  |  |         self.dragon_speed_reducer_info = {} | 
					
						
							|  |  |  |         # make sure yorgle doesn't take 2 if there's not enough for the others to get at least one | 
					
						
							|  |  |  |         if extra_filler_count <= 4: | 
					
						
							|  |  |  |             extra_filler_count = 1 | 
					
						
							|  |  |  |         self.create_dragon_slow_items(self.yorgle_min_speed, self.yorgle_speed, "Slow Yorgle", extra_filler_count) | 
					
						
							|  |  |  |         extra_filler_count = num_locations - self.created_items | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if extra_filler_count <= 3: | 
					
						
							|  |  |  |             extra_filler_count = 1 | 
					
						
							|  |  |  |         self.create_dragon_slow_items(self.grundle_min_speed, self.grundle_speed, "Slow Grundle", extra_filler_count) | 
					
						
							|  |  |  |         extra_filler_count = num_locations - self.created_items | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.create_dragon_slow_items(self.rhindle_min_speed, self.rhindle_speed, "Slow Rhindle", extra_filler_count) | 
					
						
							|  |  |  |         extra_filler_count = num_locations - self.created_items | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # traps would probably go here, if enabled | 
					
						
							|  |  |  |         freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value | 
					
						
							|  |  |  |         actual_freeincarnates = min(extra_filler_count, freeincarnate_max) | 
					
						
							|  |  |  |         self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)] | 
					
						
							|  |  |  |         self.created_items += actual_freeincarnates | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, maximum_items: int): | 
					
						
							|  |  |  |         if min_speed < speed: | 
					
						
							|  |  |  |             delta = speed - min_speed | 
					
						
							|  |  |  |             if delta > 2 and maximum_items >= 2: | 
					
						
							|  |  |  |                 self.multiworld.itempool.append(self.create_item(item_name)) | 
					
						
							|  |  |  |                 self.multiworld.itempool.append(self.create_item(item_name)) | 
					
						
							|  |  |  |                 speed_with_one = speed - math.floor(delta / 2) | 
					
						
							|  |  |  |                 self.dragon_speed_reducer_info[item_table[item_name].id] = [speed_with_one, min_speed] | 
					
						
							|  |  |  |                 self.created_items += 2 | 
					
						
							|  |  |  |             elif maximum_items >= 1: | 
					
						
							|  |  |  |                 self.multiworld.itempool.append(self.create_item(item_name)) | 
					
						
							|  |  |  |                 self.dragon_speed_reducer_info[item_table[item_name].id] = [min_speed] | 
					
						
							|  |  |  |                 self.created_items += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_regions(self) -> None: | 
					
						
							|  |  |  |         create_regions(self.multiworld, self.player, self.dragon_rooms) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     set_rules = set_rules | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def generate_basic(self) -> None: | 
					
						
							|  |  |  |         self.multiworld.get_location("Chalice Home", self.player).place_locked_item( | 
					
						
							|  |  |  |             self.create_event("Victory", ItemClassification.progression)) | 
					
						
							|  |  |  |         self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def pre_fill(self): | 
					
						
							|  |  |  |         # Place empty items in filler locations here, to limit | 
					
						
							|  |  |  |         # the number of exported empty items and the density of stuff in overworld. | 
					
						
							|  |  |  |         max_location_count = len(location_table) - 1 | 
					
						
							|  |  |  |         if self.dragon_slay_check == 0: | 
					
						
							|  |  |  |             max_location_count -= 3 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         force_empty_item_count = (max_location_count - self.created_items) | 
					
						
							|  |  |  |         if force_empty_item_count <= 0: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         overworld = self.multiworld.get_region("Overworld", self.player) | 
					
						
							|  |  |  |         overworld_locations_copy = overworld.locations.copy() | 
					
						
							|  |  |  |         all_locations = self.multiworld.get_locations(self.player) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-29 09:37:55 -08:00
										 |  |  |         locations_copy = list(all_locations) | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |         for loc in all_locations: | 
					
						
							|  |  |  |             if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT: | 
					
						
							|  |  |  |                 locations_copy.remove(loc) | 
					
						
							|  |  |  |                 if loc in overworld_locations_copy: | 
					
						
							|  |  |  |                     overworld_locations_copy.remove(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # guarantee at least one overworld location, so we can for sure get a key somewhere | 
					
						
							|  |  |  |         # if too much stuff is plando'd though, just let it go | 
					
						
							|  |  |  |         if len(overworld_locations_copy) >= 3: | 
					
						
							|  |  |  |             saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) | 
					
						
							|  |  |  |             locations_copy.remove(saved_overworld_loc) | 
					
						
							|  |  |  |             overworld_locations_copy.remove(saved_overworld_loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # if we have few items, enforce another overworld slot, fill a hard slot, and ensure we have | 
					
						
							|  |  |  |             # at least one hard slot available | 
					
						
							|  |  |  |             if self.created_items < 15: | 
					
						
							|  |  |  |                 hard_locations = [] | 
					
						
							|  |  |  |                 for loc in locations_copy: | 
					
						
							|  |  |  |                     if "Vault" in loc.name or "Credits" in loc.name: | 
					
						
							|  |  |  |                         hard_locations.append(loc) | 
					
						
							|  |  |  |                 force_empty_item_count -= 1 | 
					
						
							|  |  |  |                 loc = self.multiworld.random.choice(hard_locations) | 
					
						
							|  |  |  |                 loc.place_locked_item(self.create_item('nothing')) | 
					
						
							|  |  |  |                 hard_locations.remove(loc) | 
					
						
							|  |  |  |                 locations_copy.remove(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 loc = self.multiworld.random.choice(hard_locations) | 
					
						
							|  |  |  |                 locations_copy.remove(loc) | 
					
						
							|  |  |  |                 hard_locations.remove(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) | 
					
						
							|  |  |  |                 locations_copy.remove(saved_overworld_loc) | 
					
						
							|  |  |  |                 overworld_locations_copy.remove(saved_overworld_loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # if we have very few items, fill another two difficult slots | 
					
						
							|  |  |  |             if self.created_items < 10: | 
					
						
							|  |  |  |                 for i in range(2): | 
					
						
							|  |  |  |                     force_empty_item_count -= 1 | 
					
						
							|  |  |  |                     loc = self.multiworld.random.choice(hard_locations) | 
					
						
							|  |  |  |                     loc.place_locked_item(self.create_item('nothing')) | 
					
						
							|  |  |  |                     hard_locations.remove(loc) | 
					
						
							|  |  |  |                     locations_copy.remove(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # for the absolute minimum number of items, enforce a third overworld slot | 
					
						
							|  |  |  |             if self.created_items <= 7: | 
					
						
							|  |  |  |                 saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) | 
					
						
							|  |  |  |                 locations_copy.remove(saved_overworld_loc) | 
					
						
							|  |  |  |                 overworld_locations_copy.remove(saved_overworld_loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # finally, place nothing items | 
					
						
							|  |  |  |         while force_empty_item_count > 0 and locations_copy: | 
					
						
							|  |  |  |             force_empty_item_count -= 1 | 
					
						
							|  |  |  |             # prefer somewhat to thin out the overworld. | 
					
						
							|  |  |  |             if len(overworld_locations_copy) > 0 and self.multiworld.random.randint(0, 10) < 4: | 
					
						
							|  |  |  |                 loc = self.multiworld.random.choice(overworld_locations_copy) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 loc = self.multiworld.random.choice(locations_copy) | 
					
						
							|  |  |  |             loc.place_locked_item(self.create_item('nothing')) | 
					
						
							|  |  |  |             locations_copy.remove(loc) | 
					
						
							|  |  |  |             if loc in overworld_locations_copy: | 
					
						
							|  |  |  |                 overworld_locations_copy.remove(loc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def place_dragons(self, rom_deltas: {int, int}): | 
					
						
							|  |  |  |         for i in range(3): | 
					
						
							|  |  |  |             table_index = static_first_dragon_index + i | 
					
						
							|  |  |  |             item_position_data_start = get_item_position_data_start(table_index) | 
					
						
							|  |  |  |             rom_deltas[item_position_data_start] = self.dragon_rooms[i] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def set_dragon_speeds(self, rom_deltas: {int, int}): | 
					
						
							|  |  |  |         rom_deltas[yorgle_speed_data_location] = self.yorgle_speed | 
					
						
							|  |  |  |         rom_deltas[grundle_speed_data_location] = self.grundle_speed | 
					
						
							|  |  |  |         rom_deltas[rhindle_speed_data_location] = self.rhindle_speed | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def set_start_castle(self, rom_deltas): | 
					
						
							|  |  |  |         start_castle_value = start_castle_values[self.start_castle] | 
					
						
							|  |  |  |         rom_deltas[start_castle_offset] = start_castle_value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def generate_output(self, output_directory: str) -> None: | 
					
						
							|  |  |  |         rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.bin") | 
					
						
							|  |  |  |         foreign_item_locations: [LocationData] = [] | 
					
						
							|  |  |  |         auto_collect_locations: [AdventureAutoCollectLocation] = [] | 
					
						
							|  |  |  |         local_item_to_location: {int, int} = {} | 
					
						
							|  |  |  |         bat_no_touch_locs: [LocationData] = [] | 
					
						
							|  |  |  |         bat_logic: int = self.multiworld.bat_logic[self.player].value | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             rom_deltas: { int, int } = {} | 
					
						
							|  |  |  |             self.place_dragons(rom_deltas) | 
					
						
							|  |  |  |             self.set_dragon_speeds(rom_deltas) | 
					
						
							|  |  |  |             self.set_start_castle(rom_deltas) | 
					
						
							|  |  |  |             # start and stop indices are offsets in the ROM file, not Adventure ROM addresses (which start at f000) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # This places the local items (I still need to make it easy to inject the offset data) | 
					
						
							|  |  |  |             unplaced_local_items = dict(filter(lambda x: nothing_item_id < x[1].id <= standard_item_max, | 
					
						
							|  |  |  |                                                item_table.items())) | 
					
						
							|  |  |  |             for location in self.multiworld.get_locations(self.player): | 
					
						
							|  |  |  |                 # 'nothing' items, which are autocollected when the room is entered | 
					
						
							|  |  |  |                 if location.item.player == self.player and \ | 
					
						
							|  |  |  |                         location.item.name == "nothing": | 
					
						
							|  |  |  |                     location_data = location_table[location.name] | 
					
						
							|  |  |  |                     auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id, | 
					
						
							|  |  |  |                                                                                location_data.room_id)) | 
					
						
							|  |  |  |                 # standard Adventure items, which are placed in the rom | 
					
						
							|  |  |  |                 elif location.item.player == self.player and \ | 
					
						
							|  |  |  |                         location.item.name != "nothing" and \ | 
					
						
							|  |  |  |                         location.item.code is not None and \ | 
					
						
							|  |  |  |                         location.item.code <= standard_item_max: | 
					
						
							|  |  |  |                     # I need many of the intermediate values here. | 
					
						
							|  |  |  |                     item_table_offset = item_table[location.item.name].table_index * static_item_element_size | 
					
						
							|  |  |  |                     item_ram_address = item_ram_addresses[item_table[location.item.name].table_index] | 
					
						
							|  |  |  |                     item_position_data_start = item_position_table + item_ram_address - items_ram_start | 
					
						
							|  |  |  |                     location_data = location_table[location.name] | 
					
						
							|  |  |  |                     room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player]) | 
					
						
							|  |  |  |                     if location_data.needs_bat_logic and bat_logic == 0x0: | 
					
						
							|  |  |  |                         copied_location = copy.copy(location_data) | 
					
						
							|  |  |  |                         copied_location.local_item = item_ram_address | 
					
						
							|  |  |  |                         bat_no_touch_locs.append(copied_location) | 
					
						
							|  |  |  |                     del unplaced_local_items[location.item.name] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     rom_deltas[item_position_data_start] = location_data.room_id | 
					
						
							|  |  |  |                     rom_deltas[item_position_data_start + 1] = room_x | 
					
						
							|  |  |  |                     rom_deltas[item_position_data_start + 2] = room_y | 
					
						
							|  |  |  |                     local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \ | 
					
						
							|  |  |  |                                                               - base_location_id | 
					
						
							|  |  |  |                 # items from other worlds, and non-standard Adventure items handled by script, like difficulty switches | 
					
						
							|  |  |  |                 elif location.item.code is not None: | 
					
						
							|  |  |  |                     if location.item.code != nothing_item_id: | 
					
						
							|  |  |  |                         location_data = location_table[location.name] | 
					
						
							|  |  |  |                         foreign_item_locations.append(location_data) | 
					
						
							|  |  |  |                         if location_data.needs_bat_logic and bat_logic == 0x0: | 
					
						
							|  |  |  |                             bat_no_touch_locs.append(location_data) | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         location_data = location_table[location.name] | 
					
						
							|  |  |  |                         auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id, | 
					
						
							|  |  |  |                                                                                    location_data.room_id)) | 
					
						
							|  |  |  |             # Adventure items that are in another world get put in an invalid room until needed | 
					
						
							|  |  |  |             for unplaced_item_name, unplaced_item in unplaced_local_items.items(): | 
					
						
							|  |  |  |                 item_position_data_start = get_item_position_data_start(unplaced_item.table_index) | 
					
						
							|  |  |  |                 rom_deltas[item_position_data_start] = 0xff | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if self.multiworld.connector_multi_slot[self.player].value: | 
					
						
							|  |  |  |                 rom_deltas[connector_port_offset] = (self.player & 0xff) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 rom_deltas[connector_port_offset] = 0 | 
					
						
							|  |  |  |         except Exception as e: | 
					
						
							|  |  |  |             raise e | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             patch = AdventureDeltaPatch(os.path.splitext(rom_path)[0] + AdventureDeltaPatch.patch_file_ending, | 
					
						
							|  |  |  |                                         player=self.player, player_name=self.multiworld.player_name[self.player], | 
					
						
							|  |  |  |                                         locations=foreign_item_locations, | 
					
						
							|  |  |  |                                         autocollect=auto_collect_locations, local_item_locations=local_item_to_location, | 
					
						
							|  |  |  |                                         dragon_speed_reducer_info=self.dragon_speed_reducer_info, | 
					
						
							|  |  |  |                                         diff_a_mode=self.difficulty_switch_a, diff_b_mode=self.difficulty_switch_b, | 
					
						
							|  |  |  |                                         bat_logic=bat_logic, bat_no_touch_locations=bat_no_touch_locs, | 
					
						
							|  |  |  |                                         rom_deltas=rom_deltas, | 
					
						
							|  |  |  |                                         seed_name=bytes(self.multiworld.seed_name, encoding="ascii")) | 
					
						
							|  |  |  |             patch.write() | 
					
						
							|  |  |  |         finally: | 
					
						
							|  |  |  |             if os.path.exists(rom_path): | 
					
						
							|  |  |  |                 os.unlink(rom_path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # end of ordered Main.py calls | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_item(self, name: str) -> Item: | 
					
						
							|  |  |  |         item_data: ItemData = item_table.get(name) | 
					
						
							|  |  |  |         return AdventureItem(name, item_data.classification, item_data.id, self.player) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_event(self, name: str, classification: ItemClassification) -> Item: | 
					
						
							|  |  |  |         return AdventureItem(name, classification, None, self.player) |