From 32820ba6535fb2894caf38b468c42bbc46ee9414 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 11 Dec 2022 14:51:28 -0500 Subject: [PATCH] Pokemon R/B: Bug fixes and add trap weights (#1319) * [Pokemon R/B] Move type chart rando to generate_early and add trap weights * [Pokemon R/B] Update patching process on client to verify hash --- PokemonClient.py | 13 +++++- worlds/pokemon_rb/__init__.py | 81 +++++++++++++++++++++++++++++++++-- worlds/pokemon_rb/options.py | 39 +++++++++++++++-- worlds/pokemon_rb/rom.py | 70 ++---------------------------- 4 files changed, 127 insertions(+), 76 deletions(-) diff --git a/PokemonClient.py b/PokemonClient.py index d5f6e09f..eb1f1243 100644 --- a/PokemonClient.py +++ b/PokemonClient.py @@ -15,6 +15,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP get_base_parser from worlds.pokemon_rb.locations import location_data +from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} location_bytes_bits = {} @@ -265,8 +266,16 @@ async def run_game(romfile): async def patch_and_run_game(game_version, patch_file, ctx): base_name = os.path.splitext(patch_file)[0] comp_path = base_name + '.gb' - with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream: - base_rom = bytes(stream.read()) + if game_version == "blue": + delta_patch = BlueDeltaPatch + else: + delta_patch = RedDeltaPatch + + try: + base_rom = delta_patch.get_source_data() + except Exception as msg: + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) with zipfile.ZipFile(patch_file, 'r') as patch_archive: with patch_archive.open('delta.bsdiff4', 'r') as stream: diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 8119f259..ca695d99 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -1,6 +1,7 @@ from typing import TextIO import os import logging +from copy import deepcopy from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from Fill import fill_restrictive, FillError, sweep_from_pool @@ -62,6 +63,8 @@ class PokemonRedBlueWorld(World): self.learnsets = None self.trainer_name = None self.rival_name = None + self.type_chart = None + self.traps = None @classmethod def stage_assert_generate(cls, world): @@ -108,6 +111,69 @@ class PokemonRedBlueWorld(World): process_pokemon_data(self) + if self.multiworld.randomize_type_chart[self.player] == "vanilla": + chart = deepcopy(poke_data.type_chart) + elif self.multiworld.randomize_type_chart[self.player] == "randomize": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + self.multiworld.random.shuffle(matchups) + immunities = self.multiworld.immunity_matchups[self.player].value + super_effectives = self.multiworld.super_effective_matchups[self.player].value + not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value + normals = self.multiworld.normal_matchups[self.player].value + while super_effectives + not_very_effectives + normals < 225 - immunities: + super_effectives += self.multiworld.super_effective_matchups[self.player].value + not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value + normals += self.multiworld.normal_matchups[self.player].value + if super_effectives + not_very_effectives + normals > 225 - immunities: + total = super_effectives + not_very_effectives + normals + excess = total - (225 - immunities) + subtract_amounts = ( + int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * normals)) + super_effectives -= subtract_amounts[0] + not_very_effectives -= subtract_amounts[1] + normals -= subtract_amounts[2] + while super_effectives + not_very_effectives + normals > 225 - immunities: + r = self.multiworld.random.randint(0, 2) + if r == 0: + super_effectives -= 1 + elif r == 1: + not_very_effectives -= 1 + else: + normals -= 1 + chart = [] + for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], + [0, 10, 20, 5]): + for _ in range(matchup_list): + matchup = matchups.pop() + matchup.append(matchup_value) + chart.append(matchup) + elif self.multiworld.randomize_type_chart[self.player] == "chaos": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + chart = [] + values = list(range(21)) + self.multiworld.random.shuffle(matchups) + self.multiworld.random.shuffle(values) + for matchup in matchups: + value = values.pop(0) + values.append(value) + matchup.append(value) + chart.append(matchup) + # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" + # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to + # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes + # to the way effectiveness messages are generated. + self.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) + def create_items(self) -> None: start_inventory = self.multiworld.start_inventory[self.player].value.copy() if self.multiworld.randomize_pokedex[self.player] == "start_with": @@ -128,8 +194,7 @@ class PokemonRedBlueWorld(World): item = self.create_item(location.original_item) if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value): - item = self.create_item(self.multiworld.random.choice([item for item in item_table if - item_table[item].classification == ItemClassification.trap])) + item = self.create_item(self.select_trap()) if location.event: self.multiworld.get_location(location.name, self.player).place_locked_item(item) elif "Badge" not in item.name or self.multiworld.badgesanity[self.player].value: @@ -255,13 +320,21 @@ class PokemonRedBlueWorld(World): def get_filler_item_name(self) -> str: if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value: - return self.multiworld.random.choice([item for item in item_table if - item_table[item].classification == ItemClassification.trap]) + return self.select_trap() return self.multiworld.random.choice([item for item in item_table if item_table[ item].classification == ItemClassification.filler and item not in item_groups["Vending Machine Drinks"] + item_groups["Unique"]]) + def select_trap(self): + if self.traps is None: + self.traps = [] + self.traps += ["Poison Trap"] * self.multiworld.poison_trap_weight[self.player].value + self.traps += ["Fire Trap"] * self.multiworld.fire_trap_weight[self.player].value + self.traps += ["Paralyze Trap"] * self.multiworld.paralyze_trap_weight[self.player].value + self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value + return self.multiworld.random.choice(self.traps) + def fill_slot_data(self) -> dict: return { "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 3b6739a9..e2de9bc3 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -487,6 +487,7 @@ class BetterShops(Choice): class MasterBallPrice(Range): """Price for Master Balls. Can only be bought if better_shops is set to add_master_ball, but this will affect the sell price regardless. Vanilla is 0""" + display_name = "Master Ball Price" range_end = 999999 default = 5000 @@ -506,14 +507,42 @@ class LoseMoneyOnBlackout(Toggle): class TrapPercentage(Range): - """Chance for each filler item to be replaced with trap items: Poison Trap, Paralyze Trap, Ice Trap, and - Fire Trap. These traps apply the status to your entire party! Keep in mind that trainersanity vastly increases the - number of filler items. Make sure to stock up on Ice Heals!""" + """Chance for each filler item to be replaced with trap items. Keep in mind that trainersanity vastly increases the + number of filler items. The trap weight options will determine which traps can be chosen from and at what likelihood.""" display_name = "Trap Percentage" range_end = 100 default = 0 +class TrapWeight(Choice): + option_low = 1 + option_medium = 3 + option_high = 5 + default = 3 + + +class PoisonTrapWeight(TrapWeight): + """Weights for Poison Traps. These apply the Poison status to all your party members.""" + display_name = "Poison Trap Weight" + + +class FireTrapWeight(TrapWeight): + """Weights for Fire Traps. These apply the Burn status to all your party members.""" + display_name = "Fire Trap Weight" + + +class ParalyzeTrapWeight(TrapWeight): + """Weights for Paralyze Traps. These apply the Paralyze status to all your party members.""" + display_name = "Paralyze Trap Weight" + + +class IceTrapWeight(TrapWeight): + """Weights for Ice Traps. These apply the Ice status to all your party members. Don't forget to buy Ice Heals!""" + display_name = "Ice Trap Weight" + option_disabled = 0 + default = 0 + + pokemon_rb_options = { "game_version": GameVersion, "trainer_name": TrainerName, @@ -571,5 +600,9 @@ pokemon_rb_options = { "starting_money": StartingMoney, "lose_money_on_blackout": LoseMoneyOnBlackout, "trap_percentage": TrapPercentage, + "poison_trap_weight": PoisonTrapWeight, + "fire_trap_weight": FireTrapWeight, + "paralyze_trap_weight": ParalyzeTrapWeight, + "ice_trap_weight": IceTrapWeight, "death_link": DeathLink } \ No newline at end of file diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index a3e3510d..b48bd3ed 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -447,70 +447,8 @@ def generate_output(self, output_directory: str): if badge not in written_badges: write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) - if self.multiworld.randomize_type_chart[self.player] == "vanilla": - chart = deepcopy(poke_data.type_chart) - elif self.multiworld.randomize_type_chart[self.player] == "randomize": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - self.multiworld.random.shuffle(matchups) - immunities = self.multiworld.immunity_matchups[self.player].value - super_effectives = self.multiworld.super_effective_matchups[self.player].value - not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value - normals = self.multiworld.normal_matchups[self.player].value - while super_effectives + not_very_effectives + normals < 225 - immunities: - super_effectives += self.multiworld.super_effective_matchups[self.player].value - not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value - normals += self.multiworld.normal_matchups[self.player].value - if super_effectives + not_very_effectives + normals > 225 - immunities: - total = super_effectives + not_very_effectives + normals - excess = total - (225 - immunities) - subtract_amounts = (int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * normals)) - super_effectives -= subtract_amounts[0] - not_very_effectives -= subtract_amounts[1] - normals -= subtract_amounts[2] - while super_effectives + not_very_effectives + normals > 225 - immunities: - r = self.multiworld.random.randint(0, 2) - if r == 0: - super_effectives -= 1 - elif r == 1: - not_very_effectives -= 1 - else: - normals -= 1 - chart = [] - for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], - [0, 10, 20, 5]): - for _ in range(matchup_list): - matchup = matchups.pop() - matchup.append(matchup_value) - chart.append(matchup) - elif self.multiworld.randomize_type_chart[self.player] == "chaos": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - chart = [] - values = list(range(21)) - self.multiworld.random.shuffle(matchups) - self.multiworld.random.shuffle(values) - for matchup in matchups: - value = values.pop(0) - values.append(value) - matchup.append(value) - chart.append(matchup) - # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" - # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to - # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes - # to the way effectiveness messages are generated. - chart = sorted(chart, key=lambda matchup: -matchup[2]) - type_loc = rom_addresses["Type_Chart"] - for matchup in chart: + for matchup in self.type_chart: if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10 data[type_loc] = poke_data.type_ids[matchup[0]] data[type_loc + 1] = poke_data.type_ids[matchup[1]] @@ -520,8 +458,6 @@ def generate_output(self, output_directory: str): data[type_loc + 1] = 0xFF data[type_loc + 2] = 0xFF - self.type_chart = chart - if self.multiworld.normalize_encounter_chances[self.player].value: chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] for i, chance in enumerate(chances): @@ -652,8 +588,8 @@ def get_base_rom_bytes(game_version: str, hash: str="") -> bytes: basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) if hash != basemd5.hexdigest(): - raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. ' - 'Get the correct game and version, then dump it') + raise Exception(f"Supplied Base Rom does not match known MD5 for Pokémon {game_version.title()} UE " + "release. Get the correct game and version, then dump it") return base_rom_bytes