diff --git a/BaseClasses.py b/BaseClasses.py index e3bca8fe..32ae1908 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -852,9 +852,8 @@ class Region(object): def can_fill(self, item: Item): inside_dungeon_item = item.locked_dungeon_item - sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)' - if sewer_hack or inside_dungeon_item: - return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player + if inside_dungeon_item: + return self.dungeon.is_dungeon_item(item) and item.player == self.player return True diff --git a/Fill.py b/Fill.py index b77f0d05..91ace4c0 100644 --- a/Fill.py +++ b/Fill.py @@ -130,7 +130,6 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo if world.mode[player] == 'standard' and world.keyshuffle[player] is True: standard_keyshuffle_players.add(player) - # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots if standard_keyshuffle_players: progitempool.sort( diff --git a/LttPAdjuster.py b/LttPAdjuster.py index cb37e9f5..0cf1496b 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -19,7 +19,7 @@ from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, from urllib.parse import urlparse from urllib.request import urlopen -from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings +from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes from Utils import output_path, local_path, open_file @@ -222,7 +222,6 @@ def adjustGUI(): else: messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}") from Utils import persistent_store - from worlds.alttp.Rom import Sprite if isinstance(guiargs.sprite, Sprite): guiargs.sprite = guiargs.sprite.name persistent_store("adjuster", "last_settings_3", guiargs) @@ -412,9 +411,8 @@ def get_rom_frame(parent=None): def RomSelect(): rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")]) - import Patch try: - Patch.get_base_rom_bytes(rom) # throws error on checksum fail + get_base_rom_bytes(rom) # throws error on checksum fail except Exception as e: logging.exception(e) messagebox.showerror(title="Error while reading ROM", message=str(e)) diff --git a/Patch.py b/Patch.py index 962bde44..8bb3f302 100644 --- a/Patch.py +++ b/Patch.py @@ -2,7 +2,6 @@ import bsdiff4 import yaml import os import lzma -import hashlib import threading import concurrent.futures import zipfile @@ -10,37 +9,13 @@ import sys from typing import Tuple, Optional import Utils -from worlds.alttp.Rom import JAP10HASH + current_patch_version = 2 -def get_base_rom_path(file_name: str = "") -> str: - options = Utils.get_options() - if not file_name: - file_name = options["lttp_options"]["rom_file"] - if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) - return file_name - - -def get_base_rom_bytes(file_name: str = "") -> bytes: - from worlds.alttp.Rom import read_rom - base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) - if not base_rom_bytes: - file_name = get_base_rom_path(file_name) - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) - - basemd5 = hashlib.md5() - basemd5.update(base_rom_bytes) - if JAP10HASH != basemd5.hexdigest(): - raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. ' - 'Get the correct game and version, then dump it') - get_base_rom_bytes.base_rom_bytes = base_rom_bytes - return base_rom_bytes - - def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: + from worlds.alttp.Rom import JAP10HASH patch = yaml.dump({"meta": metadata, "patch": patch, "game": "A Link to the Past", @@ -52,6 +27,7 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes: + from worlds.alttp.Rom import get_base_rom_bytes if metadata is None: metadata = {} patch = bsdiff4.diff(get_base_rom_bytes(), rom) @@ -71,6 +47,7 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: + from worlds.alttp.Rom import get_base_rom_bytes data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) if not ignore_version and data["compatible_version"] > current_patch_version: raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") @@ -184,3 +161,11 @@ if __name__ == "__main__": traceback.print_exc() input("Press enter to close.") + + +def read_rom(stream, strip_header=True) -> bytearray: + """Reads rom into bytearray and optionally strips off any smc header""" + buffer = bytearray(stream.read()) + if strip_header and len(buffer) % 0x400 == 0x200: + return buffer[0x200:] + return buffer diff --git a/Utils.py b/Utils.py index fcb44431..7c07da13 100644 --- a/Utils.py +++ b/Utils.py @@ -286,9 +286,9 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]: if adjuster_settings: import pprint - import Patch + from worlds.alttp.Rom import get_base_rom_path adjuster_settings.rom = romfile - adjuster_settings.baserom = Patch.get_base_rom_path() + adjuster_settings.baserom = get_base_rom_path() adjuster_settings.world = None whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", "uw_palettes", "sprite"} diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index acecb67b..494ec01e 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -492,6 +492,7 @@ def set_up_take_anys(world, player): world.initialize_regions() + def create_dynamic_shop_locations(world, player): for shop in world.shops: if shop.region.player == player: @@ -511,35 +512,7 @@ def create_dynamic_shop_locations(world, player): loc.locked = True -def fill_prizes(world, attempts=15): - all_state = world.get_all_state(keys=True) - for player in world.get_game_players("A Link to the Past"): - crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player) - crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player), - world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player), - world.get_location('Misery Mire - Prize', player)] - placed_prizes = {loc.item.name for loc in crystal_locations if loc.item} - unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes] - empty_crystal_locations = [loc for loc in crystal_locations if not loc.item] - for attempt in range(attempts): - try: - prizepool = unplaced_prizes.copy() - prize_locs = empty_crystal_locations.copy() - world.random.shuffle(prize_locs) - fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True) - except FillError as e: - logging.getLogger('').exception("Failed to place dungeon prizes (%s). Will retry %s more times", e, - attempts - attempt) - for location in empty_crystal_locations: - location.item = None - continue - break - else: - raise FillError('Unable to place dungeon prizes') - - def get_pool_core(world, player: int): - progressive = world.progressive[player] shuffle = world.shuffle[player] difficulty = world.difficulty[player] timer = world.timer[player] diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index fdbdb65e..26e4bab8 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,4 +1,5 @@ import typing +import random from Options import Choice, Range, Option, Toggle, DefaultOnToggle @@ -94,6 +95,7 @@ class Palette(Choice): option_negative = 6 option_dizzy = 7 option_sick = 8 + alias_random = 1 class OWPalette(Palette): @@ -136,6 +138,11 @@ class HeartColor(Choice): option_green = 2 option_yellow = 3 + @classmethod + def from_text(cls, text: str) -> Choice: + # remove when this becomes a base Choice feature + if text == "random": + return cls(random.randint(0, 3)) class QuickSwap(DefaultOnToggle): displayname = "L/R Quickswapping" diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index f7af250a..1e227a01 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1,5 +1,8 @@ from __future__ import annotations +import Utils +from Patch import read_rom + JAP10HASH = '03a63945398191337e896e5771f77173' RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d' @@ -31,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen +from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen from worlds.alttp.Items import ItemFactory, item_table from worlds.alttp.EntranceShuffle import door_addresses import Patch @@ -168,14 +171,6 @@ class LocalRom(object): self.write_int32(startaddress + (i * 4), value) -def read_rom(stream) -> bytearray: - "Reads rom into bytearray and strips off any smc header" - buffer = bytearray(stream.read()) - if len(buffer) % 0x400 == 0x200: - buffer = buffer[0x200:] - return buffer - - check_lock = threading.Lock() @@ -546,7 +541,6 @@ class Sprite(): self.valid = False def get_vanilla_sprite_data(self): - from Patch import get_base_rom_path file_name = get_base_rom_path() base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) Sprite.sprite = base_rom_bytes[0x80000:0x87000] @@ -2934,3 +2928,27 @@ hash_alphabet = [ "Lamp", "Hammer", "Shovel", "Flute", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", "Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key" ] + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if JAP10HASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["lttp_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name \ No newline at end of file