mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00

- Perhaps most critically, adds the ability for the door shuffle code to catch door shuffle exceptions and try again. Will try up to 10 times. Should mean Door Shuffle does not need to be disallowed in the big async🤞
- Door Shuffle code has been made drastically faster by searching for the first dead end instead of sorting the whole list of entrances by whether they are dead ends.
- Renames Full to Interiors, and adds a new Full door shuffle that shuffles interior-to-interior doors separately from exterior-to-interior doors.
- Adds a new Decoupled door shuffle.
- Warp Tile Shuffle now has 3 separate options, Vanilla, Shuffle, and Mixed. Shuffle shuffles the warp tiles among themselves, Mixed mixes them into the Door Shuffle pool.
- Safari Zone connections are now shuffled on Full, Insanity, and Decoupled.
- On Simple Door Shuffle, the Town Map is updated to show the new dungeon locations. The Town Map has been updated to show the locations of dungeons that previously were not shown unless you opened the map within them, and the Sea Cottage has been removed from it.
- Adds Auto Level Scaling that chooses the level scaling mode based on the Door Shuffle choice.
- Fixes issues with Flash and Fly move interventions (where it ensures an available Pokémon that can learn it is reachable depending on settings).
- Fixes a possible generation crash with type chart randomization.
- Should fix an issue where `stage_fill_hook` was able to remove the wrong item from the item pool resulting in a duplicated item reference existing.
- Adds a stage_post_fill function which searches for Pokémon in order of spheres, setting all but the first advancement Pokémon event found to `useful` so that spoiler playthrough calculation skips them. In a solo game gen test, this cut gen time from 15 seconds to 10 seconds with same seed number. Difference is likely to be much more massive in larger multiworlds.
680 lines
36 KiB
Python
680 lines
36 KiB
Python
import os
|
|
import hashlib
|
|
import Utils
|
|
import bsdiff4
|
|
import pkgutil
|
|
from worlds.Files import APDeltaPatch
|
|
from .text import encode_text
|
|
from .items import item_table
|
|
from .pokemon import set_mon_palettes
|
|
from .rock_tunnel import randomize_rock_tunnel
|
|
from .rom_addresses import rom_addresses
|
|
from .regions import PokemonRBWarp, map_ids, town_map_coords
|
|
from . import poke_data
|
|
|
|
|
|
def write_quizzes(self, data, random):
|
|
|
|
def get_quiz(q, a):
|
|
if q == 0:
|
|
r = random.randint(0, 3)
|
|
if r == 0:
|
|
mon = self.trade_mons["Trade_Dux"]
|
|
text = "A woman in<LINE>Vermilion City<CONT>"
|
|
elif r == 1:
|
|
mon = self.trade_mons["Trade_Lola"]
|
|
text = "A man in<LINE>Cerulean City<CONT>"
|
|
elif r == 2:
|
|
mon = self.trade_mons["Trade_Marcel"]
|
|
text = "Someone on Route 2<LINE>"
|
|
elif r == 3:
|
|
mon = self.trade_mons["Trade_Spot"]
|
|
text = "Someone on Route 5<LINE>"
|
|
if not a:
|
|
answers.append(0)
|
|
old_mon = mon
|
|
while old_mon == mon:
|
|
mon = random.choice(list(poke_data.pokemon_data.keys()))
|
|
|
|
return encode_text(f"{text}was looking for<CONT>{mon}?<DONE>")
|
|
elif q == 1:
|
|
for location in self.multiworld.get_filled_locations():
|
|
if location.item.name == "Secret Key" and location.item.player == self.player:
|
|
break
|
|
player_name = self.multiworld.player_name[location.player]
|
|
if not a:
|
|
if len(self.multiworld.player_name) > 1:
|
|
old_name = player_name
|
|
while old_name == player_name:
|
|
player_name = random.choice(list(self.multiworld.player_name.values()))
|
|
else:
|
|
return encode_text("You're playing<LINE>in a multiworld<CONT>with other<CONT>players?<DONE>")
|
|
if player_name == self.multiworld.player_name[self.player]:
|
|
player_name = "yourself"
|
|
player_name = encode_text(player_name, force=True, safety=True)
|
|
return encode_text(f"The Secret Key was<LINE>found by<CONT>") + player_name + encode_text("<DONE>")
|
|
elif q == 2:
|
|
if a:
|
|
return encode_text(f"#mon is<LINE>pronounced<CONT>Po-kay-mon?<DONE>")
|
|
else:
|
|
if random.randint(0, 1):
|
|
return encode_text(f"#mon is<LINE>pronounced<CONT>Po-key-mon?<DONE>")
|
|
else:
|
|
return encode_text(f"#mon is<LINE>pronounced<CONT>Po-kuh-mon?<DONE>")
|
|
elif q == 3:
|
|
starters = [" ".join(self.multiworld.get_location(
|
|
f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
|
|
mon = random.choice(starters)
|
|
nots = random.choice(range(8, 16, 2))
|
|
if random.randint(0, 1):
|
|
while mon in starters:
|
|
mon = random.choice(list(poke_data.pokemon_data.keys()))
|
|
if a:
|
|
nots += 1
|
|
elif not a:
|
|
nots += 1
|
|
text = f"{mon} was<LINE>"
|
|
while nots > 0:
|
|
i = random.randint(1, min(4, nots))
|
|
text += ("not " * i) + "<CONT>"
|
|
nots -= i
|
|
text += "a starter choice?<DONE>"
|
|
return encode_text(text)
|
|
elif q == 4:
|
|
if a:
|
|
tm_text = self.local_tms[27]
|
|
else:
|
|
if self.multiworld.randomize_tm_moves[self.player]:
|
|
wrong_tms = self.local_tms.copy()
|
|
wrong_tms.pop(27)
|
|
tm_text = random.choice(wrong_tms)
|
|
else:
|
|
tm_text = "TOMBSTONER"
|
|
return encode_text(f"TM28 contains<LINE>{tm_text.upper()}?<DONE>")
|
|
elif q == 5:
|
|
i = 8
|
|
while not a and i in [1, 8]:
|
|
i = random.randint(0, int("99999999"[random.randint(0, 7):]))
|
|
return encode_text(f"There are {i}<LINE>certified #MON<CONT>LEAGUE BADGEs?<DONE>")
|
|
elif q == 6:
|
|
i = 2
|
|
while not a and i in [1, 2]:
|
|
i = random.randint(0, random.choice([9, 99]))
|
|
return encode_text(f"POLIWAG evolves {i}<LINE>times?<DONE>")
|
|
elif q == 7:
|
|
entity = "Motor Carrier"
|
|
if not a:
|
|
entity = random.choice(["Driver", "Shipper"])
|
|
return encode_text("Title 49 of the<LINE>U.S. Code of<CONT>Federal<CONT>Regulations part<CONT>397.67 states"
|
|
f"<CONT>that the<CONT>{entity}<CONT>is responsible<CONT>for planning<CONT>routes when"
|
|
"<CONT>hazardous<CONT>materials are<CONT>transported?<DONE>")
|
|
elif q == 8:
|
|
mon = random.choice(list(poke_data.evolution_levels.keys()))
|
|
level = poke_data.evolution_levels[mon]
|
|
if not a:
|
|
level += random.choice(range(1, 6)) * random.choice((-1, 1))
|
|
return encode_text(f"{mon} evolves<LINE>at level {level}?<DONE>")
|
|
elif q == 9:
|
|
move = random.choice(list(self.local_move_data.keys()))
|
|
actual_type = self.local_move_data[move]["type"]
|
|
question_type = actual_type
|
|
while question_type == actual_type and not a:
|
|
question_type = random.choice(list(poke_data.type_ids.keys()))
|
|
return encode_text(f"{move} is<LINE>{question_type} type?<DONE>")
|
|
elif q == 10:
|
|
mon = random.choice(list(poke_data.pokemon_data.keys()))
|
|
actual_type = self.local_poke_data[mon][random.choice(("type1", "type2"))]
|
|
question_type = actual_type
|
|
while question_type in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]] and not a:
|
|
question_type = random.choice(list(poke_data.type_ids.keys()))
|
|
return encode_text(f"{mon} is<LINE>{question_type} type?<DONE>")
|
|
elif q == 11:
|
|
equation = ""
|
|
while "*" not in equation:
|
|
equation = f"{random.randint(0, 9)} {random.choice(['+', '-', '*'])} {random.randint(0, 9)} {random.choice(['+', '-', '*'])} {random.randint(0, 9)} {random.choice(['+', '-', '*'])} {random.randint(0, 9)}"
|
|
result = eval(equation)
|
|
question_result = result
|
|
if not a:
|
|
modifiers = random.sample(range(3), 3)
|
|
for modifier in modifiers:
|
|
question_result = eval(equation[:modifier * 4] + "(" + equation[modifier * 4:(modifier * 4) + 5]
|
|
+ ")" + equation[5 + (modifier * 4):])
|
|
if question_result != result:
|
|
break
|
|
else:
|
|
question_result += random.choice(range(1, 6)) * random.choice((-1, 1))
|
|
|
|
return encode_text(f"{equation}<LINE>= {question_result}?<DONE>")
|
|
elif q == 12:
|
|
route = random.choice((12, 16))
|
|
actual_mon = self.multiworld.get_location(f"Route {route} - Sleeping Pokemon",
|
|
self.player).item.name.split("Static ")[1]
|
|
question_mon = actual_mon
|
|
while question_mon == actual_mon and not a:
|
|
question_mon = random.choice(list(poke_data.pokemon_data.keys()))
|
|
return encode_text(f"{question_mon} was<LINE>sleeping on route<CONT>{route}?<DONE>")
|
|
elif q == 13:
|
|
type1 = random.choice(list(poke_data.type_ids.keys()))
|
|
type2 = random.choice(list(poke_data.type_ids.keys()))
|
|
eff_msgs = ["super effective<CONT>", "no ", "not very<CONT>effective<CONT>", "normal "]
|
|
for matchup in self.type_chart:
|
|
if matchup[0] == type1 and matchup[1] == type2:
|
|
if matchup[2] > 10:
|
|
eff = eff_msgs[0]
|
|
elif matchup[2] == 0:
|
|
eff = eff_msgs[1]
|
|
elif matchup[2] < 10:
|
|
eff = eff_msgs[2]
|
|
else:
|
|
eff = eff_msgs[3]
|
|
break
|
|
else:
|
|
eff = eff_msgs[3]
|
|
if not a:
|
|
eff_msgs.remove(eff)
|
|
eff = random.choice(eff_msgs)
|
|
return encode_text(f"{type1} deals<LINE>{eff}damage to<CONT>{type2} type?<DONE>")
|
|
elif q == 14:
|
|
fossil_level = self.multiworld.get_location("Fossil Level - Trainer Parties",
|
|
self.player).party_data[0]['level']
|
|
if not a:
|
|
fossil_level += random.choice((-5, 5))
|
|
return encode_text(f"Fossil #MON<LINE>revive at level<CONT>{fossil_level}?<DONE>")
|
|
|
|
answers = [random.randint(0, 1) for _ in range(6)]
|
|
|
|
questions = random.sample((range(0, 15)), 6)
|
|
question_texts = []
|
|
for i, question in enumerate(questions):
|
|
question_texts.append(get_quiz(question, answers[i]))
|
|
|
|
for i, quiz in enumerate(["A", "B", "C", "D", "E", "F"]):
|
|
data[rom_addresses[f"Quiz_Answer_{quiz}"]] = int(not answers[i]) << 4 | (i + 1)
|
|
write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"])
|
|
|
|
|
|
def generate_output(self, output_directory: str):
|
|
random = self.multiworld.per_slot_randoms[self.player]
|
|
game_version = self.multiworld.game_version[self.player].current_key
|
|
data = bytes(get_base_rom_bytes(game_version))
|
|
|
|
base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4')
|
|
|
|
data = bytearray(bsdiff4.patch(data, base_patch))
|
|
|
|
basemd5 = hashlib.md5()
|
|
basemd5.update(data)
|
|
|
|
pallet_connections = {entrance: self.multiworld.get_entrance(f"Pallet Town to {entrance}",
|
|
self.player).connected_region.name for
|
|
entrance in ["Player's House 1F", "Oak's Lab",
|
|
"Rival's House"]}
|
|
paths = None
|
|
if pallet_connections["Player's House 1F"] == "Oak's Lab":
|
|
paths = ((0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x20, 5, 0x80, 5, 0xFF))
|
|
elif pallet_connections["Rival's House"] == "Oak's Lab":
|
|
paths = ((0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x10, 3, 0x80, 5, 0xFF))
|
|
if paths:
|
|
write_bytes(data, paths[0], rom_addresses["Path_Pallet_Oak"])
|
|
write_bytes(data, paths[1], rom_addresses["Path_Pallet_Player"])
|
|
if pallet_connections["Rival's House"] == "Player's House 1F":
|
|
write_bytes(data, [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01], rom_addresses["Pallet_Fly_Coords"])
|
|
elif pallet_connections["Oak's Lab"] == "Player's House 1F":
|
|
write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"])
|
|
|
|
for region in self.multiworld.get_regions(self.player):
|
|
for entrance in region.exits:
|
|
if isinstance(entrance, PokemonRBWarp):
|
|
self.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance",
|
|
self.player)
|
|
warp_ids = (entrance.warp_id,) if isinstance(entrance.warp_id, int) else entrance.warp_id
|
|
warp_to_ids = (entrance.target,) if isinstance(entrance.target, int) else entrance.target
|
|
for i, warp_id in enumerate(warp_ids):
|
|
address = rom_addresses[entrance.address]
|
|
if "Elevator" in entrance.parent_region.name:
|
|
address += (2 * warp_id)
|
|
else:
|
|
address += (4 * warp_id)
|
|
while i > len(warp_to_ids) - 1:
|
|
i -= len(warp_to_ids)
|
|
connected_map_name = entrance.connected_region.name.split("-")[0]
|
|
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
|
|
data[address + 1] = map_ids[connected_map_name]
|
|
|
|
if self.multiworld.door_shuffle[self.player] == "simple":
|
|
for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values():
|
|
destination = self.multiworld.get_entrance(entrance, self.player).connected_region.name
|
|
(_, x, y, _, _, map_order_entry) = town_map_coords[destination]
|
|
for map_coord_entry in map_coords_entries:
|
|
data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x
|
|
data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name]
|
|
|
|
if not self.multiworld.key_items_only[self.player]:
|
|
for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
|
|
"Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
|
|
"Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM",
|
|
"Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")):
|
|
item_name = self.multiworld.get_location(gym_leader, self.player).item.name
|
|
if item_name.startswith("TM"):
|
|
try:
|
|
tm = int(item_name[2:4])
|
|
move = poke_data.moves[self.local_tms[tm - 1]]["id"]
|
|
data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
|
|
except KeyError:
|
|
pass
|
|
|
|
def set_trade_mon(address, loc):
|
|
mon = self.multiworld.get_location(loc, self.player).item.name
|
|
data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"]
|
|
self.trade_mons[address] = mon
|
|
|
|
if game_version == "red":
|
|
set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5")
|
|
set_trade_mon("Trade_Spot", "Safari Zone East - Wild Pokemon - 1")
|
|
else:
|
|
set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 7")
|
|
set_trade_mon("Trade_Spot", "Safari Zone East - Wild Pokemon - 7")
|
|
set_trade_mon("Trade_Marcel", "Route 24 - Wild Pokemon - 6")
|
|
set_trade_mon("Trade_Sailor", "Pokemon Mansion 1F - Wild Pokemon - 3")
|
|
set_trade_mon("Trade_Dux", "Route 3 - Wild Pokemon - 2")
|
|
set_trade_mon("Trade_Marc", "Route 23/Cerulean Cave Fishing - Super Rod Pokemon - 1")
|
|
set_trade_mon("Trade_Lola", "Route 10/Celadon Fishing - Super Rod Pokemon - 1")
|
|
set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9")
|
|
set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4")
|
|
|
|
data[rom_addresses['Fly_Location']] = self.fly_map_code
|
|
data[rom_addresses['Map_Fly_Location']] = self.town_map_fly_map_code
|
|
|
|
if self.multiworld.fix_combat_bugs[self.player]:
|
|
data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de)
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"]] = 0xe6 # and a, direct
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"] + 1] = 0b0011111
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Struggle"]] = 0xe6 # and a, direct
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Struggle"] + 1] = 0x3f
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Dig_Fly"]] = 0b10001100
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"]] = 0x20 # jr nz,
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead
|
|
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1
|
|
|
|
if self.multiworld.poke_doll_skip[self.player] == "in_logic":
|
|
data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop
|
|
data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop
|
|
data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop
|
|
|
|
if self.multiworld.bicycle_gate_skips[self.player] == "patched":
|
|
data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop
|
|
data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop
|
|
data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop
|
|
data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop
|
|
|
|
if self.multiworld.door_shuffle[self.player]:
|
|
data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F
|
|
|
|
if self.multiworld.all_elevators_locked[self.player]:
|
|
data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz
|
|
data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz
|
|
|
|
if self.multiworld.tea[self.player].value:
|
|
data[rom_addresses["Option_Tea"]] = 1
|
|
data[rom_addresses["Guard_Drink_List"]] = 0x54
|
|
data[rom_addresses["Guard_Drink_List"] + 1] = 0
|
|
data[rom_addresses["Guard_Drink_List"] + 2] = 0
|
|
write_bytes(data, encode_text("<LINE>Gee, I have the<CONT>worst caffeine<CONT>headache though."
|
|
"<PARA>Oh wait there,<LINE>the road's closed.<DONE>"),
|
|
rom_addresses["Text_Saffron_Gate"])
|
|
|
|
data[rom_addresses["Fossils_Needed_For_Second_Item"]] = (
|
|
self.multiworld.second_fossil_check_condition[self.player].value)
|
|
|
|
data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value)
|
|
|
|
if self.multiworld.extra_key_items[self.player]:
|
|
data[rom_addresses['Option_Extra_Key_Items_A']] = 1
|
|
data[rom_addresses['Option_Extra_Key_Items_B']] = 1
|
|
data[rom_addresses['Option_Extra_Key_Items_C']] = 1
|
|
data[rom_addresses['Option_Extra_Key_Items_D']] = 1
|
|
data[rom_addresses["Option_Split_Card_Key"]] = self.multiworld.split_card_key[self.player].value
|
|
data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55)
|
|
data[rom_addresses["Option_Cerulean_Cave_Badges"]] = self.multiworld.cerulean_cave_badges_condition[self.player].value
|
|
data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = self.multiworld.cerulean_cave_key_items_condition[self.player].total
|
|
write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_badges_condition[self.player].value)), rom_addresses["Text_Cerulean_Cave_Badges"])
|
|
write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_key_items_condition[self.player].total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"])
|
|
data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.multiworld.minimum_steps_between_encounters[self.player].value
|
|
data[rom_addresses['Option_Route23_Badges']] = self.multiworld.victory_road_condition[self.player].value
|
|
data[rom_addresses['Option_Victory_Road_Badges']] = self.multiworld.route_22_gate_condition[self.player].value
|
|
data[rom_addresses['Option_Elite_Four_Pokedex']] = self.multiworld.elite_four_pokedex_condition[self.player].total
|
|
data[rom_addresses['Option_Elite_Four_Key_Items']] = self.multiworld.elite_four_key_items_condition[self.player].total
|
|
data[rom_addresses['Option_Elite_Four_Badges']] = self.multiworld.elite_four_badges_condition[self.player].value
|
|
write_bytes(data, encode_text(str(self.multiworld.elite_four_badges_condition[self.player].value)), rom_addresses["Text_Elite_Four_Badges"])
|
|
write_bytes(data, encode_text(str(self.multiworld.elite_four_key_items_condition[self.player].total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"])
|
|
write_bytes(data, encode_text(str(self.multiworld.elite_four_pokedex_condition[self.player].total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"])
|
|
write_bytes(data, encode_text(str(self.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"])
|
|
|
|
data[rom_addresses['Option_Viridian_Gym_Badges']] = self.multiworld.viridian_gym_condition[self.player].value
|
|
data[rom_addresses['Option_EXP_Modifier']] = self.multiworld.exp_modifier[self.player].value
|
|
if not self.multiworld.require_item_finder[self.player]:
|
|
data[rom_addresses['Option_Itemfinder']] = 0 # nop
|
|
if self.multiworld.extra_strength_boulders[self.player]:
|
|
for i in range(0, 3):
|
|
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
|
|
if self.multiworld.extra_key_items[self.player]:
|
|
for i in range(0, 4):
|
|
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
|
|
if self.multiworld.old_man[self.player] == "open_viridian_city":
|
|
data[rom_addresses['Option_Old_Man']] = 0x11
|
|
data[rom_addresses['Option_Old_Man_Lying']] = 0x15
|
|
data[rom_addresses['Option_Route3_Guard_B']] = self.multiworld.route_3_condition[self.player].value
|
|
if self.multiworld.route_3_condition[self.player] == "open":
|
|
data[rom_addresses['Option_Route3_Guard_A']] = 0x11
|
|
if not self.multiworld.robbed_house_officer[self.player]:
|
|
data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15
|
|
data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11
|
|
if self.multiworld.require_pokedex[self.player]:
|
|
data[rom_addresses["Require_Pokedex_A"]] = 1
|
|
data[rom_addresses["Require_Pokedex_B"]] = 1
|
|
data[rom_addresses["Require_Pokedex_C"]] = 1
|
|
else:
|
|
data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr
|
|
if self.multiworld.dexsanity[self.player]:
|
|
data[rom_addresses["Option_Dexsanity_A"]] = 1
|
|
data[rom_addresses["Option_Dexsanity_B"]] = 1
|
|
if self.multiworld.all_pokemon_seen[self.player]:
|
|
data[rom_addresses["Option_Pokedex_Seen"]] = 1
|
|
money = str(self.multiworld.starting_money[self.player].value).zfill(6)
|
|
data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16)
|
|
data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16)
|
|
data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16)
|
|
data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text(
|
|
str(self.multiworld.viridian_gym_condition[self.player].value))[0]
|
|
data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text(
|
|
str(self.multiworld.victory_road_condition[self.player].value))[0]
|
|
data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text(
|
|
str(self.multiworld.victory_road_condition[self.player].value))[0]
|
|
data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text(
|
|
str(self.multiworld.victory_road_condition[self.player].value))[0]
|
|
data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text(
|
|
str(self.multiworld.victory_road_condition[self.player].value))[0]
|
|
data[rom_addresses["Text_Badges_Needed"]] = encode_text(
|
|
str(self.multiworld.elite_four_badges_condition[self.player].value))[0]
|
|
write_bytes(data, encode_text(
|
|
" ".join(self.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", self.player).item.name.upper().split()[1:])),
|
|
rom_addresses["Text_Magikarp_Salesman"])
|
|
|
|
if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0:
|
|
for hm_move in poke_data.hm_moves:
|
|
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
|
rom_addresses["HM_" + hm_move + "_Badge_a"])
|
|
elif self.extra_badges:
|
|
written_badges = {}
|
|
for hm_move, badge in self.extra_badges.items():
|
|
data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
|
|
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
|
|
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
|
|
"Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge]
|
|
move_text = hm_move
|
|
if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
|
move_text = ", " + move_text
|
|
rom_address = rom_addresses["Badge_Text_" + badge.replace(" ", "_")]
|
|
if badge in written_badges:
|
|
rom_address += len(written_badges[badge])
|
|
move_text = ", " + move_text
|
|
write_bytes(data, encode_text(move_text.upper()), rom_address)
|
|
written_badges[badge] = move_text
|
|
for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
|
if badge not in written_badges:
|
|
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
|
|
|
|
type_loc = rom_addresses["Type_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]]
|
|
data[type_loc + 2] = matchup[2]
|
|
type_loc += 3
|
|
data[type_loc] = 0xFF
|
|
data[type_loc + 1] = 0xFF
|
|
data[type_loc + 2] = 0xFF
|
|
|
|
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):
|
|
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
|
|
|
|
for mon, mon_data in self.local_poke_data.items():
|
|
if mon == "Mew":
|
|
address = rom_addresses["Base_Stats_Mew"]
|
|
else:
|
|
address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1))
|
|
data[address + 1] = self.local_poke_data[mon]["hp"]
|
|
data[address + 2] = self.local_poke_data[mon]["atk"]
|
|
data[address + 3] = self.local_poke_data[mon]["def"]
|
|
data[address + 4] = self.local_poke_data[mon]["spd"]
|
|
data[address + 5] = self.local_poke_data[mon]["spc"]
|
|
data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]]
|
|
data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]]
|
|
data[address + 8] = self.local_poke_data[mon]["catch rate"]
|
|
data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"]
|
|
data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"]
|
|
data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"]
|
|
data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"]
|
|
write_bytes(data, self.local_poke_data[mon]["tms"], address + 20)
|
|
if mon in self.learnsets and self.learnsets[mon]:
|
|
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
|
|
for i, move in enumerate(self.learnsets[mon]):
|
|
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
|
|
|
|
data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value
|
|
data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value
|
|
data[rom_addresses["Option_Aide_Rt15"]] = self.multiworld.oaks_aide_rt_15[self.player].value
|
|
|
|
if self.multiworld.safari_zone_normal_battles[self.player].value == 1:
|
|
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
|
|
|
|
if self.multiworld.reusable_tms[self.player].value:
|
|
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
|
|
|
|
for i in range(1, 10):
|
|
data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value
|
|
|
|
data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value)
|
|
|
|
if self.multiworld.better_shops[self.player]:
|
|
inventory = ["Poke Ball", "Great Ball", "Ultra Ball"]
|
|
if self.multiworld.better_shops[self.player].value == 2:
|
|
inventory.append("Master Ball")
|
|
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote",
|
|
"Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel",
|
|
"Max Repel", "Escape Rope"]
|
|
shop_data = bytearray([0xFE, len(inventory)])
|
|
shop_data += bytearray([item_table[item].id - 172000000 for item in inventory])
|
|
shop_data.append(0xFF)
|
|
for shop in range(1, 11):
|
|
write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"])
|
|
if self.multiworld.stonesanity[self.player]:
|
|
write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"])
|
|
|
|
price = str(self.multiworld.master_ball_price[self.player].value).zfill(6)
|
|
price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)])
|
|
write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird
|
|
|
|
for item in reversed(self.multiworld.precollected_items[self.player]):
|
|
if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255:
|
|
data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1
|
|
|
|
set_mon_palettes(self, random, data)
|
|
|
|
for move_data in self.local_move_data.values():
|
|
if move_data["id"] == 0:
|
|
continue
|
|
address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6)
|
|
write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"],
|
|
poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address)
|
|
|
|
TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms])
|
|
write_bytes(data, TM_IDs, rom_addresses["TM_Moves"])
|
|
|
|
if self.multiworld.randomize_rock_tunnel[self.player]:
|
|
seed = randomize_rock_tunnel(data, random)
|
|
write_bytes(data, encode_text(f"SEED: <LINE>{seed}"), rom_addresses["Text_Rock_Tunnel_Sign"])
|
|
|
|
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
|
|
random.shuffle(mons)
|
|
data[rom_addresses['Title_Mon_First']] = mons.pop()
|
|
for mon in range(0, 16):
|
|
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
|
|
if self.multiworld.game_version[self.player].value:
|
|
mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name
|
|
else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name else
|
|
2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3)
|
|
else:
|
|
mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name
|
|
else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name else
|
|
2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3)
|
|
write_bytes(data, encode_text(self.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed'])
|
|
|
|
slot_name = self.multiworld.player_name[self.player]
|
|
slot_name.replace("@", " ")
|
|
slot_name.replace("<", " ")
|
|
slot_name.replace(">", " ")
|
|
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
|
|
|
|
if self.trainer_name == "choose_in_game":
|
|
data[rom_addresses["Skip_Player_Name"]] = 0
|
|
else:
|
|
write_bytes(data, self.trainer_name, rom_addresses['Player_Name'])
|
|
if self.rival_name == "choose_in_game":
|
|
data[rom_addresses["Skip_Rival_Name"]] = 0
|
|
else:
|
|
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
|
|
|
|
data[0xFF00] = 2 # client compatibility version
|
|
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
|
|
'utf8')[:21]
|
|
rom_name.extend([0] * (21 - len(rom_name)))
|
|
write_bytes(data, rom_name, 0xFFC6)
|
|
write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB)
|
|
write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0)
|
|
|
|
self.finished_level_scaling.wait()
|
|
|
|
write_quizzes(self, data, random)
|
|
|
|
for location in self.multiworld.get_locations(self.player):
|
|
if location.party_data:
|
|
for party in location.party_data:
|
|
if not isinstance(party["party_address"], list):
|
|
addresses = [rom_addresses[party["party_address"]]]
|
|
parties = [party["party"]]
|
|
else:
|
|
addresses = [rom_addresses[address] for address in party["party_address"]]
|
|
parties = party["party"]
|
|
levels = party["level"]
|
|
for address, party in zip(addresses, parties):
|
|
if isinstance(levels, int):
|
|
data[address] = levels
|
|
address += 1
|
|
for mon in party:
|
|
data[address] = poke_data.pokemon_data[mon]["id"]
|
|
address += 1
|
|
else:
|
|
address += 1
|
|
for level, mon in zip(levels, party):
|
|
data[address] = level
|
|
data[address + 1] = poke_data.pokemon_data[mon]["id"]
|
|
address += 2
|
|
assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties"
|
|
continue
|
|
elif location.rom_address is None:
|
|
continue
|
|
if location.item and location.item.player == self.player:
|
|
if location.rom_address:
|
|
rom_address = location.rom_address
|
|
if not isinstance(rom_address, list):
|
|
rom_address = [rom_address]
|
|
for address in rom_address:
|
|
if location.item.name in poke_data.pokemon_data.keys():
|
|
data[address] = poke_data.pokemon_data[location.item.name]["id"]
|
|
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
|
|
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
|
|
else:
|
|
item_id = self.item_name_to_id[location.item.name] - 172000000
|
|
if item_id > 255:
|
|
item_id -= 256
|
|
data[address] = item_id
|
|
if location.level:
|
|
data[location.level_address] = location.level
|
|
|
|
else:
|
|
rom_address = location.rom_address
|
|
if not isinstance(rom_address, list):
|
|
rom_address = [rom_address]
|
|
for address in rom_address:
|
|
data[address] = 0x2C # AP Item
|
|
|
|
outfilepname = f'_P{self.player}'
|
|
outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" \
|
|
if self.multiworld.player_name[self.player] != 'Player%d' % self.player else ''
|
|
rompath = os.path.join(output_directory, f'AP_{self.multiworld.seed_name}{outfilepname}.gb')
|
|
with open(rompath, 'wb') as outfile:
|
|
outfile.write(data)
|
|
if self.multiworld.game_version[self.player].current_key == "red":
|
|
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player,
|
|
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
|
|
else:
|
|
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player,
|
|
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
|
|
|
|
patch.write()
|
|
os.unlink(rompath)
|
|
|
|
|
|
def write_bytes(data, byte_array, address):
|
|
for byte in byte_array:
|
|
data[address] = byte
|
|
address += 1
|
|
|
|
|
|
def get_base_rom_bytes(game_version: str, hash: str="") -> bytes:
|
|
file_name = get_base_rom_path(game_version)
|
|
with open(file_name, "rb") as file:
|
|
base_rom_bytes = bytes(file.read())
|
|
if hash:
|
|
basemd5 = hashlib.md5()
|
|
basemd5.update(base_rom_bytes)
|
|
if hash != basemd5.hexdigest():
|
|
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
|
|
|
|
|
|
def get_base_rom_path(game_version: str) -> str:
|
|
options = Utils.get_options()
|
|
file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"]
|
|
if not os.path.exists(file_name):
|
|
file_name = Utils.user_path(file_name)
|
|
return file_name
|
|
|
|
|
|
class BlueDeltaPatch(APDeltaPatch):
|
|
patch_file_ending = ".apblue"
|
|
hash = "50927e843568814f7ed45ec4f944bd8b"
|
|
game_version = "blue"
|
|
game = "Pokemon Red and Blue"
|
|
result_file_ending = ".gb"
|
|
@classmethod
|
|
def get_source_data(cls) -> bytes:
|
|
return get_base_rom_bytes(cls.game_version, cls.hash)
|
|
|
|
|
|
class RedDeltaPatch(APDeltaPatch):
|
|
patch_file_ending = ".apred"
|
|
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
|
|
game_version = "red"
|
|
game = "Pokemon Red and Blue"
|
|
result_file_ending = ".gb"
|
|
@classmethod
|
|
def get_source_data(cls) -> bytes:
|
|
return get_base_rom_bytes(cls.game_version, cls.hash)
|