From 5021997df0997f0bd1151c6e5e523c38c4eafdac Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 11:13:01 -0500 Subject: [PATCH 01/16] Launcher: explicitly handle cli arguments to be passed to the Component (#3714) * adds handling for the `--` cli arg by having launcher capture, ignore, and pass through all of the values after it, while only processing (and validating) the values before it updates text client and its components to allow for args to be passed through, captured in run_as_textclient, and used in parse_args if present * Update worlds/LauncherComponents.py Co-authored-by: Aaron Wagener * explicitly using default args for parse_args when launched directly * revert manual arg parsing by request * Update CommonClient.py * Update LauncherComponents.py * :) --------- Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 4 ++-- Launcher.py | 5 ++++- worlds/LauncherComponents.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 7f91172acf..122de476fe 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1035,7 +1035,7 @@ def run_as_textclient(*args): parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") - args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable + args = parser.parse_args(args) if args.url: url = urllib.parse.urlparse(args.url) @@ -1053,4 +1053,4 @@ def run_as_textclient(*args): if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING - run_as_textclient() + run_as_textclient(*sys.argv[1:]) # default value for parse_args diff --git a/Launcher.py b/Launcher.py index 97903e2ad1..42f93547cc 100644 --- a/Launcher.py +++ b/Launcher.py @@ -401,7 +401,10 @@ if __name__ == '__main__': init_logging('Launcher') Utils.freeze_support() multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work - parser = argparse.ArgumentParser(description='Archipelago Launcher') + parser = argparse.ArgumentParser( + description='Archipelago Launcher', + usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]" + ) run_group = parser.add_argument_group("Run") run_group.add_argument("--update_settings", action="store_true", help="Update host.yaml and exit.") diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 4c64642aba..fe6e44bb30 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -61,7 +61,7 @@ class Component: processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()): +def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: global processes import multiprocessing process = multiprocessing.Process(target=func, name=name, args=args) @@ -85,7 +85,7 @@ class SuffixIdentifier: def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, "TextClient", args) + launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: From e4a5ed1cc45b4d58ba4ebdf095e5a990581bcee3 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 11:40:32 -0500 Subject: [PATCH 02/16] CommonClient: Explicitly parse url arg as an archipelago:// url (#3568) * Launcher "Text Client" --connect archipelago.gg:38281 should work, it doesn't, this fixes that * more explicit handling of expected values * removing launcher updates meaning this pr cannot stand alone but will not have merge issues later * add parser failure when an invalid url is found --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 122de476fe..911de4226d 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1039,11 +1039,14 @@ def run_as_textclient(*args): if args.url: url = urllib.parse.urlparse(args.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) + 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") colorama.init() From cabfef669a74936000975d911105075b51a79595 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:46:58 -0400 Subject: [PATCH 03/16] Stardew Valley: Fix masteries logic so it requires levels and tools (#3640) * fix and add test * add test to make sure we check xp can be earned * fix python 3.8 test my god I hope it gets removed soon * fixing some review comments * curse you monstersanity * move month rule to has_level vanilla, so next level is in logic once the previous item is received * use progressive masteries to skills in test alsanity * rename reset_collection_state * add more tests around skill and masteries rules * progressive level issue --------- Co-authored-by: agilbert1412 --- worlds/stardew_valley/logic/skill_logic.py | 72 +++++++------- worlds/stardew_valley/rules.py | 12 ++- worlds/stardew_valley/test/__init__.py | 8 +- .../stardew_valley/test/rules/TestSkills.py | 97 ++++++++++++++++--- 4 files changed, 137 insertions(+), 52 deletions(-) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 4d5567302a..17fabca28d 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -15,13 +15,13 @@ from .. import options from ..data.harvest import HarvestCropSource from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_skills_levels import get_mod_skill_levels -from ..stardew_rule import StardewRule, True_, False_, true_, And +from ..stardew_rule import StardewRule, true_, True_, False_ from ..strings.craftable_names import Fishing from ..strings.machine_names import Machine from ..strings.performance_names import Performance from ..strings.quality_names import ForageQuality from ..strings.region_names import Region -from ..strings.skill_names import Skill, all_mod_skills +from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills from ..strings.tool_names import ToolMaterial, Tool from ..strings.wallet_item_names import Wallet @@ -43,22 +43,17 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): if level <= 0: return True_() - tool_level = (level - 1) // 2 + tool_level = min(4, (level - 1) // 2) tool_material = ToolMaterial.tiers[tool_level] - months = max(1, level - 1) - months_rule = self.logic.time.has_lived_months(months) - if self.options.skill_progression != options.SkillProgression.option_vanilla: - previous_level_rule = self.logic.skill.has_level(skill, level - 1) - else: - previous_level_rule = true_ + previous_level_rule = self.logic.skill.has_previous_level(skill, level) if skill == Skill.fishing: xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) elif skill == Skill.foraging: - xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\ + xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) | \ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) elif skill == Skill.mining: xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \ @@ -70,22 +65,34 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5) elif skill in all_mod_skills: # Ideal solution would be to add a logic registry, but I'm too lazy. - return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) + return previous_level_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) else: raise Exception(f"Unknown skill: {skill}") - return previous_level_rule & months_rule & xp_rule + return previous_level_rule & xp_rule # Should be cached def has_level(self, skill: str, level: int) -> StardewRule: - if level <= 0: - return True_() + assert level >= 0, f"There is no level before level 0." + if level == 0: + return true_ if self.options.skill_progression == options.SkillProgression.option_vanilla: return self.logic.skill.can_earn_level(skill, level) return self.logic.received(f"{skill} Level", level) + def has_previous_level(self, skill: str, level: int) -> StardewRule: + assert level > 0, f"There is no level before level 0." + if level == 1: + return true_ + + if self.options.skill_progression == options.SkillProgression.option_vanilla: + months = max(1, level - 1) + return self.logic.time.has_lived_months(months) + + return self.logic.received(f"{skill} Level", level - 1) + @cache_self1 def has_farming_level(self, level: int) -> StardewRule: return self.logic.skill.has_level(Skill.farming, level) @@ -108,18 +115,9 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): return rule_with_fishing return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing - def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_vanilla: - return self.has_total_level(50) - skills_items = vanilla_skill_items - if included_modded_skills: - skills_items += get_mod_skill_levels(self.options.mods) - return And(*[self.logic.received(skill, 10) for skill in skills_items]) - - def can_enter_mastery_cave(self) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: - return self.logic.received(Wallet.mastery_of_the_five_ways) - return self.has_all_skills_maxed() + def has_any_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: + skills = self.content.skills.keys() if included_modded_skills else sorted(all_vanilla_skills) + return self.logic.or_(*(self.logic.skill.has_level(skill, 10) for skill in skills)) @cached_property def can_get_farming_xp(self) -> StardewRule: @@ -197,13 +195,19 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): return self.has_level(Skill.foraging, 9) return False_() - @cached_property - def can_earn_mastery_experience(self) -> StardewRule: - if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: - return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months - return self.logic.time.has_lived_max_months + def can_earn_mastery(self, skill: str) -> StardewRule: + # Checking for level 11, so it includes having level 10 and being able to earn xp. + return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave) def has_mastery(self, skill: str) -> StardewRule: - if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: - return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave) - return self.logic.received(f"{skill} Mastery") + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(f"{skill} Mastery") + + return self.logic.skill.can_earn_mastery(skill) + + @cached_property + def can_enter_mastery_cave(self) -> StardewRule: + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(Wallet.mastery_of_the_five_ways) + + return self.has_any_skills_maxed(included_modded_skills=False) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 89b1cf87c3..e9bdd8c25b 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -154,7 +154,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw extra_raccoons = extra_raccoons + num bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules if num > 1: - previous_bundle_name = f"Raccoon Request {num-1}" + previous_bundle_name = f"Raccoon Request {num - 1}" bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name) room_rules.append(bundle_rules) MultiWorldRules.set_rule(location, bundle_rules) @@ -168,13 +168,16 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta mods = world_options.mods if world_options.skill_progression == SkillProgression.option_vanilla: return + for i in range(1, 11): set_vanilla_skill_rule_for_level(logic, multiworld, player, i) set_modded_skill_rule_for_level(logic, multiworld, player, mods, i) - if world_options.skill_progression != SkillProgression.option_progressive_with_masteries: + + if world_options.skill_progression == SkillProgression.option_progressive: return + for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]: - MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience) + MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill)) def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): @@ -256,8 +259,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin) set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) - set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) - set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) + set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave) set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2)) set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two) set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 4dee0ebf6d..e7278cba28 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -85,7 +85,7 @@ def allsanity_no_mods_6_x_x(): options.QuestLocations.internal_name: 56, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, options.ToolProgression.internal_name: options.ToolProgression.option_progressive, options.TrapItems.internal_name: options.TrapItems.option_nightmare, @@ -310,6 +310,12 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): self.multiworld.worlds[self.player].total_progression_items -= 1 return created_item + def remove_one_by_name(self, item: str) -> None: + self.remove(self.create_item(item)) + + def reset_collection_state(self): + self.multiworld.state = self.original_state.copy() + pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py index 1c6874f315..77adade886 100644 --- a/worlds/stardew_valley/test/rules/TestSkills.py +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -1,23 +1,30 @@ -from ... import HasProgressionPercent +from ... import HasProgressionPercent, StardewLogic from ...options import ToolProgression, SkillProgression, Mods -from ...strings.skill_names import all_skills +from ...strings.skill_names import all_skills, all_vanilla_skills, Skill from ...test import SVTestBase -class TestVanillaSkillLogicSimplification(SVTestBase): +class TestSkillProgressionVanilla(SVTestBase): options = { SkillProgression.internal_name: SkillProgression.option_vanilla, ToolProgression.internal_name: ToolProgression.option_progressive, } def test_skill_logic_has_level_only_uses_one_has_progression_percent(self): - rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8) - self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent)) + rule = self.multiworld.worlds[1].logic.skill.has_level(Skill.farming, 8) + self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) is HasProgressionPercent)) + + def test_has_mastery_requires_month_equivalent_to_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + time_rule = logic.time.has_lived_months(10) + + self.assertIn(time_rule, rule.current_rules) -class TestAllSkillsRequirePrevious(SVTestBase): +class TestSkillProgressionProgressive(SVTestBase): options = { - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + SkillProgression.internal_name: SkillProgression.option_progressive, Mods.internal_name: frozenset(Mods.valid_keys), } @@ -25,16 +32,82 @@ class TestAllSkillsRequirePrevious(SVTestBase): for skill in all_skills: self.collect_everything() self.remove_by_name(f"{skill} Level") + for level in range(1, 11): location_name = f"Level {level} {skill}" + location = self.multiworld.get_location(location_name, self.player) + with self.subTest(location_name): - can_reach = self.can_reach_location(location_name) if level > 1: - self.assertFalse(can_reach) + self.assert_reach_location_false(location, self.multiworld.state) self.collect(f"{skill} Level") - can_reach = self.can_reach_location(location_name) - self.assertTrue(can_reach) - self.multiworld.state = self.original_state.copy() + + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_has_level_requires_exact_amount_of_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 8) + + self.assertEqual(level_rule, rule) + + def test_has_previous_level_requires_one_less_level_than_requested(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_previous_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 7) + + self.assertEqual(level_rule, rule) + + def test_has_mastery_requires_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + level_rule = logic.received("Farming Level", 10) + + self.assertIn(level_rule, rule.current_rules) +class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ToolProgression.internal_name: ToolProgression.option_progressive, + Mods.internal_name: frozenset(), + } + def test_has_mastery_requires_the_item(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + received_mastery = logic.received("Farming Mastery") + + self.assertEqual(received_mastery, rule) + + def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self): + self.collect_everything() + + for skill in all_vanilla_skills: + with self.subTest(skill): + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + for skill in all_vanilla_skills: + with self.subTest(skill): + self.collect_everything() + self.remove_one_by_name(f"{skill} Level") + + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + self.collect_everything() + + self.remove_one_by_name(f"Progressive Pickaxe") + location = self.multiworld.get_location("Mining Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + + self.reset_collection_state() From 05b257adf9bd9300acd4ff5584f6087f70716ad1 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 8 Sep 2024 09:48:48 -0700 Subject: [PATCH 04/16] Pokemon Emerald: Make use of `NamedTuple._replace` (#3727) --- worlds/pokemon_emerald/data.py | 6 ++---- worlds/pokemon_emerald/opponents.py | 6 +++--- worlds/pokemon_emerald/pokemon.py | 31 +++++++++++------------------ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index d89ab5febb..432d593873 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -276,15 +276,13 @@ def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum: return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES -@dataclass -class TrainerPokemonData: +class TrainerPokemonData(NamedTuple): species_id: int level: int moves: Optional[Tuple[int, int, int, int]] -@dataclass -class TrainerPartyData: +class TrainerPartyData(NamedTuple): pokemon: List[TrainerPokemonData] pokemon_data_type: TrainerPokemonDataTypeEnum address: int diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index 09e947546d..966d192054 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Dict, List, Set -from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data +from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, data from .options import RandomizeTrainerParties from .pokemon import filter_species_by_nearby_bst from .util import int_to_bool_array @@ -111,6 +111,6 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3] ) - new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves)) + new_party.append(pokemon._replace(species_id=new_species.species_id, moves=new_moves)) - trainer.party.pokemon = new_party + trainer.party = trainer.party._replace(pokemon=new_party) diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index c60e5e9d4f..fec1101dab 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -4,8 +4,7 @@ Functions related to pokemon species and moves import functools from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple -from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData, - SpeciesData, data) +from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data) from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters, RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon, TmTutorCompatibility) @@ -461,7 +460,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: type_bias, normal_bias, species.types) else: new_move = 0 - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 # All moves from here onward are actual moves. @@ -473,7 +472,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: new_move = get_random_move(world.random, {move.move_id for move in new_learnset} | world.blacklisted_moves, type_bias, normal_bias, species.types) - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 species.learnset = new_learnset @@ -581,8 +580,10 @@ def randomize_starters(world: "PokemonEmeraldWorld") -> None: picked_evolution = world.random.choice(potential_evolutions) for trainer_name, starter_position, is_evolved in rival_teams[i]: + new_species_id = picked_evolution if is_evolved else starter.species_id trainer_data = world.modified_trainers[data.constants[trainer_name]] - trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id + trainer_data.party.pokemon[starter_position] = \ + trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id) def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: @@ -594,10 +595,7 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: world.random.shuffle(shuffled_species) for i, encounter in enumerate(data.legendary_encounters): - world.modified_legendary_encounters.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.legendary_encounters in { RandomizeLegendaryEncounters.option_match_base_stats, @@ -621,9 +619,8 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: if should_match_bst: candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats)) - world.modified_legendary_encounters.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_legendary_encounters.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) @@ -637,10 +634,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: world.modified_misc_pokemon = [] for i, encounter in enumerate(data.misc_pokemon): - world.modified_misc_pokemon.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.misc_pokemon in { RandomizeMiscPokemon.option_match_base_stats, @@ -672,9 +666,8 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: if len(player_filtered_candidates) > 0: candidates = player_filtered_candidates - world.modified_misc_pokemon.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_misc_pokemon.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) From 6d6d35d598dbb984b7b5cccf6567d5dc9e4ddd7d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:50:08 -0500 Subject: [PATCH 05/16] Rogue Legacy: Update to Options API (#3755) * fix deprecation * multiworld.random -> world.random * Various small fixes --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic --- worlds/rogue_legacy/Options.py | 78 ++++++++++--------- worlds/rogue_legacy/Regions.py | 29 +++---- worlds/rogue_legacy/Rules.py | 50 ++++++------ worlds/rogue_legacy/__init__.py | 111 +++++++++++++-------------- worlds/rogue_legacy/test/__init__.py | 2 +- 5 files changed, 138 insertions(+), 132 deletions(-) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index d8298c85c8..9210082f73 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -1,6 +1,6 @@ -from typing import Dict +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet +from dataclasses import dataclass class StartingGender(Choice): @@ -336,42 +336,44 @@ class AvailableClasses(OptionSet): The upgraded form of your starting class will be available regardless. """ display_name = "Available Classes" - default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + default = frozenset( + {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + ) valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} -rl_options: Dict[str, type(Option)] = { - "starting_gender": StartingGender, - "starting_class": StartingClass, - "available_classes": AvailableClasses, - "new_game_plus": NewGamePlus, - "fairy_chests_per_zone": FairyChestsPerZone, - "chests_per_zone": ChestsPerZone, - "universal_fairy_chests": UniversalFairyChests, - "universal_chests": UniversalChests, - "vendors": Vendors, - "architect": Architect, - "architect_fee": ArchitectFee, - "disable_charon": DisableCharon, - "require_purchasing": RequirePurchasing, - "progressive_blueprints": ProgressiveBlueprints, - "gold_gain_multiplier": GoldGainMultiplier, - "number_of_children": NumberOfChildren, - "free_diary_on_generation": FreeDiaryOnGeneration, - "khidr": ChallengeBossKhidr, - "alexander": ChallengeBossAlexander, - "leon": ChallengeBossLeon, - "herodotus": ChallengeBossHerodotus, - "health_pool": HealthUpPool, - "mana_pool": ManaUpPool, - "attack_pool": AttackUpPool, - "magic_damage_pool": MagicDamageUpPool, - "armor_pool": ArmorUpPool, - "equip_pool": EquipUpPool, - "crit_chance_pool": CritChanceUpPool, - "crit_damage_pool": CritDamageUpPool, - "allow_default_names": AllowDefaultNames, - "additional_lady_names": AdditionalNames, - "additional_sir_names": AdditionalNames, - "death_link": DeathLink, -} +@dataclass +class RLOptions(PerGameCommonOptions): + starting_gender: StartingGender + starting_class: StartingClass + available_classes: AvailableClasses + new_game_plus: NewGamePlus + fairy_chests_per_zone: FairyChestsPerZone + chests_per_zone: ChestsPerZone + universal_fairy_chests: UniversalFairyChests + universal_chests: UniversalChests + vendors: Vendors + architect: Architect + architect_fee: ArchitectFee + disable_charon: DisableCharon + require_purchasing: RequirePurchasing + progressive_blueprints: ProgressiveBlueprints + gold_gain_multiplier: GoldGainMultiplier + number_of_children: NumberOfChildren + free_diary_on_generation: FreeDiaryOnGeneration + khidr: ChallengeBossKhidr + alexander: ChallengeBossAlexander + leon: ChallengeBossLeon + herodotus: ChallengeBossHerodotus + health_pool: HealthUpPool + mana_pool: ManaUpPool + attack_pool: AttackUpPool + magic_damage_pool: MagicDamageUpPool + armor_pool: ArmorUpPool + equip_pool: EquipUpPool + crit_chance_pool: CritChanceUpPool + crit_damage_pool: CritDamageUpPool + allow_default_names: AllowDefaultNames + additional_lady_names: AdditionalNames + additional_sir_names: AdditionalNames + death_link: DeathLink diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py index 5d07fccbc4..61b0ef73ec 100644 --- a/worlds/rogue_legacy/Regions.py +++ b/worlds/rogue_legacy/Regions.py @@ -1,15 +1,18 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from BaseClasses import MultiWorld, Region, Entrance from .Locations import RLLocation, location_table, get_locations_by_category +if TYPE_CHECKING: + from . import RLWorld + class RLRegionData(NamedTuple): locations: Optional[List[str]] region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_regions(world: "RLWorld"): regions: Dict[str, RLRegionData] = { "Menu": RLRegionData(None, ["Castle Hamson"]), "The Manor": RLRegionData([], []), @@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["The Fountain Room"].locations.append("Fountain Room") # Chests - chests = int(multiworld.chests_per_zone[player]) + chests = int(world.options.chests_per_zone) for i in range(0, chests): - if multiworld.universal_chests[player]: + if world.options.universal_chests: regions["Castle Hamson"].locations.append(f"Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}") @@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}") # Fairy Chests - chests = int(multiworld.fairy_chests_per_zone[player]) + chests = int(world.options.fairy_chests_per_zone) for i in range(0, chests): - if multiworld.universal_fairy_chests[player]: + if world.options.universal_fairy_chests: regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}") @@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int): # Set up the regions correctly. for name, data in regions.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) + world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data)) - multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player)) - multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player)) - multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player)) - multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player)) - multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player)) - multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player)) + world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson")) + world.get_entrance("The Manor").connect(world.get_region("The Manor")) + world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia")) + world.get_entrance("The Maya").connect(world.get_region("The Maya")) + world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness")) + world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room")) def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData): diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 2fac8d5613..505bbdd635 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -1,9 +1,13 @@ -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import RLWorld -def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: - return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \ - int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player]) +def get_upgrade_total(world: "RLWorld") -> int: + return int(world.options.health_pool) + int(world.options.mana_pool) + \ + int(world.options.attack_pool) + int(world.options.magic_damage_pool) def get_upgrade_count(state: CollectionState, player: int) -> int: @@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool return get_upgrade_count(state, player) >= amount -def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool: - return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100))) +def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool: + return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100))) def has_movement_rune(state: CollectionState, player: int) -> bool: @@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool: return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player) -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "RLWorld", player: int): # If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres. - if multiworld.vendors[player] == "normal": - multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \ + if world.options.vendors == "normal": + world.get_location("Forest Abkhazia Boss Reward").access_rule = \ lambda state: has_vendors(state, player) # Gate each manor location so everything isn't dumped into sphere 1. manor_rules = { - "Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [ + "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [ "Manor - Left Wing Window", "Manor - Left Wing Rooftop", "Manor - Right Wing Window", @@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Left Tree 2", "Manor - Right Tree", ], - "Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [ + "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [ "Manor - Left Big Upper 1", "Manor - Left Big Upper 2", "Manor - Left Big Windows", @@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Right Big Rooftop", "Manor - Right Extension", ], - "Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [ + "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [ "Manor - Right High Base", "Manor - Right High Upper", "Manor - Right High Tower", @@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int): # Set rules for manor locations. for event, locations in manor_rules.items(): for location in locations: - multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player) + world.get_location(location).access_rule = lambda state: state.has(event, player) # Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests. - for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]: + for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]: fairy_location.access_rule = lambda state: has_fairy_progression(state, player) # Region rules. - multiworld.get_entrance("Forest Abkhazia", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player) + world.get_entrance("Forest Abkhazia").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player) - multiworld.get_entrance("The Maya", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player) + world.get_entrance("The Maya").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player) - multiworld.get_entrance("Land of Darkness", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player) + world.get_entrance("Land of Darkness").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player) - multiworld.get_entrance("The Fountain Room", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player) + world.get_entrance("The Fountain Room").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player) # Win condition. - multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) + world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 78e56a794c..290f4a60ac 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -4,7 +4,7 @@ from BaseClasses import Tutorial from worlds.AutoWorld import WebWorld, World from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table -from .Options import rl_options +from .Options import RLOptions from .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -33,20 +33,17 @@ class RLWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game = "Rogue Legacy" - option_definitions = rl_options + options_dataclass = RLOptions + options: RLOptions topology_present = True required_client_version = (0, 3, 5) web = RLWeb() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.code for name, data in location_table.items()} - - # TODO: Replace calls to this function with "options-dict", once that PR is completed and merged. - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None} + location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None} def fill_slot_data(self) -> dict: - return {option_name: self.get_setting(option_name).value for option_name in rl_options} + return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) def generate_early(self): location_ids_used_per_game = { @@ -74,18 +71,18 @@ class RLWorld(World): ) # Check validation of names. - additional_lady_names = len(self.get_setting("additional_lady_names").value) - additional_sir_names = len(self.get_setting("additional_sir_names").value) - if not self.get_setting("allow_default_names"): - if additional_lady_names < int(self.get_setting("number_of_children")): + additional_lady_names = len(self.options.additional_lady_names.value) + additional_sir_names = len(self.options.additional_sir_names.value) + if not self.options.allow_default_names: + if additional_lady_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_lady_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}") - if additional_sir_names < int(self.get_setting("number_of_children")): + if additional_sir_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_sir_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}") def create_items(self): item_pool: List[RLItem] = [] @@ -95,110 +92,110 @@ class RLWorld(World): # Architect if name == "Architect": - if self.get_setting("architect") == "disabled": + if self.options.architect == "disabled": continue - if self.get_setting("architect") == "start_unlocked": + if self.options.architect == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("architect") == "early": + if self.options.architect == "early": self.multiworld.local_early_items[self.player]["Architect"] = 1 # Blacksmith and Enchantress if name == "Blacksmith" or name == "Enchantress": - if self.get_setting("vendors") == "start_unlocked": + if self.options.vendors == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("vendors") == "early": + if self.options.vendors == "early": self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 self.multiworld.local_early_items[self.player]["Enchantress"] = 1 # Haggling - if name == "Haggling" and self.get_setting("disable_charon"): + if name == "Haggling" and self.options.disable_charon: continue # Blueprints if data.category == "Blueprints": # No progressive blueprints if progressive_blueprints are disabled. - if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"): + if name == "Progressive Blueprints" and not self.options.progressive_blueprints: continue # No distinct blueprints if progressive_blueprints are enabled. - elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"): + elif name != "Progressive Blueprints" and self.options.progressive_blueprints: continue # Classes if data.category == "Classes": if name == "Progressive Knights": - if "Knight" not in self.get_setting("available_classes"): + if "Knight" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knight": + if self.options.starting_class == "knight": quantity = 1 if name == "Progressive Mages": - if "Mage" not in self.get_setting("available_classes"): + if "Mage" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "mage": + if self.options.starting_class == "mage": quantity = 1 if name == "Progressive Barbarians": - if "Barbarian" not in self.get_setting("available_classes"): + if "Barbarian" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "barbarian": + if self.options.starting_class == "barbarian": quantity = 1 if name == "Progressive Knaves": - if "Knave" not in self.get_setting("available_classes"): + if "Knave" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knave": + if self.options.starting_class == "knave": quantity = 1 if name == "Progressive Miners": - if "Miner" not in self.get_setting("available_classes"): + if "Miner" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "miner": + if self.options.starting_class == "miner": quantity = 1 if name == "Progressive Shinobis": - if "Shinobi" not in self.get_setting("available_classes"): + if "Shinobi" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "shinobi": + if self.options.starting_class == "shinobi": quantity = 1 if name == "Progressive Liches": - if "Lich" not in self.get_setting("available_classes"): + if "Lich" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "lich": + if self.options.starting_class == "lich": quantity = 1 if name == "Progressive Spellthieves": - if "Spellthief" not in self.get_setting("available_classes"): + if "Spellthief" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "spellthief": + if self.options.starting_class == "spellthief": quantity = 1 if name == "Dragons": - if "Dragon" not in self.get_setting("available_classes"): + if "Dragon" not in self.options.available_classes: continue if name == "Traitors": - if "Traitor" not in self.get_setting("available_classes"): + if "Traitor" not in self.options.available_classes: continue # Skills if name == "Health Up": - quantity = self.get_setting("health_pool") + quantity = self.options.health_pool.value elif name == "Mana Up": - quantity = self.get_setting("mana_pool") + quantity = self.options.mana_pool.value elif name == "Attack Up": - quantity = self.get_setting("attack_pool") + quantity = self.options.attack_pool.value elif name == "Magic Damage Up": - quantity = self.get_setting("magic_damage_pool") + quantity = self.options.magic_damage_pool.value elif name == "Armor Up": - quantity = self.get_setting("armor_pool") + quantity = self.options.armor_pool.value elif name == "Equip Up": - quantity = self.get_setting("equip_pool") + quantity = self.options.equip_pool.value elif name == "Crit Chance Up": - quantity = self.get_setting("crit_chance_pool") + quantity = self.options.crit_chance_pool.value elif name == "Crit Damage Up": - quantity = self.get_setting("crit_damage_pool") + quantity = self.options.crit_damage_pool.value # Ignore filler, it will be added in a later stage. if data.category == "Filler": @@ -215,7 +212,7 @@ class RLWorld(World): def get_filler_item_name(self) -> str: fillers = get_items_by_category("Filler") weights = [data.weight for data in fillers.values()] - return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] + return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] def create_item(self, name: str) -> RLItem: data = item_table[name] @@ -226,10 +223,10 @@ class RLWorld(World): return RLItem(name, data.classification, data.code, self.player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self, self.player) def create_regions(self): - create_regions(self.multiworld, self.player) + create_regions(self) self._place_events() def _place_events(self): @@ -238,7 +235,7 @@ class RLWorld(World): self.create_event("Defeat The Fountain")) # Khidr / Neo Khidr - if self.get_setting("khidr") == "vanilla": + if self.options.khidr == "vanilla": self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( self.create_event("Defeat Khidr")) else: @@ -246,7 +243,7 @@ class RLWorld(World): self.create_event("Defeat Neo Khidr")) # Alexander / Alexander IV - if self.get_setting("alexander") == "vanilla": + if self.options.alexander == "vanilla": self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( self.create_event("Defeat Alexander")) else: @@ -254,7 +251,7 @@ class RLWorld(World): self.create_event("Defeat Alexander IV")) # Ponce de Leon / Ponce de Freon - if self.get_setting("leon") == "vanilla": + if self.options.leon == "vanilla": self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( self.create_event("Defeat Ponce de Leon")) else: @@ -262,7 +259,7 @@ class RLWorld(World): self.create_event("Defeat Ponce de Freon")) # Herodotus / Astrodotus - if self.get_setting("herodotus") == "vanilla": + if self.options.herodotus == "vanilla": self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( self.create_event("Defeat Herodotus")) else: diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py index 2639e618c6..3346476ba6 100644 --- a/worlds/rogue_legacy/test/__init__.py +++ b/worlds/rogue_legacy/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class RLTestBase(WorldTestBase): From cf375cbcc4c399290b7ebc893e10992029761230 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 12:54:27 -0500 Subject: [PATCH 06/16] Core: Fix Generate's slot parsing to default unknown slot names to file name (#3795) * make Generate handle slots without names defined better * set name dict before loop so we don't have to check for its existence later * move setter so it's more obvious why --- Generate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index 6220c0eb81..4eba05cc52 100644 --- a/Generate.py +++ b/Generate.py @@ -155,6 +155,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output + erargs.name = {} settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -202,7 +203,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if path == args.weights_file_path: # if name came from the weights file, just use base player name erargs.name[player] = f"Player{player}" - elif not erargs.name[player]: # if name was not specified, generate it from filename + elif player not in erargs.name: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) From 5a5162c9d3a93295eccaad74fe28226f5cc0342f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 8 Sep 2024 12:55:17 -0500 Subject: [PATCH 07/16] The Messenger: improve automated installation (#3083) * add deck support to the messenger mod setup * Add tkinter cleanup because it's janky * prompt about launching the game instead of just doing it * add "better" file validation to courier checking * make it a bit more palatable * make it a bit more palatable * add the executable's md5 to ensure the correct file is selected * handle a bad md5 and show a message * make the utils wrapper snake_case and add a docstring * use stored archive instead of head * don't give other people the convenience method ig --- worlds/messenger/__init__.py | 1 + worlds/messenger/client_setup.py | 106 +++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 1bca3a37ad..9a38953ffb 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -27,6 +27,7 @@ class MessengerSettings(Group): class GamePath(FilePath): description = "The Messenger game executable" is_exe = True + md5s = ["1b53534569060bc06179356cd968ed1d"] game_path: GamePath = GamePath("TheMessenger.exe") diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 6bff78df36..77a0f63432 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -5,7 +5,6 @@ import os.path import subprocess import urllib.request from shutil import which -from tkinter.messagebox import askyesnocancel from typing import Any, Optional from zipfile import ZipFile from Utils import open_file @@ -18,11 +17,33 @@ from Utils import is_windows, messagebox, tuplize_version MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" +def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: + """ + Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. + + :param title: Title to be displayed at the top of the message box. + :param text: Text to be displayed inside the message box. + :return: Returns True if yes, False if no, None if cancel. + """ + from tkinter import Tk, messagebox + root = Tk() + root.withdraw() + ret = messagebox.askyesnocancel(title, text) + root.update() + return ret + + + def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: """Check if Courier is installed""" - return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll")) + assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll") + with open(assembly_path, "rb") as assembly: + for line in assembly: + if b"Courier" in line: + return True + return False def mod_installed() -> bool: """Check if the mod is installed""" @@ -57,27 +78,34 @@ def launch_game(*args) -> None: if not is_windows: mono_exe = which("mono") if not mono_exe: - # steam deck support but doesn't currently work - messagebox("Failure", "Failed to install Courier", True) - raise RuntimeError("Failed to install Courier") - # # download and use mono kickstart - # # this allows steam deck support - # mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip" - # target = os.path.join(folder, "monoKickstart") - # os.makedirs(target, exist_ok=True) - # with urllib.request.urlopen(mono_kick_url) as download: - # with ZipFile(io.BytesIO(download.read()), "r") as zf: - # for member in zf.infolist(): - # zf.extract(member, path=target) - # installer = subprocess.Popen([os.path.join(target, "precompiled"), - # os.path.join(folder, "MiniInstaller.exe")], shell=False) - # os.remove(target) + # download and use mono kickstart + # this allows steam deck support + mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip" + files = [] + with urllib.request.urlopen(mono_kick_url) as download: + with ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + if "precompiled/" not in member.filename or member.filename.endswith("/"): + continue + member.filename = member.filename.split("/")[-1] + if member.filename.endswith("bin.x86_64"): + member.filename = "MiniInstaller.bin.x86_64" + zf.extract(member, path=game_folder) + files.append(member.filename) + mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64") + os.chmod(mono_installer, 0o755) + installer = subprocess.Popen(mono_installer, shell=False) + failure = installer.wait() + for file in files: + os.remove(file) else: - installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False) + installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True) + failure = installer.wait() else: - installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False) + installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True) + failure = installer.wait() - failure = installer.wait() + print(failure) if failure: messagebox("Failure", "Failed to install Courier", True) os.chdir(working_directory) @@ -125,18 +153,35 @@ def launch_game(*args) -> None: return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version) from . import MessengerWorld - game_folder = os.path.dirname(MessengerWorld.settings.game_path) + try: + game_folder = os.path.dirname(MessengerWorld.settings.game_path) + except ValueError as e: + logging.error(e) + messagebox("Invalid File", "Selected file did not match expected hash. " + "Please try again and ensure you select The Messenger.exe.") + return working_directory = os.getcwd() + # setup ssl context + try: + import certifi + import ssl + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + context.set_alpn_protocols(["http/1.1"]) + https_handler = urllib.request.HTTPSHandler(context=context) + opener = urllib.request.build_opener(https_handler) + urllib.request.install_opener(opener) + except ImportError: + pass if not courier_installed(): - should_install = askyesnocancel("Install Courier", - "No Courier installation detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Courier", + "No Courier installation detected. Would you like to install now?") if not should_install: return logging.info("Installing Courier") install_courier() if not mod_installed(): - should_install = askyesnocancel("Install Mod", - "No randomizer mod detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Mod", + "No randomizer mod detected. Would you like to install now?") if not should_install: return logging.info("Installing Mod") @@ -144,17 +189,24 @@ def launch_game(*args) -> None: else: latest = request_data(MOD_URL)["tag_name"] if available_mod_update(latest): - should_update = askyesnocancel("Update Mod", - f"New mod version detected. Would you like to update to {latest} now?") + should_update = ask_yes_no_cancel("Update Mod", + f"New mod version detected. Would you like to update to {latest} now?") if should_update: logging.info("Updating mod") install_mod() elif should_update is None: return + if not args: + should_launch = ask_yes_no_cancel("Launch Game", + "Mod installed and up to date. Would you like to launch the game now?") + if not should_launch: + return + parser = argparse.ArgumentParser(description="Messenger Client Launcher") parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") args = parser.parse_args(args) + if not is_windows: if args.url: open_file(f"steam://rungameid/764790//{args.url}/") From e52ce0149a0470b594ce6f675dbd5b0bb7c994c0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:57:09 -0400 Subject: [PATCH 08/16] Rogue Legacy: Split Additional Names into two option classes #3908 --- worlds/rogue_legacy/Options.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index 9210082f73..139ff60944 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -175,13 +175,21 @@ class NumberOfChildren(Range): default = 3 -class AdditionalNames(OptionSet): +class AdditionalLadyNames(OptionSet): """ Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list of names your children can have. The first value will also be your initial character's name depending on Starting Gender. """ - display_name = "Additional Names" + display_name = "Additional Lady Names" + +class AdditionalSirNames(OptionSet): + """ + Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list + of names your children can have. The first value will also be your initial character's name depending on Starting + Gender. + """ + display_name = "Additional Sir Names" class AllowDefaultNames(DefaultOnToggle): @@ -374,6 +382,6 @@ class RLOptions(PerGameCommonOptions): crit_chance_pool: CritChanceUpPool crit_damage_pool: CritDamageUpPool allow_default_names: AllowDefaultNames - additional_lady_names: AdditionalNames - additional_sir_names: AdditionalNames + additional_lady_names: AdditionalLadyNames + additional_sir_names: AdditionalSirNames death_link: DeathLink From 4aab317665d5d2d9a72d2bc5ea9446639eaf887e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:56:15 -0500 Subject: [PATCH 09/16] ALTTP: Plando (#2904) fixes (#3834) --- Options.py | 14 ++++++++++++++ worlds/alttp/Options.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index ecde6275f1..b79714635d 100644 --- a/Options.py +++ b/Options.py @@ -973,7 +973,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + if isinstance(at, dict): + if at: + at = random.choices(list(at.keys()), + weights=list(at.values()), k=1)[0] + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") given_text = text.get("text", []) + if isinstance(given_text, dict): + if not given_text: + given_text = [] + else: + given_text = random.choices(list(given_text.keys()), + weights=list(given_text.values()), k=1) if isinstance(given_text, str): given_text = [given_text] texts.append(PlandoText( @@ -981,6 +993,8 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): given_text, text.get("percentage", 100) )) + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 20dd18038a..bd87cbf2c3 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections): entrances = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) - exits = set([connection[1] for connection in ( + exits = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) From 09c7f5f909e6ca23de5bc58f8174a1794b07f817 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:27 +0200 Subject: [PATCH 10/16] The Witness: Bump Required Client Version (#3891) The newest release of the Witness client connects with 0.5.1 https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/tag/7.0.0p10 --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index cdb17a483b..b4b38c883e 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -61,7 +61,7 @@ class WitnessWorld(World): item_name_groups = static_witness_items.ITEM_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 1) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations From 170aedba8fbe35765289a8628b89acf9fd1515ec Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:47 +0200 Subject: [PATCH 11/16] The Witness: Fix hints always displaying the Witness player (#3861) * The Witness: Fix hints always displaying the Witness player Got a bit too trigger happy with changing instances of `world.multiworld.player_name` to `world.player_name` - Some of these were actually *supposed* to be other players. Alternate title: The Witness doesn't have a Silph Scope * that one i guess --- worlds/witness/hints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 2c5f816b2b..99e8eea2eb 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.player_name + ")" + location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.player_name + ")" + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" hint_text = "" area: Optional[str] = None From 7ff201e32c859eeb1b3e07ee087f11da3249f833 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Tue, 10 Sep 2024 17:01:36 +0200 Subject: [PATCH 12/16] Yacht Dice: add get_filler_item_name (#3916) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. * add filler item name --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index 3a79eff040..d86ee3382d 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -466,6 +466,9 @@ class YachtDiceWorld(World): menu.exits.append(connection) connection.connect(board) self.multiworld.regions += [menu, board] + + def get_filler_item_name(self) -> str: + return "Good RNG" def set_rules(self): """ From 874392756b706bc07f4c1ff9429ed0b16e52abd3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:20:07 -0700 Subject: [PATCH 13/16] Pokemon Emerald: Add normalize encounter rate option to slot data (#3917) --- worlds/pokemon_emerald/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index abdee26f57..d281dde23c 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -711,6 +711,7 @@ class PokemonEmeraldWorld(World): "trainersanity", "modify_118", "death_link", + "normalize_encounter_rates", ) slot_data["free_fly_location_id"] = self.free_fly_location_id slot_data["hm_requirements"] = self.hm_requirements From c9f1a21bd2b8888e5b4dc75123c19b0a016ee261 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:22:04 -0700 Subject: [PATCH 14/16] BizHawkClient: Remove `run_gui` in favor of `make_gui` (#3910) --- CommonClient.py | 2 +- worlds/_bizhawk/context.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 911de4226d..6bdd8fc819 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -662,7 +662,7 @@ class CommonContext: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def make_gui(self) -> type: + def make_gui(self) -> typing.Type["kvui.GameManager"]: """To return the Kivy App class needed for run_gui so it can be overridden before being built""" from kvui import GameManager diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 234faf3b65..896c8fb7b5 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -59,14 +59,10 @@ class BizHawkClientContext(CommonContext): self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 - def run_gui(self): - from kvui import GameManager - - class BizHawkManager(GameManager): - base_title = "Archipelago BizHawk Client" - - self.ui = BizHawkManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def make_gui(self): + ui = super().make_gui() + ui.base_title = "Archipelago BizHawk Client" + return ui def on_package(self, cmd, args): if cmd == "Connected": From 7621889b8b626e89947d6258ddd5ade65d434ddb Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 11 Sep 2024 04:22:53 -0700 Subject: [PATCH 15/16] DS3: Add nex3 as a world maintainer (#3882) I've already discussed this with @Marechal-L and gotten his approval. --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 28dcc67362..ee7fd7ed86 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,7 +46,7 @@ /worlds/clique/ @ThePhar # Dark Souls III -/worlds/dark_souls_3/ @Marechal-L +/worlds/dark_souls_3/ @Marechal-L @nex3 # Donkey Kong Country 3 /worlds/dkc3/ @PoryGone From ed948e3e5b60ea67d126b7ef06a60dcccd71f4aa Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 13 Sep 2024 15:02:13 +0100 Subject: [PATCH 16/16] sm64ex: Add missing indirect condition for BitFS randomized entrance (#3926) The Bowser in the Fire Sea randomized entrance has an access rule that requires being able to reach "DDD: Board Bowser's Sub", but being able to reach a location also requires being able to reach the region that location is in, so an indirect condition is required. --- worlds/sm64ex/Regions.py | 4 ++-- worlds/sm64ex/Rules.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 6fc2d74b96..52126bcf9f 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -246,10 +246,10 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int): regBitS.subregions = [bits_top] -def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): +def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None) -> Entrance: sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - sourceRegion.connect(targetRegion, rule=rule) + return sourceRegion.connect(targetRegion, rule=rule) def create_region(name: str, player: int, world: MultiWorld) -> SM64Region: diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 9add8d9b29..1535f9ca1f 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -92,9 +92,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"], rf.build_rule("GP")) - connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], - lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + entrance = connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + # Access to "DDD: Board Bowser's Sub" does not require access to other locations or regions, so the only region that + # needs to be registered is its parent region. + world.register_indirect_condition(world.get_location("DDD: Board Bowser's Sub", player).parent_region, entrance) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))