diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d54e5c144..540751fe1 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.6"] + python-version: ["3.7"] steps: - uses: actions/checkout@v3 diff --git a/Colors.py b/Colors.py index e27489612..831db6104 100644 --- a/Colors.py +++ b/Colors.py @@ -1,11 +1,11 @@ +from __future__ import annotations import random import re from collections import namedtuple -from typing import Dict, Tuple, List Color = namedtuple('Color', ' R G B') -tunic_colors: Dict[str, Color] = { +tunic_colors: dict[str, Color] = { "Kokiri Green": Color(0x1E, 0x69, 0x1B), "Goron Red": Color(0x64, 0x14, 0x00), "Zora Blue": Color(0x00, 0x3C, 0x64), @@ -40,7 +40,7 @@ } # Inner Core Color Outer Glow Color -NaviColors: Dict[str, Tuple[Color, Color]] = { +NaviColors: dict[str, tuple[Color, Color]] = { "Rainbow": (Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00)), "Gold": (Color(0xFE, 0xCC, 0x3C), Color(0xFE, 0xC0, 0x07)), "White": (Color(0xFF, 0xFF, 0xFF), Color(0x00, 0x00, 0xFF)), @@ -63,7 +63,7 @@ "Phantom Zelda": (Color(0x97, 0x7A, 0x6C), Color(0x6F, 0x46, 0x67)), } -sword_trail_colors: Dict[str, Color] = { +sword_trail_colors: dict[str, Color] = { "Rainbow": Color(0x00, 0x00, 0x00), "White": Color(0xFF, 0xFF, 0xFF), "Red": Color(0xFF, 0x00, 0x00), @@ -77,7 +77,7 @@ "Pink": Color(0xFF, 0x69, 0xB4), } -bombchu_trail_colors: Dict[str, Color] = { +bombchu_trail_colors: dict[str, Color] = { "Rainbow": Color(0x00, 0x00, 0x00), "Red": Color(0xFA, 0x00, 0x00), "Green": Color(0x00, 0xFF, 0x00), @@ -90,7 +90,7 @@ "Pink": Color(0xFF, 0x69, 0xB4), } -boomerang_trail_colors: Dict[str, Color] = { +boomerang_trail_colors: dict[str, Color] = { "Rainbow": Color(0x00, 0x00, 0x00), "Yellow": Color(0xFF, 0xFF, 0x64), "Red": Color(0xFF, 0x00, 0x00), @@ -104,7 +104,7 @@ "Pink": Color(0xFF, 0x69, 0xB4), } -gauntlet_colors: Dict[str, Color] = { +gauntlet_colors: dict[str, Color] = { "Silver": Color(0xFF, 0xFF, 0xFF), "Gold": Color(0xFE, 0xCF, 0x0F), "Black": Color(0x00, 0x00, 0x06), @@ -120,7 +120,7 @@ "Purple": Color(0x80, 0x00, 0x80), } -shield_frame_colors: Dict[str, Color] = { +shield_frame_colors: dict[str, Color] = { "Red": Color(0xD7, 0x00, 0x00), "Green": Color(0x00, 0xFF, 0x00), "Blue": Color(0x00, 0x40, 0xD8), @@ -133,14 +133,14 @@ "Pink": Color(0xFF, 0x69, 0xB4), } -heart_colors: Dict[str, Color] = { +heart_colors: dict[str, Color] = { "Red": Color(0xFF, 0x46, 0x32), "Green": Color(0x46, 0xC8, 0x32), "Blue": Color(0x32, 0x46, 0xFF), "Yellow": Color(0xFF, 0xE0, 0x00), } -magic_colors: Dict[str, Color] = { +magic_colors: dict[str, Color] = { "Green": Color(0x00, 0xC8, 0x00), "Red": Color(0xC8, 0x00, 0x00), "Blue": Color(0x00, 0x30, 0xFF), @@ -152,7 +152,7 @@ # A Button Text Cursor Shop Cursor Save/Death Cursor # Pause Menu A Cursor Pause Menu A Icon A Note -a_button_colors: Dict[str, Tuple[Color, Color, Color, Color, Color, Color, Color]] = { +a_button_colors: dict[str, tuple[Color, Color, Color, Color, Color, Color, Color]] = { "N64 Blue": (Color(0x5A, 0x5A, 0xFF), Color(0x00, 0x50, 0xC8), Color(0x00, 0x50, 0xFF), Color(0x64, 0x64, 0xFF), Color(0x00, 0x32, 0xFF), Color(0x00, 0x64, 0xFF), Color(0x50, 0x96, 0xFF)), "N64 Green": (Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x64, 0x96, 0x64), @@ -188,7 +188,7 @@ } # B Button -b_button_colors: Dict[str, Color] = { +b_button_colors: dict[str, Color] = { "N64 Blue": Color(0x5A, 0x5A, 0xFF), "N64 Green": Color(0x00, 0x96, 0x00), "N64 Red": Color(0xC8, 0x00, 0x00), @@ -208,7 +208,7 @@ } # C Button Pause Menu C Cursor Pause Menu C Icon C Note -c_button_colors: Dict[str, Tuple[Color, Color, Color, Color]] = { +c_button_colors: dict[str, tuple[Color, Color, Color, Color]] = { "N64 Blue": (Color(0x5A, 0x5A, 0xFF), Color(0x00, 0x32, 0xFF), Color(0x00, 0x64, 0xFF), Color(0x50, 0x96, 0xFF)), "N64 Green": (Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00)), "N64 Red": (Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00)), @@ -228,7 +228,7 @@ } # Start Button -start_button_colors: Dict[str, Color] = { +start_button_colors: dict[str, Color] = { "N64 Blue": Color(0x5A, 0x5A, 0xFF), "N64 Green": Color(0x00, 0x96, 0x00), "N64 Red": Color(0xC8, 0x00, 0x00), @@ -247,133 +247,133 @@ "Orange": Color(0xFF, 0x80, 0x00), } -meta_color_choices: List[str] = ["Random Choice", "Completely Random", "Custom Color"] +meta_color_choices: list[str] = ["Random Choice", "Completely Random", "Custom Color"] -def get_tunic_colors() -> List[str]: +def get_tunic_colors() -> list[str]: return list(tunic_colors.keys()) -def get_tunic_color_options() -> List[str]: +def get_tunic_color_options() -> list[str]: return meta_color_choices + ["Rainbow"] + get_tunic_colors() -def get_navi_colors() -> List[str]: +def get_navi_colors() -> list[str]: return list(NaviColors.keys()) -def get_navi_color_options(outer: bool = False) -> List[str]: +def get_navi_color_options(outer: bool = False) -> list[str]: if outer: return ["[Same as Inner]"] + meta_color_choices + get_navi_colors() else: return meta_color_choices + get_navi_colors() -def get_sword_trail_colors() -> List[str]: +def get_sword_trail_colors() -> list[str]: return list(sword_trail_colors.keys()) -def get_sword_trail_color_options(outer: bool = False) -> List[str]: +def get_sword_trail_color_options(outer: bool = False) -> list[str]: if outer: return ["[Same as Inner]"] + meta_color_choices + get_sword_trail_colors() else: return meta_color_choices + get_sword_trail_colors() -def get_bombchu_trail_colors() -> List[str]: +def get_bombchu_trail_colors() -> list[str]: return list(bombchu_trail_colors.keys()) -def get_bombchu_trail_color_options(outer: bool = False) -> List[str]: +def get_bombchu_trail_color_options(outer: bool = False) -> list[str]: if outer: return ["[Same as Inner]"] + meta_color_choices + get_bombchu_trail_colors() else: return meta_color_choices + get_bombchu_trail_colors() -def get_boomerang_trail_colors() -> List[str]: +def get_boomerang_trail_colors() -> list[str]: return list(boomerang_trail_colors.keys()) -def get_boomerang_trail_color_options(outer: bool = False) -> List[str]: +def get_boomerang_trail_color_options(outer: bool = False) -> list[str]: if outer: return ["[Same as Inner]"] + meta_color_choices + get_boomerang_trail_colors() else: return meta_color_choices + get_boomerang_trail_colors() -def get_gauntlet_colors() -> List[str]: +def get_gauntlet_colors() -> list[str]: return list(gauntlet_colors.keys()) -def get_gauntlet_color_options() -> List[str]: +def get_gauntlet_color_options() -> list[str]: return meta_color_choices + get_gauntlet_colors() -def get_shield_frame_colors() -> List[str]: +def get_shield_frame_colors() -> list[str]: return list(shield_frame_colors.keys()) -def get_shield_frame_color_options() -> List[str]: +def get_shield_frame_color_options() -> list[str]: return meta_color_choices + get_shield_frame_colors() -def get_heart_colors() -> List[str]: +def get_heart_colors() -> list[str]: return list(heart_colors.keys()) -def get_heart_color_options() -> List[str]: +def get_heart_color_options() -> list[str]: return meta_color_choices + get_heart_colors() -def get_magic_colors() -> List[str]: +def get_magic_colors() -> list[str]: return list(magic_colors.keys()) -def get_magic_color_options() -> List[str]: +def get_magic_color_options() -> list[str]: return meta_color_choices + get_magic_colors() -def get_a_button_colors() -> List[str]: +def get_a_button_colors() -> list[str]: return list(a_button_colors.keys()) -def get_a_button_color_options() -> List[str]: +def get_a_button_color_options() -> list[str]: return meta_color_choices + get_a_button_colors() -def get_b_button_colors() -> List[str]: +def get_b_button_colors() -> list[str]: return list(b_button_colors.keys()) -def get_b_button_color_options() -> List[str]: +def get_b_button_color_options() -> list[str]: return meta_color_choices + get_b_button_colors() -def get_c_button_colors() -> List[str]: +def get_c_button_colors() -> list[str]: return list(c_button_colors.keys()) -def get_c_button_color_options() -> List[str]: +def get_c_button_color_options() -> list[str]: return meta_color_choices + get_c_button_colors() -def get_start_button_colors() -> List[str]: +def get_start_button_colors() -> list[str]: return list(start_button_colors.keys()) -def get_start_button_color_options() -> List[str]: +def get_start_button_color_options() -> list[str]: return meta_color_choices + get_start_button_colors() -def contrast_ratio(color1: List[int], color2: List[int]) -> float: +def contrast_ratio(color1: list[int], color2: list[int]) -> float: # Based on accessibility standards (WCAG 2.0) lum1 = relative_luminance(color1) lum2 = relative_luminance(color2) return (max(lum1, lum2) + 0.05) / (min(lum1, lum2) + 0.05) -def relative_luminance(color: List[int]) -> float: +def relative_luminance(color: list[int]) -> float: color_ratios = list(map(lum_color_ratio, color)) return color_ratios[0] * 0.299 + color_ratios[1] * 0.587 + color_ratios[2] * 0.114 @@ -386,11 +386,11 @@ def lum_color_ratio(val: float) -> float: return pow((val + 0.055) / 1.055, 2.4) -def generate_random_color() -> List[int]: +def generate_random_color() -> list[int]: return [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)] -def hex_to_color(option: str) -> List[int]: +def hex_to_color(option: str) -> list[int]: if not hasattr(hex_to_color, "regex"): hex_to_color.regex = re.compile(r'^(?:[0-9a-fA-F]{3}){1,2}$') @@ -404,5 +404,5 @@ def hex_to_color(option: str) -> List[int]: return list(int(f'{option[i]}{option[i]}', 16) for i in (0, 1, 2)) -def color_to_hex(color: List[int]) -> str: +def color_to_hex(color: list[int]) -> str: return '#' + ''.join(['{:02X}'.format(c) for c in color]) diff --git a/Cosmetics.py b/Cosmetics.py index 90ec687b6..035d3840d 100644 --- a/Cosmetics.py +++ b/Cosmetics.py @@ -1,9 +1,11 @@ +from __future__ import annotations import json import logging import os import random +from collections.abc import Iterable, Callable from itertools import chain -from typing import TYPE_CHECKING, Dict, List, Tuple, Optional, Union, Iterable, Callable, Any +from typing import TYPE_CHECKING, Optional, Any import Colors import IconManip @@ -19,7 +21,7 @@ from Settings import Settings -def patch_targeting(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_targeting(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Set default targeting option to Hold if settings.default_targeting == 'hold': rom.write_byte(0xB71E6D, 0x01) @@ -27,7 +29,7 @@ def patch_targeting(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbo rom.write_byte(0xB71E6D, 0x00) -def patch_dpad(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_dpad(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Display D-Pad HUD if settings.display_dpad: rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x01) @@ -35,7 +37,7 @@ def patch_dpad(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: D rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x00) -def patch_dpad_info(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_dpad_info(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Display D-Pad HUD in pause menu for either dungeon info or equips if settings.dpad_dungeon_menu: rom.write_byte(symbols['CFG_DPAD_DUNGEON_INFO_ENABLE'], 0x01) @@ -43,7 +45,7 @@ def patch_dpad_info(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbo rom.write_byte(symbols['CFG_DPAD_DUNGEON_INFO_ENABLE'], 0x00) -def patch_music(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_music(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch music if settings.background_music != 'normal' or settings.fanfares != 'normal' or log.src_dict.get('bgm', {}): Music.restore_music(rom) @@ -55,7 +57,7 @@ def patch_music(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: rom.write_byte(0xBE447F, 0x00) -def patch_model_colors(rom: "Rom", color: Optional[List[int]], model_addresses: Tuple[List[int], List[int], List[int]]) -> None: +def patch_model_colors(rom: Rom, color: Optional[list[int]], model_addresses: tuple[list[int], list[int], list[int]]) -> None: main_addresses, dark_addresses, light_addresses = model_addresses if color is None: @@ -76,7 +78,7 @@ def patch_model_colors(rom: "Rom", color: Optional[List[int]], model_addresses: rom.write_bytes(address, lightened_color) -def patch_tunic_icon(rom: "Rom", tunic: str, color: Optional[List[int]], rainbow: bool = False) -> None: +def patch_tunic_icon(rom: Rom, tunic: str, color: Optional[list[int]], rainbow: bool = False) -> None: # patch tunic icon colors icon_locations = { 'Kokiri Tunic': 0x007FE000, @@ -92,7 +94,7 @@ def patch_tunic_icon(rom: "Rom", tunic: str, color: Optional[List[int]], rainbow rom.write_bytes(icon_locations[tunic], tunic_icon) -def patch_tunic_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_tunic_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Need to check for the existence of the CFG_TUNIC_COLORS symbol. # This was added with rainbow tunic but custom tunic colors should still support older patch versions. tunic_address = symbols.get('CFG_TUNIC_COLORS', 0x00B6DA38) # Use new tunic color ROM address. Fall back to vanilla tunic color ROM address. @@ -162,7 +164,7 @@ def patch_tunic_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', sy log.errors.append(rainbow_error) -def patch_navi_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_navi_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch navi colors navi = [ # colors for Navi @@ -267,7 +269,7 @@ def patch_navi_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', sym log.errors.append(rainbow_error) -def patch_sword_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_sword_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch sword trail duration rom.write_byte(0x00BEFF8C, settings.sword_trail_duration) @@ -371,7 +373,7 @@ def patch_sword_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', sy log.errors.append(rainbow_error) -def patch_bombchu_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_bombchu_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch bombchu trail colors bombchu_trails = [ ('Bombchu Trail', 'bombchu_trail_color', Colors.get_bombchu_trail_colors(), Colors.bombchu_trail_colors, @@ -382,7 +384,7 @@ def patch_bombchu_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', patch_trails(rom, settings, log, bombchu_trails) -def patch_boomerang_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_boomerang_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch boomerang trail colors boomerang_trails = [ ('Boomerang Trail', 'boomerang_trail_color', Colors.get_boomerang_trail_colors(), Colors.boomerang_trail_colors, @@ -393,7 +395,7 @@ def patch_boomerang_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog' patch_trails(rom, settings, log, boomerang_trails) -def patch_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', trails) -> None: +def patch_trails(rom: Rom, settings: Settings, log: CosmeticsLog, trails) -> None: for trail_name, trail_setting, trail_color_list, trail_color_dict, trail_symbols in trails: color_inner_symbol, color_outer_symbol, rainbow_inner_symbol, rainbow_outer_symbol = trail_symbols option_inner = getattr(settings, f'{trail_setting}_inner') @@ -472,7 +474,7 @@ def patch_trails(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', trails) del log.misc_colors[trail_name]['colors'] -def patch_gauntlet_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_gauntlet_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch gauntlet colors gauntlets = [ ('Silver Gauntlets', 'silver_gauntlets_color', 0x00B6DA44, @@ -513,7 +515,7 @@ def patch_gauntlet_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', }) -def patch_shield_frame_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_shield_frame_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch shield frame colors shield_frames = [ ('Mirror Shield Frame', 'mirror_shield_frame_color', @@ -555,7 +557,7 @@ def patch_shield_frame_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsL }) -def patch_heart_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_heart_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch heart colors hearts = [ ('Heart Color', 'heart_color', symbols['CFG_HEART_COLOR'], 0xBB0994, @@ -605,7 +607,7 @@ def patch_heart_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', sy }) -def patch_magic_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_magic_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # patch magic colors magic = [ ('Magic Meter Color', 'magic_color', symbols["CFG_MAGIC_COLOR"], @@ -647,7 +649,7 @@ def patch_magic_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', sy }) -def patch_button_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_button_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: buttons = [ ('A Button Color', 'a_button_color', Colors.a_button_colors, [('A Button Color', symbols['CFG_A_BUTTON_COLOR'], @@ -736,7 +738,7 @@ def patch_button_colors(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', s log_dict['colors'][patch] = Colors.color_to_hex(colors[patch]) -def patch_sfx(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_sfx(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Configurable Sound Effects sfx_config = [ ('sfx_navi_overworld', Sounds.SoundHooks.NAVI_OVERWORLD), @@ -802,7 +804,7 @@ def patch_sfx(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Di rom.write_int16(symbols['GET_ITEM_SEQ_ID'], sound_id) -def patch_instrument(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_instrument(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Player Instrument instruments = { #'none': 0x00, @@ -828,7 +830,7 @@ def patch_instrument(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symb log.sfx['Ocarina'] = ocarina_options[choice] -def read_default_voice_data(rom: "Rom") -> Dict[str, Dict[str, int]]: +def read_default_voice_data(rom: Rom) -> dict[str, dict[str, int]]: audiobank = 0xD390 audiotable = 0x79470 soundbank = audiobank + rom.read_int32(audiobank + 0x4) @@ -849,7 +851,7 @@ def read_default_voice_data(rom: "Rom") -> Dict[str, Dict[str, int]]: return soundbank_entries -def patch_silent_voice(rom: "Rom", sfxidlist: Iterable[int], soundbank_entries: Dict[str, Dict[str, int]], log: 'CosmeticsLog') -> None: +def patch_silent_voice(rom: Rom, sfxidlist: Iterable[int], soundbank_entries: dict[str, dict[str, int]], log: CosmeticsLog) -> None: binsfxfilename = os.path.join(data_path('Voices'), 'SilentVoiceSFX.bin') if not os.path.isfile(binsfxfilename): log.errors.append(f"Could not find silent voice sfx at {binsfxfilename}. Skipping voice patching") @@ -867,7 +869,7 @@ def patch_silent_voice(rom: "Rom", sfxidlist: Iterable[int], soundbank_entries: rom.write_bytes(soundbank_entries[sfxid]["romoffset"], injectme) -def apply_voice_patch(rom: "Rom", voice_path: str, soundbank_entries: Dict[str, Dict[str, int]]) -> None: +def apply_voice_patch(rom: Rom, voice_path: str, soundbank_entries: dict[str, dict[str, int]]) -> None: if not os.path.exists(voice_path): return @@ -883,7 +885,7 @@ def apply_voice_patch(rom: "Rom", voice_path: str, soundbank_entries: Dict[str, rom.write_bytes(soundbank_entries[sfxid]["romoffset"], binsfx) -def patch_voices(rom: "Rom", settings: "Settings", log: 'CosmeticsLog', symbols: Dict[str, int]) -> None: +def patch_voices(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None: # Reset the audiotable back to default to prepare patching voices and read data rom.write_bytes(0x00079470, rom.original.read_bytes(0x00079470, 0x460AD0)) @@ -946,13 +948,13 @@ def patch_music_changes(rom, settings, log, symbols): log.slowdown_music_when_lowhp = settings.slowdown_music_when_lowhp -legacy_cosmetic_data_headers: List[int] = [ +legacy_cosmetic_data_headers: list[int] = [ 0x03481000, 0x03480810, ] -patch_sets: Dict[int, Dict[str, Any]] = {} -global_patch_sets: List[Callable[["Rom", "Settings", 'CosmeticsLog', Dict[str, int]], None]] = [ +patch_sets: dict[int, dict[str, Any]] = {} +global_patch_sets: list[Callable[[Rom, Settings, CosmeticsLog, dict[str, int]], None]] = [ patch_targeting, patch_music, patch_tunic_colors, @@ -1100,7 +1102,7 @@ def patch_music_changes(rom, settings, log, symbols): } -def patch_cosmetics(settings: "Settings", rom: "Rom") -> 'CosmeticsLog': +def patch_cosmetics(settings: Settings, rom: Rom) -> CosmeticsLog: # re-seed for aesthetic effects. They shouldn't be affected by the generation seed random.seed() settings.resolve_random_settings(cosmetic=True) @@ -1156,18 +1158,18 @@ def patch_cosmetics(settings: "Settings", rom: "Rom") -> 'CosmeticsLog': class CosmeticsLog: - def __init__(self, settings: "Settings") -> None: - self.settings: "Settings" = settings + def __init__(self, settings: Settings) -> None: + self.settings: Settings = settings - self.equipment_colors: Dict[str, dict] = {} - self.ui_colors: Dict[str, dict] = {} - self.misc_colors: Dict[str, dict] = {} - self.sfx: Dict[str, str] = {} - self.bgm: Dict[str, str] = {} - self.bgm_groups: Dict[str, Union[list, dict]] = {} + self.equipment_colors: dict[str, dict] = {} + self.ui_colors: dict[str, dict] = {} + self.misc_colors: dict[str, dict] = {} + self.sfx: dict[str, str] = {} + self.bgm: dict[str, str] = {} + self.bgm_groups: dict[str, list | dict] = {} self.src_dict: dict = {} - self.errors: List[str] = [] + self.errors: list[str] = [] if self.settings.enable_cosmetic_file: if self.settings.cosmetic_file: diff --git a/Dungeon.py b/Dungeon.py index e184621e8..dbefa67cb 100644 --- a/Dungeon.py +++ b/Dungeon.py @@ -1,30 +1,30 @@ -from typing import TYPE_CHECKING, List - -from Hints import HintArea +from __future__ import annotations +from typing import TYPE_CHECKING if TYPE_CHECKING: + from Hints import HintArea from Item import Item from Region import Region from World import World class Dungeon: - def __init__(self, world: "World", name: str, hint: HintArea) -> None: - self.world: "World" = world + def __init__(self, world: World, name: str, hint: HintArea) -> None: + self.world: World = world self.name: str = name self.hint: HintArea = hint - self.regions: "List[Region]" = [] - self.boss_key: "List[Item]" = [] - self.small_keys: "List[Item]" = [] - self.dungeon_items: "List[Item]" = [] - self.silver_rupees: "List[Item]" = [] + self.regions: list[Region] = [] + self.boss_key: list[Item] = [] + self.small_keys: list[Item] = [] + self.dungeon_items: list[Item] = [] + self.silver_rupees: list[Item] = [] for region in world.regions: if region.dungeon == self.name: region.dungeon = self self.regions.append(region) - def copy(self, new_world: "World") -> 'Dungeon': + def copy(self, new_world: World) -> Dungeon: new_dungeon = Dungeon(new_world, self.name, self.hint) new_dungeon.boss_key = [item.copy(new_world) for item in self.boss_key] @@ -35,17 +35,17 @@ def copy(self, new_world: "World") -> 'Dungeon': return new_dungeon @property - def keys(self) -> "List[Item]": + def keys(self) -> list[Item]: return self.small_keys + self.boss_key @property - def all_items(self) -> "List[Item]": + def all_items(self) -> list[Item]: return self.dungeon_items + self.keys + self.silver_rupees def item_name(self, text: str) -> str: return f"{text} ({self.name})" - def is_dungeon_item(self, item: "Item") -> bool: + def is_dungeon_item(self, item: Item) -> bool: return item.name in [dungeon_item.name for dungeon_item in self.all_items] def __str__(self) -> str: diff --git a/Entrance.py b/Entrance.py index ad19d2ea9..189002c8a 100644 --- a/Entrance.py +++ b/Entrance.py @@ -1,32 +1,32 @@ -from typing import TYPE_CHECKING, List, Optional, Dict, Any - -from RulesCommon import AccessRule +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Any if TYPE_CHECKING: from Region import Region + from RulesCommon import AccessRule from World import World class Entrance: - def __init__(self, name: str = '', parent: "Optional[Region]" = None) -> None: + def __init__(self, name: str = '', parent: Optional[Region] = None) -> None: self.name: str = name - self.parent_region: "Optional[Region]" = parent - self.world: "Optional[World]" = parent.world if parent is not None else None - self.connected_region: "Optional[Region]" = None + self.parent_region: Optional[Region] = parent + self.world: Optional[World] = parent.world if parent is not None else None + self.connected_region: Optional[Region] = None self.access_rule: AccessRule = lambda state, **kwargs: True - self.access_rules: List[AccessRule] = [] - self.reverse: 'Optional[Entrance]' = None - self.replaces: 'Optional[Entrance]' = None - self.assumed: 'Optional[Entrance]' = None + self.access_rules: list[AccessRule] = [] + self.reverse: Optional[Entrance] = None + self.replaces: Optional[Entrance] = None + self.assumed: Optional[Entrance] = None self.type: Optional[str] = None self.shuffled: bool = False - self.data: Optional[Dict[str, Any]] = None + self.data: Optional[dict[str, Any]] = None self.primary: bool = False self.always: bool = False self.never: bool = False self.rule_string: Optional[str] = None - def copy(self, new_region: "Region") -> 'Entrance': + def copy(self, new_region: Region) -> Entrance: new_entrance = Entrance(self.name, new_region) new_entrance.connected_region = self.connected_region.name # TODO: Revamp World/Region copying such that this is not a type error. new_entrance.access_rule = self.access_rule @@ -57,11 +57,11 @@ def set_rule(self, lambda_rule: AccessRule) -> None: self.access_rule = lambda_rule self.access_rules = [lambda_rule] - def connect(self, region: "Region") -> None: + def connect(self, region: Region) -> None: self.connected_region = region region.entrances.append(self) - def disconnect(self) -> "Optional[Region]": + def disconnect(self) -> Optional[Region]: if self.connected_region is None: raise Exception(f"`disconnect()` called without a valid `connected_region` for entrance {self.name}.") self.connected_region.entrances.remove(self) @@ -69,11 +69,11 @@ def disconnect(self) -> "Optional[Region]": self.connected_region = None return previously_connected - def bind_two_way(self, other_entrance: 'Entrance') -> None: + def bind_two_way(self, other_entrance: Entrance) -> None: self.reverse = other_entrance other_entrance.reverse = self - def get_new_target(self) -> 'Entrance': + def get_new_target(self) -> Entrance: if self.world is None: raise Exception(f"`get_new_target()` called without a valid `world` for entrance {self.name}.") if self.connected_region is None: @@ -85,7 +85,7 @@ def get_new_target(self) -> 'Entrance': root.exits.append(target_entrance) return target_entrance - def assume_reachable(self) -> 'Entrance': + def assume_reachable(self) -> Entrance: if self.assumed is None: self.assumed = self.get_new_target() self.disconnect() diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 882d5ba62..6f153a517 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,8 +1,10 @@ +from __future__ import annotations import random import logging from collections import OrderedDict +from collections.abc import Iterable, Container from itertools import chain -from typing import TYPE_CHECKING, List, Iterable, Container, Tuple, Dict, Optional +from typing import TYPE_CHECKING, Optional from Fill import ShuffleError from Search import Search @@ -20,7 +22,7 @@ from World import World -def set_all_entrances_data(world: "World") -> None: +def set_all_entrances_data(world: World) -> None: for type, forward_entry, *return_entry in entrance_shuffle_table: forward_entrance = world.get_entrance(forward_entry[0]) forward_entrance.data = forward_entry[1] @@ -38,7 +40,7 @@ def set_all_entrances_data(world: "World") -> None: return_entrance.data['index'] = 0x7FFF -def assume_entrance_pool(entrance_pool: "List[Entrance]") -> "List[Entrance]": +def assume_entrance_pool(entrance_pool: list[Entrance]) -> list[Entrance]: assumed_pool = [] for entrance in entrance_pool: assumed_forward = entrance.assume_reachable() @@ -53,8 +55,8 @@ def assume_entrance_pool(entrance_pool: "List[Entrance]") -> "List[Entrance]": return assumed_pool -def build_one_way_targets(world: "World", types_to_include: Iterable[str], exclude: Container[str] = (), target_region_names: Container[str] = ()) -> "List[Entrance]": - one_way_entrances: "List[Entrance]" = [] +def build_one_way_targets(world: World, types_to_include: Iterable[str], exclude: Container[str] = (), target_region_names: Container[str] = ()) -> list[Entrance]: + one_way_entrances: list[Entrance] = [] for pool_type in types_to_include: one_way_entrances += world.get_shufflable_entrances(type=pool_type) valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances)) @@ -416,7 +418,7 @@ class EntranceShuffleError(ShuffleError): # Set entrances of all worlds, first initializing them to their default regions, then potentially shuffling part of them -def set_entrances(worlds: "List[World]", savewarps_to_connect: "List[Tuple[Entrance, str]]") -> None: +def set_entrances(worlds: list[World], savewarps_to_connect: list[tuple[Entrance, str]]) -> None: for world in worlds: world.initialize_entrances() @@ -436,7 +438,7 @@ def set_entrances(worlds: "List[World]", savewarps_to_connect: "List[Tuple[Entra # Shuffles entrances that need to be shuffled in all worlds -def shuffle_random_entrances(worlds: "List[World]") -> None: +def shuffle_random_entrances(worlds: list[World]) -> None: # Store all locations reachable before shuffling to differentiate which locations were already unreachable from those we made unreachable complete_itempool = [item for world in worlds for item in world.get_itempool_with_dungeon_items()] max_search = Search.max_explore([world.state for world in worlds], complete_itempool) @@ -688,10 +690,10 @@ def shuffle_random_entrances(worlds: "List[World]") -> None: raise EntranceShuffleError('Worlds are not valid after shuffling entrances, Reason: %s' % error) -def shuffle_one_way_priority_entrances(worlds: "List[World]", world: "World", one_way_priorities: Dict[str, Tuple[List[str], List[str]]], - one_way_entrance_pools: "Dict[str, List[Entrance]]", one_way_target_entrance_pools: "Dict[str, List[Entrance]]", - locations_to_ensure_reachable: "Iterable[Location]", complete_itempool: "List[Item]", - retry_count: int = 2) -> "List[Tuple[Entrance, Entrance]]": +def shuffle_one_way_priority_entrances(worlds: list[World], world: World, one_way_priorities: dict[str, tuple[list[str], list[str]]], + one_way_entrance_pools: dict[str, list[Entrance]], one_way_target_entrance_pools: dict[str, list[Entrance]], + locations_to_ensure_reachable: Iterable[Location], complete_itempool: list[Item], + retry_count: int = 2) -> list[tuple[Entrance, Entrance]]: while retry_count: retry_count -= 1 rollbacks = [] @@ -719,9 +721,9 @@ def shuffle_one_way_priority_entrances(worlds: "List[World]", world: "World", on # Shuffle all entrances within a provided pool -def shuffle_entrance_pool(world: "World", worlds: "List[World]", entrance_pool: "List[Entrance]", target_entrances: "List[Entrance]", - locations_to_ensure_reachable: "Iterable[Location]", check_all: bool = False, retry_count: int = 20, - placed_one_way_entrances: "Optional[List[Tuple[Entrance, Entrance]]]" = None) -> "List[Tuple[Entrance, Entrance]]": +def shuffle_entrance_pool(world: World, worlds: list[World], entrance_pool: list[Entrance], target_entrances: list[Entrance], + locations_to_ensure_reachable: Iterable[Location], check_all: bool = False, retry_count: int = 20, + placed_one_way_entrances: Optional[list[tuple[Entrance, Entrance]]] = None) -> list[tuple[Entrance, Entrance]]: if placed_one_way_entrances is None: placed_one_way_entrances = [] # Split entrances between those that have requirements (restrictive) and those that do not (soft). These are primarily age or time of day requirements. @@ -764,7 +766,7 @@ def shuffle_entrance_pool(world: "World", worlds: "List[World]", entrance_pool: # Split entrances based on their requirements to figure out how each entrance should be handled when shuffling them -def split_entrances_by_requirements(worlds: "List[World]", entrances_to_split: "List[Entrance]", assumed_entrances: "List[Entrance]") -> "Tuple[List[Entrance], List[Entrance]]": +def split_entrances_by_requirements(worlds: list[World], entrances_to_split: list[Entrance], assumed_entrances: list[Entrance]) -> tuple[list[Entrance], list[Entrance]]: # First, disconnect all root assumed entrances and save which regions they were originally connected to, so we can reconnect them later original_connected_regions = {} entrances_to_disconnect = set(assumed_entrances).union(entrance.reverse for entrance in assumed_entrances if entrance.reverse) @@ -798,8 +800,8 @@ def split_entrances_by_requirements(worlds: "List[World]", entrances_to_split: " return restrictive_entrances, soft_entrances -def replace_entrance(worlds: "List[World]", entrance: "Entrance", target: "Entrance", rollbacks: "List[Tuple[Entrance, Entrance]]", - locations_to_ensure_reachable: "Iterable[Location]", itempool: "List[Item]", placed_one_way_entrances: "Optional[List[Tuple[Entrance, Entrance]]]" = None) -> bool: +def replace_entrance(worlds: list[World], entrance: Entrance, target: Entrance, rollbacks: list[tuple[Entrance, Entrance]], + locations_to_ensure_reachable: Iterable[Location], itempool: list[Item], placed_one_way_entrances: Optional[list[tuple[Entrance, Entrance]]] = None) -> bool: if placed_one_way_entrances is None: placed_one_way_entrances = [] try: @@ -820,9 +822,9 @@ def replace_entrance(worlds: "List[World]", entrance: "Entrance", target: "Entra # Connect one random entrance from entrance pools to one random target in the respective target pool. # Entrance chosen will have one of the allowed types. # Target chosen will lead to one of the allowed regions. -def place_one_way_priority_entrance(worlds: "List[World]", world: "World", priority_name: str, allowed_regions: Container[str], allowed_types: Iterable[str], - rollbacks: "List[Tuple[Entrance, Entrance]]", locations_to_ensure_reachable: "Iterable[Location]", complete_itempool: "List[Item]", - one_way_entrance_pools: "Dict[str, List[Entrance]]", one_way_target_entrance_pools: "Dict[str, List[Entrance]]") -> None: +def place_one_way_priority_entrance(worlds: list[World], world: World, priority_name: str, allowed_regions: Container[str], allowed_types: Iterable[str], + rollbacks: list[tuple[Entrance, Entrance]], locations_to_ensure_reachable: Iterable[Location], complete_itempool: list[Item], + one_way_entrance_pools: dict[str, list[Entrance]], one_way_target_entrance_pools: dict[str, list[Entrance]]) -> None: # Combine the entrances for allowed types in one list. # Shuffle this list. # Pick the first one not already set, not adult spawn, that has a valid target entrance. @@ -851,8 +853,8 @@ def place_one_way_priority_entrance(worlds: "List[World]", world: "World", prior # Shuffle entrances by placing them instead of entrances in the provided target entrances list # While shuffling entrances, the algorithm will ensure worlds are still valid based on multiple criterias -def shuffle_entrances(worlds: "List[World]", entrances: "List[Entrance]", target_entrances: "List[Entrance]", rollbacks: "List[Tuple[Entrance, Entrance]]", - locations_to_ensure_reachable: "Iterable[Location]" = (), placed_one_way_entrances: "Optional[List[Tuple[Entrance, Entrance]]]" = None) -> None: +def shuffle_entrances(worlds: list[World], entrances: list[Entrance], target_entrances: list[Entrance], rollbacks: list[tuple[Entrance, Entrance]], + locations_to_ensure_reachable: Iterable[Location] = (), placed_one_way_entrances: Optional[list[tuple[Entrance, Entrance]]] = None) -> None: if placed_one_way_entrances is None: placed_one_way_entrances = [] # Retrieve all items in the itempool, all worlds included @@ -878,8 +880,8 @@ def shuffle_entrances(worlds: "List[World]", entrances: "List[Entrance]", target # Check and validate that an entrance is compatible to replace a specific target -def check_entrances_compatibility(entrance: "Entrance", target: "Entrance", rollbacks: "List[Tuple[Entrance, Entrance]]" = (), - placed_one_way_entrances: "Optional[List[Tuple[Entrance, Entrance]]]" = None) -> None: +def check_entrances_compatibility(entrance: Entrance, target: Entrance, rollbacks: list[tuple[Entrance, Entrance]] = (), + placed_one_way_entrances: Optional[list[tuple[Entrance, Entrance]]] = None) -> None: if placed_one_way_entrances is None: placed_one_way_entrances = [] # An entrance shouldn't be connected to its own scene, so we fail in that situation @@ -904,8 +906,8 @@ def check_entrances_compatibility(entrance: "Entrance", target: "Entrance", roll # Validate the provided worlds' structures, raising an error if it's not valid based on our criterias -def validate_world(world: "World", worlds: "List[World]", entrance_placed: "Optional[Entrance]", locations_to_ensure_reachable: "Iterable[Location]", - itempool: "List[Item]", placed_one_way_entrances: "Optional[List[Tuple[Entrance, Entrance]]]" = None) -> None: +def validate_world(world: World, worlds: list[World], entrance_placed: Optional[Entrance], locations_to_ensure_reachable: Iterable[Location], + itempool: list[Item], placed_one_way_entrances: Optional[list[tuple[Entrance, Entrance]]] = None) -> None: if placed_one_way_entrances is None: placed_one_way_entrances = [] # For various reasons, we don't want the player to end up through certain entrances as the wrong age @@ -1024,7 +1026,7 @@ def validate_world(world: "World", worlds: "List[World]", entrance_placed: "Opti # Returns whether or not we can affirm the entrance can never be accessed as the given age -def entrance_unreachable_as(entrance: "Entrance", age: str, already_checked: "Optional[List[Entrance]]" = None) -> bool: +def entrance_unreachable_as(entrance: Entrance, age: str, already_checked: Optional[list[Entrance]] = None) -> bool: if already_checked is None: already_checked = [] @@ -1053,7 +1055,7 @@ def entrance_unreachable_as(entrance: "Entrance", age: str, already_checked: "Op # Returns whether two entrances are in the same hint area -def same_hint_area(first: HintArea, second: HintArea) -> bool: +def same_hint_area(first: Entrance, second: Entrance) -> bool: try: return HintArea.at(first) == HintArea.at(second) except HintAreaNotFound: @@ -1061,7 +1063,7 @@ def same_hint_area(first: HintArea, second: HintArea) -> bool: # Shorthand function to find an entrance with the requested name leading to a specific region -def get_entrance_replacing(region: Region, entrance_name: str) -> "Optional[Entrance]": +def get_entrance_replacing(region: Region, entrance_name: str) -> Optional[Entrance]: original_entrance = region.world.get_entrance(entrance_name) if not original_entrance.shuffled: @@ -1076,7 +1078,7 @@ def get_entrance_replacing(region: Region, entrance_name: str) -> "Optional[Entr # Change connections between an entrance and a target assumed entrance, in order to test the connections afterwards if necessary -def change_connections(entrance: "Entrance", target_entrance: "Entrance") -> None: +def change_connections(entrance: Entrance, target_entrance: Entrance) -> None: entrance.connect(target_entrance.disconnect()) entrance.replaces = target_entrance.replaces if entrance.reverse: @@ -1085,7 +1087,7 @@ def change_connections(entrance: "Entrance", target_entrance: "Entrance") -> Non # Restore connections between an entrance and a target assumed entrance -def restore_connections(entrance: "Entrance", target_entrance: "Entrance") -> None: +def restore_connections(entrance: Entrance, target_entrance: Entrance) -> None: target_entrance.connect(entrance.disconnect()) entrance.replaces = None if entrance.reverse: @@ -1094,7 +1096,7 @@ def restore_connections(entrance: "Entrance", target_entrance: "Entrance") -> No # Confirm the replacement of a target entrance by a new entrance, logging the new connections and completely deleting the target entrances -def confirm_replacement(entrance: "Entrance", target_entrance: "Entrance") -> None: +def confirm_replacement(entrance: Entrance, target_entrance: Entrance) -> None: delete_target_entrance(target_entrance) logging.getLogger('').debug('Connected %s To %s [World %d]', entrance, entrance.connected_region, entrance.world.id) if entrance.reverse: @@ -1104,7 +1106,7 @@ def confirm_replacement(entrance: "Entrance", target_entrance: "Entrance") -> No # Delete an assumed target entrance, by disconnecting it if needed and removing it from its parent region -def delete_target_entrance(target_entrance: "Entrance") -> None: +def delete_target_entrance(target_entrance: Entrance) -> None: if target_entrance.connected_region is not None: target_entrance.disconnect() if target_entrance.parent_region is not None: diff --git a/Fill.py b/Fill.py index ff69e0580..1ae4b07c8 100644 --- a/Fill.py +++ b/Fill.py @@ -1,6 +1,7 @@ +from __future__ import annotations import random import logging -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional from Hints import HintArea from Item import Item, ItemFactory, ItemInfo @@ -26,7 +27,7 @@ class FillError(ShuffleError): # Places all items into the world -def distribute_items_restrictive(worlds: "List[World]", fill_locations: Optional[List[Location]] = None) -> None: +def distribute_items_restrictive(worlds: list[World], fill_locations: Optional[list[Location]] = None) -> None: if worlds[0].settings.shuffle_song_items == 'song': song_location_names = location_groups['Song'] elif worlds[0].settings.shuffle_song_items == 'dungeon': @@ -237,7 +238,7 @@ def distribute_items_restrictive(worlds: "List[World]", fill_locations: Optional # Places restricted dungeon items into the worlds. To ensure there is room for them. # they are placed first, so it will assume all other items are reachable -def fill_dungeons_restrictive(worlds: "List[World]", search: Search, shuffled_locations: List[Location], dungeon_items: List[Item], itempool: List[Item]) -> None: +def fill_dungeons_restrictive(worlds: list[World], search: Search, shuffled_locations: list[Location], dungeon_items: list[Item], itempool: list[Item]) -> None: # List of states with all non-key items base_search = search.copy() base_search.collect_all(itempool) @@ -259,7 +260,7 @@ def fill_dungeons_restrictive(worlds: "List[World]", search: Search, shuffled_lo # Places items into dungeon locations. This is used when there should be exactly # one progression item per dungeon. This should be run before all the progression # items are places to ensure there is space to place them. -def fill_dungeon_unique_item(worlds: "List[World]", search: Search, fill_locations: List[Location], itempool: List[Item]) -> None: +def fill_dungeon_unique_item(worlds: list[World], search: Search, fill_locations: list[Location], itempool: list[Item]) -> None: # We should make sure that we don't count event items, shop items, # token items, or dungeon items as a major item. itempool at this # point should only be able to have tokens of those restrictions @@ -333,8 +334,8 @@ def fill_dungeon_unique_item(worlds: "List[World]", search: Search, fill_locatio # Places items restricting placement to the recipient player's own world -def fill_ownworld_restrictive(worlds: "List[World]", search: Search, locations: List[Location], ownpool: List[Item], - itempool: List[Item], description: str = "Unknown", attempts: int = 15) -> None: +def fill_ownworld_restrictive(worlds: list[World], search: Search, locations: list[Location], ownpool: list[Item], + itempool: list[Item], description: str = "Unknown", attempts: int = 15) -> None: # look for preplaced items placed_prizes = [loc.item.name for loc in locations if loc.item is not None] unplaced_prizes = [item for item in ownpool if item.name not in placed_prizes] @@ -390,9 +391,9 @@ def fill_ownworld_restrictive(worlds: "List[World]", search: Search, locations: # every item. Raises an error if specified count of items are not placed. # # This function will modify the location and itempool arguments. placed items and -# filled locations will be removed. If this returns and error, then the state of +# filled locations will be removed. If this returns an error, then the state of # those two lists cannot be guaranteed. -def fill_restrictive(worlds: "List[World]", base_search: Search, locations: List[Location], itempool: List[Item], count: int = -1) -> None: +def fill_restrictive(worlds: list[World], base_search: Search, locations: list[Location], itempool: list[Item], count: int = -1) -> None: unplaced_items = [] # don't run over this search, just keep it as an item collection @@ -507,7 +508,7 @@ def fill_restrictive(worlds: "List[World]", base_search: Search, locations: List # This places items in the itempool into the locations # It does not check for reachability, only that the item is # allowed in the location -def fill_restrictive_fast(worlds: "List[World]", locations: List[Location], itempool: List[Item]) -> None: +def fill_restrictive_fast(worlds: list[World], locations: list[Location], itempool: list[Item]) -> None: while itempool and locations: item_to_place = itempool.pop() random.shuffle(locations) @@ -535,7 +536,7 @@ def fill_restrictive_fast(worlds: "List[World]", locations: List[Location], item # this places item in item_pool completely randomly into # fill_locations. There is no checks for validity since # there should be none for these remaining items -def fast_fill(locations: List[Location], itempool: List[Item]) -> None: +def fast_fill(locations: list[Location], itempool: list[Item]) -> None: random.shuffle(locations) while itempool and locations: spot_to_fill = locations.pop() diff --git a/Goals.py b/Goals.py index c88f76387..d765760a8 100644 --- a/Goals.py +++ b/Goals.py @@ -1,6 +1,8 @@ +from __future__ import annotations import sys from collections import defaultdict -from typing import TYPE_CHECKING, List, Union, Dict, Optional, Any, Tuple, Iterable, Callable, Collection +from collections.abc import Iterable, Collection +from typing import TYPE_CHECKING, Optional, Any from HintList import goalTable, get_hint_group, hint_exclusions from ItemList import item_table @@ -18,10 +20,10 @@ from State import State from World import World -RequiredLocations: TypeAlias = "Dict[str, Union[Dict[str, Dict[int, List[Tuple[Location, int, int]]]], List[Location]]]" -GoalItem: TypeAlias = Dict[str, Union[str, int, bool]] +RequiredLocations: TypeAlias = "dict[str, dict[str, dict[int, list[tuple[Location, int, int]]]] | list[Location]]" +GoalItem: TypeAlias = "dict[str, str | int | bool]" -validColors: List[str] = [ +validColors: list[str] = [ 'White', 'Red', 'Green', @@ -34,28 +36,28 @@ class Goal: - def __init__(self, world: "World", name: str, hint_text: Union[str, Dict[str, str]], color: str, items: Optional[List[Dict[str, Any]]] = None, - locations=None, lock_locations=None, lock_entrances: List[str] = None, required_locations=None, create_empty: bool = False) -> None: + def __init__(self, world: World, name: str, hint_text: str | dict[str, str], color: str, items: Optional[list[dict[str, Any]]] = None, + locations=None, lock_locations=None, lock_entrances: list[str] = None, required_locations=None, create_empty: bool = False) -> None: # early exit if goal initialized incorrectly if not items and not locations and not create_empty: raise Exception('Invalid goal: no items or destinations set') if color not in validColors: raise Exception(f'Invalid goal: Color {color} not supported') - self.world: "World" = world + self.world: World = world self.name: str = name - self.hint_text: Union[str, Dict[str, str]] = hint_text + self.hint_text: str | dict[str, str] = hint_text self.color: str = color - self.items: List[GoalItem] = items or [] + self.items: list[GoalItem] = items or [] self.locations = locations # Unused? self.lock_locations = lock_locations # Unused? - self.lock_entrances: List[str] = lock_entrances - self.required_locations: "List[Tuple[Location, int, int, List[int]]]" = required_locations or [] + self.lock_entrances: list[str] = lock_entrances + self.required_locations: list[tuple[Location, int, int, list[int]]] = required_locations or [] self.weight: int = 0 - self.category: 'Optional[GoalCategory]' = None - self._item_cache: Dict[str, GoalItem] = {} + self.category: Optional[GoalCategory] = None + self._item_cache: dict[str, GoalItem] = {} - def copy(self) -> 'Goal': + def copy(self) -> Goal: new_goal = Goal(self.world, self.name, self.hint_text, self.color, self.items, self.locations, self.lock_locations, self.lock_entrances, self.required_locations, True) return new_goal @@ -80,18 +82,18 @@ def requires(self, item: str) -> bool: class GoalCategory: def __init__(self, name: str, priority: int, goal_count: int = 0, minimum_goals: int = 0, - lock_locations=None, lock_entrances: List[str] = None) -> None: + lock_locations=None, lock_entrances: list[str] = None) -> None: self.name: str = name self.priority: int = priority self.lock_locations = lock_locations # Unused? - self.lock_entrances: List[str] = lock_entrances - self.goals: List[Goal] = [] + self.lock_entrances: list[str] = lock_entrances + self.goals: list[Goal] = [] self.goal_count: int = goal_count self.minimum_goals: int = minimum_goals self.weight: int = 0 - self._goal_cache: Dict[str, Goal] = {} + self._goal_cache: dict[str, Goal] = {} - def copy(self) -> 'GoalCategory': + def copy(self) -> GoalCategory: new_category = GoalCategory(self.name, self.priority, self.goal_count, self.minimum_goals, self.lock_locations, self.lock_entrances) new_category.goals = list(goal.copy() for goal in self.goals) return new_category @@ -141,7 +143,7 @@ def update_reachable_goals(self, starting_search: Search, full_search: Search) - i['quantity'] = min(full_search.state_list[index].item_name_count(i['name']), i['quantity']) -def replace_goal_names(worlds: "List[World]") -> None: +def replace_goal_names(worlds: list[World]) -> None: for world in worlds: bosses = [location for location in world.get_filled_locations() if location.item.type == 'DungeonReward'] for cat_name, category in world.goal_categories.items(): @@ -158,7 +160,7 @@ def replace_goal_names(worlds: "List[World]") -> None: break -def update_goal_items(spoiler: "Spoiler") -> None: +def update_goal_items(spoiler: Spoiler) -> None: worlds = spoiler.worlds # get list of all the progressive items that can appear in hints @@ -280,7 +282,7 @@ def update_goal_items(spoiler: "Spoiler") -> None: spoiler.goal_locations = required_locations_dict -def lock_category_entrances(category: GoalCategory, state_list: "Iterable[State]") -> "Dict[int, Dict[str, AccessRule]]": +def lock_category_entrances(category: GoalCategory, state_list: Iterable[State]) -> dict[int, dict[str, AccessRule]]: # Disable access rules for specified entrances category_locks = {} if category.lock_entrances is not None: @@ -293,8 +295,8 @@ def lock_category_entrances(category: GoalCategory, state_list: "Iterable[State] return category_locks -def unlock_category_entrances(category_locks: "Dict[int, Dict[str, AccessRule]]", - state_list: "List[State]") -> None: +def unlock_category_entrances(category_locks: dict[int, dict[str, AccessRule]], + state_list: list[State]) -> None: # Restore access rules for state_id, exits in category_locks.items(): for exit_name, access_rule in exits.items(): @@ -302,8 +304,8 @@ def unlock_category_entrances(category_locks: "Dict[int, Dict[str, AccessRule]]" exit.access_rule = access_rule -def search_goals(categories: Dict[str, GoalCategory], reachable_goals: ValidGoals, search: Search, priority_locations: Dict[int, Dict[str, str]], - all_locations: "List[Location]", item_locations: "Collection[Location]", always_locations: Collection[str], +def search_goals(categories: dict[str, GoalCategory], reachable_goals: ValidGoals, search: Search, priority_locations: dict[int, dict[str, str]], + all_locations: list[Location], item_locations: Collection[Location], always_locations: Collection[str], search_woth: bool = False) -> RequiredLocations: # required_locations[category.name][goal.name][world_id] = [...] required_locations: RequiredLocations = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) @@ -324,11 +326,11 @@ def search_goals(categories: Dict[str, GoalCategory], reachable_goals: ValidGoal if category.name in reachable_goals and reachable_goals[category.name]: for goal in category.goals: if (category.name in valid_goals - and goal.name in valid_goals[category.name] - and goal.name in reachable_goals[category.name] - and (location.name not in priority_locations[location.world.id] - or priority_locations[location.world.id][location.name] == category.name) - and not goal.requires(old_item.name)): + and goal.name in valid_goals[category.name] + and goal.name in reachable_goals[category.name] + and (location.name not in priority_locations[location.world.id] + or priority_locations[location.world.id][location.name] == category.name) + and not goal.requires(old_item.name)): invalid_states = set(world_ids) - set(valid_goals[category.name][goal.name]) hintable_states = list(invalid_states & set(reachable_goals[category.name][goal.name])) if hintable_states: diff --git a/Gui.py b/Gui.py index 3f4f32588..558f898b3 100755 --- a/Gui.py +++ b/Gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import sys -if sys.version_info < (3, 6, 0): - print("OoT Randomizer requires Python version 3.6 or newer and you are using %s" % '.'.join([str(i) for i in sys.version_info[0:3]])) +if sys.version_info < (3, 7, 0): + print("OoT Randomizer requires Python version 3.7 or newer and you are using %s" % '.'.join([str(i) for i in sys.version_info[0:3]])) # raw_input was renamed to input in 3.0, handle both 2.x and 3.x by trying the rename for 2.x try: input = raw_input diff --git a/HintList.py b/HintList.py index 3fc787328..3f58d44e2 100644 --- a/HintList.py +++ b/HintList.py @@ -1,5 +1,7 @@ +from __future__ import annotations import random -from typing import TYPE_CHECKING, Union, List, Dict, Callable, Tuple, Optional, Any, Collection +from collections.abc import Callable, Collection +from typing import TYPE_CHECKING, Optional, Any if TYPE_CHECKING: from World import World @@ -26,9 +28,9 @@ class Hint: - def __init__(self, name: str, text: Union[str, List[str]], hint_type: Union[str, List[str]], choice: Optional[int] = None) -> None: + def __init__(self, name: str, text: str | list[str], hint_type: str | list[str], choice: Optional[int] = None) -> None: self.name: str = name - self.type: List[str] = [hint_type] if not isinstance(hint_type, list) else hint_type + self.type: list[str] = [hint_type] if not isinstance(hint_type, list) else hint_type self.text: str if isinstance(text, str): @@ -41,9 +43,9 @@ def __init__(self, name: str, text: Union[str, List[str]], hint_type: Union[str, class Multi: - def __init__(self, name: str, locations: List[str]) -> None: + def __init__(self, name: str, locations: list[str]) -> None: self.name: str = name - self.locations: List[str] = locations + self.locations: list[str] = locations def get_hint(name: str, clearer_hint: bool = False) -> Hint: @@ -61,7 +63,7 @@ def get_multi(name: str) -> Multi: return Multi(name, locations) -def get_hint_group(group: str, world: "World") -> List[Hint]: +def get_hint_group(group: str, world: World) -> list[Hint]: ret = [] for name in hintTable: @@ -117,7 +119,7 @@ def get_hint_group(group: str, world: "World") -> List[Hint]: return ret -def get_required_hints(world: "World") -> List[Hint]: +def get_required_hints(world: World) -> list[Hint]: ret = [] for name in hintTable: hint = get_hint(name) @@ -127,7 +129,7 @@ def get_required_hints(world: "World") -> List[Hint]: # Get the multi hints containing the list of locations for a possible hint upgrade. -def get_upgrade_hint_list(world: "World", locations: List[str]) -> List[Hint]: +def get_upgrade_hint_list(world: World, locations: list[str]) -> list[Hint]: ret = [] for name in multiTable: if name not in hint_exclusions(world): @@ -162,7 +164,7 @@ def get_upgrade_hint_list(world: "World", locations: List[str]) -> List[Hint]: # Helpers for conditional always hints # TODO: Make these properties of World or Settings. -def stones_required_by_settings(world: "World") -> int: +def stones_required_by_settings(world: World) -> int: stones = 0 if world.settings.bridge == 'stones' and not world.shuffle_special_dungeon_entrances: stones = max(stones, world.settings.bridge_stones) @@ -180,7 +182,7 @@ def stones_required_by_settings(world: "World") -> int: return stones -def medallions_required_by_settings(world: "World") -> int: +def medallions_required_by_settings(world: World) -> int: medallions = 0 if world.settings.bridge == 'medallions' and not world.shuffle_special_dungeon_entrances: medallions = max(medallions, world.settings.bridge_medallions) @@ -198,7 +200,7 @@ def medallions_required_by_settings(world: "World") -> int: return medallions -def tokens_required_by_settings(world: "World") -> int: +def tokens_required_by_settings(world: World) -> int: tokens = 0 if world.settings.bridge == 'tokens' and not world.shuffle_special_dungeon_entrances: tokens = max(tokens, world.settings.bridge_tokens) @@ -211,7 +213,7 @@ def tokens_required_by_settings(world: "World") -> int: # Hints required under certain settings -conditional_always: "Dict[str, Callable[[World], bool]]" = { +conditional_always: dict[str, Callable[[World], bool]] = { 'Market 10 Big Poes': lambda world: world.settings.big_poe_count > 3, 'Deku Theater Mask of Truth': lambda world: not world.settings.complete_mask_quest, 'Song from Ocarina of Time': lambda world: stones_required_by_settings(world) < 2, @@ -226,7 +228,7 @@ def tokens_required_by_settings(world: "World") -> int: } # Entrance hints required under certain settings -conditional_entrance_always: "Dict[str, Callable[[World], bool]]" = { +conditional_entrance_always: dict[str, Callable[[World], bool]] = { 'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: (world.settings.bridge != 'open' and (world.settings.bridge != 'stones' or world.settings.bridge_stones > 1) and (world.settings.bridge != 'medallions' or world.settings.bridge_medallions > 1) @@ -236,14 +238,14 @@ def tokens_required_by_settings(world: "World") -> int: } # Dual hints required under certain settings -conditional_dual_always: "Dict[str, Callable[[World], bool]]" = { +conditional_dual_always: dict[str, Callable[[World], bool]] = { 'HF Ocarina of Time Retrieval': lambda world: stones_required_by_settings(world) < 2, 'Deku Theater Rewards': lambda world: not world.settings.complete_mask_quest, 'ZR Frogs Rewards': lambda world: not world.settings.shuffle_frog_song_rupees and 'frogs2' not in world.settings.misc_hints, } # Some sometimes, dual, and entrance hints should only be enabled under certain settings -conditional_sometimes: "Dict[str, Callable[[World], bool]]" = { +conditional_sometimes: dict[str, Callable[[World], bool]] = { # Conditional sometimes hints 'HC Great Fairy Reward': lambda world: world.settings.shuffle_interior_entrances == 'off', 'OGC Great Fairy Reward': lambda world: world.settings.shuffle_interior_entrances == 'off', @@ -290,7 +292,7 @@ def tokens_required_by_settings(world: "World") -> int: # \u00A9 Down arrow # \u00AA Joystick -hintTable: Dict[str, Tuple[Union[List[str], str], Optional[str], Union[str, List[str]]]] = { +hintTable: dict[str, tuple[list[str] | str, Optional[str], str | list[str]]] = { 'Kokiri Emerald': (["a tree's farewell", "the Spiritual Stone of the Forest"], "the Kokiri Emerald", 'item'), 'Goron Ruby': (["the Gorons' hidden treasure", "the Spiritual Stone of Fire"], "the Goron Ruby", 'item'), 'Zora Sapphire': (["an engagement ring", "the Spiritual Stone of Water"], "the Zora Sapphire", 'item'), @@ -1723,7 +1725,7 @@ def tokens_required_by_settings(world: "World") -> int: # Table containing the groups of locations for the multi hints (dual, etc.) # The is used in order to add the locations to the checked list -multiTable: Dict[str, List[str]] = { +multiTable: dict[str, list[str]] = { 'Deku Theater Rewards': ['Deku Theater Skull Mask', 'Deku Theater Mask of Truth'], 'HF Ocarina of Time Retrieval': ['HF Ocarina of Time Item', 'Song from Ocarina of Time'], 'HF Valley Grotto': ['HF Cow Grotto Cow', 'HF GS Cow Grotto'], @@ -1772,7 +1774,7 @@ def tokens_required_by_settings(world: "World") -> int: } # TODO: Make these a type of some sort instead of a dict. -misc_item_hint_table: Dict[str, Dict[str, Any]] = { +misc_item_hint_table: dict[str, dict[str, Any]] = { 'dampe_diary': { 'id': 0x5003, 'hint_location': 'Dampe Diary Hint', @@ -1801,7 +1803,7 @@ def tokens_required_by_settings(world: "World") -> int: }, } -misc_location_hint_table: Dict[str, Dict[str, Any]] = { +misc_location_hint_table: dict[str, dict[str, Any]] = { '10_skulltulas': { 'id': 0x9004, 'hint_location': '10 Skulltulas Reward Hint', @@ -1849,7 +1851,7 @@ def tokens_required_by_settings(world: "World") -> int: # Separate table for goal names to avoid duplicates in the hint table. # Link's Pocket will always be an empty goal, but it's included here to # prevent key errors during the dungeon reward lookup. -goalTable: Dict[str, Tuple[str, str, str]] = { +goalTable: dict[str, tuple[str, str, str]] = { 'Queen Gohma': ("path to the #Spider#", "path to #Queen Gohma#", "Green"), 'King Dodongo': ("path to the #Dinosaur#", "path to #King Dodongo#", "Red"), 'Barinade': ("path to the #Tentacle#", "path to #Barinade#", "Blue"), @@ -1863,8 +1865,8 @@ def tokens_required_by_settings(world: "World") -> int: # This specifies which hints will never appear due to either having known or known useless contents or due to the locations not existing. -def hint_exclusions(world: "World", clear_cache: bool = False) -> List[str]: - exclusions: Dict[int, List[str]] = hint_exclusions.exclusions +def hint_exclusions(world: World, clear_cache: bool = False) -> list[str]: + exclusions: dict[int, list[str]] = hint_exclusions.exclusions if not clear_cache and world.id in exclusions: return exclusions[world.id] @@ -1913,7 +1915,7 @@ def hint_exclusions(world: "World", clear_cache: bool = False) -> List[str]: hint_exclusions.exclusions = {} -def name_is_location(name: str, hint_type: Union[str, Collection[str]], world: "World") -> bool: +def name_is_location(name: str, hint_type: str | Collection[str], world: World) -> bool: if isinstance(hint_type, (list, tuple)): for htype in hint_type: if htype in ['sometimes', 'song', 'overworld', 'dungeon', 'always', 'exclude'] and name not in hint_exclusions( diff --git a/Hints.py b/Hints.py index 2aa762d6b..48f3f67df 100644 --- a/Hints.py +++ b/Hints.py @@ -1,3 +1,4 @@ +from __future__ import annotations import itertools import json import logging @@ -6,8 +7,9 @@ import sys import urllib.request from collections import OrderedDict, defaultdict +from collections.abc import Callable, Iterable from enum import Enum -from typing import TYPE_CHECKING, Set, List, Optional, Dict, Union, MutableSet, Tuple, Callable, Iterable +from typing import TYPE_CHECKING, Optional from urllib.error import URLError, HTTPError from HintList import Hint, get_hint, get_multi, get_hint_group, get_upgrade_hint_list, hint_exclusions, \ @@ -31,18 +33,18 @@ from Spoiler import Spoiler from World import World -Spot: TypeAlias = "Union[Entrance, Location, Region]" -HintReturn: TypeAlias = "Optional[Tuple[GossipText, Optional[List[Location]]]]" -HintFunc: TypeAlias = Callable[["Spoiler", "World", MutableSet[str]], HintReturn] -BarrenFunc: TypeAlias = Callable[["Spoiler", "World", MutableSet[str], MutableSet[str]], HintReturn] +Spot: TypeAlias = "Entrance | Location | Region" +HintReturn: TypeAlias = "Optional[tuple[GossipText, Optional[list[Location]]]]" +HintFunc: TypeAlias = "Callable[[Spoiler, World, set[str]], HintReturn]" +BarrenFunc: TypeAlias = "Callable[[Spoiler, World, set[str], set[str]], HintReturn]" -bingoBottlesForHints: Set[str] = { +bingoBottlesForHints: set[str] = { "Bottle", "Bottle with Red Potion", "Bottle with Green Potion", "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Big Poe", "Bottle with Poe", } -defaultHintDists: List[str] = [ +defaultHintDists: list[str] = [ 'balanced.json', 'bingo.json', 'chaos.json', @@ -61,7 +63,7 @@ 'weekly.json', ] -unHintableWothItems: Set[str] = {'Triforce Piece', 'Gold Skulltula Token', 'Piece of Heart', 'Piece of Heart (Treasure Chest Game)', 'Heart Container'} +unHintableWothItems: set[str] = {'Triforce Piece', 'Gold Skulltula Token', 'Piece of Heart', 'Piece of Heart (Treasure Chest Game)', 'Heart Container'} class RegionRestriction(Enum): @@ -78,14 +80,14 @@ def __init__(self, name: str, location: str) -> None: class GossipText: - def __init__(self, text: str, colors: Optional[List[str]] = None, hinted_locations: Optional[List[str]] = None, - hinted_items: Optional[List[str]] = None, prefix: str = "They say that ") -> None: + def __init__(self, text: str, colors: Optional[list[str]] = None, hinted_locations: Optional[list[str]] = None, + hinted_items: Optional[list[str]] = None, prefix: str = "They say that ") -> None: text = prefix + text text = text[:1].upper() + text[1:] self.text: str = text - self.colors: Optional[List[str]] = colors - self.hinted_locations: Optional[List[str]] = hinted_locations - self.hinted_items: Optional[List[str]] = hinted_items + self.colors: Optional[list[str]] = colors + self.hinted_locations: Optional[list[str]] = hinted_locations + self.hinted_items: Optional[list[str]] = hinted_items def to_json(self) -> dict: return {'text': self.text, 'colors': self.colors, 'hinted_locations': self.hinted_locations, 'hinted_items': self.hinted_items} @@ -110,7 +112,7 @@ def __str__(self) -> str: # ZF Zora's Fountain # ZR Zora's River -gossipLocations: Dict[int, GossipStone] = { +gossipLocations: dict[int, GossipStone] = { 0x0405: GossipStone('DMC (Bombable Wall)', 'DMC Gossip Stone'), 0x0404: GossipStone('DMT (Biggoron)', 'DMT Gossip Stone'), 0x041A: GossipStone('Colossus (Spirit Temple)', 'Colossus Gossip Stone'), @@ -154,7 +156,7 @@ def __str__(self) -> str: 0x044A: GossipStone('DMC (Upper Grotto)', 'DMC Upper Grotto Gossip Stone'), } -gossipLocations_reversemap: Dict[str, int] = { +gossipLocations_reversemap: dict[str, int] = { stone.name: stone_id for stone_id, stone in gossipLocations.items() } @@ -176,8 +178,8 @@ def is_restricted_dungeon_item(item: Item) -> bool: ) -def add_hint(spoiler: "Spoiler", world: "World", groups: List[List[int]], gossip_text: GossipText, count: int, - locations: "Optional[List[Location]]" = None, force_reachable: bool = False) -> bool: +def add_hint(spoiler: Spoiler, world: World, groups: list[list[int]], gossip_text: GossipText, count: int, + locations: Optional[list[Location]] = None, force_reachable: bool = False) -> bool: random.shuffle(groups) skipped_groups = [] duplicates = [] @@ -266,7 +268,7 @@ def add_hint(spoiler: "Spoiler", world: "World", groups: List[List[int]], gossip return success -def can_reach_hint(worlds: "List[World]", hint_location: "Location", location: "Location") -> bool: +def can_reach_hint(worlds: list[World], hint_location: Location, location: Location) -> bool: if location is None: return True @@ -279,7 +281,7 @@ def can_reach_hint(worlds: "List[World]", hint_location: "Location", location: " and (hint_location.type != 'HintStone' or search.state_list[location.world.id].guarantee_hint())) -def write_gossip_stone_hints(spoiler: "Spoiler", world: "World", messages: List[Message]) -> None: +def write_gossip_stone_hints(spoiler: Spoiler, world: World, messages: list[Message]) -> None: for id, gossip_text in spoiler.hints[world.id].items(): update_message_by_id(messages, id, str(gossip_text), 0x23) @@ -291,7 +293,7 @@ def filter_trailing_space(text: str) -> str: return text -hintPrefixes: List[str] = [ +hintPrefixes: list[str] = [ 'a few ', 'some ', 'plenty of ', @@ -385,7 +387,7 @@ class HintArea(Enum): # Performs a breadth first search to find the closest hint area from a given spot (region, location, or entrance). # May fail to find a hint if the given spot is only accessible from the root and not from any other region with a hint area @staticmethod - def at(spot: Spot, use_alt_hint: bool = False) -> 'HintArea': + def at(spot: Spot, use_alt_hint: bool = False) -> HintArea: if isinstance(spot, Region): original_parent = spot else: @@ -422,7 +424,7 @@ def at(spot: Spot, use_alt_hint: bool = False) -> 'HintArea': raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id)) @classmethod - def for_dungeon(cls, dungeon_name: str) -> 'Optional[HintArea]': + def for_dungeon(cls, dungeon_name: str) -> Optional[HintArea]: if '(' in dungeon_name and ')' in dungeon_name: # A dungeon item name was passed in - get the name of the dungeon from it. dungeon_name = dungeon_name[dungeon_name.index('(') + 1:dungeon_name.index(')')] @@ -473,7 +475,7 @@ def is_dungeon_item(self, item: Item) -> bool: # Formats the hint text for this area with proper grammar. # Dungeons are hinted differently depending on the clearer_hints setting. - def text(self, clearer_hints: bool, preposition: bool = False, world: "Optional[int]" = None) -> str: + def text(self, clearer_hints: bool, preposition: bool = False, world: Optional[int] = None) -> str: if self.is_dungeon and self.dungeon_name: text = get_hint(self.dungeon_name, clearer_hints).text else: @@ -500,7 +502,7 @@ def text(self, clearer_hints: bool, preposition: bool = False, world: "Optional[ return text -def get_woth_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_woth_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: locations = spoiler.required_locations[world.id] locations = list(filter(lambda location: location.name not in checked @@ -525,8 +527,8 @@ def get_woth_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) return GossipText('%s is on the way of the hero.' % location_text, ['Light Blue'], [location.name], [location.item.name]), [location] -def get_checked_areas(world: "World", checked: MutableSet[str]) -> Set[Union[HintArea, str]]: - def get_area_from_name(check: str) -> Union[HintArea, str]: +def get_checked_areas(world: World, checked: set[str]) -> set[HintArea | str]: + def get_area_from_name(check: str) -> HintArea | str: try: location = world.get_location(check) except Exception: @@ -538,7 +540,7 @@ def get_area_from_name(check: str) -> Union[HintArea, str]: return set(get_area_from_name(check) for check in checked) -def get_goal_category(spoiler: "Spoiler", world: "World", goal_categories: "Dict[str, GoalCategory]") -> "GoalCategory": +def get_goal_category(spoiler: Spoiler, world: World, goal_categories: dict[str, GoalCategory]) -> GoalCategory: cat_sizes = [] cat_names = [] zero_weights = True @@ -573,7 +575,7 @@ def get_goal_category(spoiler: "Spoiler", world: "World", goal_categories: "Dict return goal_category -def get_goal_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: goal_category = get_goal_category(spoiler, world, world.goal_categories) # check if no goals were generated (and thus no categories available) @@ -639,12 +641,12 @@ def get_goal_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) required_locations = [loc[0] for loc in other_goal.required_locations] goal_locations = list(filter(lambda loc: - loc.name not in checked - and loc.name not in world.hint_exclusions - and loc.name not in world.hint_type_overrides['goal'] - and loc.item.name not in world.item_hint_type_overrides['goal'] - and loc.item.name not in unHintableWothItems, - required_locations)) + loc.name not in checked + and loc.name not in world.hint_exclusions + and loc.name not in world.hint_type_overrides['goal'] + and loc.item.name not in world.item_hint_type_overrides['goal'] + and loc.item.name not in unHintableWothItems, + required_locations)) if not goal_locations: other_goal.weight = 0 if world.one_hint_per_goal and location in required_locations: @@ -667,7 +669,7 @@ def get_goal_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) return GossipText('%s is on %s %s.' % (location_text, player_text, goal_text), ['Light Blue', goal.color], [location.name], [location.item.name]), [location] -def get_barren_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str], all_checked: MutableSet[str]) -> HintReturn: +def get_barren_hint(spoiler: Spoiler, world: World, checked: set[str], all_checked: set[str]) -> HintReturn: if not hasattr(world, 'get_barren_hint_prev'): world.get_barren_hint_prev = RegionRestriction.NONE @@ -726,11 +728,11 @@ def get_barren_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str] return GossipText("plundering %s is a foolish choice." % area.text(world.settings.clearer_hints), ['Pink']), None -def is_not_checked(locations: "Iterable[Location]", checked: MutableSet[Union[HintArea, str]]) -> bool: +def is_not_checked(locations: Iterable[Location], checked: set[HintArea | str]) -> bool: return not any(location.name in checked or HintArea.at(location) in checked for location in locations) -def get_good_item_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: locations = list(filter(lambda location: is_not_checked([location], checked) and ((location.item.majoritem @@ -758,7 +760,7 @@ def get_good_item_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[s return GossipText('#%s# can be found %s.' % (item_text, location_text), ['Green', 'Red'], [location.name], [location.item.name]), [location] -def get_specific_item_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: if len(world.named_item_pool) == 0: logger = logging.getLogger('') logger.info("Named item hint requested, but pool is empty.") @@ -897,7 +899,7 @@ def get_specific_item_hint(spoiler: "Spoiler", world: "World", checked: MutableS return GossipText('#%s# can be found %s.' % (item_text, location_text), ['Green', 'Red'], [location.name], [location.item.name]), [location] -def get_random_location_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: locations = list(filter(lambda location: is_not_checked([location], checked) and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward') @@ -923,7 +925,7 @@ def get_random_location_hint(spoiler: "Spoiler", world: "World", checked: Mutabl return GossipText('#%s# can be found %s.' % (item_text, location_text), ['Green', 'Red'], [location.name], [location.item.name]), [location] -def get_specific_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str], hint_type: str) -> HintReturn: +def get_specific_hint(spoiler: Spoiler, world: World, checked: set[str], hint_type: str) -> HintReturn: hint_group = get_hint_group(hint_type, world) hint_group = list(filter(lambda hint: is_not_checked([world.get_location(hint.name)], checked), hint_group)) if not hint_group: @@ -963,23 +965,23 @@ def get_specific_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[st return GossipText('%s #%s#.' % (location_text, item_text), ['Red', 'Green'], [location.name], [location.item.name]), [location] -def get_sometimes_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_sometimes_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'sometimes') -def get_song_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_song_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'song') -def get_overworld_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_overworld_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'overworld') -def get_dungeon_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_dungeon_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: return get_specific_hint(spoiler, world, checked, 'dungeon') -def get_random_multi_hint(spoiler: "Spoiler", world: "World", checked: "MutableSet", hint_type: str) -> HintReturn: +def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hint_type: str) -> HintReturn: hint_group = get_hint_group(hint_type, world) multi_hints = list(filter(lambda hint: is_not_checked([world.get_location(location) for location in get_multi( hint.name).locations], checked), hint_group)) @@ -1007,7 +1009,7 @@ def get_random_multi_hint(spoiler: "Spoiler", world: "World", checked: "MutableS return get_specific_multi_hint(spoiler, world, checked, hint) -def get_specific_multi_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str], hint: Hint) -> HintReturn: +def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hint: Hint) -> HintReturn: multi = get_multi(hint.name) locations = [world.get_location(location) for location in multi.locations] @@ -1036,11 +1038,11 @@ def get_specific_multi_hint(spoiler: "Spoiler", world: "World", checked: Mutable return GossipText(gossip_string % tuple(text_segments), colors, [location.name for location in locations], [item.name for item in items]), locations -def get_dual_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_dual_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: return get_random_multi_hint(spoiler, world, checked, 'dual') -def get_entrance_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: if not world.entrance_shuffle: return None @@ -1076,7 +1078,7 @@ def get_entrance_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[st return GossipText('%s %s.' % (entrance_text, region_text), ['Green', 'Light Blue']), None -def get_junk_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: +def get_junk_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: hints = get_hint_group('junk', world) hints = list(filter(lambda hint: hint.name not in checked, hints)) if not hints: @@ -1087,36 +1089,37 @@ def get_junk_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) return GossipText(hint.text, prefix=''), None -def get_important_check_hint(spoiler: "Spoiler", world: "World", checked: MutableSet[str]) -> HintReturn: + +def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn: top_level_locations = [] for location in world.get_filled_locations(): if (HintArea.at(location).text(world.settings.clearer_hints) not in top_level_locations - and (HintArea.at(location).text(world.settings.clearer_hints) + ' Important Check') not in checked - and "pocket" not in HintArea.at(location).text(world.settings.clearer_hints)): + and (HintArea.at(location).text(world.settings.clearer_hints) + ' Important Check') not in checked + and "pocket" not in HintArea.at(location).text(world.settings.clearer_hints)): top_level_locations.append(HintArea.at(location).text(world.settings.clearer_hints)) - hintLoc = random.choice(top_level_locations) + hint_loc = random.choice(top_level_locations) item_count = 0 for location in world.get_filled_locations(): region = HintArea.at(location).text(world.settings.clearer_hints) - if region == hintLoc: + if region == hint_loc: if (location.item.majoritem - #exclude locked items + # exclude locked items and not location.locked - #exclude triforce pieces as it defeats the idea of a triforce hunt + # exclude triforce pieces as it defeats the idea of a triforce hunt and not location.item.name == 'Triforce Piece' and not (location.name == 'Song from Impa' and 'Zeldas Letter' in world.settings.starting_items and 'Zeldas Letter' not in world.settings.shuffle_child_trade) - #Handle make keys not in own dungeon major items + # Handle make keys not in own dungeon major items or (location.item.type == 'SmallKey' and not (world.settings.shuffle_smallkeys == 'dungeon' or world.settings.shuffle_smallkeys == 'vanilla')) or (location.item.type == 'HideoutSmallKey' and not world.settings.shuffle_hideoutkeys == 'vanilla') or (location.item.type == 'TCGSmallKey' and not world.settings.shuffle_tcgkeys == 'vanilla') or (location.item.type == 'BossKey' and not (world.settings.shuffle_bosskeys == 'dungeon' or world.settings.shuffle_bosskeys == 'vanilla')) or (location.item.type == 'GanonBossKey' and not (world.settings.shuffle_ganon_bosskey == 'vanilla' - or world.settings.shuffle_ganon_bosskey == 'dungeon' or world.settings.shuffle_ganon_bosskey == 'on_lacs' - or world.settings.shuffle_ganon_bosskey == 'stones' or world.settings.shuffle_ganon_bosskey == 'medallions' - or world.settings.shuffle_ganon_bosskey == 'dungeons' or world.settings.shuffle_ganon_bosskey == 'tokens'))): + or world.settings.shuffle_ganon_bosskey == 'dungeon' or world.settings.shuffle_ganon_bosskey == 'on_lacs' + or world.settings.shuffle_ganon_bosskey == 'stones' or world.settings.shuffle_ganon_bosskey == 'medallions' + or world.settings.shuffle_ganon_bosskey == 'dungeons' or world.settings.shuffle_ganon_bosskey == 'tokens'))): item_count = item_count + 1 - checked.add(hintLoc + ' Important Check') + checked.add(hint_loc + ' Important Check') if item_count == 0: numcolor = 'Red' @@ -1129,10 +1132,10 @@ def get_important_check_hint(spoiler: "Spoiler", world: "World", checked: Mutabl else: numcolor = 'Green' - return GossipText('#%s# has #%d# major item%s.' % (hintLoc, item_count, "s" if item_count != 1 else ""), ['Green', numcolor]), None + return GossipText('#%s# has #%d# major item%s.' % (hint_loc, item_count, "s" if item_count != 1 else ""), ['Green', numcolor]), None -hint_func: "Dict[str, Union[HintFunc, BarrenFunc]]" = { +hint_func: dict[str, HintFunc | BarrenFunc] = { 'trial': lambda spoiler, world, checked: None, 'always': lambda spoiler, world, checked: None, 'dual_always': lambda spoiler, world, checked: None, @@ -1153,7 +1156,7 @@ def get_important_check_hint(spoiler: "Spoiler", world: "World", checked: Mutabl 'important_check': get_important_check_hint } -hint_dist_keys: Set[str] = { +hint_dist_keys: set[str] = { 'trial', 'always', 'dual_always', @@ -1174,7 +1177,7 @@ def get_important_check_hint(spoiler: "Spoiler", world: "World", checked: Mutabl } -def build_bingo_hint_list(board_url: str) -> List[str]: +def build_bingo_hint_list(board_url: str) -> list[str]: try: if len(board_url) > 256: raise URLError(f"URL too large {len(board_url)}") @@ -1218,7 +1221,7 @@ def build_bingo_hint_list(board_url: str) -> List[str]: return hints -def always_named_item(world: "World", locations: "Iterable[Location]"): +def always_named_item(world: World, locations: Iterable[Location]): for location in locations: if location.item.name in bingoBottlesForHints and world.settings.hint_dist == 'bingo': always_item = 'Bottle' @@ -1228,7 +1231,7 @@ def always_named_item(world: "World", locations: "Iterable[Location]"): world.named_item_pool.remove(always_item) -def build_gossip_hints(spoiler: "Spoiler", worlds: "List[World]") -> None: +def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None: checked_locations = dict() # Add misc. item hint locations to "checked" locations if the respective hint is reachable without the hinted item. for world in worlds: @@ -1259,7 +1262,7 @@ def build_gossip_hints(spoiler: "Spoiler", worlds: "List[World]") -> None: # builds out general hints based on location and whether an item is required or not -def build_world_gossip_hints(spoiler: "Spoiler", world: "World", checked_locations: Optional[MutableSet[str]] = None) -> None: +def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: Optional[set[str]] = None) -> None: world.barren_dungeon = 0 world.woth_dungeon = 0 @@ -1567,7 +1570,7 @@ def build_world_gossip_hints(spoiler: "Spoiler", world: "World", checked_locatio # builds text that is displayed at the temple of time altar for child and adult, rewards pulled based off of item in a fixed order. -def build_altar_hints(world: "World", messages: List[Message], include_rewards: bool = True, include_wincons: bool = True) -> None: +def build_altar_hints(world: World, messages: list[Message], include_rewards: bool = True, include_wincons: bool = True) -> None: # text that appears at altar as a child. child_text = '\x08' if include_rewards: @@ -1608,7 +1611,7 @@ def build_altar_hints(world: "World", messages: List[Message], include_rewards: # pulls text string from hintlist for reward after sending the location to hintlist. -def build_boss_string(reward: str, color: str, world: "World") -> str: +def build_boss_string(reward: str, color: str, world: World) -> str: item_icon = chr(Item(reward).special['item_id']) if reward in world.distribution.effective_starting_items and world.distribution.effective_starting_items[reward].count > 0: if world.settings.clearer_hints: @@ -1622,7 +1625,7 @@ def build_boss_string(reward: str, color: str, world: "World") -> str: return str(text) + '\x04' -def build_bridge_reqs_string(world: "World") -> str: +def build_bridge_reqs_string(world: World) -> str: string = "\x13\x12" # Light Arrow Icon if world.settings.bridge == 'open': string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells." @@ -1644,7 +1647,7 @@ def build_bridge_reqs_string(world: "World") -> str: return str(GossipText(string, ['Green'], prefix='')) -def build_ganon_boss_key_string(world: "World") -> str: +def build_ganon_boss_key_string(world: World) -> str: string = "\x13\x74" # Boss Key Icon if world.settings.shuffle_ganon_bosskey == 'remove': string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#." @@ -1687,7 +1690,7 @@ def build_ganon_boss_key_string(world: "World") -> str: # fun new lines for Ganon during the final battle -def build_ganon_text(world: "World", messages: List[Message]) -> None: +def build_ganon_text(world: World, messages: list[Message]) -> None: # empty now unused messages to make space for ganon lines update_message_by_id(messages, 0x70C8, " ") update_message_by_id(messages, 0x70C9, " ") @@ -1700,7 +1703,7 @@ def build_ganon_text(world: "World", messages: List[Message]) -> None: update_message_by_id(messages, 0x70CB, text) -def build_misc_item_hints(world: "World", messages: List[Message]) -> None: +def build_misc_item_hints(world: World, messages: list[Message]) -> None: for hint_type, data in misc_item_hint_table.items(): if hint_type in world.settings.misc_hints: item = world.misc_hint_items[hint_type] @@ -1733,7 +1736,7 @@ def build_misc_item_hints(world: "World", messages: List[Message]) -> None: update_message_by_id(messages, data['id'], str(GossipText(text, ['Green'], prefix=''))) -def build_misc_location_hints(world: "World", messages: List[Message]) -> None: +def build_misc_location_hints(world: World, messages: list[Message]) -> None: for hint_type, data in misc_location_hint_table.items(): text = data['location_fallback'] if hint_type in world.settings.misc_hints: @@ -1761,14 +1764,14 @@ def get_raw_text(string: str) -> str: return text -def hint_dist_files() -> List[str]: +def hint_dist_files() -> list[str]: return [os.path.join(data_path('Hints/'), d) for d in defaultHintDists] + [ os.path.join(data_path('Hints/'), d) for d in sorted(os.listdir(data_path('Hints/'))) if d.endswith('.json') and d not in defaultHintDists] -def hint_dist_list() -> Dict[str, str]: +def hint_dist_list() -> dict[str, str]: dists = {} for d in hint_dist_files(): with open(d, 'r') as dist_file: diff --git a/IconManip.py b/IconManip.py index 63fffb359..a50515169 100644 --- a/IconManip.py +++ b/IconManip.py @@ -1,5 +1,7 @@ +from __future__ import annotations import sys -from typing import TYPE_CHECKING, Sequence, MutableSequence, Optional +from collections.abc import Sequence, MutableSequence +from typing import TYPE_CHECKING, Optional from Utils import data_path @@ -11,7 +13,7 @@ if TYPE_CHECKING: from Rom import Rom -RGBValues: TypeAlias = MutableSequence[MutableSequence[int]] +RGBValues: TypeAlias = "MutableSequence[MutableSequence[int]]" # TODO @@ -140,7 +142,7 @@ def rgb_to_rgb5a1(rgb_values: RGBValues) -> bytes: # Patch overworld icons -def patch_overworld_icon(rom: "Rom", color: Optional[Sequence[int]], address: int, filename: Optional[str] = None) -> None: +def patch_overworld_icon(rom: Rom, color: Optional[Sequence[int]], address: int, filename: Optional[str] = None) -> None: original = rom.original.read_bytes(address, 0x800) if color is None: diff --git a/Item.py b/Item.py index 0a8992017..4767bc543 100644 --- a/Item.py +++ b/Item.py @@ -1,4 +1,6 @@ -from typing import TYPE_CHECKING, Optional, Tuple, List, Dict, Union, Iterable, Set, Any, Callable, overload +from __future__ import annotations +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Optional, Any, overload from ItemList import item_table from RulesCommon import allowed_globals, escape_name @@ -9,17 +11,17 @@ class ItemInfo: - items: 'Dict[str, ItemInfo]' = {} - events: 'Dict[str, ItemInfo]' = {} - bottles: Set[str] = set() - medallions: Set[str] = set() - stones: Set[str] = set() - junk_weight: Dict[str, int] = {} - - solver_ids: Dict[str, int] = {} - bottle_ids: Set[int] = set() - medallion_ids: Set[int] = set() - stone_ids: Set[int] = set() + items: dict[str, ItemInfo] = {} + events: dict[str, ItemInfo] = {} + bottles: set[str] = set() + medallions: set[str] = set() + stones: set[str] = set() + junk_weight: dict[str, int] = {} + + solver_ids: dict[str, int] = {} + bottle_ids: set[int] = set() + medallion_ids: set[int] = set() + stone_ids: set[int] = set() def __init__(self, name: str = '', event: bool = False) -> None: if event: @@ -34,13 +36,13 @@ def __init__(self, name: str = '', event: bool = False) -> None: self.advancement: bool = (progressive is True) self.priority: bool = (progressive is False) self.type: str = item_type - self.special: Dict[str, Any] = special or {} + self.special: dict[str, Any] = special or {} self.index: Optional[int] = item_id self.price: Optional[int] = self.special.get('price', None) self.bottle: bool = self.special.get('bottle', False) self.medallion: bool = self.special.get('medallion', False) self.stone: bool = self.special.get('stone', False) - self.alias: Optional[Tuple[str, int]] = self.special.get('alias', None) + self.alias: Optional[tuple[str, int]] = self.special.get('alias', None) self.junk: Optional[int] = self.special.get('junk', None) self.trade: bool = self.special.get('trade', False) @@ -68,9 +70,9 @@ def __init__(self, name: str = '', event: bool = False) -> None: class Item: - def __init__(self, name: str = '', world: "Optional[World]" = None, event: bool = False) -> None: + def __init__(self, name: str = '', world: Optional[World] = None, event: bool = False) -> None: self.name: str = name - self.location: "Optional[Location]" = None + self.location: Optional[Location] = None self.event: bool = event if event: if name not in ItemInfo.events: @@ -79,22 +81,22 @@ def __init__(self, name: str = '', world: "Optional[World]" = None, event: bool else: self.info: ItemInfo = ItemInfo.items[name] self.price: Optional[int] = self.info.special.get('price', None) - self.world: "Optional[World]" = world - self.looks_like_item: 'Optional[Item]' = None + self.world: Optional[World] = world + self.looks_like_item: Optional[Item] = None self.advancement: bool = self.info.advancement self.priority: bool = self.info.priority self.type: str = self.info.type self.special: dict = self.info.special self.index: Optional[int] = self.info.index - self.alias: Optional[Tuple[str, int]] = self.info.alias + self.alias: Optional[tuple[str, int]] = self.info.alias self.solver_id: Optional[int] = self.info.solver_id # Do not alias to junk--it has no solver id! self.alias_id: Optional[int] = ItemInfo.solver_ids[escape_name(self.alias[0])] if self.alias else None - item_worlds_to_fix: 'Dict[Item, int]' = {} + item_worlds_to_fix: dict[Item, int] = {} - def copy(self, new_world: "Optional[World]" = None) -> 'Item': + def copy(self, new_world: Optional[World] = None) -> Item: if new_world is not None and self.world is not None and new_world.id != self.world.id: new_world = None @@ -107,7 +109,7 @@ def copy(self, new_world: "Optional[World]" = None) -> 'Item': return new_item @classmethod - def fix_worlds_after_copy(cls, worlds: "List[World]") -> None: + def fix_worlds_after_copy(cls, worlds: list[World]) -> None: items_fixed = [] for item, world_id in cls.item_worlds_to_fix.items(): item.world = worlds[world_id] @@ -200,14 +202,16 @@ def __unicode__(self) -> str: @overload -def ItemFactory(items: str, world: "Optional[World]" = None, event: bool = False) -> Item: +def ItemFactory(items: str, world: Optional[World] = None, event: bool = False) -> Item: pass + @overload -def ItemFactory(items: Iterable[str], world: "Optional[World]" = None, event: bool = False) -> List[Item]: +def ItemFactory(items: Iterable[str], world: Optional[World] = None, event: bool = False) -> list[Item]: pass -def ItemFactory(items: Union[str, Iterable[str]], world: "Optional[World]" = None, event: bool = False) -> Union[Item, List[Item]]: + +def ItemFactory(items: str | Iterable[str], world: Optional[World] = None, event: bool = False) -> Item | list[Item]: if isinstance(items, str): if not event and items not in ItemInfo.items: raise KeyError('Unknown Item: %s' % items) @@ -222,7 +226,7 @@ def ItemFactory(items: Union[str, Iterable[str]], world: "Optional[World]" = Non return ret -def make_event_item(name: str, location: "Location", item: Optional[Item] = None) -> Item: +def make_event_item(name: str, location: Location, item: Optional[Item] = None) -> Item: if location.world is None: raise Exception(f"`make_event_item` called with location '{location.name}' that doesn't have a world.") if item is None: @@ -239,7 +243,7 @@ def is_item(name: str) -> bool: return name in item_table -def ItemIterator(predicate: Callable[[Item], bool] = lambda item: True, world: "Optional[World]" = None) -> Iterable[Item]: +def ItemIterator(predicate: Callable[[Item], bool] = lambda item: True, world: Optional[World] = None) -> Iterable[Item]: for item_name in item_table: item = ItemFactory(item_name, world) if predicate(item): diff --git a/ItemList.py b/ItemList.py index 73ac0062a..b70338a99 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1,10 +1,11 @@ -from typing import Dict, Tuple, Optional, Any +from __future__ import annotations +from typing import Optional, Any # Progressive: True -> Advancement # False -> Priority # None -> Normal # Item: (type, Progressive, GetItemID, special), -item_table: Dict[str, Tuple[str, Optional[bool], Optional[int], Optional[Dict[str, Any]]]] = { +item_table: dict[str, tuple[str, Optional[bool], Optional[int], Optional[dict[str, Any]]]] = { 'Bombs (5)': ('Item', None, 0x0001, {'junk': 8}), 'Deku Nuts (5)': ('Item', None, 0x0002, {'junk': 5}), 'Bombchus (10)': ('Item', True, 0x0003, None), diff --git a/ItemPool.py b/ItemPool.py index e39415bb6..f0cd6babb 100644 --- a/ItemPool.py +++ b/ItemPool.py @@ -1,6 +1,8 @@ +from __future__ import annotations import random +from collections.abc import Sequence from decimal import Decimal, ROUND_UP -from typing import TYPE_CHECKING, List, Dict, Tuple, Sequence, Union, Optional +from typing import TYPE_CHECKING, Optional from Item import ItemInfo, ItemFactory from Location import DisableType @@ -10,7 +12,7 @@ from World import World -plentiful_items: List[str] = ([ +plentiful_items: list[str] = ([ 'Biggoron Sword', 'Boomerang', 'Lens of Truth', @@ -40,7 +42,7 @@ # Ludicrous replaces all health upgrades with heart containers # as done in plentiful. The item list is used separately to # dynamically replace all junk with even levels of each item. -ludicrous_health: List[str] = ['Heart Container'] * 8 +ludicrous_health: list[str] = ['Heart Container'] * 8 # List of items that can be multiplied in ludicrous mode. # Used to filter the pre-plando pool for candidates instead @@ -52,7 +54,7 @@ # # Base items will always be candidates to replace junk items, # even if the player starts with all "normal" copies of an item. -ludicrous_items_base: List[str] = [ +ludicrous_items_base: list[str] = [ 'Light Arrows', 'Megaton Hammer', 'Progressive Hookshot', @@ -82,7 +84,7 @@ 'Deku Nut Capacity' ] -ludicrous_items_extended: List[str] = [ +ludicrous_items_extended: list[str] = [ 'Zeldas Lullaby', 'Eponas Song', 'Suns Song', @@ -192,7 +194,7 @@ 'Silver Rupee Pouch (Ganons Castle Forest Trial)', ] -ludicrous_exclusions: List[str] = [ +ludicrous_exclusions: list[str] = [ 'Triforce Piece', 'Gold Skulltula Token', 'Rutos Letter', @@ -201,7 +203,7 @@ 'Piece of Heart (Treasure Chest Game)' ] -item_difficulty_max: Dict[str, Dict[str, int]] = { +item_difficulty_max: dict[str, dict[str, int]] = { 'ludicrous': { 'Piece of Heart': 3, }, @@ -239,13 +241,13 @@ }, } -shopsanity_rupees: List[str] = ( +shopsanity_rupees: list[str] = ( ['Rupees (20)'] * 5 + ['Rupees (50)'] * 3 + ['Rupees (200)'] * 2 ) -min_shop_items: List[str] = ( +min_shop_items: list[str] = ( ['Buy Deku Shield'] + ['Buy Hylian Shield'] + ['Buy Goron Tunic'] + @@ -264,7 +266,7 @@ ['Buy Fish'] ) -deku_scrubs_items: Dict[str, Union[str, List[Tuple[str, int]]]] = { +deku_scrubs_items: dict[str, str | list[tuple[str, int]]] = { 'Buy Deku Shield': 'Deku Shield', 'Buy Deku Nut (5)': 'Deku Nuts (5)', 'Buy Deku Stick (1)': 'Deku Stick (1)', @@ -275,7 +277,7 @@ 'Buy Deku Seeds (30)': [('Arrows (30)', 3), ('Deku Seeds (30)', 1)], } -trade_items: Tuple[str, ...] = ( +trade_items: tuple[str, ...] = ( "Pocket Egg", "Pocket Cucco", "Cojiro", @@ -289,7 +291,7 @@ "Claim Check", ) -child_trade_items: Tuple[str, ...] = ( +child_trade_items: tuple[str, ...] = ( "Weird Egg", "Chicken", "Zeldas Letter", @@ -303,12 +305,12 @@ "Mask of Truth", ) -normal_bottles: List[str] = [bottle for bottle in sorted(ItemInfo.bottles) if bottle not in ['Deliver Letter', 'Sell Big Poe']] + ['Bottle with Big Poe'] -song_list: List[str] = [item.name for item in sorted([i for n, i in ItemInfo.items.items() if i.type == 'Song'], key=lambda x: x.index if x.index is not None else 0)] -junk_pool_base: List[Tuple[str, int]] = [(item, weight) for (item, weight) in sorted(ItemInfo.junk_weight.items()) if weight > 0] -remove_junk_items: List[str] = [item for (item, weight) in sorted(ItemInfo.junk_weight.items()) if weight >= 0] +normal_bottles: list[str] = [bottle for bottle in sorted(ItemInfo.bottles) if bottle not in ['Deliver Letter', 'Sell Big Poe']] + ['Bottle with Big Poe'] +song_list: list[str] = [item.name for item in sorted([i for n, i in ItemInfo.items.items() if i.type == 'Song'], key=lambda x: x.index if x.index is not None else 0)] +junk_pool_base: list[tuple[str, int]] = [(item, weight) for (item, weight) in sorted(ItemInfo.junk_weight.items()) if weight > 0] +remove_junk_items: list[str] = [item for (item, weight) in sorted(ItemInfo.junk_weight.items()) if weight >= 0] -remove_junk_ludicrous_items: List[str] = [ +remove_junk_ludicrous_items: list[str] = [ 'Ice Arrows', 'Deku Nut Capacity', 'Deku Stick Capacity', @@ -320,10 +322,10 @@ # (e.g. HC Malon Egg with Skip Child Zelda, or the carpenters with Open Gerudo Fortress) IGNORE_LOCATION: str = 'Recovery Heart' -pending_junk_pool: List[str] = [] -junk_pool: List[Tuple[str, int]] = [] +pending_junk_pool: list[str] = [] +junk_pool: list[tuple[str, int]] = [] -exclude_from_major: List[str] = [ +exclude_from_major: list[str] = [ 'Deliver Letter', 'Sell Big Poe', 'Magic Bean', @@ -339,7 +341,7 @@ 'Piece of Heart (Treasure Chest Game)', ] -item_groups: Dict[str, Sequence[str]] = { +item_groups: dict[str, Sequence[str]] = { 'Junk': remove_junk_items, 'JunkSong': ('Prelude of Light', 'Serenade of Water'), 'AdultTrade': trade_items, @@ -364,7 +366,7 @@ } -def get_junk_item(count: int = 1, pool: Optional[List[str]] = None, plando_pool: "Optional[Dict[str, ItemPoolRecord]]" = None) -> List[str]: +def get_junk_item(count: int = 1, pool: Optional[list[str]] = None, plando_pool: Optional[dict[str, ItemPoolRecord]] = None) -> list[str]: if count < 1: raise ValueError("get_junk_item argument 'count' must be greater than 0.") @@ -386,7 +388,7 @@ def get_junk_item(count: int = 1, pool: Optional[List[str]] = None, plando_pool: return return_pool -def replace_max_item(items: List[str], item: str, max_count: int) -> None: +def replace_max_item(items: list[str], item: str, max_count: int) -> None: count = 0 for i, val in enumerate(items): if val == item: @@ -395,7 +397,7 @@ def replace_max_item(items: List[str], item: str, max_count: int) -> None: count += 1 -def generate_itempool(world: "World") -> None: +def generate_itempool(world: World) -> None: junk_pool[:] = list(junk_pool_base) if world.settings.junk_ice_traps == 'on': junk_pool.append(('Ice Trap', 10)) @@ -424,7 +426,7 @@ def generate_itempool(world: "World") -> None: raise ValueError(f"Not enough available Gold Skulltula Tokens to meet requirements. Available: {world.available_tokens}, Required: {world.max_progressions['Gold Skulltula Token']}.") -def get_pool_core(world: "World") -> Tuple[List[str], Dict[str, str]]: +def get_pool_core(world: World) -> tuple[list[str], dict[str, str]]: pool = [] placed_items = {} remain_shop_items = [] diff --git a/JSONDump.py b/JSONDump.py index a015c244a..0c04c5f04 100644 --- a/JSONDump.py +++ b/JSONDump.py @@ -1,6 +1,8 @@ +from __future__ import annotations import json +from collections.abc import Sequence from functools import reduce -from typing import Optional, Tuple +from typing import Optional INDENT = ' ' @@ -73,7 +75,7 @@ def get_keys(obj: AlignedDict, depth: int): yield from get_keys(value, depth - 1) -def dump_dict(obj: dict, current_indent: str = '', sub_width: Optional[Tuple[int, int]] = None, ensure_ascii: bool = False) -> str: +def dump_dict(obj: dict, current_indent: str = '', sub_width: Optional[Sequence[int, int]] = None, ensure_ascii: bool = False) -> str: entries = [] key_width = None @@ -120,7 +122,7 @@ def dump_dict(obj: dict, current_indent: str = '', sub_width: Optional[Tuple[int return output -def dump_obj(obj, current_indent: str = '', sub_width: Optional[Tuple[int, int]] = None, ensure_ascii: bool = False) -> str: +def dump_obj(obj, current_indent: str = '', sub_width: Optional[Sequence[int, int]] = None, ensure_ascii: bool = False) -> str: if is_list(obj): return dump_list(obj, current_indent, ensure_ascii) elif is_dict(obj): diff --git a/Location.py b/Location.py index e2ba0e147..dbc1de162 100644 --- a/Location.py +++ b/Location.py @@ -1,15 +1,17 @@ +from __future__ import annotations import logging +from collections.abc import Callable, Iterable from enum import Enum -from typing import TYPE_CHECKING, Optional, List, Tuple, Callable, Union, Iterable, overload +from typing import TYPE_CHECKING, Optional, overload from HintList import misc_item_hint_table, misc_location_hint_table from LocationList import location_table, location_is_viewable, LocationAddress, LocationDefault, LocationFilterTags -from RulesCommon import AccessRule if TYPE_CHECKING: from Dungeon import Dungeon from Item import Item from Region import Region + from RulesCommon import AccessRule from State import State from World import World @@ -22,11 +24,11 @@ class DisableType(Enum): class Location: def __init__(self, name: str = '', address: LocationAddress = None, address2: LocationAddress = None, default: LocationDefault = None, - location_type: str = 'Chest', scene: Optional[int] = None, parent: "Optional[Region]" = None, + location_type: str = 'Chest', scene: Optional[int] = None, parent: Optional[Region] = None, filter_tags: LocationFilterTags = None, internal: bool = False, vanilla_item: Optional[str] = None) -> None: self.name: str = name - self.parent_region: "Optional[Region]" = parent - self.item: "Optional[Item]" = None + self.parent_region: Optional[Region] = parent + self.item: Optional[Item] = None self.vanilla_item: Optional[str] = vanilla_item self.address: LocationAddress = address self.address2: LocationAddress = address2 @@ -36,19 +38,19 @@ def __init__(self, name: str = '', address: LocationAddress = None, address2: Lo self.internal: bool = internal self.staleness_count: int = 0 self.access_rule: AccessRule = lambda state, **kwargs: True - self.access_rules: List[AccessRule] = [] - self.item_rule: "Callable[[Location, Item], bool]" = lambda location, item: True + self.access_rules: list[AccessRule] = [] + self.item_rule: Callable[[Location, Item], bool] = lambda location, item: True self.locked: bool = False self.price: Optional[int] = None self.minor_only: bool = False - self.world: "Optional[World]" = None + self.world: Optional[World] = None self.disabled: DisableType = DisableType.ENABLED self.always: bool = False self.never: bool = False - self.filter_tags: Optional[Tuple[str, ...]] = (filter_tags,) if isinstance(filter_tags, str) else filter_tags + self.filter_tags: Optional[tuple[str, ...]] = (filter_tags,) if isinstance(filter_tags, str) else filter_tags self.rule_string: Optional[str] = None - def copy(self, new_region: "Region") -> 'Location': + def copy(self, new_region: Region) -> Location: new_location = Location(self.name, self.address, self.address2, self.default, self.type, self.scene, new_region, self.filter_tags, self.internal, self.vanilla_item) new_location.world = new_region.world @@ -67,7 +69,7 @@ def copy(self, new_region: "Region") -> 'Location': return new_location @property - def dungeon(self) -> "Optional[Dungeon]": + def dungeon(self) -> Optional[Dungeon]: return self.parent_region.dungeon if self.parent_region is not None else None def add_rule(self, lambda_rule: AccessRule) -> None: @@ -80,7 +82,6 @@ def add_rule(self, lambda_rule: AccessRule) -> None: self.access_rules.append(lambda_rule) self.access_rule = self._run_rules - def _run_rules(self, state, **kwargs): for rule in self.access_rules: if not rule(state, **kwargs): @@ -91,7 +92,7 @@ def set_rule(self, lambda_rule: AccessRule) -> None: self.access_rule = lambda_rule self.access_rules = [lambda_rule] - def can_fill(self, state: "State", item: "Item", check_access: bool = True) -> bool: + def can_fill(self, state: State, item: Item, check_access: bool = True) -> bool: if state.search is None: return False if self.minor_only and item.majoritem: @@ -102,7 +103,7 @@ def can_fill(self, state: "State", item: "Item", check_access: bool = True) -> b (not check_access or state.search.spot_access(self, 'either')) ) - def can_fill_fast(self, item: "Item", manual: bool = False) -> bool: + def can_fill_fast(self, item: Item, manual: bool = False) -> bool: if self.parent_region is None: return False return self.parent_region.can_fill(item, manual) and self.item_rule(self, item) @@ -157,11 +158,13 @@ def __unicode__(self) -> str: def LocationFactory(locations: str) -> Location: pass + @overload -def LocationFactory(locations: List[str]) -> List[Location]: +def LocationFactory(locations: list[str]) -> list[Location]: pass -def LocationFactory(locations: Union[str, List[str]]) -> Union[Location, List[Location]]: + +def LocationFactory(locations: str | list[str]) -> Location | list[Location]: ret = [] singleton = False if isinstance(locations, str): diff --git a/LocationList.py b/LocationList.py index f4ff2f867..363a25b0f 100644 --- a/LocationList.py +++ b/LocationList.py @@ -1,16 +1,17 @@ +from __future__ import annotations import sys from collections import OrderedDict -from typing import Dict, Tuple, Optional, Union, List +from typing import Optional if sys.version_info >= (3, 10): from typing import TypeAlias else: TypeAlias = str -LocationDefault: TypeAlias = Optional[Union[int, Tuple[int, ...], List[Tuple[int, ...]]]] -LocationAddress: TypeAlias = Optional[Union[int, List[int]]] -LocationAddresses: TypeAlias = Optional[Tuple[LocationAddress, LocationAddress]] -LocationFilterTags: TypeAlias = Optional[Union[Tuple[str, ...], str]] +LocationDefault: TypeAlias = "Optional[int | tuple[int, ...] | list[tuple[int, ...]]]" +LocationAddress: TypeAlias = "Optional[int | list[int]]" +LocationAddresses: TypeAlias = "Optional[tuple[LocationAddress, LocationAddress]]" +LocationFilterTags: TypeAlias = "Optional[tuple[str, ...] | str]" def shop_address(shop_id: int, shelf_id: int) -> int: @@ -56,7 +57,7 @@ def shop_address(shop_id: int, shelf_id: int) -> int: # Note: for ActorOverride locations, the "Addresses" variable is in the form ([addresses], [bytes]) where addresses is a list of memory locations in ROM to be updated, and bytes is the data that will be written to that location # Location: Type Scene Default Addresses Vanilla Item Categories -location_table: Dict[str, Tuple[str, Optional[int], LocationDefault, LocationAddresses, Optional[str], LocationFilterTags]] = OrderedDict([ +location_table: dict[str, tuple[str, Optional[int], LocationDefault, LocationAddresses, Optional[str], LocationFilterTags]] = OrderedDict([ ## Dungeon Rewards ("Links Pocket", ("Boss", None, None, None, 'Light Medallion', None)), ("Queen Gohma", ("Boss", None, 0x6C, (0x0CA315F, 0x2079571), 'Kokiri Emerald', None)), @@ -2260,12 +2261,12 @@ def shop_address(shop_id: int, shelf_id: int) -> int: ("Ganondorf Hint", ("Hint", None, None, None, None, None)), ]) -location_sort_order: Dict[str, int] = { +location_sort_order: dict[str, int] = { loc: i for i, loc in enumerate(location_table.keys()) } # Business Scrub Details -business_scrubs: List[Tuple[int, int, int, List[str]]] = [ +business_scrubs: list[tuple[int, int, int, list[str]]] = [ # id price text text replacement (0x30, 20, 0x10A0, ["Deku Nuts", "a \x05\x42mysterious item\x05\x40"]), (0x31, 15, 0x10A1, ["Deku Sticks", "a \x05\x42mysterious item\x05\x40"]), @@ -2280,8 +2281,8 @@ def shop_address(shop_id: int, shelf_id: int) -> int: (0x79, 40, 0x10DD, ["enable you to pick up more \x05\x41Deku\x01Nuts", "sell you a \x05\x42mysterious item"]), ] -dungeons: Tuple[str, ...] = ('Deku Tree', "Dodongo's Cavern", "Jabu Jabu's Belly", 'Forest Temple', 'Fire Temple', 'Water Temple', 'Spirit Temple', 'Shadow Temple', 'Ice Cavern', 'Bottom of the Well', 'Gerudo Training Ground', "Ganon's Castle") -location_groups: Dict[str, List[str]] = { +dungeons: tuple[str, ...] = ('Deku Tree', "Dodongo's Cavern", "Jabu Jabu's Belly", 'Forest Temple', 'Fire Temple', 'Water Temple', 'Spirit Temple', 'Shadow Temple', 'Ice Cavern', 'Bottom of the Well', 'Gerudo Training Ground', "Ganon's Castle") +location_groups: dict[str, list[str]] = { 'Song': [name for (name, data) in location_table.items() if data[0] == 'Song'], 'Chest': [name for (name, data) in location_table.items() if data[0] == 'Chest'], 'Collectable': [name for (name, data) in location_table.items() if data[0] == 'Collectable'], diff --git a/MBSDIFFPatch.py b/MBSDIFFPatch.py index 3e3a4f791..650696055 100644 --- a/MBSDIFFPatch.py +++ b/MBSDIFFPatch.py @@ -1,10 +1,10 @@ +from __future__ import annotations import logging import gzip import os import platform import shutil -import struct -from typing import Optional, MutableSequence +from typing import Optional from Rom import Rom from Utils import default_output_path, is_bundled, local_path, run_process @@ -99,7 +99,7 @@ def apply_minibsdiff_patch_file(rom: Rom, file: str) -> None: if size_difference > 0: rom.append_bytes([0] * size_difference) - ctrl_block: MutableSequence[int] = [0] * 3 + ctrl_block: list[int] = [0] * 3 ctrl_block_address: int = 32 diff_block_address: int = ctrl_block_address + ctrl_len extra_block_address: int = diff_block_address + data_len diff --git a/MQ.py b/MQ.py index 684117d40..56572a54f 100644 --- a/MQ.py +++ b/MQ.py @@ -43,9 +43,10 @@ # the floor map data is missing a vertex pointer that would point within kaleido_scope. # As such, if the file moves, the patch will break. +from __future__ import annotations import json from struct import pack, unpack -from typing import Dict, List, Tuple, Optional, Union, Any +from typing import Optional, Any from Rom import Rom from Utils import data_path @@ -65,7 +66,7 @@ def __init__(self, name: str, start: int = 0, end: Optional[int] = None, remap: self.dma_key: int = self.start @classmethod - def from_json(cls, file: Dict[str, Optional[str]]) -> 'File': + def from_json(cls, file: dict[str, Optional[str]]) -> File: return cls( file['Name'], int(file['Start'], 16) if file.get('Start', None) is not None else 0, @@ -113,18 +114,18 @@ def write_to_scene(self, rom: Rom, start: int) -> None: class ColDelta: - def __init__(self, delta: Dict[str, Union[bool, List[Dict[str, int]]]]) -> None: + def __init__(self, delta: dict[str, bool | list[dict[str, int]]]) -> None: self.is_larger: bool = delta['IsLarger'] - self.polys: List[Dict[str, int]] = delta['Polys'] - self.polytypes: List[Dict[str, int]] = delta['PolyTypes'] - self.cams: List[Dict[str, int]] = delta['Cams'] + self.polys: list[dict[str, int]] = delta['Polys'] + self.polytypes: list[dict[str, int]] = delta['PolyTypes'] + self.cams: list[dict[str, int]] = delta['Cams'] class Icon: - def __init__(self, data: Dict[str, Union[int, List[Dict[str, int]]]]) -> None: + def __init__(self, data: dict[str, int | list[dict[str, int]]]) -> None: self.icon: int = data["Icon"] self.count: int = data["Count"] - self.points: List[IconPoint] = [IconPoint(x) for x in data["IconPoints"]] + self.points: list[IconPoint] = [IconPoint(x) for x in data["IconPoints"]] def write_to_minimap(self, rom: Rom, addr: int) -> None: rom.write_sbyte(addr, self.icon) @@ -145,7 +146,7 @@ def write_to_floormap(self, rom: Rom, addr: int) -> None: class IconPoint: - def __init__(self, point: Dict[str, int]) -> None: + def __init__(self, point: dict[str, int]) -> None: self.flag = point["Flag"] self.x = point["x"] self.y = point["y"] @@ -162,15 +163,15 @@ def write_to_floormap(self, rom: Rom, addr: int) -> None: class Scene: - def __init__(self, scene: Dict[str, Any]) -> None: + def __init__(self, scene: dict[str, Any]) -> None: self.file: File = File.from_json(scene['File']) self.id: int = scene['Id'] - self.transition_actors: List[List[int]] = [convert_actor_data(x) for x in scene['TActors']] - self.rooms: List[Room] = [Room(x) for x in scene['Rooms']] - self.paths: List[List[List[int]]] = [] + self.transition_actors: list[list[int]] = [convert_actor_data(x) for x in scene['TActors']] + self.rooms: list[Room] = [Room(x) for x in scene['Rooms']] + self.paths: list[list[list[int]]] = [] self.coldelta: ColDelta = ColDelta(scene["ColDelta"]) - self.minimaps: List[List[Icon]] = [[Icon(icon) for icon in minimap['Icons']] for minimap in scene['Minimaps']] - self.floormaps: List[List[Icon]] = [[Icon(icon) for icon in floormap['Icons']] for floormap in scene['Floormaps']] + self.minimaps: list[list[Icon]] = [[Icon(icon) for icon in minimap['Icons']] for minimap in scene['Minimaps']] + self.floormaps: list[list[Icon]] = [[Icon(icon) for icon in floormap['Icons']] for floormap in scene['Floormaps']] temp_paths = scene['Paths'] for item in temp_paths: self.paths.append(item['Points']) @@ -332,7 +333,7 @@ def patch_mesh(self, rom: Rom, mesh: CollisionMesh) -> None: mesh.write_to_scene(rom, self.file.start) @staticmethod - def write_cam_data(rom: Rom, addr: int, cam_data: List[Tuple[int, int]]) -> None: + def write_cam_data(rom: Rom, addr: int, cam_data: list[tuple[int, int]]) -> None: for item in cam_data: data, pos = item rom.write_int32s(addr, [data, pos]) @@ -367,11 +368,11 @@ def append_path_data(self, rom: Rom) -> int: class Room: - def __init__(self, room: Dict[str, Union[int, List[str], Dict[str, Optional[str]]]]): + def __init__(self, room: dict[str, int | list[str] | dict[str, Optional[str]]]): self.file: File = File.from_json(room['File']) self.id: int = room['Id'] - self.objects: List[int] = [int(x, 16) for x in room['Objects']] - self.actors: List[List[int]] = [convert_actor_data(x) for x in room['Actors']] + self.objects: list[int] = [int(x, 16) for x in room['Objects']] + self.actors: list[list[int]] = [convert_actor_data(x) for x in room['Actors']] def write_data(self, rom: Rom) -> None: # move file to remap address @@ -407,7 +408,7 @@ def write_data(self, rom: Rom) -> None: self.file.end = align16(self.file.end) update_dmadata(rom, self.file) - def append_object_data(self, rom: Rom, objects: List[int]) -> int: + def append_object_data(self, rom: Rom, objects: list[int]) -> int: offset = self.file.end - self.file.start cur = self.file.end rom.write_int16s(cur, objects) @@ -417,7 +418,7 @@ def append_object_data(self, rom: Rom, objects: List[int]) -> int: return offset -def patch_files(rom: Rom, mq_scenes: List[int]) -> None: +def patch_files(rom: Rom, mq_scenes: list[int]) -> None: data = get_json() scenes = [Scene(x) for x in data] for scene in scenes: @@ -433,7 +434,7 @@ def get_json() -> Any: return data -def convert_actor_data(string: str) -> List[int]: +def convert_actor_data(string: str) -> list[int]: spawn_args = string.split(" ") return [ int(x,16) for x in spawn_args ] @@ -505,7 +506,7 @@ def patch_spirit_temple_mq_room_6(rom: Rom, room_addr: int) -> None: rom.write_int32s(room_addr, [0x18000000, seg]) -def verify_remap(scenes: List[Scene]) -> None: +def verify_remap(scenes: list[Scene]) -> None: def test_remap(file: File) -> bool: if file.remap is not None: if file.start < file.remap: @@ -535,7 +536,7 @@ def update_scene_table(rom: Rom, scene_id: int, start: int, end: int) -> None: rom.write_int32s(cur, [start, end]) -def write_actor_data(rom: Rom, cur: int, actors: List[List[int]]) -> None: +def write_actor_data(rom: Rom, cur: int, actors: list[list[int]]) -> None: for actor in actors: rom.write_int16s(cur, actor) cur += 0x10 @@ -662,7 +663,7 @@ def insert_space(rom: Rom, file: File, vram_start: int, insert_section: int, ins file.end += insert_size -def add_relocations(rom: Rom, file: File, addresses: List[Union[int, Tuple[int, int]]]) -> None: +def add_relocations(rom: Rom, file: File, addresses: list[int | tuple[int, int]]) -> None: relocations = [] sections = [] header_size = rom.read_int32(file.end - 4) diff --git a/Main.py b/Main.py index f339875d4..7ac1c4da4 100644 --- a/Main.py +++ b/Main.py @@ -1,3 +1,4 @@ +from __future__ import annotations import copy import hashlib import logging @@ -7,7 +8,7 @@ import shutil import time import zipfile -from typing import List, Optional +from typing import Optional from Cosmetics import CosmeticsLog, patch_cosmetics @@ -119,7 +120,7 @@ def generate(settings: Settings) -> Spoiler: return make_spoiler(settings, worlds) -def build_world_graphs(settings: Settings) -> List[World]: +def build_world_graphs(settings: Settings) -> list[World]: logger = logging.getLogger('') worlds = [] for i in range(0, settings.world_count): @@ -165,13 +166,13 @@ def build_world_graphs(settings: Settings) -> List[World]: return worlds -def place_items(worlds: List[World]) -> None: +def place_items(worlds: list[World]) -> None: logger = logging.getLogger('') logger.info('Fill the world.') distribute_items_restrictive(worlds) -def make_spoiler(settings: Settings, worlds: List[World]) -> Spoiler: +def make_spoiler(settings: Settings, worlds: list[World]) -> Spoiler: logger = logging.getLogger('') spoiler = Spoiler(worlds) if settings.create_spoiler: diff --git a/Messages.py b/Messages.py index b593bee46..54c79e8f8 100644 --- a/Messages.py +++ b/Messages.py @@ -1,7 +1,9 @@ # text details: https://wiki.cloudmodding.com/oot/Text_Format +from __future__ import annotations import random -from typing import TYPE_CHECKING, Dict, List, Tuple, Callable, Any, Union, Optional, Iterable, Set +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Optional, Any from HintList import misc_item_hint_table, misc_location_hint_table from TextBox import line_wrap @@ -23,14 +25,14 @@ JPN_TABLE_SIZE: int = ENG_TABLE_START - JPN_TABLE_START ENG_TABLE_SIZE: int = CREDITS_TABLE_START - ENG_TABLE_START -EXTENDED_TABLE_START: int = JPN_TABLE_START # start writing entries to the jp table instead of english for more space -EXTENDED_TABLE_SIZE: int = JPN_TABLE_SIZE + ENG_TABLE_SIZE # 0x8360 bytes, 4204 entries +EXTENDED_TABLE_START: int = JPN_TABLE_START # start writing entries to the jp table instead of english for more space +EXTENDED_TABLE_SIZE: int = JPN_TABLE_SIZE + ENG_TABLE_SIZE # 0x8360 bytes, 4204 entries -EXTENDED_TEXT_START: int = JPN_TABLE_START # start writing text to the jp table instead of english for more space -EXTENDED_TEXT_SIZE_LIMIT: int = JPN_TEXT_SIZE_LIMIT + ENG_TEXT_SIZE_LIMIT # 0x74000 bytes +EXTENDED_TEXT_START: int = JPN_TABLE_START # start writing text to the jp table instead of english for more space +EXTENDED_TEXT_SIZE_LIMIT: int = JPN_TEXT_SIZE_LIMIT + ENG_TEXT_SIZE_LIMIT # 0x74000 bytes # name of type, followed by number of additional bytes to read, follwed by a function that prints the code -CONTROL_CODES: Dict[int, Tuple[str, int, Callable[[Any], str]]] = { +CONTROL_CODES: dict[int, tuple[str, int, Callable[[Any], str]]] = { 0x00: ('pad', 0, lambda _: '' ), 0x01: ('line-break', 0, lambda _: '\n' ), 0x02: ('end', 0, lambda _: '' ), @@ -65,7 +67,7 @@ } # Maps unicode characters to corresponding bytes in OOTR's character set. -CHARACTER_MAP: Dict[str, int] = { +CHARACTER_MAP: dict[str, int] = { 'â’¶': 0x9F, 'â’·': 0xA0, 'â’¸': 0xA1, @@ -96,7 +98,7 @@ start=0x7f )) -SPECIAL_CHARACTERS: Dict[int, str] = { +SPECIAL_CHARACTERS: dict[int, str] = { 0x9F: '[A]', 0xA0: '[B]', 0xA1: '[C]', @@ -111,22 +113,22 @@ 0xAA: '[Control Stick]', } -REVERSE_MAP: List[str] = list(chr(x) for x in range(256)) +REVERSE_MAP: list[str] = list(chr(x) for x in range(256)) for char, byte in CHARACTER_MAP.items(): SPECIAL_CHARACTERS.setdefault(byte, char) REVERSE_MAP[byte] = char # [0x0500,0x0560] (inclusive) are reserved for plandomakers -GOSSIP_STONE_MESSAGES: List[int] = list(range(0x0401, 0x04FF)) # ids of the actual hints +GOSSIP_STONE_MESSAGES: list[int] = list(range(0x0401, 0x04FF)) # ids of the actual hints GOSSIP_STONE_MESSAGES += [0x2053, 0x2054] # shared initial stone messages -TEMPLE_HINTS_MESSAGES: List[int] = [0x7057, 0x707A] # dungeon reward hints from the temple of time pedestal -GS_TOKEN_MESSAGES: List[int] = [0x00B4, 0x00B5] # Get Gold Skulltula Token messages +TEMPLE_HINTS_MESSAGES: list[int] = [0x7057, 0x707A] # dungeon reward hints from the temple of time pedestal +GS_TOKEN_MESSAGES: list[int] = [0x00B4, 0x00B5] # Get Gold Skulltula Token messages ERROR_MESSAGE: int = 0x0001 # messages for shorter item messages # ids are in the space freed up by move_shop_item_messages() -ITEM_MESSAGES: List[Tuple[int, str]] = [ +ITEM_MESSAGES: list[tuple[int, str]] = [ (0x0001, "\x08\x06\x30\x05\x41TEXT ID ERROR!\x05\x40"), (0x9001, "\x08\x13\x2DYou borrowed a \x05\x41Pocket Egg\x05\x40!\x01A Pocket Cucco will hatch from\x01it overnight. Be sure to give it\x01back."), (0x0002, "\x08\x13\x2FYou returned the Pocket Cucco\x01and got \x05\x41Cojiro\x05\x40 in return!\x01Unlike other Cuccos, Cojiro\x01rarely crows."), @@ -273,7 +275,7 @@ (0x901A, "\x08You can't buy Bombchus without a\x01\x05\x41Bombchu Bag\x05\x40!") ] -KEYSANITY_MESSAGES: List[Tuple[int, str]] = [ +KEYSANITY_MESSAGES: list[tuple[int, str]] = [ (0x001C, "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09"), (0x0006, "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09"), (0x001D, "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09"), @@ -485,7 +487,7 @@ KEYSANITY_MESSAGES.append((i, f"\x13\x77\x08You found a \x05\x41Key Ring\x05\x40\x01for {dungeon_name}!\x09\x01It includes the \x05\x41Boss Key\x05\x40!")) i += 1 -COLOR_MAP: Dict[str, str] = { +COLOR_MAP: dict[str, str] = { 'White': '\x40', 'Red': '\x41', 'Green': '\x42', @@ -496,16 +498,16 @@ 'Black': '\x47', } -MISC_MESSAGES: Dict[int, Tuple[Union[str, bytearray], int]] = { +MISC_MESSAGES: dict[int, tuple[str | bytearray, int]] = { 0x507B: (bytearray( b"\x08I tell you, I saw him!\x04" b"\x08I saw the ghostly figure of Damp\x96\x01" b"the gravekeeper sinking into\x01" b"his grave. It looked like he was\x01" b"holding some kind of \x05\x41treasure\x05\x40!\x02" - ), None), + ), 0x00), 0x0422: ("They say that once \x05\x41Morpha's Curse\x05\x40\x01is lifted, striking \x05\x42this stone\x05\x40 can\x01shift the tides of \x05\x44Lake Hylia\x05\x40.\x02", 0x23), - 0x401C: ("Please find my dear \05\x41Princess Ruto\x05\x40\x01immediately... Zora!\x12\x68\x7A", 0x23), + 0x401C: ("Please find my dear \05\x41Princess Ruto\x05\x40\x01immediately... Zora!\x12\x68\x7A", 0x03), 0x9100: ("I am out of goods now.\x01Sorry!\x04The mark that will lead you to\x01the Spirit Temple is the \x05\x41flag on\x01the left \x05\x40outside the shop.\x01Be seeing you!\x02", 0x00), 0x0451: ("\x12\x68\x7AMweep\x07\x04\x52", 0x23), 0x0452: ("\x12\x68\x7AMweep\x07\x04\x53", 0x23), @@ -532,14 +534,14 @@ def int_to_bytes(num: int, width: int, signed: bool = False) -> bytes: return int.to_bytes(num, width, byteorder='big', signed=signed) -def display_code_list(codes: 'List[TextCode]') -> str: +def display_code_list(codes: list[TextCode]) -> str: message = "" for code in codes: message += str(code) return message -def encode_text_string(text: str) -> List[int]: +def encode_text_string(text: str) -> list[int]: result = [] it = iter(text) for ch in it: @@ -560,7 +562,7 @@ def encode_text_string(text: str) -> List[int]: return result -def parse_control_codes(text: Union[List[int], bytearray, str]) -> 'List[TextCode]': +def parse_control_codes(text: list[int] | bytearray | str) -> list[TextCode]: if isinstance(text, list): text_bytes = text elif isinstance(text, bytearray): @@ -643,7 +645,7 @@ def size(self) -> int: return size # writes the code to the given offset, and returns the offset of the next byte - def write(self, rom: "Rom", text_start: int, offset: int) -> int: + def write(self, rom: Rom, text_start: int, offset: int) -> int: rom.write_byte(text_start + offset, self.code) extra_bytes = 0 @@ -659,7 +661,7 @@ def write(self, rom: "Rom", text_start: int, offset: int) -> int: # holds a single message, and all its data class Message: - def __init__(self, raw_text: Union[List[int], bytearray, str], index: int, id: int, opts: int, offset: int, length: int) -> None: + def __init__(self, raw_text: list[int] | bytearray | str, index: int, id: int, opts: int, offset: int, length: int) -> None: if isinstance(raw_text, str): raw_text = bytearray(raw_text, encoding='utf-8') elif not isinstance(raw_text, bytearray): @@ -684,7 +686,7 @@ def __init__(self, raw_text: Union[List[int], bytearray, str], index: int, id: i self.has_three_choice: bool = False self.ending: Optional[TextCode] = None - self.text_codes: List[TextCode] = [] + self.text_codes: list[TextCode] = [] self.text: str = '' self.unpadded_length: int = 0 self.parse_text() @@ -820,7 +822,7 @@ def transform(self, replace_ending: bool = False, ending: Optional[TextCode] = N # writes a Message back into the rom, using the given index and offset to update the table # returns the offset of the next message - def write(self, rom: "Rom", index: int, text_start: int, offset: int, bank: int) -> int: + def write(self, rom: Rom, index: int, text_start: int, offset: int, bank: int) -> int: # construct the table entry id_bytes = int_to_bytes(self.id, 2) offset_bytes = int_to_bytes(offset, 3) @@ -839,7 +841,7 @@ def write(self, rom: "Rom", index: int, text_start: int, offset: int, bank: int) # read a single message from rom @classmethod - def from_rom(cls, rom: "Rom", index: int, eng: bool = True) -> 'Message': + def from_rom(cls, rom: Rom, index: int, eng: bool = True) -> Message: if eng: table_start = ENG_TABLE_START text_start = ENG_TEXT_START @@ -860,12 +862,12 @@ def from_rom(cls, rom: "Rom", index: int, eng: bool = True) -> 'Message': return cls(raw_text, index, id, opts, offset, length) @classmethod - def from_string(cls, text: str, id: int = 0, opts: int = 0x00) -> 'Message': + def from_string(cls, text: str, id: int = 0, opts: int = 0x00) -> Message: bytes = text + "\x02" return cls(bytes, 0, id, opts, 0, len(bytes) + 1) @classmethod - def from_bytearray(cls, text: bytearray, id: int = 0, opts: int = 0x00) -> 'Message': + def from_bytearray(cls, text: bytearray, id: int = 0, opts: int = 0x00) -> Message: bytes = list(text) + [0x02] return cls(bytes, 0, id, opts, 0, len(bytes) + 1) @@ -874,7 +876,7 @@ def from_bytearray(cls, text: bytearray, id: int = 0, opts: int = 0x00) -> 'Mess # wrapper for updating the text of a message, given its message id # if the id does not exist in the list, then it will add it -def update_message_by_id(messages: List[Message], id: int, text: Union[bytearray, str], opts: Optional[int] = None) -> None: +def update_message_by_id(messages: list[Message], id: int, text: bytearray | str, opts: Optional[int] = None) -> None: # get the message index index = next( (m.index for m in messages if m.id == id), -1) # update if it was found @@ -885,7 +887,7 @@ def update_message_by_id(messages: List[Message], id: int, text: Union[bytearray # Gets the message by its ID. Returns None if the index does not exist -def get_message_by_id(messages: List[Message], id: int) -> Optional[Message]: +def get_message_by_id(messages: list[Message], id: int) -> Optional[Message]: # get the message index index = next( (m.index for m in messages if m.id == id), -1) if index >= 0: @@ -895,7 +897,7 @@ def get_message_by_id(messages: List[Message], id: int) -> Optional[Message]: # wrapper for updating the text of a message, given its index in the list -def update_message_by_index(messages: List[Message], index: int, text: Union[bytearray, str], opts: Optional[int] = None) -> None: +def update_message_by_index(messages: list[Message], index: int, text: bytearray | str, opts: Optional[int] = None) -> None: if opts is None: opts = messages[index].opts @@ -907,7 +909,7 @@ def update_message_by_index(messages: List[Message], index: int, text: Union[byt # wrapper for adding a string message to a list of messages -def add_message(messages: List[Message], text: Union[bytearray, str], id: int = 0, opts: int = 0x00) -> None: +def add_message(messages: list[Message], text: bytearray | str, id: int = 0, opts: int = 0x00) -> None: if isinstance(text, bytearray): messages.append(Message.from_bytearray(text, id, opts)) else: @@ -918,7 +920,7 @@ def add_message(messages: List[Message], text: Union[bytearray, str], id: int = # holds a row in the shop item table (which contains pointers to the description and purchase messages) class ShopItem: # read a single message - def __init__(self, rom: "Rom", shop_table_address: int, index: int) -> None: + def __init__(self, rom: Rom, shop_table_address: int, index: int) -> None: entry_offset = shop_table_address + 0x20 * index entry = rom.read_bytes(entry_offset, 0x20) @@ -956,7 +958,7 @@ def display(self) -> str: return ', '.join(meta_data) + '\n' + ', '.join(func_data) # write the shop item back - def write(self, rom: "Rom", shop_table_address: int, index: int) -> None: + def write(self, rom: Rom, shop_table_address: int, index: int) -> None: entry_offset = shop_table_address + 0x20 * index data = [] @@ -979,7 +981,7 @@ def write(self, rom: "Rom", shop_table_address: int, index: int) -> None: # reads each of the shop items -def read_shop_items(rom: "Rom", shop_table_address: int) -> List[ShopItem]: +def read_shop_items(rom: Rom, shop_table_address: int) -> list[ShopItem]: shop_items = [] for index in range(0, 100): @@ -989,17 +991,17 @@ def read_shop_items(rom: "Rom", shop_table_address: int) -> List[ShopItem]: # writes each of the shop item back into rom -def write_shop_items(rom: "Rom", shop_table_address: int, shop_items: Iterable[ShopItem]) -> None: +def write_shop_items(rom: Rom, shop_table_address: int, shop_items: Iterable[ShopItem]) -> None: for s in shop_items: s.write(rom, shop_table_address, s.index) # these are unused shop items, and contain text ids that are used elsewhere, and should not be moved -SHOP_ITEM_EXCEPTIONS: List[int] = [0x0A, 0x0B, 0x11, 0x12, 0x13, 0x14, 0x29] +SHOP_ITEM_EXCEPTIONS: list[int] = [0x0A, 0x0B, 0x11, 0x12, 0x13, 0x14, 0x29] # returns a set of all message ids used for shop items -def get_shop_message_id_set(shop_items: Iterable[ShopItem]) -> Set[int]: +def get_shop_message_id_set(shop_items: Iterable[ShopItem]) -> set[int]: ids = set() for shop in shop_items: if shop.index not in SHOP_ITEM_EXCEPTIONS: @@ -1009,14 +1011,14 @@ def get_shop_message_id_set(shop_items: Iterable[ShopItem]) -> Set[int]: # remove all messages that easy to tell are unused to create space in the message index table -def remove_unused_messages(messages: List[Message]) -> None: +def remove_unused_messages(messages: list[Message]) -> None: messages[:] = [m for m in messages if not m.is_id_message()] for index, m in enumerate(messages): m.index = index # takes all messages used for shop items, and moves messages from the 00xx range into the unused 80xx range -def move_shop_item_messages(messages: List[Message], shop_items: Iterable[ShopItem]) -> None: +def move_shop_item_messages(messages: list[Message], shop_items: Iterable[ShopItem]) -> None: # checks if a message id is in the item message range def is_in_item_range(id: int) -> bool: bytes = int_to_bytes(id, 2) @@ -1089,7 +1091,7 @@ def make_player_message(text: str) -> str: # reduce item message sizes and add new item messages # make sure to call this AFTER move_shop_item_messages() -def update_item_messages(messages: List[Message], world: "World") -> None: +def update_item_messages(messages: list[Message], world: World) -> None: new_item_messages = ITEM_MESSAGES + KEYSANITY_MESSAGES check_message_dupes(new_item_messages) for id, text in new_item_messages: @@ -1112,12 +1114,12 @@ def check_message_dupes(new_item_messages): raise Exception("Duplicate MessageID found: " + hex(message_id1) + ", " + message1 + ", " + message2) # run all keysanity related patching to add messages for dungeon specific items -def add_item_messages(messages: List[Message], shop_items: Iterable[ShopItem], world: "World") -> None: +def add_item_messages(messages: list[Message], shop_items: Iterable[ShopItem], world: World) -> None: move_shop_item_messages(messages, shop_items) update_item_messages(messages, world) # reads each of the game's messages into a list of Message objects -def read_messages(rom: "Rom") -> List[Message]: +def read_messages(rom: Rom) -> list[Message]: table_offset = ENG_TABLE_START index = 0 messages = [] @@ -1145,7 +1147,7 @@ def read_messages(rom: "Rom") -> List[Message]: # title and file select screens. Preserve this table entry and text data when # overwriting the JP data. The regular read_messages function only reads English # data. -def read_fffc_message(rom: "Rom") -> Message: +def read_fffc_message(rom: Rom) -> Message: table_offset = JPN_TABLE_START index = 0 while True: @@ -1163,7 +1165,7 @@ def read_fffc_message(rom: "Rom") -> Message: # write the messages back -def repack_messages(rom: "Rom", messages: List[Message], permutation: Optional[List[int]] = None, +def repack_messages(rom: Rom, messages: list[Message], permutation: Optional[list[int]] = None, always_allow_skip: bool = True, speed_up_text: bool = True) -> None: rom.update_dmadata_record_by_key(ENG_TEXT_START, ENG_TEXT_START, ENG_TEXT_START + ENG_TEXT_SIZE_LIMIT) rom.update_dmadata_record_by_key(JPN_TEXT_START, JPN_TEXT_START, JPN_TEXT_START + JPN_TEXT_SIZE_LIMIT) @@ -1251,7 +1253,7 @@ def repack_messages(rom: "Rom", messages: List[Message], permutation: Optional[L # shuffles the messages in the game, making sure to keep various message types in their own group -def shuffle_messages(messages: List[Message], except_hints: bool = True) -> List[int]: +def shuffle_messages(messages: list[Message], except_hints: bool = True) -> list[int]: if not hasattr(shuffle_messages, "shop_item_messages"): shuffle_messages.shop_item_messages = [] if not hasattr(shuffle_messages, "scrubs_message_ids"): @@ -1300,7 +1302,7 @@ def is_exempt(m: Message) -> bool: have_three_choice = list(filter(lambda m: not is_exempt(m) and m.has_three_choice, messages)) basic_messages = list(filter(lambda m: not is_exempt(m) and m.is_basic(), messages)) - def shuffle_group(group: List[Message]) -> None: + def shuffle_group(group: list[Message]) -> None: group_permutation = [i for i, _ in enumerate(group)] random.shuffle(group_permutation) @@ -1319,7 +1321,7 @@ def shuffle_group(group: List[Message]) -> None: # Update warp song text boxes for ER -def update_warp_song_text(messages: List[Message], world: "World") -> None: +def update_warp_song_text(messages: list[Message], world: World) -> None: from Hints import HintArea msg_list = { diff --git a/Models.py b/Models.py index d6e00d124..ff994f53a 100644 --- a/Models.py +++ b/Models.py @@ -1,7 +1,8 @@ +from __future__ import annotations import os import random from enum import IntEnum -from typing import TYPE_CHECKING, List, Union, Dict, Tuple +from typing import TYPE_CHECKING from Utils import data_path @@ -11,7 +12,7 @@ from Settings import Settings -def get_model_choices(age: int) -> List[str]: +def get_model_choices(age: int) -> list[str]: names = ["Default"] path = data_path("Models/Adult") if age == 1: @@ -36,8 +37,8 @@ class ModelDefinitionError(ModelError): # Used for writer model pointers to the rom in place of the vanilla pointers class ModelPointerWriter: - def __init__(self, rom: "Rom") -> None: - self.rom: "Rom" = rom + def __init__(self, rom: Rom) -> None: + self.rom: Rom = rom self.offset: int = 0 self.advance: int = 4 self.base: int = CODE_START @@ -92,7 +93,7 @@ def WriteModelDataLo(self, data: int) -> None: # Either return the starting index of the requested data (when start == 0) # or the offset of the element in the footer, if it exists (start > 0) -def scan(bytes: bytearray, data: Union[bytearray, str], start: int = 0) -> int: +def scan(bytes: bytearray, data: bytearray | str, start: int = 0) -> int: databytes = data # If a string was passed, encode string as bytes if isinstance(data, str): @@ -193,7 +194,7 @@ def unwrap(zobj: bytearray, address: int) -> int: # Used to overwrite pointers in the displaylist with new ones -def WriteDLPointer(dl: List[int], index: int, data: int) -> None: +def WriteDLPointer(dl: list[int], index: int, data: int) -> None: bytes = data.to_bytes(4, 'big') for i in range(4): dl[index + i] = bytes[i] @@ -201,8 +202,8 @@ def WriteDLPointer(dl: List[int], index: int, data: int) -> None: # An extensive function which loads pieces from the vanilla Link model to add to the user-provided zobj # Based on https://github.com/hylian-modding/ML64-Z64Lib/blob/master/cores/Z64Lib/API/zzoptimize.ts function optimize() -def LoadVanilla(rom: "Rom", missing: List[str], rebase: int, linkstart: int, linksize: int, - pieces: 'Dict[str, Tuple[Offsets, int]]', skips: Dict[str, List[Tuple[int, int]]]) -> Tuple[List[int], Dict[str, int]]: +def LoadVanilla(rom: Rom, missing: list[str], rebase: int, linkstart: int, linksize: int, + pieces: dict[str, tuple[Offsets, int]], skips: dict[str, list[tuple[int, int]]]) -> tuple[list[int], dict[str, int]]: # Get vanilla "zobj" of Link's model vanillaData = [] for i in range(linksize): @@ -440,7 +441,7 @@ def CheckDiff(limb: int, skeleton: int) -> bool: return diff > TOLERANCE -def CorrectSkeleton(zobj: bytearray, skeleton: List[List[int]], agestr: str) -> bool: +def CorrectSkeleton(zobj: bytearray, skeleton: list[list[int]], agestr: str) -> bool: # Get the hierarchy pointer hierarchy = FindHierarchy(zobj, agestr) # Get what the hierarchy pointer points to (pointer to limb 0) @@ -483,7 +484,7 @@ def CorrectSkeleton(zobj: bytearray, skeleton: List[List[int]], agestr: str) -> # Loads model from file and processes it by adding vanilla pieces and setting up the LUT if necessary. -def LoadModel(rom: "Rom", model: str, age: int) -> int: +def LoadModel(rom: Rom, model: str, age: int) -> int: # age 0 = adult, 1 = child linkstart = ADULT_START linksize = ADULT_SIZE @@ -603,7 +604,7 @@ def LoadModel(rom: "Rom", model: str, age: int) -> int: # Write in the adult model and repoint references to it -def patch_model_adult(rom: "Rom", settings: "Settings", log: "CosmeticsLog") -> None: +def patch_model_adult(rom: Rom, settings: Settings, log: CosmeticsLog) -> None: # Get model filepath model = settings.model_adult_filepicker # Default to filepicker if non-empty @@ -775,7 +776,7 @@ def patch_model_adult(rom: "Rom", settings: "Settings", log: "CosmeticsLog") -> # Write in the child model and repoint references to it -def patch_model_child(rom: "Rom", settings: "Settings", log: "CosmeticsLog") -> None: +def patch_model_child(rom: Rom, settings: Settings, log: CosmeticsLog) -> None: # Get model filepath model = settings.model_child_filepicker # Default to filepicker if non-empty @@ -1089,7 +1090,7 @@ class Offsets(IntEnum): # Adult model pieces and their offsets, both in the LUT and in vanilla -AdultPieces: Dict[str, Tuple[Offsets, int]] = { +AdultPieces: dict[str, tuple[Offsets, int]] = { "Sheath": (Offsets.ADULT_LINK_LUT_DL_SWORD_SHEATH, 0x249D8), "FPS.Hookshot": (Offsets.ADULT_LINK_LUT_DL_FPS_HOOKSHOT, 0x2A738), "Hilt.2": (Offsets.ADULT_LINK_LUT_DL_SWORD_HILT, 0x22060), # 0x21F78 + 0xE8, skips blade @@ -1149,7 +1150,7 @@ class Offsets(IntEnum): # Note: Some skips which can be implemented by skipping the beginning portion of the model # rather than specifying those indices here, simply have their offset in the table above # increased by whatever amount of starting indices would be skipped. -adultSkips: Dict[str, List[Tuple[int, int]]] = { +adultSkips: dict[str, list[tuple[int, int]]] = { "FPS.Hookshot": [(0x2F0, 0x618)], "Hilt.2": [(0x1E8, 0x430)], "Hilt.3": [(0x160, 0x480)], @@ -1163,7 +1164,7 @@ class Offsets(IntEnum): "Shield.3": [(0x1B8, 0x3E8)], } -adultSkeleton: List[List[int]] = [ +adultSkeleton: list[list[int]] = [ [0xFFC7, 0x0D31, 0x0000], # Limb 0 [0x0000, 0x0000, 0x0000], # Limb 1 [0x03B1, 0x0000, 0x0000], # Limb 2 @@ -1188,7 +1189,7 @@ class Offsets(IntEnum): ] -ChildPieces: Dict[str, Tuple[Offsets, int]] = { +ChildPieces: dict[str, tuple[Offsets, int]] = { "Slingshot.String": (Offsets.CHILD_LINK_LUT_DL_SLINGSHOT_STRING, 0x221A8), "Sheath": (Offsets.CHILD_LINK_LUT_DL_SWORD_SHEATH, 0x15408), "Blade.2": (Offsets.CHILD_LINK_LUT_DL_MASTER_SWORD, 0x15698), # 0x15540 + 0x158, skips fist @@ -1235,14 +1236,14 @@ class Offsets(IntEnum): } -childSkips: Dict[str, List[Tuple[int, int]]] = { +childSkips: dict[str, list[tuple[int, int]]] = { "Boomerang": [(0x140, 0x240)], "Hilt.1": [(0xC8, 0x170)], "Shield.1": [(0x140, 0x218)], "Ocarina.1": [(0x110, 0x240)], } -childSkeleton: List[List[int]] = [ +childSkeleton: list[list[int]] = [ [0x0000, 0x0948, 0x0000], # Limb 0 [0xFFFC, 0xFF98, 0x0000], # Limb 1 [0x025F, 0x0000, 0x0000], # Limb 2 @@ -1313,7 +1314,7 @@ class Offsets(IntEnum): CHILD_POST_START: int = 0x00005228 # Parts of the rom to not overwrite when applying a patch file -restrictiveBytes: List[Tuple[int, int]] = [ +restrictiveBytes: list[tuple[int, int]] = [ (ADULT_START, ADULT_SIZE), # Ignore adult model (CHILD_START, CHILD_SIZE), # Ignore child model # Adult model pointers diff --git a/Music.py b/Music.py index 54da66884..54e306b09 100644 --- a/Music.py +++ b/Music.py @@ -1,8 +1,10 @@ # Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer +from __future__ import annotations import itertools import os import random -from typing import TYPE_CHECKING, Tuple, List, Dict, Iterable, Optional, Union +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional from Rom import Rom from Utils import compare_version, data_path @@ -14,7 +16,7 @@ AUDIOSEQ_DMADATA_INDEX: int = 4 # Format: (Title, Sequence ID) -bgm_sequence_ids: Tuple[Tuple[str, int], ...] = ( +bgm_sequence_ids: tuple[tuple[str, int], ...] = ( ("Hyrule Field", 0x02), ("Dodongos Cavern", 0x18), ("Kakariko Adult", 0x19), @@ -64,7 +66,7 @@ ("Mini-game", 0x6C), ) -fanfare_sequence_ids: Tuple[Tuple[str, int], ...] = ( +fanfare_sequence_ids: tuple[tuple[str, int], ...] = ( ("Game Over", 0x20), ("Boss Defeated", 0x21), ("Item Get", 0x22), @@ -82,7 +84,7 @@ ("Door of Time", 0x59), ) -ocarina_sequence_ids: Tuple[Tuple[str, int], ...] = ( +ocarina_sequence_ids: tuple[tuple[str, int], ...] = ( ("Prelude of Light", 0x25), ("Bolero of Fire", 0x33), ("Minuet of Forest", 0x34), @@ -109,7 +111,7 @@ def __init__(self, name: str, cosmetic_name: str, type: int = 0x0202, instrument self.type: int = type self.instrument_set: int = instrument_set - def copy(self) -> 'Sequence': + def copy(self) -> Sequence: copy = Sequence(self.name, self.cosmetic_name, self.type, self.instrument_set, self.replaces, self.vanilla_id) return copy @@ -122,10 +124,10 @@ def __init__(self) -> None: self.data: bytearray = bytearray() -def process_sequences(rom: Rom, ids: Iterable[Tuple[str, int]], seq_type: str = 'bgm', disabled_source_sequences: Optional[List[str]] = None, - disabled_target_sequences: Optional[Dict[str, Tuple[str, int]]] = None, include_custom: bool = True, - sequences: Optional[Dict[str, Sequence]] = None, target_sequences: Optional[Dict[str, Sequence]] = None, - groups: Optional[Dict[str, List[str]]] = None) -> Tuple[Dict[str, Sequence], Dict[str, Sequence], Dict[str, List[str]]]: +def process_sequences(rom: Rom, ids: Iterable[tuple[str, int]], seq_type: str = 'bgm', disabled_source_sequences: Optional[list[str]] = None, + disabled_target_sequences: Optional[dict[str, tuple[str, int]]] = None, include_custom: bool = True, + sequences: Optional[dict[str, Sequence]] = None, target_sequences: Optional[dict[str, Sequence]] = None, + groups: Optional[dict[str, list[str]]] = None) -> tuple[dict[str, Sequence], dict[str, Sequence], dict[str, list[str]]]: disabled_source_sequences = [] if disabled_source_sequences is None else disabled_source_sequences disabled_target_sequences = {} if disabled_target_sequences is None else disabled_target_sequences sequences = {} if sequences is None else sequences @@ -208,8 +210,8 @@ def process_sequences(rom: Rom, ids: Iterable[Tuple[str, int]], seq_type: str = return sequences, target_sequences, groups -def shuffle_music(log: "CosmeticsLog", source_sequences: Dict[str, Sequence], target_sequences: Dict[str, Sequence], - music_mapping: Dict[str, str], seq_type: str = "music") -> List[Sequence]: +def shuffle_music(log: CosmeticsLog, source_sequences: dict[str, Sequence], target_sequences: dict[str, Sequence], + music_mapping: dict[str, str], seq_type: str = "music") -> list[Sequence]: sequences = [] favorites = log.src_dict.get('bgm_groups', {}).get('favorites', []).copy() @@ -243,7 +245,7 @@ def shuffle_music(log: "CosmeticsLog", source_sequences: Dict[str, Sequence], ta return sequences -def rebuild_sequences(rom: Rom, sequences: List[Sequence]) -> None: +def rebuild_sequences(rom: Rom, sequences: list[Sequence]) -> None: dma_entry = rom.dma[AUDIOSEQ_DMADATA_INDEX] audioseq_start, audioseq_end, audioseq_size = dma_entry.as_tuple() replacement_dict = {seq.replaces: seq for seq in sequences} @@ -351,7 +353,7 @@ def rebuild_sequences(rom: Rom, sequences: List[Sequence]) -> None: rom.write_byte(base, j.instrument_set) -def rebuild_pointers_table(rom: Rom, sequences: List[Sequence]) -> None: +def rebuild_pointers_table(rom: Rom, sequences: list[Sequence]) -> None: for sequence in [s for s in sequences if s.vanilla_id and s.replaces]: bgm_sequence = rom.original.read_bytes(0xB89AE0 + (sequence.vanilla_id * 0x10), 0x10) bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (sequence.vanilla_id * 2)) @@ -362,7 +364,7 @@ def rebuild_pointers_table(rom: Rom, sequences: List[Sequence]) -> None: rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), rom.read_int16(0xB89910 + 0xDD + (0x28 * 2))) -def randomize_music(rom: Rom, settings: "Settings", log: "CosmeticsLog") -> None: +def randomize_music(rom: Rom, settings: Settings, log: CosmeticsLog) -> None: shuffled_sequences = shuffled_fanfare_sequences = [] sequences = fanfare_sequences = target_sequences = target_fanfare_sequences = bgm_groups = fanfare_groups = {} disabled_source_sequences = log.src_dict.get('bgm_groups', {}).get('exclude', []).copy() @@ -521,7 +523,7 @@ def randomize_music(rom: Rom, settings: "Settings", log: "CosmeticsLog") -> None disable_music(rom, log, disabled_target_sequences.values()) -def disable_music(rom: Rom, log: "CosmeticsLog", ids: Iterable[Tuple[str, int]]) -> None: +def disable_music(rom: Rom, log: CosmeticsLog, ids: Iterable[tuple[str, int]]) -> None: # First track is no music blank_track = rom.read_bytes(0xB89AE0 + (0 * 0x10), 0x10) for bgm in ids: diff --git a/N64Patch.py b/N64Patch.py index fc6f8927c..9c981370f 100644 --- a/N64Patch.py +++ b/N64Patch.py @@ -1,8 +1,9 @@ +from __future__ import annotations import copy import random import zipfile import zlib -from typing import TYPE_CHECKING, Tuple, List, Optional +from typing import TYPE_CHECKING, Optional from Rom import Rom from ntype import BigStream @@ -14,7 +15,7 @@ # get the next XOR key. Uses some location in the source rom. # This will skip of 0s, since if we hit a block of 0s, the # patch data will be raw. -def key_next(rom: Rom, key_address: int, address_range: Tuple[int, int]) -> Tuple[int, int]: +def key_next(rom: Rom, key_address: int, address_range: tuple[int, int]) -> tuple[int, int]: key = 0 while key == 0: key_address += 1 @@ -27,8 +28,8 @@ def key_next(rom: Rom, key_address: int, address_range: Tuple[int, int]) -> Tupl # creates a XOR block for the patch. This might break it up into # multiple smaller blocks if there is a concern about the XOR key # or if it is too long. -def write_block(rom: Rom, xor_address: int, xor_range: Tuple[int, int], block_start: int, - data: List[int], patch_data: BigStream) -> int: +def write_block(rom: Rom, xor_address: int, xor_range: tuple[int, int], block_start: int, + data: list[int], patch_data: BigStream) -> int: new_data = [] key_offset = 0 continue_block = False @@ -79,7 +80,7 @@ def write_block(rom: Rom, xor_address: int, xor_range: Tuple[int, int], block_st # then it will include the address to write to. Otherwise, it will # have a number of XOR keys to skip and then continue writing after # the previous block -def write_block_section(start: int, key_skip: int, in_data: List[int], patch_data: BigStream, is_continue: bool) -> None: +def write_block_section(start: int, key_skip: int, in_data: list[int], patch_data: BigStream, is_continue: bool) -> None: if not is_continue: patch_data.append_int32(start) else: @@ -92,7 +93,7 @@ def write_block_section(start: int, key_skip: int, in_data: List[int], patch_dat # xor_range is the range the XOR key will read from. This range is not # too important, but I tried to choose from a section that didn't really # have big gaps of 0s which we want to avoid. -def create_patch_file(rom: Rom, file: str, xor_range: Tuple[int, int] = (0x00B8AD30, 0x00F029A0)) -> None: +def create_patch_file(rom: Rom, file: str, xor_range: tuple[int, int] = (0x00B8AD30, 0x00F029A0)) -> None: dma_start, dma_end = rom.dma.dma_start, rom.dma.dma_end # add header @@ -179,7 +180,7 @@ def create_patch_file(rom: Rom, file: str, xor_range: Tuple[int, int] = (0x00B8A # This will apply a patch file to a source rom to generate a patched rom. -def apply_patch_file(rom: Rom, settings: "Settings", sub_file: Optional[str] = None) -> None: +def apply_patch_file(rom: Rom, settings: Settings, sub_file: Optional[str] = None) -> None: file = settings.patch_file # load the patch file and decompress diff --git a/OcarinaSongs.py b/OcarinaSongs.py index b97e0aeab..e46968c12 100644 --- a/OcarinaSongs.py +++ b/OcarinaSongs.py @@ -1,7 +1,9 @@ +from __future__ import annotations import random import sys +from collections.abc import Callable, Sequence from itertools import chain -from typing import TYPE_CHECKING, Dict, List, Tuple, Sequence, Callable, Optional, Union +from typing import TYPE_CHECKING, Optional from Fill import ShuffleError @@ -14,16 +16,16 @@ from Rom import Rom from World import World -ActivationTransform: TypeAlias = Callable[[List[int]], List[int]] -PlaybackTransform: TypeAlias = Callable[[List[Dict[str, int]]], List[Dict[str, int]]] -Transform: TypeAlias = Union[ActivationTransform, PlaybackTransform] +ActivationTransform: TypeAlias = "Callable[[list[int]], list[int]]" +PlaybackTransform: TypeAlias = "Callable[[list[dict[str, int]]], list[dict[str, int]]]" +Transform: TypeAlias = "ActivationTransform | PlaybackTransform" PLAYBACK_START: int = 0xB781DC PLAYBACK_LENGTH: int = 0xA0 ACTIVATION_START: int = 0xB78E5C ACTIVATION_LENGTH: int = 0x09 -FORMAT_ACTIVATION: Dict[int, str] = { +FORMAT_ACTIVATION: dict[int, str] = { 0: 'A', 1: 'v', 2: '>', @@ -31,7 +33,7 @@ 4: '^', } -READ_ACTIVATION: Dict[str, int] = { # support both Av><^ and ADRLU +READ_ACTIVATION: dict[str, int] = { # support both Av><^ and ADRLU 'a': 0, 'v': 1, 'd': 1, @@ -43,7 +45,7 @@ 'u': 4, } -ACTIVATION_TO_PLAYBACK_NOTE: Dict[int, int] = { +ACTIVATION_TO_PLAYBACK_NOTE: dict[int, int] = { 0: 0x02, # A 1: 0x05, # Down 2: 0x09, # Right @@ -52,7 +54,7 @@ 0xFF: 0xFF, # Rest } -DIFFICULTY_ORDER: List[str] = [ +DIFFICULTY_ORDER: list[str] = [ 'Zeldas Lullaby', 'Sarias Song', 'Eponas Song', @@ -68,7 +70,7 @@ ] # Song name: (rom index, warp, vanilla activation), -SONG_TABLE: Dict[str, Tuple[int, bool, str]] = { +SONG_TABLE: dict[str, tuple[int, bool, str]] = { 'Zeldas Lullaby': ( 8, False, '<^><^>'), 'Eponas Song': ( 7, False, '^<>^<>'), 'Sarias Song': ( 6, False, 'v><'), @@ -86,7 +88,7 @@ # checks if one list is a sublist of the other (in either direction) # python is magic..... -def subsong(song1: 'Song', song2: 'Song') -> bool: +def subsong(song1: Song, song2: Song) -> bool: # convert both lists to strings s1 = ''.join( map(chr, song1.activation)) s2 = ''.join( map(chr, song2.activation)) @@ -95,7 +97,7 @@ def subsong(song1: 'Song', song2: 'Song') -> bool: # give random durations and volumes to the notes -def fast_playback(activation: List[int]) -> List[Dict[str, int]]: +def fast_playback(activation: list[int]) -> list[dict[str, int]]: playback = [] for note_index, note in enumerate(activation): playback.append({'note': note, 'duration': 0x04, 'volume': 0x57}) @@ -103,7 +105,7 @@ def fast_playback(activation: List[int]) -> List[Dict[str, int]]: # give random durations and volumes to the notes -def random_playback(activation: List[int]) -> List[Dict[str, int]]: +def random_playback(activation: list[int]) -> list[dict[str, int]]: playback = [] for note_index, note in enumerate(activation): duration = random.randint(0x8, 0x20) @@ -120,7 +122,7 @@ def random_playback(activation: List[int]) -> List[Dict[str, int]]: # gives random volume and duration to the notes of piece -def random_piece_playback(piece: List[int]) -> List[Dict[str, int]]: +def random_piece_playback(piece: list[int]) -> list[dict[str, int]]: playback = [] for note in piece: duration = random.randint(0x8, 0x20) @@ -131,23 +133,23 @@ def random_piece_playback(piece: List[int]) -> List[Dict[str, int]]: # takes the volume/duration of playback, and notes of piece, and creates a playback piece # assumes the lists are the same length -def copy_playback_info(playback: List[Dict[str, int]], piece: List[int]): +def copy_playback_info(playback: list[dict[str, int]], piece: list[int]): return [{'note': n, 'volume': p['volume'], 'duration': p['duration']} for (p, n) in zip(playback, piece)] -def identity(x: List[Union[int, Dict[str, int]]]) -> List[Union[int, Dict[str, int]]]: +def identity(x: list[int | dict[str, int]]) -> list[int | dict[str, int]]: return x -def random_piece(count: int, allowed: Sequence[int] = range(0, 5)) -> List[int]: +def random_piece(count: int, allowed: Sequence[int] = range(0, 5)) -> list[int]: return random.choices(allowed, k=count) -def invert_piece(piece: List[int]) -> List[int]: +def invert_piece(piece: list[int]) -> list[int]: return [4 - note for note in piece] -def reverse_piece(piece: List[Union[int, Dict[str, int]]]) -> List[Union[int, Dict[str, int]]]: +def reverse_piece(piece: list[int | dict[str, int]]) -> list[int | dict[str, int]]: return piece[::-1] @@ -156,7 +158,7 @@ def clamp(val: int, low: int, high: int) -> int: def transpose_piece(amount: int) -> ActivationTransform: - def transpose(piece: List[int]) -> List[int]: + def transpose(piece: list[int]) -> list[int]: return [clamp(note + amount, 0, 4) for note in piece] return transpose @@ -165,11 +167,11 @@ def compose(f: Transform, g: Transform) -> Transform: return lambda x: f(g(x)) -def add_transform_to_piece(piece: List[int], transform: ActivationTransform) -> List[int]: +def add_transform_to_piece(piece: list[int], transform: ActivationTransform) -> list[int]: return piece + transform(piece) -def repeat(piece: List[int]) -> List[int]: +def repeat(piece: list[int]) -> list[int]: return 2 * piece @@ -178,13 +180,13 @@ class Song: # create a song, based on a given scheme def __init__(self, rand_song: bool = True, piece_size: int = 3, extra_position: str = 'none', starting_range: Sequence[int] = range(0, 5), activation_transform: ActivationTransform = identity, - playback_transform: PlaybackTransform = identity, *, activation: Optional[List[int]] = None, + playback_transform: PlaybackTransform = identity, *, activation: Optional[list[int]] = None, playback_fast: bool = False) -> None: self.length: int = 0 - self.activation: List[int] = [] - self.playback: List[Dict[str, int]] = [] - self.activation_data: List[int] = [] - self.playback_data: List[int] = [] + self.activation: list[int] = [] + self.playback: list[dict[str, int]] = [] + self.activation_data: list[int] = [] + self.playback_data: list[int] = [] self.total_duration: int = 0 if activation: @@ -234,7 +236,7 @@ def increase_duration_to(self, duration: int) -> None: self.playback.append({'note': 0xFF, 'duration': duration_needed, 'volume': 0}) self.format_playback_data() - def two_piece_playback(self, piece: List[int], extra_position: str = 'none', activation_transform: ActivationTransform = identity, + def two_piece_playback(self, piece: list[int], extra_position: str = 'none', activation_transform: ActivationTransform = identity, playback_transform: PlaybackTransform = identity) -> None: piece_length = len(piece) piece2 = activation_transform(piece) @@ -306,7 +308,7 @@ def __repr__(self) -> str: return activation_string + '\n' + playback_string @classmethod - def from_str(cls, notes: str) -> 'Song': + def from_str(cls, notes: str) -> Song: return cls(activation=[READ_ACTIVATION[note.lower()] for note in notes]) def __str__(self) -> str: @@ -346,7 +348,6 @@ def get_random_song() -> Song: song = Song(rand_song, piece_size, extra_position, starting_range, activation_transform, playback_transform) # rate its difficulty - difficulty = 0 difficulty = piece_size * 12 if extra_position != 'none': difficulty += 12 @@ -364,7 +365,7 @@ def get_random_song() -> Song: # create a list of 12 songs, none of which are sub-strings of any other song -def generate_song_list(world: "World", frog: bool, warp: bool) -> Dict[str, Song]: +def generate_song_list(world: World, frog: bool, warp: bool) -> dict[str, Song]: fixed_songs = {} if not frog: fixed_songs.update({name: Song.from_str(notes) for name, (_, is_warp, notes) in SONG_TABLE.items() if not is_warp}) @@ -410,7 +411,7 @@ def generate_song_list(world: "World", frog: bool, warp: bool) -> Dict[str, Song # replace the playback and activation requirements for the ocarina songs -def replace_songs(world: "World", rom: "Rom", frog: bool, warp: bool) -> None: +def replace_songs(world: World, rom: Rom, frog: bool, warp: bool) -> None: songs = generate_song_list(world, frog, warp) world.song_notes = songs diff --git a/OoTRandomizer.py b/OoTRandomizer.py index 88c489b84..395e51226 100755 --- a/OoTRandomizer.py +++ b/OoTRandomizer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import sys -if sys.version_info < (3, 6, 0): - print("OoT Randomizer requires Python version 3.6 or newer and you are using %s" % '.'.join([str(i) for i in sys.version_info[0:3]])) +if sys.version_info < (3, 7, 0): + print("OoT Randomizer requires Python version 3.7 or newer and you are using %s" % '.'.join([str(i) for i in sys.version_info[0:3]])) sys.exit(1) import datetime diff --git a/Patches.py b/Patches.py index 6e2e76cc0..e32ff5cf1 100644 --- a/Patches.py +++ b/Patches.py @@ -1,3 +1,4 @@ +from __future__ import annotations import datetime import itertools import random @@ -5,7 +6,8 @@ import struct import sys import zlib -from typing import Dict, List, Iterable, Tuple, Set, Callable, Optional, Any +from collections.abc import Callable, Iterable +from typing import Optional, Any from Entrance import Entrance from HintList import get_hint @@ -35,7 +37,7 @@ else: TypeAlias = str -OverrideEntry: TypeAlias = Tuple[int, int, int, int, int, int] +OverrideEntry: TypeAlias = "tuple[int, int, int, int, int, int]" def patch_rom(spoiler: Spoiler, world: World, rom: Rom) -> Rom: @@ -231,7 +233,7 @@ def patch_rom(spoiler: Spoiler, world: World, rom: Rom) -> Rom: rom.write_int32(rom.sym('FREE_BOMBCHU_DROPS'), 1) # show seed info on file select screen - def make_bytes(txt: str, size: int) -> List[int]: + def make_bytes(txt: str, size: int) -> list[int]: bytes = list(ord(c) for c in txt[:size-1]) + [0] * size return bytes[:size] @@ -911,7 +913,7 @@ def make_bytes(txt: str, size: int) -> List[int]: exit_updates = [] - def generate_exit_lookup_table() -> Dict[int, List[int]]: + def generate_exit_lookup_table() -> dict[int, list[int]]: # Assumes that the last exit on a scene's exit list cannot be 0000 exit_table = { 0x0028: [0xAC95C2] # Jabu with the fish is entered from a cutscene hardcode @@ -2031,7 +2033,7 @@ def calculate_traded_flags(world): rom.write_int16s(0x3417400, list(shop_objs)) # Scrub text stuff. - def update_scrub_text(message: bytearray, text_replacement: List[str], default_price: int, price: int, + def update_scrub_text(message: bytearray, text_replacement: list[str], default_price: int, price: int, item_name: Optional[str] = None) -> bytearray: scrub_strip_text = ["some ", "1 piece ", "5 pieces ", "30 pieces "] for text in scrub_strip_text: @@ -2538,20 +2540,20 @@ def add_to_extended_object_table(rom: Rom, object_id: int, start_adddress: int, item_row_struct = struct.Struct('>BBHHBBIIhhBxxxI') # Match item_row_t in item_table.h -item_row_fields = [ +item_row_fields: list[str] = [ 'base_item_id', 'action_id', 'text_id', 'object_id', 'graphic_id', 'chest_type', 'upgrade_fn', 'effect_fn', 'effect_arg1', 'effect_arg2', 'collectible', 'alt_text_fn', ] -def read_rom_item(rom: Rom, item_id: int) -> Dict[str, Any]: +def read_rom_item(rom: Rom, item_id: int) -> dict[str, Any]: addr = rom.sym('item_table') + (item_id * item_row_struct.size) row_bytes = rom.read_bytes(addr, item_row_struct.size) row = item_row_struct.unpack(row_bytes) return { item_row_fields[i]: row[i] for i in range(len(item_row_fields)) } -def write_rom_item(rom: Rom, item_id: int, item: Dict[str, Any]) -> None: +def write_rom_item(rom: Rom, item_id: int, item: dict[str, Any]) -> None: addr = rom.sym('item_table') + (item_id * item_row_struct.size) row = [item[f] for f in item_row_fields] row_bytes = item_row_struct.pack(*row) @@ -2559,17 +2561,17 @@ def write_rom_item(rom: Rom, item_id: int, item: Dict[str, Any]) -> None: texture_struct = struct.Struct('>HBxxxxxII') # Match texture_t in textures.c -texture_fields: List[str] = ['texture_id', 'file_buf', 'file_vrom_start', 'file_size'] +texture_fields: list[str] = ['texture_id', 'file_buf', 'file_vrom_start', 'file_size'] -def read_rom_texture(rom: Rom, texture_id: int) -> Dict[str, Any]: +def read_rom_texture(rom: Rom, texture_id: int) -> dict[str, Any]: addr = rom.sym('texture_table') + (texture_id * texture_struct.size) row_bytes = rom.read_bytes(addr, texture_struct.size) row = texture_struct.unpack(row_bytes) return {texture_fields[i]: row[i] for i in range(len(texture_fields))} -def write_rom_texture(rom: Rom, texture_id: int, texture: Dict[str, Any]) -> None: +def write_rom_texture(rom: Rom, texture_id: int, texture: dict[str, Any]) -> None: addr = rom.sym('texture_table') + (texture_id * texture_struct.size) row = [texture[f] for f in texture_fields] row_bytes = texture_struct.pack(*row) @@ -2645,7 +2647,7 @@ def check_location_dupes(world: World) -> None: raise Exception(f'Discovered duplicate location: {check_i.name}') -chestTypeMap: Dict[int, List[int]] = { +chestTypeMap: dict[int, list[int]] = { # small big boss 0x0000: [0x5000, 0x0000, 0x2000], # Large 0x1000: [0x7000, 0x1000, 0x1000], # Large, Appears, Clear Flag @@ -2667,7 +2669,7 @@ def check_location_dupes(world: World) -> None: def room_get_actors(rom: Rom, actor_func: Callable[[Rom, int, int, int], Any], room_data: int, scene: int, - alternate: Optional[int] = None) -> Dict[int, Any]: + alternate: Optional[int] = None) -> dict[int, Any]: actors = {} room_start = alternate if alternate else room_data command = 0 @@ -2694,7 +2696,7 @@ def room_get_actors(rom: Rom, actor_func: Callable[[Rom, int, int, int], Any], r def scene_get_actors(rom: Rom, actor_func: Callable[[Rom, int, int, int], Any], scene_data: int, scene: int, - alternate: Optional[int] = None, processed_rooms: Optional[List[int]] = None) -> Dict[int, Any]: + alternate: Optional[int] = None, processed_rooms: Optional[list[int]] = None) -> dict[int, Any]: if processed_rooms is None: processed_rooms = [] actors = {} @@ -2733,7 +2735,7 @@ def scene_get_actors(rom: Rom, actor_func: Callable[[Rom, int, int, int], Any], return actors -def get_actor_list(rom: Rom, actor_func: Callable[[Rom, int, int, int], Any]) -> Dict[int, Any]: +def get_actor_list(rom: Rom, actor_func: Callable[[Rom, int, int, int], Any]) -> dict[int, Any]: actors = {} scene_table = 0x00B71440 for scene in range(0x00, 0x65): @@ -2858,8 +2860,8 @@ def move_fado(rom, actor_id, actor, scene): # If boss keys are set to remove, returns boss key doors # If ganons boss key is set to remove, returns ganons boss key doors # If pot/crate shuffle is enabled, returns the first ganon's boss key door so that it can be unlocked separately to allow access to the room w/ the pots.. -def get_doors_to_unlock(rom: Rom, world: World) -> Dict[int, List[int]]: - def get_door_to_unlock(rom: Rom, actor_id: int, actor: int, scene: int) -> List[int]: +def get_doors_to_unlock(rom: Rom, world: World) -> dict[int, list[int]]: + def get_door_to_unlock(rom: Rom, actor_id: int, actor: int, scene: int) -> list[int]: actor_var = rom.read_int16(actor + 14) door_type = actor_var >> 6 switch_flag = actor_var & 0x003F @@ -2886,7 +2888,7 @@ def get_door_to_unlock(rom: Rom, actor_id: int, actor: int, scene: int) -> List[ def create_fake_name(name: str) -> str: vowels = 'aeiou' list_name = list(name) - vowel_indexes = [i for i,c in enumerate(list_name) if c in vowels] + vowel_indexes = [i for i, c in enumerate(list_name) if c in vowels] for i in random.sample(vowel_indexes, min(2, len(vowel_indexes))): c = list_name[i] list_name[i] = random.choice([v for v in vowels if v != c]) @@ -2901,7 +2903,7 @@ def create_fake_name(name: str) -> str: return new_name -def place_shop_items(rom: Rom, world: World, shop_items, messages, locations, init_shop_id: bool = False) -> Set[int]: +def place_shop_items(rom: Rom, world: World, shop_items, messages, locations, init_shop_id: bool = False) -> set[int]: if init_shop_id: place_shop_items.shop_id = 0x32 @@ -2950,9 +2952,9 @@ def place_shop_items(rom: Rom, world: World, shop_items, messages, locations, in # give it and set this as sold out. # With complete mask quest, it's free to take normally if not world.settings.complete_mask_quest and \ - ((location.vanilla_item == 'Mask of Truth' and 'Mask of Truth' in world.settings.shuffle_child_trade) or \ - ('mask_shop' in world.settings.misc_hints and location.vanilla_item == 'Goron Mask' and 'Goron Mask' in world.settings.shuffle_child_trade) or \ - ('mask_shop' in world.settings.misc_hints and location.vanilla_item == 'Zora Mask' and 'Zora Mask' in world.settings.shuffle_child_trade) or \ + ((location.vanilla_item == 'Mask of Truth' and 'Mask of Truth' in world.settings.shuffle_child_trade) or + ('mask_shop' in world.settings.misc_hints and location.vanilla_item == 'Goron Mask' and 'Goron Mask' in world.settings.shuffle_child_trade) or + ('mask_shop' in world.settings.misc_hints and location.vanilla_item == 'Zora Mask' and 'Zora Mask' in world.settings.shuffle_child_trade) or ('mask_shop' in world.settings.misc_hints and location.vanilla_item == 'Gerudo Mask' and 'Gerudo Mask' in world.settings.shuffle_child_trade)): shop_item.func2 = 0x80863714 # override to custom CanBuy function to prevent purchase before trade quest complete diff --git a/Plandomizer.py b/Plandomizer.py index 7669999d2..8fe0f083d 100644 --- a/Plandomizer.py +++ b/Plandomizer.py @@ -1,11 +1,13 @@ +from __future__ import annotations import itertools import json import math import re import random from collections import defaultdict +from collections.abc import Callable, Iterable, Sequence from functools import reduce -from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Sequence, Iterable, Callable, Optional, Union +from typing import TYPE_CHECKING, Any, Optional import StartingItems from Entrance import Entrance @@ -51,12 +53,12 @@ class InvalidFileException(Exception): class Record: - def __init__(self, properties: Optional[Dict[str, Any]] = None, src_dict: Optional[Dict[str, Any]] = None) -> None: - self.properties: Dict[str, Any] = properties if properties is not None else getattr(self, "properties") + def __init__(self, properties: Optional[dict[str, Any]] = None, src_dict: Optional[dict[str, Any]] = None) -> None: + self.properties: dict[str, Any] = properties if properties is not None else getattr(self, "properties") if src_dict is not None: self.update(src_dict, update_all=True) - def update(self, src_dict: Dict[str, Any], update_all: bool = False) -> None: + def update(self, src_dict: dict[str, Any], update_all: bool = False) -> None: if src_dict is None: src_dict = {} if isinstance(src_dict, list): @@ -65,7 +67,7 @@ def update(self, src_dict: Dict[str, Any], update_all: bool = False) -> None: if update_all or k in src_dict: setattr(self, k, src_dict.get(k, p)) - def to_json(self) -> Dict[str, Any]: + def to_json(self) -> dict[str, Any]: return {k: getattr(self, k) for (k, d) in self.properties.items() if getattr(self, k) != d} def __str__(self) -> str: @@ -73,13 +75,13 @@ def __str__(self) -> str: class DungeonRecord(Record): - mapping: Dict[str, Optional[bool]] = { + mapping: dict[str, Optional[bool]] = { 'random': None, 'mq': True, 'vanilla': False, } - def __init__(self, src_dict: Union[str, Dict[str, Optional[bool]]] = 'random') -> None: + def __init__(self, src_dict: str | dict[str, Optional[bool]] = 'random') -> None: self.mq: Optional[bool] = None if isinstance(src_dict, str): @@ -93,7 +95,7 @@ def to_json(self) -> str: class EmptyDungeonRecord(Record): - def __init__(self, src_dict: Union[Optional[bool], str, Dict[str, Optional[bool]]] = 'random') -> None: + def __init__(self, src_dict: Optional[bool | str | dict[str, Optional[bool]]] = 'random') -> None: self.empty: Optional[bool] = None if src_dict == 'random': @@ -107,13 +109,13 @@ def to_json(self) -> Optional[bool]: class GossipRecord(Record): - def __init__(self, src_dict: Dict[str, Any]) -> None: + def __init__(self, src_dict: dict[str, Any]) -> None: self.colors: Optional[Sequence[str]] = None self.hinted_locations: Optional[Sequence[str]] = None self.hinted_items: Optional[Sequence[str]] = None super().__init__({'text': None, 'colors': None, 'hinted_locations': None, 'hinted_items': None}, src_dict) - def to_json(self) -> Dict[str, Any]: + def to_json(self) -> dict[str, Any]: if self.colors is not None: self.colors = CollapseList(self.colors) if self.hinted_locations is not None: @@ -124,7 +126,7 @@ def to_json(self) -> Dict[str, Any]: class ItemPoolRecord(Record): - def __init__(self, src_dict: Union[int, Dict[str, int]] = 1) -> None: + def __init__(self, src_dict: int | dict[str, int] = 1) -> None: self.type: str = 'set' self.count: int = 1 @@ -132,13 +134,13 @@ def __init__(self, src_dict: Union[int, Dict[str, int]] = 1) -> None: src_dict = {'count': src_dict} super().__init__({'type': 'set', 'count': 1}, src_dict) - def to_json(self) -> Union[int, CollapseDict]: + def to_json(self) -> int | CollapseDict: if self.type == 'set': return self.count else: return CollapseDict(super().to_json()) - def update(self, src_dict: Dict[str, Any], update_all: bool = False) -> None: + def update(self, src_dict: dict[str, Any], update_all: bool = False) -> None: super().update(src_dict, update_all) if self.count < 0: raise ValueError("Count cannot be negative in a ItemPoolRecord.") @@ -147,15 +149,15 @@ def update(self, src_dict: Dict[str, Any], update_all: bool = False) -> None: class LocationRecord(Record): - def __init__(self, src_dict: Union[Dict[str, Any], str]) -> None: - self.item: Optional[Union[str, List[str]]] = None + def __init__(self, src_dict: dict[str, Any] | str) -> None: + self.item: Optional[str | list[str]] = None self.player: Optional[int] = None if isinstance(src_dict, str): src_dict = {'item': src_dict} super().__init__({'item': None, 'player': None, 'price': None, 'model': None}, src_dict) - def to_json(self) -> Union[str, CollapseDict]: + def to_json(self) -> str | CollapseDict: self_dict = super().to_json() if list(self_dict.keys()) == ['item']: return str(self.item) @@ -163,7 +165,7 @@ def to_json(self) -> Union[str, CollapseDict]: return CollapseDict(self_dict) @staticmethod - def from_item(item: Item) -> 'LocationRecord': + def from_item(item: Item) -> LocationRecord: if item.world.settings.world_count > 1: player = item.world.id + 1 else: @@ -178,7 +180,7 @@ def from_item(item: Item) -> 'LocationRecord': class EntranceRecord(Record): - def __init__(self, src_dict: Union[Dict[str, Optional[str]], str]) -> None: + def __init__(self, src_dict: dict[str, Optional[str]] | str) -> None: self.region: Optional[str] = None self.origin: Optional[str] = None @@ -189,7 +191,7 @@ def __init__(self, src_dict: Union[Dict[str, Optional[str]], str]) -> None: del src_dict['from'] super().__init__({'region': None, 'origin': None}, src_dict) - def to_json(self) -> Union[str, CollapseDict]: + def to_json(self) -> str | CollapseDict: self_dict = super().to_json() if list(self_dict.keys()) == ['region']: return str(self.region) @@ -199,7 +201,7 @@ def to_json(self) -> Union[str, CollapseDict]: return CollapseDict(self_dict) @staticmethod - def from_entrance(entrance: Entrance) -> 'EntranceRecord': + def from_entrance(entrance: Entrance) -> EntranceRecord: if entrance.replaces.primary and entrance.replaces.type in ('Interior', 'SpecialInterior', 'Grotto', 'Grave'): origin_name = None else: @@ -218,7 +220,7 @@ def __init__(self, src_dict: int = 1) -> None: src_dict = {'count': src_dict} super().__init__({'count': 1}, src_dict) - def copy(self) -> 'StarterRecord': + def copy(self) -> StarterRecord: return StarterRecord(self.count) def to_json(self) -> int: @@ -226,13 +228,13 @@ def to_json(self) -> int: class TrialRecord(Record): - mapping: Dict[str, Optional[bool]] = { + mapping: dict[str, Optional[bool]] = { 'random': None, 'active': True, 'inactive': False, } - def __init__(self, src_dict: Union[str, Dict[str, Optional[bool]]] = 'random') -> None: + def __init__(self, src_dict: str | dict[str, Optional[bool]] = 'random') -> None: self.active: Optional[bool] = None if isinstance(src_dict, str): @@ -246,7 +248,7 @@ def to_json(self) -> str: class SongRecord(Record): - def __init__(self, src_dict: Union[Optional[str], Dict[str, Optional[str]]] = None) -> None: + def __init__(self, src_dict: Optional[str | dict[str, Optional[str]]] = None) -> None: self.notes: Optional[str] = None if src_dict is None or isinstance(src_dict, str): @@ -258,32 +260,32 @@ def to_json(self) -> str: class WorldDistribution: - def __init__(self, distribution: 'Distribution', id: int, src_dict: Optional[Dict[str, Any]] = None) -> None: - self.randomized_settings: Optional[Dict[str, Any]] = None - self.dungeons: Optional[Dict[str, DungeonRecord]] = None - self.empty_dungeons: Optional[Dict[str, EmptyDungeonRecord]] = None - self.trials: Optional[Dict[str, TrialRecord]] = None - self.songs: Optional[Dict[str, SongRecord]] = None - self.item_pool: Optional[Dict[str, ItemPoolRecord]] = None - self.entrances: Optional[Dict[str, EntranceRecord]] = None - self.locations: Optional[Dict[str, Union[LocationRecord, List[LocationRecord]]]] = None - self.woth_locations: Optional[Dict[str, LocationRecord]] = None - self.goal_locations: Optional[Dict[str, Dict[str, Dict[str, Union[LocationRecord, Dict[str, LocationRecord]]]]]] = None - self.barren_regions: Optional[List[str]] = None - self.gossip_stones: Optional[Dict[str, GossipRecord]] = None - - self.distribution: 'Distribution' = distribution + def __init__(self, distribution: Distribution, id: int, src_dict: Optional[dict[str, Any]] = None) -> None: + self.randomized_settings: Optional[dict[str, Any]] = None + self.dungeons: Optional[dict[str, DungeonRecord]] = None + self.empty_dungeons: Optional[dict[str, EmptyDungeonRecord]] = None + self.trials: Optional[dict[str, TrialRecord]] = None + self.songs: Optional[dict[str, SongRecord]] = None + self.item_pool: Optional[dict[str, ItemPoolRecord]] = None + self.entrances: Optional[dict[str, EntranceRecord]] = None + self.locations: Optional[dict[str, LocationRecord | list[LocationRecord]]] = None + self.woth_locations: Optional[dict[str, LocationRecord]] = None + self.goal_locations: Optional[dict[str, dict[str, dict[str, LocationRecord | dict[str, LocationRecord]]]]] = None + self.barren_regions: Optional[list[str]] = None + self.gossip_stones: Optional[dict[str, GossipRecord]] = None + + self.distribution: Distribution = distribution self.id: int = id - self.base_pool: List[str] = [] - self.major_group: List[str] = [] + self.base_pool: list[str] = [] + self.major_group: list[str] = [] self.song_as_items: bool = False - self.skipped_locations: List[Location] = [] - self.effective_starting_items: Dict[str, StarterRecord] = {} + self.skipped_locations: list[Location] = [] + self.effective_starting_items: dict[str, StarterRecord] = {} src_dict = {} if src_dict is None else src_dict self.update(src_dict, update_all=True) - def update(self, src_dict: Dict[str, Any], update_all: bool = False) -> None: + def update(self, src_dict: dict[str, Any], update_all: bool = False) -> None: update_dict = { 'randomized_settings': {name: record for (name, record) in src_dict.get('randomized_settings', {}).items()}, 'dungeons': {name: DungeonRecord(record) for (name, record) in src_dict.get('dungeons', {}).items()}, @@ -314,7 +316,7 @@ def update(self, src_dict: Dict[str, Any], update_all: bool = False) -> None: else: setattr(self, k, None) - def to_json(self) -> Dict[str, Any]: + def to_json(self) -> dict[str, Any]: return { 'randomized_settings': self.randomized_settings, 'dungeons': {name: record.to_json() for (name, record) in self.dungeons.items()}, @@ -334,7 +336,7 @@ def to_json(self) -> Dict[str, Any]: def __str__(self) -> str: return dump_obj(self.to_json()) - def pattern_matcher(self, pattern: Union[str, List[str]]) -> Callable[[str], bool]: + def pattern_matcher(self, pattern: str | list[str]) -> Callable[[str], bool]: if isinstance(pattern, list): pattern_list = [] for pattern_item in pattern: @@ -417,7 +419,7 @@ def add_location(self, new_location: str, new_item: str) -> None: raise KeyError('Cannot add location that already exists') self.locations[new_location] = LocationRecord(new_item) - def configure_dungeons(self, world: "World", mq_dungeon_pool: List[str], empty_dungeon_pool: List[str]) -> Tuple[int, int]: + def configure_dungeons(self, world: World, mq_dungeon_pool: list[str], empty_dungeon_pool: list[str]) -> tuple[int, int]: dist_num_mq, dist_num_empty = 0, 0 for (name, record) in self.dungeons.items(): if record.mq is not None: @@ -433,7 +435,7 @@ def configure_dungeons(self, world: "World", mq_dungeon_pool: List[str], empty_d world.empty_dungeons[name].empty = True return dist_num_mq, dist_num_empty - def configure_trials(self, trial_pool: List[str]) -> List[str]: + def configure_trials(self, trial_pool: list[str]) -> list[str]: dist_chosen = [] for (name, record) in self.trials.items(): if record.active is not None: @@ -442,7 +444,7 @@ def configure_trials(self, trial_pool: List[str]) -> List[str]: dist_chosen.append(name) return dist_chosen - def configure_songs(self) -> Dict[str, str]: + def configure_songs(self) -> dict[str, str]: dist_notes = {} for (name, record) in self.songs.items(): if record.notes is not None: @@ -450,7 +452,7 @@ def configure_songs(self) -> Dict[str, str]: return dist_notes # Add randomized_settings defined in distribution to world's randomized settings list - def configure_randomized_settings(self, world: "World") -> None: + def configure_randomized_settings(self, world: World) -> None: settings = world.settings for name, record in self.randomized_settings.items(): if not hasattr(settings, name): @@ -459,8 +461,8 @@ def configure_randomized_settings(self, world: "World") -> None: if name not in world.randomized_list: world.randomized_list.append(name) - def pool_remove_item(self, pools: List[List[Union[str, Item]]], item_name: str, count: int, - world_id: Optional[int] = None, use_base_pool: bool = True) -> List[Union[str, Item]]: + def pool_remove_item(self, pools: list[list[str | Item]], item_name: str, count: int, + world_id: Optional[int] = None, use_base_pool: bool = True) -> list[str | Item]: removed_items = [] base_remove_matcher = self.pattern_matcher(item_name) @@ -490,7 +492,7 @@ def pool_remove_item(self, pools: List[List[Union[str, Item]]], item_name: str, return removed_items - def pool_add_item(self, pool: List[str], item_name: str, count: int) -> List[str]: + def pool_add_item(self, pool: list[str], item_name: str, count: int) -> list[str]: if item_name == '#Junk': added_items = get_junk_item(count, pool=pool, plando_pool=self.item_pool) elif is_pattern(item_name): @@ -512,7 +514,7 @@ def pool_add_item(self, pool: List[str], item_name: str, count: int) -> List[str return added_items - def alter_pool(self, world: "World", pool: List[str]) -> List[str]: + def alter_pool(self, world: World, pool: list[str]) -> list[str]: self.base_pool = list(pool) pool_size = len(pool) bottle_matcher = self.pattern_matcher("#Bottle") @@ -616,7 +618,7 @@ def alter_pool(self, world: "World", pool: List[str]) -> List[str]: return pool - def set_complete_itempool(self, pool: List[Item]) -> None: + def set_complete_itempool(self, pool: list[Item]) -> None: self.item_pool = {} for item in pool: if item.dungeonitem or item.type in ('Drop', 'Event', 'DungeonReward'): @@ -626,13 +628,13 @@ def set_complete_itempool(self, pool: List[Item]) -> None: else: self.item_pool[item.name] = ItemPoolRecord() - def collect_starters(self, state: "State") -> None: + def collect_starters(self, state: State) -> None: for (name, record) in self.starting_items.items(): for _ in range(record.count): item = ItemFactory("Bottle" if name == "Bottle with Milk (Half)" else name) state.collect(item) - def pool_replace_item(self, item_pools: List[List[Item]], item_group: str, player_id: int, new_item: str, worlds: "List[World]") -> Item: + def pool_replace_item(self, item_pools: list[list[Item]], item_group: str, player_id: int, new_item: str, worlds: list[World]) -> Item: removed_item = self.pool_remove_item(item_pools, item_group, 1, world_id=player_id)[0] item_matcher = lambda item: self.pattern_matcher(new_item)(item.name) if self.item_pool[removed_item.name].count > 1: @@ -646,8 +648,8 @@ def pool_replace_item(self, item_pools: List[List[Item]], item_group: str, playe return ItemFactory(get_junk_item(1))[0] return random.choice(list(ItemIterator(item_matcher, worlds[player_id]))) - def set_shuffled_entrances(self, worlds: "List[World]", entrance_pools: Dict[str, List[Entrance]], target_entrance_pools: Dict[str, List[Entrance]], - locations_to_ensure_reachable: Iterable[Location], itempool: List[Item]) -> None: + def set_shuffled_entrances(self, worlds: list[World], entrance_pools: dict[str, list[Entrance]], target_entrance_pools: dict[str, list[Entrance]], + locations_to_ensure_reachable: Iterable[Location], itempool: list[Item]) -> None: for (name, record) in self.entrances.items(): if record.region is None: continue @@ -707,7 +709,7 @@ def set_shuffled_entrances(self, worlds: "List[World]", entrance_pools: Dict[str if not entrance_found: raise RuntimeError('Entrance does not belong to a pool of shuffled entrances in world %d: %s' % (self.id + 1, name)) - def pattern_dict_items(self, pattern_dict: Dict[str, Any]) -> Iterable[Tuple[str, Any]]: + def pattern_dict_items(self, pattern_dict: dict[str, Any]) -> Iterable[tuple[str, Any]]: """Retrieve a location by pattern. :param pattern_dict: the location dictionary. Capable of containing a pattern. @@ -726,7 +728,7 @@ def pattern_dict_items(self, pattern_dict: Dict[str, Any]) -> Iterable[Tuple[str else: yield key, value - def get_valid_items_from_record(self, itempool: List[Item], used_items: List[str], record: LocationRecord) -> List[str]: + def get_valid_items_from_record(self, itempool: list[Item], used_items: list[str], record: LocationRecord) -> list[str]: """Gets items that are valid for placement. :param itempool: a list of the item pool to search through for the record @@ -762,7 +764,7 @@ def get_valid_items_from_record(self, itempool: List[Item], used_items: List[str return valid_items - def pull_item_or_location(self, pools: List[List[Union[Item, Location]]], world: "World", name: str, remove: bool = True) -> Optional[Union[Item, Location]]: + def pull_item_or_location(self, pools: list[list[Item | Location]], world: World, name: str, remove: bool = True) -> Optional[Item | Location]: """Finds and removes (unless told not to do so) an item or location matching the criteria from a list of pools. :param pools: the item pools to pull from @@ -779,7 +781,7 @@ def pull_item_or_location(self, pools: List[List[Union[Item, Location]]], world: else: return pull_first_element(pools, lambda e: e.world is world and e.name == name, remove) - def fill_bosses(self, world: "World", prize_locs: List[Location], prizepool: List[Item]) -> int: + def fill_bosses(self, world: World, prize_locs: list[Location], prizepool: list[Item]) -> int: count = 0 used_items = [] for (name, record) in self.pattern_dict_items(self.locations): @@ -815,7 +817,7 @@ def fill_bosses(self, world: "World", prize_locs: List[Location], prizepool: Lis world.push_item(boss, reward, True) return count - def fill(self, worlds: "List[World]", location_pools: List[List[Location]], item_pools: List[List[Item]]) -> None: + def fill(self, worlds: list[World], location_pools: list[list[Location]], item_pools: list[list[Item]]) -> None: """Fills the world with restrictions defined in a plandomizer JSON distribution file. :param worlds: A list of the world objects that define the rules of each game world. @@ -913,8 +915,8 @@ def fill(self, worlds: "List[World]", location_pools: List[List[Location]], item if not search.can_beat_game(False): raise FillError('%s in world %d is not reachable without %s in world %d!' % (location.name, self.id + 1, item.name, player_id + 1)) - def get_item(self, ignore_pools: List[int], item_pools: List[List[Item]], location: Location, player_id: int, - record: LocationRecord, worlds: "List[World]") -> Item: + def get_item(self, ignore_pools: list[int], item_pools: list[list[Item]], location: Location, player_id: int, + record: LocationRecord, worlds: list[World]) -> Item: """Get or create the item specified by the record and replace something in the item pool with it :param ignore_pools: Pools to not replace items in @@ -995,7 +997,7 @@ def get_item(self, ignore_pools: List[int], item_pools: List[List[Item]], locati item_pools[i] = new_pool return item - def cloak(self, worlds: "List[World]", location_pools: List[List[Location]], model_pools: List[List[Item]]) -> None: + def cloak(self, worlds: list[World], location_pools: list[list[Location]], model_pools: list[list[Item]]) -> None: for (name, record) in self.pattern_dict_items(self.locations): if record.model is None: continue @@ -1019,7 +1021,7 @@ def cloak(self, worlds: "List[World]", location_pools: List[List[Location]], mod if can_cloak(location.item, model): location.item.looks_like_item = model - def configure_gossip(self, spoiler: Spoiler, stone_ids: List[int]) -> None: + def configure_gossip(self, spoiler: Spoiler, stone_ids: list[int]) -> None: for (name, record) in self.pattern_dict_items(self.gossip_stones): matcher = self.pattern_matcher(name) stone_id = pull_random_element([stone_ids], lambda id: matcher(gossipLocations[id].name)) @@ -1032,7 +1034,7 @@ def configure_gossip(self, spoiler: Spoiler, stone_ids: List[int]) -> None: raise RuntimeError('Gossip stone unknown or already assigned in world %d: %r. %s' % (self.id + 1, name, build_close_match(name, 'stone'))) spoiler.hints[self.id][stone_id] = GossipText(text=record.text, colors=record.colors, prefix='') - def give_items(self, world: "World", save_context: "SaveContext") -> None: + def give_items(self, world: World, save_context: SaveContext) -> None: # copy Triforce pieces to all worlds triforce_count = sum( world_dist.effective_starting_items['Triforce Piece'].count @@ -1055,7 +1057,7 @@ def get_starting_item(self, item: str) -> int: return 0 @property - def starting_items(self) -> Dict[str, StarterRecord]: + def starting_items(self) -> dict[str, StarterRecord]: data = defaultdict(lambda: StarterRecord(0)) world_names = ['World %d' % (i + 1) for i in range(len(self.distribution.world_dists))] @@ -1072,7 +1074,7 @@ def starting_items(self) -> Dict[str, StarterRecord]: return data - def configure_effective_starting_items(self, worlds: "List[World]", world: "World") -> None: + def configure_effective_starting_items(self, worlds: list[World], world: World) -> None: items = {item_name: record.copy() for item_name, record in self.starting_items.items()} if world.settings.start_with_rupees: @@ -1141,14 +1143,14 @@ def configure_effective_starting_items(self, worlds: "List[World]", world: "Worl class Distribution: - def __init__(self, settings: "Settings", src_dict: Optional[Dict[str, Any]] = None) -> None: - self.file_hash: Optional[List[str]] = None - self.playthrough: Optional[Dict[str, Dict[str, LocationRecord]]] = None - self.entrance_playthrough: Optional[Dict[str, Dict[str, EntranceRecord]]] = None - - self.src_dict: Dict[str, Any] = src_dict or {} - self.settings: "Settings" = settings - self.search_groups: Dict[str, Sequence[str]] = { + def __init__(self, settings: Settings, src_dict: Optional[dict[str, Any]] = None) -> None: + self.file_hash: Optional[list[str]] = None + self.playthrough: Optional[dict[str, dict[str, LocationRecord]]] = None + self.entrance_playthrough: Optional[dict[str, dict[str, EntranceRecord]]] = None + + self.src_dict: dict[str, Any] = src_dict or {} + self.settings: Settings = settings + self.search_groups: dict[str, Sequence[str]] = { **location_groups, **item_groups, } @@ -1158,7 +1160,7 @@ def __init__(self, settings: "Settings", src_dict: Optional[Dict[str, Any]] = No if 'starting_items' in self.src_dict: raise ValueError('"starting_items" at the top level is no longer supported, please move it into "settings"') - self.world_dists: List[WorldDistribution] = [WorldDistribution(self, id) for id in range(settings.world_count)] + self.world_dists: list[WorldDistribution] = [WorldDistribution(self, id) for id in range(settings.world_count)] # One-time init update_dict = { 'file_hash': (self.src_dict.get('file_hash', []) + [None, None, None, None, None])[0:5], @@ -1192,7 +1194,7 @@ def add_location(self, new_location: str, new_item: str) -> None: except KeyError: print('Cannot place item at excluded location because it already has an item defined in the Distribution.') - def fill(self, worlds: "List[World]", location_pools: List[List[Location]], item_pools: List[List[Item]]) -> None: + def fill(self, worlds: list[World], location_pools: list[list[Location]], item_pools: list[list[Item]]) -> None: search = Search.max_explore([world.state for world in worlds], itertools.chain.from_iterable(item_pools)) if not search.can_beat_game(False): raise FillError('Item pool does not contain items required to beat game!') @@ -1200,11 +1202,11 @@ def fill(self, worlds: "List[World]", location_pools: List[List[Location]], item for world_dist in self.world_dists: world_dist.fill(worlds, location_pools, item_pools) - def cloak(self, worlds: "List[World]", location_pools: List[List[Location]], model_pools: List[List[Item]]) -> None: + def cloak(self, worlds: list[World], location_pools: list[list[Location]], model_pools: list[list[Item]]) -> None: for world_dist in self.world_dists: world_dist.cloak(worlds, location_pools, model_pools) - def configure_triforce_hunt(self, worlds: "List[World]") -> None: + def configure_triforce_hunt(self, worlds: list[World]) -> None: total_count = 0 total_starting_count = 0 for world in worlds: @@ -1252,7 +1254,7 @@ def reset(self) -> None: # normalize starting items to use the dictionary format starting_items = itertools.chain(self.settings.starting_equipment, self.settings.starting_songs, self.settings.starting_inventory) - data: Dict[str, Union[StarterRecord, Dict[str, StarterRecord]]] = defaultdict(lambda: StarterRecord(0)) + data: dict[str, StarterRecord | dict[str, StarterRecord]] = defaultdict(lambda: StarterRecord(0)) if isinstance(self.settings.starting_items, dict) and self.settings.starting_items: world_names = ['World %d' % (i + 1) for i in range(len(self.world_dists))] for name, record in self.settings.starting_items.items(): @@ -1294,7 +1296,7 @@ def reset(self) -> None: data['Heart Container'].count += math.floor(num_hearts_to_collect / 2) self.settings.starting_items = data - def to_json(self, include_output: bool = True, spoiler: bool = True) -> Dict[str, Any]: + def to_json(self, include_output: bool = True, spoiler: bool = True) -> dict[str, Any]: self_dict = { ':version': __version__, 'file_hash': CollapseList(self.file_hash), @@ -1416,7 +1418,7 @@ def update_spoiler(self, spoiler: Spoiler, output_spoiler: bool) -> None: ent_rec_sphere[entrance_key] = EntranceRecord.from_entrance(entrance) @staticmethod - def from_file(settings: "Settings", filename: str) -> 'Distribution': + def from_file(settings: Settings, filename: str) -> Distribution: if any(map(filename.endswith, ['.z64', '.n64', '.v64'])): raise InvalidFileException("Your Ocarina of Time ROM doesn't belong in the plandomizer setting. If you don't know what plandomizer is, or don't plan to use it, leave that setting blank and try again.") @@ -1433,7 +1435,7 @@ def to_file(self, filename: str, output_spoiler: bool) -> None: outfile.write(json) -def add_starting_ammo(starting_items: Dict[str, StarterRecord]) -> None: +def add_starting_ammo(starting_items: dict[str, StarterRecord]) -> None: for item in StartingItems.inventory.values(): if item.item_name in starting_items and item.ammo: for ammo, qty in item.ammo.items(): @@ -1443,7 +1445,7 @@ def add_starting_ammo(starting_items: Dict[str, StarterRecord]) -> None: starting_items[ammo].count = qty[starting_items[item.item_name].count - 1] -def add_starting_item_with_ammo(starting_items: Dict[str, StarterRecord], item_name: str, count: int = 1) -> None: +def add_starting_item_with_ammo(starting_items: dict[str, StarterRecord], item_name: str, count: int = 1) -> None: if item_name not in starting_items: starting_items[item_name] = StarterRecord(0) starting_items[item_name].count += count @@ -1456,7 +1458,7 @@ def add_starting_item_with_ammo(starting_items: Dict[str, StarterRecord], item_n break -def strip_output_only(obj: Union[list, dict]) -> None: +def strip_output_only(obj: list | dict) -> None: if isinstance(obj, list): for elem in obj: strip_output_only(elem) @@ -1480,7 +1482,7 @@ def is_pattern(pattern: str) -> bool: return pattern.startswith('!') or pattern.startswith('*') or pattern.startswith('#') or pattern.endswith('*') -def pull_first_element(pools: List[List[Any]], predicate: Callable[[Any], bool] = lambda k: True, remove: bool = True) -> Optional[Any]: +def pull_first_element(pools: list[list[Any]], predicate: Callable[[Any], bool] = lambda k: True, remove: bool = True) -> Optional[Any]: for pool in pools: for element in pool: if predicate(element): @@ -1490,7 +1492,7 @@ def pull_first_element(pools: List[List[Any]], predicate: Callable[[Any], bool] return None -def pull_random_element(pools: List[List[Any]], predicate: Callable[[Any], bool] = lambda k: True, remove: bool = True) -> Optional[Any]: +def pull_random_element(pools: list[list[Any]], predicate: Callable[[Any], bool] = lambda k: True, remove: bool = True) -> Optional[Any]: candidates = [(element, pool) for pool in pools for element in pool if predicate(element)] if len(candidates) == 0: return None @@ -1500,7 +1502,7 @@ def pull_random_element(pools: List[List[Any]], predicate: Callable[[Any], bool] return element -def pull_all_elements(pools: List[List[Any]], predicate: Callable[[Any], bool] = lambda k: True, remove: bool = True) -> Optional[List[Any]]: +def pull_all_elements(pools: list[list[Any]], predicate: Callable[[Any], bool] = lambda k: True, remove: bool = True) -> Optional[list[Any]]: elements = [] for pool in pools: for element in pool: diff --git a/README.md b/README.md index 89cf5e091..ae1110b55 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ It is strongly suggested users use the web generator from here: https://ootrandomizer.com If you wish to run the script raw, clone this repository and either run ```Gui.py``` for a -graphical interface or ```OoTRandomizer.py``` for the command line version. They both require Python 3.6+. This will be fully featured, +graphical interface or ```OoTRandomizer.py``` for the command line version. They both require Python 3.7+. This will be fully featured, but the seeds you generate will have different random factors than the bundled release. To use the GUI, [NodeJS](https://nodejs.org/download/release/v18.12.1/) (v18 LTS, with npm) will additionally need to be installed. NodeJS v14.14.0 and earlier are no longer supported. The first time ```Gui.py``` is run it will need to install necessary components, which could take a few minutes. Subsequent instances will run much quicker. @@ -174,6 +174,7 @@ issue. You should always Hard Reset to avoid this issue entirely. * Junk items being sent to another world will now float up into the air to indicate this. * An unnecessary polygon check function is skipped to increase game performance. * In Triforce Hunt, your current and goal number of triforce pieces are now displayed on the file select screen. +* Python 3.6 is no longer supported. #### New Speedups * Various cutscenes removed or shortened, such as Water Temple and Gerudo Fortress gates and scarecrow spawn cutscenes. diff --git a/Region.py b/Region.py index 187293579..f26488e6e 100644 --- a/Region.py +++ b/Region.py @@ -1,5 +1,6 @@ +from __future__ import annotations from enum import Enum, unique -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from Dungeon import Dungeon @@ -32,14 +33,14 @@ class TimeOfDay: class Region: - def __init__(self, world: "World", name: str, region_type: RegionType = RegionType.Overworld) -> None: - self.world: "World" = world + def __init__(self, world: World, name: str, region_type: RegionType = RegionType.Overworld) -> None: + self.world: World = world self.name: str = name self.type: RegionType = region_type - self.entrances: "List[Entrance]" = [] - self.exits: "List[Entrance]" = [] - self.locations: "List[Location]" = [] - self.dungeon: "Optional[Dungeon]" = None + self.entrances: list[Entrance] = [] + self.exits: list[Entrance] = [] + self.locations: list[Location] = [] + self.dungeon: Optional[Dungeon] = None self.hint_name: Optional[str] = None self.alt_hint_name: Optional[str] = None self.price: Optional[int] = None @@ -47,9 +48,9 @@ def __init__(self, world: "World", name: str, region_type: RegionType = RegionTy self.provides_time: int = TimeOfDay.NONE self.scene: Optional[str] = None self.is_boss_room: bool = False - self.savewarp: "Optional[Entrance]" = None + self.savewarp: Optional[Entrance] = None - def copy(self, new_world: "World") -> 'Region': + def copy(self, new_world: World) -> Region: new_region = Region(new_world, self.name, self.type) new_region.price = self.price new_region.hint_name = self.hint_name @@ -67,7 +68,7 @@ def copy(self, new_world: "World") -> 'Region': return new_region @property - def hint(self) -> "Optional[HintArea]": + def hint(self) -> Optional[HintArea]: from Hints import HintArea if self.hint_name is not None: @@ -76,13 +77,13 @@ def hint(self) -> "Optional[HintArea]": return self.dungeon.hint @property - def alt_hint(self) -> "Optional[HintArea]": + def alt_hint(self) -> Optional[HintArea]: from Hints import HintArea if self.alt_hint_name is not None: return HintArea[self.alt_hint_name] - def can_fill(self, item: "Item", manual: bool = False) -> bool: + def can_fill(self, item: Item, manual: bool = False) -> bool: from Hints import HintArea if not manual and self.world.settings.empty_dungeons_mode != 'none' and item.dungeonitem: diff --git a/Rom.py b/Rom.py index f26c478df..35012a7a2 100644 --- a/Rom.py +++ b/Rom.py @@ -1,9 +1,11 @@ +from __future__ import annotations import copy import json import os import platform import subprocess -from typing import List, Tuple, Sequence, Iterator, Optional +from collections.abc import Iterator, Sequence +from typing import Optional from Models import restrictiveBytes from Utils import is_bundled, subprocess_args, local_path, data_path, get_version_bytes @@ -21,9 +23,9 @@ def __init__(self, file: Optional[str] = None) -> None: self.original: Rom = self self.changed_address: dict[int, int] = {} - self.changed_dma: dict[int, Tuple[int, int, int]] = {} - self.force_patch: List[int] = [] - self.dma: 'DMAIterator' = DMAIterator(self, DMADATA_START, DMADATA_INDEX) + self.changed_dma: dict[int, tuple[int, int, int]] = {} + self.force_patch: list[int] = [] + self.dma: DMAIterator = DMAIterator(self, DMADATA_START, DMADATA_INDEX) if file is None: return @@ -53,7 +55,7 @@ def __init__(self, file: Optional[str] = None) -> None: # Add version number to header. self.write_version_bytes() - def copy(self) -> 'Rom': + def copy(self) -> Rom: new_rom: Rom = Rom() new_rom.buffer = copy.copy(self.buffer) new_rom.changed_address = copy.copy(self.changed_address) @@ -273,7 +275,7 @@ def end(self) -> int: def size(self) -> int: return self.end - self.start - def as_tuple(self) -> Tuple[int, int, int]: + def as_tuple(self) -> tuple[int, int, int]: start, end = self.start, self.end return start, end, end - start diff --git a/RuleParser.py b/RuleParser.py index b331c4a8b..28a76afd1 100644 --- a/RuleParser.py +++ b/RuleParser.py @@ -1,8 +1,9 @@ +from __future__ import annotations import ast import logging import re from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Tuple, Set, Pattern, Union, Optional, Any +from typing import TYPE_CHECKING, Optional, Any from Entrance import Entrance from Item import ItemInfo, Item, make_event_item @@ -15,26 +16,26 @@ if TYPE_CHECKING: from World import World -escaped_items: Dict[str, str] = {} +escaped_items: dict[str, str] = {} for item in ItemInfo.items: escaped_items[escape_name(item)] = item -event_name: Pattern[str] = re.compile(r'[A-Z]\w+') +event_name: re.Pattern[str] = re.compile(r'[A-Z]\w+') # All generated lambdas must accept these keyword args! # For evaluation at a certain age (required as all rules are evaluated at a specific age) # or at a certain spot (can be omitted in many cases) # or at a specific time of day (often unused) -kwarg_defaults: Dict[str, Any] = { +kwarg_defaults: dict[str, Any] = { 'age': None, 'spot': None, 'tod': TimeOfDay.NONE, } -special_globals: Dict[str, Any] = {'TimeOfDay': TimeOfDay} +special_globals: dict[str, Any] = {'TimeOfDay': TimeOfDay} allowed_globals.update(special_globals) -rule_aliases: Dict[str, Tuple[List[Pattern[str]], str]] = {} -nonaliases: Set[str] = set() +rule_aliases: dict[str, tuple[list[re.Pattern[str]], str]] = {} +nonaliases: set[str] = set() def load_aliases() -> None: @@ -56,19 +57,19 @@ def isliteral(expr: ast.expr) -> bool: class Rule_AST_Transformer(ast.NodeTransformer): - def __init__(self, world: "World") -> None: - self.world: "World" = world - self.current_spot: Optional[Union[Location, Entrance]] = None - self.events: Set[str] = set() + def __init__(self, world: World) -> None: + self.world: World = world + self.current_spot: Optional[Location | Entrance] = None + self.events: set[str] = set() # map Region -> rule ast string -> item name - self.replaced_rules: Dict[str, Dict[str, ast.Call]] = defaultdict(dict) + self.replaced_rules: dict[str, dict[str, ast.Call]] = defaultdict(dict) # delayed rules need to keep: region name, ast node, event name - self.delayed_rules: List[Tuple[str, ast.AST, str]] = [] + self.delayed_rules: list[tuple[str, ast.AST, str]] = [] # lazy load aliases if not rule_aliases: load_aliases() # final rule cache - self.rule_cache: Dict[str, AccessRule] = {} + self.rule_cache: dict[str, AccessRule] = {} def visit_Name(self, node: ast.Name) -> Any: if node.id in dir(self): @@ -346,7 +347,7 @@ def visit_BoolOp(self, node: ast.BoolOp) -> Any: # Generates an ast.Call invoking the given State function 'name', # providing given args and keywords, and adding in additional # keyword args from kwarg_defaults (age, etc.) - def make_call(self, node: ast.AST, name: str, args: List[Any], keywords: List[ast.keyword]) -> ast.Call: + def make_call(self, node: ast.AST, name: str, args: list[Any], keywords: list[ast.keyword]) -> ast.Call: if not hasattr(State, name): raise Exception('Parse Error: No such function State.%s' % name, self.current_spot.name, ast.dump(node, False)) @@ -476,11 +477,11 @@ def at_night(self, node: ast.Call) -> ast.expr: # Parse entry point # If spot is None, here() rules won't work. - def parse_rule(self, rule_string: str, spot: Optional[Union[Location, Entrance]] = None) -> AccessRule: + def parse_rule(self, rule_string: str, spot: Optional[Location | Entrance] = None) -> AccessRule: self.current_spot = spot return self.make_access_rule(self.visit(ast.parse(rule_string, mode='eval').body)) - def parse_spot_rule(self, spot: Union[Location, Entrance]) -> None: + def parse_spot_rule(self, spot: Location | Entrance) -> None: rule = spot.rule_string.split('#', 1)[0].strip() access_rule = self.parse_rule(rule, spot) diff --git a/Rules.py b/Rules.py index c876fc717..a2110dd6d 100644 --- a/Rules.py +++ b/Rules.py @@ -1,5 +1,7 @@ +from __future__ import annotations import logging -from typing import TYPE_CHECKING, Collection, Iterable, Callable, Union +from collections.abc import Callable, Collection, Iterable +from typing import TYPE_CHECKING, Optional from ItemPool import song_list from Location import Location, DisableType @@ -13,7 +15,7 @@ from World import World -def set_rules(world: "World") -> None: +def set_rules(world: World) -> None: logger = logging.getLogger('') # ganon can only carry triforce @@ -85,7 +87,9 @@ def set_rules(world: "World") -> None: def create_shop_rule(location: Location) -> AccessRule: - def required_wallets(price: int) -> int: + def required_wallets(price: Optional[int]) -> int: + if price is None: + return 0 if price > 500: return 3 if price > 200: @@ -96,11 +100,11 @@ def required_wallets(price: int) -> int: return location.world.parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price)) -def set_rule(spot: "Union[Location, Entrance]", rule: AccessRule) -> None: +def set_rule(spot: Location | Entrance, rule: AccessRule) -> None: spot.access_rule = rule -def add_item_rule(spot: Location, rule: "Callable[[Location, Item], bool]") -> None: +def add_item_rule(spot: Location, rule: Callable[[Location, Item], bool]) -> None: old_rule = spot.item_rule spot.item_rule = lambda location, item: rule(location, item) and old_rule(location, item) @@ -110,12 +114,12 @@ def forbid_item(location: Location, item_name: str) -> None: location.item_rule = lambda loc, item: item.name != item_name and old_rule(loc, item) -def limit_to_itemset(location: Location, itemset: "Collection[Item]"): +def limit_to_itemset(location: Location, itemset: Collection[Item]): old_rule = location.item_rule location.item_rule = lambda loc, item: item.name in itemset and old_rule(loc, item) -def item_in_locations(state: State, item: "Item", locations: Iterable[Location]) -> bool: +def item_in_locations(state: State, item: Item, locations: Iterable[Location]) -> bool: for location in locations: if state.item_name(location) == item: return True @@ -128,7 +132,7 @@ def item_in_locations(state: State, item: "Item", locations: Iterable[Location]) # accessible when all items are obtained and every shop item is not. # This function should also be called when a world is copied if the original world # had called this function because the world.copy does not copy the rules -def set_shop_rules(world: "World"): +def set_shop_rules(world: World): found_bombchus = world.parser.parse_rule('found_bombchus') wallet = world.parser.parse_rule('Progressive_Wallet') wallet2 = world.parser.parse_rule('(Progressive_Wallet, 2)') @@ -163,7 +167,7 @@ def set_shop_rules(world: "World"): # This function should be run once after setting up entrances and before placing items # The goal is to automatically set item rules based on age requirements in case entrances were shuffled -def set_entrances_based_rules(worlds: "Collection[World]") -> None: +def set_entrances_based_rules(worlds: Collection[World]) -> None: # Use the states with all items available in the pools for this seed complete_itempool = [item for world in worlds for item in world.get_itempool_with_dungeon_items()] search = Search([world.state for world in worlds]) diff --git a/RulesCommon.py b/RulesCommon.py index f6a0f0dff..4db1d1c40 100644 --- a/RulesCommon.py +++ b/RulesCommon.py @@ -1,6 +1,7 @@ +from __future__ import annotations import re import sys -from typing import TYPE_CHECKING, Dict, Pattern, Callable, Any +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from State import State @@ -10,18 +11,19 @@ from typing import Protocol class AccessRule(Protocol): - def __call__(self, state: "State", **kwargs) -> bool: + def __call__(self, state: State, **kwargs) -> bool: ... else: + from typing import Callable AccessRule = Callable[["State"], bool] # Variable names and values used by rule execution, # will be automatically filled by Items -allowed_globals: Dict[str, Any] = {} +allowed_globals: dict[str, Any] = {} -_escape: Pattern[str] = re.compile(r'[\'()[\]-]') +_escape: re.Pattern[str] = re.compile(r'[\'()[\]-]') def escape_name(name: str) -> str: diff --git a/SaveContext.py b/SaveContext.py index 96f277e24..c1adb7085 100644 --- a/SaveContext.py +++ b/SaveContext.py @@ -1,6 +1,8 @@ +from __future__ import annotations import sys +from collections.abc import Callable, Iterable from enum import IntEnum -from typing import TYPE_CHECKING, Dict, List, Iterable, Callable, Optional, Union, Any +from typing import TYPE_CHECKING, Optional, Any from ItemPool import IGNORE_LOCATION @@ -13,7 +15,7 @@ from Rom import Rom from World import World -AddressesDict: TypeAlias = 'Dict[str, Union[Address, Dict[str, Union[Address, Dict[str, Address]]]]]' +AddressesDict: TypeAlias = "dict[str, Address | dict[str, Address | dict[str, Address]]]" class Scenes(IntEnum): @@ -54,14 +56,14 @@ class Address: prev_address: int = 0 EXTENDED_CONTEXT_START = 0x1450 - def __init__(self, address: Optional[int] = None, extended: bool = False, size: int = 4, mask: int = 0xFFFFFFFF, - max: Optional[int] = None, choices: Optional[Dict[str, int]] = None, value: Optional[str] = None) -> None: + def __init__(self, address: Optional[int] = None, extended: bool = False, size: int = 4, mask: int = 0xFFFFFFFF, max: Optional[int] = None, + choices: Optional[dict[str, int]] = None, value: Optional[str] = None) -> None: self.address: int = Address.prev_address if address is None else address if extended and address is not None: self.address += Address.EXTENDED_CONTEXT_START - self.value: Optional[Union[str, int]] = value + self.value: Optional[str | int] = value self.size: int = size - self.choices: Optional[Dict[str, int]] = choices + self.choices: Optional[dict[str, int]] = choices self.mask: int = mask Address.prev_address = self.address + self.size @@ -73,7 +75,7 @@ def __init__(self, address: Optional[int] = None, extended: bool = False, size: self.max: int = mask if max is None else max - def get_value(self, default: Union[str, int] = 0) -> Union[str, int]: + def get_value(self, default: str | int = 0) -> str | int: if self.value is None: return default return self.value @@ -116,7 +118,7 @@ def set_value_raw(self, value: int) -> None: self.value = value - def get_writes(self, save_context: 'SaveContext') -> None: + def get_writes(self, save_context: SaveContext) -> None: if self.value is None: return @@ -136,7 +138,7 @@ def get_writes(self, save_context: 'SaveContext') -> None: save_context.write_bits(self.address + i, byte, mask=mask) @staticmethod - def to_bytes(value: int, size: int) -> List[int]: + def to_bytes(value: int, size: int) -> list[int]: ret = [] for _ in range(size): ret.insert(0, value & 0xFF) @@ -146,8 +148,8 @@ def to_bytes(value: int, size: int) -> List[int]: class SaveContext: def __init__(self): - self.save_bits: Dict[int, int] = {} - self.save_bytes: Dict[int, int] = {} + self.save_bits: dict[int, int] = {} + self.save_bytes: dict[int, int] = {} self.addresses: AddressesDict = self.get_save_context_addresses() # will set the bits of value to the offset in the save (or'ing them with what is already there) @@ -232,7 +234,7 @@ def set_ammo_max(self) -> None: self.addresses['ammo'][ammo].max = ammo_max # will overwrite the byte at offset with the given value - def write_save_table(self, rom: "Rom") -> None: + def write_save_table(self, rom: Rom) -> None: self.set_ammo_max() for name, address in self.addresses.items(): self.write_save_entry(address) @@ -284,7 +286,7 @@ def give_health(self, health: float): self.addresses['health'].value = int(health) * 0x10 self.addresses['quest']['heart_pieces'].value = int((health % 1) * 4) * 0x10 - def give_item(self, world: "World", item: str, count: int = 1) -> None: + def give_item(self, world: World, item: str, count: int = 1) -> None: if item.endswith(')'): item_base, implicit_count = item[:-1].split(' (', 1) if implicit_count.isdigit(): @@ -443,7 +445,7 @@ def give_item(self, world: "World", item: str, count: int = 1) -> None: else: raise ValueError("Cannot give unknown starting item %s" % item) - def give_bombchu_item(self, world: "World") -> None: + def give_bombchu_item(self, world: World) -> None: self.give_item(world, "Bombchus", 0) def equip_default_items(self, age: str) -> None: @@ -886,7 +888,7 @@ def get_save_context_addresses() -> AddressesDict: } } - item_id_map: Dict[str, int] = { + item_id_map: dict[str, int] = { 'none' : 0xFF, 'stick' : 0x00, 'nut' : 0x01, @@ -1010,7 +1012,7 @@ def get_save_context_addresses() -> AddressesDict: 'small_key' : 0x67, } - slot_id_map: Dict[str, int] = { + slot_id_map: dict[str, int] = { 'stick' : 0x00, 'nut' : 0x01, 'bomb' : 0x02, @@ -1037,7 +1039,7 @@ def get_save_context_addresses() -> AddressesDict: 'child_trade' : 0x17, } - bottle_types: Dict[str, str] = { + bottle_types: dict[str, str] = { "Bottle" : 'bottle', "Bottle with Red Potion" : 'red_potion', "Bottle with Green Potion" : 'green_potion', @@ -1053,7 +1055,7 @@ def get_save_context_addresses() -> AddressesDict: "Bottle with Poe" : 'poe', } - save_writes_table: Dict[str, Dict[str, Any]] = { + save_writes_table: dict[str, dict[str, Any]] = { "Deku Stick Capacity": { 'item_slot.stick' : 'stick', 'upgrades.stick_upgrade' : [2, 3], @@ -1363,7 +1365,7 @@ def get_save_context_addresses() -> AddressesDict: 'Silver Rupee Pouch (Ganons Castle Forest Trial)': {'silver_rupee_counts.trials_forest': 5}, } - equipable_items: Dict[str, Dict[str, List[str]]] = { + equipable_items: dict[str, dict[str, list[str]]] = { 'equips_adult' : { 'items': [ 'hookshot', diff --git a/SceneFlags.py b/SceneFlags.py index 10d3f37eb..46d701bd9 100644 --- a/SceneFlags.py +++ b/SceneFlags.py @@ -1,5 +1,6 @@ +from __future__ import annotations from math import ceil -from typing import TYPE_CHECKING, Dict, List, Tuple +from typing import TYPE_CHECKING if TYPE_CHECKING: from Location import Location @@ -14,7 +15,7 @@ # } # where room_setup_number defines the room + scene setup as ((setup << 6) + room) for scene n # and max_flags is the highest used enemy flag for that setup/room -def get_collectible_flag_table(world: "World") -> "Tuple[Dict[int, Dict[int, int]], List[Tuple[Location, Tuple[int, int, int], Tuple[int, int, int]]]]": +def get_collectible_flag_table(world: World) -> tuple[dict[int, dict[int, int]], list[tuple[Location, tuple[int, int, int], tuple[int, int, int]]]]: scene_flags = {} alt_list = [] for i in range(0, 101): @@ -42,7 +43,7 @@ def get_collectible_flag_table(world: "World") -> "Tuple[Dict[int, Dict[int, int # Create a byte array from the scene flag table created by get_collectible_flag_table -def get_collectible_flag_table_bytes(scene_flag_table: Dict[int, Dict[int, int]]) -> Tuple[bytearray, int]: +def get_collectible_flag_table_bytes(scene_flag_table: dict[int, dict[int, int]]) -> tuple[bytearray, int]: num_flag_bytes = 0 bytes = bytearray() bytes.append(len(scene_flag_table.keys())) @@ -60,7 +61,7 @@ def get_collectible_flag_table_bytes(scene_flag_table: Dict[int, Dict[int, int]] return bytes, num_flag_bytes -def get_alt_list_bytes(alt_list: "List[Tuple[Location, Tuple[int, int, int], Tuple[int, int, int]]]") -> bytearray: +def get_alt_list_bytes(alt_list: list[tuple[Location, tuple[int, int, int], tuple[int, int, int]]]) -> bytearray: bytes = bytearray() for entry in alt_list: location, alt, primary = entry diff --git a/Search.py b/Search.py index 015024715..6c050d4b5 100644 --- a/Search.py +++ b/Search.py @@ -1,7 +1,10 @@ +from __future__ import annotations import copy import itertools import sys -from typing import TYPE_CHECKING, Dict, List, Tuple, Iterable, Set, Callable, Union, Optional +from collections.abc import Callable, Iterable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Optional from Region import Region, TimeOfDay from State import State @@ -17,20 +20,34 @@ from Location import Location from Goals import GoalCategory -ValidGoals: TypeAlias = Dict[str, Union[bool, Dict[str, Union[List[int], Dict[int, List[str]]]]]] -SearchCache: TypeAlias = "Dict[str, Union[List[Entrance], Set[Location], Dict[Region, int]]]" +ValidGoals: TypeAlias = "dict[str, bool | dict[str, list[int] | dict[int, list[str]]]]" + + +@dataclass +class SearchCache: + child_queue: list[Entrance] = field(default_factory=list) + adult_queue: list[Entrance] = field(default_factory=list) + visited_locations: set[Location] = field(default_factory=set) + child_regions: dict[Region, int] = field(default_factory=dict) + adult_regions: dict[Region, int] = field(default_factory=dict) + + def copy(self) -> SearchCache: + new = type(self)() + for name, value in self.__dict__.items(): + setattr(new, name, copy.copy(value)) + return new class Search: - def __init__(self, state_list: "Iterable[State]", initial_cache: Optional[SearchCache] = None) -> None: - self.state_list: List[State] = [state.copy() for state in state_list] + def __init__(self, state_list: Iterable[State], initial_cache: Optional[SearchCache] = None) -> None: + self.state_list: list[State] = [state.copy() for state in state_list] # Let the states reference this search. for state in self.state_list: state.search = self self._cache: SearchCache - self.cached_spheres: List[SearchCache] + self.cached_spheres: list[SearchCache] if initial_cache: self._cache = initial_cache self.cached_spheres = [self._cache] @@ -41,34 +58,33 @@ def __init__(self, state_list: "Iterable[State]", initial_cache: Optional[Search # values are lazily-determined tod flags (see TimeOfDay). # child_queue, adult_queue: queue of Entrance, all the exits to try next sphere # visited_locations: set of Locations visited in or before that sphere. - self._cache = { - 'child_queue': list(exit for region in root_regions for exit in region.exits), - 'adult_queue': list(exit for region in root_regions for exit in region.exits), - 'visited_locations': set(), - 'child_regions': {region: TimeOfDay.NONE for region in root_regions}, - 'adult_regions': {region: TimeOfDay.NONE for region in root_regions}, - } + self._cache = SearchCache( + child_queue=list(exit for region in root_regions for exit in region.exits), + adult_queue=list(exit for region in root_regions for exit in region.exits), + visited_locations=set(), + child_regions={region: TimeOfDay.NONE for region in root_regions}, + adult_regions={region: TimeOfDay.NONE for region in root_regions}, + ) self.cached_spheres = [self._cache] self.next_sphere() - def copy(self) -> 'Search': + def copy(self) -> Search: # we only need to copy the top sphere since that's what we're starting with and we don't go back - new_cache = {k: copy.copy(v) for k,v in self._cache.items()} # copy always makes a nonreversible instance - return Search(self.state_list, initial_cache=new_cache) + return Search(self.state_list, initial_cache=self._cache.copy()) - def collect_all(self, itempool: "Iterable[Item]") -> None: + def collect_all(self, itempool: Iterable[Item]) -> None: for item in itempool: if item.solver_id is not None and item.world is not None: self.state_list[item.world.id].collect(item) - def collect(self, item: "Item") -> None: + def collect(self, item: Item) -> None: if item.world is None: raise Exception(f"Item '{item.name}' cannot be collected as it does not have a world.") self.state_list[item.world.id].collect(item) @classmethod - def max_explore(cls, state_list: Iterable[State], itempool: "Optional[Iterable[Item]]" = None) -> 'Search': + def max_explore(cls, state_list: Iterable[State], itempool: Optional[Iterable[Item]] = None) -> Search: p = cls(state_list) if itempool: p.collect_all(itempool) @@ -76,7 +92,7 @@ def max_explore(cls, state_list: Iterable[State], itempool: "Optional[Iterable[I return p @classmethod - def with_items(cls, state_list: Iterable[State], itempool: "Optional[Iterable[Item]]" = None) -> 'Search': + def with_items(cls, state_list: Iterable[State], itempool: Optional[Iterable[Item]] = None) -> Search: p = cls(state_list) if itempool: p.collect_all(itempool) @@ -91,12 +107,12 @@ def with_items(cls, state_list: Iterable[State], itempool: "Optional[Iterable[It # Locations never visited in this Search are assumed to have been visited # in sphere 0, so unvisiting them will discard the entire cache. # Not safe to call during iteration. - def unvisit(self, location: "Location") -> None: + def unvisit(self, location: Location) -> None: raise Exception('Unimplemented for Search. Perhaps you want RewindableSearch.') # Drops the item from its respective state. # Has no effect on cache! - def uncollect(self, item: "Item") -> None: + def uncollect(self, item: Item) -> None: if item.world is None: raise Exception(f"Item '{item.name}' cannot be uncollected as it does not have a world.") self.state_list[item.world.id].remove(item) @@ -110,7 +126,7 @@ def reset(self) -> None: # Internal to the iteration. Modifies the exit_queue, regions. # Returns a queue of the exits whose access rule failed, # as a cache for the exits to try on the next iteration. - def _expand_regions(self, exit_queue: "List[Entrance]", regions: Dict[Region, int], age: Optional[str]) -> "List[Entrance]": + def _expand_regions(self, exit_queue: list[Entrance], regions: dict[Region, int], age: Optional[str]) -> list[Entrance]: failed = [] for exit in exit_queue: if exit.world and exit.connected_region and exit.connected_region not in regions: @@ -129,7 +145,7 @@ def _expand_regions(self, exit_queue: "List[Entrance]", regions: Dict[Region, in failed.append(exit) return failed - def _expand_tod_regions(self, regions: Dict[Region, int], goal_region: Region, age: Optional[str], tod: int) -> bool: + def _expand_tod_regions(self, regions: dict[Region, int], goal_region: Region, age: Optional[str], tod: int) -> bool: # grab all the exits from the regions with the given tod in the same world as our goal. # we want those that go to existing regions without the tod, until we reach the goal. has_tod_world = lambda regtod: regtod[1] & tod and regtod[0].world == goal_region.world @@ -150,18 +166,16 @@ def _expand_tod_regions(self, regions: Dict[Region, int], goal_region: Region, a # the regions accessible as adult, and the set of visited locations. # These are references to the new entry in the cache, so they can be modified # directly. - def next_sphere(self) -> "Tuple[Dict[Region, int], Dict[Region, int], Set[Location]]": + def next_sphere(self) -> tuple[dict[Region, int], dict[Region, int], set[Location]]: # Use the queue to iteratively add regions to the accessed set, # until we are stuck or out of regions. - self._cache.update({ - # Replace the queues (which have been modified) with just the - # failed exits that we can retry next time. - 'adult_queue': self._expand_regions( - self._cache['adult_queue'], self._cache['adult_regions'], 'adult'), - 'child_queue': self._expand_regions( - self._cache['child_queue'], self._cache['child_regions'], 'child'), - }) - return self._cache['child_regions'], self._cache['adult_regions'], self._cache['visited_locations'] + + # Replace the queues (which have been modified) with just the + # failed exits that we can retry next time. + self._cache.adult_queue = self._expand_regions(self._cache.adult_queue, self._cache.adult_regions, 'adult') + self._cache.child_queue = self._expand_regions(self._cache.child_queue, self._cache.child_regions, 'child') + + return self._cache.child_regions, self._cache.adult_regions, self._cache.visited_locations # Yields every reachable location, by iteratively deepening explored sets of regions # (one as child, one as adult) and invoking access rules. @@ -171,7 +185,7 @@ def next_sphere(self) -> "Tuple[Dict[Region, int], Dict[Region, int], Set[Locati # Inside the loop, the caller usually wants to collect items at these # locations to see if the game is beatable. Collection should be done # using internal State (recommended to just call search.collect). - def iter_reachable_locations(self, item_locations: "Iterable[Location]") -> "Iterable[Location]": + def iter_reachable_locations(self, item_locations: Iterable[Location]) -> Iterable[Location]: had_reachable_locations = True # will loop as long as any visits were made, and at least once while had_reachable_locations: @@ -201,20 +215,20 @@ def iter_reachable_locations(self, item_locations: "Iterable[Location]") -> "Ite # This collects all item locations available in the state list given that # the states have collected items. The purpose is that it will search for # all new items that become accessible with a new item set. - def collect_locations(self, item_locations: "Optional[Iterable[Location]]" = None) -> None: + def collect_locations(self, item_locations: Optional[Iterable[Location]] = None) -> None: item_locations = item_locations or self.progression_locations() for location in self.iter_reachable_locations(item_locations): # Collect the item for the state world it is for self.collect(location.item) # A shorthand way to iterate over locations without collecting items. - def visit_locations(self, locations: "Optional[Iterable[Location]]" = None) -> None: + def visit_locations(self, locations: Optional[Iterable[Location]] = None) -> None: locations = locations or self.progression_locations() for _ in self.iter_reachable_locations(locations): pass # Retrieve all item locations in the worlds that have progression items - def progression_locations(self) -> "List[Location]": + def progression_locations(self) -> list[Location]: return [location for state in self.state_list for location in state.world.get_locations() if location.item and location.item.advancement] # This returns True if every state is beatable. It's important to ensure @@ -244,7 +258,7 @@ def can_beat_game(self, scan_for_items: bool = True, predicate: Callable[[State] else: return False - def beatable_goals_fast(self, goal_categories: "Dict[str, GoalCategory]", world_filter: Optional[int] = None) -> ValidGoals: + def beatable_goals_fast(self, goal_categories: dict[str, GoalCategory], world_filter: Optional[int] = None) -> ValidGoals: valid_goals = self.test_category_goals(goal_categories, world_filter) if all(map(State.won, self.state_list)): valid_goals['way of the hero'] = True @@ -252,7 +266,7 @@ def beatable_goals_fast(self, goal_categories: "Dict[str, GoalCategory]", world_ valid_goals['way of the hero'] = False return valid_goals - def beatable_goals(self, goal_categories: "Dict[str, GoalCategory]") -> ValidGoals: + def beatable_goals(self, goal_categories: dict[str, GoalCategory]) -> ValidGoals: # collect all available items # make a new search since we might be iterating over one already search = self.copy() @@ -264,7 +278,7 @@ def beatable_goals(self, goal_categories: "Dict[str, GoalCategory]") -> ValidGoa valid_goals['way of the hero'] = False return valid_goals - def test_category_goals(self, goal_categories: "Dict[str, GoalCategory]", world_filter: Optional[int] = None) -> ValidGoals: + def test_category_goals(self, goal_categories: dict[str, GoalCategory], world_filter: Optional[int] = None) -> ValidGoals: valid_goals: ValidGoals = {} for category_name, category in goal_categories.items(): valid_goals[category_name] = {} @@ -292,12 +306,12 @@ def test_category_goals(self, goal_categories: "Dict[str, GoalCategory]", world_ valid_goals[category_name]['stateReverse'][state.world.id].append(goal.name) return valid_goals - def iter_pseudo_starting_locations(self) -> "Iterable[Location]": + def iter_pseudo_starting_locations(self) -> Iterable[Location]: for state in self.state_list: for location in state.world.distribution.skipped_locations: # We need to use the locations in the current world location = state.world.get_location(location.name) - self._cache['visited_locations'].add(location) + self._cache.visited_locations.add(location) yield location def collect_pseudo_starting_items(self) -> None: @@ -310,14 +324,14 @@ def collect_pseudo_starting_items(self) -> None: def can_reach(self, region: Region, age: Optional[str] = None, tod: int = TimeOfDay.NONE) -> bool: if age == 'adult': if tod: - return region in self._cache['adult_regions'] and (self._cache['adult_regions'][region] & tod or self._expand_tod_regions(self._cache['adult_regions'], region, age, tod)) + return region in self._cache.adult_regions and (self._cache.adult_regions[region] & tod or self._expand_tod_regions(self._cache.adult_regions, region, age, tod)) else: - return region in self._cache['adult_regions'] + return region in self._cache.adult_regions elif age == 'child': if tod: - return region in self._cache['child_regions'] and (self._cache['child_regions'][region] & tod or self._expand_tod_regions(self._cache['child_regions'], region, age, tod)) + return region in self._cache.child_regions and (self._cache.child_regions[region] & tod or self._expand_tod_regions(self._cache.child_regions, region, age, tod)) else: - return region in self._cache['child_regions'] + return region in self._cache.child_regions elif age == 'both': return self.can_reach(region, age='adult', tod=tod) and self.can_reach(region, age='child', tod=tod) else: @@ -330,21 +344,21 @@ def can_reach_spot(self, state: State, location_name: str, age: Optional[str] = # Use the cache in the search to determine location reachability. # Only works for locations that had progression items... - def visited(self, location: "Location") -> bool: - return location in self._cache['visited_locations'] + def visited(self, location: Location) -> bool: + return location in self._cache.visited_locations # Use the cache in the search to get all reachable regions. - def reachable_regions(self, age: Optional[str] = None) -> Set[Region]: + def reachable_regions(self, age: Optional[str] = None) -> set[Region]: if age == 'adult': - return set(self._cache['adult_regions'].keys()) + return set(self._cache.adult_regions.keys()) elif age == 'child': - return set(self._cache['child_regions'].keys()) + return set(self._cache.child_regions.keys()) else: - return set(self._cache['adult_regions'].keys()).union(self._cache['child_regions'].keys()) + return set(self._cache.adult_regions.keys()).union(self._cache.child_regions.keys()) # Returns whether the given age can access the spot at this age and tod, # by checking whether the search has reached the containing region, and evaluating the spot's access rule. - def spot_access(self, spot: "Union[Location, Entrance]", age: Optional[str] = None, tod: int = TimeOfDay.NONE) -> bool: + def spot_access(self, spot: Location | Entrance, age: Optional[str] = None, tod: int = TimeOfDay.NONE) -> bool: if age == 'adult' or age == 'child': return (self.can_reach(spot.parent_region, age=age, tod=tod) and spot.access_rule(self.state_list[spot.world.id], spot=spot, age=age, tod=tod)) @@ -360,16 +374,16 @@ def spot_access(self, spot: "Union[Location, Entrance]", age: Optional[str] = No class RewindableSearch(Search): - def unvisit(self, location: "Location") -> None: + def unvisit(self, location: Location) -> None: # A location being unvisited is either: # in the top two caches (if it's the first being unvisited for a sphere) # in the topmost cache only (otherwise) # After we unvisit every location in a sphere, the top two caches have identical visited locations. - assert location in self.cached_spheres[-1]['visited_locations'] - if location in self.cached_spheres[-2]['visited_locations']: + assert location in self.cached_spheres[-1].visited_locations + if location in self.cached_spheres[-2].visited_locations: self.cached_spheres.pop() self._cache = self.cached_spheres[-1] - self._cache['visited_locations'].discard(location) + self._cache.visited_locations.discard(location) def reset(self) -> None: self._cache = self.cached_spheres[0] @@ -378,7 +392,5 @@ def reset(self) -> None: # Adds a new layer to the sphere cache, as a copy of the previous. def checkpoint(self) -> None: # Save the current data into the cache. - self.cached_spheres.append({ - k: copy.copy(v) for k, v in self._cache.items() - }) + self.cached_spheres.append(self._cache.copy()) self._cache = self.cached_spheres[-1] diff --git a/SettingTypes.py b/SettingTypes.py index 8524aa49e..01916b379 100644 --- a/SettingTypes.py +++ b/SettingTypes.py @@ -1,12 +1,13 @@ +from __future__ import annotations import math import operator -from typing import Dict, Optional, Union, Any +from typing import Optional, Any # holds the info for a single setting class SettingInfo: def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Optional[str], shared: bool, - choices: Optional[Union[dict, list]] = None, default: Any = None, disabled_default: Any = None, + choices: Optional[dict | list] = None, default: Any = None, disabled_default: Any = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: self.type: type = setting_type # type of the setting's value, used to properly convert types to setting strings @@ -15,7 +16,7 @@ def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Option self.gui_text: Optional[str] = gui_text self.gui_type: Optional[str] = gui_type self.gui_tooltip: Optional[str] = "" if gui_tooltip is None else gui_tooltip - self.gui_params: Dict[str, Any] = {} if gui_params is None else gui_params # additional parameters that the randomizer uses for the gui + self.gui_params: dict[str, Any] = {} if gui_params is None else gui_params # additional parameters that the randomizer uses for the gui self.disable: Optional[dict] = disable # dictionary of settings this setting disabled self.dependency = None # lambda that determines if this is disabled. Generated later @@ -130,7 +131,7 @@ def __set__(self, obj, value: bool) -> None: class SettingInfoStr(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool = False, - choices: Optional[Union[dict, list]] = None, default: Optional[str] = None, + choices: Optional[dict | list] = None, default: Optional[str] = None, disabled_default: Optional[str] = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(setting_type=str, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, @@ -151,7 +152,7 @@ def __set__(self, obj, value: str) -> None: class SettingInfoInt(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, - choices: Optional[Union[dict, list]] = None, default: Optional[int] = None, + choices: Optional[dict | list] = None, default: Optional[int] = None, disabled_default: Optional[int] = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(setting_type=int, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, @@ -172,7 +173,7 @@ def __set__(self, obj, value: int) -> None: class SettingInfoList(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, - choices: Optional[Union[dict, list]] = None, default: Optional[list] = None, + choices: Optional[dict | list] = None, default: Optional[list] = None, disabled_default: Optional[list] = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(setting_type=list, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, @@ -193,7 +194,7 @@ def __set__(self, obj, value: list) -> None: class SettingInfoDict(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, - choices: Optional[Union[dict, list]] = None, default: Optional[dict] = None, + choices: Optional[dict | list] = None, default: Optional[dict] = None, disabled_default: Optional[dict] = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(setting_type=dict, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, @@ -234,7 +235,7 @@ def __init__(self, gui_text: Optional[str], gui_tooltip: Optional[str] = None, d class Combobox(SettingInfoStr): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]], default: Optional[str], + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[str], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Combobox', shared=shared, choices=choices, default=default, @@ -243,7 +244,7 @@ def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] class Radiobutton(SettingInfoStr): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]], default: Optional[str], + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[str], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Radiobutton', shared=shared, choices=choices, default=default, @@ -252,7 +253,7 @@ def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] class Fileinput(SettingInfoStr): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] = None, default: Optional[str] = None, + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Fileinput', shared=shared, choices=choices, default=default, @@ -261,7 +262,7 @@ def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] class Directoryinput(SettingInfoStr): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] = None, default: Optional[str] = None, + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Directoryinput', shared=shared, choices=choices, default=default, @@ -270,7 +271,7 @@ def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] class Textinput(SettingInfoStr): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] = None, default: Optional[str] = None, + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Textinput', shared=shared, choices=choices, default=default, @@ -279,7 +280,7 @@ def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] class ComboboxInt(SettingInfoInt): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]], default: Optional[int], + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[int], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[int] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Combobox', shared=shared, choices=choices, default=default, @@ -324,7 +325,7 @@ def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: Opt class MultipleSelect(SettingInfoList): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]], default: Optional[list], + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[list], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[list] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='MultipleSelect', shared=shared, choices=choices, default=default, @@ -333,7 +334,7 @@ def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]] class SearchBox(SettingInfoList): - def __init__(self, gui_text: Optional[str], choices: Optional[Union[dict, list]], default: Optional[list], + def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[list], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[list] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='SearchBox', shared=shared, choices=choices, default=default, diff --git a/Settings.py b/Settings.py index 75d1e29d3..c4cd53b30 100644 --- a/Settings.py +++ b/Settings.py @@ -1,5 +1,5 @@ +from __future__ import annotations import argparse -from collections.abc import Iterable import copy import hashlib import json @@ -10,7 +10,8 @@ import string import sys import textwrap -from typing import Dict, List, Tuple, Set, Any, Optional +from collections.abc import Iterable +from typing import Any, Optional import StartingItems from version import __version__ @@ -18,7 +19,7 @@ from SettingsList import SettingInfos, validate_settings from Plandomizer import Distribution -LEGACY_STARTING_ITEM_SETTINGS: Dict[str, Dict[str, StartingItems.Entry]] = { +LEGACY_STARTING_ITEM_SETTINGS: dict[str, dict[str, StartingItems.Entry]] = { 'starting_equipment': StartingItems.equipment, 'starting_inventory': StartingItems.inventory, 'starting_songs': StartingItems.songs, @@ -34,11 +35,11 @@ def _get_help_string(self, action) -> Optional[str]: # 32 characters letters: str = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" -index_to_letter: Dict[int, str] = {i: letters[i] for i in range(32)} -letter_to_index: Dict[str, int] = {v: k for k, v in index_to_letter.items()} +index_to_letter: dict[int, str] = {i: letters[i] for i in range(32)} +letter_to_index: dict[str, int] = {v: k for k, v in index_to_letter.items()} -def bit_string_to_text(bits: List[int]) -> str: +def bit_string_to_text(bits: list[int]) -> str: # pad the bits array to be multiple of 5 if len(bits) % 5 > 0: bits += [0] * (5 - len(bits) % 5) @@ -53,7 +54,7 @@ def bit_string_to_text(bits: List[int]) -> str: return result -def text_to_bit_string(text: str) -> List[int]: +def text_to_bit_string(text: str) -> list[int]: bits = [] for c in text: index = letter_to_index[c] @@ -62,7 +63,7 @@ def text_to_bit_string(text: str) -> List[int]: return bits -def get_preset_files() -> List[str]: +def get_preset_files() -> list[str]: return [data_path('presets_default.json')] + sorted( os.path.join(data_path('Presets'), fn) for fn in os.listdir(data_path('Presets')) @@ -72,7 +73,7 @@ def get_preset_files() -> List[str]: # holds the particular choices for a run's settings class Settings(SettingInfos): # add the settings as fields, and calculate information based on them - def __init__(self, settings_dict: Dict[str, Any], strict: bool = False) -> None: + def __init__(self, settings_dict: dict[str, Any], strict: bool = False) -> None: super().__init__() self.numeric_seed: Optional[int] = None if settings_dict.get('compress_rom', None): @@ -93,13 +94,13 @@ def __init__(self, settings_dict: Dict[str, Any], strict: bool = False) -> None: if self.world_count > 255: self.world_count = 255 - self._disabled: Set[str] = set() + self._disabled: set[str] = set() self.settings_string: str = self.get_settings_string() self.distribution: Distribution = Distribution(self) self.update_seed(self.seed) self.custom_seed: bool = False - def copy(self) -> 'Settings': + def copy(self) -> Settings: settings = copy.copy(self) settings.settings_dict = copy.deepcopy(settings.settings_dict) return settings @@ -345,7 +346,7 @@ def resolve_random_settings(self, cosmetic: bool, randomize_key: Optional[str] = for randomize_keys in randomize_keys_enabled: self.settings_dict[randomize_keys] = True - def to_json(self, *, legacy_starting_items: bool = False) -> Dict[str, Any]: + def to_json(self, *, legacy_starting_items: bool = False) -> dict[str, Any]: if legacy_starting_items: settings = self.copy() for setting_name, items in LEGACY_STARTING_ITEM_SETTINGS.items(): @@ -378,12 +379,12 @@ def to_json(self, *, legacy_starting_items: bool = False) -> Dict[str, Any]: and (setting.name != 'starting_items' or not legacy_starting_items) } - def to_json_cosmetics(self) -> Dict[str, Any]: + def to_json_cosmetics(self) -> dict[str, Any]: return {setting.name: self.settings_dict[setting.name] for setting in self.setting_infos.values() if setting.cosmetic} # gets the randomizer settings, whether to open the gui, and the logger level from command line arguments -def get_settings_from_command_line_args() -> Tuple[Settings, bool, str, bool, str]: +def get_settings_from_command_line_args() -> tuple[Settings, bool, str, bool, str]: parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument('--gui', help='Launch the GUI', action='store_true') diff --git a/SettingsList.py b/SettingsList.py index 0027c33f1..6b49a3a76 100644 --- a/SettingsList.py +++ b/SettingsList.py @@ -1,6 +1,8 @@ +from __future__ import annotations import difflib import json -from typing import TYPE_CHECKING, Dict, List, Iterable, Union, Optional, Any +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional, Any import Colors from Hints import hint_dist_list, hint_dist_tips, gossipLocations @@ -4897,11 +4899,11 @@ class SettingInfos: } ) - setting_infos: Dict[str, SettingInfo] = {} + setting_infos: dict[str, SettingInfo] = {} setting_map: dict = {} def __init__(self) -> None: - self.settings_dict: Dict[str, Any] = {} + self.settings_dict: dict[str, Any] = {} def get_settings_from_section(section_name: str) -> Iterable[str]: @@ -4932,7 +4934,7 @@ def is_mapped(setting_name: str) -> bool: # When a string isn't found in the source list, attempt to get the closest match from the list # ex. Given "Recovery Hart" returns "Did you mean 'Recovery Heart'?" -def build_close_match(name: str, value_type: str, source_list: "Optional[Union[List[str], Dict[str, List[Entrance]]]]" = None) -> str: +def build_close_match(name: str, value_type: str, source_list: Optional[list[str] | dict[str, list[Entrance]]] = None) -> str: source = [] if value_type == 'item': source = ItemInfo.items.keys() @@ -4955,7 +4957,7 @@ def build_close_match(name: str, value_type: str, source_list: "Optional[Union[L return "" # No matches -def validate_settings(settings_dict: Dict[str, Any], *, check_conflicts: bool = True) -> None: +def validate_settings(settings_dict: dict[str, Any], *, check_conflicts: bool = True) -> None: for setting, choice in settings_dict.items(): # Ensure the supplied setting name is a real setting if setting not in SettingInfos.setting_infos: @@ -4993,7 +4995,7 @@ def validate_settings(settings_dict: Dict[str, Any], *, check_conflicts: bool = validate_disabled_setting(settings_dict, setting, choice, other_setting) -def validate_disabled_setting(settings_dict: Dict[str, Any], setting: str, choice, other_setting: str) -> None: +def validate_disabled_setting(settings_dict: dict[str, Any], setting: str, choice, other_setting: str) -> None: if other_setting in settings_dict: if settings_dict[other_setting] != SettingInfos.setting_infos[other_setting].disabled_default: raise ValueError(f'The {other_setting!r} setting cannot be used since {setting!r} is set to {choice!r}') diff --git a/SettingsListTricks.py b/SettingsListTricks.py index 7a1d97ba3..ed3f09429 100644 --- a/SettingsListTricks.py +++ b/SettingsListTricks.py @@ -1,11 +1,11 @@ -from typing import Dict, Tuple, Union +from __future__ import annotations # Below is the list of possible glitchless tricks. # The order they are listed in is also the order in which # they appear to the user in the GUI, so a sensible order was chosen -logic_tricks: Dict[str, Dict[str, Union[str, Tuple[str, ...]]]] = { +logic_tricks: dict[str, dict[str, str | tuple[str, ...]]] = { # General tricks diff --git a/SettingsToJson.py b/SettingsToJson.py index 2908def32..0dddae3ef 100755 --- a/SettingsToJson.py +++ b/SettingsToJson.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 +from __future__ import annotations import copy import json import sys -from typing import Dict, List, Any, Optional +from typing import Any, Optional from Hints import hint_dist_files from SettingsList import SettingInfos, get_settings_from_section, get_settings_from_tab from Utils import data_path -tab_keys: List[str] = ['text', 'app_type', 'footer'] -section_keys: List[str] = ['text', 'app_type', 'is_colors', 'is_sfx', 'col_span', 'row_span', 'subheader'] -setting_keys: List[str] = ['hide_when_disabled', 'min', 'max', 'size', 'max_length', 'file_types', 'no_line_break', 'function', 'option_remove', 'dynamic'] -types_with_options: List[str] = ['Checkbutton', 'Radiobutton', 'Combobox', 'SearchBox', 'MultipleSelect'] +tab_keys: list[str] = ['text', 'app_type', 'footer'] +section_keys: list[str] = ['text', 'app_type', 'is_colors', 'is_sfx', 'col_span', 'row_span', 'subheader'] +setting_keys: list[str] = ['hide_when_disabled', 'min', 'max', 'size', 'max_length', 'file_types', 'no_line_break', 'function', 'option_remove', 'dynamic'] +types_with_options: list[str] = ['Checkbutton', 'Radiobutton', 'Combobox', 'SearchBox', 'MultipleSelect'] def remove_trailing_lines(text: str) -> str: @@ -34,7 +35,7 @@ def deep_update(source: dict, new_dict: dict) -> dict: return source -def add_disable_option_to_json(disable_option: Dict[str, Any], option_json: Dict[str, Any]) -> None: +def add_disable_option_to_json(disable_option: dict[str, Any], option_json: dict[str, Any]) -> None: if disable_option.get('settings') is not None: if 'controls_visibility_setting' not in option_json: option_json['controls_visibility_setting'] = ','.join(disable_option['settings']) @@ -52,7 +53,7 @@ def add_disable_option_to_json(disable_option: Dict[str, Any], option_json: Dict option_json['controls_visibility_tab'] += ',' + ','.join(disable_option['tabs']) -def get_setting_json(setting: str, web_version: bool, as_array: bool = False) -> Optional[Dict[str, Any]]: +def get_setting_json(setting: str, web_version: bool, as_array: bool = False) -> Optional[dict[str, Any]]: try: setting_info = SettingInfos.setting_infos[setting] except KeyError: @@ -64,7 +65,7 @@ def get_setting_json(setting: str, web_version: bool, as_array: bool = False) -> if setting_info.gui_text is None: return None - setting_json: Dict[str, Any] = { + setting_json: dict[str, Any] = { 'options': [], 'default': setting_info.default, 'text': setting_info.gui_text, @@ -180,7 +181,7 @@ def get_setting_json(setting: str, web_version: bool, as_array: bool = False) -> return setting_json -def get_section_json(section: Dict[str, Any], web_version: bool, as_array: bool = False) -> Dict[str, Any]: +def get_section_json(section: dict[str, Any], web_version: bool, as_array: bool = False) -> dict[str, Any]: if as_array: section_json = { 'name' : section['name'], @@ -205,7 +206,7 @@ def get_section_json(section: Dict[str, Any], web_version: bool, as_array: bool return section_json -def get_tab_json(tab: Dict[str, Any], web_version: bool, as_array: bool = False) -> Dict[str, Any]: +def get_tab_json(tab: dict[str, Any], web_version: bool, as_array: bool = False) -> dict[str, Any]: if as_array: tab_json = { 'name' : tab['name'], diff --git a/Sounds.py b/Sounds.py index f4827e3cb..2bef54aea 100644 --- a/Sounds.py +++ b/Sounds.py @@ -24,19 +24,13 @@ # hook would contain a bunch of addresses, whether they share the same default # value or not. +from __future__ import annotations import os -import sys +from dataclasses import dataclass from enum import Enum -from typing import Dict, List from Utils import data_path -# Python 3.6 support. We can drop the conditional usage of namedtuple if we decide to no longer support Python 3.6. -if sys.version_info >= (3, 7): - from dataclasses import dataclass -else: - from collections import namedtuple - class Tags(Enum): LOOPED = 0 @@ -56,15 +50,12 @@ class Tags(Enum): # I'm now thinking it has to do with a limit of concurrent sounds) -if sys.version_info >= (3, 7): - @dataclass(frozen=True) - class Sound: - id: int - keyword: str - label: str - tags: List[Tags] -else: - Sound = namedtuple('Sound', 'id keyword label tags') +@dataclass(frozen=True) +class Sound: + id: int + keyword: str + label: str + tags: list[Tags] class Sounds(Enum): @@ -171,17 +162,6 @@ class Sounds(Enum): ZELDA_ADULT_GASP = Sound(0x6879, 'adult-zelda-gasp', 'Zelda Gasp (Adult)', [Tags.NAVI, Tags.HPLOW]) -if sys.version_info >= (3, 7): - @dataclass(frozen=True) - class SoundHook: - name: str - pool: List[Sounds] - locations: List[int] - sfx_flag: bool -else: - SoundHook = namedtuple('SoundHook', 'name pool locations sfx_flag') - - # Sound pools standard = [s for s in Sounds if Tags.LOOPED not in s.value.tags] looping = [s for s in Sounds if Tags.LOOPED in s.value.tags] @@ -195,6 +175,14 @@ class SoundHook: horse_neigh = [s for s in Sounds if Tags.HORSE in s.value.tags] +@dataclass(frozen=True) +class SoundHook: + name: str + pool: list[Sounds] + locations: list[int] + sfx_flag: bool + + class SoundHooks(Enum): # name pool locations sfx_flag NAVI_OVERWORLD = SoundHook('Navi - Overworld', navi, [0xAE7EF2, 0xC26C7E], False) @@ -236,11 +224,11 @@ class SoundHooks(Enum): # SWORD_SLASH = SoundHook('Sword Slash', standard, [0xAC2942]) -def get_patch_dict() -> Dict[str, int]: +def get_patch_dict() -> dict[str, int]: return {s.value.keyword: s.value.id for s in Sounds} -def get_hook_pool(sound_hook: SoundHooks, earsafeonly: bool = False) -> List[Sounds]: +def get_hook_pool(sound_hook: SoundHooks, earsafeonly: bool = False) -> list[Sounds]: if earsafeonly: list = [s for s in sound_hook.value.pool if Tags.PAINFUL not in s.value.tags] return list @@ -248,7 +236,7 @@ def get_hook_pool(sound_hook: SoundHooks, earsafeonly: bool = False) -> List[Sou return sound_hook.value.pool -def get_setting_choices(sound_hook: SoundHooks) -> Dict[str, str]: +def get_setting_choices(sound_hook: SoundHooks) -> dict[str, str]: pool = sound_hook.value.pool choices = {s.value.keyword: s.value.label for s in sorted(pool, key=lambda s: s.value.label)} result = { @@ -262,7 +250,7 @@ def get_setting_choices(sound_hook: SoundHooks) -> Dict[str, str]: return result -def get_voice_sfx_choices(age: int, include_random: bool = True) -> List[str]: +def get_voice_sfx_choices(age: int, include_random: bool = True) -> list[str]: # Dynamically populate the SettingsList entry for the voice effects # Voice packs should be a folder of .bin files in the Voices/{age} directory names = ['Default', 'Silent'] diff --git a/Spoiler.py b/Spoiler.py index 79a5c0c4c..7bcdea3e9 100644 --- a/Spoiler.py +++ b/Spoiler.py @@ -1,7 +1,8 @@ +from __future__ import annotations from collections import OrderedDict import logging import random -from typing import TYPE_CHECKING, List, Dict +from typing import TYPE_CHECKING from Item import Item from LocationList import location_sort_order @@ -15,7 +16,7 @@ from Settings import Settings from World import World -HASH_ICONS: List[str] = [ +HASH_ICONS: list[str] = [ 'Deku Stick', 'Deku Nut', 'Bow', @@ -52,20 +53,20 @@ class Spoiler: - def __init__(self, worlds: "List[World]") -> None: - self.worlds: "List[World]" = worlds - self.settings: "Settings" = worlds[0].settings - self.playthrough: "Dict[str, List[Location]]" = {} - self.entrance_playthrough: "Dict[str, List[Entrance]]" = {} - self.full_playthrough: Dict[str, int] = {} + def __init__(self, worlds: list[World]) -> None: + self.worlds: list[World] = worlds + self.settings: Settings = worlds[0].settings + self.playthrough: dict[str, list[Location]] = {} + self.entrance_playthrough: dict[str, list[Entrance]] = {} + self.full_playthrough: dict[str, int] = {} self.max_sphere: int = 0 - self.locations: "Dict[int, Dict[str, Item]]" = {} - self.entrances: "Dict[int, List[Entrance]]" = {} - self.required_locations: "Dict[int, List[Location]]" = {} - self.goal_locations: "Dict[int, Dict[str, Dict[str, Dict[int, List[Location]]]]]" = {} - self.goal_categories: "Dict[int, Dict[str, GoalCategory]]" = {} - self.hints: "Dict[int, Dict[int, GossipText]]" = {world.id: {} for world in worlds} - self.file_hash: List[int] = [] + self.locations: dict[int, dict[str, Item]] = {} + self.entrances: dict[int, list[Entrance]] = {} + self.required_locations: dict[int, list[Location]] = {} + self.goal_locations: dict[int, dict[str, dict[str, dict[int, list[Location]]]]] = {} + self.goal_categories: dict[int, dict[str, GoalCategory]] = {} + self.hints: dict[int, dict[int, GossipText]] = {world.id: {} for world in worlds} + self.file_hash: list[int] = [] def build_file_hash(self) -> None: dist_file_hash = self.settings.distribution.file_hash @@ -116,7 +117,7 @@ def parse_data(self) -> None: spoiler_entrances.sort(key=lambda entrance: entrance_sort_order.get(entrance.type, -1)) self.entrances[world.id] = spoiler_entrances - def copy_worlds(self) -> "List[World]": + def copy_worlds(self) -> list[World]: worlds = [world.copy() for world in self.worlds] Item.fix_worlds_after_copy(worlds) return worlds diff --git a/StartingItems.py b/StartingItems.py index f922ebd9e..7a79ef8ca 100644 --- a/StartingItems.py +++ b/StartingItems.py @@ -1,12 +1,13 @@ +from __future__ import annotations from collections import namedtuple from itertools import chain -from typing import Dict, List, Tuple, Optional +from typing import Optional Entry = namedtuple("Entry", ['setting_name', 'item_name', 'available', 'gui_text', 'special', 'ammo', 'i']) def _entry(setting_name: str, item_name: Optional[str] = None, available: int = 1, gui_text: Optional[str] = None, - special: bool = False, ammo: Optional[Dict[str, Tuple[int, ...]]] = None) -> List[Tuple[str, Entry]]: + special: bool = False, ammo: Optional[dict[str, tuple[int, ...]]] = None) -> list[tuple[str, Entry]]: if item_name is None: item_name = setting_name.capitalize() if gui_text is None: @@ -22,7 +23,7 @@ def _entry(setting_name: str, item_name: Optional[str] = None, available: int = # Ammo items must be declared in ItemList.py. -inventory: Dict[str, Entry] = dict(chain( +inventory: dict[str, Entry] = dict(chain( _entry('deku_stick', 'Deku Stick Capacity', available=2, ammo={'Deku Sticks': (20, 30)}), _entry('deku_nut', 'Deku Nut Capacity', available=2, ammo={'Deku Nuts': (30, 40)}), _entry('bombs', 'Bomb Bag', available=3, ammo={'Bombs': (20, 30, 40)}), @@ -67,7 +68,7 @@ def _entry(setting_name: str, item_name: Optional[str] = None, available: int = _entry("mask_of_truth","Mask of Truth", gui_text="Mask of Truth"), )) -songs: Dict[str, Entry] = dict(chain( +songs: dict[str, Entry] = dict(chain( _entry('lullaby', 'Zeldas Lullaby', gui_text="Zelda's Lullaby"), _entry('eponas_song', 'Eponas Song', gui_text="Epona's Song"), _entry('sarias_song', 'Sarias Song', gui_text="Saria's Song"), @@ -82,7 +83,7 @@ def _entry(setting_name: str, item_name: Optional[str] = None, available: int = _entry('prelude', 'Prelude of Light'), )) -equipment: Dict[str, Entry] = dict(chain( +equipment: dict[str, Entry] = dict(chain( _entry('kokiri_sword', 'Kokiri Sword'), _entry('giants_knife', 'Giants Knife'), _entry('biggoron_sword', 'Biggoron Sword'), @@ -101,4 +102,4 @@ def _entry(setting_name: str, item_name: Optional[str] = None, available: int = _entry('defense', 'Double Defense'), )) -everything: Dict[str, Entry] = {**equipment, **inventory, **songs} +everything: dict[str, Entry] = {**equipment, **inventory, **songs} diff --git a/State.py b/State.py index 249cb0c6b..17cc02eae 100644 --- a/State.py +++ b/State.py @@ -1,4 +1,6 @@ -from typing import TYPE_CHECKING, Dict, List, Iterable, Optional, Union, Any +from __future__ import annotations +from collections.abc import Iterable +from typing import TYPE_CHECKING, Optional, Any from Item import Item, ItemInfo from RulesCommon import escape_name @@ -16,12 +18,12 @@ class State: - def __init__(self, parent: "World") -> None: - self.solv_items: List[int] = [0] * len(ItemInfo.solver_ids) - self.world: "World" = parent - self.search: "Optional[Search]" = None + def __init__(self, parent: World) -> None: + self.solv_items: list[int] = [0] * len(ItemInfo.solver_ids) + self.world: World = parent + self.search: Optional[Search] = None - def copy(self, new_world: "Optional[World]" = None) -> 'State': + def copy(self, new_world: Optional[World] = None) -> State: if not new_world: new_world = self.world new_state = State(new_world) @@ -29,7 +31,7 @@ def copy(self, new_world: "Optional[World]" = None) -> 'State': new_state.solv_items[i] = val return new_state - def item_name(self, location: "Union[str, Location]") -> Optional[str]: + def item_name(self, location: str | Location) -> Optional[str]: location = self.world.get_location(location) if location.item is None: return None @@ -96,10 +98,10 @@ def has_dungeon_rewards(self, count: int) -> bool: return (self.count_of(ItemInfo.medallion_ids) + self.count_of(ItemInfo.stone_ids)) >= count # TODO: Store the item's solver id in the goal - def has_item_goal(self, item_goal: Dict[str, Any]) -> bool: + def has_item_goal(self, item_goal: dict[str, Any]) -> bool: return self.solv_items[ItemInfo.solver_ids[escape_name(item_goal['name'])]] >= item_goal['minimum'] - def has_full_item_goal(self, category: "GoalCategory", goal: "Goal", item_goal: Dict[str, Any]) -> bool: + def has_full_item_goal(self, category: GoalCategory, goal: Goal, item_goal: dict[str, Any]) -> bool: local_goal = self.world.goal_categories[category.name].get_goal(goal.name) per_world_max_quantity = local_goal.get_item(item_goal['name'])['quantity'] return self.solv_items[ItemInfo.solver_ids[escape_name(item_goal['name'])]] >= per_world_max_quantity @@ -170,13 +172,13 @@ def remove(self, item: Item) -> None: def region_has_shortcuts(self, region_name: str) -> bool: return self.world.region_has_shortcuts(region_name) - def __getstate__(self) -> Dict[str, Any]: + def __getstate__(self) -> dict[str, Any]: return self.__dict__.copy() - def __setstate__(self, state: Dict[str, Any]) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) - def get_prog_items(self) -> Dict[str, int]: + def get_prog_items(self) -> dict[str, int]: return { **{item.name: self.solv_items[item.solver_id] for item in ItemInfo.items.values() diff --git a/TextBox.py b/TextBox.py index 499ea21e7..377dc4769 100644 --- a/TextBox.py +++ b/TextBox.py @@ -1,5 +1,6 @@ +from __future__ import annotations import re -from typing import TYPE_CHECKING, Dict, List, Pattern, Match +from typing import TYPE_CHECKING import Messages @@ -17,7 +18,7 @@ # appear in lower areas of the text box. Eventually, the text box will become uncloseable. MAX_CHARACTERS_PER_BOX: int = 200 -CONTROL_CHARS: Dict[str, List[str]] = { +CONTROL_CHARS: dict[str, list[str]] = { 'LINE_BREAK': ['&', '\x01'], 'BOX_BREAK': ['^', '\x04'], 'NAME': ['@', '\x0F'], @@ -26,13 +27,13 @@ TEXT_END: str = '\x02' -hex_string_regex: Pattern = re.compile(r"\$\{((?:[0-9a-f][0-9a-f] ?)+)}", flags=re.IGNORECASE) +hex_string_regex: re.Pattern = re.compile(r"\$\{((?:[0-9a-f][0-9a-f] ?)+)}", flags=re.IGNORECASE) def line_wrap(text: str, strip_existing_lines: bool = False, strip_existing_boxes: bool = False, replace_control_chars: bool = True): # Replace stand-in characters with their actual control code. if replace_control_chars: - def replace_bytes(match: Match) -> str: + def replace_bytes(match: re.Match) -> str: return ''.join(chr(x) for x in bytes.fromhex(match[1])) for char in CONTROL_CHARS.values(): @@ -146,7 +147,7 @@ def replace_bytes(match: Match) -> str: return '\x04'.join(['\x01'.join([' '.join([''.join([code.get_string() for code in word]) for word in line]) for line in box]) for box in processed_boxes]) -def calculate_width(words: "List[List[TextCode]]"): +def calculate_width(words: list[list[TextCode]]): words_width = 0 for word in words: index = 0 @@ -176,7 +177,7 @@ def get_character_width(character: str) -> int: return character_table[' '] -control_code_width: Dict[str, str] = { +control_code_width: dict[str, str] = { '\x0F': '00000000', '\x16': '00\'00"', '\x17': '00\'00"', @@ -194,7 +195,7 @@ def get_character_width(character: str) -> int: # at worst. This ensures that we will never bleed text out of the text box while line wrapping. # Larger numbers in the denominator mean more of that character fits on a line; conversely, larger values in this table # mean the character is wider and can't fit as many on one line. -character_table: Dict[str, int] = { +character_table: dict[str, int] = { '\x0F': 655200, '\x16': 292215, '\x17': 292215, diff --git a/Unittest.py b/Unittest.py index cf1c68280..3ec2c19ca 100644 --- a/Unittest.py +++ b/Unittest.py @@ -2,6 +2,7 @@ # With python3.10, you can instead run pytest Unittest.py # See `python -m unittest -h` or `pytest -h` for more options. +from __future__ import annotations import json import logging import os @@ -10,7 +11,7 @@ import sys import unittest from collections import Counter, defaultdict -from typing import Dict, Tuple, Optional, Union, Any, overload +from typing import Optional, Any, overload from EntranceShuffle import EntranceShuffleError from Fill import ShuffleError @@ -60,7 +61,7 @@ ludicrous_set = set(ludicrous_items_base) | set(ludicrous_items_extended) | ludicrous_junk | set(trade_items) | set(bottles) | set(ludicrous_exclusions) | {'Bottle with Big Poe'} | shop_items -def make_settings_for_test(settings_dict: Dict[str, Any], seed: Optional[str] = None, outfilename: str = '', strict: bool = True) -> Settings: +def make_settings_for_test(settings_dict: dict[str, Any], seed: Optional[str] = None, outfilename: str = '', strict: bool = True) -> Settings: # Some consistent settings for testability settings_dict.update({ 'create_patch_file': False, @@ -76,7 +77,7 @@ def make_settings_for_test(settings_dict: Dict[str, Any], seed: Optional[str] = return Settings(settings_dict, strict=strict) -def load_settings(settings_file: Union[Dict[str, Any], str], seed: Optional[str] = None, filename: Optional[str] = None) -> Settings: +def load_settings(settings_file: dict[str, Any] | str, seed: Optional[str] = None, filename: Optional[str] = None) -> Settings: if isinstance(settings_file, dict): # Check if settings_file is a distribution file settings dict if filename is None: raise RuntimeError("Running test with in memory file but did not supply a filename for output file.") @@ -99,14 +100,16 @@ def load_spoiler(json_file: str) -> Any: @overload -def generate_with_plandomizer(filename: str, live_copy: LiteralFalse = False, max_attempts: int = 10) -> Tuple[Dict[str, Any], Dict[str, Any]]: +def generate_with_plandomizer(filename: str, live_copy: LiteralFalse = False, max_attempts: int = 10) -> tuple[dict[str, Any], dict[str, Any]]: pass + @overload -def generate_with_plandomizer(filename: str, live_copy: LiteralTrue, max_attempts: int = 10) -> Tuple[Dict[str, Any], Spoiler]: +def generate_with_plandomizer(filename: str, live_copy: LiteralTrue, max_attempts: int = 10) -> tuple[dict[str, Any], Spoiler]: pass -def generate_with_plandomizer(filename: str, live_copy: bool = False, max_attempts: int = 10) -> Tuple[Dict[str, Any], Union[Spoiler, Dict[str, Any]]]: + +def generate_with_plandomizer(filename: str, live_copy: bool = False, max_attempts: int = 10) -> tuple[dict[str, Any], Spoiler | dict[str, Any]]: distribution_file = load_spoiler(os.path.join(test_dir, 'plando', filename + '.json')) try: settings = load_settings(distribution_file['settings'], seed='TESTTESTTEST', filename=filename) @@ -129,7 +132,7 @@ def generate_with_plandomizer(filename: str, live_copy: bool = False, max_attemp return distribution_file, spoiler -def get_actual_pool(spoiler: Dict[str, Any]) -> Dict[str, int]: +def get_actual_pool(spoiler: dict[str, Any]) -> dict[str, int]: """Retrieves the actual item pool based on items placed in the spoiler log. :param spoiler: Spoiler log output from generator diff --git a/Utils.py b/Utils.py index 5660d829b..01152a947 100644 --- a/Utils.py +++ b/Utils.py @@ -1,3 +1,4 @@ +from __future__ import annotations import io import json import logging @@ -6,7 +7,8 @@ import subprocess import sys import urllib.request -from typing import Dict, List, Sequence, Optional, AnyStr, Any +from collections.abc import Sequence +from typing import AnyStr, Optional, Any from urllib.error import URLError, HTTPError from version import __version__, base_version, supplementary_version, branch_url @@ -79,7 +81,7 @@ def open_file(filename: str) -> None: subprocess.call([open_command, filename]) -def get_version_bytes(a: str, b: int = 0x00, c: int = 0x00) -> List[int]: +def get_version_bytes(a: str, b: int = 0x00, c: int = 0x00) -> list[int]: version_bytes = [0x00, 0x00, 0x00, b, c] if not a: @@ -163,7 +165,7 @@ def check_version(checked_version: str) -> None: # variants) call work with or without Pyinstaller, ``--noconsole`` or # not, on Windows and Linux. Typical use:: # subprocess.call(['program_to_run', 'arg_1'], **subprocess_args()) -def subprocess_args(include_stdout: bool = True) -> Dict[str, Any]: +def subprocess_args(include_stdout: bool = True) -> dict[str, Any]: # The following is true only on Windows. if hasattr(subprocess, 'STARTUPINFO'): # On Windows, subprocess calls will pop up a command window by default @@ -222,6 +224,7 @@ def try_find_last(source_list: Sequence[Any], sought_element: Any) -> Optional[i return len(source_list) - 1 - reverse_index return None + def find_last(source_list: Sequence[Any], sought_element: Any) -> int: last = try_find_last(source_list, sought_element) if last is None: diff --git a/World.py b/World.py index 2e01f8321..d59fedfbc 100644 --- a/World.py +++ b/World.py @@ -1,10 +1,12 @@ +from __future__ import annotations import copy import json import logging import os import random from collections import OrderedDict -from typing import Dict, List, Tuple, Set, Any, Union, Iterable, Optional +from collections.abc import Iterable +from typing import Any, Optional from Dungeon import Dungeon from Entrance import Entrance @@ -27,28 +29,28 @@ class World: def __init__(self, world_id: int, settings: Settings, resolve_randomized_settings: bool = True) -> None: self.id: int = world_id - self.dungeons: List[Dungeon] = [] - self.regions: List[Region] = [] - self.itempool: List[Item] = [] - self._cached_locations: List[Location] = [] - self._entrance_cache: Dict[str, Entrance] = {} - self._region_cache: Dict[str, Region] = {} - self._location_cache: Dict[str, Location] = {} - self.shop_prices: Dict[str, int] = {} - self.scrub_prices: Dict[int, int] = {} + self.dungeons: list[Dungeon] = [] + self.regions: list[Region] = [] + self.itempool: list[Item] = [] + self._cached_locations: list[Location] = [] + self._entrance_cache: dict[str, Entrance] = {} + self._region_cache: dict[str, Region] = {} + self._location_cache: dict[str, Location] = {} + self.shop_prices: dict[str, int] = {} + self.scrub_prices: dict[int, int] = {} self.maximum_wallets: int = 0 - self.hinted_dungeon_reward_locations: Dict[str, Location] = {} - self.misc_hint_item_locations: Dict[str, Location] = {} - self.misc_hint_location_items: Dict[str, Item] = {} + self.hinted_dungeon_reward_locations: dict[str, Location] = {} + self.misc_hint_item_locations: dict[str, Location] = {} + self.misc_hint_location_items: dict[str, Item] = {} self.triforce_count: int = 0 self.total_starting_triforce_count: int = 0 - self.empty_areas: Dict[HintArea, Dict[str, Any]] = {} + self.empty_areas: dict[HintArea, dict[str, Any]] = {} self.barren_dungeon: int = 0 self.woth_dungeon: int = 0 - self.randomized_list: List[str] = [] + self.randomized_list: list[str] = [] self.parser: Rule_AST_Transformer = Rule_AST_Transformer(self) - self.event_items: Set[str] = set() + self.event_items: set[str] = set() self.settings: Settings = settings self.distribution: WorldDistribution = settings.distribution.world_dists[world_id] @@ -93,7 +95,7 @@ def __init__(self, world_id: int, settings: Settings, resolve_randomized_setting self.settings.shuffle_ganon_bosskey = 'triforce' # trials that can be skipped will be decided later - self.skipped_trials: Dict[str, bool] = { + self.skipped_trials: dict[str, bool] = { 'Forest': False, 'Fire': False, 'Water': False, @@ -128,10 +130,10 @@ def __init__(self): def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: return self.EmptyDungeonInfo(None) - self.empty_dungeons: Dict[str, EmptyDungeons.EmptyDungeonInfo] = EmptyDungeons() + self.empty_dungeons: dict[str, EmptyDungeons.EmptyDungeonInfo] = EmptyDungeons() # dungeon forms will be decided later - self.dungeon_mq: Dict[str, bool] = { + self.dungeon_mq: dict[str, bool] = { 'Deku Tree': False, 'Dodongos Cavern': False, 'Jabu Jabus Belly': False, @@ -154,7 +156,7 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: with open(d, 'r') as dist_file: dist = json.load(dist_file) if dist['name'] == self.settings.hint_dist: - self.hint_dist_user: Dict[str, Any] = dist + self.hint_dist_user: dict[str, Any] = dist else: self.settings.hint_dist = 'custom' self.hint_dist_user = self.settings.hint_dist_user @@ -186,13 +188,13 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: shuffled, set its order to 0. Hint type format is \"type\": { \"order\": 0, \"weight\": 0.0, \"fixed\": 0, \"copies\": 0 }""") - self.added_hint_types: Dict[str, List[str]] = {} - self.item_added_hint_types: Dict[str, List[str]] = {} - self.hint_exclusions: Set[str] = set() + self.added_hint_types: dict[str, list[str]] = {} + self.item_added_hint_types: dict[str, list[str]] = {} + self.hint_exclusions: set[str] = set() if self.skip_child_zelda: self.hint_exclusions.add('Song from Impa') - self.hint_type_overrides: Dict[str, List[str]] = {} - self.item_hint_type_overrides: Dict[str, List[str]] = {} + self.hint_type_overrides: dict[str, list[str]] = {} + self.item_hint_type_overrides: dict[str, list[str]] = {} for dist in hint_dist_keys: self.added_hint_types[dist] = [] @@ -219,7 +221,7 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: if info.empty: self.hint_type_overrides['barren'].append(str(info.hint_name)) - self.hint_text_overrides: Dict[str, str] = {} + self.hint_text_overrides: dict[str, str] = {} for loc in self.hint_dist_user['add_locations']: if 'text' in loc: # Arbitrarily throw an error at 80 characters to prevent overfilling the text box. @@ -227,19 +229,19 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: raise Exception('Custom hint text too large for %s', loc['location']) self.hint_text_overrides.update({loc['location']: loc['text']}) - self.item_hints: List[str] = self.settings.item_hints + self.item_added_hint_types["named-item"] - self.named_item_pool: List[str] = list(self.item_hints) + self.item_hints: list[str] = self.settings.item_hints + self.item_added_hint_types["named-item"] + self.named_item_pool: list[str] = list(self.item_hints) - self.always_hints: List[str] = [hint.name for hint in get_required_hints(self)] + self.always_hints: list[str] = [hint.name for hint in get_required_hints(self)] self.dungeon_rewards_hinted: bool = 'altar' in settings.misc_hints or settings.enhance_map_compass - self.misc_hint_items: Dict[str, str] = {hint_type: self.hint_dist_user.get('misc_hint_items', {}).get(hint_type, data['default_item']) for hint_type, data in misc_item_hint_table.items()} - self.misc_hint_locations: Dict[str, str] = {hint_type: self.hint_dist_user.get('misc_hint_locations', {}).get(hint_type, data['item_location']) for hint_type, data in misc_location_hint_table.items()} + self.misc_hint_items: dict[str, str] = {hint_type: self.hint_dist_user.get('misc_hint_items', {}).get(hint_type, data['default_item']) for hint_type, data in misc_item_hint_table.items()} + self.misc_hint_locations: dict[str, str] = {hint_type: self.hint_dist_user.get('misc_hint_locations', {}).get(hint_type, data['item_location']) for hint_type, data in misc_location_hint_table.items()} self.state: State = State(self) # Allows us to cut down on checking whether some items are required - self.max_progressions: Dict[str, int] = {name: item.special.get('progressive', 1) for name, item in ItemInfo.items.items()} + self.max_progressions: dict[str, int] = {name: item.special.get('progressive', 1) for name, item in ItemInfo.items.items()} max_tokens = 0 if self.settings.bridge == 'tokens': max_tokens = max(max_tokens, self.settings.bridge_tokens) @@ -279,7 +281,7 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: self.enable_goal_hints = True # Initialize default goals for win condition - self.goal_categories: Dict[str, GoalCategory] = OrderedDict() + self.goal_categories: dict[str, GoalCategory] = OrderedDict() if self.hint_dist_user['use_default_goals']: self.set_goals() @@ -328,8 +330,8 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: self.goal_items.append(item['name']) # Separate goal categories into locked and unlocked for search optimization - self.locked_goal_categories: Dict[str, GoalCategory] = {name: category for (name, category) in self.goal_categories.items() if category.lock_entrances} - self.unlocked_goal_categories: Dict[str, GoalCategory] = {name: category for (name, category) in self.goal_categories.items() if not category.lock_entrances} + self.locked_goal_categories: dict[str, GoalCategory] = {name: category for (name, category) in self.goal_categories.items() if category.lock_entrances} + self.unlocked_goal_categories: dict[str, GoalCategory] = {name: category for (name, category) in self.goal_categories.items() if not category.lock_entrances} def copy(self) -> 'World': new_world = World(self.id, self.settings, False) @@ -538,7 +540,7 @@ def resolve_random_settings(self) -> None: elif self.settings.silver_rupee_pouches_choice == 'all': self.settings.silver_rupee_pouches = self.silver_rupee_puzzles() - def load_regions_from_json(self, file_path: str) -> "List[Tuple[Entrance, str]]": + def load_regions_from_json(self, file_path: str) -> list[tuple[Entrance, str]]: region_json = read_logic_file(file_path) savewarps_to_connect = [] @@ -604,7 +606,7 @@ def load_regions_from_json(self, file_path: str) -> "List[Tuple[Entrance, str]]" self.regions.append(new_region) return savewarps_to_connect - def create_dungeons(self) -> "List[Tuple[Entrance, str]]": + def create_dungeons(self) -> list[tuple[Entrance, str]]: savewarps_to_connect = [] for hint_area in HintArea: if hint_area.is_dungeon: @@ -1008,7 +1010,7 @@ def set_goals(self) -> None: g.minimum_goals = 1 self.goal_categories[g.name] = g - def get_region(self, region_name: Union[str, Region]) -> Region: + def get_region(self, region_name: str | Region) -> Region: if isinstance(region_name, Region): return region_name try: @@ -1020,7 +1022,7 @@ def get_region(self, region_name: Union[str, Region]) -> Region: return region raise KeyError('No such region %s' % region_name) - def get_entrance(self, entrance: Union[str, Entrance]) -> Entrance: + def get_entrance(self, entrance: str | Entrance) -> Entrance: if isinstance(entrance, Entrance): return entrance try: @@ -1033,7 +1035,7 @@ def get_entrance(self, entrance: Union[str, Entrance]) -> Entrance: return exit raise KeyError('No such entrance %s' % entrance) - def get_location(self, location: Union[str, Location]) -> Location: + def get_location(self, location: str | Location) -> Location: if isinstance(location, Location): return location try: @@ -1046,14 +1048,14 @@ def get_location(self, location: Union[str, Location]) -> Location: return r_location raise KeyError('No such location %s' % location) - def get_items(self) -> List[Item]: + def get_items(self) -> list[Item]: return [loc.item for loc in self.get_filled_locations()] + self.itempool - def get_itempool_with_dungeon_items(self) -> List[Item]: + def get_itempool_with_dungeon_items(self) -> list[Item]: return self.get_restricted_dungeon_items() + self.itempool # get a list of items that should stay in their proper dungeon - def get_restricted_dungeon_items(self) -> List[Item]: + def get_restricted_dungeon_items(self) -> list[Item]: itempool = [] if self.settings.shuffle_mapcompass == 'dungeon': @@ -1084,7 +1086,7 @@ def get_restricted_dungeon_items(self) -> List[Item]: return itempool # get a list of items that don't have to be in their proper dungeon - def get_unrestricted_dungeon_items(self) -> List[Item]: + def get_unrestricted_dungeon_items(self) -> list[Item]: itempool = [] if self.settings.shuffle_mapcompass in ['any_dungeon', 'overworld', 'keysanity', 'regional']: itempool.extend([item for dungeon in self.dungeons if not self.empty_dungeons[dungeon.name].empty for item in dungeon.dungeon_items]) @@ -1101,8 +1103,7 @@ def get_unrestricted_dungeon_items(self) -> List[Item]: item.world = self return itempool - - def silver_rupee_puzzles(self): + def silver_rupee_puzzles(self) -> list[str]: return ([ 'Shadow Temple Scythe Shortcut', 'Shadow Temple Huge Pit', 'Shadow Temple Invisible Spikes', 'Gerudo Training Ground Slopes', 'Gerudo Training Ground Lava', 'Gerudo Training Ground Water', @@ -1115,11 +1116,10 @@ def silver_rupee_puzzles(self): + (['Ganons Castle Shadow Trial', 'Ganons Castle Water Trial'] if self.dungeon_mq['Ganons Castle'] else ['Ganons Castle Spirit Trial', 'Ganons Castle Light Trial', 'Ganons Castle Forest Trial']) ) - - def find_items(self, item: str) -> List[Location]: + def find_items(self, item: str) -> list[Location]: return [location for location in self.get_locations() if location.item is not None and location.item.name == item] - def push_item(self, location: Union[str, Location], item: Item, manual: bool = False) -> None: + def push_item(self, location: str | Location, item: Item, manual: bool = False) -> None: if not isinstance(location, Location): location = self.get_location(location) @@ -1134,7 +1134,7 @@ def push_item(self, location: Union[str, Location], item: Item, manual: bool = F else: raise RuntimeError('Cannot assign item %s to location %s.' % (item, location)) - def get_locations(self) -> List[Location]: + def get_locations(self) -> list[Location]: if not self._cached_locations: for region in self.regions: self._cached_locations.extend(region.locations) @@ -1149,13 +1149,13 @@ def get_filled_locations(self) -> Iterable[Location]: def get_progression_locations(self) -> Iterable[Location]: return filter(Location.has_progression_item, self.get_locations()) - def get_entrances(self) -> List[Entrance]: + def get_entrances(self) -> list[Entrance]: return [exit for region in self.regions for exit in region.exits] - def get_shufflable_entrances(self, type=None, only_primary=False) -> List[Entrance]: + def get_shufflable_entrances(self, type=None, only_primary=False) -> list[Entrance]: return [entrance for entrance in self.get_entrances() if (type is None or entrance.type == type) and (not only_primary or entrance.primary)] - def get_shuffled_entrances(self, type=None, only_primary=False) -> List[Entrance]: + def get_shuffled_entrances(self, type=None, only_primary=False) -> list[Entrance]: return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled] def region_has_shortcuts(self, region_name: str) -> bool: @@ -1179,7 +1179,7 @@ def set_drop_location_names(self): # the true list of possible useless areas, but this will generate a # reasonably sized list of areas that fit this property. def update_useless_areas(self, spoiler: Spoiler) -> None: - areas: Dict[HintArea, Dict[str, Any]] = {} + areas: dict[HintArea, dict[str, Any]] = {} # Link's Pocket and None are not real areas excluded_areas = [None, HintArea.ROOT] for location in self.get_locations(): diff --git a/crc.py b/crc.py index 6ff2c16eb..4f4dbaa6f 100644 --- a/crc.py +++ b/crc.py @@ -4,24 +4,16 @@ def calculate_crc(data: BigStream) -> bytearray: - t1: int - t2: int - t3: int - t4: int - t5: int - t6: int t1 = t2 = t3 = t4 = t5 = t6 = 0xDF26F436 - u32: int = 0xFFFFFFFF + u32 = 0xFFFFFFFF - m1: bytearray = data.read_bytes(0x1000, 0x100000) + m1 = data.read_bytes(0x1000, 0x100000) words = map(uint32.value, zip(m1[0::4], m1[1::4], m1[2::4], m1[3::4])) - m2: bytearray = data.read_bytes(0x750, 0x100) + m2 = data.read_bytes(0x750, 0x100) words2 = map(uint32.value, zip(m2[0::4], m2[1::4], m2[2::4], m2[3::4])) - d: int - d2: int for d, d2 in zip(words, itertools.cycle(words2)): # keep t2 and t6 in u32 for comparisons; others can wait to be truncated if ((t6 + d) & u32) < t6: diff --git a/ntype.py b/ntype.py index 6db5ac030..2cbd2be95 100644 --- a/ntype.py +++ b/ntype.py @@ -1,5 +1,7 @@ # Originally written by mzxrules -from typing import Sequence, Optional +from __future__ import annotations +from collections.abc import Sequence +from typing import Optional import struct diff --git a/texture_util.py b/texture_util.py index 7b28d53a3..9f7dcc2fb 100755 --- a/texture_util.py +++ b/texture_util.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from typing import List, Tuple +from __future__ import annotations from Rom import Rom @@ -9,7 +9,7 @@ # address - address of the ci4 texture in Rom # length - size of the texture in PIXELS # palette - 4-bit color palette to use (max of 16 colors) -def ci4_to_rgba16(rom: Rom, address: int, length: int, palette: List[int]) -> List[int]: +def ci4_to_rgba16(rom: Rom, address: int, length: int, palette: list[int]) -> list[int]: new_pixels = [] texture = rom.read_bytes(address, length // 2) for byte in texture: @@ -21,7 +21,7 @@ def ci4_to_rgba16(rom: Rom, address: int, length: int, palette: List[int]) -> Li # Convert an rgba16 texture to ci8 # rgba16_texture - texture to convert # returns - tuple (ci8_texture, palette) -def rgba16_to_ci8(rgba16_texture: List[int]) -> Tuple[List[int], List[int]]: +def rgba16_to_ci8(rgba16_texture: list[int]) -> tuple[list[int], list[int]]: ci8_texture = [] palette = get_colors_from_rgba16(rgba16_texture) # Get all the colors in the texture if len(palette) > 0x100: # Make sure there are <= 256 colors. Could probably do some fancy stuff to convert, but nah. @@ -38,7 +38,7 @@ def rgba16_to_ci8(rgba16_texture: List[int]) -> Tuple[List[int], List[int]]: # Load a palette (essentially just an rgba16 texture) from rom -def load_palette(rom: Rom, address: int, length: int) -> List[int]: +def load_palette(rom: Rom, address: int, length: int) -> list[int]: palette = [] for i in range(0, length): palette.append(rom.read_int16(address + 2 * i)) @@ -46,7 +46,7 @@ def load_palette(rom: Rom, address: int, length: int) -> List[int]: # Get a list of unique colors (palette) from an rgba16 texture -def get_colors_from_rgba16(rgba16_texture: List[int]) -> List[int]: +def get_colors_from_rgba16(rgba16_texture: list[int]) -> list[int]: colors = [] for pixel in rgba16_texture: if pixel not in colors: @@ -58,7 +58,7 @@ def get_colors_from_rgba16(rgba16_texture: List[int]) -> List[int]: # rgba16_texture - Original texture # rgba16_patch - Patch texture. If this parameter is not supplied, this function will simply return the original texture. # returns - new texture = texture xor patch -def apply_rgba16_patch(rgba16_texture: List[int], rgba16_patch: List[int]) -> List[int]: +def apply_rgba16_patch(rgba16_texture: list[int], rgba16_patch: list[int]) -> list[int]: if rgba16_patch is not None and (len(rgba16_texture) != len(rgba16_patch)): raise(Exception("OG Texture and Patch not the same length!")) @@ -73,7 +73,7 @@ def apply_rgba16_patch(rgba16_texture: List[int], rgba16_patch: List[int]) -> Li # Save a rgba16 texture to a file -def save_rgba16_texture(rgba16_texture: List[int], filename: str) -> None: +def save_rgba16_texture(rgba16_texture: list[int], filename: str) -> None: file = open(filename, 'wb') bytes = bytearray() for pixel in rgba16_texture: @@ -83,7 +83,7 @@ def save_rgba16_texture(rgba16_texture: List[int], filename: str) -> None: # Save a ci8 texture to a file -def save_ci8_texture(ci8_texture: List[int], filename: str) -> None: +def save_ci8_texture(ci8_texture: list[int], filename: str) -> None: file = open(filename, 'wb') bytes = bytearray() for pixel in ci8_texture: @@ -97,7 +97,7 @@ def save_ci8_texture(ci8_texture: List[int], filename: str) -> None: # base_texture_address - Address of the rbga16 texture in ROM # size - Size of the texture in PIXELS # returns - list of ints representing each 16-bit pixel -def load_rgba16_texture_from_rom(rom: Rom, base_texture_address: int, size: int) -> List[int]: +def load_rgba16_texture_from_rom(rom: Rom, base_texture_address: int, size: int) -> list[int]: texture = [] for i in range(0, size): texture.append(int.from_bytes(rom.read_bytes(base_texture_address + 2 * i, 2), 'big')) @@ -107,7 +107,7 @@ def load_rgba16_texture_from_rom(rom: Rom, base_texture_address: int, size: int) # Load an rgba16 texture from a binary file. # filename - path to the file # size - number of 16-bit pixels in the texture. -def load_rgba16_texture(filename: str, size: int) -> List[int]: +def load_rgba16_texture(filename: str, size: int) -> list[int]: texture = [] file = open(filename, 'rb') for i in range(0, size):