From 3d5c277c310684969d8cebfd67a495fafb94e024 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 17 Jan 2025 07:39:41 -0600 Subject: [PATCH 01/50] Core: don't log warnings for plando_items and missing lttp options (#3606) * Core: don't log a warning for the "options" that are valid in a game section but not on the options system * don't rebuild a set every loop --- Generate.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Generate.py b/Generate.py index 8a2e72d1ce..d6611b0f8a 100644 --- a/Generate.py +++ b/Generate.py @@ -438,7 +438,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if "linked_options" in weights: weights = roll_linked_options(weights) - valid_keys = set() + valid_keys = {"triggers"} if "triggers" in weights: weights = roll_triggers(weights, weights["triggers"], valid_keys) @@ -497,16 +497,23 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) valid_keys.add(option_key) - for option_key in game_weights: - if option_key in {"triggers", *valid_keys}: - continue - logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers " - f"for player {ret.name}.") + + # TODO remove plando_items after moving it to the options system + valid_keys.add("plando_items") if PlandoOptions.items in plando_options: ret.plando_items = copy.deepcopy(game_weights.get("plando_items", [])) if ret.game == "A Link to the Past": + # TODO there are still more LTTP options not on the options system + valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"} roll_alttp_settings(ret, game_weights) + # log a warning for options within a game section that aren't determined as valid + for option_key in game_weights: + if option_key in valid_keys: + continue + logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers " + f"for player {ret.name}.") + return ret From d218dec82699876cea22c41bdaa63b9bae849922 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 17 Jan 2025 06:41:12 -0700 Subject: [PATCH 02/50] MMBN3: Logic and Bug Fixes, New Checks (#3646) * PMDs now check to make sure you have enough unlockers for all of them before any are in logic, to avoid softlocks * Adds Humor and BlckMnd to the pool and sets logic for Villain and Comedian. Patch not yet updated to remove starting inventory * Adds Serenade as a check * Fixes hide and seek completion to use proper Yoka Zoo map. Updates bsdiff patch to 1.2 * Adds option for excluding Secret Area, and item/location groups for further customization * Update worlds/mmbn3/Locations.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mmbn3/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mmbn3/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mmbn3/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mmbn3/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Replaces can_reach generic with can_reach_region or can_reach_location, where applciable * Unlocker is now a progression item, Excluded Locations is now a Set * Missed a merge marker * Excluded locations is no longer a set since you can't append to a set with += * Excluded locations is now a set again since you apparent can append to a set with |= * Replaces more lists with sets. Fixes wording in option descriptions * Update worlds/mmbn3/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/mmbn3/Items.py | 29 ++++- worlds/mmbn3/Locations.py | 37 +++--- worlds/mmbn3/Names/ItemName.py | 2 + worlds/mmbn3/Names/LocationName.py | 2 + worlds/mmbn3/Options.py | 12 +- worlds/mmbn3/Regions.py | 4 +- worlds/mmbn3/__init__.py | 178 +++++++++++++++----------- worlds/mmbn3/data/bn3-ap-patch.bsdiff | Bin 59914 -> 61276 bytes 8 files changed, 165 insertions(+), 99 deletions(-) diff --git a/worlds/mmbn3/Items.py b/worlds/mmbn3/Items.py index 30ec311ecb..7e3458c913 100644 --- a/worlds/mmbn3/Items.py +++ b/worlds/mmbn3/Items.py @@ -85,7 +85,7 @@ keyItemList: typing.List[ItemData] = [ ] subChipList: typing.List[ItemData] = [ - ItemData(0xB31018, ItemName.Unlocker, ItemClassification.useful, ItemType.SubChip, 117), + ItemData(0xB31018, ItemName.Unlocker, ItemClassification.progression, ItemType.SubChip, 117), ItemData(0xB31019, ItemName.Untrap, ItemClassification.filler, ItemType.SubChip, 115), ItemData(0xB3101A, ItemName.LockEnmy, ItemClassification.filler, ItemType.SubChip, 116), ItemData(0xB3101B, ItemName.MiniEnrg, ItemClassification.filler, ItemType.SubChip, 112), @@ -290,7 +290,9 @@ programList: typing.List[ItemData] = [ ItemData(0xB31099, ItemName.WpnLV_plus_Yellow, ItemClassification.filler, ItemType.Program, 35, ProgramColor.Yellow), ItemData(0xB3109A, ItemName.Press, ItemClassification.progression, ItemType.Program, 20, ProgramColor.White), - ItemData(0xB310B7, ItemName.UnderSht, ItemClassification.useful, ItemType.Program, 30, ProgramColor.White) + ItemData(0xB310B7, ItemName.UnderSht, ItemClassification.useful, ItemType.Program, 30, ProgramColor.White), + ItemData(0xB310E0, ItemName.Humor, ItemClassification.progression, ItemType.Program, 45, ProgramColor.Pink), + ItemData(0xB310E1, ItemName.BlckMnd, ItemClassification.progression, ItemType.Program, 46, ProgramColor.White) ] zennyList: typing.List[ItemData] = [ @@ -338,8 +340,29 @@ item_frequencies: typing.Dict[str, int] = { ItemName.zenny_800z: 2, ItemName.zenny_1000z: 2, ItemName.zenny_1200z: 2, - ItemName.bugfrag_01: 5, + ItemName.bugfrag_01: 10, + ItemName.bugfrag_10: 5 } + +item_groups: typing.Dict[str, typing.Set[str]] = { + "Key Items": {loc.itemName for loc in keyItemList}, + "Subchips": {loc.itemName for loc in subChipList}, + "Programs": {loc.itemName for loc in programList}, + "BattleChips": {loc.itemName for loc in chipList}, + "Zenny": {loc.itemName for loc in zennyList}, + "BugFrags": {loc.itemName for loc in bugFragList}, + "Navi Chips": { + ItemName.Roll_R, ItemName.RollV2_R, ItemName.RollV3_R, ItemName.GutsMan_G, ItemName.GutsManV2_G, + ItemName.GutsManV3_G, ItemName.ProtoMan_B, ItemName.ProtoManV2_B, ItemName.ProtoManV3_B, ItemName.FlashMan_F, + ItemName.FlashManV2_F, ItemName.FlashManV3_F, ItemName.BeastMan_B, ItemName.BeastManV2_B, ItemName.BeastManV3_B, + ItemName.BubblMan_B, ItemName.BubblManV2_B, ItemName.BubblManV3_B, ItemName.DesertMan_D, ItemName.DesertManV2_D, + ItemName.DesertManV3_D, ItemName.PlantMan_P, ItemName.PlantManV2_P, ItemName.PlantManV3_P, ItemName.FlamMan_F, + ItemName.FlamManV2_F, ItemName.FlamManV3_F, ItemName.DrillMan_D, ItemName.DrillManV2_D, ItemName.DrillManV3_D, + ItemName.MetalMan_M, ItemName.MetalManV2_M, ItemName.MetalManV3_M, ItemName.KingMan_K, ItemName.KingManV2_K, + ItemName.KingManV3_K, ItemName.BowlMan_B, ItemName.BowlManV2_B, ItemName.BowlManV3_B + } +} + all_items: typing.List[ItemData] = keyItemList + subChipList + chipList + programList + zennyList + bugFragList item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in all_items} items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in all_items} diff --git a/worlds/mmbn3/Locations.py b/worlds/mmbn3/Locations.py index 0e2a1c51d1..bc16c99a58 100644 --- a/worlds/mmbn3/Locations.py +++ b/worlds/mmbn3/Locations.py @@ -221,7 +221,8 @@ overworlds = [ LocationData(LocationName.Hades_Boat_Dock, 0xb310ab, 0x200024c, 0x10, 0x7519B0, 223, [3]), LocationData(LocationName.WWW_Control_Room_1_Screen, 0xb310ac, 0x200024d, 0x40, 0x7596C4, 222, [3, 4]), LocationData(LocationName.WWW_Wilys_Desk, 0xb310ad, 0x200024d, 0x2, 0x759384, 229, [3]), - LocationData(LocationName.Undernet_4_Pillar_Prog, 0xb310ae, 0x2000161, 0x1, 0x7746C8, 191, [0, 1]) + LocationData(LocationName.Undernet_4_Pillar_Prog, 0xb310ae, 0x2000161, 0x1, 0x7746C8, 191, [0, 1]), + LocationData(LocationName.Serenade, 0xb3110f, 0x2000178, 0x40, 0x7B3C74, 1, [0]) ] jobs = [ @@ -240,7 +241,8 @@ jobs = [ # LocationData(LocationName.Gathering_Data, 0xb310bb, 0x2000300, 0x10, 0x739580, 193, [0]), LocationData(LocationName.Somebody_please_help, 0xb310bc, 0x2000301, 0x4, 0x73A14C, 193, [0]), LocationData(LocationName.Looking_for_condor, 0xb310bd, 0x2000301, 0x2, 0x749444, 203, [0]), - LocationData(LocationName.Help_with_rehab, 0xb310be, 0x2000301, 0x1, 0x762CF0, 192, [3]), + LocationData(LocationName.Help_with_rehab, 0xb310be, 0x2000301, 0x1, 0x762CF0, 192, [0]), + LocationData(LocationName.Help_with_rehab_bonus, 0xb3110e, 0x2000301, 0x1, 0x762CF0, 192, [3]), LocationData(LocationName.Old_Master, 0xb310bf, 0x2000302, 0x80, 0x760E80, 193, [0]), LocationData(LocationName.Catching_gang_members, 0xb310c0, 0x2000302, 0x40, 0x76EAE4, 193, [0]), LocationData(LocationName.Please_adopt_a_virus, 0xb310c1, 0x2000302, 0x20, 0x76A4F4, 193, [0]), @@ -250,7 +252,7 @@ jobs = [ LocationData(LocationName.Hide_and_seek_Second_Child, 0xb310c5, 0x2000188, 0x2, 0x75ADA8, 191, [0]), LocationData(LocationName.Hide_and_seek_Third_Child, 0xb310c6, 0x2000188, 0x1, 0x75B5EC, 191, [0]), LocationData(LocationName.Hide_and_seek_Fourth_Child, 0xb310c7, 0x2000189, 0x80, 0x75BEB0, 191, [0]), - LocationData(LocationName.Hide_and_seek_Completion, 0xb310c8, 0x2000302, 0x8, 0x7406A0, 193, [0]), + LocationData(LocationName.Hide_and_seek_Completion, 0xb310c8, 0x2000302, 0x8, 0x742D40, 193, [0]), LocationData(LocationName.Finding_the_blue_Navi, 0xb310c9, 0x2000302, 0x4, 0x773700, 192, [0]), LocationData(LocationName.Give_your_support, 0xb310ca, 0x2000302, 0x2, 0x752D80, 192, [0]), LocationData(LocationName.Stamp_collecting, 0xb310cb, 0x2000302, 0x1, 0x756074, 193, [0]), @@ -329,10 +331,7 @@ chocolate_shop = [ LocationData(LocationName.Chocolate_Shop_32, 0xb3110d, 0x20001c3, 0x01, 0x73F8FC, 181, [0]), ] -always_excluded_locations = [ - LocationName.Undernet_7_PMD, - LocationName.Undernet_7_Northeast_BMD, - LocationName.Undernet_7_Northwest_BMD, +secret_locations = { LocationName.Secret_1_Northwest_BMD, LocationName.Secret_1_Northeast_BMD, LocationName.Secret_1_South_BMD, @@ -341,19 +340,23 @@ always_excluded_locations = [ LocationName.Secret_2_Island_BMD, LocationName.Secret_3_Island_BMD, LocationName.Secret_3_BugFrag_BMD, - LocationName.Secret_3_South_BMD -] + LocationName.Secret_3_South_BMD, + LocationName.Serenade +} +location_groups: typing.Dict[str, typing.Set[str]] = { + "BMDs": {loc.name for loc in bmds}, + "PMDs": {loc.name for loc in pmds}, + "Jobs": {loc.name for loc in jobs}, + "Number Trader": {loc.name for loc in number_traders}, + "Bugfrag Trader": {loc.name for loc in chocolate_shop}, + "Secret Area": {LocationName.Secret_1_Northwest_BMD, LocationName.Secret_1_Northeast_BMD, + LocationName.Secret_1_South_BMD, LocationName.Secret_2_Upper_BMD, LocationName.Secret_2_Lower_BMD, + LocationName.Secret_2_Island_BMD, LocationName.Secret_3_Island_BMD, + LocationName.Secret_3_BugFrag_BMD, LocationName.Secret_3_South_BMD, LocationName.Serenade}, +} all_locations: typing.List[LocationData] = bmds + pmds + overworlds + jobs + number_traders + chocolate_shop scoutable_locations: typing.List[LocationData] = [loc for loc in all_locations if loc.hint_flag is not None] location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations} location_data_table: typing.Dict[str, LocationData] = {locData.name: locData for locData in all_locations} - - -""" -def setup_locations(world, player: int): - # If we later include options to change what gets added to the random pool, - # this is where they would be changed - return {locData.name: locData.id for locData in all_locations} -""" diff --git a/worlds/mmbn3/Names/ItemName.py b/worlds/mmbn3/Names/ItemName.py index 677eff22b3..af645db90c 100644 --- a/worlds/mmbn3/Names/ItemName.py +++ b/worlds/mmbn3/Names/ItemName.py @@ -173,6 +173,8 @@ class ItemName(): WpnLV_plus_White = "WpnLV+1 (White)" Press = "Press" UnderSht = "UnderSht" + Humor = "Humor" + BlckMnd = "BlckMnd" ## Currency zenny_200z = "200z" diff --git a/worlds/mmbn3/Names/LocationName.py b/worlds/mmbn3/Names/LocationName.py index 36060b12ec..61c64faa9d 100644 --- a/worlds/mmbn3/Names/LocationName.py +++ b/worlds/mmbn3/Names/LocationName.py @@ -210,6 +210,7 @@ class LocationName(): WWW_Control_Room_1_Screen = "WWW Control Room 1 Screen" WWW_Wilys_Desk = "WWW Wily's Desk" Undernet_4_Pillar_Prog = "Undernet 4 Pillar Prog" + Serenade = "Serenade" ## Numberman Codes Numberman_Code_01 = "Numberman Code 01" @@ -261,6 +262,7 @@ class LocationName(): Somebody_please_help = "Job: Somebody, please help!" Looking_for_condor = "Job: Looking for condor" Help_with_rehab = "Job: Help with rehab" + Help_with_rehab_bonus = "Job: Help with rehab bonus" Old_Master = "Job: Old Master" Catching_gang_members = "Job: Catching gang members" Please_adopt_a_virus = "Job: Please adopt a virus!" diff --git a/worlds/mmbn3/Options.py b/worlds/mmbn3/Options.py index 4ed64e3d9d..a127d25dda 100644 --- a/worlds/mmbn3/Options.py +++ b/worlds/mmbn3/Options.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from Options import Choice, Range, DefaultOnToggle, PerGameCommonOptions +from Options import Choice, Range, DefaultOnToggle, Toggle, PerGameCommonOptions class ExtraRanks(Range): @@ -17,10 +17,17 @@ class ExtraRanks(Range): class IncludeJobs(DefaultOnToggle): """ - Whether Jobs can be included in logic. + Whether Jobs can contain progression or useful items. """ display_name = "Include Jobs" + +class IncludeSecretArea(Toggle): + """ + Whether the Secret Area (including Serenade) can contain progression or useful items. + """ + display_name = "Include Secret Area" + # Possible logic options: # - Include Number Trader # - Include Secret Area @@ -46,5 +53,6 @@ class TradeQuestHinting(Choice): class MMBN3Options(PerGameCommonOptions): extra_ranks: ExtraRanks include_jobs: IncludeJobs + include_secret: IncludeSecretArea trade_quest_hinting: TradeQuestHinting \ No newline at end of file diff --git a/worlds/mmbn3/Regions.py b/worlds/mmbn3/Regions.py index 1dc58600cb..286e95a1c2 100644 --- a/worlds/mmbn3/Regions.py +++ b/worlds/mmbn3/Regions.py @@ -135,6 +135,7 @@ regions = [ LocationName.Somebody_please_help, LocationName.Looking_for_condor, LocationName.Help_with_rehab, + LocationName.Help_with_rehab_bonus, LocationName.Old_Master, LocationName.Catching_gang_members, LocationName.Please_adopt_a_virus, @@ -349,6 +350,7 @@ regions = [ LocationName.Secret_2_Upper_BMD, LocationName.Secret_3_Island_BMD, LocationName.Secret_3_South_BMD, - LocationName.Secret_3_BugFrag_BMD + LocationName.Secret_3_BugFrag_BMD, + LocationName.Serenade ]) ] diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index 6d28b101c3..08165a7df6 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -9,14 +9,14 @@ from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, Region, 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 .Items import MMBN3Item, ItemData, item_table, all_items, item_frequencies, items_by_id, ItemType, item_groups from .Locations import Location, MMBN3Location, all_locations, location_table, location_data_table, \ - always_excluded_locations, jobs + secret_locations, jobs, location_groups 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 +from worlds.generic.Rules import add_item_rule, add_rule class MMBN3Settings(settings.Group): @@ -57,12 +57,16 @@ class MMBN3World(World): 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] + excluded_locations: typing.Set[str] item_frequencies: typing.Dict[str, int] + location_name_groups = location_groups + item_name_groups = item_groups + web = MMBN3Web() def generate_early(self) -> None: @@ -74,10 +78,11 @@ class MMBN3World(World): if self.options.extra_ranks > 0: self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.options.extra_ranks + self.excluded_locations = set() + if not self.options.include_secret: + self.excluded_locations |= secret_locations 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 + self.excluded_locations |= {job.name for job in jobs} def create_regions(self) -> None: """ @@ -140,19 +145,19 @@ class MMBN3World(World): 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) + state.can_reach_region(RegionName.SciLab_Overworld, 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.can_reach_region(RegionName.SciLab_Overworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, 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\ @@ -198,122 +203,138 @@ class MMBN3World(World): # 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 + add_rule(self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player), has_www_id) + add_rule(self.multiworld.get_location(LocationName.SciLab_1_WWW_BMD, self.player), has_www_id) + add_rule(self.multiworld.get_location(LocationName.Yoka_1_WWW_BMD, self.player), has_www_id) + add_rule(self.multiworld.get_location(LocationName.Undernet_1_WWW_BMD, self.player), 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 + add_rule(self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player), has_press) + add_rule(self.multiworld.get_location(LocationName.Yoka_2_Upper_BMD, self.player), has_press) + add_rule(self.multiworld.get_location(LocationName.Beach_2_East_BMD, self.player), has_press) + add_rule(self.multiworld.get_location(LocationName.Hades_South_BMD, self.player), has_press) + add_rule(self.multiworld.get_location(LocationName.Secret_3_BugFrag_BMD, self.player), has_press) + add_rule(self.multiworld.get_location(LocationName.Secret_3_Island_BMD, self.player), has_press) + + # Set Purple Mystery Data Unlocker access + def can_unlock(state): return state.can_reach_region(RegionName.SciLab_Overworld, self.player) or \ + state.can_reach_region(RegionName.SciLab_Cyberworld, self.player) or \ + state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) or \ + state.has(ItemName.Unlocker, self.player, 8) # There are 8 PMDs that aren't in one of the above areas + add_rule(self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Beach_1_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Undernet_7_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Mayls_HP_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.SciLab_Dads_Computer_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Zoo_Panda_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Beach_DNN_Security_Panel_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Beach_DNN_Main_Console_PMD, self.player), can_unlock) + add_rule(self.multiworld.get_location(LocationName.Tamakos_HP_PMD, self.player), can_unlock) # 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) + state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \ + state.can_reach_region(RegionName.ACDC_Cyberworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \ + state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) self.multiworld.get_location(LocationName.Transmission_error, self.player).access_rule = \ lambda state: \ - state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) + state.can_reach_region(RegionName.Yoka_Overworld, 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) + state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \ + state.can_reach_region(RegionName.SciLab_Cyberworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \ + state.can_reach_region(RegionName.Yoka_Cyberworld, 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) + state.can_reach_region(RegionName.ACDC_Overworld, 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) + state.can_reach_region(RegionName.Beach_Cyberworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \ + state.can_reach_region(RegionName.Beach_Overworld, self.player) and \ + state.can_reach_region(RegionName.ACDC_Cyberworld, 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) + state.can_reach_region(RegionName.ACDC_Overworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \ + state.can_reach_region(RegionName.Beach_Overworld, self.player) and \ + state.can_reach_region(RegionName.ACDC_Overworld, 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) + state.can_reach_region(RegionName.Beach_Overworld, 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) + state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \ + state.can_reach_region(RegionName.Beach_Overworld, 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.can_reach_region(RegionName.Yoka_Cyberworld, 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) + state.can_reach_region(RegionName.SciLab_Cyberworld, 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.can_reach_region(RegionName.Beach_Overworld, self.player) and \ + state.can_reach_region(RegionName.Undernet, self.player) and \ + state.can_reach_region(RegionName.Deep_Undernet, 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) + state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \ + state.can_reach_location(LocationName.Legendary_Tomes, 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) + state.can_reach_region(RegionName.Yoka_Overworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, 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) + state.can_reach_region(RegionName.Yoka_Overworld, 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) + state.can_reach_region(RegionName.Undernet, 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) + state.can_reach_region(RegionName.Beach_Overworld, 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) + state.can_reach_region(RegionName.Beach_Overworld, self.player) and \ + state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \ + state.can_reach_region(RegionName.SciLab_Cyberworld, self.player) and \ + state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) and \ + state.can_reach_region(RegionName.Beach_Cyberworld, 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) + state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \ + state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \ + state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \ + state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) and \ + state.can_reach_region(RegionName.Beach_Overworld, self.player) and \ + state.can_reach_region(RegionName.Undernet, self.player) # Set Trade quests self.multiworld.get_location(LocationName.ACDC_SonicWav_W_Trade, self.player).access_rule =\ @@ -390,6 +411,11 @@ class MMBN3World(World): self.multiworld.get_location(LocationName.Numberman_Code_31, self.player).access_rule =\ lambda state: self.explore_score(state) > 10 + #miscellaneous locations with extra requirements + add_rule(self.multiworld.get_location(LocationName.Comedian, self.player), + lambda state: state.has(ItemName.Humor, self.player)) + add_rule(self.multiworld.get_location(LocationName.Villain, self.player), + lambda state: state.has(ItemName.BlckMnd, self.player)) 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 @@ -500,24 +526,24 @@ class MMBN3World(World): 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): + if state.can_reach_region(RegionName.WWW_Island, self.player): return 999 - if state.can_reach(RegionName.SciLab_Overworld, "Region", self.player): + if state.can_reach_region(RegionName.SciLab_Overworld, self.player): score += 3 - if state.can_reach(RegionName.SciLab_Cyberworld, "Region", self.player): + if state.can_reach_region(RegionName.SciLab_Cyberworld, self.player): score += 1 - if state.can_reach(RegionName.Yoka_Overworld, "Region", self.player): + if state.can_reach_region(RegionName.Yoka_Overworld, self.player): score += 2 - if state.can_reach(RegionName.Yoka_Cyberworld, "Region", self.player): + if state.can_reach_region(RegionName.Yoka_Cyberworld, self.player): score += 1 - if state.can_reach(RegionName.Beach_Overworld, "Region", self.player): + if state.can_reach_region(RegionName.Beach_Overworld, self.player): score += 3 - if state.can_reach(RegionName.Beach_Cyberworld, "Region", self.player): + if state.can_reach_region(RegionName.Beach_Cyberworld, self.player): score += 1 - if state.can_reach(RegionName.Undernet, "Region", self.player): + if state.can_reach_region(RegionName.Undernet, self.player): score += 2 - if state.can_reach(RegionName.Deep_Undernet, "Region", self.player): + if state.can_reach_region(RegionName.Deep_Undernet, self.player): score += 1 - if state.can_reach(RegionName.Secret_Area, "Region", self.player): + if state.can_reach_region(RegionName.Secret_Area, self.player): score += 1 return score diff --git a/worlds/mmbn3/data/bn3-ap-patch.bsdiff b/worlds/mmbn3/data/bn3-ap-patch.bsdiff index d3548b4c949a459da53701090d5f6fa5f565b7f8..d55fecad80641b372bc7752600f335d6961f9185 100644 GIT binary patch literal 61276 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�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 literal 59914 zcmaI7V{j$R6YzWDb(0+uYc8?(=`|t$M%R{?aquJyX+F zT|G5bzY$Rtla`QRXZ{NX_&>5l^8ae_e?k9eAfjW%!y>Frsj7_?&GHKX%MkeepL~9Q zKmSi(e|HC7Utdc;_Wp0S@wx36X!xxJiJOdtZ#9)=G)D{q4N&HngA&cmkZB47Dia^s z0X`9n5Q~Tq!iDU3_IO!{0H2__qCx;WLOj6A3PMpi5nx4_ov%b17q+-=<6|$iF5*L2 zd0O%KQwc93Kgt-99jpZrC!1L!iyhDEksVr2R3d0>)(Rn1XqFuhkA2MgzpW6#iqKKS z%*spGf2oR!E?NQXh2}uPr+8pzRX01!JF@D+nk}gU_ z1WN<}{yQcB03Gx{FCK)%|IuJ)|E*w$&IX4N`kxtK_1~)hVuYgr09*hb01PA%oQOaf z^k0nGfA(}@_ZS`dKInMrJh#lWZtEu+VgJr48dp^Z1H^=Fi+SY5FTt z&lR6n{Syg-O62$n?r88NNn!^ztRe`)ek8VzNP!dUFG8h`U zP*ho5R21e@FT>IbXC?KJWJAgyZ-e1fvn`2F8uu#9E{>nbdUu4l9>Y~aB#EY#v9a%5 z(R8ps(KuFzm;Al$YB<%fn_mL{NN*~9pFIRRV`UVi` zb?3xw7E8m4Q%{45B~AcAm=#k0)u|F1VDUzM_bf}@eRRKLU{=X_>gErnOo)yM4gNk( zfb;jU9xg??k6fcwq>0!cNuE<;x<+2Zg?MOfmv;)};84abA!mVqWHMB{)f6&WFIi2U zdEpRVRj=;Em8gxE7e^q3k#A+t6`ew zkW52?mo!*0@#3kvRKJqGGy=M;k1<5N23QHSu|79>oX#|_dtbx@i0cvtL~$@;E9Vf! zS!!mKf(RKP7_lDbudQ3{zg#!WiQOhRw_#PZ^=r?azoqZ!v>B)Kamb-iVTsXT0pMX6 zBw^t~r6@uaVALVcpUw}+p)Jb%*|wxJsnE?a`kjh5GGDG~26Y$!Q-NSm1r zmm-&66(G;SQfw0d0zC>S(r|()TFdjx%ay(4i{FLPDKJtnjtgQKSI9aUL{BrV=VK(-7W{P-OoS-6%G0JRkMG4AmE715J#=7-M zwvmn8;+-+aPqj}pdk*%e5Tg{cU-F9bh2`O#<>3NUAvQkw<*5Rqi^j-upxFpTpicx` z^WqfAKRh4NMIXu3M>c8!s>(An+I-3=GdwFqZ~>H|@I~ zlX<%s!r6Jw=xo%Gg3?F+%D3$N{BVIYv0KFn0f}F#a~wbB__L|EOCN^D7M6aBkxj{y zNuFRBL#llOf+WJmpp~FQF-9mut=9vvxxvuyyD{GlQM|r2*jZXw3o|VKrNvJ<*1HxmWR!y;`tc3}vs=womP{Zl zNS|4anOQxsM#M9dad4{QWwW!ISy>nU5M!T$D;zTOVa+5dkwBR{MP#3~Vzn-6K?uUL zG6Ar|rY3@5t@6X!zz}{(%?aSKW}4%IQszp>9y0N)*`yl*@|?khAUQ=cG@LOjGSp0Y zGI>ybg+-LvkdHMpgjhdk)B-zP;(8WVkHR5wV0a}rhhUC`oAK{{S?|@!H}P8OUy!F6 z4_jI4$uAn5L4lme>iU|UfeEHTz44ZrBX6FFNp3EId|yl_0h}Ue?YX;8Zun@+%^K%z z2s@jB-JE${Aj6 zN$e)`JC}@;cY}{@s;hV9_ewS6JhS(EO7%hC8MeEV(Mb~GV<@o@L}rfT0HI9T=vP2w z1(~7yPzyEFo1IRdL>4E#_@{ASO*AH5$;4>$n?}PMdP;k%wgbka-RsszFWksyZVumX z{bAJlx?29c*9P2~hrb>_ht;^M2Hh!Qdd*645jC>W+qCcYkRcA_J(H&thZ!|IDEB)& zp1Xop8Z~Avn+4-wFvPAq-B5XPRAY#q7d<8YYIUeSXqHdO>3+p3f~TI zw}fb>mzoARKc0FcB9QkPE)hK{F`=y5pFfFW&cpcxCMB*Id0MR}W!7*I7N!X5|2f!E zb!zbkdDi!wJodPy#Ah0}Uz)d0>tE7iTep4Y3i8+Oh7#hHkoAO6MXJ1}dHVTHbcL(F z)Z*bMSkM!{Co>`B*d*J(7PZv3DK#c%mHG2Fe=w2CzgatEkE5z0gr8v6#^%KISDz_e zx#=3)!Pb{;Ej=?@u*m(sYIb_fiRodWO`rH61V)Cv>v^6u zqj5`z3uXs9tfD*{kNrv)HC??27t)p|lY&RSZYZ~C_ZU&&%J6w1uC8{1Iy%ezMcIN_YUT1cOV->nZ;E^%AfX#?Pj_wos%{^0&JUs=?roB)?qM= zPu4jJG@7qSJ%FBErT#;U)IXsyu^{5HXfKQJ*gJe~!s$x*dnW=d={7is!~QHUfvsb; z#Cj~wJVI>K%E}7-VJBVi7Mfm)_@Vr^>h{j(Xg!Y zq&WYAV-~>X4B)XM85t&&fGP-A=%7l$UW)d8)1XCn&HF1431b#TpoguFeIYKI6|ylT z(j}Lx@hVu`Cah9(=iv;00k|WMa$-Ti5IZaObMlPBIbpoSWVvTRC2XXzM4>MZJ{EaL z3HU(a`2*b+^=O0AKdK5qZ1qb#Gi~gX@9L5*fi&}coX}}^ zC0-*$q%U8UpL>UkZzl2v$q)~k+D z!+5^3#n>mz!zDOh!jv%M3aArI4k9y-j9+)aZPrY>_+$K2C%Dgzc1=2wpLQG?T5P-ishoYe7!13L{7h4aqx6eI^BN6`N_x{b9O7qa@^XPp3aF0mQ z(8&oB%u;y`k%iu#^+n%``0(G>{LtRfwxEqcCeKE4 z#T{ifOfaaZX=r+Amx>K)dWgfBbCU(3}`ET2~g*x5Ipof@n z5kbxxLKtvLRf&8Fk@;#>=a2L&xO@r<{sBSm=VAjl{kBTk*rw9Z5HjO@c&frOP&4>q z)I7!;7if6YmQ@(hL$bb7lD%2kkN{ylV#>VqdA%V1^iK)Nlt|1s^HL`p`WoP@%!Goz z8D&)b!Yq5CutKM)ugzBh(_LI_^N#SUabh6T#^5mlQ(UxvDmuA%t_v>{yuf>B`4uPAKLCDHDsNQzP?=CKrw?Zv;~F))&W6d zc_}$JrojR&_+zhj`0vt>AMs}h6|cuOE_YTYvm*2$)`?yxAvLHwLxuSfORCO^mAN!Au3`|oZ4ltjM zj*8t-6u_a@;WB^q0t@RzIp!15mH|*vKzkO(!iXJgbwmX9B}KBFVh|QA$;BuK#)1%1 zt(TLiDhA@VdxNMh3ja$a6}B-3d}er;E6b#~{2A>Z>N+N`!jqyKA-B(Xu-0EUQ7RO< z1{zfUBXjtF`TpQaK~+O2^H}K^SqaD?(^9_T$2r`vHz0oDV@SpYJ0s^2wT#TF{z{Oc zHR^~F91>#Y?tp$KG=MXqgeHub5)Dg0gZc|o5cwG7jK_;3Mfse4QFTD<&GJmrp~yPBm<2r^l^NZP)J4 z7HDX=_4{V87o6B>w8EE*`$v+Zt_4Yqr$StBry=JXi#~W>BgGY!^JTPh0fA>S;H^0s zFYWd_+$tq#wBw@iHXuWKmyh35mXmRi_0$_n(*}%&%=pr(aDq~PbD+aXh=i4*pos)V z*Gdq_Y=0>Q#hsr@-0N?$To9l!ceH3JUO{tcTD=4sW+6CjL}`9L1V|}AL%nn-^YJFN8Ign*Zt;| zX96cWXQ#rLfo9<5CFuN7*F1P+`oOVhd;=lebr6b_v`cw7ft5}yM?6Z6ogp#g^DHq@ zgBT@v+L;mhs_lY(D}1uM6BjBv!+p}4h?}!oi`2`;=0sTUC^tzG@nZ3VQu?+ep4^j! zt~*&+@iw+EjQB&uOA`!&{1?yuiyW7J+ZtvYrnE_yrX=w-mM!GjQOO}YA-{T5Qq#~- zeY!iNfcwZ(1*zdUH;AK|FyM#g1V_~heqVKbtrvzzMXGHz`+wY*KeVhsp?nNan1D(!~@d@fUwW%T9c z9GUCj7n3duFFgz|^OEu@jFr5~|5vxWB zuHn`g3C~m!do*bi(D3mEsQ`MP7s#ffjNSCgIka+(n>gRVfW@)GSM@ zBTa60n!jF98kp|TitAJoO-v|w#en%1HzOD&i5s$Epm*R4KfATnw-ciiJ~O%?wz0sn z`6qx!RkC6E6tgydJ(U1YqwL5?YGUNqxR1Ye7Th!_%G|E|t**6p(5f>l*yY&pZ1je0 z8Al1i+hwyAt0wIl4RC4<{LJ6?BWOuH0S5M^qPqPXvJw$W zED4KbdEg2foH>+OIKJ(}@E6?QUWtpQQC>N18{-|_{R)#m$R>$(9)5ZzFX9`6$p5vbDx>wgT%|q#_BxP7 zB98CE$kd45(u(jAA;&sX#?g5j}{pi1_MdT12c$2d#j>Sgwa8pQ|0(Hb#xJ}GZ3 z#h);6h*=cbsGr+|PTzQ&N2SkRh9^=@PDXC4RIz6_5WY0J=XHu5yGQQ3!cHvVZ`1W( zjak`~V6B%H&>s}uUJ%;~w>xW?cU_T!?Yhs1U%E@Gd?A5V$ek{$w0)}rA+}49lZxUT z0#V8>>ZId-YuwqX=A%ChA>sYC03YyQik`%)6?LEXn9hq-Mx6FN@AWFGS+h zEeRLo80maEbDx0LvC9;NT+(Qi;668eb3V#xd3OW0^tr1E!zkVy?G?c;rxxiqfAk*= ziVnEgvUCQh>cW`F%nJf89Hcp4y@tg_VZ|Dx<(-^_E|Nk~ST`D3Dv;@$MLJW?ySbr2 zmDrIjO~2_|qyF&D1lodeCc*KHY+q$Tgh`X0%0M&xzy^p2(J6AT)p~F7!U^&AAHzi` z2U+<<$q-q??8Xa}n7h!@xVuF)m#>!OI58?_s0AG^g8;2h15ZpE6-mTQYMud`ub1{E zh`F4U6$Do~q=2DM;=eDq;z3oSFJC)M9!>(o@ZjkMraxZC3|;HHSy9#UL%fyxgDEoq zAO=dcxt{VA42P9Ybsz-U!$P6C9tmu9Y-YmR!Eo!_>U!4qMzX}!;*a#e_p-}%4z zYPPU%d~@pNi`9RxIfZ)bQj@F*l5b1r`#_aTgF= z@p(x~tTAbs#?9oUD^{9KW4_+=UCMyUYUjfW5zq#t-ySt+r(ta8+0QwqsCUqX{E(|( zt3&E|k68WXEU!*(D-36KvCh zM3$CISj~=X-p5}EA8AOKXvP0J&}4%@XATY2Mw;ljv-wfXZzRZMBs>^ri?2B&>C~C+ zX*ms)7&m*Ag)Uu&UW*>mg~E>z6bF#$I$F*CF){)y$vxTbc0LxY3Gx;aZ~bsPfTN+a zN8?Yir@^DO{PdtOMZHj{&ag$QlpHaf2mnJT{^BvwK-qke5=IO3Mu$QRTACi^)Z{RW zs)xJpES;?!5X)|~)TCT;iTGqrkRMv{kxup!o%RDaA*0}W4h@ferWWu_1t;hW9j(GC z<(Dr%vz~%`^P&8@K6_%M8S>?a9?S}z)GY4~r{WawH%qAzqwFUh&^O79BxtoMY_x6R zsTpt6t0SY{_#FwS^2jo!nPC#0;5gpj&2eM(rh~A;yCVQ}Q-zV!M(~qFU|}f;u*as6 zf5`HNz)a?*i0)^autTTNHsdKCPqRHD@X~jk$Yl0w!j^6~0y&1VpNmRL_Gpj|hbeU+ zl)?o5efyY9fS72~MMsMVUQEH8PcBKP13;80W#Ha zOeojXS%hZAjUS(`K}{FzCQyg1O!_b6B)^sESy;7F!I*aIyTRh1vKuQdfqR)svabbwciG1HEj!9z0GOuotbc&>c#eGuA(+IMFszRc6 z-P#l5yQeo2@>lrNGa!02%S780P4oI<*U+wCOf= zjjrvX*KagKO4^Q5&ee4k>=3B))Vch ziacA7ZmFciiB@aO+bzr0&Jm-+WM+cG@--IY5oxZ(F7h3r_r&;u(Qz8*Q`Wecym)cT zO|^eG=Uc;-D2Ia)Q$~Brx()H+scycS*~r*mwd`7Bgzhkm_}K}!I6nORFpWQ`E*oot zK|v$*?07_{KnY1k`yC7w+ilpgWb7F?kw`c6g9ki0nVLvTNJpD9oV+Y@vXLJyz|ui2r&A|A?9g?W7Q|o9k64cnV7u>48Ef zBEqB!8MS^Ym?v_c&>S3XQ=#8uLyj0xOtO#RDbmMov`h>Kk8m)5tW- ztRmDz1N=xT0b8SamSW)bk$D}J^F|bQY5}6s_n$8A*wkH>X+_4u9*R~Xh+H7$dj@WB zh>E}jHpf)1&1a3=&%GTNI@T^(bFry!)+`_xcfqlk1a;J&_lQt9xww>2>P6SZ*p-OAX%I zB;lY^-zFCeUr(Jnd+i>*-5)>CP>SI-?`08vnT*}(fZo$T|IYU!**@n}VR86%wOi3F z?ZQ_56@5&Sg!UAp+g7EK=#45K!pHZ`JHiO12DB_^fX{ZYEPwnqCA~~0Cuji2`>~vM z8%3|4!}q0yB&oYrV`>(440iex0vpIvPO#+c>3?T6uYm4s$_`sAXo~7A0LbG9VTQ0OWSub)uzkl5lHu*eR4$mLdV%8AgJ!K*`(?G!M|MbTOR|H zSB(q5_WXzbb9tWr*)l~eW58+Dw>30OKRXdnMYgD$#4>So>FP25M=hpy+D>>%q4Sq8 z({r|tMbg33E4`7{FZWULq_JULOW_0Q%(MyKoh&%c_1~+A)?^;>Z85oF3Qz2iGr^}36+-jIYE?B_uSwqEqjCBPrGOjK1~91 zY&S2Jlj%l*tF$#Y@booPk(ms=iF=m_uJP?vQkOqRzpJ4w>mL!li@los-B6OmB* za%`d_eTEo?W03#_mDneq0BiTX!o`M%#rp}72ERAo^6LXL(Oa!L_$q2*;6Iy4G?d$I zS*k0i9#Dsh1opPx^xHg_P*Q)Bijp^c9w+Lj6n?Q#32Zyy;z-7-Oss8v)OB7M``^a- z1oH`+3mzs=z1ZM@;Jo090y@2Fw~n)sPEE;f+Ch+*J9MDzye&bX<)o2;>c;4R7ZS~J{tm1k+pJ}=|uyqcf zqXCe~{P+BI!!@t9P0-S9aCyP98OZ{vlPg^(QE=(`1SR>HwfkOiYeZUzF-v9na)bMX z<$mVc4Qgf5;(N+`(fEdw++4CksxZk52q*=G6Y8_}QmvL|*SYoha4}VuP&%e|taW@~OVtARAx#|}6 zpnyo@Uvv}ba1xA9L&}Stvy6x#n^@=0w;84NY%55}!aBV&E*3sOTaqbxY~19ocEQ$i zz~)wq5*2p_ItOSb|MZhVwmKcD#}M~At{Rl!3I-v&{@B?s`a3W5X2`nWIHAy1mL_6~ zdSFizcj>`XD;Xt+27cQ5M`IY6=FdSV`NB%e*5e?t6f^fg_5$zVipM>&jMCX6yBBNo?bez&X9Jv(urPtNd=5ju&9nzKNzTa1SM32x+?<*uo5SVmMbTb zp+?EyF81nu$9QGpR#&4jHZi#mcYA3#_K?C*nk#Ck>HJ4DIIe|xNRw@Ygs+2;EZGn- z;bn`%c7e86{&J0P@y!xvYQ{l7St6~g-fZw1F;A%cKn{V)7Fm3PQ<;>r#6;Q<=+_Ev-FOLplHVjVW zaW?~BPx|vz#@mjz!=vxP`O_HRO3o|_}__?h&1dN2~AJ@ayz@M&!!FZ)t}uh!_w z-$UGY+PJ@jlJ}Mc+xg((;EJ&Wrc}fQzSBoa$<9aPfra%_{khBFyzMTb!+08BVhcUb zWrcMEoGnvVi!_$O5&2MMmORPmO+uS}U(({sPq#0rap~dwHooL)LyW_&MKR)pO;Dz7 zd5|K*`kf;L!(w4t7R{|c;*f=-&Wxwpa%KYm`l>rt&UO($lH}Q=aS~xQ6d{5+WN7q!L{>z>zEv7c1f7QEQUz< z{F=R@(*$*uda+D|G?lTj#PZLY*~!w~5|y^W6%4JdTA>ov+UaO}f`027c`H}9KiIX- zjL(j*AxzKPR?pRY(?sNkU}yK^v*&1QtNL>Skvh8<& znoEY}B}0#M9f;c8i>+e+v{bG(rPMlLi~H&^g>3cKc4yDtgSz+Q8Lhvxxs-4*)v2)!5k3|Hl=ALuV5ANx-F0#>vGA znf1wmn;F64fu)E-jsXLL=8aS25y3=h4grvTa;RZeV+-gg?EixfUXpVhhSd=yZ{6AU|f--0p z0EUS7A3K@FOeQA>2a2HJ$M{5)0L(GN4%m1}fX4r$cvy|1PtOn)$z*(qtt-!btmMel zzG<>iQ$%TT6#S0l|20|Y@DktDLi1v3N4VCM=EB!VFO z*yjq-0dkb=l<}+x=;#s=a^)e=mEDMV>l@vW1N7?A7QguNw z_+TnjkixfOM67+7v(4zCH|UvB218(xN#bBHrF593!m6QPWUCWy9$6u86yBOVC!@qE zJ~55oL{kDjpBcD_izvR+b~ibqhZ5WwqE@$&!KQ2hU{2K*=N6K z)JJDq@b+`82e9SUo!2jfgh?2|*M^5S0;>&LMotyKZRz2VB&pL}R(*8oa5Dl7G zopY;aS%vid3kbUKDHz^Q^tW?0W#32H?SmCXFAie{4r3g0hehxQ1IC*3Ykuk62D5S< z+kNfkgW)Na{j6kHgnf|nGaTKO+YXR# z_8a}-!1L?Ijhf+C?O!;y9Q5v>orNiRy1aE~fy{$!y3&4TD4%@<%e6;4+ZnGmo zIROP)qp?f63mQjq0Yt7b`-1pqDQJpl@J^sKo+zuC7e{Cy(-?n$t!#RA?iN&s&~h>d zXavOeK&in-KL1nNlx)lKBO*+QEgVe!7j3?axcxxutDMfy?2jXrGKanDr4pwR$`giX zIUVk^y-gj|hTPtS0Eyn`6z)73R|t_+(t^TFowpG!=XSrM5274_{UgL}efrNoT8+O&qRXUd#@w+dNHzODFFhdG4PGeMX4{wW6^$nECeZJQHIW+UICtR@2k4nz`U472^%qx zXr*rs)hd~l0i~~F`@a`D40=vn2Y_!V(=*>Al&y$uV=|~T+?ucvyzvS5SgdiTjf@kg z<-V>Do7WPF%p_5~mUlGYx)?Ih-~MFGKW^w1KmkBf{aJnW27|6Z-uDPlL;27KgXLJoR-2twehwbfJEEE z59h9Jn4V2!I7P-dn6_F=dE-9q5YL*VL;IO}DKf>>}O%21~ox5)^7VRJBNeR8LrnJs776kFldQ%6PkArhNJm0v3y zu*ZB|^!yCawZWGXIZv9Z8JV3!*V)S~d$zBb@RM2#FRiJ|NBw*`J}~1cpekIvhnN<_ zPyC7gVUnWFRxEEEz+QRztLe2z{|t zwbxS)G2|SSj9r!-Q@(eacP~>~%2=d`t%VE<2`Rso%zmec#?oM-Aob;)1C_yLLuvho z-y~&B$T9SIn@Ed2VgnP@kjKAN?7x~>A`E0QMG5sZ95C z)%Y?FKo?)uRoY)SM$=NJs&!lJ=U1?@uPP=tog4`BPyH;63N}Mrh$2q4gJULdQGV|G zHWKS76oq`)54B9p%zv5w-44XQtOwzMcL60l+;Jwf69P^r&MJ|AawSQ+7E!*fjAbuM z*buhAnsUac1cz|W>$MjVtJsw-7hiq+0&@`}Ex2K{{E=Q;v<;i%>>gPbnlaHrdYLwN zMR?p@P=w=J6KbDIGd`R*kv87wMhpSE!A_>Hlb2NRjrIBWrQZ0@&u4zXK<(KnGZpR? z9oo={0XNOdWEyPE4uvu3MH0BQ8!g+>ljD?)lN&Sq@gm2xX zGK7qOIe3;bz&%*tndHWrE$XjEtj!K9G-=syLqyGn*E4^{BNp=Bc{)FB2&@JOV)l96 zELb`)3986uqDP}R*>b#>fY-Mr`V88oRoc^s7On5R9jY3{SXV--wF2xm;YJG58gQ=Z zR@T~~*EO$VqK{0K3<1$^^}|Ua5#l(spBZgF>6HXJVNzaaR&j>k%Kz|p?K9>A)4@G0 z?ZpxS0jJvVgZoAJ3l-n|>23Pt;y&YtqXe1#=KCQGtZOP^_@wo3*P z?^}q+D}1jspUaErXPR8FP(1HD(^wJN-x}5t)}voG9@y(1%k2y)siE9VY%Yuskt2b( zNH)m0ZM9xk)wbT=3kQpA8kCWqT_%WT?AwU^^x*LCo~Qt5dd41by)gFv4^%SP-WfTPBuCV8YA318f#-!f68Bq7mHf)(AE#BEJXt%rxi1eF44z%g| zH^P;|p`?&jwxLLBobFw+m$eur}G!5=Ikiv>)#_Y^$ ziargis={49)Q1T*MQ)>#yglDMRp6{xDg6ZPe)81h`t<_eUz!!pmzk0sDtFC9k_M&h zQlMf#iz2>$bpHlhpo`d)XJ^}-xlT|t5OXK5#@=!=`FXTIzV+(cdfUBCkv73U>p-8! zvB7qKJ$^d!DrY%psXo5+FwC_Po*ZLqY12chJ_!Hvo z>bt(J<7d~Wk0yAdqCuV!x3m55i_`1?+^{5W3kchk|F8bgpZxP5jKJ3yApJHq!+s(XFvhnV1?Dt5?kB{>wX zq>lh&>Qs_$dk~D|C>SHlon<&*Nyt z#4FoGic3fMhkQ$h=NNb@DlbeN)WS3XjxBpR^FQeo&-p3X+rSXX469sK8ms!lXWT-b zg_FB7a~zTp%RgcZN~j7LE$$59#K8l+=rN~lYF(6QwN z?OXX8Z<3q1zkTU0~T2U=gHOOQwO4sOHnj^l5Yuf#Hlr6V1d|RIc_-umNlvx>2 zX-7`x7ZB2wKu7gA$>mG}hsyKi#xnEy(Z!^^v-UF-&;rzdwWgI-Cp$j}9>lGa@HTZV z=}J!A(nq`K*pu++aNH$znuAs%xv(K|U1_B)AzKuBjt}sB=})8*r;D9ggViBIRX{eURNn2Qqo>EHf#ndG(;#xNns6@blThz0?RoWdS_6v1U1>4}nQKv>=XCqNhc=uiM4->LDeBzOpP@ ztFkxgk2g=G(v~+yTN9QMI7?Mr88i5Ki+HrvD|q6g`ggBD(f6!q-@v&QxX~-HMCa*> zl|^^Q?|C$U_Ny&o0FpbtRgvsv!|abbzrawuj2aF`CaxCR6|<$hu)~VMq^=eYzE~IK z?Tt>dVJXp`6FrMsNr*k*YZ+$E)?h}UD>k4x1bi21@fReUbUcJl00={fyOS7|M|oWw%ePp4eEI8`|*{ zEdR;-ABtGZF2EF|@7__$aP1l5fTuyV@J0o`nuoOK{c}1eys<=RzTPNY!3OI(q!c-% z8(^|`{Xu8G*CP5xM+W=*eVV{U)nSLPBi+E4srM$9O`v7u*w*C@zc#wb(A7WId^PrT zb?oba6zuroVe|KPJ;#|!sO7{rUGYBzC;$&JRi$U9dIL5V7CI2&Q*}NP5=nApHDl>b zVz(3c9X3UPY+IGZ?a7W8NiSHI3-$2*&rxM!S>HaQkaRR`QJm8|W?sC(fNz4XLn*z;3x;f5cr$Ysqvxm1`V%kTJl=MD! zJr2CMtq^{F7Kt`R!WRPuc$Y+k1`b)f)MMrQ)ucn7K(^aiiB+65*g_jLp}cR`@o^YG zn^Fkcg15A^aY9$uadO<=;!+3ZNxT};U#jXEPSTFi65scq8D>a1g%l4SE2d{xg~Q`8 zZIKqf-7i8f|C#|LzUx|9|G+SvM7rq#GJ>4Q%YbX((6;C6?+qzi2U}2g->sBx={vcm zmhv17)QuUULz}UMBHYN(x8MntgA=un=uzXZungZ+s|INGTw`gYWXL+?X4ewx4i(>_ zjp9YDrP~fjL7vUu*xtX}M#%%L@-q=rNmOU`LvCe1d7uU@&*i(im117}GkEc!+X)2= z<2C-l5UXW+Dt?U{UM$ej_5@OSugu-GDz|Axcf96?d3Hp_++3;Z7=6@vp)|DESqa6r z4a#Is2K0?P)zMfStq(R`Z&7_$D>}>*xG1 zKG1BjH;vP!MTs|dJLO;e%)y_UWkMoZa_o!fsAw#h)W|)M7gApK^51iv`Oh4a^Kg$s zExT@(u}LwWv5UJE!BIO4!e{^-czCi>EqkVc14~+7JYlE(e7|la8~$))J|^U}dvKPI zv_+mg@BfYQEoqFW6c7c{3S?Uj@oYx%1x+zO)AVscL@f32ednYNeJx@W^^Zkve4>Kz#JacK%JWmj3lqhaUu9~aH z8GTa7a;L_G(F7e=%Zg zW{@!s8rCpo@)VzU3wKm?>*cj#A-z)Fsf1H;ySM?HGhEgWn@B73Ukr)BEa)*!6mT^_ zkF$4Ad*O+0Y9tbQRmL6xzYYt9{v*X0?%RbK(;L!LTEQWo_p*FC^Syf9V@4M0(5kmI zpx5e)MGb!Zvi8amPg0ocN;Qs^n67reQ?01k85uWzji4o#Wj^>l(z9X_8)O6o$V;cc zHNc`XAV9rNx;is=GU!Inz^h}Y{{mNyjujW)d_TQ`I322iW=I^Va^j5I23ZN8OMA}L zTrV>s@(7=jp$rK2)dkQuZP?0D58*DxcH>Qv(d>^k`Brdcl`tR)RUB>_TP>|Ab6d^wA(wReE8yUuC3W+e53u}5LNtf@9l0GwC}=# zO{N!l2F9ZCn)Zgc;}nU_9a&tbTRPH`_o96t+d>byz|_*h6t1|WzRQETlo#?OGZE0s ziR`U-P5z7K+9vkqYR@u>}E;WNPkPf$J5mte%iIop2$W^P4ic7wl4u_p$`MVmkXqXKW^)y z7MMs?{CGZwirR(xq)uA!eYZmY{1QNh`t{pzJdz$E?$5#UwORpu&JQxBmY*4xd`rzN zY4!DY*yi?!KpsbwUcPZI3DJ6JbMWr?B(1ZzcxOXrR5eX(|Eb%yYJ1%;jC~QZOpAQ%55?o zpK@6c#iAb_uG8H>_e5`<6J4sY21A}ef9LB)^@vqQd(KK$YW?axoY>N>fiAZjiAM-k zYUHz=2%-QF01o9Ft_yo+pTIo*N3KMDM|H6&n!jS;*Ao&HRh$Qigak-q0LXa!0je5n z9mlh7Jd>vN2MxpxVb~i2DwRZ$10zamCSZt=P804^prJ^)4Om%3;d7wjCw})E)d{C! z0?Tt$1GaCHSRh#iTy}YWaHB&K5`%6Kp|Tj>FO8VMztcr8(jd=#HCUAG*Fd*0I7KaE z01~|z&Y2}!jJ8mNJYz%8qsYuVz~&hQP#Vl#bZ}}oA*mee8GA9HYiUAst5>7otLkGB z)|q5VI=Q)ALiy!Z!suj@e~-^sJZppy-;F1E-=c}GE=ZT$tU2W~97mf)Va6n!j&DEB95xl>S z(1mtX5L#adY}o2gkBD@^ighF6bq>o(RINkMHc8q^YBpora4NsN$)*_};@Qr?a6EXee|2H^*uKg4}hb z8RiEjZ|kaJN_2ooG!g|dFR%FMDoh12G^t6ri^~l zHCZjyt%BJM76*BuqL|qdOO4D1i;oi`nk@>`ykZEQ#tKXBwwsZmjRVQaf77pR_W!Mw zLLAastM(JVn_Vr-T|{on!fCah%ldujt%*t#R>7Mk84_LYaSOjPJXVl!AJ8AIfc6t> z`4RyMF2I8rf83N2h0J%e%$huN-9AW&l%Cm=q~O*^eIs+|1WJ3&Xh+gX`I%D=U3(y{ zaw^QViS-lU=6t?=OjSlJyagN;w8Ii005j{QM9Y6}IoFMc`lV%GnJO-?L*W>oH_%Pt z@%WO&KW2s;;!MbfK8pAuvAg9m0+#E`&S`clgxmpzr|w1{|28xf1k(M3sn_skBbwLR z$1E7};mQZ)FqfE#+_pKwP6}X&UURQi4e+Aa`20{R8vDmC^JIm7N(|M|JP}L26 zbZvy2xF5`aj~bZU$pzY#G6GEuc|>D5t}3W0X|#BQfmm{7H0B6XvCDX>KaA1O+O?b! z{F`3P7~WO0` zD@5Wpqwv(5F+erV{K}S?GjP}C9=?T=haV+Gu5x3Czh{d}Fhqr#>LWf<4lw1;3=@VDp0 zdHG$XEM9CjobNGnrB9>qw*M~+o{fAK{`aKgC5roO%_++~m>DB!C*qQ4!5hnqWuIo^ z*k8L)cGaDoP{#KF0+%6Iwy<8^{Kgv#muTZgGhb;8hmg72?$BS!Ns7gRv8Ns?>;$R; z0SKJFVI!bwG)pW80bhU;%d9&x7W;f8$q=0Z`c5Idh&D#Qn;knM8Y{LkC7v$p5qTdq zAN`cf>%0Sg8^g-%vTQw3PmRlkRXgWx7B|<>)Qbx|wn2>e#<0%nCfnrw--Dy|ISngf zE&ggaErPK0t~#NVgL&XjR2~Q8=<4XzRQaVU+y5y#F2!|mCjP@QDeW4zgi>2af~V@|_qa-%q~_H=R?R>GLsBX?g@e zyTg^JdGXL3hT=_o``IX!`(Dll2imgqizH7`$^YK&PU2BEFsnf5gs1gU4C9Q8ZTCdb zd)o=a&Eozo7PY>R9UbtS7+UTB@~2Ow`jsJ4O7dt>AEj=4wggp3O`vV8GqC#%0LktY zo!>nycjE7C?m*jth{ih#*>#5@40mna#6Yt^anA?+Rl>6)@X-q&VPUn$a1oKWrKG`+ zkqR^xAQylT)QEy?l}x8{;a&6fx&y}z#0JbWo<>S&XE=Tn*;|XT3#1{DEVZ$FG+o}A zQ2L+42tY@D6$Cp$H&bodRkU7et+R-QEPxZzxwa9~D0_!vFU=O&Jqi8+KuctKnmWVa#=NI**H_Z*RE0(=$-JN;}kp~qz31mi_XuM z4YcMSbAfl=7~w|pXbNnBn`jBwqHh^@O0L~LTYt5>D6?#7=jU*i*E?o7E3^^G(|=S1 zeQvc6d=e~$ji9Y5h&Fk4#>j+rKbzQFJu=5t7Az<_G_T*#RG_-UIy2IelcoAHq|QrN zRYInx=>PKaX&>%tA^-bRyWosj01;7-Y-r{uHk!6xQY2$0tSO#UwCSlbmBp8E^Y30y z4z`^QJ+rj1myt)_8ufng>a>tY=Hml5cE$H#!K&IYEPg&uT9N?nt9kPS7fW7i6ynE! z+f%C_p4H_?N@2v_$2ISMUN6GKk@{PPGm!n-BeJ5|MVpFW(VM7pnZ_ipGE5>}1)74|=CWUgdFQaxR1G_?M!tUcQR8qZ;y zvKzZlY2j||!X5{#pRh8BJivI}&NXeNkO6g^qGa}qIz-Hg{&Xf&mBsI~cd3>)aAl(7 z`LIcV!v?8%UDi$KoQ*Ih7YybRJXvlG>ODGxGLilnOU^UK;-++z+6}dXr;YL5kHyBB zRI(ANkW$lqiz7IO5M1T{*pK0AL<`nybjg=%;e*KPk?TCU-SG0|+clW>InG226hIKY5SFtX*<&7B4zWPn~Ct zdtp)$bix>)!BB>#5yUS-D>_IZ6+gp)q_OV{v8Vn0mH9d9q1N`KFDGpz}_#yHjB zsyTn2UA}F>jlS+J^o|597MDbgW(T9;gU3pZx(8#}E4k^5bmn?9YblQ#?u9G0(r;ZO z=cni(G1V>>loRVdR~D5~fP3p(wJK#;x`1r7#%P6p!Qv4`7?-QBWx8+DFOkYKbi-UT zy|uftl26YH65V$f=aqmfg(@r#-T_83MqPx_(lXhgIN;kwKFO(yniF{U0|{rpYL z;uG9$4`XYyx{~^E5@5y51i+F!$!$dY0yqxtyOKT+`8J7BXwfZ5^?VamXL}e}(n|{B zEosvhy(h^3I>`35_54>HT4S{xyDk}EXp_*dGt z=`O$mv6H^1%Srz6$L{_dOO3_76k`AP&~);jAV^@6 zB9mNr58=K6UU!u$1p_S9GbMRW=Gac}#uM0_TDaBhC>_gSts4;=6NH##WIt))uOUt+ zUC7E+%6W>_^_4FmdExhT4D~!8+ORH;9yXBVKTl<{(Scl@8P~2;z>mf7oUrCfly+P@ zT)z?bi#hUdM*z4H_5A9eONd;TfYM2f^)WtI;?J_SY~3!tf>p=C@1E&2 zC8mChJS8^WK{xy5Ev?Zrrwx^~Jsr2kpxNw7Ys+gdR8Z1?H&-83%opAbuU6r%r73^z zd5zNxY_kBmrXlXD<;p~`*Lp#f0p$Dg^-%LWXwFOA`1Tl~9#m$(eh zX5?1k6kBT`5<<1Cv~d&|!EnXpBUg{})=T0?wCJw)G8Vj6AyCKY<#`z`hp4l?9mnS+ z?dxx9i{H%X2%*fy-b9>V1leQkt9IpsdlpxuHlV^-rF55!iux_M8}4yCv(CiB)Be}- zo?N+Us_`a-vqtA_U>XCsHxx?ZW^}RLbK6#ZRlI(+GdjLQBaR zRz6X7cH+(Y$G5Ky`+#&EPH7jz)!=7>Ed}VcWE$7lt!dyB{(fU~i0`><=;@j#q@+gy zPfoiNA&LIhQqb^DE6;S+|Jh$RU&irI^C6My^iSj4I$Y$jwLx|oZjMWIx?o*g?`xTJ zxtQ-}B4fE%`9`9w<5~OtEwgkDLu}l6Ho58)3iB5aG9TN1I}GMO6Q6AqwPntc`hVzS zk@8W=|0%_177d~d_Vv;8ojw~-ndavG+BS?19kyTob)x4DhsF=KjQ;^5lrf4T}%%*p%<&XKli#Pdb+4O!rAR@ZXwyhy8LF2+z zTOp2HwqYxQ*kfjGb{89C>}X@GUiL>L=v8%N>V1zWG4Hp0^qe(&6vmqFkfFfiwK5AvARC6DuKFA$USNQ_JIkQ- zqa{HwcrPgZn;r8%8`g7_mpm0hI)Sd7q=;4ST-f2cxF zCl`bGGfm2{`mTGH^J2!aQTPh{dO>+PAtS@wxhv_|qB|nxQg5%yNrLfILb!lBjvx{{b40V3eek57kD9WD5Jhofq0<;{bMHUmPhWshk7CD z2fk#;LObiY3^|oBbaSE|SxNeux{tD(j&~7KJQ8V&c)eK9rw;9Md;>S9O{p0Ipnlr_ zgV(yr*fU*e8Th7@_*!fc5$KR}IFpKm>tIW7 zD1R(@3q@6H4C&TeFhCWKIP%Tm9`jpCR)(ua#kL~-buExq{l$=U>yE{DaF$|c;?Z@$ zD$D&^HYUg1pIEh}$Igl;fDVaw(NPm8~?5}y4 zeZNf4QIz!v)vABb{v9DFOX%RK_vlVEuffy7{^V5nW{d6^k?e+huC3X)c4VPhpKDO~ z-ggt(PiCSx%^Erv+4KyXp~u}k8O(kEtG@S?n<;3;%J1YiWZq3FU;(<=EwA3m5kjNB z)!IB;^9JLr)H3gM(}bclz^B~z2mUruw$iqT93&)VJkuz^yVK99nQFY2slJGugA-d$ zjSo30HhEVQ-^O%AF0Kz%s9F4@7lgMHZ8cP{QX7J+s9b-iLwnFb{r`p1vUw-;jwlX% z<1pU;?_W+UwQ_+kX&Bsg{M~qcQomy#mrF|Khl_6k@16Dj+^Gv&3RnGhHU|I)xvVa% zce*ODvO|v$;fvI9?crfh;m2+M4$JOw-}zm%@53W>2HN0x0`UKta%rm#R!bQwJ0Lrm zihOM^@K6y6oSdqDjjhL(2(`##oxgE$iL-Wup~%(RtzPL$H5!`sZ*L*#GnN0O7}V-` zxBNOTt4Sti*5enFitc%EOb!7_n1xlp7J- zKkY2802d0{*c%iSN&5V{ifyrO)F(-~axs}b{t3g*hEXe$@^`@bakKh6)p)NoZ4|QC zSAozp24eL?7nKcXqqjv;yIe$u@<0ILmR~1?fbU#TXW$5Ph|Tgu=KGN6En|ESEg?`6 zd~-=RgPBHp+uq+x8xfrsFhfuwO3&ym?`5KZaD81w3E7n+e_htRTJ1;`f(ME&p14ac zhv-I?yjodIK}+bRWS}Uo`RLW!rJ3T4tTXj2K7V5S#~WRRzp$j>LadU{|9s%SV0AZ( zcFk$Xj`mi@-V;fyv(v9?L?Bau^=jb_onk~=~!dFBz?!?p19rO6+ z8?0bCjEt^oA4jttnlFkL(cX-q_sL^Yfe=k;N+TJ2ppa7BWf=X!iSRLQP_rC$7+{E1 z4z61uvrGlUXHkhDdr$H&;g*?!S2G_2?W)D zTwq|xpmmKT!*o7Epylq7`Yi(}h6Zo>c>%!ow8xx@(M8&r?gFpF+v0R!aR+QaMADeL z$V3IJ6temY_p#?0D3CV0LJTt}1nOhhBn*C6Z0fXg)pr;*W0^d3!+l~|nIND#(lc>S zEg&=31>IM7g*GGtzUcB4Q2eTW|IJsU(M7hd@^4BA{P6Q5{XPd4^+ufd0j43HvXmt( z*e;n=NZZofMRftg{v(>WaJzj(79Amp{oG^s!uhj4g(a~b5C5MWR&_lerq+|UArHM^ zMhnuqnpuOuP&KXbn`B^M-5GehD>|yJd^cqt44A?bMNbm_zMZw>=ZJu^pLS~72SaYv z80PZ_blOJP!w_8>V99s6!a&vuEn~A4GXx6;$Y!^Wn0H2WH1$1j%3G)cce?2@W*=l5 zk042IoJfY)f~>xYP@_Y|9TMATnkihQXTv+nAZln&VQxvoY~zs1?R9<0gf;S;36}sK z4viM&KslUvpudMHJG3)G(y(F2?{pj2_|fF{UD)-3!ZnvPOj|+}Ks~e^qGC8WFro=E z{jRVLOSrJnh{$qJ*zxCQhTY5>*GV>E>6@M_T^po07BAXEX^=#!L(Tb+LeK0IrSp}c zF(1@kB`h;j!hgdS&4{38rf6`6b%n}v8K1z)qn14guTD{zC%GGz!y-GS=}yPR&a!V8 zgAPa80V@1gh@6kN86->sR3D`OFJK=RNrzCG4Lh?{TMJlKA1ha9WNh86cU65AE~4nM zC0<~Tm5K5Q$Ld~0c#?V=>lDXuAHbyEb&O)^<{}hC}Gm@O_ zcXbq?jk}>~we@33&qS#Ie3c7|Dij%?@6-sHYeb@qS6v=E^)-Zndk4Elbl+ zXnQa%Y>Fp<*DI8h0fatOz5!xN_(AdSK}K}}uJY9dZod_g$`OFH=%2prXVcL49_w6d zf+5uDqs!;uHIHo||QcYNf8|VZm4@jxgQ$nQ7A}}(0&tE^7jWCO^d)--5;|QB;r?1 z#@e&fOfN;% z#i7%znZ`6ih6RlzS}!4Bns;>1OpuDExL}jZ*gY~HZ+zQ>X0V$<%3r7Jxw`o}8xHpi3U0-x%?SV)cJpL(BAEHmC8M<^S$qA=#@%_a zJjD+cNtULeKS>np;2Y<#`z8M`4T)cN~ne%PR`aO z8v|cD;d=gbqdCA}1OWMV#P_vZ(Oh%&jGnlf^GXSG3k*R>;HLNJz4fe9(opvg+)q;> zmChFt_p6nLtYzn%%l?=Cc6Ohg(y|U3ixk|FO&l&W1N|r1SGdMGpAlU(iW>{aj2$!p zd5lKk)sz$Tq#bJOFCnqC?{O8it^1}j-Cp)BUIO%P(Bx<${?rs=R z`y}};6Ub{0!)0v{Rs423jHpEKHffq(eE$RtxZga9r(btWQR9?r)U@FSaC|7{Ysv<; zz;LT_+koaCzfEQDJQ&$s7i=+8OGg|ke0Y({*$U~dpm8{1uXV_ILghI+L*>27*tBZS zhTb^*>l&{ZC{Fkkp-D4UpzjtU@_V4kX3STNjF#C{I?0B(euOFrhxMgxxst~mKkKX4 z9vs?oO&_mr4*l!7TA9hY@&OJo!C(0vRxIy7xv@WPWkV)zT|CA~QygR2go=58yCgHV za;QPPqc+=zzc5ztcNZqN0}cOc@$7!5rp&2mZ;}%V3S0Y*v)1l)GwMO$#42UvK2GC5 z5L$$}-PCikx_A36(&Y=_-5auZt9oc| zkoW`gdo?QG08!pO1z5ak)I?mJ8ve#$fUpZ2(;C1~!oGvLgjd-f}ME z#TN#Q7YHi=(Lls8&j0L1LI_YGp|*p*9xJLTIyD;%5fmWP8!`7g%5433di@7N@tlWv zl%0x2oe5u?EiaagW3Dgf%Fnxayv3LE@;QBkT!h=BiVaCeKVvgpLbv{#A3&$hU}{%E z@ zjH*FG%;|?+m#Cb;A2tl9y&moW#AyNdAYDga+n}-&%3LMd*quUAV{;v65Go(J!&ENg zSJW2w*c^+VJnu-pv*?}d3IDTic7=cdMFdY~febAu*%8e2LQmB~jb5^hNq;tu)avM& z2xrgnM=k`M6NeF=q^Jd%0s-rLc}^uX`(e=dtx(nJorjU?%y{k}yYBp~cNrpf4z_hr zS_$EIH+Os_*w-HYTVo#&2%OZ9TU}kD-MwD%p_{1d5>ZYyO%A?VqZqdPgEj)zKGCX- zK0Y%FJntI+TcWjQ%eV3UzC9?9JVg10uOzM0#P;|uTtOU(JKfZQ@dZFD{DQ`$8@0T)} z`A2P!LX7Wec4N6e&TH>>@;pwvOg0VqE}aa%UIaOB10u65&BZX26s}cpn>}7#-3qc8 zeW7>azOP(g?iaDw{X*&zr2;*12>Cru&@OGM9bH|5=1P)-)9Z52y?*R!;wywj7eJS8 z|8aF*NHb&^Ve(g_yf|g zzJ%R2h|72maCrw^K5tyjt106r8BlMH5|z~Rkob!CZ($fmbH4fr)ryaEr}Xv25n|ZE zZ+Naz#r@Aejl(;)Oe)#R>C>pRc^@Fj!BBq3p{h$AMrJ6F0}qBu2Q4~=;1}<~YK}}r zj@Nt}T+FCz30zE`6{Vj8He)!@>Wcik+_4OsxBSnki!a7(tVrOA=HG#x-i@3P&3C}# zLf)Qt8>$E*v-E22gd=oJLkSrEnTViKhlbo{#Sjf=EG%#`(liaW#5}097zLG+(lQW% zH_M5>`eS!KQWTwe;&Z|9RNo;QV;{x6H`Mu@2j<#LIs#H{4yWD1MQtSkU2vsNi&5T^ z2RD#|DC)dp2wK?#U7w>deXQd!45Z+Mk^}4y_8vAhlgeF~v8aU!GV%U;s^}O&*9{jw zOJN?X2P9{_oN$26)34{V&e=l%%qtXQ??DLH*})2n$00}Es3C6%?OE%=vRy?A@quMI$;EOMqp7GzJ8|>mybi+VwtG0ob~irLq5)3oVNNb% z0h57|HRM}LP9IyBV{_Sv>vX9#s`Y%OGd8~OF+5h2^Uy!$&MC}X7mKfl!R*~hk zv##Y}Wk`pV;2XOWotE}-yO(X4*c`Ci(SbH&KHC(KeF#!XZA+8W8#|wrj@wM}T9*AtQ6 z{#pK&-0I4_`^a~c)I6{3L^#5&(Tw3h*K`STJSUJB8BeC9GLqxNLeuNu<2zRU@-J*N zb)Q9#s7Ef>By7;hFeL7u18*{-&{Ml?ehp4j&K1gnUxDAqr>m8F_dZj&8#g5rr)ZMG z$Y#&TQdVc)J3?=nMNT@oyKwqL9+GFo#us<+<>t1m#-vxpV)E`h&%7LLiI36hs$ddX zeS8SBw&luYIZRY`@*gWF3VLoCd%t&TO;op>Hv$^#hy3g`hGW4|S^Vbl?zJ3qi0SI5 zJ9_n=XZi-l%w|bT$B_*dALo9QQz$zO@qlX|_7I80TJYnmt5g_;4NKj&{N5fcGWCwB zTG>tt$QS{eI^sNuIbX_3*dB?c$q6IRa_iY_#}kY-DTdbIza0YJanO=`O9wp9#_Xid z{Sc%n83i{6mHO}KO%$!P;d{s6QRt~QHPHE+Fr2?`^X(6>(|)>`d77~)Iez>x8Qo<) zCw~lZBYBejR{s7f@~vie`wX1Rg#fGQ{ie-V{M>z{*C>sD-xK+c>bTNfz&3Tt;3j{0 zN+L!(QbqIBx|SBNaiP4Q#NV}SIj`;KZ1Z@&9s3D5@Av5yJT~?JqD80GPSmuy-DmK>@EiuO%_@hzRkgb+a5ek|Z-?hI zan8fO*_Yf_tbs__MJ0D+MoAy2Dig+DVB?C3aeQgW;|Rj#lL9uXF=3l}GIo3pC~x1a z<0no$0(w>becB~=Zp9&)*PBOm$P~jT{jmU)_BaD z0{dgf(WY)DL!st|9r_;X1SI6}jCH})I6$T{heS@WkB{wtImNm)WB?84 zvOMZJ4Z97-?7_^Sif{odX)U(!)!k~YJD`BWiWYysEpULN?OoJyOB?Dv6YzbT z2%a!b(+^lk+*}0F1jE<_;oR8FHW*-{rxp(43)<8i={9kuV3g^#X~qFDOcRm%U|f%- z>k7fxBPNC&u)+T{6v+6y@LEHDz>E|gvQ(tm_tcxWmoIAa|=?Z$2*H#+pG#6%ywtlvz+8+w7&IvzX zk>dRlqVrZ2pAG?sz+<_NGqCplPRmf*&*<(_3Xvj1CUy?-8L$syaGxtf$?N?atgzj^ zG#MIxY{9)wQ#r`sSgk?*hidBIzToMbkaPvN#LBZK!Ttv-r=WW2Kx zdb>A{p0blCu(8y4AF-NpRh)Ktq)p?oIlJGD)(zQ>&wTtVPKjCRsvOBKB^?ZRUh>V8 zCOv*Tl&}?RlhsB)8>;J6zX8Wq;kg&%JsZ$KwQF3B#e9YcT|WrT|8Vq9{7<@h+m`z& z;wi%P!6f2cveCWZugpsCmH20`Lvw`5HU$v=>Q{deza?(>-%8h47``mZ?l(ua(1Drf zw{iOGk6tSq=<3M?K|_g0U1fGWyn^8cyv zC(W#(!YL3q2|ME>TAItzQvJj}N2)llX(x=YW*)sR_83AT6?4Sc6$ENTHp@<~>M4QET?+`G}=``oc1EwlmB7JR9rz>i?k<;4hd^Xn@>1H*Hg4bibSz z&3_9U5u~1S<+)T^>wEn#V9Ae+T0!P!{f;dozu|b$-eKR7lHL~6cRz(azm9WfA0e+P z841dR$6@?k`ujIko{wzj_Faf=PLFHJ7bGW#&1u%E=({{Bf9W3QytM4?mh5OaGar|d zH>NfH0jCA4L+M{Q0o3M-^`okYntC0D#?(8ImH$bT=e5@a>ZfRzO(k>`3`SB)+{$5k zPjkC*{hjnoZ;MVy$36! zUI_6#-GZhCAh5s0Lc};+9mBG7`_Ighm{MaaS%PlNKL2m@)Q9AANZ_Y>NlC$k`9NW+ zx4}EV^GZ)NbDrLR=*7|ayE$=+YAPN9nWQoN@>;TeU*M*y(T*1OLk5| zr_mg*SwQ$swefVL3;m8Bg36}*>*Q%H@t@J#e!87G?sSv2@@}lP4a{aqCN;p*54ct9 z&?L#95M0n`=d6FfdQ8P1Nf~v9U3bm;cHGkb)*2eq{@Dxc3eiwqFF`H(>uz5^V2l75 zC*SL3`N`aK4*MwBJ5djWb`|0J|JRGxc{NmD0C)sN|KK*1Eo<+iy%i@~z4g4bzSJb>Alm+vbTB46;$bdklWoxs;;pE(x!p(EwNe0&aXq6*#6x~LGlKD4Yx2+(u=Oi zu8jY&8Lz~LqwB)aco;X`a&+hgrCx9@6DPGEQuPZfCi%R$#T0D|XcJS#`E64odZ~RD z{ZmtO2g9xcv$a)K85&Py zF!w=8>6+NiSh+}hM4hg8ju(agHz~_UXVG=D;det=GOOQhrU##wY?} z_k(5|+(HI6l!xJ{vi2$D8-KR%Yu%sYAig$hT@COuvZ&-&OxqGVd~o#@z-jG)5UKBY@{{43!N~kJ68wkDqouwtBbmP zSxxi>m|2xCa~qY+nsFGc3N2-aCP7TSoKJ^Sk79&rDz=rIK3a7m7ZZrY<^>OJaO(*= zjx2hR4DkYPLu>js?!o|GO=-lP`cc_ zH$2x`nZAiz%BFwlH?cFBtVc?j$>kJ*nZe1hdt`-0!rXe;wDC1gF=^`bdYCN#4d>=; zv5}{X^*ftHdR^0+qk#Zi-e(HM56aE;TcDD&O|a)aCq9~4v^s1FZ)F2d70%I$>dy>Q z3*5W&3@RoLg}kxqw1z$5|CXYjDHD^)#dXNjr2oaqYa4}n)x;`SSb4^~{NF~0$G-pv zTBKXVJnBXSUe+L|K;I zi)^DmH=xFR3BRuOn^!fWLYl4?eOfFs@bvite+X&K)^IWZ!yoS*HaJKFreieJd#**K zEM0N%kXzf%zIU1b7>uEDW~LnQ^RBk^vvhv@9+ouv8tE=mg#dO?0}W#gmCknJ8Fe2Y zSR-!I;MP&Mh;dd|RVyuzQiY|0!T+gz6=@bDktb@;VrX4e?vh(g{HSKbK)eYGDT7*V zX)zqu5+@ZR%p^P=n>UHO6#$YJu+f?kj^+}$oo{jiy>G~PNlE$U?FIPMVRs{f+||Rp zsHnqQ-o(-=t`b=NRQ%x(XTbc1uylDJ&2is?!KC?rv#;rA)NPo>Q?@OV(l#N1b%!rg`E1rh_xLW6?3wUfi z7Yo*5s6=2kiE=W0o$PdKB0{$W6ci#On$!s4^D!G}76h3m2tb6S{*hB%B`4+N69|WV z%8hU+0qhXdAc0@b9(k0CS?@bkS{AZOChb(eKMOOAeGakWR#gT^UiL(P4oaJoFuxnP z<723mdC(W#2SNbs5-XG~99JwF0o=vg)W9}i#^8g8hqS`)vW_ceH-hdR%t%q5mWS~B zAN2|)8dOMc2|FbIxM7jSyCFToi|DL}e+N~3%iEv3xn4J`sy|Dv++BPPoJsxoCW`4# zu}C;JM5KB|2Bx<<#EK@6N~i3nK#&>$xYN422iaO?-2>$lXcOxoqADm=ZvqG{VEFrQ z4w(|DV}JtfX{&%I&^A(YRJDS+2Ub71eSG zQG`a85Fy7U?#6qT5Ha7=hYZ<1B37-r4?qcGg3ds~ z_-6$lpw4{=alnWw(x1e3#}@iTjU1s14J=S($Bg#1O_lk(Cc(INxY)yZb*!u4c-JV- zVYEmXi3FF6KU&|xJl(LWpcaayjn;`&^U&CvHb#&f`BKaIk7lZ3-w24-P$dIk@xXSP z*6U5M7ZEeDk}2OoA0%=G087vBE0-7N^s~-Fp$^<(XAL1B16@z{8zP!%^tlsIl#>N%30!z*#XEd-2)072UJ4uGCWNc@r@q7hc@$It1S1%H*%0L z9eEw16x$jcoi&GFMWa%YeA@apj7T#4QkU#QgrO5cCUin#irxSZ1KAW!zm5&^nA=0g z?`4R5LlL>Ufo0;l=Or~r2);%~eh8mMD`2qfyuC1$V0J|McqT-~Oqr?S!s}*4wvOnq zsbbX>GS`&{Af1NsTG!qX{=_S_i!->E2qiAzi6aSaU^C{u`D&&h1_5Yy`VJq(?S^}@ z+W)I~l2k*s`5xXDIWa%5B@!TF8!GO4-us4GFCBnTfjP0(T=3IE($P-{3eq!PfU;LY zQL&|;r^mn^S=LFm+Tp!K0QXgt3PBwAl_MeD#(df1fXtNR16gwq`a!sJ#ihOPy#r2X zkjf0t>}q~#BtnT<*bx=LxoJA#+GTRlU>`UD0j>*;?R(RGBRpm|6#uyYwhG{2z8!Dd zZ^7EupwFF$hv3!qqhBiJQH;Vm>cjAb(+$IA}u7{`+Fl00@GFAm5iORc8tDy31mh4@xciQf>Hp2_qI1* z!W&x8q8Kc9pSe+Z$gCC)?u3No@83aig&eSI<&IL0Rue)8(R+G?^IgnsXEDm}rWGf} ztv5bPU5L~`jsvaj+_3)OB)dX0=@8ra$HS=|?9c--2?HSskr)J%68*iRl_P+hLX$ks zA>IEJndA=~=^)$-c0-eWrtH05JLF4(;k|$UdG`Kv(TaUy3nCg95N#Sp#OrQ9q^eih zi-_MarrjSrFes_vDE{sGBx7aB9Sqz6E|bk7I@lB5k+g_P09HDt%lOoAfh8^s{`QyC z8nl_tF-T)=afA=~ZpljO7ySFgF4b2x+gs1yz|_t7DWfj%b^k^!SS>-b^L$L3*gdX1 zv4t#Ji!Zr#E$y@@JuP)7m08b;AFU5lfL);d7`ohqYPN2gB;tywyY>r0|L=@5aEK>LJEDQWM6Kq zr~B?hPIC;nElz+#za2ZB?W(2E4W;}(1B&VTWMxB~ljjVB^;QrrcbnC=`*=Ns{s5}Ivuw9B-e#~%g7QfO6v%Cciops+5P~8 z7&&bqnfdV2!XQ!oQgH;#jXFip$)Y8)nt(n(@)nPdNBQbfdo?YFc01%3U^2rd5hN+d zPb#B(PAu^(l*tR{bxaFJyxlk=K11LKv&7J)yKQ(a&C!>Tzf7kgj(aMyzr0;lWe3Tab$I0{h0I_4p_XLVG$G}VJWP=l zhop=#hcq_#d}s{e2#XLH8i~;zUzAF$2%reQiR>#N8ez+$_=H=Ni@r>4%ALnu%R|HO zG*o_n5Y6bKa*k|uoPAI3gOZV~UI28N+Lg{K9U+Y|ssad*PbxA291a1z0YU&5(z(y- z403&WE^R*EhWP@^M6U+s)*fq#$D30e5xDf3boSwo8t?<)5DW@wDxDb8)(e)q``J76R$86X~b0IpDpYGS@*{l5$SSwZAmVnY0FMt0qG?@k`I0B4(`AQ-J_m8pi=w90*%_r^88 z0ke2jf`nvWnYEUgM-{Wd#=i(~@KBdSOV($40F%Oy46}0mxMT#or%jx9C^Xx7|jFZ?Iz^S@oi&npP7-L-k-7p z3E)%BZQYvTuax`Q;iAtkY^#;T7NvunYKhmN=r~az+rL=#f`xLkOT76P?2k>{-3*bI z#0q6#Lec@MZrBIOHqWQ*$d)N<(u;87e_4OWsE0Rwv z=h7e&%rMM0b`oI(FmQn}45F8@YYI2CFJU(rENLemtZ%NqZ?3oXl*4wlmt^1U_1O{7 zlHTc+4+;r^at}|j-?J+~C~oN~11zCpU)q29v(aWLnvdmVAT}Rc*=invPXtgM0eFbsElD_smJK!-xS;E` z|EG@?$nuP47y%L7mj}A?vQa#K~^_ATD27 zH8Jw1s&!4W*EelupwHk{x3s02>mlL@GQx?|3?T1mzbX{c@&>#>xbB8$Fv2Qwvo9j> z!Khwi8>P5tmRYJ*enIBKwOHA_fP~E%S>M)s!?j-6l>1GH8-Ase*OaWL%*U5<6`So@ zCmna*A#G-`Q&$peQLb6=3U+!G!Y#Kj3+_KzGt!+B%V`6iSJ9gA;|E_8S8{`yF^RK# zLdC@AyoT{M|5+`mN9q1)oc6*4PC4-A!iG!R`*a%QGZVbWTxva4#<{X?0jU7#K8ZeT zC&M9i5dr6HBb+?Wp+DA{_52@Kk*@!`mQT3baCxBcfgt9Idr>s#ehEP?&#vKYMEpbL zIZy?%)YL3APU=|ASm4Btd;;@=H!8c6WG(UNez_U!+!-b=C;W~auUqUClsvUY*)B&; z7T2Z9vAd=5JO4sk(1gFI>+Dk1p|zB{v2cB6J8Fm-_TK|;V_@$7NsUmLd0v#!hmWN% zk1^2?iZ3c#Q`3W+6f2)JeO(AyS{}`Izc`6usW1Ww(2~S$B0mU z<4b#$Dt^r;d-I@w%E4j8@@Zq=y&vKht)6iIJRzfTG=gfAT+zqnkt>0_FHxA>otwvl zpLP5#P{>@O?<$`m``vy_h5v2KT!$cIhV`^AAWBi|aKP{XhU@0?*Mq7n$4h6ai7zGU zZWeA6duf*8=*S@FHV_k3-7o72&8HG@k19-mc*JFtQYj!B03d%c$OLyb+427X3?TR zYVd*YrjIPambUnFu5WV4rT{InFWH~_?^}E6!bBb3N&)fE62;OwU;V;(vEd0Eg#(-2 zd%#FS0D&aLAc$?6?O+2fqs+xWI*vLyl6_M}aUQ@TO%RyGX~ z!<&8*jWcxj0^_d$|IoBrzE>yFJ76?C$V}XN_4wu)oqK_pOAc6?ZXuNrFbMn?qlQh% zGoS9wt!O0spy1_#6s{s`TP%~kW=y+(Srgn(bWa&6a&u9g2!)ft^Q8@HINe%Lw%X78 z?2G<>my-pf3p!&$0Z@`yjy6!1xnXh>ES3V$Lkr#`?BC6&M@pt#E6?t*yo!0^i`b@< zKsLJ`sTX3$@4)M#M*8%)JAxgEV{o?usCRPJu=!HCaC3sa3{#XBMS-N;^lN5Yvf>0`ny2?tkIm zq|FD)5xd%Ds`Uw$P~0%9WQp*}w^E=_Fei|gV_EtVBFKksex7}A#M4y%pTF51$*77ds~6!!VQ0AF9}kXk&3ad`^bJUxUMAK}E%t#42b5h+Fjv>Is8ZJ@4~7 z+Qu9(%H(KjQ??&ne|w_O#)i?xMw!wVXJYEm!6DVc_T2SkO-K$=f1r3y;i}5&>iG5NKCBtZN{DiT(Lefy=2+%Nnc532-N5 zCIbQ72INO|a`*I2fq)MNz|a~K*q`7qWXokN3REx0+EcZCXwQ`|?IfXzV?ti@OwY?f zm(cqM;8u&)Qfb?Tn`czim;c^R6Meb78wTGH*xVk~0@BzSm`(~v2!J9|7aAwZ;zkG^ znT-y=s5eo*?k$Eu7>w?{bLX7>j_i{?3F0rZcHx877AgmP{QWj~km}Mh!^ob;`;x~b zvKKq1vcFxQhhxk5@xIl)Mbh$(n8S(NEMrk4GC9w3<}G?4cxaO%UueCJ9dZGkknp0} zMUijpXAz&9Jqp(jgOrvaqylENP5Uwv&KGZK$SxtH25BfZr_Z^#@wt@^)Vx1ZfP@mj z*u|PF_114*#VD^klKN_#`(pgR1adK(s@8WXdaJYy$>3_!@%fiNG+39_Pczb#>zYuu zVq75Ph`xYEF8ie`W57d{*O;)ClR))0m!q>heufk zs8`&Q#p6dy$9MHcTfdf90&*NS{}HKy-L|MRq?=c#>3p1974{urp1*kx%+Ge?U#$*$ z_xp=@yFo6+7$E+BxY-C-Nem2U^ z3;l-;#hh&g)W>0f+%B`Dg$=9~u&=Og7-y?XayGPVf3tGNv!nk`Qc%}ohYn$*Tp4Em zaONj@F70=FxygROP*Xg3nL@3GdKftqs8jylj>Hi30Q`3kd-Q?oVf9Ofdv)Jr|Idp8 zmz|~tfT+5uuhc7Ck|xr%-s}2g3=dbbM!tv8!yA zqX7Y<#$+G&;0LW#&pzcr4Oq+{$fiK>QjvSHn!2>REXxr$myZnr$9chhz@g2R_&vSf z^X(y*1**^t0N&srFH63Qc7bk6_Wwf~hVKXcK%Y6;2nbqyc72MVsD<4kD-x<#fW4|I zQb7~im4H=zN7Vvi6D2@NJVik`vU-!Ub|it}mZ+o}4{y%;$CqM#5(V+ExeC?#=}I*b z99zueF>c;-q0iT{v0#KF;K4Ki(CR$|biuD;-b5!GG)PTlV$-A!3xx1%o@Q$rHbr04h7{}aQ`(L`7rpxF~|M$x8gEa!{men z0dLXW7xBoOH5WuC29d&=5%0&a_bn+Y11QBkaV7n+QgDd${KN@=_flyX@r zzsudqrq*rMmv&>mi`}Rt(diBluFiJFs_Xgnp3_$n3UPIgM8sip8q0br#=H`D(F?%E zxne>q@&^(7BmfWxC35xBG_QHbjUUY4aU8&}R;celSrwEshzSxcY9ABM=p`7U+AW7@ z%`&9ZUY2JvyRd`7KeGl3D;^y2_Y3wnTY@fekhCRpEG9`p1^d&NG*TOX`8Zhm4MdDt zkJj$p7%9~^{dwg8piA+e^~Z`r&}e}!pI^cWvK@8B1yKp8`v9cc_HetoaVmLA?8{Rk zwcrV0TRPSQI~r=~gE6=8DF96CI_ky6q7U&D&F`$){S9#u`SVIy>RQF2eBeooNlVn* zi>$A-tQfy>jP-UqHZi-f4a4rwrt+NFpr9bop1s>pJV%OA*$Cq#>)#;m-ZxK4$c3Z( zHBpV&Cz{CTpU-CBrOn>sWIv8?(|N@0&R4g_i_%&;(UnzOA{NKLM_-TP;N=cD9Y!ad zy;Dm*|CaA6)^lg>xz0Lf%%wGAqz(~JPz^8n9$5Nc)mvl11nXzu8Z=Vu5;R+5BGCM^ zvAUOlPo_1e?UG>byp^`T-w42%e)yfgmLU0nGy#0xV(z2R%@r<;x2!O5Kry)iJyn#& zr%~E{*+I@zJtI=>n2plX?qgwgkAM!wzWhdrbf(BI?n(H(D!qRAA{usf3}y0X@bYBy z_k7UW{oxgl>Mip1`Yq-C1j)+6E5AWxGV$)Fjaz*|Dsj;2^IJKh32COE(#75HRbSrz zi8~qsaaX$EHV#a6T~8Lsx8EQ`QmG5?Ye0y|F&G>r=((oQAvi;7aha_BhMoQhBqZnWRD`q!}tlKc`qoDJtY*C z#@6}2Y4Sc|jRkY_tiU?FBUOo~+4jpEQ-}eZ7uKv#7eXEWn$qUeCZHxg!6s;gw$j!x zSXE$9+Z#_u^ts<27EBcn*mBPPEwq*!v zu~q`qw&gSia}j0F$j&Nc3M3HM6Gu1Cx@4XPPyc-RM+V$FC}CX{QUSyyaUKdBlhBNJ zYkEDVe$vBk;BrAwnj+loLMY&ugjMLffQ_jaAA9T)Hh1E@hAmasFh^gwdR$tLvsn|s( z%?WHxRIRBw0|^#UfRuosQ{9@G0%kA!n%9YW=2bcUqh!oV7XmasvgK|P(hin2=(#Ak z7evp<(@R$tvuF$q%fK6zLnP*Z)E7eemQF#arX?Z+%f}Hn=V!qW@qcKix;h3H-XuCC zkP!ylvWGeq%E9(899ZmmR72@wetOPnt3?Uqyqju(KWM){xI2SAGT=uRT@pFN^A8*V zXGV!*$d=<0Mo;0&B)O*=|D$Z{$}z#ad~c#qDGAEx6HZmI*$8Gzls8AC!$Uk^CRyg}Su+e~GoGJ$txGS#o<(apd_6MM@lLeN8G#uo*l^xV1p=Xhd z$j1?YWIFMUl0jfaTtLPi2qYv3@odOvhWqj3{`8wo4dxo`1+sF({llF>swxxbXz7qz zOz!|1Iw`=J!AF%0~b>PxM(+ z+0tl|2V*SuE}<}JEem>U6x2x>emA7Sx>zPgmAwf{BJ%%W}Wl$I-%& z+@i~~>0>d8CAtsz!uf*9$D&q20&3cJ>9GaK1NH)&JB&_~5d0mqCzI3;qB3Nh75!NH$WtiP%+a-mJF5S0r24^kcQVt4XG;@5FWEDmCAIKfr=jWaJj6( zt&{<{8ex*+(?L`L6+M!@9xQ?3Nd-E$WvPca!3gH*QxJ(stAWKXf@h*P#zaWOtxTM5 zf_KP)d$e!!J>r${`g{E^y>7ADs)X<`*H1EaUjf@RVwyR`zw&`NP^W_e2eawi%Xec_ zdjOH9!L3I{*IRRW`Bls?{UVct-~8HH6m zW#LB3blr_5Ok+qFsbkzpWqrua_r2h$+vmcCS>6Jed0G~JY|vS@m$Fb1xiCnDYTdGc zKyP%N4+=tglr1{*C3z0uU*g@p0VaQG5T%0$9j!sWHf~WZ#WHw_+X?)gC-2(Gktaer zTNsqTMC@Mr(mJ6()J?e=970rl#t>1y zR|=XcMi8Xlzx0zp?-%2TSNWr!jhKKz*bZxtlSxc# zfY&xYUHZHB^v01-uZK{`pP<&5$x#-a+Iz-Fw<1F@a6KVgp-+Ke$iRXtMg{m(vrlao z!#y~_qWlSen{I}$LJm`S_l@@k!}I z*|46p?cXMC4sm8P_E!gb44@Mj^Lw7c(|1ZC=j`6jvD;LjvjZQg}l=_ z5v8?hws$n+k8Mx!Cs$s@@7uv8;Z(Cru=o&^>vghbWrI|CTmBgEZzyN_;#4Hdg4@zi zs|h8#E(S|J(>#H`hi!wi=X@MaICmS`pYc5Xz@Ty0HLbr|4s~O$zK1%pe=cI*Lk&;aW zlMl4uMJ&BpM@?lzmmm4GyH5_fCgs`7rkil;w({Qp#ta+?5CH`kfd~Ox3T>hCn+?>@ z=z4)kGlOG7%Q(ounXATds#lY$lFqzLi!9rBjQL%lk=8MxLx;^X>)kDhRj)E(V)%G0 zFH{~N&gQ&oD@@3bWbjc%{N#_px8b1{K=IV-9Z%~pAcXz)$AL{*J5D|kFM1++{Kjk; z{=FUreN=^_pnGV3jNKuNovrd2-Krq zIP2R{>WX6Tth3%n!u>msp0qgym?U!3{}Y(hcn?*HjtEZvv3&s0Ic^_>2ghoZ!A#d^ zS-vjQqA+%Ssj~jGto7JhS`!TqiQblq861b{Eq9)kFqiINSpSUGshDhDToF z_#nVt?d4{6ELp}wtoG&S4?kliAPvt0%Y-JrWXPL#CBl&4%JNan^(6Ol$A|Fti3TT; z`8kfH8XY|)S-zQ+atO3$G^h|zVxOSX?Mic&!exeqnr!pj-)BJ#Ko0v}u#hJYib!sMEUmrOp#i|MQ9 z;%R(1W$3Pzgx9!2m9+5)@cd?310sitI85K>d}|UqDPgukgvGLc(HjIP?c$Gu3CcnWm0)O^aQk`!?RR15wTBvRVVcG~1F5#eV7$=#--a zt$v^%;nj|eLA%L#bc$@(zqIMt;?+T3jJND7*#r^5hcpW|rCKotf6-uEj4f+OEYxvT zoqn^PLlDfv+o8PhQhrmAnM)syW{jq>VeToYfXbq<89w`)ssD2I6!P3vM z06h&X^$zTMdwfpihVP-Qfcb-*M4>XN`}O`;oTII?Jjs29fs-L8_J7S5_NvXPabx<3 zB^oNnfLL`9rVu~SB(o7%g+Kefn3Z)g4nzjTBf|r`JAji;OKQ%3D;FS;4-qMB$QO)% zB2Wxu2B#wv4G;F0yjX)U#F?p7dJm7~GY=fI`Qik%j?vr0qL6l7?c|*n?Xa=(Q5M^Y zQ-kI0Jq-@c1VKU2QfrSTbDK}GKH>++el2J~4C*NyG>bqA^g(?IBuiaeGDKcIFK+`= zMq74?A+m;HK}$_6qzO^@PN@yO^${r-Kx_Px zYAiKD$y=Z7@$S(d#&6H2N^z{qp{aSt?#_p`S{*i5LLQ0Ap7s7MZGrtc=iURzjidDD zO)NeQ>X9wOeIvpI@bE)3gdOncjTlnGQ5ZHF!49Y2K_T4IE4=)WPh0d`^IA}#yjE+L zP^&ak!aLTGhNBXY0_qV_Oi@4xVnY&b(2r8*EYujZO>-CRBs2c(xsYe=J&NNg3a66Q zX6wmgE}3M~dJ}0p?g#viS%_wF%@VOd|I7iXP$yAi+AOC1)_5@ldbj3^A;3fWgeQ+{ z*yM;fIs`OfqGyG{`-3tt;ehFKp<;e902g-K&*>i{QCwhca<#2CPEjAzrHo5)d08J5 z9mU7o1rOM7*`~WEd(+Rx68NwmE+@z^aGpGu3MfE>f$#UQ2sKAu)4DrTE{J_i5dn}i z3Vc2ZJOS$A`{%`Hx5xlzmVWn^@tj^=cmQLAuv6VBxOkFZs_>L-Gvi7%2Gg+90z?Qn z{#KTB{9vKnf)BJ;;T0}(Oj}Z(YU}plSeKp=BnOP{8k(rCv}390$i}P zFkdYTs#QKRfGhwf0)Nffl0e?xo4a)CW~poMmZPpbD)^U8$R`DFh99NBzakz(KL&}c zkS_)w&$}ja|6!387tR_@Z7;m%($k<9xw*B}Zqj(g1OL`Mt;8FoL{YZZ8}mVCf+mMA zbWR}C2K(vW}`4Ux@qzMU^0WSexOSftUgcq7JGZ_hYMvX*LNRy zNF|!W0HZYd$j62k0G<_4&LEMX4ryBChQ#lYcnLMy))PKl*9Y$|Kgd(~(#NShabor< zT>p%)9ngErg49m`f_?G`6}OS}^kG}a70oCR5TN6W_h`)dW5lfSdkNW*07xWa&Lj?r zuH5)gLR=Y~hK=5Z0rgZdB`rYss?!yGd^k2|Urq@~Gl_ zK^UAAI_(L+Ky(6Q5YksTjR|6O({RT|d2r^Ei1o0wHJ3p9Oq>?5wq$bMpTEp_-AEs= z$?aqDwn=F0gTu6IE@(r^v2p~?2q7mZ6cztLR%Wa`yFQ#03VAUaitebH&D#W^2TTwg ztSVF?0(Ktm()Ota1BVAeo|9xtu%H4j*lh+~=nQh8+@Xb;T{;--??L&P#gJq|ZhRUl zRmlJvDXbyyDUhAK3Vz7{q4p2J)%HKThAfnB(PkbGBJ@xN?B^AUN-LZXu+tCbOe`j6 zD+~rS6U35Nt4GWOpEGSf!}+mF%l|Tz05CeOa15P@GxoiThad(Zi@;x^e`wgcVA;vi zm&U_YP%&)9l#cz~QWG`4luNU>e(J%nnh&7@)F2{Adttzyr0P&Ki)MfQM&|Pn zB68CWR$0jTXx#v;K|}LTH6)r4*6^Ehzt>cjdelHLQyy8JyMxb4nH>i8oykSy&US$^ zXeRyBx@=3g9lbiN(TvIzr8Q8FTLlU6$ka03mub;Wc|uWBpXy(>j4Kcy``PiZ`mlFV zCXA|%^(k>D^HSGkJ!Psq>z6uUoUE_ZNO@jxlG4T<6K zoUR<4NMExBioIyE%HWGcu9XHXyh;Tk!hgxd?PSh1HNQN7)=cw$#8 z`X279n2;5S?G(({2D}OdlqFP8OsMmnF+*P&3?NDtK%nmLx0ez&yN}&@O@s6gS%#;?#E_C!{np7Ts-JDgg?WY8TkccMPnN8AmIbh<#lHb~2Y|=d zr<^cTY+TG$jaEqkVFq`%N`%@s@Wgz5jZON z*~PuT2Mx>g8Y}qnb!G46G;J%<&E|X%lmrO zey^2Urb+VYVA{{dOFY#gLX?6s_!YD;h-+Y^(vEZ##pRGm z+AqgV_Vtx9e4gsSmKy2h-oO)dH`>{ThcZQJ&yLs-D0+o6JjH3?5*dHdn4+PyFMry*Da9fotZ= zH96_j`ixEkcf4But|suWgxqjlx)1UFs@O?!gd=p#Ovu!KeCSWT`(8kbLNknepC!I3 zadD{dgs`nu=q|VKLgu?k-{S;lm^pTlzblSrMBlY9=+pln4z*P`yi$$(48)o}#V1?G zH^!NVK0>sDLVFIwbJiT(nKyiBkeSm&gw_5Nh#wpzx1Z3n*w$;Fyt24vqynB<>^2T{ zzuMGy@UnJDT9+@RGbX<}%PA&DUESrhMX}hoE&u{3N%n~_M`wUGIx1e08I#1*Xke)& zLbssLJ}H&HS9!!WmNQ zb;wPsRWy$us_X9_dIl^f^v>gm5){3u?o!qi_!qQcFj7=bX?%6&ikBOBz&i)T@I+n5 z@=(yc!7E=81yW6Zbi_xDAp^v%}>_w216#7d47(&JZQraH) zq>oqA5;HuqsF*ngg{5Ot=(Bj=jNWSe{`#S!dUieQ@^J*Z7&2Ns<3lX2K3S3bO?jnsD@%&R)?g904WWj6Eo@ePrdZf@7GDw*(55t*tM1{rM+BkfvJqUPB>Uw+Jg>>a z+18L>I_1Ilt~dR#o);_ji^ch>gBtpDPu`N;j#!~2$&5uW0&LK3fEeJa3JnNO-!=$W8rM`0 zqD6BWMs?P~7Ky~^6(-RiTNe^!3gOlV6kCa=(;AnNC#pJ>ncApN@X}t*suEQQ{EG|c zz+uR-w#8&|Fjn82*AAQG=GTZGvCwm~^BJCQfDWLL%IokSvP=e(CY~mFrp^6AVGBm9 zUjLrs!)%>K0h7o1FJh*S9$vfATbwN6;$bOHMo%A@_0@A8JE4NI?>#&G!QA8%ykMrO z&;1x+-zWakyL=NA2JhO?Ya5eols6XIg9P{8VVuBl|$^;U`fv84H@C znD~Tax%-%YYeS$ia@BrQj{*QYs{%9**CQT)ySM~w5563SxM(?Pkn#dOU8ZQ)he7pM zG^6<>d*o_&dAE4A)SOew^>?1ljR$a0_H)oFsFoN5wV$XPY2y}hDve6!n8%($mZ@q| z=B0!&T9GT_0if4kxN@1Jfv6pf#*ymCjbBg3G$TkUnE~spChB6~RM)Hd7U`@!M;Ns{ zdmzk&T;`PjR{Vt&d85Fmu(K~Z!?;`k!|xJT@*drZR!I;89nm9lE##jS1pY9>*LYM) zd?p57x6+Hip8cQtXDUZlbOHP>ALV?7=wD9lqlGgj&G*tbP>4}}-a$eDJV5PItIb$% zB-&!tY`v}xm7ZavI6ZfF7A|%+xAYRIV&BnUQH}S94|((Qtyh}i(ld*UvDGGaGy}Vh zp%j*#fmP*CFv}|Kje+%yNA+=gS)Jl)aM!@0ShP#RNByv6-oTf|$QzXSnXckzxKKm9>Km$=HKudlry~LTz;AD0e792)qQ}LxNI^F*d^jpK?d{qLmOm5 zhuQyr;g^yx1iY3uL9X~BXXa;#+VE0to$daMw@(UrD(|L;yGCe6IYPps7@g9XlED+NTKj&LH__G_`g!o3ZtdK{}SD5WE|x zO;bQf@r;@@K51l56}B<(Vcn3x$r6$fzYT=tm1USx8=K-}d+6u-)jJB_!ejXrgB-iF zlj9=Tij$55ZBRHEZF<<|X=1Khvi@7Uzj7#jg?#>;Jh=L{BdAXg!N#H75Lr5$Wx$c` z)r@i8K6&I+t$VRDFRMK=B(Nk;JMag9K9*?~_0U5sbd!BIL#qj_6?EdKB7EFjVZ0&2 z;A5}QCF735B*L04TtIq;G(=lfa8s>CwDZXlgST;HOWercElc)LEz4+e*IzH@dyTXr z0VQ&YjhEwPm>zTNUWNiSzt#}!rlZ!6sYD#osf(j;6)kd#D?Wh#sDW}L@x^BDQBFz~WPiU*3Nb-l$8_fKBWQrH+2vHegJ+D~Q;^EO1 zZZz+*+;~nEJ&)M-*TvXZMc?rxHne za<4TL_PonPisrwS|4um?pNDq0T7GJ``eXcQOfk7`y$RUGEK!rk8K@YI!sNT)5R3Z< z{2ceJDza4%mI>04>f~&|FB?m~_-(-s1iw#OJ(ASE9O#F;#|I{yFkBP!Hr?D=TxG{p z5cG|FRA-D<>+|JSD)DM+udyyvV>c&+r|-S7Y0=j|*`=S+Dqt9OJT5H!oDuy{Zm84D zP^7QlYF=*>-PqgK{l5>|C7BFg4`}iYz{1{Hy2Xe0muXyK-Pu=b27yXdOWSip_%8Jf zY%;cMqyQ^HRRR93qw5^s6btrh#sv{`wKeg?ps-fG^8%0NkM|$9jUqIn8^krnk&1I+ zdtwUyLGEtA(L-w$dP54d)G27$-5mWqjwkN90DcDFr9Hm+v=2;&GE9W>+8w+$n1eY@zd0e@LHhg_0ZDHE`0h~1HuZ{`yZsyDVl z#-Mts`cEaa8oCP?>i-G`2c;zbH7rKbIxfYB35Wjk;U=K{?%rz?=k(a`t23;ukGsN< zy%T@}uH}}Y zv)7LGm>R z(Ad)d4IT*|DKIH0X$vJ@NrCu=cLJ@2GaSgbx#ND4=Y;yZE=2Li)!|v{58Gt!QhEXJ zW{3b>SC0O%K*21;jz#gSjXjDeKq>s2Oa~2MkUs4d4liQK9bQJqw?ZisL}kZavO8m} zKiRUd)lQ|G^Y5Iml3TI)($?R{{EvC~$=ZB*{BRmNyZ&UE#UDEkW72u3>M2X54vcg? zX=*ExIImReL!)^%(0@0$b2X*lXJ1xq<8Kr98a3mEjHxm%?^dR@7(9}2i4ar7Dz*Ba zYi|JQkwZq=%LM!fmWdHJ4?Z}=`7O6ub`>>{kdlCRecXxY%jP@*gaAi9!Ld9+zyvs1z6yu?9@Ox6!ISuCjc7 z<%v3ctkqG&v3vYOP*bL!x`SrcxC~yd*aZ#y%@ z(=X7ZS||xi>hMOx=v1ma?bboZzuMI*r6e`MOWT`%m{9h@=0gQ849JnLVXnm80NA*h zz+HId$#*Sd)hTi}&VcF2)+Y&J!6JDtSfRCNAMzbV=XtpVS^s->U0|Nph^x5U^4)KS zb9L*Q+Fs|Nk77?hLyEz%)W>T0=DlR^L|EVp$m^Yi8IxVMX?$i2&PlwaN%^9ws3=Ob zxC5DJiWyOj-6K%Z@7HnY^QFA?@alXdt0q?(N@w?a)S$gyqW9q=4R~3_EowQJx+1GH zDC_qZ+ujVF;E|fL{!MZep9BMNwgJZUQ>{}t1KR`QmP*6mJQ|Z~b8bi>rD$5v_(V@5v~AuquV722{o}|#c7;g3+-064JFy+z6wi7Xw~*z4*gP}wXJK$ zLCF**3GA`knDRlwqGs_SF&;dF*Q)j0m=WMuwC{1~$g#V4TNG}#sGHJd6_HML9)q;Wh0ER%#E}R)ImF<#8sH|RBtBr1< zp`7E`bWHBg7mGl6#SMM2_{hNh#syNwSQec+w&|n+9jQ-hbZPZu=aROlY)^5o`j+|> zwV1HnPR$BLf&A3w$)PwSYSLo?ZET@pso&=}q63dzl^oY8H)DxlZD#*t{5b{WufD2e zq^o?FWB{k@hP$ul&LF4(O%~d(EA4q-N2u50xE)1I*|}omH?8#djmv8hG5g9jVIkIL zx!KE+?RY!~wB+O3ZEo}$Y9%VxGk6;Qp#%{3IJ+I-SWUvKij3-8U9!Na)Njs)?g=Mj zEZo490%kx+$dfu@nZXtG$ZHL!T4$Q5ILj{%55~OOAcx4uF3AhH)Q0Ldue@!}k+{AnA8uctN%Z{g=C=u~DuQp7D>kfIPWRS)1Yt>h);^HI&}j1vjt{-W)i zu|h7m$S+^3JS|xFe<=wtZjbG)uo1K!Q@VmWN!3ky%_z>b{t5#QvjtsiU%Mc@3_j}5 z(e7-_{?6~xGK0{HQ7z_v?Fe@Pz0}SpNTV4XlIBwtoMz%BZcM-ezC{ok+EgpaAcbTD-=R&f4^RdJGY%D7y>i&{s6?dG`@Az!iA&pqTB3Wyf) zpPFUplRSVZQ4>&5LmAOW)H4MK1(@a0Q%}k&l)t(#yq-^Hitx`STgN+!+fPTy{~4Du z8CUPNp?^g_SU)|D*q=kRp#_IoH@FH8WjsclRYx*!kAA859{<>K7$9gtlw4AhZ*ZLL zI&i9hE(OOi6q6U*8*b7~*IB(=j9iWpryQEIyrgUP`?J6r4C!n**Qoq?8b8>(D$?hyD3;a60#Td2R;_7rZBy5I z`{u*UfRU*e>4~^802VD5EX;5BT_=nc@toj(d1gE;?eb!{%MOr#@=d{z5+SvkpivwO zW@$wAx?!hZOwMWDU?-mN_D_#K%y=TS29bl9s??FcjavuhO0Fg0mz^<-Ug2=&mqrk3 zZc6$g%;R^%i?gE+MRfSDG_Lvj_1huLzA_nU4jMRCq#9}K?J1#JnI_}&lhlM!rx&5- zY6~!er!xS1LXU$<9?j-pHjt|qtu<3t16n8^fjZl^n*lG%Zz&NcT?S4pvaBC9hhB+= z>iiSp(U9H-U;qJ(5kV?Ot}c0$31AYw_B2MQfrW-Nf|>+i4)BiuYQmb;KN5^6M%`_s zD}cT}&`qjRoM`QPWv|fdv+XG6DEd&$ zdg!a52{xy`kk>@3VY~UL<^9j^I1kL`1vjXSi|Z|6T>;fQ!EOPWdPxUBO$%toZDJY0 zB635f94%ZTL*!TId7B#rbQ6UxGR0XPlVy+S$4YA@o1-v=_{{zP1{#Jl=6X;wG9C?`G-an)WnCUa-q-Q0{m?Gx&UzC`4=q?J zt}?6J-Pg-pdfg*7hdV~h^r;G`67`8n9!oEpzi<|aIv}*hmxiIU(8y_I(6DN&eM)tc z7g)-2<}Ni2jPb2}wL8kB6&A(ppI-WsT`_SQgj;ahiad79#_p@zQJn>*y(50jk|z`l zP@~j|o9x;4yFJHJFI>{n4g|$wvP!po*4GeQIja^^S?9%H8pg(^okw|g z5sI_hR_l~t-_e_J`CKH{PQ2JWm3uAnuHo&FDwx`<%iy<>pdQz-VO%(`My7jZJ(roz zSiE4fGyx)r3E^zr9CUh>&1Y%9f!OJw}#cOHIK z>YiFssjIkX2SQ~(^I&#(0)`lz%p?f(V_C&(J*M+=tuuEDu&6+<03?B~Cm4qr7MV{< zPAUxEV%Db$sgT`Ur#_SfUD>jqpMQyq^}a@sF(F}~0tPiNJ*+%LB_eu{mBz6^>B%qs zc4QQPX63;w++XT`k}J`>{#)v;cf zJlG>-@-T{46IaltV!(ybaR086Gjfj7S$o3u4d=TS*guy3c<+k{7sfUUJO}3q)2zY? zO(I_XmM=u4R1;o+VdzC=)x@>l6-KM?Q_ilob>UAwqup*gT@i|80Q@R-*1?F)_f!-| zAJyvV?5!}h_3W%Ze_A?(b=*UdMFdq~DgAX>e@Aw?Hw%9fun09UsQo`@66?cL&<8CV z6*A*jhqe_QRxXzGd{}|7t3**H4P}?<>M0;=Cc+OC=I!BYF>+iQ**oRs`z9n>tWmbb ztgz<#ULi&OB}36g*cS@@nfyH9d*7(Y?7@5Ep*yQ&dI1@R5!cQ|L!`gp4 z=80YSFu^FYe8w;ZcCmmPXr?zMonp|65(C$M;O1Dsa^nUX+5UEk?PB#MUCm+3x|3p* z%dBB3H9e_@?z8&tIEXu)Jb>%H`CJ)`9JI6qC|Ob*!4dV#uL`^~|CH7jLu*!=;fgYRq3YkL(SE&6D!Mv; zljc661$O9c)`7H66%wr;DEA<*w=5=_zgBoqI}N9^@iDv2 z2n2w=X8J;?7&#(j480`CC;)6Rvh_*xgWuSPCcYPUO5N&oq_F7N=BAjC!#ox>U4%@P z8(o<2bEac&b<|!%?Gv-N4(T7923BnuGhnr21Z8xF(jr+0w)A(T=-vWC8meIiFcB=X z-5J{wTcgWAH2q<8%%2lh>S@ zAl}kaf$ADbb_U!a7|f#jP3|cy^O@Gsj6jIwqcasQ&%MnLd1Ra?NW$&227AMwD2WN8 zOl?Q$U_JDPHA!ZQl<1-HsnJ+Vr#}dY0t!x0z#(3ws{;ebjdyx`W z!#u>1yhk>NZ-uF4%_Qe!YXR~m>#{9xBWz^0GU|+fq);xbCRpnFc9mzF>S18~0g47( zEgP@7Mn0i0KuaW|+Mvh%1O=_oO`L4ek^%pkON)bCHnbXG9P8<(FEz~6K;#visLAIe z|B7((6E5n|$RvD==MV0{X=P-gHTn)oK{A(#9_)7=bm4&QZ3im2(@N@wHxCaqvS+AJ z3SB=!A%JU#O!M4H^3brAu%X9GK<=&(BsBaU0QuuA0(ekbwGdKPUzx^ZZVf<@3_3;p=^-ac>o)iH+>1?Vb9w1$}eUKG%xaoKQMIB3-rc-91X% z85G&;cXaW3$)lfeH44*64j7?|L+)yFxhuz_WMjwc)(GsG>1rgP7iT4JvSl#MyCPni z*WpgJhpbD6eVpMv8BTQeN4TTN%_~2)Tm}eWlDicM(jzHi|HWIZeg}UuJ&r-Sr9kql zm#Uz7B1%A6!l?Nzio8N=SSvn4#$=lYvM@6%kJPzl^4Pq}mNQ}3!Sw1d6*athDy9{I zBe+Q~!Li#+&-Z9#z$&yPdEt<5_{44y*S#-JS0FXE?&zQEm@jXYfBLvJvbiTnnt-Z! zM1&8`Y_J7XiWEIMg&OHbi!)l|V;j~!wOn$$isxBtKXl(6d1unH;{p!3V4JB53{217 ztSXEtU!2B(5_56|)vEWk$&J2Ykw~e1e!>8wymn9?%pXup{HH%#hC=^$ z{IX7Op+hM7TaukDWPgH54Pv(g?xREVNqwY+beg#*8gh~LP2or@eO%8G9g38al!e;T zx=UK)S!42Mi4QrRxm&xnK#L4kQET(37I{yhS#-N;V%pj2;58K^n32$~+uLQ)$fifx zgO}5b_;BY`DH{|J^RqR?o&B{#GZp%83i_u&7KZO2gjnZ-mGHZu+>-8Gxk@dj&|4Vc zonkhCS!&%}hJRE>>VJMHk$9@(sk%<CGVq^<`n0Kn;B3j-l)ri1L}ra1oTukh~~ zJ3QWt(x|#Ka0fp8c{nk?6pE&yRc7u}=sf9xeC-gs!qs87nA9$O_DC~zPf_<2TRm8> zRzaJ=>Ht#tHv{a4s@LwE`o7T3d2uhY=II6|PjLO3ZKJ-S5mHvq!k!7gZ<3*WS2!yd#@ z1Y@4mQAt{TZNB2cnKJRC&Q`#)dzx_&7c1e6ls1S*X?SNHZ7XI6iUOXA!~#A!pj5-U z6_&!oXJ0HzG7vAvsDNWBCX$JnucgF=Y_8noR7w=~MNE<>)#ImPx^j`3feiV;07rZ(#*nSw_a#OTfpm0%T?v^w<{ z^>?!5o>ad%zHH1bgaP6x9oHjARQ?%pz>=&SUodK-m_loddl(|bY&x*)xe!E3tL-7Hd+;k?kM?2bTmnM#=c( zHB97V$eX6v3;7g1F)O#M^U;dA`rO8LWw82uK8sT%yLnGW;45POtvrba9V(q}|0kU? z1Xxs~Y$TpXnLtrOO}y-;&v9LaJis72%W~_(~_K<^@$pnazBcWxN z+P%~cGMzZ$B}7d*m3KgS3#y7kQ_4GJ1H9Go(`v&RWQQZ1rzTF|wSm_pl(L7BsbnUK zhelH0&wrqtW4aqn0srX<)!DbDsI7YWP3De_Lna7zEf-%&GwclYPoQS5Kb779`3PF4 zPafG4@=ej~q=9uzvlzf0@P~&kl=0J55C~xOw80*-8TS^_qOX8?b{RtW59sO_@+*Y( z!hf%DQ!Qzvy}Ct2!Ig?-{=)kH^H3ngVs{SAO_Hq%X;tJ!$MGPvHQ9Sqr7Zo;ggrU)@VNZK8IL%*LtOIbrC3cNNki9@4|>lStay zExLwq=s1XF+ozH7d#Xy=Y&^Fj^s3;Ze(`OzxCyACDh!ZNwE14(5`)iH2V$=LHvOzQ z_X``KSRujgXT(}I+0??bgD?$J8!t{+q79y@gw3m}t8cjy;?QnotgjFcRHi1cCkuY@ zHx-Z_hSk+N4)3Vi{(kktU_MA2q-#wH$#u%>kAudXD(7|s{h`Q4{gy$tS#QexA@cBI z8mEA@{%|UJsV8Jvf1i-bhIrTc5?fHS^rtm9fmF^AITA(lfz`)wm`E zasu7=PUhH}BIrq|=Ak2<0nl5gMo}lnS27dm!8R+~X61Eqfsj6Qc^#ix>BKDJiq_9m z`i!;a78grVHhtEoOw=$X7=enYkqzy_gp8{U zabvey@*et8^U>&v4Vh(DCKmUBV4`+(%7e{J2|!`lm#Fy}=tq*ciu;H`p`E+=2?^4; zA2b>d+VsnR0MG4UV8fE$WrDF3%>`q7ScKj`uTy#g{Q#X8tfQilE<%xEG6j`iHF(y! zZSey`jkK!H90gCr-E_5sONHgVz&O5KD1L3}2YAJn4yA&%S*lVnIutQX zmkJguFq??bg68P;;PZxuqdmwkr$P?GmdjI3utqW$60i z1`gvHU)8V`<(f&liyC7NmNHjR>CGWpg1n0ZZUN}om`KsGEs9}{{gJKSxT2W4=EKjy z3T;3pzUQAAmI5C1>$kC}Dg$=7tB9r;&}gUtdfBEOSx2Vn=B9f^W^1qU8ii@-Tin)) z$lGY>PFLhZqfW2U8|Y~Id~P?&cNZmg($2RCO!YIy-nB37U^crk37%>cR-g@A!u)XKpgkvsSH4XMYR)uR;FdMUE_Rs*(4p_BRGX`&pfI(g4@;>O^~CV^YFOODs+)q@mFGu`Hr} ziV^CIZXGtMz?Oj@vG_c>pm+K!xFE~1Z-xW83(O(}XH6Bu2^m5f2K^g{-b7dJhTy$` z5m}36CvQO?6moz%))ZWmimf;*VYH9El7=ED%AIlaokP;b(DoizDCPcZ{F}7i#YOXF z3gLtM5}GwRX%$i>0>&RwE_I{C(kCY`7v;#jmBX}Q%^5WtRL4o!6ptFvvB|~&$k+RG zNn)hw`vfolV+5FJRI~ywpa!7!%>o8KOiA{<76^NlJIC6v@1|i+rtv;BD+rZCO*v{Gi&W~`?W9SP z)*x4ebA@fGhN4e9RWAgC|{$hZd=SzY@i)9Y+3hEW#@T47C9O(d2^p7*}RA>3^!9r zIFJ3$xvc|6`twh9*n;CemE00y#vDZyPPAUY-h2s#z>0lzWY!B`L`8_3v3?Zb9Jo>t z31xY}zPrg_KO45jj*cnC-W%h1!&Ec~5i|%AWr6KdiKk3~oV?*lCi= zR}fb2F%WQSmo^GKOXz|fO#lm1viMxp42U+0Xbk3>_WuL+di{^z2Oug zeVw;T;aeA2;?;JZFb|0n5G#7ob;u+s=2tA$?Dkgls|{Cd1T)kce!7;Z8LIJ|5f*Y~ znq(NP;>VbZcAh1^a^lV|@ME=&oB;SG&GY;vy~=1D6Q0|S(vGqyiAiUqpRMM(e;;i_ zY?0*$iF}$ojec&>#Y_C>i^`-9g750|sVaeLiG^PRdvt-1P$cN|=S1SnE-dC^q5lhO zK54mm@~01ZyKzgwC|ntYjV4LpPtAx{xL9RZ`Wd$RvL)&dMyg{0?6K$3l%!ei_!69r zG1^{Ha1mBm)8=(@?H5>?m&Zw10ac>F8Tb^?*&&D}*ft3IlGh-62BD?rOB8GTEH0O+ z_t)v0k5t2y9lw^2?d?0m4ON-b=AR)12MyE=qfIsS0NnN85qJqlymR6K$$*b=s9LW> zgzEW_5JzkD?e`*F9U!^8pY@wqt|K|v7*JX}tEmm)pd{?9yx zN2wXe=xOBqx;h@;gcXEx(@i*VSjERqJuEQR z8n8%_h>i_~4(`exsQX9-Xx7y$SNeR&6Q3`&^0I10)iPG8Xcz*xsi&oF$UzCqEu4O` z8j9mQ2IO*dKB=t~^yAMoT2?d4eylb_g#{jtEgQ2+?Un1tpi~&5!wy&Wl#jxZM#P1- z6r+_lVGN?8%M4Hf2~=JAea6bqqa+KK){;-MWF7Hh(gN3{0#<3+AHa46>z~1u!`%}2mQvbMH}3?Bp+A1)IdVfF zwbh2dS%1@DGi||lru+x7}1NC z1rmdxsXrz$VTb{b=Cvn2-UC%hHHJEBuH%Nj>VZST7*yO8V!_kq@)8ORP22ie2B6G0 z4FWyccTBzvW(ldL#`e~J(JXgmbe!?vk#vL3vt*U0uUwt3fG;t=j5PBH(QmK#cG8@; zUU4-N4;SBRWoGLicdC9n+_E$$yo$IFhK*NmuL6Zx7OrZI$G36g`W~HKU%t7}VX6dB z@+;FLl~T)%&Yrn!(8XjODO}~n95m$s26&5njoViZq~a*1AtqAv9&yH8{)xhx|uaBiNe6byAf*A4mtz+nQ zf#Ouui$}Ns;53?Bg9`177#iW33iwOT&TkGIeJm` z`iR1Nyq^Ejc4yxlHOD=x6rYddAv>CUf4nuFrSDwjt*c|4AT_Fqvu>bS!eSqSM_5HI z8rsJ+%1wL6q6S81WhC$;wrS=lT#QB`rjrCamVGl6g5mAG{e2B>_X+yh?UQEA0NfA{ zq;ciRlcJ*OT*$*j#yIm3D)zgHZhzBVPMH42?RmpyP4*o|jx-5~WN0C9-O+W}zDc15 zMsS{&Uet^fS6HIA#9da3hY`u<3BQUt1;fqhc67PPs9ovZ>F8>M)tN*jJQO0mEo$nu zv`Wsbv76RX-IPTtty{vZ^Dd?_XTdfK02@x?^|0w- z8}1F$74ry9XUwOqleM0og3Jm?ZtFNLTM3)+h>|Xwi2+ zQ2*{e=AxRdQL%_f^xF=5(VYl(L1e=~AI|Q8)G=s6it+)f4w%s(%*;z9*S2lJD;)dS z+Z`xsP0=kl`L#w&i0|4*{=@(lm|dWHNavWhEgJb^<%H(2Mg$4E6L(wIwCAzmQquh) zG_`vI#W{GGCR~D+Y@_6LIsu*-@sE<95LHM|Fdwn>@F7pEgJ@Dsz}}0;AU& z!xb|O*H%(gr7TKQG-?OlI7*4L&4ShVAkQf?TE8bOxdKYIZb{-*oOavDGP^9HO5)3-& z54$f7_6YPmya}J&89qwg!5;5SMx|dZ9Dl zGVkQ3qd7*=?G^oMDyirCbLkrf2MH->KyBuEW*9zT`@(Q+ga8UB9c&;6a#?0KJ-F~9 z{yP8%XE69n@9b;dzKqcn)fQXSEZN=K=6r=I7Nn(%+HeM0Gud`*$m2b{f>Jn( z#1oFMOI9EYVdjO-q{Ytl$j2B|oSn%uTf^eWozDT=L8&4*VgB**)h?H^Yn?cSYX;{}*9 zMMHBGB~%I$Z1!$rHrl#rU(3NCqLGt}%?1UEmBq_qmkBj-nm8$&_?cmetfw-9;Hp;Hqa|b1GI#BIcEN#EUp;dgQ$2 zbo*i;J2A*5I^_JP2M)D1ZD8DzUsB7oft1ZMTH~8Wl`L@~I}7*8u3n~riNJ8+%FjoI zpg$)a@+W1kpGdL~Ha_!lJC5Rhn`K)K3jJ#oQyau#Bw-JR>`m3tXqBIHNeh&X@oRJn z+@eUU{26St{d4cBIek)g@(t+ZH#KStUUI=^*&M0z3CXgdvh=Adkf!IXUGw)TwV8*+ zt*iKL^eu1W?WWY6ITbW6%!C_jZ4mlc3N2!CbmLAyS2Hc&v4dJ;#o>`ve!cy*RWjE} zycBqoXotiFv*a2Qmc>pO&0WLWp}>`~S^F zLB;^Dl9?if_0>VjXd#e+-;jN^dso=37MdO**u(C=zE)#e>srCXJmF463H%@aE&^f$ zEei(7v#74p5ou3HfVuDp_31uh#Ufm-wCrN`7zM|R%CvKmKZ^OdGl3Mb19OQdF+;sc zgy*bkT}?+oP+1YW-`cv}O?I_BPmj(%j?rX0aw{{Nio|jVomnsFkBZ>1;Ft?4C{kvO z54nHqjHjJmi_DF+1pO>lKJb8<%lsX3!2V~O7iL~N8UW2Mc|L7^^@|6NXgZ7)JgTYo zlpiV?kyO@V%#S8iX2LJoQ^6ufoeO}ZULy|UYGvX=^(MwkHYtE; zie)Lo#P$$Usa!^zUWrUtR%ifv`)#0-wo-}$HCwE@mv3SY>TlGKRo1Cu8_x9Uvy&h| zkZ}c6zTB7~PJfU>hSm!q{G2P!!(`>Wm%a=B1R7zMF)ha8(@EYsGb@M)h{1b28l>cT zB?b#(9T&TIM}O^zC_gwL(aWLg37TdS4K=rkge}HRqrkl$Gq<;4_i&XD5vCP*fB^Eo zuH9`nd0nBo+87iw&f@n!pXj*DV{tY^ z_yEo2m5Xf?n}dZ3Og(Z))R$-J&3vZcnNKv|5smrwcnz9tze(#WpLd?((4vdV)}w{Q z3yHUmq0>$yrIll32jZvvv&tgknalaw`G}xsn2P95nxH)=-J}j*T-f-4>Wc|m5B)lS zzLaq(vB@r<2NS!Zi-qJCY~o1Et-NtCuTyillv9lq8yo}k#knp8L-Zl`(Q8e&E9U-D z)tZin=b!tP6S|?k-n!mcGt)b&RJ^gepjK^}8}fDd=A@gJ-}abSwIp<5$REC=&u*07 zdtnp1rSXjO-eq|qKXoJ>uq-P$srDlYRv;T$0vVt96;z1io`yqbap_BByf=!>TM(${ zHos``9co6_51>k^$Iqf(2>}X`l~3XvYG4xtF#cU@$5n8*?D2LR%hK@DOymXQT58q1 z8kiIPkG8XzTMdm`cL~JXX(2WFc5Av~w}h6A!#l>ll>x$rB?{mGosDC>(IP!Xwq-eH6(p`~ znlFDTnWB(c53L|2K%3C4dTyKi3}@>XdiYQ#AnE@#?r9k6v$v|cDp1?p=Wh<=0~Ye{ zjpsuLyQd?OT-N)*>vy~Ii-nyit_@gBMaQ;sV2cAc z@OxBwiXY&~_krwFwlpW}-tw#l`Ek7e&E1^mi4Xi^1K?dXR3D|)G>_~M;geW=c>?TWqLR67ElK#cRQ#&c&>Ku)#i#F!#w)egu(!w5)oMl5r+t{h6B2^L&kMm`*D<#U?S zi%2g*zm&7T`!kq~0U7H%@d80-60Ch=&;7?Skt*t*yiou=l0h)7?a7kvkW}zBN2GgM zq&IRruJ%bvo@&Y8;T>cVFOm@C7(G8P7je{L;@&sN54pti-g?!l@duJIPM*U70#Ntc zB!R5L*FC?!%r?ouLvVpvF4*IMvZEJIh5rSVF!`4Sc110ZTdR2#eCj2~Op^;L zxw%?%3PGih*-qWQWgcJS71}~~XA)AvRjNkT#WAF$3|G~mzXEl01`hxX+>JQEG`3>r z9lOq)a&|jRO6^F+g+lL%GYW>uGAcUCe05gdZ-qD!*_Bp3!mcC)s-Z)s{vnw@inAjP z6szqnCNSsRPKz`JVxTWd=>An`vm&Vwua3MUE(3g|f{gOQdHiv{bpRC;qFdNe$)%!cQ_W3zNo9&@5igZvRa7sl#rv5e z_`-5?$vWw=?Tq6z{am_xD#c{h5)lA9`c^%-jB*sK zT67HcGl-JU;1|l~CKbT{jkj0IwRXT-T04OIN8?i!vd&N3SyEU6Q0?t5>D|A#0a4;vbh1*_1J=g$w z&X9F{8w!Ua9p7V>AO_IS+R;gza|gCq3H7R@ z_yXEj2`7TBf-iXe+ssy&Jmn7Y#18Gu;!vcNgoxl2$RXoea6;`8lQ=n-Qv?-0Rs|2^ zQ=)5=q!m4~&FEUg?^_-Ds^WQ6F>uScexDBshr(Lh-C3Odpcu>!vFKq7o>7(sI4s*N7Geh9iY zhTB*5wQH$zRlG~1D6X>y2xT|0X}Y`j>!~>J90)?}AfE36F*p040NcIy_75PHcS59m zmbxy2$7@A2;+`gA8L5N`^w{h81@yUqKKEGj3LbM<5Btl(!eoj+cGqdu)Ho(!P|rKk z+XZBRE^jvtbhx(AOGJ=KN)ZPTBleY}m>)3~iKs6(P-b!n(o zyPVB&V@v}FPqXoHh@eDV?5SqC0D}Cz05F2v;>He)N3Wg(;FOh>ydjT|@*m-P+&ZUHQ^Kpp`H!Z=vk z0a8jm>RNN9;(ZJYVYK*MXKm2{(!g3Cra{Ynar<-tT_XW$^au9#Q%4r^ z>4){wIvY8-Sb~}390}QYbqD}QrsU;=TnZ3C*oX!1GcoC_yhD}p@YTxA5#-9?9_2LB z&=CWKW&B^U>9EWQa9`NIoZjI*i)nM9knQN*k(`#WW8w}b{t-jO9k|fQ>s1lu}DuX@w5pZxDLeul+JAU^2JLh%4VWk}@hCIng$I4GU*O#PhSn<4h}4`$^yFBcMuii)CVFfi27 zw$Y!?rHoIGk6Xq}<}nJ-z4z^+!f1N0oFAtw1n&x-v<;7?}0$f%z*pdQj{4=rUo z1aF!_%+~p!^CAQGRe6ooi+!!(6uC%?n}2H8dfkkE5r(etU}(WMJEUN6o;0-&_GNrq z3QJIN{6A)jLqo;yF6lXLV9H<}HB6KK&8>WKL*^FFWZSolRd$$4P`T8yBLP6L!?U+1 zc^@^#gzt)_MOIPzlo2-$uS2`fL_B-U!J*S-f8=x|xxOj8ij_FsI5* zSB^sU-K}pW6y9ch1`mdJ0b8Ss@ESoa%9Ip90W<@5AiFppw}qvX^yb@2^y+7(eea5z z@2Hlin!q0>;mk!;lN#nbrmZV$l^~zZQrK37t!eMHaY9=8T)W_`&+dD@xDYSF5jQL;uplmC*L zzH=pp0s*vV9}le7u7;6C!sHR+>wT}Nsw9XWKR1Wq+(N?H!qV^|xHdR(lh+SznjtJR z$TD@G_v?BIIu~@*dLB!}J>k+1E0+`C=X<%i{uH$|0;?IBvlL$w(v5yC8tme9m)G6X zt_eVErPfNSw&~lhfxSk@d=nnqgcoE5sUMSr=Yp9TKPNqd+FWQ6I2+cDXMZb7g?LvC zEp$Eom1-)dS`p!aiVgo#O0@R z&Mep1_;c!lfgZi`z5XTqK>z>@|B%7R7z5xBz?0N3ypmOdb)=jVVMvHm5@1g?o-K<5 j2!^xij=fBMKhMRYPy#B8Q{}Zg|Ha&qP81{{t54Q|)AB3| From 0d6db291de4d2427b6fa0c8624a7001dfd449d93 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 17 Jan 2025 12:30:00 -0500 Subject: [PATCH 03/50] TUNIC: Reorder options (#4491) * Reorder options * Also make ability shuffling on by default --- worlds/tunic/options.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 9a04a137b0..14bf5d8a18 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -29,7 +29,7 @@ class KeysBehindBosses(Toggle): display_name = "Keys Behind Bosses" -class AbilityShuffling(Toggle): +class AbilityShuffling(DefaultOnToggle): """ Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found. If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount. @@ -290,30 +290,37 @@ class LogicRules(Choice): @dataclass class TunicOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool + sword_progression: SwordProgression start_with_sword: StartWithSword keys_behind_bosses: KeysBehindBosses ability_shuffling: AbilityShuffling - shuffle_ladders: ShuffleLadders - entrance_rando: EntranceRando - fixed_shop: FixedShop fool_traps: FoolTraps + laurels_location: LaurelsLocation + hexagon_quest: HexagonQuest hexagon_goal: HexagonGoal extra_hexagon_percentage: ExtraHexagonPercentage - laurels_location: LaurelsLocation + + shuffle_ladders: ShuffleLadders + grass_randomizer: GrassRandomizer + local_fill: LocalFill + + entrance_rando: EntranceRando + fixed_shop: FixedShop + combat_logic: CombatLogic lanternless: Lanternless maskless: Maskless - grass_randomizer: GrassRandomizer - local_fill: LocalFill laurels_zips: LaurelsZips ice_grappling: IceGrappling ladder_storage: LadderStorage ladder_storage_without_items: LadderStorageWithoutItems + plando_connections: TunicPlandoConnections + logic_rules: LogicRules - + tunic_option_groups = [ OptionGroup("Logic Options", [ From 9507300939415f49f1463223c3c0812e16fadbf2 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:53:29 +0100 Subject: [PATCH 04/50] SoE: update to v050 (#4497) * Cuts some cutscenes * Adds meta data for tracker to detect settings --- worlds/soe/options.py | 2 +- worlds/soe/requirements.txt | 74 ++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/worlds/soe/options.py b/worlds/soe/options.py index 5ecd0f9e66..9e0798e1a6 100644 --- a/worlds/soe/options.py +++ b/worlds/soe/options.py @@ -309,7 +309,7 @@ class SoEOptions(PerGameCommonOptions): @property def flags(self) -> str: - flags = '' + flags = 'AGBo' # configures auto-tracker to AP's fill for field in fields(self): option = getattr(self, field.name) if isinstance(option, (EvermizerFlag, EvermizerFlags)): diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 7d1bae0d6a..6a569e83a1 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,37 +1,37 @@ -pyevermizer==0.48.1 \ - --hash=sha256:db85cb4760abfde9d4b566d4613f2eddb8c2ff6f1c202ca0c2c5800bd62c9507 \ - --hash=sha256:1c67d0dff0a42b9a037cdb138c0c7b2c776d8d7425830e7fd32f7ebf8f35ac00 \ - --hash=sha256:d417f5b0407b063496aca43a65389e3308b6d0933c1d7907f7ecc8a00057903b \ - --hash=sha256:abf6560204128783239c8f0fb15059a7c2ff453812f85fb8567766706b7839cc \ - --hash=sha256:39e0cba1de1bc108c5b770ebe0fcbf3f6cb05575daf6bebe78c831c74848d101 \ - --hash=sha256:a16054ce0d904749ef27ede375c0ca8f420831e28c4e84c67361e8181207f00d \ - --hash=sha256:e6de509e4943bcde3e207a3640cad8efe3d8183740b63dc3cdbf5013db0f618b \ - --hash=sha256:e9269cf1290ab2967eaac0bc24e658336fb0e1f6612efce8d7ef0e76c1c26200 \ - --hash=sha256:f69e244229a110183d36b6a43ca557e716016d17e11265dca4070b8857afdb8d \ - --hash=sha256:118d059b8ccd246dafb0a51d0aa8e4543c172f9665378983b9f43c680487732e \ - --hash=sha256:185210c68b16351b3add4896ecfc26fe3867dadee9022f6a256e13093cca4a3b \ - --hash=sha256:10e281612c38bbec11d35f5c09f5a5174fb884cc60e6f16b6790d854e4346678 \ - --hash=sha256:9fc7d7e986243a96e96c1c05a386eb5d2ae4faef1ba810ab7e9e63dd83e86c2b \ - --hash=sha256:c26eafc2230dca9e91aaf925a346532586d0f448456437ea4ce5054e15653fd8 \ - --hash=sha256:8f96ffc5cfbe17b5c08818052be6f96906a1c9d3911e7bc4fbefee9b9ffa8f15 \ - --hash=sha256:e40948cbcaab27aa4febb58054752f83357e81f4a6f088da22a71c4ec9aa7ef2 \ - --hash=sha256:d59369cafa5df0fd2ce5cd5656c926e2fc0226a5a67a003d95497d56a0728dd3 \ - --hash=sha256:345a25675d92aada5d94bc3f3d3e2946efd940a7228628bf8c05d2853ddda86d \ - --hash=sha256:c0aa5054178c5e9900bfcf393c2bffdc69921d165521a3e9e5271528b01ef442 \ - --hash=sha256:719d417fc21778d5036c9d25b7ce55582ab6f49da63ab93ec17d75ea6042364c \ - --hash=sha256:28e220939850cfd8da16743365b28fa36d5bfc1dc58564789ae415e014ebc354 \ - --hash=sha256:770e582000abf64dc7f0c62672e4a1f64729bb20695664c59e29d238398cb865 \ - --hash=sha256:61d451b6f7d76fd435a5e9d2df111533e6e43da397a457f310151917318bd175 \ - --hash=sha256:1c8b596e246bb8437c7fc6c9bb8d9c2c70bd9942f09b06ada02d2fabe596fa0b \ - --hash=sha256:617f3eb0938e71a07b16477529f97fdf64487875462eb2edba6c9820b9686c0a \ - --hash=sha256:98d655a256040a3ae6305145a9692a5483ddcfb9b9bbdb78d43f5e93e002a3ae \ - --hash=sha256:d565bde7b1eb873badeedc2c9f327b4e226702b571aab2019778d46aa4509572 \ - --hash=sha256:e04b89d6edf6ffdbf5c725b0cbf7375c87003378da80e6666818a2b6d59d3fc9 \ - --hash=sha256:cc35e72f2a9e438786451f54532ce663ca63aedc3b4a43532f4ee97b45a71ed1 \ - --hash=sha256:2e4640a975bf324e75f15edd6450e63db8228e2046b893bbdc47d896d5aec890 \ - --hash=sha256:752716024255f13f96e40877b932694a517100a382a13f76c0bed3116b77f6d6 \ - --hash=sha256:d36518349132cf2f3f4e5a6b0294db0b40f395daa620b0938227c2c8f5b1213e \ - --hash=sha256:b5bca6e7fe5dcccd1e8757db4fb20d4bd998ed2b0f4b9ed26f7407c0a9b48d9f \ - --hash=sha256:4663b727d2637ce7713e3db7b68828ca7dc6f03482f4763a055156f3fd16e026 \ - --hash=sha256:7732bec7ffb29337418e62f15dc924e229faf09c55b079ad3f46f47eedc10c0d \ - --hash=sha256:b83a7a4df24800f82844f6acc6d43cd4673de0c24c9041ab56e57f518defa5a1 \ +pyevermizer==0.50.1 \ + --hash=sha256:4d1f43d5f8016e7bfcb5cd80b447a4f278b60b1b250a6153e66150230bf280e8 \ + --hash=sha256:06af4f66ae1f21932a936bf741a0547bbb8ff92eea8fb8efece6bc1760a8a999 \ + --hash=sha256:1ddbc36860704385a767d24364eac6504acc74f185c98b50cf52219c6e0148c6 \ + --hash=sha256:61f0adc4f615867e51bfcd7d7c90f19779a61391a995c721e7393005e8413950 \ + --hash=sha256:d84761ee03ebdaf011befe01638db1fff128b1c37405088868f0025e064977f3 \ + --hash=sha256:0433507dd8ad96375f3b64534faefdf9d325b69a19e108db1414fc75d6e72160 \ + --hash=sha256:e8857f719da9eaaa54f564886ff1b36cb89b8ccf08aa6ccca2d5d3c41da0b067 \ + --hash=sha256:40e76a30968b1fce3d727b47b2693d4151a9ad29b053a33bf06cde8fa63c3d15 \ + --hash=sha256:09ced5349a183656c1f8dcb85e41bdd496d1c5f2bb8f712d12a055d6efa7b917 \ + --hash=sha256:162806e7b0156e25612e60d25af68772cf553b3352a5cf31866d838295ccb591 \ + --hash=sha256:79750965bc63ffa351c167672b51c32f2a8d3242e07e769f925d1f306564a18d \ + --hash=sha256:b1875eb79c8800352f30180db296036d8b512082d6609e2368aa7032c1cf7e27 \ + --hash=sha256:7989e6f06c1ea38687a6b14416b179f459282ea81edbb86086d426fe0d63bf7a \ + --hash=sha256:8a4c5c62997e7378457624a88c12b27b52d345b365c3cfae7fee77ee46eb7cd0 \ + --hash=sha256:a22557f56ada1ace61b781e731e06466c22b6cc605c1aa9dec10e3697b10f5e6 \ + --hash=sha256:d1057e70be839e9c3a91f0f173bc795fc0014cf560767d699cc26eba5f5cfc6f \ + --hash=sha256:8540bd8e8ec49422b494beece1f6bf4cca61aa452a4c0f85c3a8b77283b24753 \ + --hash=sha256:569b98352fc6e1fae85a8c2ee3f2e61276762bc158ac5b7e07a476ee0f9e2617 \ + --hash=sha256:1b21eed21eb9338a6e7024b015d0107eaecf78c61f8ece8e6553d77f7f0ba726 \ + --hash=sha256:51ff863e92c7b608d464da10c775b5df5ad3651a05c2d316c1d60a46572fdef9 \ + --hash=sha256:0f920d745df15e3171412cbda05fc21c9354323d0e8dfc066ed6051fa7df9879 \ + --hash=sha256:d78970415fb03c1dd22aef8da7526e5b33eaf4c9848f5cbad843ad159254f526 \ + --hash=sha256:50536924bbf702d310b92d307d7c5060f6a4307bf99b61f79571ba2675ebb1ff \ + --hash=sha256:1123f8f87ce6415183126842eca1fff98362ff545204adfd4c7b6cf1c396b738 \ + --hash=sha256:1b248af5aa7321e46ae05675b15a5993e28311dfabc68cee2e398ce571f28eb2 \ + --hash=sha256:a76e9d17ec3af9317b3a9d5e9f9f04aea80a5902c33f6fe82d02381f2fd2cb69 \ + --hash=sha256:081ed52f8e1693ca48262cb5a9687ee62c4f9a50c667a487192c72be4c1b7fac \ + --hash=sha256:2978aa13826337d59799f41dda41fa4cecd9f59fae8843613230cf298b06fa6e \ + --hash=sha256:d377c2fd68c3d529d89ba40a762b6424c3b04c0d58593c02f06adbdf236f72ad \ + --hash=sha256:800d6c30eab6ca3ee39a6c297d08cb74cfa5a4bce498aa3f05a612596f8c513b \ + --hash=sha256:0cf40413f4b7ae5d561e47706f446b91440a1b74abe33b8fabc995d92c3325ca \ + --hash=sha256:97791b8695aa215ef407824d1e6c0582a2a2f89f3a0f254f5d791a5a84a0ad00 \ + --hash=sha256:2174db5e4550f94cb63e17584973c9f9afdc23e5230cb556de8bf87bd72145ff \ + --hash=sha256:f3a4cd6a9b292e7385722d8200e834a936886136ddaef2069035f7ec5eb50d34 \ + --hash=sha256:7646efdf7e091c75dac9aebb6c9faf215de4f6b8567c049944790e43cbe63d51 \ + --hash=sha256:cd56cca26ed9675790154dd70402ad28a381fc3c9031bd02eb9b1dad8c317398 \ From 3a46c9fd3e3ff153d956e267c10f1486cff8dcae Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Fri, 17 Jan 2025 20:05:02 +0100 Subject: [PATCH 05/50] LADX: Closing the client window closes the window (#4350) --- LinksAwakeningClient.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index aede742b82..e2e16922fa 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -560,6 +560,10 @@ class LinksAwakeningContext(CommonContext): while self.client.auth == None: await asyncio.sleep(0.1) + + # Just return if we're closing + if self.exit_event.is_set(): + return self.auth = self.client.auth await self.send_connect() From 698d27aada253d11c8df0db1e7d5a7e9d99a6fe7 Mon Sep 17 00:00:00 2001 From: Pierre-Alain BESSERO Date: Fri, 17 Jan 2025 20:06:20 +0100 Subject: [PATCH 06/50] OoT: Allow Crowd Control support for Ocarina of Time (Bizhawk) #4501 Changed the name of the default "receive" function in order to work with Crowd Control --- data/lua/connector_oot.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/lua/connector_oot.lua b/data/lua/connector_oot.lua index 7bec37244b..9df9bdc4c9 100644 --- a/data/lua/connector_oot.lua +++ b/data/lua/connector_oot.lua @@ -1816,7 +1816,7 @@ end -- Main control handling: main loop and socket receive -function receive() +function APreceive() l, e = ootSocket:receive() -- Handle incoming message if e == 'closed' then @@ -1874,7 +1874,7 @@ function main() end if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then if (frame % 30 == 0) then - receive() + APreceive() end elseif (curstate == STATE_UNINITIALIZED) then if (frame % 60 == 0) then From 23ea3c0efc4cd5a9f78436a26e4225aaac527801 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 17 Jan 2025 11:14:21 -0800 Subject: [PATCH 07/50] Core: some low-hanging fruit on the strict type check (#3416) * Core: some low-hanging fruit on the strict type check * bump pyright version * bump pyright version * bump pyright and remove file that's no longer easy --- .github/pyright-config.json | 16 ++++++++++++++-- .github/workflows/strict-type-check.yml | 2 +- test/general/test_helpers.py | 11 +++++++---- test/general/test_memory.py | 2 +- test/general/test_names.py | 4 ++-- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 7d98177890..de7758a715 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -1,8 +1,20 @@ { "include": [ - "type_check.py", + "../BizHawkClient.py", + "../Patch.py", + "../test/general/test_groups.py", + "../test/general/test_helpers.py", + "../test/general/test_memory.py", + "../test/general/test_names.py", + "../test/multiworld/__init__.py", + "../test/multiworld/test_multiworlds.py", + "../test/netutils/__init__.py", + "../test/programs/__init__.py", + "../test/programs/test_multi_server.py", + "../test/utils/__init__.py", + "../test/webhost/test_descriptions.py", "../worlds/AutoSNIClient.py", - "../Patch.py" + "type_check.py" ], "exclude": [ diff --git a/.github/workflows/strict-type-check.yml b/.github/workflows/strict-type-check.yml index bafd572a26..91f4aed92a 100644 --- a/.github/workflows/strict-type-check.yml +++ b/.github/workflows/strict-type-check.yml @@ -26,7 +26,7 @@ jobs: - name: "Install dependencies" run: | - python -m pip install --upgrade pip pyright==1.1.358 + python -m pip install --upgrade pip pyright==1.1.377 python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes - name: "pyright: strict check on specific files" diff --git a/test/general/test_helpers.py b/test/general/test_helpers.py index be84739756..7e850f9744 100644 --- a/test/general/test_helpers.py +++ b/test/general/test_helpers.py @@ -1,6 +1,8 @@ import unittest from typing import Callable, Dict, Optional +from typing_extensions import override + from BaseClasses import CollectionState, MultiWorld, Region @@ -8,6 +10,7 @@ class TestHelpers(unittest.TestCase): multiworld: MultiWorld player: int = 1 + @override def setUp(self) -> None: self.multiworld = MultiWorld(self.player) self.multiworld.game[self.player] = "helper_test_game" @@ -38,15 +41,15 @@ class TestHelpers(unittest.TestCase): "TestRegion1": {"TestRegion2": "connection"}, "TestRegion2": {"TestRegion1": None}, } - + reg_exit_set: Dict[str, set[str]] = { "TestRegion1": {"TestRegion3"} } - + exit_rules: Dict[str, Callable[[CollectionState], bool]] = { "TestRegion1": lambda state: state.has("test_item", self.player) } - + self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions] with self.subTest("Test Location Creation Helper"): @@ -73,7 +76,7 @@ class TestHelpers(unittest.TestCase): entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}" self.assertEqual(exit_rules[exit_reg], self.multiworld.get_entrance(entrance_name, self.player).access_rule) - + for region in reg_exit_set: current_region = self.multiworld.get_region(region, self.player) current_region.add_exits(reg_exit_set[region]) diff --git a/test/general/test_memory.py b/test/general/test_memory.py index e352b9e875..987d19acf3 100644 --- a/test/general/test_memory.py +++ b/test/general/test_memory.py @@ -5,7 +5,7 @@ from . import setup_solo_multiworld class TestWorldMemory(unittest.TestCase): - def test_leak(self): + def test_leak(self) -> None: """Tests that worlds don't leak references to MultiWorld or themselves with default options.""" import gc import weakref diff --git a/test/general/test_names.py b/test/general/test_names.py index 7be76eed4b..8ad74a3354 100644 --- a/test/general/test_names.py +++ b/test/general/test_names.py @@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister class TestNames(unittest.TestCase): - def test_item_names_format(self): + def test_item_names_format(self) -> None: """Item names must not be all numeric in order to differentiate between ID and name in !hint""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): @@ -11,7 +11,7 @@ class TestNames(unittest.TestCase): self.assertFalse(item_name.isnumeric(), f"Item name \"{item_name}\" is invalid. It must not be numeric.") - def test_location_name_format(self): + def test_location_name_format(self) -> None: """Location names must not be all numeric in order to differentiate between ID and name in !hint_location""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): From 2b9fa890509df1d53a68be0758119a767faf8aa8 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 17 Jan 2025 15:22:36 -0500 Subject: [PATCH 08/50] Bizhawk: adds typing to bizhawk component launch (#4505) --- worlds/_bizhawk/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 8d029f92ec..e20b6551cb 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -241,7 +241,7 @@ def _patch_and_run_game(patch_file: str): return {} -def launch(*launch_args) -> None: +def launch(*launch_args: str) -> None: async def main(): parser = get_base_parser() parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") From 1ac8349bd427e7ba2980c4083e28ceaeafa923e8 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:30:18 +0100 Subject: [PATCH 09/50] CI: update pyright (#4506) --- .github/workflows/strict-type-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/strict-type-check.yml b/.github/workflows/strict-type-check.yml index 91f4aed92a..2ccdad8d11 100644 --- a/.github/workflows/strict-type-check.yml +++ b/.github/workflows/strict-type-check.yml @@ -26,7 +26,7 @@ jobs: - name: "Install dependencies" run: | - python -m pip install --upgrade pip pyright==1.1.377 + python -m pip install --upgrade pip pyright==1.1.392.post0 python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes - name: "pyright: strict check on specific files" From 8732974857f620fb05d98f53aee6802a31eb77e5 Mon Sep 17 00:00:00 2001 From: CarlosBor <48533334+CarlosBor@users.noreply.github.com> Date: Sat, 18 Jan 2025 03:38:59 +0100 Subject: [PATCH 10/50] ALttP: update Spanish Setup Docs (#2670) Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/alttp/docs/multiworld_es.md | 241 +++++++++-------------------- 1 file changed, 70 insertions(+), 171 deletions(-) diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md index a8ed11cd32..eade0372ec 100644 --- a/worlds/alttp/docs/multiworld_es.md +++ b/worlds/alttp/docs/multiworld_es.md @@ -1,224 +1,123 @@ # Guía de instalación para A Link to the Past Randomizer Multiworld -
- -
- ## Software requerido -- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) -- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities) -- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES - - Un emulador capaz de ejecutar scripts Lua - ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). +- [SNI](https://github.com/alttpo/sni/releases). Esto está incluido automáticamente en la instalación de Archipelago. +- SNI no es compatible con (Q)Usb2Snes. +- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES, por ejemplo: + - Un emulador capaz de conectarse a SNI + ([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases), + [BSNES-plus](https://github.com/black-sliver/bsnes-plus), [BizHawk](https://tasvideos.org/BizHawk), o - [RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). O, - - Un flashcart SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), o otro hardware compatible + [RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). + - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), u otro hardware compatible. **nota: +Las SNES minis modificadas no tienen soporte de SNI. Algunos usuarios dicen haber tenido éxito con Qusb2Snes para esta consola, +pero no tiene soporte.** - Tu archivo ROM japones v1.0, probablemente se llame `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc` ## Procedimiento de instalación -### Instalación en Windows - -1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente. - **El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu - intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe` - - Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar ' - Setup.BerserkerMultiWorld.Doors.exe' - - Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías - instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del - archivo una segunda vez. - - Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador ( - posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación. - -2. Si estas usando un emulador, deberías asignar la versión capaz de ejecutar scripts Lua como programa por defecto para - lanzar ficheros de ROM de SNES. - 1. Extrae tu emulador al escritorio, o cualquier sitio que después recuerdes. - 2. Haz click derecho en un fichero de ROM (ha de tener la extensión sfc) y selecciona **Abrir con...** - 3. Marca la opción **Usar siempre esta aplicación para abrir los archivos .sfc** - 4. Baja hasta el final de la lista y haz click en la opción **Buscar otra aplicación en el equipo** (Si usas Windows - 10 es posible que debas hacer click en **Más aplicaciones**) - 5. Busca el archivo .exe de tu emulador y haz click en **Abrir**. Este archivo debe estar en el directorio donde - extrajiste en el paso 1. - -### Instalación en Macintosh - -- ¡Necesitamos voluntarios para rellenar esta seccion! Contactad con **Farrak Kilhn** (en inglés) en Discord si queréis - ayudar. - -## Configurar tu archivo YAML - -### Que es un archivo YAML y por qué necesito uno? - -Tu archivo YAML contiene un conjunto de opciones de configuración que proveen al generador con información sobre como -debe generar tu juego. Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta configuración -permite que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida -de multiworld puede tener diferentes opciones. - -### Donde puedo obtener un fichero YAML? - -La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu -configuración personal y descargar un fichero "YAML". - -### Configuración YAML avanzada - -Una version mas avanzada del fichero Yaml puede ser creada usando la pagina -["Weighted settings"](/games/A Link to the Past/weighted-options), -la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones -representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser -elegidos sobre otros de la misma. - -Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada -sub-opción. Ademas imaginemos que tu valor elegido para "on" es 20 y el elegido para "off" es 40. - -Por tanto, en este ejemplo, habrán 60 trozos de papel. 20 para "on" y 40 para "off". Cuando el generador esta decidiendo -si activar o no "map shuffle" para tu partida, meterá la mano en el cubo y sacara un trozo de papel al azar. En este -ejemplo, es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado. - -Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción -debe tener al menos un valor mayor que cero, si no la generación fallará. - -### Verificando tu archivo YAML - -Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina -[YAML Validator](/check). - -## Generar una partida para un jugador - -1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz - click en el boton "Generate game". -2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche. -3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no - es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld - WebUI") que se ha abierto automáticamente. - -## Unirse a una partida MultiWorld +1. Descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest). + **El archivo del instalador se encuentra en la sección de assets al final de la información de version**. +2. La primera vez que realices una generación local o parchees tu juego, se te pedirá que ubiques tu archivo ROM base. + Este es tu archivo ROM de Link to the Past japonés. Esto sólo debe hacerse una vez. + +4. Si estás usando un emulador, deberías de asignar tu emulador con compatibilidad con Lua como el programa por defecto para abrir archivos + ROM. + 1. Extrae la carpeta de tu emulador al Escritorio, o algún otro sitio que vayas a recordar. + 2. Haz click derecho en un archivo ROM y selecciona **Abrir con...** + 3. Marca la casilla junto a **Usar siempre este programa para abrir archivos .sfc** + 4. Baja al final de la lista y haz click en el texto gris **Buscar otro programa en este PC** + 5. Busca el archivo `.exe` de tu emulador y haz click en **Abrir**. Este archivo debería de encontrarse dentro de la carpeta que + extrajiste en el paso uno. ### Obtener el fichero de parche y crea tu ROM -Cuando te unes a una partida multiworld, debes proveer tu fichero YAML a quien sea el creador de la partida. Una vez +Cuando te unas a una partida multiworld, se te pedirá enviarle tu archivo de configuración a quien quiera que esté creando. Una vez eso este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros -de parche de la partida Tu fichero de parche debe tener la extensión `.aplttp`. +de parche de la partida. Tu fichero de parche debe de tener la extensión `.aplttp`. -Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y haz doble click. Esto debería ejecutar -automáticamente el cliente, y ademas creara la rom en el mismo directorio donde este el fichero de parche. +Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y hazle doble click. Esto debería ejecutar +automáticamente el cliente, y además creará la rom en el mismo directorio donde este el fichero de parche. ### Conectar al cliente #### Con emulador -Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo -ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación. +Cuando el cliente se lance automáticamente, SNI debería de ejecutarse en segundo plano. Si es la +primera vez que se ejecuta, tal vez se te pida permitir que se comunique a través del firewall de Windows + +#### snes9x-nwa + +1. Haz click en el menu Network y marca 'Enable Emu Network Control +2. Carga tu archivo ROM si no lo habías hecho antes ##### snes9x-rr -1. Carga tu fichero de ROM, si no lo has hecho ya +1. Carga tu fichero ROM, si no lo has hecho ya 2. Abre el menu "File" y situa el raton en **Lua Scripting** 3. Haz click en **New Lua Script Window...** 4. En la nueva ventana, haz click en **Browse...** -5. Navega hacia el directorio donde este situado snes9x-rr, entra en el directorio `lua`, y - escoge `multibridge.lua` -6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo - nombre en la esquina superior izquierda. +5. Selecciona el archivo lua conector incluido con tu cliente + - Busca en la carpeta de Archipelago `/SNI/lua/`. +6. Si ves un error mientras carga el script que dice `socket.dll missing` o algo similar, ve a la carpeta de +el lua que estas usando en tu gestor de archivos y copia el `socket.dll` a la raíz de tu instalación de snes9x. + +##### BNES-Plus + +1. Cargue su archivo ROM si aún no se ha cargado. +2. El emulador debería conectarse automáticamente mientras SNI se está ejecutando. ##### BizHawk -1. Asegurate que se ha cargado el nucleo BSNES. Debes hacer esto en el menu Tools y siguiento estas opciones: - `Config --> Cores --> SNES --> BSNES` - Una vez cambiado el nucleo cargado, BizHawk ha de ser reiniciado. +1. Asegurate que se ha cargado el núcleo BSNES. Se hace en la barra de menú principal, bajo: + - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES` + - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+` 2. Carga tu fichero de ROM, si no lo has hecho ya. -3. Haz click en el menu Tools y en la opción **Lua Console** -4. Haz click en el botón para abrir un nuevo script Lua. -5. Navega al directorio de instalación de MultiWorld Utilities, y en los siguiente directorios: - `QUsb2Snes/Qusb2Snes/LuaBridge` -6. Selecciona `luabridge.lua` y haz click en Abrir. -7. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo - nombre en la esquina superior izquierda. + Si has cambiado tu preferencia de núcleo tras haber cargado la ROM, no te olvides de volverlo a cargar (atajo por defecto: Ctrl+R). +3. Arrastra el archivo `Connector.lua` que has descargado a la ventana principal de EmuHawk. + - Busca en la carpeta de Archipelago `/SNI/lua/`. + - También podrías abrir la consola de Lua manualmente, hacer click en `Script` 〉 `Open Script`, e ir a `Connector.lua` + con el selector de archivos. ##### RetroArch 1.10.1 o más nuevo -Sólo hay que segiur estos pasos una vez. +Sólo hay que seguir estos pasos una vez. 1. Comienza en la pantalla del menú principal de RetroArch. 2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON. -3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el - default) el Puerto de comandos de red. +3. Ve a Ajustes --> Red. Pon "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 el valor por defecto, + el Puerto de comandos de red. ![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) 4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES / SFC (bsnes-mercury Performance)". -Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los sólos núcleos que permiten +Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los únicos núcleos que permiten que herramientas externas lean datos del ROM. #### Con Hardware -Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, hazlo ahora. Los +Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, por favor hazlo ahora. Los usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado -[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información +[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Puede que los usuarios de otros dispositivos encuentren informacion útil [en esta página](http://usb2snes.com/#supported-platforms). 1. Cierra tu emulador, el cual debe haberse autoejecutado. -2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente. -3. Ejecuta la version correcta de QUsb2Snes (v0.7.16). -4. Enciende tu dispositivo y carga la ROM. -5. Observa en el cliente que ahora muestra "SNES Device: Connected", y aparece el nombre del dispositivo. +2. Enciende tu dispositivo y carga la ROM. -### Conecta al MultiServer +### Conecta al Servidor Archipelago -El fichero de parche que ha lanzado el cliente debe haberte conectado automaticamente al MultiServer. Hay algunas -razonas por las que esto puede que no pase, incluyendo que el juego este hospedado en el sitio web pero se genero en -algún otro sitio. Si el cliente muestra "Server Status: Not Connected", preguntale al creador de la partida la dirección -del servidor, copiala en el campo "Server" y presiona Enter. +El fichero de parche que ha lanzado el cliente debería de haberte conectado automaticamente al MultiServer. Sin embargo hay algunas +razones por las que puede que esto no suceda, como que la partida este hospedada en la página web pero generada en otra parte. Si la +ventana del cliente muestra "Server Status: Not Connected", simplemente preguntale al creador de la partida la dirección +del servidor, cópiala en el campo "Server" y presiona Enter. -El cliente intentara conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" en algún momento. -Si el cliente no se conecta al cabo de un rato, puede ser que necesites refrescar la pagina web. +El cliente intentará conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" momentáneamente. -### Jugando +### Jugar al juego -Cuando ambos SNES Device and Server aparezcan como "connected", estas listo para empezar a jugar. Felicidades por unirte -satisfactoriamente a una partida de multiworld! - -## Hospedando una partida de multiworld - -La manera recomendad para hospedar una partida es usar el servicio proveído en -[el sitio web](/generate). El proceso es relativamente sencillo: - -1. Recolecta los ficheros YAML de todos los jugadores que participen. -2. Crea un fichero ZIP conteniendo esos ficheros. -3. Carga el fichero zip en el sitio web enlazado anteriormente. -4. Espera a que la seed sea generada. -5. Cuando esto acabe, se te redigirá a una pagina titulada "Seed Info". -6. Haz click en "Create New Room". Esto te llevara a la pagina del servidor. Pasa el enlace a esta pagina a los - jugadores para que puedan descargar los ficheros de parche de ahi. - **Nota:** Los ficheros de parche de esta pagina permiten a los jugadores conectarse al servidor automaticamente, - mientras que los de la pagina "Seed info" no. -7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este - enlace a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar - este enlace. -8. Una vez todos los jugadores se han unido, podeis empezar a jugar. - -## Auto-Tracking - -Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad. -El programa recomentdado actualmente es: -[OpenTracker](https://github.com/trippsc2/OpenTracker/releases). - -### Instalación - -1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi"). -2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace este - programa se muestra durante la proceso, y debe ser ejecutado manualmente. - -### Activar auto-tracking - -1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige ** - AutoTracker...** -2. Click the **Get Devices** button -3. Selecciona tu "SNES device" de la lista -4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal - Tracking** -5. Haz click en el boton **Start Autotracking** -6. Cierra la ventana AutoTracker, ya que deja de ser necesaria +Cuando el cliente muestre tanto el dispositivo SNES como el servidor como conectados, estas listo para empezar a jugar. Felicidades por +haberte unido a una partida multiworld con exito! Puedes ejecutar varios comandos en tu cliente. Para mas informacion +acerca de estos comando puedes usar `/help` para comandos locales del cliente y `!help` para comandos de servidor. From 005a143e3e1add0e781cbb87c696bdb2413e8ee1 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:59:26 +0100 Subject: [PATCH 11/50] MultiServer: Add slot to SetReply packets (#3747) * Add slot to datastorage set response * update docs as well --- MultiServer.py | 1 + docs/network protocol.md | 1 + 2 files changed, 2 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index 81426cb132..653c2ecaab 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1992,6 +1992,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): args["cmd"] = "SetReply" value = ctx.stored_data.get(args["key"], args.get("default", 0)) args["original_value"] = copy.copy(value) + args["slot"] = client.slot for operation in args["operations"]: func = modify_functions[operation["operation"]] value = func(value, operation["value"]) diff --git a/docs/network protocol.md b/docs/network protocol.md index 160f83031c..e32c266ffb 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -261,6 +261,7 @@ Sent to clients in response to a [Set](#Set) package if want_reply was set to tr | key | str | The key that was updated. | | value | any | The new value for the key. | | original_value | any | The value the key had before it was updated. Not present on "_read" prefixed special keys. | +| slot | int | The slot that originally sent the Set package causing this change. | Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along. From 1c9409cac9e4612b8287d9499124b9e71872e197 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 19 Jan 2025 00:26:42 +0100 Subject: [PATCH 12/50] CommonClient: implement check_locations to send missing locations only (#4484) Co-authored-by: Scipio Wright --- CommonClient.py | 7 +++++++ worlds/alttp/Client.py | 2 +- worlds/factorio/Client.py | 5 ++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index fc6ae6d9a5..b43bf57d19 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -459,6 +459,13 @@ class CommonContext: await self.send_msgs([payload]) await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) + async def check_locations(self, locations: typing.Collection[int]) -> set[int]: + """Send new location checks to the server. Returns the set of actually new locations that were sent.""" + locations = set(locations) & self.missing_locations + if locations: + await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}]) + return locations + async def console_input(self) -> str: if self.ui: self.ui.focus_textinput() diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index a0b28829f4..78745f438b 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -464,7 +464,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.") return False else: - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}]) + await ctx.check_locations(new_locations) await snes_flush_writes(ctx) return True diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 3c35c4cb09..ac58339c5e 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -234,8 +234,7 @@ async def game_watcher(ctx: FactorioContext): f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") else: data = data["info"] - research_data = data["research_done"] - research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} + research_data: set[int] = {int(tech_name.split("-")[1]) for tech_name in data["research_done"]} victory = data["victory"] await ctx.update_death_link(data["death_link"]) ctx.multiplayer = data.get("multiplayer", False) @@ -249,7 +248,7 @@ async def game_watcher(ctx: FactorioContext): f"New researches done: " f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) + await ctx.check_locations(research_data) death_link_tick = data.get("death_link_tick", 0) if death_link_tick != ctx.death_link_tick: ctx.death_link_tick = death_link_tick From 992f1925296f7fe2e60b4c5358e810b640dadc3d Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sat, 18 Jan 2025 20:36:01 -0500 Subject: [PATCH 13/50] Stardew Valley: Improve generation performance by around 11% by moving calculating from rule evaluation to collect (#4231) --- worlds/stardew_valley/__init__.py | 40 +++++++++++------ worlds/stardew_valley/stardew_rule/state.py | 45 +++---------------- .../strings/ap_names/event_names.py | 2 + .../stardew_valley/test/rules/TestShipping.py | 11 +++-- 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 9da650520f..ef842263ad 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,6 +1,6 @@ import logging from random import Random -from typing import Dict, Any, Iterable, Optional, Union, List, TextIO +from typing import Dict, Any, Iterable, Optional, List, TextIO from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from Options import PerGameCommonOptions @@ -88,7 +88,6 @@ class StardewValleyWorld(World): randomized_entrances: Dict[str, str] total_progression_items: int - excluded_from_total_progression_items: List[str] = [Event.received_walnuts] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) @@ -176,7 +175,7 @@ class StardewValleyWorld(World): if self.options.season_randomization == SeasonRandomization.option_disabled: for season in season_pool: - self.multiworld.push_precollected(self.create_starting_item(season)) + self.multiworld.push_precollected(self.create_item(season)) return if [item for item in self.multiworld.precollected_items[self.player] @@ -186,12 +185,12 @@ class StardewValleyWorld(World): if self.options.season_randomization == SeasonRandomization.option_randomized_not_winter: season_pool = [season for season in season_pool if season.name != "Winter"] - starting_season = self.create_starting_item(self.random.choice(season_pool)) + starting_season = self.create_item(self.random.choice(season_pool)) self.multiworld.push_precollected(starting_season) def precollect_farm_type_items(self): if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive: - self.multiworld.push_precollected(self.create_starting_item("Progressive Coop")) + self.multiworld.push_precollected(self.create_item("Progressive Coop")) def setup_logic_events(self): def register_event(name: str, region: str, rule: StardewRule): @@ -271,7 +270,7 @@ class StardewValleyWorld(World): def get_all_location_names(self) -> List[str]: return list(location.name for location in self.multiworld.get_locations(self.player)) - def create_item(self, item: Union[str, ItemData], override_classification: ItemClassification = None) -> StardewItem: + def create_item(self, item: str | ItemData, override_classification: ItemClassification = None) -> StardewItem: if isinstance(item, str): item = item_table[item] @@ -280,12 +279,6 @@ class StardewValleyWorld(World): return StardewItem(item.name, override_classification, item.code, self.player) - def create_starting_item(self, item: Union[str, ItemData]) -> StardewItem: - if isinstance(item, str): - item = item_table[item] - - return StardewItem(item.name, item.classification, item.code, self.player) - def create_event_location(self, location_data: LocationData, rule: StardewRule = None, item: Optional[str] = None): if rule is None: rule = True_() @@ -393,9 +386,19 @@ class StardewValleyWorld(World): if not change: return False + player_state = state.prog_items[self.player] + + received_progression_count = player_state[Event.received_progression_item] + received_progression_count += 1 + if self.total_progression_items: + # Total progression items is not set until all items are created, but collect will be called during the item creation when an item is precollected. + # We can't update the percentage if we don't know the total progression items, can't divide by 0. + player_state[Event.received_progression_percent] = received_progression_count * 100 // self.total_progression_items + player_state[Event.received_progression_item] = received_progression_count + walnut_amount = self.get_walnut_amount(item.name) if walnut_amount: - state.prog_items[self.player][Event.received_walnuts] += walnut_amount + player_state[Event.received_walnuts] += walnut_amount return True @@ -404,9 +407,18 @@ class StardewValleyWorld(World): if not change: return False + player_state = state.prog_items[self.player] + + received_progression_count = player_state[Event.received_progression_item] + received_progression_count -= 1 + if self.total_progression_items: + # We can't update the percentage if we don't know the total progression items, can't divide by 0. + player_state[Event.received_progression_percent] = received_progression_count * 100 // self.total_progression_items + player_state[Event.received_progression_item] = received_progression_count + walnut_amount = self.get_walnut_amount(item.name) if walnut_amount: - state.prog_items[self.player][Event.received_walnuts] -= walnut_amount + player_state[Event.received_walnuts] -= walnut_amount return True diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index 6fc349a627..d60f08ac4c 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -4,6 +4,7 @@ from typing import Iterable, Union, List, Tuple, Hashable, TYPE_CHECKING from BaseClasses import CollectionState from .base import BaseStardewRule, CombinableStardewRule from .protocol import StardewRule +from ..strings.ap_names.event_names import Event if TYPE_CHECKING: from .. import StardewValleyWorld @@ -87,45 +88,13 @@ class Reach(BaseStardewRule): return f"Reach {self.resolution_hint} {self.spot}" -@dataclass(frozen=True) -class HasProgressionPercent(CombinableStardewRule): - player: int - percent: int +class HasProgressionPercent(Received): + def __init__(self, player: int, percent: int): + super().__init__(Event.received_progression_percent, player, percent, event=True) def __post_init__(self): - assert self.percent > 0, "HasProgressionPercent rule must be above 0%" - assert self.percent <= 100, "HasProgressionPercent rule can't require more than 100% of items" - - @property - def combination_key(self) -> Hashable: - return HasProgressionPercent.__name__ - - @property - def value(self): - return self.percent - - def __call__(self, state: CollectionState) -> bool: - stardew_world: "StardewValleyWorld" = state.multiworld.worlds[self.player] - total_count = stardew_world.total_progression_items - needed_count = (total_count * self.percent) // 100 - player_state = state.prog_items[self.player] - - if needed_count <= len(player_state) - len(stardew_world.excluded_from_total_progression_items): - return True - - total_count = 0 - for item, item_count in player_state.items(): - if item in stardew_world.excluded_from_total_progression_items: - continue - - total_count += item_count - if total_count >= needed_count: - return True - - return False - - def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: - return self, self(state) + assert self.count > 0, "HasProgressionPercent rule must be above 0%" + assert self.count <= 100, "HasProgressionPercent rule can't require more than 100% of items" def __repr__(self): - return f"Received {self.percent}% progression items" + return f"Received {self.count}% progression items" diff --git a/worlds/stardew_valley/strings/ap_names/event_names.py b/worlds/stardew_valley/strings/ap_names/event_names.py index b7881b3bfd..68f000bdc3 100644 --- a/worlds/stardew_valley/strings/ap_names/event_names.py +++ b/worlds/stardew_valley/strings/ap_names/event_names.py @@ -10,3 +10,5 @@ class Event: victory = event("Victory") received_walnuts = event("Received Walnuts") + received_progression_item = event("Received Progression Item") + received_progression_percent = event("Received Progression Percent") diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py index b26d1e94ee..125b7f31d0 100644 --- a/worlds/stardew_valley/test/rules/TestShipping.py +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -69,14 +69,17 @@ class TestShipsanityEverything(SVTestBase): def test_all_shipsanity_locations_require_shipping_bin(self): bin_name = "Shipping Bin" self.collect_all_except(bin_name) - shipsanity_locations = [location for location in self.get_real_locations() if - LocationTags.SHIPSANITY in location_table[location.name].tags] + shipsanity_locations = [location + for location in self.get_real_locations() + if LocationTags.SHIPSANITY in location_table[location.name].tags] bin_item = self.create_item(bin_name) + for location in shipsanity_locations: with self.subTest(location.name): - self.remove(bin_item) self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) - self.multiworld.state.collect(bin_item) + + self.collect(bin_item) shipsanity_rule = self.world.logic.region.can_reach_location(location.name) self.assert_rule_true(shipsanity_rule, self.multiworld.state) + self.remove(bin_item) From 0bb657d2c867ae3edc6a3145843ffe235de2ae4a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 19 Jan 2025 01:21:54 -0800 Subject: [PATCH 14/50] Pokemon Emerald: Use new check_locations helper (#4518) --- worlds/pokemon_emerald/client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 5add7b3fca..411fdd1a33 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -287,7 +287,7 @@ class PokemonEmeraldClient(BizHawkClient): pokedex_caught_bytes = read_result[0] game_clear = False - local_checked_locations = set() + local_checked_locations: set[int] = set() local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS} local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS} defeated_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()} @@ -350,10 +350,7 @@ class PokemonEmeraldClient(BizHawkClient): self.local_checked_locations = local_checked_locations if local_checked_locations is not None: - await ctx.send_msgs([{ - "cmd": "LocationChecks", - "locations": list(local_checked_locations), - }]) + await ctx.check_locations(local_checked_locations) # Send game clear if not ctx.finished_game and game_clear: From 9183e8f9c9fcc124cf5b8a4b1cc9fc58056dc3fa Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 19 Jan 2025 01:23:06 -0800 Subject: [PATCH 15/50] BizHawkClient: Use built-ins for typing (#4508) --- worlds/_bizhawk/__init__.py | 26 +++++++++++++------------- worlds/_bizhawk/client.py | 12 ++++++------ worlds/_bizhawk/context.py | 10 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index e7b8edc0b6..b10e33d396 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -10,7 +10,7 @@ import base64 import enum import json import sys -import typing +from typing import Any, Sequence BIZHAWK_SOCKET_PORT_RANGE_START = 43055 @@ -44,10 +44,10 @@ class SyncError(Exception): class BizHawkContext: - streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] + streams: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None connection_status: ConnectionStatus _lock: asyncio.Lock - _port: typing.Optional[int] + _port: int | None def __init__(self) -> None: self.streams = None @@ -122,12 +122,12 @@ async def get_script_version(ctx: BizHawkContext) -> int: return int(await ctx._send_message("VERSION")) -async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]: +async def send_requests(ctx: BizHawkContext, req_list: list[dict[str, Any]]) -> list[dict[str, Any]]: """Sends a list of requests to the BizHawk connector and returns their responses. It's likely you want to use the wrapper functions instead of this.""" responses = json.loads(await ctx._send_message(json.dumps(req_list))) - errors: typing.List[ConnectorError] = [] + errors: list[ConnectorError] = [] for response in responses: if response["type"] == "ERROR": @@ -180,7 +180,7 @@ async def get_system(ctx: BizHawkContext) -> str: return res["value"] -async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]: +async def get_cores(ctx: BizHawkContext) -> dict[str, str]: """Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have entries.""" res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0] @@ -233,8 +233,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None: raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") -async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]], - guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]: +async def guarded_read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]], + guard_list: Sequence[tuple[int, Sequence[int], str]]) -> list[bytes] | None: """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected value. @@ -262,7 +262,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu "domain": domain } for address, size, domain in read_list]) - ret: typing.List[bytes] = [] + ret: list[bytes] = [] for item in res: if item["type"] == "GUARD_RESPONSE": if not item["value"]: @@ -276,7 +276,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu return ret -async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]: +async def read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]]) -> list[bytes]: """Reads data at 1 or more addresses. Items in `read_list` should be organized `(address, size, domain)` where @@ -288,8 +288,8 @@ async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, return await guarded_read(ctx, read_list, []) -async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]], - guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool: +async def guarded_write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]], + guard_list: Sequence[tuple[int, Sequence[int], str]]) -> bool: """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. Items in `write_list` should be organized `(address, value, domain)` where @@ -326,7 +326,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing. return True -async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None: +async def write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]]) -> None: """Writes data to 1 or more addresses. Items in write_list should be organized `(address, value, domain)` where diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index 415b663e60..ce75b864b8 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -5,7 +5,7 @@ A module containing the BizHawkClient base class and metaclass from __future__ import annotations import abc -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, ClassVar from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess @@ -24,9 +24,9 @@ components.append(component) class AutoBizHawkClientRegister(abc.ABCMeta): - game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {} + game_handlers: ClassVar[dict[tuple[str, ...], dict[str, BizHawkClient]]] = {} - def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister: + def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> AutoBizHawkClientRegister: new_class = super().__new__(cls, name, bases, namespace) # Register handler @@ -54,7 +54,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta): return new_class @staticmethod - async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]: + async def get_handler(ctx: "BizHawkClientContext", system: str) -> BizHawkClient | None: for systems, handlers in AutoBizHawkClientRegister.game_handlers.items(): if system in systems: for handler in handlers.values(): @@ -65,13 +65,13 @@ class AutoBizHawkClientRegister(abc.ABCMeta): class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): - system: ClassVar[Union[str, Tuple[str, ...]]] + system: ClassVar[str | tuple[str, ...]] """The system(s) that the game this client is for runs on""" game: ClassVar[str] """The game this client is for""" - patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]] + patch_suffix: ClassVar[str | tuple[str, ...] | None] """The file extension(s) this client is meant to open and patch (e.g. ".apz3")""" @abc.abstractmethod diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index e20b6551cb..cb59050b84 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -6,7 +6,7 @@ checking or launching the client, otherwise it will probably cause circular impo import asyncio import enum import subprocess -from typing import Any, Dict, Optional +from typing import Any from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled import Patch @@ -43,15 +43,15 @@ class BizHawkClientContext(CommonContext): command_processor = BizHawkClientCommandProcessor auth_status: AuthStatus password_requested: bool - client_handler: Optional[BizHawkClient] - slot_data: Optional[Dict[str, Any]] = None - rom_hash: Optional[str] = None + client_handler: BizHawkClient | None + slot_data: dict[str, Any] | None = None + rom_hash: str | None = None bizhawk_ctx: BizHawkContext watcher_timeout: float """The maximum amount of time the game watcher loop will wait for an update from the server before executing""" - def __init__(self, server_address: Optional[str], password: Optional[str]): + def __init__(self, server_address: str | None, password: str | None): super().__init__(server_address, password) self.auth_status = AuthStatus.NOT_AUTHENTICATED self.password_requested = False From 9e353ebb8e2661bcdc95b11ff6520bcb6379368f Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:17:12 -0600 Subject: [PATCH 16/50] =?UTF-8?q?SMZ3:=20Fix=20Itemlinks=20with=20link=5Fr?= =?UTF-8?q?eplacement=C2=A0#4099?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/smz3/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 5998db8e65..7ebec7d4e4 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -87,6 +87,21 @@ class SMZ3World(World): self.rom_name_available_event = threading.Event() self.locations: Dict[str, Location] = {} self.unreachable = [] + self.junkItemsNames = [item.name for item in [ + ItemType.Arrow, + ItemType.OneHundredRupees, + ItemType.TenArrows, + ItemType.ThreeBombs, + ItemType.OneRupee, + ItemType.FiveRupees, + ItemType.TwentyRupees, + ItemType.FiftyRupees, + ItemType.ThreeHundredRupees, + ItemType.ETank, + ItemType.Missile, + ItemType.Super, + ItemType.PowerBomb + ]] super().__init__(world, player) @classmethod From cbf4bbbca8633592c93e3352b0d3d5027f1002f4 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:17:31 -0500 Subject: [PATCH 17/50] OoT Adjuster: Remove per_slot_randoms (#4264) --- OoTAdjuster.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/OoTAdjuster.py b/OoTAdjuster.py index 9519b191e7..1581d65398 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -1,7 +1,6 @@ import tkinter as tk import argparse import logging -import random import os import zipfile from itertools import chain @@ -197,7 +196,6 @@ def set_icon(window): def adjust(args): # Create a fake multiworld and OOTWorld to use as a base multiworld = MultiWorld(1) - multiworld.per_slot_randoms = {1: random} ootworld = OOTWorld(multiworld, 1) # Set options in the fake OOTWorld for name, option in chain(cosmetic_options.items(), sfx_options.items()): From 94438618495c0f9b051caea21ddb5d69c8c38c56 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sun, 19 Jan 2025 23:20:45 +0000 Subject: [PATCH 18/50] Zillion: Finalize item locations in either generate_output or fill_slot_data (#4121) Co-authored-by: Doug Hoskisson --- test/general/test_implemented.py | 2 +- worlds/zillion/__init__.py | 36 +++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 7abf995993..1082a02912 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -39,7 +39,7 @@ class TestImplemented(unittest.TestCase): """Tests that if a world creates slot data, it's json serializable.""" for game_name, world_type in AutoWorldRegister.world_types.items(): # has an await for generate_output which isn't being called - if game_name in {"Ocarina of Time", "Zillion"}: + if game_name in {"Ocarina of Time"}: continue multiworld = setup_solo_multiworld(world_type) with self.subTest(game=game_name, seed=multiworld.seed): diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 5a4e2bb48f..6fa5f86d07 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -119,8 +119,13 @@ class ZillionWorld(World): """ my_locations: list[ZillionLocation] = [] """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ - slot_data_ready: threading.Event - """ This event is set in `generate_output` when the data is ready for `fill_slot_data` """ + finalized_gen_data: GenData | None + """ Finalized generation data needed by `generate_output` and by `fill_slot_data`. """ + item_locations_finalization_lock: threading.Lock + """ + This lock is used in `generate_output` and `fill_slot_data` to ensure synchronized access to `finalized_gen_data`, + so that whichever is run first can finalize the item locations while the other waits. + """ logic_cache: ZillionLogicCache | None = None def __init__(self, world: MultiWorld, player: int) -> None: @@ -128,7 +133,8 @@ class ZillionWorld(World): self.logger = logging.getLogger("Zillion") self.lsi = ZillionWorld.LogStreamInterface(self.logger) self.zz_system = System() - self.slot_data_ready = threading.Event() + self.finalized_gen_data = None + self.item_locations_finalization_lock = threading.Lock() def _make_item_maps(self, start_char: Chars) -> None: _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) @@ -305,6 +311,19 @@ class ZillionWorld(World): self.zz_system.post_fill() + def finalize_item_locations_thread_safe(self) -> GenData: + """ + Call self.finalize_item_locations() and cache the result in a thread-safe manner so that either + `generate_output` or `fill_slot_data` can finalize item locations without concern for which of the two functions + is called first. + """ + # The lock is acquired when entering the context manager and released when exiting the context manager. + with self.item_locations_finalization_lock: + # If generation data has yet to be finalized, finalize it. + if self.finalized_gen_data is None: + self.finalized_gen_data = self.finalize_item_locations() + return self.finalized_gen_data + def finalize_item_locations(self) -> GenData: """ sync zilliandomizer item locations with AP item locations @@ -363,12 +382,7 @@ class ZillionWorld(World): def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use multiworld.random here. If you need any last-second randomization, use self.random instead.""" - try: - gen_data = self.finalize_item_locations() - except BaseException: - raise - finally: - self.slot_data_ready.set() + gen_data = self.finalize_item_locations_thread_safe() out_file_base = self.multiworld.get_out_file_name_base(self.player) @@ -392,9 +406,7 @@ class ZillionWorld(World): # TODO: tell client which canisters are keywords # so it can open and get those when restoring doors - self.slot_data_ready.wait() - assert self.zz_system.randomizer, "didn't get randomizer from generate_early" - game = self.zz_system.get_game() + game = self.finalize_item_locations_thread_safe().zz_game return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty) # end of ordered Main.py calls From 563794ab832b813c1ff7bc51bf1b8c5a7243b17c Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 19 Jan 2025 15:29:13 -0800 Subject: [PATCH 19/50] Zillion: Use Useful Item Classification (#4179) --- worlds/zillion/__init__.py | 11 +++-------- worlds/zillion/item.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 6fa5f86d07..58f513ba6f 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -9,8 +9,7 @@ import logging from typing_extensions import override -from BaseClasses import ItemClassification, LocationProgressType, \ - MultiWorld, Item, CollectionState, Entrance, Tutorial +from BaseClasses import LocationProgressType, MultiWorld, Item, CollectionState, Entrance, Tutorial from .gen_data import GenData from .logic import ZillionLogicCache @@ -19,7 +18,7 @@ from .options import ZillionOptions, validate, z_option_groups from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id -from .item import ZillionItem +from .item import ZillionItem, get_classification from .patch import ZillionPatch from zilliandomizer.system import System @@ -422,12 +421,8 @@ class ZillionWorld(World): self.logger.warning("warning: called `create_item` without calling `generate_early` first") assert self.id_to_zz_item, "failed to get item maps" - classification = ItemClassification.filler zz_item = self.id_to_zz_item[item_id] - if zz_item.required: - classification = ItemClassification.progression - if not zz_item.is_progression: - classification = ItemClassification.progression_skip_balancing + classification = get_classification(name, zz_item, self._item_counts) z_item = ZillionItem(name, classification, item_id, self.player, zz_item) return z_item diff --git a/worlds/zillion/item.py b/worlds/zillion/item.py index fdf0fa8ba2..5fa481ac36 100644 --- a/worlds/zillion/item.py +++ b/worlds/zillion/item.py @@ -1,6 +1,34 @@ +from typing import Counter from BaseClasses import Item, ItemClassification as IC from zilliandomizer.logic_components.items import Item as ZzItem +_useful_thresholds = { + "Apple": 9999, + "Champ": 9999, + "JJ": 9999, + "Win": 9999, + "Empty": 0, + "ID Card": 10, + "Red ID Card": 2, + "Floppy Disk": 7, + "Bread": 0, + "Opa-Opa": 20, + "Zillion": 8, + "Scope": 8, +} +""" make the item useful if the number in the item pool is below this number """ + + +def get_classification(name: str, zz_item: ZzItem, item_counts: Counter[str]) -> IC: + classification = IC.filler + if zz_item.required: + classification = IC.progression + if not zz_item.is_progression: + classification = IC.progression_skip_balancing + if item_counts[name] < _useful_thresholds.get(name, 0): + classification |= IC.useful + return classification + class ZillionItem(Item): game = "Zillion" From ca8ffe583d019c9a82928757263e5daee4cafbd3 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 19 Jan 2025 15:31:09 -0800 Subject: [PATCH 20/50] Zillion: Priority Dead Ends Feature (#4220) --- worlds/zillion/__init__.py | 14 ++++++++++++++ worlds/zillion/options.py | 15 +++++++++++++++ worlds/zillion/requirements.txt | 2 +- worlds/zillion/test/TestOptions.py | 17 ++++++++++++++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 58f513ba6f..d0064b9cb1 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -24,6 +24,7 @@ from .patch import ZillionPatch from zilliandomizer.system import System from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem from zilliandomizer.logic_components.locations import Location as ZzLocation, Req +from zilliandomizer.map_gen.region_maker import DEAD_END_SUFFIX from zilliandomizer.options import Chars from worlds.AutoWorld import World, WebWorld @@ -172,6 +173,7 @@ class ZillionWorld(World): self.logic_cache = logic_cache w = self.multiworld self.my_locations = [] + dead_end_locations: list[ZillionLocation] = [] self.zz_system.randomizer.place_canister_gun_reqs() # low probability that place_canister_gun_reqs() results in empty 1st sphere @@ -224,6 +226,16 @@ class ZillionWorld(World): here.locations.append(loc) self.my_locations.append(loc) + if (( + zz_here.name.endswith(DEAD_END_SUFFIX) + ) or ( + (self.options.map_gen.value != self.options.map_gen.option_full) and + (loc.name in self.options.priority_dead_ends.vanilla_dead_ends) + ) or ( + loc.name in self.options.priority_dead_ends.always_dead_ends + )): + dead_end_locations.append(loc) + for zz_dest in zz_here.connections.keys(): dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name) dest = all_regions[dest_name] @@ -233,6 +245,8 @@ class ZillionWorld(World): queue.append(zz_dest) done.add(here.name) + if self.options.priority_dead_ends.value: + self.options.priority_locations.value |= {loc.name for loc in dead_end_locations} @override def create_items(self) -> None: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 22a6984722..13f3d43ab0 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -272,6 +272,20 @@ class ZillionMapGen(Choice): return "full" +class ZillionPriorityDeadEnds(DefaultOnToggle): + """ + Single locations that are in a dead end behind a door + (example: vanilla Apple location) + are prioritized for progression items. + """ + display_name = "priority dead ends" + + vanilla_dead_ends: ClassVar = frozenset(("E-5 top far right", "J-4 top left")) + """ dead ends when not generating these rooms """ + always_dead_ends: ClassVar = frozenset(("A-6 top right",)) + """ dead ends in rooms that never get generated """ + + @dataclass class ZillionOptions(PerGameCommonOptions): continues: ZillionContinues @@ -293,6 +307,7 @@ class ZillionOptions(PerGameCommonOptions): skill: ZillionSkill starting_cards: ZillionStartingCards map_gen: ZillionMapGen + priority_dead_ends: ZillionPriorityDeadEnds room_gen: Removed diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index d6b01ac107..4f79626c9a 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@96d9a20f8278cee64bb4db859fbd874e0f332d36#0.9.1 typing-extensions>=4.7, <5 diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index 3820c32dd0..904063fd3c 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -1,6 +1,7 @@ from . import ZillionTestBase -from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate +from .. import ZillionWorld +from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, ZillionPriorityDeadEnds, validate from zilliandomizer.options import VBLR_CHOICES @@ -28,3 +29,17 @@ class OptionsTest(ZillionTestBase): assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options + + +class DeadEndsTest(ZillionTestBase): + def test_vanilla_dead_end_names(self) -> None: + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) + for loc_name in ZillionPriorityDeadEnds.vanilla_dead_ends: + assert any(loc.name == loc_name for loc in z_world.my_locations), f"{loc_name=} {z_world.my_locations=}" + + def test_always_dead_end_names(self) -> None: + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) + for loc_name in ZillionPriorityDeadEnds.always_dead_ends: + assert any(loc.name == loc_name for loc in z_world.my_locations), f"{loc_name=} {z_world.my_locations=}" From 130232b45707be8d9b8d7570344a8c3700d0f87b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:56:37 +0100 Subject: [PATCH 21/50] Core: Make log time an optional arg & setting for Generate.py as well #4312 --- Generate.py | 6 ++++-- settings.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Generate.py b/Generate.py index d6611b0f8a..b057db25a3 100644 --- a/Generate.py +++ b/Generate.py @@ -42,7 +42,9 @@ def mystery_argparse(): help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) - parser.add_argument('--log_level', default='info', help='Sets log level') + parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level') + parser.add_argument('--log_time', help="Add timestamps to STDOUT", + default=defaults.logtime, action='store_true') parser.add_argument("--csv_output", action="store_true", help="Output rolled player options to csv (made for async multiworld).") parser.add_argument("--plando", default=defaults.plando_options, @@ -75,7 +77,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: seed = get_seed(args.seed) - Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) + Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time) random.seed(seed) seed_name = get_seed_name(random) diff --git a/settings.py b/settings.py index 04d8760c3c..12dace632c 100644 --- a/settings.py +++ b/settings.py @@ -678,6 +678,8 @@ class GeneratorOptions(Group): race: Race = Race(0) plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") panic_method: PanicMethod = PanicMethod("swap") + loglevel: str = "info" + logtime: bool = False class SNIOptions(Group): From 39847c55027117e43d44bd034585e21b79d4ed20 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Jan 2025 02:05:07 +0100 Subject: [PATCH 22/50] WebHost: sort slots by player_id in api blueprint (#4354) --- WebHostLib/api/__init__.py | 4 ++-- WebHostLib/api/user.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cf05e87374..d0b9d05c16 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -3,13 +3,13 @@ from typing import List, Tuple from flask import Blueprint -from ..models import Seed +from ..models import Seed, Slot api_endpoints = Blueprint('api', __name__, url_prefix="/api") def get_players(seed: Seed) -> List[Tuple[str, str]]: - return [(slot.player_name, slot.game) for slot in seed.slots] + return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)] from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 116d3afa22..0ddb6fe83e 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -30,4 +30,4 @@ def get_seeds(): "creation_time": seed.creation_time, "players": get_players(seed.slots), }) - return jsonify(response) \ No newline at end of file + return jsonify(response) From eb3c3d6bf2fb9b161723c7e7ae2990ab0ab7b6bd Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 19 Jan 2025 20:12:44 -0500 Subject: [PATCH 23/50] FFMQ: Adds Items Accessibility (#4322) --- worlds/ffmq/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index 41c397315f..4dcf1467d6 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions +from Options import Choice, FreeText, ItemsAccessibility, Toggle, Range, PerGameCommonOptions from dataclasses import dataclass @@ -324,6 +324,7 @@ class KaelisMomFightsMinotaur(Toggle): @dataclass class FFMQOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility logic: Logic brown_boxes: BrownBoxes sky_coin_mode: SkyCoinMode From 992841a951d71bbee05e28655e587567ab9555ca Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 19 Jan 2025 20:18:36 -0500 Subject: [PATCH 24/50] CommonClient: abstract url handling so it's importable (#4068) Co-authored-by: Doug Hoskisson Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> --- CommonClient.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index b43bf57d19..f6b2623f8c 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -31,6 +31,7 @@ import ssl if typing.TYPE_CHECKING: import kvui + import argparse logger = logging.getLogger("Client") @@ -1048,6 +1049,32 @@ def get_base_parser(description: typing.Optional[str] = None): return parser +def handle_url_arg(args: "argparse.Namespace", + parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace": + """ + Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient + If alternate data is required the urlparse response is saved back to args.url if valid + """ + if not args.url: + return args + + url = urllib.parse.urlparse(args.url) + if url.scheme != "archipelago": + if not parser: + parser = get_base_parser() + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + return args + + args.url = url + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + + return args + + def run_as_textclient(*args): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry @@ -1089,17 +1116,7 @@ def run_as_textclient(*args): parser.add_argument("url", nargs="?", help="Archipelago connection url") args = parser.parse_args(args) - # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost - if args.url: - url = urllib.parse.urlparse(args.url) - if url.scheme == "archipelago": - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) - else: - parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + args = handle_url_arg(args, parser=parser) # use colorama to display colored text highlighting on windows colorama.init() From 4fa8c432666039ec7cd82e88ba2fd29a62bacbd9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:06:09 -0500 Subject: [PATCH 25/50] FFMQ: Fix collect_item (#4433) * Fix FFMQ collect_item --- worlds/ffmq/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index 3c58487265..58dc4bf13e 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -152,14 +152,23 @@ class FFMQWorld(World): return FFMQItem(name, self.player) def collect_item(self, state, item, remove=False): + if not item.advancement: + return None if "Progressive" in item.name: i = item.code - 256 + if remove: + if state.has(self.item_id_to_name[i+1], self.player): + if state.has(self.item_id_to_name[i+2], self.player): + return self.item_id_to_name[i+2] + return self.item_id_to_name[i+1] + return self.item_id_to_name[i] + if state.has(self.item_id_to_name[i], self.player): if state.has(self.item_id_to_name[i+1], self.player): return self.item_id_to_name[i+2] return self.item_id_to_name[i+1] return self.item_id_to_name[i] - return item.name if item.advancement else None + return item.name def modify_multidata(self, multidata): # wait for self.rom_name to be available. From a2fbf856ff1f6d274468b879521c72f78762aa3a Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:07:01 -0500 Subject: [PATCH 26/50] SMZ3: Change locality options earlier (#4424) --- worlds/smz3/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7ebec7d4e4..dca105b162 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -217,6 +217,10 @@ class SMZ3World(World): SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys()) self.multiworld.state.smz3state[self.player] = TotalSMZ3Item.Progression([]) + + if not self.smz3World.Config.Keysanity: + # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local + self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) def create_items(self): self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World) @@ -233,8 +237,6 @@ class SMZ3World(World): progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems else: progressionItems = self.progression - # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local - self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) for item in self.keyCardsItems: self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) From d5cd95c7fba516480d303df3a842bffd98d70ff4 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 20 Jan 2025 03:01:45 -0500 Subject: [PATCH 27/50] Docs: Clarify usage of slot data for trackers in World API doc (#3986) * Clarify usage of slot data for trackers in world API. * Typo. * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update docs/world api.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Keep to 120 char lines. --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- docs/world api.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 487c5b4a36..762189a908 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -835,14 +835,16 @@ def generate_output(self, output_directory: str) -> None: ### Slot Data -If the game client needs to know information about the generated seed, a preferred method of transferring the data -is through the slot data. This is filled with the `fill_slot_data` method of your world by returning -a `dict` with `str` keys that can be serialized with json. -But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client -once it has successfully [connected](network%20protocol.md#connected). +If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data +is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with +`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is +absolutely necessary. Slot data is sent to your client once it has successfully +[connected](network%20protocol.md#connected). + If you need to know information about locations in your world, instead of propagating the slot data, it is preferable -to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most -common usage of slot data is sending option results that the client needs to be aware of. +to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding +item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients +that request it. The most common usage of slot data is sending option results that the client needs to be aware of. ```python def fill_slot_data(self) -> Dict[str, Any]: From 4f77abac4f567048d909a049004d03aa303c043c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 20 Jan 2025 09:53:30 -0500 Subject: [PATCH 28/50] TUNIC: Fix failure in 1-player grass (#4520) * Fix failure in 1-player grass * Update worlds/tunic/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/tunic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 388a44113a..1394c11c90 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -411,7 +411,7 @@ class TunicWorld(World): def stage_pre_fill(cls, multiworld: MultiWorld) -> None: tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC") if world.options.local_fill.value > 0] - if tunic_fill_worlds: + if tunic_fill_worlds and multiworld.players > 1: grass_fill: List[TunicItem] = [] non_grass_fill: List[TunicItem] = [] grass_fill_locations: List[Location] = [] From 96f469c73792199f84fe3128244c0dfa73331df4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 20 Jan 2025 10:04:39 -0500 Subject: [PATCH 29/50] TUNIC: Fix hero relics not being prog if hex quest is on in combat logic #4509 --- worlds/tunic/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 1394c11c90..96d3c10b82 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -331,10 +331,11 @@ class TunicWorld(World): remove_filler(items_to_create[gold_hexagon]) - # Sort for deterministic order - for hero_relic in sorted(item_name_groups["Hero Relics"]): - tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) - items_to_create[hero_relic] = 0 + if not self.options.combat_logic: + # Sort for deterministic order + for hero_relic in sorted(item_name_groups["Hero Relics"]): + tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) + items_to_create[hero_relic] = 0 if not self.options.ability_shuffling: # Sort for deterministic order From 436c0a41048f6f10084387e61f73995381808eed Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:07:15 +0100 Subject: [PATCH 30/50] Core: Add connect_entrances world step/stage (#4420) * Add connect_entrances * update ER docs * fix that test, but also ew * Add a test that asserts the new finalization * Rewrite test a bit * rewrite some more * blank line * rewrite rewrite rewrite * rewrite rewrite rewrite * RE. WRITE. * oops * Bruh * I guess, while we're at it * giga oops * It's been a long day * Switch KH1 over to this design with permission of GICU * Revert * Oops * Bc I like it * Update locations.py --- Main.py | 3 ++- docs/entrance randomization.md | 18 ++++++---------- docs/world api.md | 3 +++ test/benchmark/locations.py | 10 ++++++++- test/general/__init__.py | 10 ++++++++- test/general/test_entrances.py | 36 +++++++++++++++++++++++++++++++ test/general/test_items.py | 4 ++-- test/general/test_locations.py | 6 ++++++ test/general/test_reachability.py | 4 ++-- worlds/AutoWorld.py | 4 ++++ worlds/kh1/Regions.py | 3 +++ worlds/kh1/__init__.py | 5 ++++- 12 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 test/general/test_entrances.py diff --git a/Main.py b/Main.py index d105bd4ad0..d0e7a7f879 100644 --- a/Main.py +++ b/Main.py @@ -148,7 +148,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: multiworld.worlds[1].options.non_local_items.value = set() multiworld.worlds[1].options.local_items.value = set() - + + AutoWorld.call_all(multiworld, "connect_entrances") AutoWorld.call_all(multiworld, "generate_basic") # remove starting inventory from pool items. diff --git a/docs/entrance randomization.md b/docs/entrance randomization.md index 9e3e281bcc..0f9d764716 100644 --- a/docs/entrance randomization.md +++ b/docs/entrance randomization.md @@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups) #### When to call `randomize_entrances` -The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading. +The correct step for this is `World.connect_entrances`. -ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures. -This means 2 things about when you can call ER: -1. You must supply your item pool before calling ER, or call ER before setting any rules which require items. -2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules - and create your events before you call ER if you want to guarantee a correct output. - -If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also -a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER -in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or -generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as -well. +Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`. +However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better +together. +These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`. +It is fine for your Entrances to be connected differently or not at all before this step. #### Informing your client about randomized entrances diff --git a/docs/world api.md b/docs/world api.md index 762189a908..90fe446d61 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -490,6 +490,9 @@ In addition, the following methods can be implemented and are called in this ord after this step. Locations cannot be moved to different regions after this step. * `set_rules(self)` called to set access and item rules on locations and entrances. +* `connect_entrances(self)` + by the end of this step, all entrances must exist and be connected to their source and target regions. + Entrance randomization should be done here. * `generate_basic(self)` player-specific randomization that does not affect logic can be done here. * `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` diff --git a/test/benchmark/locations.py b/test/benchmark/locations.py index f2209eb689..857e188236 100644 --- a/test/benchmark/locations.py +++ b/test/benchmark/locations.py @@ -18,7 +18,15 @@ def run_locations_benchmark(): class BenchmarkRunner: gen_steps: typing.Tuple[str, ...] = ( - "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + "generate_early", + "create_regions", + "create_items", + "set_rules", + "connect_entrances", + "generate_basic", + "pre_fill", + ) + rule_iterations: int = 100_000 if sys.version_info >= (3, 9): diff --git a/test/general/__init__.py b/test/general/__init__.py index 8afd849765..6c4d5092cf 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul from worlds import network_data_package from worlds.AutoWorld import World, call_all -gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") +gen_steps = ( + "generate_early", + "create_regions", + "create_items", + "set_rules", + "connect_entrances", + "generate_basic", + "pre_fill", +) def setup_solo_multiworld( diff --git a/test/general/test_entrances.py b/test/general/test_entrances.py new file mode 100644 index 0000000000..72161dfbde --- /dev/null +++ b/test/general/test_entrances.py @@ -0,0 +1,36 @@ +import unittest +from worlds.AutoWorld import AutoWorldRegister, call_all, World +from . import setup_solo_multiworld + + +class TestBase(unittest.TestCase): + def test_entrance_connection_steps(self): + """Tests that Entrances are connected and not changed after connect_entrances.""" + def get_entrance_name_to_source_and_target_dict(world: World): + return [ + (entrance.name, entrance.parent_region, entrance.connected_region) + for entrance in world.get_entrances() + ] + + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances") + additional_steps = ("generate_basic", "pre_fill") + + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + + original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1]) + + self.assertTrue( + all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances), + f"{game_name} had unconnected entrances after connect_entrances" + ) + + for step in additional_steps: + with self.subTest("Step", step=step): + call_all(multiworld, step) + step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1]) + + self.assertEqual( + original_entrances, step_entrances, f"{game_name} modified entrances during {step}" + ) diff --git a/test/general/test_items.py b/test/general/test_items.py index 64ce1b6997..91d334e968 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -67,7 +67,7 @@ class TestBase(unittest.TestCase): def test_itempool_not_modified(self): """Test that worlds don't modify the itempool after `create_items`""" gen_steps = ("generate_early", "create_regions", "create_items") - additional_steps = ("set_rules", "generate_basic", "pre_fill") + additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill") excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3") worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games} @@ -84,7 +84,7 @@ class TestBase(unittest.TestCase): def test_locality_not_modified(self): """Test that worlds don't modify the locality of items after duplicates are resolved""" gen_steps = ("generate_early", "create_regions", "create_items") - additional_steps = ("set_rules", "generate_basic", "pre_fill") + additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill") worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()} for game_name, world_type in worlds_to_test.items(): with self.subTest("Game", game=game_name): diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 4b95ebd22c..37ae94e003 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -45,6 +45,12 @@ class TestBase(unittest.TestCase): self.assertEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during rule creation") + call_all(multiworld, "connect_entrances") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during rule creation") + self.assertEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during rule creation") + call_all(multiworld, "generate_basic") self.assertEqual(region_count, len(multiworld.get_regions()), f"{game_name} modified region count during generate_basic") diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index fafa702389..b45a2bdfc0 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -2,11 +2,11 @@ import unittest from BaseClasses import CollectionState from worlds.AutoWorld import AutoWorldRegister -from . import setup_solo_multiworld +from . import setup_solo_multiworld, gen_steps class TestBase(unittest.TestCase): - gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] + gen_steps = gen_steps default_settings_unreachable_regions = { "A Link to the Past": { diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index a510717920..0fcacc8ab3 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister): """Method for setting the rules on the World's regions and locations.""" pass + def connect_entrances(self) -> None: + """Method to finalize the source and target regions of the World's entrances""" + pass + def generate_basic(self) -> None: """ Useful for randomizing things that don't affect logic but are better to be determined before the output stage. diff --git a/worlds/kh1/Regions.py b/worlds/kh1/Regions.py index a6f85fe617..6189adf207 100644 --- a/worlds/kh1/Regions.py +++ b/worlds/kh1/Regions.py @@ -483,6 +483,8 @@ def create_regions(multiworld: MultiWorld, player: int, options): for name, data in regions.items(): multiworld.regions.append(create_region(multiworld, player, name, data)) + +def connect_entrances(multiworld: MultiWorld, player: int): multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player)) multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player)) multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player)) @@ -500,6 +502,7 @@ def create_regions(multiworld: MultiWorld, player: int, options): multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player)) multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player)) + def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData): region = Region(name, player, multiworld) if data.locations: diff --git a/worlds/kh1/__init__.py b/worlds/kh1/__init__.py index 63b4575568..3b498acf46 100644 --- a/worlds/kh1/__init__.py +++ b/worlds/kh1/__init__.py @@ -6,7 +6,7 @@ from worlds.AutoWorld import WebWorld, World from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups from .Options import KH1Options, kh1_option_groups -from .Regions import create_regions +from .Regions import connect_entrances, create_regions from .Rules import set_rules from .Presets import kh1_option_presets from worlds.LauncherComponents import Component, components, Type, launch_subprocess @@ -242,6 +242,9 @@ class KH1World(World): def create_regions(self): create_regions(self.multiworld, self.player, self.options) + + def connect_entrances(self): + connect_entrances(self.multiworld, self.player) def generate_early(self): value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"] From 05d1b2129a9bdef4709f5a5b892324faa5c6b3bd Mon Sep 17 00:00:00 2001 From: "Chris J." Date: Mon, 20 Jan 2025 11:18:09 -0500 Subject: [PATCH 31/50] Docs: Update ID Overlapping Docs (#4447) --- docs/world api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 90fe446d61..da74be70fb 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules, and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1 -letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs. -Locations and items can share IDs, so typically a game's locations and items start at the same ID. +letter or symbol). The ID needs to be unique across all locations within the game. +Locations and items can share IDs, and locations can share IDs with other games' locations. World-specific IDs must be in the range 1 to 253-1; IDs ≤ 0 are global and reserved. @@ -243,7 +243,9 @@ progression. Progression items will be assigned to locations with higher priorit and satisfy progression balancing. The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they -will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). +will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). +The ID thus also needs to be unique across all items with different names within the game. +Items and locations can share IDs, and items can share IDs with other games' items. Other classifications include: From 823b17c386f1dce27e537e1c0263b0bc8670c377 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 20 Jan 2025 11:44:39 -0500 Subject: [PATCH 32/50] TUNIC: Make grass go in the regular location name group too (#4504) * Make grass go in the normal loc group too * Make it not overwrite old groups --- worlds/tunic/__init__.py | 3 ++- worlds/tunic/grass.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 96d3c10b82..087e17c3e4 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -78,7 +78,8 @@ class TunicWorld(World): settings: ClassVar[TunicSettings] item_name_groups = item_name_groups location_name_groups = location_name_groups - location_name_groups.update(grass_location_name_groups) + for group_name, members in grass_location_name_groups.items(): + location_name_groups.setdefault(group_name, set()).update(members) item_name_to_id = item_name_to_id location_name_to_id = standard_location_name_to_id.copy() diff --git a/worlds/tunic/grass.py b/worlds/tunic/grass.py index 592b2938b1..eb688199dc 100644 --- a/worlds/tunic/grass.py +++ b/worlds/tunic/grass.py @@ -7767,8 +7767,10 @@ grass_location_name_to_id: Dict[str, int] = {name: location_base_id + 302 + inde grass_location_name_groups: Dict[str, Set[str]] = {} for loc_name, loc_data in grass_location_table.items(): - loc_group_name = loc_name.split(" - ", 1)[0] + " Grass" - grass_location_name_groups.setdefault(loc_group_name, set()).add(loc_name) + area_name = loc_name.split(" - ", 1)[0] + # adding it to the normal location group and a grass-only one + grass_location_name_groups.setdefault(area_name, set()).add(loc_name) + grass_location_name_groups.setdefault(area_name + " Grass", set()).add(loc_name) def can_break_grass(state: CollectionState, world: "TunicWorld") -> bool: From e2b942139a5ed1908725812edd52aa479a5d835b Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Jan 2025 13:10:29 -0500 Subject: [PATCH 33/50] HK: Save GrubHuntGoal by value (#4521) --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 7e9b7442a7..daf177fb8d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -209,7 +209,7 @@ class HKWorld(World): # defaulting so completion condition isn't incorrect before pre_fill self.grub_count = ( 46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"] - else options.GrubHuntGoal + else options.GrubHuntGoal.value ) self.grub_player_count = {self.player: self.grub_count} From a126dee06824d3fee890eecfbf8a5d4870261e67 Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Jan 2025 17:42:12 -0500 Subject: [PATCH 34/50] HK: some stuff ruff and pycodestyle complained about (#4523) --- worlds/hk/Options.py | 2 +- worlds/hk/__init__.py | 52 ++++++++++++++++--------------- worlds/hk/test/__init__.py | 1 - worlds/hk/test/test_grub_count.py | 3 +- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 53dda96e2b..e76e7eba9d 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -333,7 +333,7 @@ class PlandoCharmCosts(OptionDict): continue try: self.value[key] = CharmCost.from_any(data).value - except ValueError as ex: + except ValueError: # will fail schema afterwords self.value[key] = data diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index daf177fb8d..4a0da109fa 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -7,22 +7,22 @@ import itertools import operator from collections import defaultdict, Counter -logger = logging.getLogger("Hollow Knight") - -from .Items import item_table, lookup_type_to_names, item_name_groups -from .Regions import create_regions +from .Items import item_table, item_name_groups from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ shop_to_option, HKOptions, GrubHuntGoal -from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ - event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs +from .ExtractedData import locations, starts, multi_locations, event_names, item_effects, connectors, \ + vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names -from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState +from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, \ + CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld from settings import Group, Bool +logger = logging.getLogger("Hollow Knight") + class HollowKnightSettings(Group): class DisableMapModSpoilers(Bool): @@ -160,7 +160,7 @@ class HKWeb(WebWorld): class HKWorld(World): - """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, + """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, searching for riches, or glory, or answers to old secrets. As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. @@ -231,7 +231,6 @@ class HKWorld(World): def create_regions(self): menu_region: Region = create_region(self.multiworld, self.player, 'Menu') self.multiworld.regions.append(menu_region) - # wp_exclusions = self.white_palace_exclusions() # check for any goal that godhome events are relevant to all_event_names = event_names.copy() @@ -241,21 +240,17 @@ class HKWorld(World): # Link regions for event_name in sorted(all_event_names): - #if event_name in wp_exclusions: - # continue loc = HKLocation(self.player, event_name, None, menu_region) loc.place_locked_item(HKItem(event_name, - True, #event_name not in wp_exclusions, + True, None, "Event", self.player)) menu_region.locations.append(loc) for entry_transition, exit_transition in connectors.items(): - #if entry_transition in wp_exclusions: - # continue if exit_transition: # if door logic fulfilled -> award vanilla target as event loc = HKLocation(self.player, entry_transition, None, menu_region) loc.place_locked_item(HKItem(exit_transition, - True, #exit_transition not in wp_exclusions, + True, None, "Event", self.player)) menu_region.locations.append(loc) @@ -292,7 +287,10 @@ class HKWorld(World): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) + item = (self.create_item(item_name) + if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations + else self.create_event(item_name) + ) if location_name == "Start": if item_name in randomized_starting_items: @@ -347,8 +345,8 @@ class HKWorld(World): randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) - for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): + for shop, shop_locations in self.created_multi_locations.items(): + for _ in range(len(shop_locations), getattr(self.options, shop_to_option[shop]).value): self.create_location(shop) unfilled_locations += 1 @@ -358,7 +356,7 @@ class HKWorld(World): # Add additional shop items, as needed. if additional_shop_items > 0: - shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) + shops = [shop for shop, shop_locations in self.created_multi_locations.items() if len(shop_locations) < 16] if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') @@ -380,8 +378,8 @@ class HKWorld(World): self.sort_shops_by_cost() def sort_shops_by_cost(self): - for shop, locations in self.created_multi_locations.items(): - randomized_locations = list(loc for loc in locations if not loc.vanilla) + for shop, shop_locations in self.created_multi_locations.items(): + randomized_locations = [loc for loc in shop_locations if not loc.vanilla] prices = sorted( (loc.costs for loc in randomized_locations), key=lambda costs: (len(costs),) + tuple(costs.values()) @@ -405,7 +403,7 @@ class HKWorld(World): return {k: v for k, v in weights.items() if v} random = self.random - hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value + hybrid_chance = getattr(self.options, "CostSanityHybridChance").value weights = { data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() @@ -493,7 +491,11 @@ class HKWorld(World): worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] if worlds: grubs = [item for item in multiworld.get_items() if item.name == "Grub"] - all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + all_grub_players = [ + world.player + for world in worlds + if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"] + ] if all_grub_players: group_lookup = defaultdict(set) @@ -668,8 +670,8 @@ class HKWorld(World): ): spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") else: - for shop_name, locations in hk_world.created_multi_locations.items(): - for loc in locations: + for shop_name, shop_locations in hk_world.created_multi_locations.items(): + for loc in shop_locations: spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str: diff --git a/worlds/hk/test/__init__.py b/worlds/hk/test/__init__.py index c41d20127f..67591001a7 100644 --- a/worlds/hk/test/__init__.py +++ b/worlds/hk/test/__init__.py @@ -2,7 +2,6 @@ import typing from argparse import Namespace from BaseClasses import CollectionState, MultiWorld from Options import ItemLinks -from test.bases import WorldTestBase from worlds.AutoWorld import AutoWorldRegister, call_all from .. import HKWorld diff --git a/worlds/hk/test/test_grub_count.py b/worlds/hk/test/test_grub_count.py index dba15b614d..a58293c078 100644 --- a/worlds/hk/test/test_grub_count.py +++ b/worlds/hk/test/test_grub_count.py @@ -1,5 +1,6 @@ -from . import linkedTestHK, WorldTestBase +from test.bases import WorldTestBase from Options import ItemLinks +from . import linkedTestHK class test_grubcount_limited(linkedTestHK, WorldTestBase): From 33fd9de281f0d4bbbda5f6134eaa7759a3978cb4 Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Jan 2025 18:56:20 -0500 Subject: [PATCH 35/50] Core: Add Retry to Priority Fill (#4477) * adds a retry to priority fill in case the one item per player optimization would cause the priority fill to fail to find valid placements * Update Fill.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- Fill.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 0da2d5def9..d1773c8213 100644 --- a/Fill.py +++ b/Fill.py @@ -502,7 +502,13 @@ def distribute_items_restrictive(multiworld: MultiWorld, # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, single_player_placement=single_player, swap=False, on_place=mark_for_locking, - name="Priority", one_item_per_player=False) + name="Priority", one_item_per_player=True, allow_partial=True) + + if prioritylocations: + # retry with one_item_per_player off because some priority fills can fail to fill with that optimization + fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry", one_item_per_player=False) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations From edacb17171478be7b512b61deac520c394e34b3f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Jan 2025 16:12:53 +0100 Subject: [PATCH 36/50] Factorio: remove debug print (#4533) --- worlds/factorio/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 8f8abeb292..a2bc518ae3 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -280,9 +280,6 @@ class Factorio(World): self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) for technology in victory_tech_names) - for tech_name in victory_tech_names: - if not self.multiworld.get_all_state(True).has(tech_name, player): - print(tech_name) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) def get_recipe(self, name: str) -> Recipe: From 1a1b7e9cf4c14729f6d3cc0a06f69260610e50e0 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 21 Jan 2025 12:39:08 -0500 Subject: [PATCH 37/50] TUNIC: Reduce range end for local_fill option #4534 --- worlds/tunic/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 14bf5d8a18..d2ea828037 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -173,7 +173,7 @@ class LocalFill(NamedRange): internal_name = "local_fill" display_name = "Local Fill Percent" range_start = 0 - range_end = 100 + range_end = 98 special_range_names = { "default": -1 } From 949527f9cb45ac7248d6aa7a73ec2ff980a64fcd Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:28:33 -0500 Subject: [PATCH 38/50] KH2: Bug fixes and game update future proofing (#4075) Co-authored-by: qwint --- worlds/kh2/Client.py | 136 ++++++++++++++++++++++-------------- worlds/kh2/Regions.py | 2 +- worlds/kh2/Rules.py | 6 +- worlds/kh2/docs/setup_en.md | 2 +- 4 files changed, 89 insertions(+), 57 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 3ea47e40eb..0254d46e93 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -5,8 +5,10 @@ ModuleUpdate.update() import os import asyncio import json +import requests from pymem import pymem -from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot +from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \ + SupportAbility_Table, ActionAbility_Table, all_weapon_slot from .Names import ItemName from .WorldLocations import * @@ -82,6 +84,7 @@ class KH2Context(CommonContext): } self.kh2seedname = None self.kh2slotdata = None + self.mem_json = None self.itemamount = {} if "localappdata" in os.environ: self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") @@ -178,7 +181,8 @@ class KH2Context(CommonContext): self.base_accessory_slots = 1 self.base_armor_slots = 1 self.base_item_slots = 3 - self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772] + self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, + 0x2770, 0x2772] async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -340,12 +344,8 @@ class KH2Context(CommonContext): self.locations_checked |= new_locations if cmd in {"DataPackage"}: - self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"] - self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()} - self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"] - self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} - self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] - + if "Kingdom Hearts 2" in args["data"]["games"]: + self.data_package_kh2_cache(args) if "KeybladeAbilities" in self.kh2slotdata.keys(): # sora ability to slot self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"]) @@ -359,24 +359,9 @@ class KH2Context(CommonContext): self.all_weapon_location_id = set(all_weapon_location_id) try: - self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - if self.kh2_game_version is None: - if self.kh2_read_string(0x09A9830, 4) == "KH2J": - self.kh2_game_version = "STEAM" - self.Now = 0x0717008 - self.Save = 0x09A9830 - self.Slot1 = 0x2A23518 - self.Journal = 0x7434E0 - self.Shop = 0x7435D0 - - elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": - self.kh2_game_version = "EGS" - else: - self.kh2_game_version = None - logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") - if self.kh2_game_version is not None: - logger.info(f"You are now auto-tracking. {self.kh2_game_version}") - self.kh2connected = True + if not self.kh2: + self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + self.get_addresses() except Exception as e: if self.kh2connected: @@ -385,6 +370,13 @@ class KH2Context(CommonContext): self.serverconneced = True asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}])) + def data_package_kh2_cache(self, args): + self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"] + self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()} + self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"] + self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} + self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] + async def checkWorldLocations(self): try: currentworldint = self.kh2_read_byte(self.Now) @@ -425,7 +417,6 @@ class KH2Context(CommonContext): 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels] } - # TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3 for i in range(6): for location, data in formDict[i][1].items(): formlevel = self.kh2_read_byte(self.Save + data.addrObtained) @@ -469,9 +460,11 @@ class KH2Context(CommonContext): if locationName in self.chest_set: if locationName in self.location_name_to_worlddata.keys(): locationData = self.location_name_to_worlddata[locationName] - if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0: + if self.kh2_read_byte( + self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0: roomData = self.kh2_read_byte(self.Save + locationData.addrObtained) - self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex) + self.kh2_write_byte(self.Save + locationData.addrObtained, + roomData | 0x01 << locationData.bitIndex) except Exception as e: if self.kh2connected: @@ -494,6 +487,9 @@ class KH2Context(CommonContext): async def give_item(self, item, location): try: # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + #sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts + while not self.lookup_id_to_item: + await asyncio.sleep(0.5) itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] # itemcode = self.kh2_item_name_to_id[itemname] @@ -637,7 +633,8 @@ class KH2Context(CommonContext): item_data = self.item_name_to_data[item_name] # if the inventory slot for that keyblade is less than the amount they should have, # and they are not in stt - if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13: + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte( + self.Save + 0x1CFF) != 13: # Checking form anchors for the keyblade to remove extra keyblades if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \ or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \ @@ -738,7 +735,8 @@ class KH2Context(CommonContext): item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}: + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte( + self.Shop) in {10, 8}: self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: @@ -797,7 +795,8 @@ class KH2Context(CommonContext): # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) if "PoptrackerVersionCheck" in self.kh2slotdata: - if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 + if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte( + self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 self.kh2_write_byte(self.Save + 0x3607, 1) except Exception as e: @@ -806,10 +805,59 @@ class KH2Context(CommonContext): logger.info(e) logger.info("line 840") + def get_addresses(self): + if not self.kh2connected and self.kh2 is not None: + if self.kh2_game_version is None: + + if self.kh2_read_string(0x09A9830, 4) == "KH2J": + self.kh2_game_version = "STEAM" + self.Now = 0x0717008 + self.Save = 0x09A9830 + self.Slot1 = 0x2A23518 + self.Journal = 0x7434E0 + self.Shop = 0x7435D0 + elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": + self.kh2_game_version = "EGS" + else: + if self.game_communication_path: + logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment") + #if mem addresses file is found then check version and if old get new one + kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json") + if not os.path.exists(kh2memaddresses_path): + mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json") + if mem_resp.status_code == 200: + self.mem_json = json.loads(mem_resp.content) + with open(kh2memaddresses_path, + 'w') as f: + f.write(json.dumps(self.mem_json, indent=4)) + else: + with open(kh2memaddresses_path, 'r') as f: + self.mem_json = json.load(f) + if self.mem_json: + for key in self.mem_json.keys(): + + if self.kh2_read_string(eval(self.mem_json[key]["GameVersionCheck"]), 4) == "KH2J": + self.Now = eval(self.mem_json[key]["Now"]) + self.Save=eval(self.mem_json[key]["Save"]) + self.Slot1 = eval(self.mem_json[key]["Slot1"]) + self.Journal = eval(self.mem_json[key]["Journal"]) + self.Shop = eval(self.mem_json[key]["Shop"]) + self.kh2_game_version = key + + if self.kh2_game_version is not None: + logger.info(f"You are now auto-tracking {self.kh2_game_version}") + self.kh2connected = True + else: + logger.info("Your game version does not match what the client requires. Check in the " + "kingdom-hearts-2-final-mix channel for more information on correcting the game " + "version.") + self.kh2connected = False + def finishedGame(ctx: KH2Context): if ctx.kh2slotdata['FinalXemnas'] == 1: - if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ + if not ctx.final_xemnas and ctx.kh2_read_byte( + ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0: ctx.final_xemnas = True # three proofs @@ -843,7 +891,8 @@ def finishedGame(ctx: KH2Context): for boss in ctx.kh2slotdata["hitlist"]: if boss in locations: ctx.hitlist_bounties += 1 - if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]: + if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"][ + "Bounty"] >= ctx.kh2slotdata["BountyRequired"]: if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) @@ -894,24 +943,7 @@ async def kh2_watcher(ctx: KH2Context): while not ctx.kh2connected and ctx.serverconneced: await asyncio.sleep(15) ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - if ctx.kh2 is not None: - if ctx.kh2_game_version is None: - if ctx.kh2_read_string(0x09A9830, 4) == "KH2J": - ctx.kh2_game_version = "STEAM" - ctx.Now = 0x0717008 - ctx.Save = 0x09A9830 - ctx.Slot1 = 0x2A23518 - ctx.Journal = 0x7434E0 - ctx.Shop = 0x7435D0 - - elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J": - ctx.kh2_game_version = "EGS" - else: - ctx.kh2_game_version = None - logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") - if ctx.kh2_game_version is not None: - logger.info(f"You are now auto-tracking {ctx.kh2_game_version}") - ctx.kh2connected = True + ctx.get_addresses() except Exception as e: if ctx.kh2connected: ctx.kh2connected = False diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index 7fc2ad8a87..e6e8a7b2f6 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -540,7 +540,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = { LocationName.SephirothFenrir, LocationName.SephiEventLocation ], - RegionName.CoR: [ + RegionName.CoR: [ #todo: make logic for getting these checks. LocationName.CoRDepthsAPBoost, LocationName.CoRDepthsPowerCrystal, LocationName.CoRDepthsFrostCrystal, diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 0f26b56d0e..767c564341 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -194,8 +194,8 @@ class KH2WorldRules(KH2Rules): RegionName.Oc: lambda state: self.oc_unlocked(state, 1), RegionName.Oc2: lambda state: self.oc_unlocked(state, 2), + #twtnw1 is actually the roxas fight region thus roxas requires 1 way to the dawn RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2), - # These will be swapped and First Visit lock for twtnw is in development. # RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2), RegionName.Ht: lambda state: self.ht_unlocked(state, 1), @@ -919,8 +919,8 @@ class KH2FightRules(KH2Rules): # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus sephiroth_rules = { - "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, - "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state), + "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([gap_closer], state) >= 1, "hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, } return sephiroth_rules[self.fight_logic] diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index cb80ec6098..bee60bd36b 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -52,7 +52,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot

What the Mod Manager Should Look Like.

-![image](https://i.imgur.com/Si4oZ8w.png) +![image](https://i.imgur.com/N0WJ8Qn.png)

Using the KH2 Client

From 5a42c7067553995f9f630125a1860242452df4c7 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:00:47 +0100 Subject: [PATCH 39/50] Core: Fix worlds that rely on other worlds having their Entrances connected before connect_entrances, add unit test (#4530) * unit test that get all state is called with partial entrances before connect_entrances * fix the two worlds doing it * lol * unused import * Update test/general/test_entrances.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update test_entrances.py --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- test/general/test_entrances.py | 27 +++++++++++++++++++++++++++ worlds/alttp/Rules.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/test/general/test_entrances.py b/test/general/test_entrances.py index 72161dfbde..88362c8fa6 100644 --- a/test/general/test_entrances.py +++ b/test/general/test_entrances.py @@ -34,3 +34,30 @@ class TestBase(unittest.TestCase): self.assertEqual( original_entrances, step_entrances, f"{game_name} modified entrances during {step}" ) + + def test_all_state_before_connect_entrances(self): + """Before connect_entrances, Entrance objects may be unconnected. + Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during + connect_entrances.""" + + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances") + + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + multiworld = setup_solo_multiworld(world_type, ()) + + original_get_all_state = multiworld.get_all_state + + def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False): + self.assertTrue(allow_partial_entrances, ( + "Before the connect_entrances step finishes, other worlds might still have partial entrances. " + "As such, any call to get_all_state must use allow_partial_entrances = True." + )) + + return original_get_all_state(use_cache, allow_partial_entrances) + + multiworld.get_all_state = patched_get_all_state + + for step in gen_steps: + with self.subTest("Step", step=step): + call_all(multiworld, step) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 386e0b0e9e..f13178c6c5 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -1125,7 +1125,7 @@ def set_trock_key_rules(world, player): for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']: set_rule(world.get_entrance(entrance, player), lambda state: False) - all_state = world.get_all_state(use_cache=False) + all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True) all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work all_state.stale[player] = True From fa2816822b46a770417b745e97d0f25c42b3a9ac Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:45:11 -0500 Subject: [PATCH 40/50] AHIT: Fix broken link in setup guide (#4524) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/ahit/docs/setup_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 23b3490707..167c6c2faa 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -21,7 +21,7 @@ 3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. - While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)) + While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601). 4. Once the game finishes downloading, start it up. @@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked. ### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work! There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly -if you have too many save files. Delete them and it should fix the problem. \ No newline at end of file +if you have too many save files. Delete them and it should fix the problem. From bb0948154da8e3436ebd1ac9bbbc29ee230cc695 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 24 Jan 2025 12:42:31 -0500 Subject: [PATCH 41/50] TUNIC: Make the standard entrances get made with tuples instead of sets (#4546) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/tunic/regions.py | 46 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index 93ec5640e0..8f5df8896a 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -1,26 +1,24 @@ -from typing import Dict, Set - -tunic_regions: Dict[str, Set[str]] = { - "Menu": {"Overworld"}, - "Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", +tunic_regions: dict[str, tuple[str]] = { + "Menu": ("Overworld",), + "Overworld": ("Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", - "Spirit Arena"}, - "Overworld Holy Cross": set(), - "East Forest": set(), - "Dark Tomb": {"West Garden"}, - "Beneath the Well": set(), - "West Garden": set(), - "Ruined Atoll": {"Frog's Domain", "Library"}, - "Frog's Domain": set(), - "Library": set(), - "Eastern Vault Fortress": {"Beneath the Vault"}, - "Beneath the Vault": {"Eastern Vault Fortress"}, - "Quarry Back": {"Quarry"}, - "Quarry": {"Monastery", "Lower Quarry"}, - "Monastery": set(), - "Lower Quarry": {"Rooted Ziggurat"}, - "Rooted Ziggurat": set(), - "Swamp": {"Cathedral"}, - "Cathedral": set(), - "Spirit Arena": set() + "Spirit Arena"), + "Overworld Holy Cross": tuple(), + "East Forest": tuple(), + "Dark Tomb": ("West Garden",), + "Beneath the Well": tuple(), + "West Garden": tuple(), + "Ruined Atoll": ("Frog's Domain", "Library"), + "Frog's Domain": tuple(), + "Library": tuple(), + "Eastern Vault Fortress": ("Beneath the Vault",), + "Beneath the Vault": ("Eastern Vault Fortress",), + "Quarry Back": ("Quarry",), + "Quarry": ("Monastery", "Lower Quarry"), + "Monastery": tuple(), + "Lower Quarry": ("Rooted Ziggurat",), + "Rooted Ziggurat": tuple(), + "Swamp": ("Cathedral",), + "Cathedral": tuple(), + "Spirit Arena": tuple() } From 7474c273729f68bc9f791626999e180cacbc6b46 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 24 Jan 2025 13:52:12 -0500 Subject: [PATCH 42/50] Core: Add launch function to call launch_subprocess only if multiprocessing is actually necessary (#4237) * skips opening a subprocess if kivy (and thus the launcher gui) hasn't been loaded so stdin can function as expected on --nogui and similar * this exists lol * keep old function around and use new function for CC component * fix name=None typing --- worlds/LauncherComponents.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index d1b274c19a..41c83db419 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -87,7 +87,7 @@ class Component: processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: +def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: global processes import multiprocessing process = multiprocessing.Process(target=func, name=name, args=args) @@ -95,6 +95,14 @@ def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = processes.add(process) +def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: + from Utils import is_kivy_running + if is_kivy_running(): + launch_subprocess(func, name, args) + else: + func(*args) + + class SuffixIdentifier: suffixes: Iterable[str] @@ -111,7 +119,7 @@ class SuffixIdentifier: def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args) + launch(CommonClient.run_as_textclient, name="TextClient", args=args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: From 3d1d6908c8081f325659377e7d0dae4487badd07 Mon Sep 17 00:00:00 2001 From: Jasper den Brok Date: Fri, 24 Jan 2025 22:30:21 +0100 Subject: [PATCH 43/50] Pokemon Emerald: Add Free Fly Blacklist (#4165) Co-authored-by: Jasper den Brok --- worlds/pokemon_emerald/locations.py | 28 ++++++++++++++++------------ worlds/pokemon_emerald/options.py | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 473c189166..2bae8e00ed 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -33,6 +33,18 @@ VISITED_EVENT_NAME_TO_ID = { "EVENT_VISITED_SOUTHERN_ISLAND": 17, } +BLACKLIST_OPTION_TO_VISITED_EVENT = { + "Slateport City": "EVENT_VISITED_SLATEPORT_CITY", + "Mauville City": "EVENT_VISITED_MAUVILLE_CITY", + "Verdanturf Town": "EVENT_VISITED_VERDANTURF_TOWN", + "Fallarbor Town": "EVENT_VISITED_FALLARBOR_TOWN", + "Lavaridge Town": "EVENT_VISITED_LAVARIDGE_TOWN", + "Fortree City": "EVENT_VISITED_FORTREE_CITY", + "Lilycove City": "EVENT_VISITED_LILYCOVE_CITY", + "Mossdeep City": "EVENT_VISITED_MOSSDEEP_CITY", + "Sootopolis City": "EVENT_VISITED_SOOTOPOLIS_CITY", + "Ever Grande City": "EVENT_VISITED_EVER_GRANDE_CITY", +} class PokemonEmeraldLocation(Location): game: str = "Pokemon Emerald" @@ -129,18 +141,10 @@ def set_free_fly(world: "PokemonEmeraldWorld") -> None: # If not enabled, set it to Littleroot Town by default fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN" if world.options.free_fly_location: - fly_location_name = world.random.choice([ - "EVENT_VISITED_SLATEPORT_CITY", - "EVENT_VISITED_MAUVILLE_CITY", - "EVENT_VISITED_VERDANTURF_TOWN", - "EVENT_VISITED_FALLARBOR_TOWN", - "EVENT_VISITED_LAVARIDGE_TOWN", - "EVENT_VISITED_FORTREE_CITY", - "EVENT_VISITED_LILYCOVE_CITY", - "EVENT_VISITED_MOSSDEEP_CITY", - "EVENT_VISITED_SOOTOPOLIS_CITY", - "EVENT_VISITED_EVER_GRANDE_CITY", - ]) + blacklisted_locations = set(BLACKLIST_OPTION_TO_VISITED_EVENT[city] for city in world.options.free_fly_blacklist.value) + free_fly_locations = sorted(set(BLACKLIST_OPTION_TO_VISITED_EVENT.values()) - blacklisted_locations) + if free_fly_locations: + fly_location_name = world.random.choice(free_fly_locations) world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name] diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 8fcc74d1c3..cf0c692d06 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -725,6 +725,24 @@ class FreeFlyLocation(Toggle): """ display_name = "Free Fly Location" +class FreeFlyBlacklist(OptionSet): + """ + Disables specific locations as valid free fly locations. + Has no effect if Free Fly Location is disabled. + """ + display_name = "Free Fly Blacklist" + valid_keys = [ + "Slateport City", + "Mauville City", + "Verdanturf Town", + "Fallarbor Town", + "Lavaridge Town", + "Fortree City", + "Lilycove City", + "Mossdeep City", + "Sootopolis City", + "Ever Grande City", + ] class HmRequirements(Choice): """ @@ -876,6 +894,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions): extra_bumpy_slope: ExtraBumpySlope modify_118: ModifyRoute118 free_fly_location: FreeFlyLocation + free_fly_blacklist: FreeFlyBlacklist hm_requirements: HmRequirements turbo_a: TurboA From 3df2dbe051024df890f322280ee4373d4690c258 Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:55:49 -0500 Subject: [PATCH 44/50] TUNIC: Add ability shuffle information to spoiler log (#4498) --- worlds/tunic/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 087e17c3e4..ed2923037e 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set +from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO from logging import warning from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names, @@ -502,6 +502,13 @@ class TunicWorld(World): state.tunic_need_to_reset_combat_from_remove[self.player] = True return change + def write_spoiler_header(self, spoiler_handle: TextIO): + if self.options.hexagon_quest and self.options.ability_shuffling: + spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n") + for ability in self.ability_unlocks: + # Remove parentheses for better readability + spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n') + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.options.entrance_rando: hint_data.update({self.player: {}}) From ddf7fdccc718380e8611ab946ca5c529f897075b Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:57:23 -0500 Subject: [PATCH 45/50] TUNIC: Add Torch Item (#4538) Co-authored-by: Scipio Wright --- worlds/tunic/items.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 729bfd4411..846650c68f 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -48,6 +48,7 @@ item_table: Dict[str, TunicItemData] = { "Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"), "Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful), "Dath Stone": TunicItemData(IC.useful, 1, 32), + "Torch": TunicItemData(IC.useful, 0, 156), "Hourglass": TunicItemData(IC.useful, 1, 33), "Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"), "Key": TunicItemData(IC.progression, 2, 35, "Keys"), From 513e361764aea8a04e56010c6c47ee4bb53f5303 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 24 Jan 2025 17:10:58 -0500 Subject: [PATCH 46/50] TUNIC: Fix UT create_item classification (#4514) Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com> --- worlds/tunic/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index ed2923037e..e86f731381 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -242,10 +242,18 @@ class TunicWorld(World): def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: item_data = item_table[name] - # if item_data.combat_ic is None, it'll take item_data.classification instead - itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None) + # evaluate alternate classifications based on options + # it'll choose whichever classification isn't None first in this if else tree + itemclass: ItemClassification = (classification + or (item_data.combat_ic if self.options.combat_logic else None) + or (ItemClassification.progression | ItemClassification.useful + if name == "Glass Cannon" and self.options.grass_randomizer + and not self.options.start_with_sword else None) + or (ItemClassification.progression | ItemClassification.useful + if name == "Shield" and self.options.ladder_storage + and not self.options.ladder_storage_without_items else None) or item_data.classification) - return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player) + return TunicItem(name, itemclass, self.item_name_to_id[name], self.player) def create_items(self) -> None: tunic_items: List[TunicItem] = [] @@ -278,8 +286,6 @@ class TunicWorld(World): if self.options.grass_randomizer: items_to_create["Grass"] = len(grass_location_table) - tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression)) - items_to_create["Glass Cannon"] = 0 for grass_location in excluded_grass_locations: self.get_location(grass_location).place_locked_item(self.create_item("Grass")) items_to_create["Grass"] -= len(excluded_grass_locations) @@ -351,11 +357,6 @@ class TunicWorld(World): tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful)) items_to_create[page] = 0 - # logically relevant if you have ladder storage enabled - if self.options.ladder_storage and not self.options.ladder_storage_without_items: - tunic_items.append(self.create_item("Shield", ItemClassification.progression)) - items_to_create["Shield"] = 0 - if self.options.maskless: tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) items_to_create["Scavenger Mask"] = 0 From cc770418f2d1d5c88ec08f30ac45c05ea704445c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:22:33 +0100 Subject: [PATCH 47/50] MultiServer: optimize PrintJSON for !release (#4545) * MultiServer: optimize PrintJSON for !release * MultiServer: safer comparison Co-authored-by: Doug Hoskisson --------- Co-authored-by: Doug Hoskisson --- MultiServer.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 653c2ecaab..9e0868b0f4 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1060,21 +1060,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], count_activity: bool = True): + slot_locations = ctx.locations[slot] new_locations = set(locations) - ctx.location_checks[team, slot] - new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata + new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata if new_locations: if count_activity: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) + + sortable: list[tuple[int, int, int, int]] = [] for location in new_locations: - item_id, target_player, flags = ctx.locations[slot][location] + # extract all fields to avoid runtime overhead in LocationStore + item_id, target_player, flags = slot_locations[location] + # sort/group by receiver and item + sortable.append((target_player, item_id, location, flags)) + + info_texts: list[dict[str, typing.Any]] = [] + for target_player, item_id, location, flags in sorted(sortable): new_item = NetworkItem(item_id, location, slot, flags) send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) - info_text = json_format_send_event(new_item, target_player) - ctx.broadcast_team(team, [info_text]) + if len(info_texts) >= 140: + # split into chunks that are close to compression window of 64K but not too big on the wire + # (roughly 1300-2600 bytes after compression depending on repetitiveness) + ctx.broadcast_team(team, info_texts) + info_texts.clear() + info_texts.append(json_format_send_event(new_item, target_player)) + ctx.broadcast_team(team, info_texts) + del info_texts + del sortable ctx.location_checks[team, slot] |= new_locations send_new_items(ctx) From 86641223c12852d998d45638ea664317d29f8e25 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 24 Jan 2025 18:35:54 -0500 Subject: [PATCH 48/50] Shivers: Stop using get_all_state cache to fix timing issue #4522 Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/shivers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index 5c6203fd57..85f2cf1861 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -245,7 +245,7 @@ class ShiversWorld(World): storage_items += [self.create_item("Empty") for _ in range(3)] - state = self.multiworld.get_all_state(True) + state = self.multiworld.get_all_state(False) self.random.shuffle(storage_locs) self.random.shuffle(storage_items) From 1832bac1a3c0e9b046c67271ee09601b26b0fe94 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Jan 2025 06:35:42 -0800 Subject: [PATCH 49/50] BizHawkClient: Update README for `get_memory_size` (#4511) --- worlds/_bizhawk/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/worlds/_bizhawk/README.md b/worlds/_bizhawk/README.md index ddc70c3dd7..9058fc3061 100644 --- a/worlds/_bizhawk/README.md +++ b/worlds/_bizhawk/README.md @@ -55,6 +55,7 @@ async def lock(ctx) -> None async def unlock(ctx) -> None async def get_hash(ctx) -> str +async def get_memory_size(ctx, domain: str) -> int async def get_system(ctx) -> str async def get_cores(ctx) -> dict[str, str] async def ping(ctx) -> None @@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe associate the file extension with Archipelago. `validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is -running on a system you specified in your `system` class variable. In most cases, that will be a single system and you -can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this -ROM as yours, this is where you should do setup for things like `items_handling`. +running on a system you specified in your `system` class variable. Take extra care here, because your code will run +against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size +of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where +you should do setup for things like `items_handling`. `game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM. `BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do @@ -268,6 +270,8 @@ server connection before trying to interact with it. - By default, the player will be asked to provide their slot name after connecting to the server and validating, and that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to set it automatically based on data in the ROM or on your client instance. +- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a +smaller ROM size. - You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a subclass of `CommonContext` and its API. - You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at From 96b941ed35cb5d34a44e261be6e00a7efd38175d Mon Sep 17 00:00:00 2001 From: josephwhite Date: Sat, 25 Jan 2025 09:36:23 -0500 Subject: [PATCH 50/50] Super Mario 64: Add Star Costs to Spoiler (#4544) --- worlds/sm64ex/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index afa67f233c..d54e0fc64d 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -48,6 +48,17 @@ class SM64World(World): filler_count: int star_costs: typing.Dict[str, int] + # Spoiler specific variable(s) + star_costs_spoiler_key_maxlen = len(max([ + 'First Floor Big Star Door', + 'Basement Big Star Door', + 'Second Floor Big Star Door', + 'MIPS 1', + 'MIPS 2', + 'Endless Stairs', + ], key=len)) + + def generate_early(self): max_stars = 120 if (not self.options.enable_coin_stars): @@ -238,3 +249,19 @@ class SM64World(World): for location in region.locations: er_hint_data[location.address] = entrance_name hint_data[self.player] = er_hint_data + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + # Write calculated star costs to spoiler. + star_cost_spoiler_header = '\n\n' + self.player_name + ' Star Costs for Super Mario 64:\n\n' + spoiler_handle.write(star_cost_spoiler_header) + # - Reformat star costs dictionary in spoiler to be a bit more readable. + star_costs_spoiler = {} + star_costs_copy = self.star_costs.copy() + star_costs_spoiler['First Floor Big Star Door'] = star_costs_copy['FirstBowserDoorCost'] + star_costs_spoiler['Basement Big Star Door'] = star_costs_copy['BasementDoorCost'] + star_costs_spoiler['Second Floor Big Star Door'] = star_costs_copy['SecondFloorDoorCost'] + star_costs_spoiler['MIPS 1'] = star_costs_copy['MIPS1Cost'] + star_costs_spoiler['MIPS 2'] = star_costs_copy['MIPS2Cost'] + star_costs_spoiler['Endless Stairs'] = star_costs_copy['StarsToFinish'] + for star, cost in star_costs_spoiler.items(): + spoiler_handle.write(f"{star:{self.star_costs_spoiler_key_maxlen}s} = {cost}\n")