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
This commit is contained in:
Alchav 2022-12-11 14:51:28 -05:00 committed by GitHub
parent 6173bc6e03
commit 32820ba653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 76 deletions

View File

@ -15,6 +15,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
get_base_parser get_base_parser
from worlds.pokemon_rb.locations import location_data from worlds.pokemon_rb.locations import location_data
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
location_bytes_bits = {} location_bytes_bits = {}
@ -265,8 +266,16 @@ async def run_game(romfile):
async def patch_and_run_game(game_version, patch_file, ctx): async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0] base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb' comp_path = base_name + '.gb'
with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream: if game_version == "blue":
base_rom = bytes(stream.read()) 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 zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream: with patch_archive.open('delta.bsdiff4', 'r') as stream:

View File

@ -1,6 +1,7 @@
from typing import TextIO from typing import TextIO
import os import os
import logging import logging
from copy import deepcopy
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from Fill import fill_restrictive, FillError, sweep_from_pool from Fill import fill_restrictive, FillError, sweep_from_pool
@ -62,6 +63,8 @@ class PokemonRedBlueWorld(World):
self.learnsets = None self.learnsets = None
self.trainer_name = None self.trainer_name = None
self.rival_name = None self.rival_name = None
self.type_chart = None
self.traps = None
@classmethod @classmethod
def stage_assert_generate(cls, world): def stage_assert_generate(cls, world):
@ -108,6 +111,69 @@ class PokemonRedBlueWorld(World):
process_pokemon_data(self) 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: def create_items(self) -> None:
start_inventory = self.multiworld.start_inventory[self.player].value.copy() start_inventory = self.multiworld.start_inventory[self.player].value.copy()
if self.multiworld.randomize_pokedex[self.player] == "start_with": if self.multiworld.randomize_pokedex[self.player] == "start_with":
@ -128,8 +194,7 @@ class PokemonRedBlueWorld(World):
item = self.create_item(location.original_item) item = self.create_item(location.original_item)
if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100)
<= self.multiworld.trap_percentage[self.player].value): <= self.multiworld.trap_percentage[self.player].value):
item = self.create_item(self.multiworld.random.choice([item for item in item_table if item = self.create_item(self.select_trap())
item_table[item].classification == ItemClassification.trap]))
if location.event: if location.event:
self.multiworld.get_location(location.name, self.player).place_locked_item(item) 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: 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: def get_filler_item_name(self) -> str:
if self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value: 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 return self.select_trap()
item_table[item].classification == ItemClassification.trap])
return self.multiworld.random.choice([item for item in item_table if item_table[ 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].classification == ItemClassification.filler and item not in item_groups["Vending Machine Drinks"] +
item_groups["Unique"]]) 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: def fill_slot_data(self) -> dict:
return { return {
"second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value,

View File

@ -487,6 +487,7 @@ class BetterShops(Choice):
class MasterBallPrice(Range): 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 """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""" sell price regardless. Vanilla is 0"""
display_name = "Master Ball Price"
range_end = 999999 range_end = 999999
default = 5000 default = 5000
@ -506,14 +507,42 @@ class LoseMoneyOnBlackout(Toggle):
class TrapPercentage(Range): class TrapPercentage(Range):
"""Chance for each filler item to be replaced with trap items: Poison Trap, Paralyze Trap, Ice Trap, and """Chance for each filler item to be replaced with trap items. Keep in mind that trainersanity vastly increases the
Fire Trap. These traps apply the status to your entire party! 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."""
number of filler items. Make sure to stock up on Ice Heals!"""
display_name = "Trap Percentage" display_name = "Trap Percentage"
range_end = 100 range_end = 100
default = 0 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 = { pokemon_rb_options = {
"game_version": GameVersion, "game_version": GameVersion,
"trainer_name": TrainerName, "trainer_name": TrainerName,
@ -571,5 +600,9 @@ pokemon_rb_options = {
"starting_money": StartingMoney, "starting_money": StartingMoney,
"lose_money_on_blackout": LoseMoneyOnBlackout, "lose_money_on_blackout": LoseMoneyOnBlackout,
"trap_percentage": TrapPercentage, "trap_percentage": TrapPercentage,
"poison_trap_weight": PoisonTrapWeight,
"fire_trap_weight": FireTrapWeight,
"paralyze_trap_weight": ParalyzeTrapWeight,
"ice_trap_weight": IceTrapWeight,
"death_link": DeathLink "death_link": DeathLink
} }

View File

@ -447,70 +447,8 @@ def generate_output(self, output_directory: str):
if badge not in written_badges: if badge not in written_badges:
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) 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"] 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 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] = poke_data.type_ids[matchup[0]]
data[type_loc + 1] = poke_data.type_ids[matchup[1]] 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 + 1] = 0xFF
data[type_loc + 2] = 0xFF data[type_loc + 2] = 0xFF
self.type_chart = chart
if self.multiworld.normalize_encounter_chances[self.player].value: if self.multiworld.normalize_encounter_chances[self.player].value:
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
for i, chance in enumerate(chances): 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 = hashlib.md5()
basemd5.update(base_rom_bytes) basemd5.update(base_rom_bytes)
if hash != basemd5.hexdigest(): if hash != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. ' raise Exception(f"Supplied Base Rom does not match known MD5 for Pokémon {game_version.title()} UE "
'Get the correct game and version, then dump it') "release. Get the correct game and version, then dump it")
return base_rom_bytes return base_rom_bytes