diff --git a/CI.py b/CI.py
index f5f3930e4..731581da9 100644
--- a/CI.py
+++ b/CI.py
@@ -4,32 +4,39 @@
from __future__ import annotations
import argparse
+from io import StringIO
import json
import os.path
import pathlib
import sys
+from typing import Any, NoReturn
import unittest
-from io import StringIO
-from typing import NoReturn
+
from Main import resolve_settings
+from Messages import ITEM_MESSAGES, KEYSANITY_MESSAGES, MISC_MESSAGES
from Patches import get_override_table, get_override_table_bytes
from Rom import Rom
-import Unittest as Tests
-from Messages import ITEM_MESSAGES, KEYSANITY_MESSAGES, MISC_MESSAGES
from SettingsList import SettingInfos, logic_tricks, validate_settings
import Unittest as Tests
from Utils import data_path
+ERROR_COUNT: int = 0
+ANY_FIXABLE_ERRORS: bool = False
+ANY_UNFIXABLE_ERRORS: bool = False
+
+
def error(msg: str, can_fix: bool) -> None:
- if not hasattr(error, "count"):
- error.count = 0
+ global ERROR_COUNT
+ global ANY_FIXABLE_ERRORS
+ global ANY_UNFIXABLE_ERRORS
+
print(msg, file=sys.stderr)
- error.count += 1
+ ERROR_COUNT += 1
if can_fix:
- error.can_fix = True
+ ANY_FIXABLE_ERRORS = True
else:
- error.cannot_fix = True
+ ANY_UNFIXABLE_ERRORS = True
def run_unit_tests() -> None:
@@ -133,7 +140,7 @@ def check_preset_spoilers(fix_errors: bool = False) -> None:
# This is not a perfect check because it doesn't account for everything that gets manually done in Patches.py
# For that, we perform additional checking at patch time
def check_message_duplicates() -> None:
- def check_for_duplicates(new_item_messages: list[tuple[int, str]]) -> None:
+ def check_for_duplicates(new_item_messages: list[tuple[int, Any]]) -> None:
for i in range(0, len(new_item_messages)):
for j in range(i, len(new_item_messages)):
if i != j:
@@ -264,22 +271,22 @@ def run_ci_checks() -> NoReturn:
exit_ci(args.fix)
def exit_ci(fix_errors: bool = False) -> NoReturn:
- if hasattr(error, "count") and error.count:
- print(f'CI failed with {error.count} errors.', file=sys.stderr)
+ if ERROR_COUNT > 0:
+ print(f'CI failed with {ERROR_COUNT} errors.', file=sys.stderr)
if fix_errors:
- if getattr(error, 'cannot_fix', False):
+ if ANY_UNFIXABLE_ERRORS:
print('Some errors could not be fixed automatically.', file=sys.stderr)
sys.exit(1)
else:
print('All errors fixed.', file=sys.stderr)
sys.exit(0)
else:
- if getattr(error, 'can_fix', False):
- if getattr(error, 'can_fix_release', False):
+ if ANY_FIXABLE_ERRORS:
+ if ANY_FIXABLE_ERRORS_FOR_RELEASE_CHECKS:
release_arg = ' --release'
else:
release_arg = ''
- if getattr(error, 'cannot_fix', False):
+ if ANY_UNFIXABLE_ERRORS:
which_errors = 'some of these errors'
else:
which_errors = 'these errors'
diff --git a/Colors.py b/Colors.py
index 831db6104..1f0ac2cb2 100644
--- a/Colors.py
+++ b/Colors.py
@@ -1,8 +1,14 @@
from __future__ import annotations
+import sys
import random
import re
from collections import namedtuple
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+else:
+ TypeAlias = str
+
Color = namedtuple('Color', ' R G B')
tunic_colors: dict[str, Color] = {
@@ -152,7 +158,8 @@
# 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]] = {
+AButtonColors: TypeAlias = "dict[str, tuple[Color, Color, Color, Color, Color, Color, Color]]"
+a_button_colors: AButtonColors = {
"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),
@@ -208,7 +215,8 @@
}
# C Button Pause Menu C Cursor Pause Menu C Icon C Note
-c_button_colors: dict[str, tuple[Color, Color, Color, Color]] = {
+CButtonColors: TypeAlias = "dict[str, tuple[Color, Color, Color, Color]]"
+c_button_colors: CButtonColors = {
"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)),
@@ -366,14 +374,14 @@ 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: Color, color2: Color) -> 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: Color) -> 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,23 +394,21 @@ def lum_color_ratio(val: float) -> float:
return pow((val + 0.055) / 1.055, 2.4)
-def generate_random_color() -> list[int]:
- return [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
-
+def generate_random_color() -> Color:
+ return Color(random.getrandbits(8), random.getrandbits(8), random.getrandbits(8))
-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}$')
+HEX_TO_COLOR_REGEX: re.Pattern[str] = re.compile(r'^(?:[0-9a-fA-F]{3}){1,2}$')
+def hex_to_color(option: str) -> Color:
# build color from hex code
option = option[1:] if option[0] == "#" else option
- if not hex_to_color.regex.search(option):
+ if not HEX_TO_COLOR_REGEX.search(option):
raise Exception(f"Invalid color value provided: {option}")
if len(option) > 3:
- return list(int(option[i:i + 2], 16) for i in (0, 2, 4))
+ return Color(*(int(option[i:i + 2], 16) for i in (0, 2, 4)))
else:
- return list(int(f'{option[i]}{option[i]}', 16) for i in (0, 1, 2))
+ return Color(*(int(f'{option[i]}{option[i]}', 16) for i in (0, 1, 2)))
-def color_to_hex(color: list[int]) -> str:
- return '#' + ''.join(['{:02X}'.format(c) for c in color])
+def color_to_hex(color: Color) -> str:
+ return '#' + ''.join('{:02X}'.format(c) for c in color)
diff --git a/Cosmetics.py b/Cosmetics.py
index d32668cb0..9851e0188 100644
--- a/Cosmetics.py
+++ b/Cosmetics.py
@@ -5,9 +5,10 @@
import random
from collections.abc import Iterable, Callable
from itertools import chain
-from typing import TYPE_CHECKING, Optional, Any
+from typing import TYPE_CHECKING, Optional, Any, TypedDict
import Colors
+from Colors import Color
import IconManip
import Music
import Sounds
@@ -57,7 +58,7 @@ def patch_music(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[s
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[Color], model_addresses: tuple[list[int], list[int], list[int]]) -> None:
main_addresses, dark_addresses, light_addresses = model_addresses
if color is None:
@@ -78,7 +79,7 @@ def patch_model_colors(rom: Rom, color: Optional[list[int]], model_addresses: tu
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[Color], rainbow: bool = False) -> None:
# patch tunic icon colors
icon_locations = {
'Kokiri Tunic': 0x007FE000,
@@ -139,9 +140,9 @@ def patch_tunic_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
color = Colors.generate_random_color()
# grab the color from the list
elif tunic_option in Colors.tunic_colors:
- color = list(Colors.tunic_colors[tunic_option])
+ color = Colors.tunic_colors[tunic_option]
elif tunic_option == 'Rainbow':
- color = list(Colors.Color(0x00, 0x00, 0x00))
+ color = Color(0x00, 0x00, 0x00)
# build color from hex code
else:
color = Colors.hex_to_color(tunic_option)
@@ -203,7 +204,7 @@ def patch_navi_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
colors = []
option_dict = {}
for address_index, address in enumerate(navi_addresses):
- address_colors = {}
+ address_colors: dict[str, Color] = {}
colors.append(address_colors)
for index, (navi_part, option, rainbow_symbol) in enumerate([
('inner', navi_option_inner, rainbow_inner_symbol),
@@ -218,7 +219,7 @@ def patch_navi_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
# set rainbow option
if rainbow_symbol is not None and option == 'Rainbow':
rom.write_byte(rainbow_symbol, 0x01)
- color = [0x00, 0x00, 0x00]
+ color = Color(0x00, 0x00, 0x00)
elif rainbow_symbol is not None:
rom.write_byte(rainbow_symbol, 0x00)
elif option == 'Rainbow':
@@ -231,7 +232,7 @@ def patch_navi_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
# grab the color from the list
if color is None and option in Colors.NaviColors:
- color = list(Colors.NaviColors[option][index])
+ color = Colors.NaviColors[option][index]
# build color from hex code
if color is None:
@@ -246,8 +247,7 @@ def patch_navi_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
option_dict[navi_part] = option
# write color
- color = address_colors['inner'] + [0xFF] + address_colors['outer'] + [0xFF]
- rom.write_bytes(address, color)
+ rom.write_bytes(address, [*address_colors['inner'], 0xFF, *address_colors['outer'], 0xFF])
# Get the colors into the log.
log.misc_colors[navi_action] = CollapseDict({
@@ -300,7 +300,7 @@ def patch_sword_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
colors = []
option_dict = {}
for address_index, (address, inner_transparency, inner_white_transparency, outer_transparency, outer_white_transparency) in enumerate(trail_addresses):
- address_colors = {}
+ address_colors: dict[str, Color] = {}
colors.append(address_colors)
transparency_dict = {}
for index, (trail_part, option, rainbow_symbol, white_transparency, transparency) in enumerate([
@@ -316,7 +316,7 @@ def patch_sword_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
# set rainbow option
if rainbow_symbol is not None and option == 'Rainbow':
rom.write_byte(rainbow_symbol, 0x01)
- color = [0x00, 0x00, 0x00]
+ color = Color(0x00, 0x00, 0x00)
elif rainbow_symbol is not None:
rom.write_byte(rainbow_symbol, 0x00)
elif option == 'Rainbow':
@@ -329,7 +329,7 @@ def patch_sword_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
# grab the color from the list
if color is None and option in Colors.sword_trail_colors:
- color = list(Colors.sword_trail_colors[option])
+ color = Colors.sword_trail_colors[option]
# build color from hex code
if color is None:
@@ -350,8 +350,7 @@ def patch_sword_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
option_dict[trail_part] = option
# write color
- color = address_colors['outer'] + [transparency_dict['outer']] + address_colors['inner'] + [transparency_dict['inner']]
- rom.write_bytes(address, color)
+ rom.write_bytes(address, [*address_colors['outer'], transparency_dict['outer'], *address_colors['inner'], transparency_dict['inner']])
# Get the colors into the log.
log.misc_colors[trail_name] = CollapseDict({
@@ -395,7 +394,7 @@ def patch_boomerang_trails(rom: Rom, settings: Settings, log: CosmeticsLog, symb
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: list[tuple[str, str, list[str], dict[str, Color], tuple[int, int, int, int]]]) -> 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')
@@ -427,7 +426,7 @@ def patch_trails(rom: Rom, settings: Settings, log: CosmeticsLog, trails) -> Non
# set rainbow option
if option == 'Rainbow':
rom.write_byte(rainbow_symbol, 0x01)
- color = [0x00, 0x00, 0x00]
+ color = Color(0x00, 0x00, 0x00)
else:
rom.write_byte(rainbow_symbol, 0x00)
@@ -435,8 +434,8 @@ def patch_trails(rom: Rom, settings: Settings, log: CosmeticsLog, trails) -> Non
if color is None and option == 'Completely Random':
# Specific handling for inner bombchu trails for contrast purposes.
if trail_name == 'Bombchu Trail' and trail_part == 'inner':
- fixed_dark_color = [0, 0, 0]
- color = [0, 0, 0]
+ fixed_dark_color = Color(0, 0, 0)
+ color = Color(0, 0, 0)
# Avoid colors which have a low contrast so the bombchu ticking is still visible
while Colors.contrast_ratio(color, fixed_dark_color) <= 4:
color = Colors.generate_random_color()
@@ -445,7 +444,7 @@ def patch_trails(rom: Rom, settings: Settings, log: CosmeticsLog, trails) -> Non
# grab the color from the list
if color is None and option in trail_color_dict:
- color = list(trail_color_dict[option])
+ color = trail_color_dict[option]
# build color from hex code
if color is None:
@@ -476,7 +475,7 @@ def patch_trails(rom: Rom, settings: Settings, log: CosmeticsLog, trails) -> Non
def patch_gauntlet_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None:
# patch gauntlet colors
- gauntlets = [
+ gauntlets: list[tuple[str, str, int, tuple[list[int], list[int], list[int]]]] = [
('Silver Gauntlets', 'silver_gauntlets_color', 0x00B6DA44,
([0x173B4CC], [0x173B4D4, 0x173B50C, 0x173B514], [])), # GI Model DList colors
('Gold Gauntlets', 'golden_gauntlets_color', 0x00B6DA47,
@@ -499,7 +498,7 @@ def patch_gauntlet_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbo
color = Colors.generate_random_color()
# grab the color from the list
elif gauntlet_option in Colors.gauntlet_colors:
- color = list(Colors.gauntlet_colors[gauntlet_option])
+ color = Colors.gauntlet_colors[gauntlet_option]
# build color from hex code
else:
color = Colors.hex_to_color(gauntlet_option)
@@ -517,7 +516,7 @@ def patch_gauntlet_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbo
def patch_shield_frame_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None:
# patch shield frame colors
- shield_frames = [
+ shield_frames: list[tuple[str, str, list[int], tuple[list[int], list[int], list[int]]]] = [
('Mirror Shield Frame', 'mirror_shield_frame_color',
[0xFA7274, 0xFA776C, 0xFAA27C, 0xFAC564, 0xFAC984, 0xFAEDD4],
([0x1616FCC], [0x1616FD4], [])),
@@ -536,10 +535,10 @@ def patch_shield_frame_colors(rom: Rom, settings: Settings, log: CosmeticsLog, s
shield_frame_option = random.choice(shield_frame_color_list)
# handle completely random
if shield_frame_option == 'Completely Random':
- color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
+ color = Colors.generate_random_color()
# grab the color from the list
elif shield_frame_option in Colors.shield_frame_colors:
- color = list(Colors.shield_frame_colors[shield_frame_option])
+ color = Colors.shield_frame_colors[shield_frame_option]
# build color from hex code
else:
color = Colors.hex_to_color(shield_frame_option)
@@ -583,7 +582,7 @@ def patch_heart_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
color = Colors.generate_random_color()
# grab the color from the list
elif heart_option in Colors.heart_colors:
- color = list(Colors.heart_colors[heart_option])
+ color = Colors.heart_colors[heart_option]
# build color from hex code
else:
color = Colors.hex_to_color(heart_option)
@@ -630,7 +629,7 @@ def patch_magic_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
if magic_option == 'Completely Random':
color = Colors.generate_random_color()
elif magic_option in Colors.magic_colors:
- color = list(Colors.magic_colors[magic_option])
+ color = Colors.magic_colors[magic_option]
else:
color = Colors.hex_to_color(magic_option)
magic_option = 'Custom'
@@ -650,7 +649,7 @@ def patch_magic_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
def patch_button_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None:
- buttons = [
+ buttons: list[tuple[str, str, Colors.AButtonColors | Colors.CButtonColors | dict[str, Color], list[tuple[str, Optional[int], Optional[list[tuple[int, int, int]]]]]]] = [
('A Button Color', 'a_button_color', Colors.a_button_colors,
[('A Button Color', symbols['CFG_A_BUTTON_COLOR'],
None),
@@ -702,14 +701,18 @@ def patch_button_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols
button_option = random.choice(list(button_colors.keys()))
# handle completely random
if button_option == 'Completely Random':
- fixed_font_color = [10, 10, 10]
- color = [0, 0, 0]
+ fixed_font_color = Color(10, 10, 10)
+ color = Color(0, 0, 0)
# Avoid colors which have a low contrast with the font inside buttons (eg. the A letter)
while Colors.contrast_ratio(color, fixed_font_color) <= 3:
color = Colors.generate_random_color()
# grab the color from the list
elif button_option in button_colors:
- color_set = [button_colors[button_option]] if isinstance(button_colors[button_option][0], int) else list(button_colors[button_option])
+ button_color = button_colors[button_option]
+ if isinstance(button_color, Color):
+ color_set = [button_color]
+ else:
+ color_set = list(button_color)
color = color_set[0]
# build color from hex code
else:
@@ -1273,6 +1276,12 @@ def patch_cosmetics(settings: Settings, rom: Rom) -> CosmeticsLog:
return log
+class BgmGroups(TypedDict):
+ favorites: list
+ exclude: list
+ groups: dict
+
+
class CosmeticsLog:
def __init__(self, settings: Settings) -> None:
self.settings: Settings = settings
@@ -1282,7 +1291,6 @@ def __init__(self, settings: Settings) -> None:
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] = []
@@ -1309,9 +1317,11 @@ def __init__(self, settings: Settings) -> None:
logging.getLogger('').warning("Cosmetic Plandomizer enabled, but no file provided.")
self.settings.enable_cosmetic_file = False
- self.bgm_groups['favorites'] = CollapseList(self.src_dict.get('bgm_groups', {}).get('favorites', []).copy())
- self.bgm_groups['exclude'] = CollapseList(self.src_dict.get('bgm_groups', {}).get('exclude', []).copy())
- self.bgm_groups['groups'] = AlignedDict(self.src_dict.get('bgm_groups', {}).get('groups', {}).copy(), 1)
+ self.bgm_groups: BgmGroups = {
+ 'favorites': CollapseList(self.src_dict.get('bgm_groups', {}).get('favorites', []).copy()),
+ 'exclude': CollapseList(self.src_dict.get('bgm_groups', {}).get('exclude', []).copy()),
+ 'groups': AlignedDict(self.src_dict.get('bgm_groups', {}).get('groups', {}).copy(), 1),
+ }
for key, value in self.bgm_groups['groups'].items():
self.bgm_groups['groups'][key] = CollapseList(value.copy())
diff --git a/EntranceShuffle.py b/EntranceShuffle.py
index d56900e16..e2e20d97b 100644
--- a/EntranceShuffle.py
+++ b/EntranceShuffle.py
@@ -23,15 +23,15 @@
def set_all_entrances_data(world: World) -> None:
- for type, forward_entry, *return_entry in entrance_shuffle_table:
+ for type, forward_entry, *return_entries in entrance_shuffle_table:
forward_entrance = world.get_entrance(forward_entry[0])
forward_entrance.data = forward_entry[1]
forward_entrance.type = type
forward_entrance.primary = True
if type == 'Grotto':
forward_entrance.data['index'] = 0x1000 + forward_entrance.data['grotto_id']
- if return_entry:
- return_entry = return_entry[0]
+ if return_entries:
+ return_entry = return_entries[0]
return_entrance = world.get_entrance(return_entry[0])
return_entrance.data = return_entry[1]
return_entrance.type = type
@@ -61,8 +61,12 @@ def build_one_way_targets(world: World, types_to_include: Iterable[str], exclude
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))
if target_region_names:
- return [entrance.get_new_target() for entrance in valid_one_way_entrances
- if entrance.connected_region.name in target_region_names]
+ return [
+ entrance.get_new_target()
+ for entrance in valid_one_way_entrances
+ if entrance.connected_region is not None
+ and entrance.connected_region.name in target_region_names
+ ]
return [entrance.get_new_target() for entrance in valid_one_way_entrances]
@@ -86,7 +90,7 @@ def build_one_way_targets(world: World, types_to_include: Iterable[str], exclude
# ZF Zora's Fountain
# ZR Zora's River
-entrance_shuffle_table = [
+entrance_shuffle_table: list[tuple[str, tuple[str, dict]] | tuple[str, tuple[str, dict], tuple[str, dict]]] = [
('Dungeon', ('KF Outside Deku Tree -> Deku Tree Lobby', { 'index': 0x0000 }),
('Deku Tree Lobby -> KF Outside Deku Tree', { 'index': 0x0209 })),
('Dungeon', ('Death Mountain -> Dodongos Cavern Beginning', { 'index': 0x0004 }),
@@ -426,7 +430,9 @@ def set_entrances(worlds: list[World], savewarps_to_connect: list[tuple[Entrance
world.initialize_entrances()
for savewarp, replaces in savewarps_to_connect:
+ assert savewarp.world is not None
savewarp.replaces = savewarp.world.get_entrance(replaces)
+ assert savewarp.replaces.connected_region is not None
savewarp.connect(savewarp.replaces.connected_region)
for world in worlds:
@@ -562,20 +568,20 @@ def shuffle_random_entrances(worlds: list[World]) -> None:
for pool_type, entrance_pool in one_way_entrance_pools.items():
# One way entrances are extra entrances that will be connected to entrance positions from a selection of entrance pools
if pool_type == 'OverworldOneWay':
- valid_target_types = ('WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
+ valid_target_types_owow = ('WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Extra')
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types_owow, exclude=['Prelude of Light Warp -> Temple of Time'])
elif pool_type == 'OwlDrop':
- valid_target_types = ('WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
+ valid_target_types_owl = ('WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Extra')
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types_owl, exclude=['Prelude of Light Warp -> Temple of Time'])
for target in one_way_target_entrance_pools[pool_type]:
target.set_rule(lambda state, age=None, **kwargs: age == 'child')
elif pool_type == 'Spawn':
- valid_target_types = ('Spawn', 'WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
+ valid_target_types_spawn = ('Spawn', 'WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
# Restrict spawn entrances from linking to regions with no or extremely specific glitchless itemless escapes.
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types, exclude=['Volvagia Boss Room -> DMC Central Local', 'Bolero of Fire Warp -> DMC Central Local', 'Queen Gohma Boss Room -> KF Outside Deku Tree'])
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types_spawn, exclude=['Volvagia Boss Room -> DMC Central Local', 'Bolero of Fire Warp -> DMC Central Local', 'Queen Gohma Boss Room -> KF Outside Deku Tree'])
elif pool_type == 'WarpSong':
- valid_target_types = ('Spawn', 'WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types)
+ valid_target_types_song = ('Spawn', 'WarpSong', 'BlueWarp', 'OwlDrop', 'OverworldOneWay', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(world, valid_target_types_song)
# Ensure that when trying to place the last entrance of a one way pool, we don't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]:
target.add_rule((lambda entrances=entrance_pool: (lambda state, **kwargs: any(
@@ -709,7 +715,7 @@ def shuffle_one_way_priority_entrances(worlds: list[World], world: World, one_wa
retry_count: int = 2) -> list[tuple[Entrance, Entrance]]:
while retry_count:
retry_count -= 1
- rollbacks = []
+ rollbacks: list[tuple[Entrance, Entrance]] = []
try:
for key, (regions, types) in one_way_priorities.items():
@@ -744,7 +750,7 @@ def shuffle_entrance_pool(world: World, worlds: list[World], entrance_pool: list
while retry_count:
retry_count -= 1
- rollbacks = []
+ rollbacks: list[tuple[Entrance, Entrance]] = []
try:
# Shuffle restrictive entrances first while more regions are available in order to heavily reduce the chances of the placement failing.
@@ -768,7 +774,7 @@ def shuffle_entrance_pool(world: World, worlds: list[World], entrance_pool: list
except EntranceShuffleError as error:
for entrance, target in rollbacks:
restore_connections(entrance, target)
- logging.getLogger('').info('Failed to place all entrances in a pool for world %d. Will retry %d more times', entrance_pool[0].world.id, retry_count)
+ logging.getLogger('').info(f'Failed to place all entrances in a pool for {world}. Will retry {retry_count} more times')
logging.getLogger('').info('\t%s' % error)
if world.settings.custom_seed:
@@ -785,7 +791,9 @@ def split_entrances_by_requirements(worlds: list[World], entrances_to_split: lis
entrances_to_disconnect = set(assumed_entrances).union(entrance.reverse for entrance in assumed_entrances if entrance.reverse)
for entrance in entrances_to_disconnect:
if entrance.connected_region:
- original_connected_regions[entrance] = entrance.disconnect()
+ previously_connected = entrance.disconnect()
+ assert previously_connected is not None
+ original_connected_regions[entrance] = previously_connected
# Generate the states with all assumed entrances disconnected
# This ensures no assumed entrances corresponding to those we are shuffling are required in order for an entrance to be reachable as some age/tod
@@ -818,6 +826,8 @@ def replace_entrance(worlds: list[World], entrance: Entrance, target: Entrance,
if placed_one_way_entrances is None:
placed_one_way_entrances = []
try:
+ if entrance.world is None:
+ raise EntranceShuffleError('Entrance has no world')
check_entrances_compatibility(entrance, target, rollbacks, placed_one_way_entrances)
change_connections(entrance, target)
validate_world(entrance.world, worlds, entrance, locations_to_ensure_reachable, itempool, placed_one_way_entrances=placed_one_way_entrances)
@@ -825,8 +835,7 @@ def replace_entrance(worlds: list[World], entrance: Entrance, target: Entrance,
return True
except EntranceShuffleError as error:
# If the entrance can't be placed there, log a debug message and change the connections back to what they were previously
- logging.getLogger('').debug('Failed to connect %s To %s (Reason: %s) [World %d]',
- entrance, entrance.connected_region or target.connected_region, error, entrance.world.id)
+ logging.getLogger('').debug(f'Failed to connect {entrance} To {entrance.connected_region or target.connected_region} (Reason: {error}) [{entrance.world}]')
if entrance.connected_region:
restore_connections(entrance, target)
return False
@@ -847,6 +856,9 @@ def place_one_way_priority_entrance(worlds: list[World], world: World, priority_
for entrance in avail_pool:
if entrance.replaces:
continue
+ assert entrance.parent_region is not None
+ assert entrance.type is not None
+ assert entrance.world is not None
# Only allow Adult Spawn as sole Nocturne access if hints != mask.
# Otherwise, child access is required here (adult access assumed or guaranteed later).
if entrance.parent_region.name == 'Adult Spawn':
@@ -889,12 +901,17 @@ def shuffle_entrances(worlds: list[World], entrances: list[Entrance], target_ent
break
if entrance.connected_region is None:
- raise EntranceShuffleError('No more valid entrances to replace with %s in world %d' % (entrance, entrance.world.id))
+ raise EntranceShuffleError(f'No more valid entrances to replace with {entrance} in {entrance.world}')
# 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]] = (),
+def check_entrances_compatibility(entrance: Entrance, target: Entrance, rollbacks: Iterable[tuple[Entrance, Entrance]] = (),
placed_one_way_entrances: Optional[list[tuple[Entrance, Entrance]]] = None) -> None:
+
+ if entrance.parent_region is None:
+ raise EntranceShuffleError('Entrance has no parent region')
+ if target.connected_region is None:
+ raise EntranceShuffleError('Target has no connected region')
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
@@ -912,6 +929,7 @@ def check_entrances_compatibility(entrance: Entrance, target: Entrance, rollback
for rollback in (*rollbacks, *placed_one_way_entrances):
try:
placed_entrance = rollback[0]
+ assert placed_entrance.connected_region is not None
if entrance.type == placed_entrance.type and HintArea.at(placed_entrance.connected_region) == hint_area:
raise EntranceShuffleError(f'Another {entrance.type} entrance already leads to {hint_area}')
except HintAreaNotFound:
@@ -937,14 +955,14 @@ def validate_world(world: World, worlds: list[World], entrance_placed: Optional[
for entrance in world.get_shufflable_entrances():
if entrance.shuffled:
if entrance.replaces:
- if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]):
+ if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[] if entrance.replaces.reverse is None else [entrance.replaces.reverse]):
raise EntranceShuffleError('%s is replaced by an entrance with a potential child access' % entrance.replaces.name)
- elif entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]):
+ elif entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[] if entrance.replaces.reverse is None else [entrance.replaces.reverse]):
raise EntranceShuffleError('%s is replaced by an entrance with a potential adult access' % entrance.replaces.name)
else:
- if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]):
+ if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[] if entrance.reverse is None else [entrance.reverse]):
raise EntranceShuffleError('%s is potentially accessible as child' % entrance.name)
- elif entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
+ elif entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[] if entrance.reverse is None else [entrance.reverse]):
raise EntranceShuffleError('%s is potentially accessible as adult' % entrance.name)
if locations_to_ensure_reachable:
@@ -987,7 +1005,7 @@ def validate_world(world: World, worlds: list[World], entrance_placed: Optional[
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
if (world.shuffle_special_interior_entrances or world.settings.shuffle_overworld_entrances or world.settings.spawn_positions) and \
- (entrance_placed == None or entrance_placed.type in ('SpecialInterior', 'Hideout', 'Overworld', 'OverworldOneWay', 'Spawn', 'WarpSong', 'OwlDrop')):
+ (entrance_placed is None or entrance_placed.type in ('SpecialInterior', 'Hideout', 'Overworld', 'OverworldOneWay', 'Spawn', 'WarpSong', 'OwlDrop')):
# At least one valid starting region with all basic refills should be reachable without using any items at the beginning of the seed
# Note this creates new empty states rather than reuse the worlds' states (which already have starting items)
no_items_search = Search([State(w) for w in worlds])
@@ -1011,7 +1029,7 @@ def validate_world(world: World, worlds: list[World], entrance_placed: Optional[
raise EntranceShuffleError('Path to Temple of Time as child is not guaranteed')
if (world.shuffle_interior_entrances or world.settings.shuffle_overworld_entrances) and \
- (entrance_placed == None or entrance_placed.type in ('Interior', 'SpecialInterior', 'Hideout', 'Overworld', 'OverworldOneWay', 'Spawn', 'WarpSong', 'OwlDrop')):
+ (entrance_placed is None or entrance_placed.type in ('Interior', 'SpecialInterior', 'Hideout', 'Overworld', 'OverworldOneWay', 'Spawn', 'WarpSong', 'OwlDrop')):
# The Big Poe Shop should always be accessible as adult without the need to use any bottles
# This is important to ensure that players can never lock their only bottles by filling them with Big Poes they can't sell
# We can use starting items in this check as long as there are no exits requiring the use of a bottle without refills
@@ -1025,6 +1043,7 @@ def validate_world(world: World, worlds: list[World], entrance_placed: Optional[
for idx1 in range(len(placed_one_way_entrances)):
try:
entrance1 = placed_one_way_entrances[idx1][0]
+ assert entrance1.connected_region is not None
hint_area1 = HintArea.at(entrance1.connected_region)
except HintAreaNotFound:
pass
@@ -1032,6 +1051,7 @@ def validate_world(world: World, worlds: list[World], entrance_placed: Optional[
for idx2 in range(idx1):
try:
entrance2 = placed_one_way_entrances[idx2][0]
+ assert entrance2.connected_region is not None
if entrance1.type == entrance2.type and hint_area1 == HintArea.at(entrance2.connected_region):
raise EntranceShuffleError(f'Multiple {entrance1.type} entrances lead to {hint_area1}')
except HintAreaNotFound:
@@ -1058,6 +1078,7 @@ def entrance_unreachable_as(entrance: Entrance, age: str, already_checked: Optio
# Other entrances such as Interior, Dungeon or Grotto are fine unless they have a parent which is one of the above cases
# Recursively check parent entrances to verify that they are also not reachable as the wrong age
+ assert entrance.parent_region is not None
for parent_entrance in entrance.parent_region.entrances:
if parent_entrance in already_checked: continue
unreachable = entrance_unreachable_as(parent_entrance, age, already_checked)
@@ -1094,30 +1115,47 @@ def get_entrance_replacing(region: Region, entrance_name: str) -> Optional[Entra
# 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:
- entrance.connect(target_entrance.disconnect())
+ previously_connected = target_entrance.disconnect()
+ assert previously_connected is not None
+ entrance.connect(previously_connected)
entrance.replaces = target_entrance.replaces
- if entrance.reverse:
- target_entrance.replaces.reverse.connect(entrance.reverse.assumed.disconnect())
+ if entrance.reverse is not None:
+ assert target_entrance.replaces is not None
+ assert target_entrance.replaces.reverse is not None
+ assert entrance.reverse.assumed is not None
+ previously_connected_reverse = entrance.reverse.assumed.disconnect()
+ assert previously_connected_reverse is not None
+ target_entrance.replaces.reverse.connect(previously_connected_reverse)
target_entrance.replaces.reverse.replaces = entrance.reverse
# Restore connections between an entrance and a target assumed entrance
def restore_connections(entrance: Entrance, target_entrance: Entrance) -> None:
- target_entrance.connect(entrance.disconnect())
+ previously_connected = entrance.disconnect()
+ assert previously_connected is not None
+ target_entrance.connect(previously_connected)
entrance.replaces = None
- if entrance.reverse:
- entrance.reverse.assumed.connect(target_entrance.replaces.reverse.disconnect())
+ if entrance.reverse is not None:
+ assert entrance.reverse.assumed is not None
+ assert target_entrance.replaces is not None
+ assert target_entrance.replaces.reverse is not None
+ previously_connected_reverse = target_entrance.replaces.reverse.disconnect()
+ assert previously_connected_reverse is not None
+ entrance.reverse.assumed.connect(previously_connected_reverse)
target_entrance.replaces.reverse.replaces = None
# 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:
delete_target_entrance(target_entrance)
- logging.getLogger('').debug('Connected %s To %s [World %d]', entrance, entrance.connected_region, entrance.world.id)
- if entrance.reverse:
+ logging.getLogger('').debug(f'Connected {entrance} To {entrance.connected_region} [{entrance.world}]')
+ if entrance.reverse is not None:
+ assert target_entrance.replaces is not None
+ assert entrance.reverse.assumed is not None
replaced_reverse = target_entrance.replaces.reverse
+ assert replaced_reverse is not None
delete_target_entrance(entrance.reverse.assumed)
- logging.getLogger('').debug('Connected %s To %s [World %d]', replaced_reverse, replaced_reverse.connected_region, replaced_reverse.world.id)
+ logging.getLogger('').debug(f'Connected {replaced_reverse} To {replaced_reverse.connected_region} [{replaced_reverse.world}]')
# Delete an assumed target entrance, by disconnecting it if needed and removing it from its parent region
diff --git a/Fill.py b/Fill.py
index b2ef1d081..36ce36e7f 100644
--- a/Fill.py
+++ b/Fill.py
@@ -27,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]) -> None:
if worlds[0].settings.shuffle_song_items == 'song':
song_location_names = location_groups['Song']
elif worlds[0].settings.shuffle_song_items == 'dungeon':
@@ -54,13 +54,15 @@ def distribute_items_restrictive(worlds: list[World], fill_locations: Optional[l
shop_locations = [location for world in worlds for location in world.get_unfilled_locations() if location.type == 'Shop' and location.price is None]
- # If not passed in, then get a shuffled list of locations to fill in
- if not fill_locations:
- fill_locations = [
- location for world in worlds for location in world.get_unfilled_locations()
- if location not in song_locations
- and location not in shop_locations
- and not location.type.startswith('Hint')]
+ # get a shuffled list of locations to fill in
+ fill_locations = [
+ location
+ for world in worlds
+ for location in world.get_unfilled_locations()
+ if location not in song_locations
+ and location not in shop_locations
+ and not location.type.startswith('Hint')
+ ]
world_states = [world.state for world in worlds]
# Generate the itempools
@@ -98,7 +100,7 @@ def distribute_items_restrictive(worlds: list[World], fill_locations: Optional[l
junk_items = remove_junk_items.copy()
junk_items.remove('Ice Trap')
major_items = [name for name, item in ItemInfo.items.items() if item.type == 'Item' and item.advancement and item.index is not None]
- fake_items = []
+ fake_items: list[Item] = []
if worlds[0].settings.ice_trap_appearance == 'major_only':
model_items = [item for item in itempool if item.majoritem]
if len(model_items) == 0: # All major items were somehow removed from the pool (can happen in plando)
@@ -146,9 +148,14 @@ def distribute_items_restrictive(worlds: list[World], fill_locations: Optional[l
# If some dungeons are supposed to be empty, fill them with useless items.
if worlds[0].settings.empty_dungeons_mode != 'none':
- empty_locations = [location for location in fill_locations
- if location.world.empty_dungeons[HintArea.at(location).dungeon_name].empty]
+ empty_locations = []
+ for location in fill_locations:
+ assert location.world is not None
+ dungeon_name = HintArea.at(location).dungeon_name
+ if dungeon_name is not None and location.world.empty_dungeons[dungeon_name].empty:
+ empty_locations.append(location)
for location in empty_locations:
+ assert location.world is not None
fill_locations.remove(location)
if worlds[0].settings.shuffle_mapcompass in ['any_dungeon', 'overworld', 'keysanity', 'regional']:
@@ -208,9 +215,9 @@ def distribute_items_restrictive(worlds: list[World], fill_locations: Optional[l
# Log unplaced item/location warnings
for item in progitempool + prioitempool + restitempool:
- logger.error('Unplaced Items: %s [World %d]' % (item.name, item.world.id))
+ logger.error(f'Unplaced Items: {item.name} [{item.world}]')
for location in fill_locations:
- logger.error('Unfilled Locations: %s [World %d]' % (location.name, location.world.id))
+ logger.error(f'Unfilled Locations: {location.name} [{location.world}]')
if progitempool + prioitempool + restitempool:
raise FillError('Not all items are placed.')
@@ -224,9 +231,9 @@ def distribute_items_restrictive(worlds: list[World], fill_locations: Optional[l
worlds[0].settings.distribution.cloak(worlds, [cloakable_locations], [all_models])
for world in worlds:
- for location in world.get_filled_locations():
+ for location in world.get_filled_locations(lambda item: item.advancement):
# Get the maximum amount of wallets required to purchase an advancement item.
- if world.maximum_wallets < 3 and location.price and location.item.advancement:
+ if world.maximum_wallets < 3 and location.price:
if location.price > 500:
world.maximum_wallets = 3
elif world.maximum_wallets < 2 and location.price > 200:
@@ -305,6 +312,7 @@ def fill_dungeon_unique_item(worlds: list[World], search: Search, fill_locations
# Sort major items in such a way that they are placed first if dungeon restricted.
# There still won't be enough locations for small keys in one item per dungeon mode, though.
for item in list(major_items):
+ assert item.world is not None
if not item.world.get_region('Root').can_fill(item):
major_items.remove(item)
major_items.append(item)
@@ -329,6 +337,7 @@ def fill_dungeon_unique_item(worlds: list[World], search: Search, fill_locations
# Error out if we have any items that won't be placeable in the overworld left.
for item in major_items:
+ assert item.world is not None
if not item.world.get_region('Root').can_fill(item):
raise FillError(f"No more dungeon locations available for {item.name} to be placed with 'Dungeons Have One Major Item' enabled. To fix this, either disable 'Dungeons Have One Major Item' or enable some settings that add more locations for shuffled items in the overworld.")
@@ -343,8 +352,8 @@ def fill_ownworld_restrictive(worlds: list[World], search: Search, locations: li
unplaced_prizes = [item for item in ownpool if item.name not in placed_prizes]
empty_locations = [loc for loc in locations if loc.item is None]
- prizepool_dict = {world.id: [item for item in unplaced_prizes if item.world.id == world.id] for world in worlds}
- prize_locs_dict = {world.id: [loc for loc in empty_locations if loc.world.id == world.id] for world in worlds}
+ prizepool_dict = {world.id: [item for item in unplaced_prizes if item.world is not None and item.world.id == world.id] for world in worlds}
+ prize_locs_dict = {world.id: [loc for loc in empty_locations if loc.world is not None and loc.world.id == world.id] for world in worlds}
# Shop item being sent in to this method are tied to their own world.
# Therefore, let's do this one world at a time. We do this to help
@@ -412,6 +421,7 @@ def fill_restrictive(worlds: list[World], base_search: Search, locations: list[L
# get an item and remove it from the itempool
item_to_place = itempool.pop()
+ assert item_to_place.world is not None
if item_to_place.priority:
l2cations = [l for l in locations if l.can_fill_fast(item_to_place)]
elif item_to_place.majoritem:
@@ -444,6 +454,7 @@ def fill_restrictive(worlds: list[World], base_search: Search, locations: list[L
# in the world we are placing it (possibly checking for reachability)
spot_to_fill = None
for location in l2cations:
+ assert location.world is not None
if location.can_fill(max_search.state_list[location.world.id], item_to_place, perform_access_check):
# for multiworld, make it so that the location is also reachable
# in the world the item is for. This is to prevent early restrictions
@@ -492,6 +503,7 @@ def fill_restrictive(worlds: list[World], base_search: Search, locations: list[L
raise FillError(f'Game unbeatable: No more spots to place {item_to_place} [World {item_to_place.world.id + 1}] from {len(l2cations)} locations ({len(locations)} total); {len(itempool)} other items left to place, plus {len(unplaced_items)} skipped')
# Place the item in the world and continue
+ assert spot_to_fill.world is not None
spot_to_fill.world.push_item(spot_to_fill, item_to_place)
locations.remove(spot_to_fill)
@@ -531,6 +543,7 @@ def fill_restrictive_fast(worlds: list[World], locations: list[Location], itempo
break
# Place the item in the world and continue
+ assert spot_to_fill.world is not None
spot_to_fill.world.push_item(spot_to_fill, item_to_place)
locations.remove(spot_to_fill)
@@ -542,5 +555,6 @@ def fast_fill(locations: list[Location], itempool: list[Item]) -> None:
random.shuffle(locations)
while itempool and locations:
spot_to_fill = locations.pop()
+ assert spot_to_fill.world is not None
item_to_place = itempool.pop()
spot_to_fill.world.push_item(spot_to_fill, item_to_place)
diff --git a/Goals.py b/Goals.py
index 47371eecd..16600e84f 100644
--- a/Goals.py
+++ b/Goals.py
@@ -2,7 +2,7 @@
import sys
from collections import defaultdict
from collections.abc import Iterable, Collection
-from typing import TYPE_CHECKING, Optional, Any
+from typing import TYPE_CHECKING, Optional, Any, TypedDict
from HintList import BOSS_GOAL_TABLE, REWARD_GOAL_TABLE, get_hint_group, hint_exclusions
from ItemList import item_table
@@ -21,7 +21,6 @@
from World import World
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] = [
'White',
@@ -35,8 +34,15 @@
]
+class GoalItem(TypedDict):
+ name: str
+ quantity: int
+ minimum: int
+ hintable: bool
+
+
class Goal:
- def __init__(self, world: World, name: str, hint_text: str | dict[str, str], color: str, items: Optional[list[dict[str, Any]]] = None,
+ def __init__(self, world: World, name: str, hint_text: str | dict[str, str], color: str, items: Optional[list[GoalItem]] = None,
locations=None, lock_locations=None, lock_entrances: Optional[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:
@@ -75,7 +81,7 @@ def get_item(self, item: str) -> GoalItem:
def requires(self, item: str) -> bool:
# Prevent direct hints for certain items that can have many duplicates, such as tokens and Triforce Pieces
names = [item]
- if item_table[item][3] is not None and 'alias' in item_table[item][3]:
+ if 'alias' in item_table[item][3]:
names.append(item_table[item][3]['alias'][0])
return any(i['name'] in names and not i['hintable'] for i in self.items)
@@ -85,11 +91,11 @@ def __repr__(self) -> str:
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: Optional[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.lock_entrances: Optional[list[str]] = lock_entrances
self.goals: list[Goal] = []
self.goal_count: int = goal_count
self.minimum_goals: int = minimum_goals
@@ -159,6 +165,7 @@ def replace_goal_names(worlds: list[World]) -> None:
for goal in category.goals:
if isinstance(goal.hint_text, dict):
for boss in bosses:
+ assert boss.item is not None
if boss.item.name == goal.hint_text['replace']:
flavor_text, clear_text, color = BOSS_GOAL_TABLE[boss.name]
if world.settings.clearer_hints:
@@ -186,11 +193,15 @@ def update_goal_items(spoiler: Spoiler) -> None:
# item_locations: only the ones that should appear as "required"/WotH
all_locations = [location for world in worlds for location in world.get_filled_locations()]
# Set to test inclusion against
- item_locations = {location for location in all_locations if location.item.majoritem and not location.locked}
+ item_locations = set()
+ for location in all_locations:
+ assert location.item is not None
+ if location.item.majoritem and not location.locked:
+ item_locations.add(location)
# required_locations[category.name][goal.name][world_id] = [...]
required_locations: RequiredLocations = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
- priority_locations = {world.id: {} for world in worlds}
+ priority_locations: dict[int, dict[str, str]] = {world.id: {} for world in worlds}
# rebuild hint exclusion list
for world in worlds:
diff --git a/Gui.py b/Gui.py
index e7c7efb58..19ae6cb0f 100755
--- a/Gui.py
+++ b/Gui.py
@@ -39,10 +39,12 @@ def gui_main() -> None:
def version_check(name: str, version: str, url: str) -> None:
- try:
- process = subprocess.Popen([shutil.which(name.lower()), "--version"], stdout=subprocess.PIPE)
- except Exception as ex:
- raise VersionError('{name} is not installed. Please install {name} {version} or later'.format(name=name, version=version), url)
+ path = shutil.which(name.lower())
+ if path is None:
+ raise VersionError(f'{name} is not installed. Please install {name} {version} or later', url)
+ else:
+ process = subprocess.Popen([path, "--version"], stdout=subprocess.PIPE)
+ assert process.stdout is not None
while True:
line = str(process.stdout.readline().strip(), 'UTF-8')
diff --git a/HintList.py b/HintList.py
index fd5cfe69a..22b460b1c 100644
--- a/HintList.py
+++ b/HintList.py
@@ -70,13 +70,13 @@ def get_hint_group(group: str, world: World) -> list[Hint]:
hint = get_hint(name, world.settings.clearer_hints)
if hint.name in world.always_hints and group == 'always':
- hint.type = 'always'
+ hint.type = ['always']
if group == 'dual_always' and hint.name in conditional_dual_always and conditional_dual_always[hint.name](world):
- hint.type = 'dual_always'
+ hint.type = ['dual_always']
if group == 'entrance_always' and hint.name in conditional_entrance_always and conditional_entrance_always[hint.name](world):
- hint.type = 'entrance_always'
+ hint.type = ['entrance_always']
conditional_keep = True
type_append = False
@@ -86,13 +86,14 @@ def get_hint_group(group: str, world: World) -> list[Hint]:
# Hint inclusion override from distribution
if (group in world.added_hint_types or group in world.item_added_hint_types):
if hint.name in world.added_hint_types[group]:
- hint.type = group
+ hint.type = [group]
type_append = True
if name_is_location(name, hint.type, world):
location = world.get_location(name)
+ assert location.item is not None
for i in world.item_added_hint_types[group]:
if i == location.item.name:
- hint.type = group
+ hint.type = [group]
type_append = True
for i in world.item_hint_type_overrides[group]:
if i == location.item.name:
@@ -104,6 +105,7 @@ def get_hint_group(group: str, world: World) -> list[Hint]:
if group in world.item_hint_type_overrides:
if name_is_location(name, hint.type, world):
location = world.get_location(name)
+ assert location.item is not None
if location.item.name in world.item_hint_type_overrides[group]:
type_override = True
elif name in multiTable.keys():
@@ -111,6 +113,7 @@ def get_hint_group(group: str, world: World) -> list[Hint]:
for locationName in multi.locations:
if locationName not in hint_exclusions(world):
location = world.get_location(locationName)
+ assert location.item is not None
if location.item.name in world.item_hint_type_overrides[group]:
type_override = True
@@ -148,6 +151,7 @@ def get_upgrade_hint_list(world: World, locations: list[str]) -> list[Hint]:
for locationName in multi.locations:
if locationName not in hint_exclusions(world):
location = world.get_location(locationName)
+ assert location.item is not None
if location.item.name in world.item_hint_type_overrides[hint_type]:
type_override = True
@@ -1950,18 +1954,17 @@ def rainbow_bridge_hint_kind(world: World) -> str:
# This specifies which hints will never appear due to either having known or known useless contents or due to the locations not existing.
+HINT_EXCLUSION_CACHE: dict[int, list[str]] = {}
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 HINT_EXCLUSION_CACHE:
+ return HINT_EXCLUSION_CACHE[world.id]
- if not clear_cache and world.id in exclusions:
- return exclusions[world.id]
-
- exclusions[world.id] = []
- exclusions[world.id].extend(world.settings.disabled_locations)
+ HINT_EXCLUSION_CACHE[world.id] = []
+ HINT_EXCLUSION_CACHE[world.id].extend(world.settings.disabled_locations)
for location in world.get_locations():
if location.locked:
- exclusions[world.id].append(location.name)
+ HINT_EXCLUSION_CACHE[world.id].append(location.name)
world_location_names = [
location.name for location in world.get_locations()]
@@ -1986,18 +1989,15 @@ def hint_exclusions(world: World, clear_cache: bool = False) -> list[str]:
'dual_always']):
multi = get_multi(hint.name)
exclude_hint = False
- for location in multi.locations:
- if location not in world_location_names or world.get_location(location).locked:
+ for location_name in multi.locations:
+ if location_name not in world_location_names or world.get_location(location_name).locked:
exclude_hint = True
if exclude_hint:
- exclusions[world.id].append(hint.name)
+ HINT_EXCLUSION_CACHE[world.id].append(hint.name)
else:
- if hint.name not in world_location_names and hint.name not in exclusions[world.id]:
- exclusions[world.id].append(hint.name)
- return exclusions[world.id]
-
-
-hint_exclusions.exclusions = {}
+ if hint.name not in world_location_names and hint.name not in HINT_EXCLUSION_CACHE[world.id]:
+ HINT_EXCLUSION_CACHE[world.id].append(hint.name)
+ return HINT_EXCLUSION_CACHE[world.id]
def name_is_location(name: str, hint_type: str | Collection[str], world: World) -> bool:
@@ -2013,4 +2013,4 @@ def name_is_location(name: str, hint_type: str | Collection[str], world: World)
def clear_hint_exclusion_cache() -> None:
- hint_exclusions.exclusions.clear()
+ HINT_EXCLUSION_CACHE.clear()
diff --git a/Hints.py b/Hints.py
index 3817f5384..e8fc4ccba 100644
--- a/Hints.py
+++ b/Hints.py
@@ -30,15 +30,14 @@
if TYPE_CHECKING:
from Dungeon import Dungeon
from Entrance import Entrance
- from Goals import GoalCategory
+ from Goals import Goal, GoalCategory
from Location import Location
from Spoiler import Spoiler
from World import World
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]"
+HintFunc: TypeAlias = "Callable[[Spoiler, World, set[HintArea | str], set[HintArea | str]], HintReturn]"
bingoBottlesForHints: set[str] = {
"Bottle", "Bottle with Red Potion", "Bottle with Green Potion", "Bottle with Blue Potion",
@@ -70,9 +69,9 @@
class RegionRestriction(Enum):
- NONE = 0,
- DUNGEON = 1,
- OVERWORLD = 2,
+ NONE = 0
+ DUNGEON = 1
+ OVERWORLD = 2
class GossipStone:
@@ -186,10 +185,10 @@ 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, hint_type: str = None) -> bool:
+ locations: Optional[list[Location]] = None, force_reachable: bool = False, *, hint_type: str) -> bool:
random.shuffle(groups)
skipped_groups = []
- duplicates = []
+ duplicates: list[list[int]] = []
first = True
success = True
@@ -302,8 +301,7 @@ def add_hint(spoiler: Spoiler, world: World, groups: list[list[int]], gossip_tex
def can_reach_hint(worlds: list[World], hint_location: Location, location: Location) -> bool:
- if location is None:
- return True
+ assert location.world is not None
old_item = location.item
location.item = None
@@ -337,7 +335,7 @@ def filter_trailing_space(text: str) -> str:
]
-def get_simple_hint_no_prefix(item: Item) -> Hint:
+def get_simple_hint_no_prefix(item: Item) -> str:
hint = get_hint(item.name, True).text
for prefix in hintPrefixes:
@@ -424,10 +422,11 @@ def at(spot: Spot, use_alt_hint: bool = False) -> HintArea:
if isinstance(spot, Region):
original_parent = spot
else:
+ assert spot.parent_region is not None
original_parent = spot.parent_region
already_checked = []
- spot_queue = [spot]
- fallback_spot_queue = []
+ spot_queue: list[Spot] = [spot]
+ fallback_spot_queue: list[Spot] = []
while spot_queue or fallback_spot_queue:
if not spot_queue:
@@ -439,6 +438,7 @@ def at(spot: Spot, use_alt_hint: bool = False) -> HintArea:
if isinstance(current_spot, Region):
parent_region = current_spot
else:
+ assert current_spot.parent_region is not None
parent_region = current_spot.parent_region
if (parent_region.hint or (use_alt_hint and parent_region.alt_hint)) and (original_parent.name == 'Root' or parent_region.name != 'Root'):
@@ -454,7 +454,7 @@ def at(spot: Spot, use_alt_hint: bool = False) -> HintArea:
else:
spot_queue.append(entrance)
- raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id))
+ raise HintAreaNotFound(f'No hint area could be found for {spot} [{spot.world}]')
@classmethod
def for_dungeon(cls, dungeon_name: str) -> Optional[HintArea]:
@@ -506,6 +506,7 @@ def dungeon(self, world: World) -> Optional[Dungeon]:
return dungeons[0]
def is_dungeon_item(self, item: Item) -> bool:
+ assert item.world is not None
for dungeon in item.world.dungeons:
if dungeon.name == self.dungeon_name:
return dungeon.is_dungeon_item(item)
@@ -514,7 +515,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, use_2nd_person: bool = False, world: Optional[int] = None) -> str:
- if self.is_dungeon and self.dungeon_name:
+ if self.dungeon_name is not None:
text = get_hint(self.dungeon_name, clearer_hints).text
else:
text = str(self)
@@ -543,22 +544,25 @@ def text(self, clearer_hints: bool, preposition: bool = False, use_2nd_person: b
return text
-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
+def get_woth_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ locations = [
+ location
+ for location in spoiler.required_locations[world.id]
+ if location.name not in all_checked
and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] and HintArea.at(location).is_dungeon)
and location.name not in world.hint_exclusions
and location.name not in world.hint_type_overrides['woth']
+ and location.item is not None
and location.item.name not in world.item_hint_type_overrides['woth']
- and location.item.name not in unHintableWothItems,
- locations))
+ and location.item.name not in unHintableWothItems
+ ]
if not locations:
return None
location = random.choice(locations)
- checked.add(location.name)
+ assert location.item is not None
+ all_checked.add(location.name)
hint_area = HintArea.at(location)
if hint_area.is_dungeon:
@@ -568,21 +572,29 @@ def get_woth_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu
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: set[str]) -> set[HintArea | str]:
- def get_area_from_name(check: str) -> HintArea | str:
+def get_checked_areas(world: World, checked: set[HintArea | str]) -> set[HintArea]:
+ def get_area_from_name(check: str) -> Optional[HintArea]:
try:
location = world.get_location(check)
except Exception:
- return check
+ return None
# Don't consider dungeons as already hinted from the reward hint on the Temple of Time altar
if location.type == 'Boss' and world.settings.shuffle_dungeon_rewards in ('vanilla', 'reward'):
return None
return HintArea.at(location)
- return set(get_area_from_name(check) for check in checked)
+ areas = set()
+ for check in checked:
+ if isinstance(check, HintArea):
+ areas.add(check)
+ else:
+ area = get_area_from_name(check)
+ if area is not None:
+ areas.add(area)
+ return areas
-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]) -> Optional[GoalCategory]:
cat_sizes = []
cat_names = []
zero_weights = True
@@ -617,7 +629,7 @@ def get_goal_category(spoiler: Spoiler, world: World, goal_categories: dict[str,
return goal_category
-def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
+def get_goal_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
goal_category = get_goal_category(spoiler, world, world.goal_categories)
# check if no goals were generated (and thus no categories available)
@@ -625,13 +637,12 @@ def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu
return None
goals = goal_category.goals
- category_locations = []
zero_weights = True
- required_location_reverse_map = defaultdict(list)
+ required_location_reverse_map: defaultdict[Location, list[tuple[Goal, int]]] = defaultdict(list)
# Filters Goal.required_locations to those still eligible to be hinted.
hintable_required_locations_filter = (lambda required_location:
- required_location[0].name not in checked
+ required_location[0].name not in all_checked
and required_location[0].name not in world.hint_exclusions
and required_location[0].name not in world.hint_type_overrides['goal']
and required_location[0].item.name not in world.item_hint_type_overrides['goal']
@@ -668,8 +679,9 @@ def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu
goals = goal_category.goals
location, goal_list = random.choice(list(required_location_reverse_map.items()))
+ assert location.item is not None
goal, world_id = random.choice(goal_list)
- checked.add(location.name)
+ all_checked.add(location.name)
# Make sure this wasn't the last hintable location for other goals.
# If so, set weights to zero. This is important for one-hint-per-goal.
@@ -707,10 +719,7 @@ def get_goal_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintRetu
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: set[str], all_checked: set[str]) -> HintReturn:
- if not hasattr(world, 'get_barren_hint_prev'):
- world.get_barren_hint_prev = RegionRestriction.NONE
-
+def get_barren_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
checked_areas = get_checked_areas(world, checked)
areas = list(filter(lambda area:
area not in checked_areas
@@ -770,9 +779,12 @@ def is_not_checked(locations: Iterable[Location], checked: set[HintArea | str])
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: set[str]) -> HintReturn:
- locations = list(filter(lambda location:
- is_not_checked([location], checked)
+def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ locations = [
+ location
+ for location in world.get_filled_locations()
+ if is_not_checked([location], all_checked)
+ and location.item is not None
and ((location.item.majoritem
and location.item.name not in unHintableWothItems)
or location.name in world.added_hint_types['item']
@@ -780,13 +792,14 @@ def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hin
and not location.locked
and location.name not in world.hint_exclusions
and location.name not in world.hint_type_overrides['item']
- and location.item.name not in world.item_hint_type_overrides['item'],
- world.get_filled_locations()))
+ and location.item.name not in world.item_hint_type_overrides['item']
+ ]
if not locations:
return None
location = random.choice(locations)
- checked.add(location.name)
+ assert location.item is not None
+ all_checked.add(location.name)
item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text
hint_area = HintArea.at(location)
@@ -798,7 +811,7 @@ def get_good_item_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hin
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: set[str]) -> HintReturn:
+def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
if len(world.named_item_pool) == 0:
logger = logging.getLogger('')
logger.info("Named item hint requested, but pool is empty.")
@@ -809,8 +822,9 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
if itemname == "Bottle" and world.settings.hint_dist == "bingo":
locations = [
location for location in world.get_filled_locations()
- if (is_not_checked([location], checked)
+ if (is_not_checked([location], all_checked)
and location.name not in world.hint_exclusions
+ and location.item is not None
and location.item.name in bingoBottlesForHints
and not location.locked
and location.name not in world.hint_type_overrides['named-item']
@@ -819,8 +833,9 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
else:
locations = [
location for location in world.get_filled_locations()
- if (is_not_checked([location], checked)
+ if (is_not_checked([location], all_checked)
and location.name not in world.hint_exclusions
+ and location.item is not None
and location.item.name == itemname
and not location.locked
and location.name not in world.hint_type_overrides['named-item']
@@ -841,7 +856,8 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
return None
location = random.choice(locations)
- checked.add(location.name)
+ assert location.item is not None
+ all_checked.add(location.name)
item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text
hint_area = HintArea.at(location)
@@ -867,13 +883,15 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
all_named_items = set(itertools.chain.from_iterable([w.named_item_pool for w in worlds]))
if "Bottle" in all_named_items and world.settings.hint_dist == "bingo":
all_named_items.update(bingoBottlesForHints)
- named_item_locations = [location for w in worlds for location in w.get_filled_locations() if (location.item.name in all_named_items)]
+ named_item_locations = [location for w in worlds for location in w.get_filled_locations(lambda item: item.name in all_named_items)]
spoiler._cached_named_item_locations = named_item_locations
always_hints = [(hint, w.id) for w in worlds for hint in get_hint_group('always', w)]
always_locations = []
for hint, id in always_hints:
location = worlds[id].get_location(hint.name)
+ assert location.item is not None
+ assert location.item.world is not None
if location.item.name in bingoBottlesForHints and world.settings.hint_dist == 'bingo':
always_item = 'Bottle'
else:
@@ -885,7 +903,7 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
if itemname == "Bottle" and world.settings.hint_dist == "bingo":
locations = [
location for location in named_item_locations
- if (is_not_checked([location], checked)
+ if (is_not_checked([location], all_checked)
and location.item.world.id == world.id
and location.name not in world.hint_exclusions
and location.item.name in bingoBottlesForHints
@@ -896,7 +914,7 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
else:
locations = [
location for location in named_item_locations
- if (is_not_checked([location], checked)
+ if (is_not_checked([location], all_checked)
and location.item.world.id == world.id
and location.name not in world.hint_exclusions
and location.item.name == itemname
@@ -922,7 +940,9 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
return None
location = random.choice(locations)
- checked.add(location.name)
+ assert location.item is not None
+ assert location.world is not None
+ all_checked.add(location.name)
item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text
hint_area = HintArea.at(location)
@@ -937,9 +957,10 @@ def get_specific_item_hint(spoiler: Spoiler, world: World, checked: set[str]) ->
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: set[str]) -> HintReturn:
+def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
locations = list(filter(lambda location:
is_not_checked([location], checked)
+ and location.item is not None
and location.item.type not in ('Drop', 'Event', 'Shop')
and not is_restricted_dungeon_item(location.item)
and not location.locked
@@ -952,7 +973,8 @@ def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[str])
return None
location = random.choice(locations)
- checked.add(location.name)
+ assert location.item is not None
+ all_checked.add(location.name)
item_text = get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text
hint_area = HintArea.at(location)
@@ -964,7 +986,7 @@ def get_random_location_hint(spoiler: Spoiler, world: World, checked: set[str])
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: set[str], hint_type: str) -> HintReturn:
+def get_specific_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], hint_type: str) -> HintReturn:
def is_valid_hint(hint: Hint) -> bool:
location = world.get_location(hint.name)
if not is_not_checked([world.get_location(hint.name)], checked):
@@ -999,6 +1021,7 @@ def is_valid_hint(hint: Hint) -> bool:
return get_specific_multi_hint(spoiler, world, checked, hint)
location = world.get_location(hint.name)
+ assert location.item is not None
checked.add(location.name)
if location.name in world.hint_text_overrides:
@@ -1012,23 +1035,23 @@ def is_valid_hint(hint: Hint) -> bool:
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: set[str]) -> HintReturn:
- return get_specific_hint(spoiler, world, checked, 'sometimes')
+def get_sometimes_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ return get_specific_hint(spoiler, world, all_checked, 'sometimes')
-def get_song_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
- return get_specific_hint(spoiler, world, checked, 'song')
+def get_song_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ return get_specific_hint(spoiler, world, all_checked, 'song')
-def get_overworld_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
- return get_specific_hint(spoiler, world, checked, 'overworld')
+def get_overworld_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ return get_specific_hint(spoiler, world, all_checked, 'overworld')
-def get_dungeon_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
- return get_specific_hint(spoiler, world, checked, 'dungeon')
+def get_dungeon_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ return get_specific_hint(spoiler, world, all_checked, 'dungeon')
-def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hint_type: str) -> HintReturn:
+def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[HintArea | 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))
@@ -1056,7 +1079,7 @@ def get_random_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hin
return get_specific_multi_hint(spoiler, world, checked, hint)
-def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: set[str], hint: Hint) -> HintReturn:
+def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], hint: Hint) -> HintReturn:
multi = get_multi(hint.name)
locations = [world.get_location(location) for location in multi.locations]
@@ -1080,33 +1103,36 @@ def get_specific_multi_hint(spoiler: Spoiler, world: World, checked: set[str], h
else:
gossip_string = gossip_string + '#%s# '
- items = [location.item for location in locations]
+ items = [location.item for location in locations if location.item is not None]
text_segments = [multi_text] + [get_hint(get_item_generic_name(item), world.settings.clearer_hints).text for item in items]
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: set[str]) -> HintReturn:
- return get_random_multi_hint(spoiler, world, checked, 'dual')
+def get_dual_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
+ return get_random_multi_hint(spoiler, world, all_checked, 'dual')
-def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
+def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
if not world.entrance_shuffle:
return None
- entrance_hints = list(filter(lambda hint: hint.name not in checked, get_hint_group('entrance', world)))
+ entrance_hints = list(filter(lambda hint: hint.name not in all_checked, get_hint_group('entrance', world)))
shuffled_entrance_hints = list(filter(lambda entrance_hint: world.get_entrance(entrance_hint.name).shuffled, entrance_hints))
regions_with_hint = [hint.name for hint in get_hint_group('region', world)]
- valid_entrance_hints = list(filter(lambda entrance_hint:
- (world.get_entrance(entrance_hint.name).connected_region.name in regions_with_hint or
- world.get_entrance(entrance_hint.name).connected_region.dungeon), shuffled_entrance_hints))
+ valid_entrance_hints = []
+ for entrance_hint in shuffled_entrance_hints:
+ entrance = world.get_entrance(entrance_hint.name)
+ assert entrance.connected_region is not None
+ if entrance.connected_region.name in regions_with_hint or entrance.connected_region.dungeon:
+ valid_entrance_hints.append(entrance_hint)
if not valid_entrance_hints:
return None
entrance_hint = random.choice(valid_entrance_hints)
entrance = world.get_entrance(entrance_hint.name)
- checked.add(entrance.name)
+ all_checked.add(entrance.name)
entrance_text = entrance_hint.text
@@ -1114,7 +1140,8 @@ def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hint
entrance_text = '#%s#' % entrance_text
connected_region = entrance.connected_region
- if connected_region.dungeon:
+ assert connected_region is not None
+ if connected_region.dungeon is not None:
region_text = get_hint(connected_region.dungeon.name, world.settings.clearer_hints).text
else:
region_text = get_hint(connected_region.name, world.settings.clearer_hints).text
@@ -1125,28 +1152,29 @@ def get_entrance_hint(spoiler: Spoiler, world: World, checked: set[str]) -> Hint
return GossipText('%s %s.' % (entrance_text, region_text), ['Green', 'Light Blue']), None
-def get_junk_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
+def get_junk_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | str]) -> HintReturn:
hints = get_hint_group('junk', world)
- hints = list(filter(lambda hint: hint.name not in checked, hints))
+ hints = list(filter(lambda hint: hint.name not in all_checked, hints))
if not hints:
return None
hint = random.choice(hints)
- checked.add(hint.name)
+ all_checked.add(hint.name)
return GossipText(hint.text, prefix=''), None
-def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[str]) -> HintReturn:
+def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[HintArea | str], all_checked: set[HintArea | 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 (HintArea.at(location).text(world.settings.clearer_hints) + ' Important Check') not in all_checked
and HintArea.at(location) != HintArea.ROOT):
top_level_locations.append(HintArea.at(location).text(world.settings.clearer_hints))
hint_loc = random.choice(top_level_locations)
item_count = 0
for location in world.get_filled_locations():
+ assert location.item is not None
region = HintArea.at(location).text(world.settings.clearer_hints)
if region == hint_loc:
if (location.item.majoritem
@@ -1169,7 +1197,7 @@ def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[str])
or world.settings.shuffle_ganon_bosskey == 'dungeons' or world.settings.shuffle_ganon_bosskey == 'tokens'))):
item_count = item_count + 1
- checked.add(hint_loc + ' Important Check')
+ all_checked.add(hint_loc + ' Important Check')
if item_count == 0:
numcolor = 'Red'
@@ -1185,11 +1213,11 @@ def get_important_check_hint(spoiler: Spoiler, world: World, checked: set[str])
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, HintFunc | BarrenFunc] = {
- 'trial': lambda spoiler, world, checked: None,
- 'always': lambda spoiler, world, checked: None,
- 'dual_always': lambda spoiler, world, checked: None,
- 'entrance_always': lambda spoiler, world, checked: None,
+hint_func: dict[str, HintFunc] = {
+ 'trial': lambda spoiler, world, checked, all_checked: None,
+ 'always': lambda spoiler, world, checked, all_checked: None,
+ 'dual_always': lambda spoiler, world, checked, all_checked: None,
+ 'entrance_always': lambda spoiler, world, checked, all_checked: None,
'woth': get_woth_hint,
'goal': get_goal_hint,
'barren': get_barren_hint,
@@ -1231,7 +1259,7 @@ def build_bingo_hint_list(board_url: str) -> list[str]:
with open(data_path('Bingo/bingo_goals.json'), 'r') as bingoFile:
goal_hint_requirements = json.load(bingoFile)
- hints_to_add = {}
+ hints_to_add: dict[str, int] = {}
for goal in goal_list:
# Using 'get' here ensures some level of forward compatibility, where new goals added to randomiser bingo won't
# cause the generator to crash (though those hints won't have item hints for them)
@@ -1255,6 +1283,7 @@ def build_bingo_hint_list(board_url: str) -> list[str]:
def always_named_item(world: World, locations: Iterable[Location]):
for location in locations:
+ assert location.item is not None
if location.item.name in bingoBottlesForHints and world.settings.hint_dist == 'bingo':
always_item = 'Bottle'
else:
@@ -1266,10 +1295,11 @@ def always_named_item(world: World, locations: Iterable[Location]):
def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None:
from Dungeon import Dungeon
- checked_locations = dict()
+ checked_locations: dict[int, set[HintArea | str]] = dict()
# Add misc. item hint locations to "checked" locations if the respective hint is reachable without the hinted item.
for world in worlds:
for location in world.hinted_dungeon_reward_locations.values():
+ assert location.item is not None
if world.settings.enhance_map_compass:
if world.entrance_rando_reward_hints:
# In these settings, there is not necessarily one dungeon reward in each dungeon,
@@ -1299,6 +1329,7 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None:
for compass_location in compass_locations:
if can_reach_hint(worlds, compass_location, location):
item_world = location.world
+ assert item_world is not None
if item_world.id not in checked_locations:
checked_locations[item_world.id] = set()
checked_locations[item_world.id].add(location.name)
@@ -1306,12 +1337,14 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None:
else:
if 'altar' in world.settings.misc_hints and can_reach_hint(worlds, world.get_location('ToT Child Altar Hint' if location.item.info.stone else 'ToT Adult Altar Hint'), location):
item_world = location.world
+ assert item_world is not None
if item_world.id not in checked_locations:
checked_locations[item_world.id] = set()
checked_locations[item_world.id].add(location.name)
for hint_type, location in world.misc_hint_item_locations.items():
if hint_type in world.settings.misc_hints and can_reach_hint(worlds, world.get_location(misc_item_hint_table[hint_type]['hint_location']), location):
item_world = location.world
+ assert item_world is not None
if item_world.id not in checked_locations:
checked_locations[item_world.id] = set()
checked_locations[item_world.id].add(location.name)
@@ -1319,6 +1352,7 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None:
location = world.get_location(misc_location_hint_table[hint_type]['item_location'])
if hint_type in world.settings.misc_hints and can_reach_hint(worlds, world.get_location(misc_location_hint_table[hint_type]['hint_location']), location):
item_world = location.world
+ assert item_world is not None
if item_world.id not in checked_locations:
checked_locations[item_world.id] = set()
checked_locations[item_world.id].add(location.name)
@@ -1336,7 +1370,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[set[str]] = None) -> None:
+def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations: Optional[set[HintArea | str]] = None) -> None:
world.barren_dungeon = 0
world.woth_dungeon = 0
@@ -1348,7 +1382,7 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
if checked_locations is None:
checked_locations = set()
- checked_always_locations = set()
+ checked_always_locations: set[HintArea | str] = set()
stone_ids = list(gossipLocations.keys())
@@ -1366,7 +1400,10 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
raise ValueError(f'Gossip stone location "{stone_name}" is not valid')
if stone_id in stone_ids:
stone_ids.remove(stone_id)
- (gossip_text, _) = get_junk_hint(spoiler, world, checked_locations)
+ all_checked_locations = checked_locations | checked_always_locations
+ hint_return = get_junk_hint(spoiler, world, checked_locations, all_checked_locations)
+ assert hint_return is not None
+ gossip_text, _ = hint_return
spoiler.hints[world.id][stone_id] = gossip_text
stone_groups = []
@@ -1466,7 +1503,9 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
for hint in always_duals:
multi = get_multi(hint.name)
first_location = world.get_location(multi.locations[0])
+ assert first_location.item is not None
second_location = world.get_location(multi.locations[1])
+ assert second_location.item is not None
checked_always_locations.add(first_location.name)
checked_always_locations.add(second_location.name)
@@ -1485,10 +1524,14 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
# Add required location hints, only if hint copies > 0
if hint_dist['always'][1] > 0:
- always_locations = list(filter(lambda hint: is_not_checked([world.get_location(hint.name)], checked_always_locations),
- get_hint_group('always', world)))
+ always_locations = [
+ hint
+ for hint in get_hint_group('always', world)
+ if is_not_checked([world.get_location(hint.name)], checked_always_locations)
+ ]
for hint in always_locations:
location = world.get_location(hint.name)
+ assert location.item is not None
checked_always_locations.add(hint.name)
always_named_item(world, [location])
@@ -1509,6 +1552,7 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
for entrance_hint in always_entrances:
entrance = world.get_entrance(entrance_hint.name)
connected_region = entrance.connected_region
+ assert connected_region is not None
if entrance.shuffled and (connected_region.dungeon or any(hint.name == connected_region.name for hint in
get_hint_group('region', world))):
checked_always_locations.add(entrance.name)
@@ -1556,19 +1600,25 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
# Prevent conflict between Ganondorf Light Arrows hint and required named item hints.
# Assumes that a "wasted" hint is desired since Light Arrows have to be added
# explicitly to the list for named item hints.
- filtered_checked = set(checked_locations | checked_always_locations)
- for location in (checked_locations | checked_always_locations):
- try:
- if world.get_location(location).item.name == 'Light Arrows':
- filtered_checked.remove(location)
- except KeyError:
- pass # checked_always_locations can also contain entrances from entrance_always hints, ignore those here
+ filtered_checked = set(checked_locations)
+ filtered_all_checked = set(checked_locations | checked_always_locations)
+ for checked_location_name in (checked_locations | checked_always_locations):
+ if isinstance(checked_location_name, str):
+ try:
+ checked_location = world.get_location(checked_location_name)
+ except KeyError:
+ pass # checked_always_locations can also contain entrances from entrance_always hints, ignore those here
+ else:
+ assert checked_location.item is not None
+ if checked_location.item.name == 'Light Arrows':
+ filtered_checked.discard(checked_location_name)
+ filtered_all_checked.discard(checked_location_name)
for i in range(0, len(world.named_item_pool)):
- hint = get_specific_item_hint(spoiler, world, filtered_checked)
- if hint:
+ hint_return = get_specific_item_hint(spoiler, world, filtered_checked, filtered_all_checked)
+ if hint_return is not None:
checked_locations.update(filtered_checked - checked_always_locations)
- gossip_text, location = hint
- place_ok = add_hint(spoiler, world, stone_groups, gossip_text, hint_dist['named-item'][1], location, hint_type='named-item')
+ gossip_text, locations = hint_return
+ place_ok = add_hint(spoiler, world, stone_groups, gossip_text, hint_dist['named-item'][1], locations, hint_type='named-item')
if not place_ok:
raise Exception('Not enough gossip stones for user-provided item hints')
@@ -1578,8 +1628,8 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
random.shuffle(world.named_item_pool)
hint_types = list(hint_types)
- hint_prob = list(hint_prob)
- hint_counts = {}
+ hint_prob = list(hint_prob)
+ hint_counts: dict[str, int] = {}
custom_fixed = True
while stone_groups:
@@ -1623,29 +1673,27 @@ def build_world_gossip_hints(spoiler: Spoiler, world: World, checked_locations:
raise Exception('Not enough valid hints to fill gossip stone locations.')
all_checked_locations = checked_locations | checked_always_locations
- if hint_type == 'barren':
- hint = hint_func[hint_type](spoiler, world, checked_locations, all_checked_locations)
- else:
- hint = hint_func[hint_type](spoiler, world, all_checked_locations)
+ hint_return = hint_func[hint_type](spoiler, world, checked_locations, all_checked_locations)
+ if hint_type != 'barren':
checked_locations.update(all_checked_locations - checked_always_locations)
- if hint is None:
+ if hint_return is None:
index = hint_types.index(hint_type)
hint_prob[index] = 0
# Zero out the probability in the base distribution in case the probability list is modified
# to fit hint types in remaining gossip stones
hint_dist[hint_type] = (0.0, copies)
else:
- gossip_text, locations = hint
+ gossip_text, locations = hint_return
place_ok = add_hint(spoiler, world, stone_groups, gossip_text, copies, locations, hint_type=hint_type)
if place_ok:
hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1
if locations is None:
- logging.getLogger('').debug('Placed %s hint.', hint_type)
+ logging.getLogger('').debug(f'Placed {hint_type} hint.')
else:
- logging.getLogger('').debug('Placed %s hint for %s.', hint_type, ', '.join([location.name for location in locations]))
+ logging.getLogger('').debug(f'Placed {hint_type} hint for {locations}.')
if not place_ok and custom_fixed:
- logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, ', '.join([location.name for location in locations]))
+ logging.getLogger('').debug(f'Failed to place {hint_type} fixed hint for {locations}.')
fixed_hint_types.insert(0, hint_type)
@@ -1788,6 +1836,8 @@ def build_misc_item_hints(world: World, messages: list[Message], allow_duplicate
text = data['custom_item_text'].format(area='#your pocket#', item=item)
elif hint_type in world.misc_hint_item_locations:
location = world.misc_hint_item_locations[hint_type]
+ assert location.item is not None
+ assert location.world is not None
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.settings.clearer_hints, world=None if location.world.id == world.id else location.world.id + 1)
if item == data['default_item']:
text = data['default_item_text'].format(area=area)
@@ -1801,6 +1851,7 @@ def build_misc_item_hints(world: World, messages: list[Message], allow_duplicate
else:
text = get_hint('Validation Line', world.settings.clearer_hints).text
for location in world.get_filled_locations():
+ assert location.item is not None
if location.name == 'Ganons Tower Boss Key Chest':
text += f"#{get_hint(get_item_generic_name(location.item), world.settings.clearer_hints).text}#"
break
diff --git a/IconManip.py b/IconManip.py
index 919ebc834..43c82efe4 100644
--- a/IconManip.py
+++ b/IconManip.py
@@ -13,7 +13,7 @@
if TYPE_CHECKING:
from Rom import Rom
-RGBValues: TypeAlias = "MutableSequence[MutableSequence[int]]"
+RGBValues: TypeAlias = "list[list[int]]"
# TODO
diff --git a/Item.py b/Item.py
index f233ee774..7cc1bc03b 100644
--- a/Item.py
+++ b/Item.py
@@ -28,17 +28,17 @@ class ItemInfo:
def __init__(self, name: str = '', event: bool = False) -> None:
if event:
item_type = 'Event'
- progressive = True
+ progressive: Optional[bool] = True
item_id = None
- special = None
+ special: dict[str, Any] = {}
else:
- (item_type, progressive, item_id, special) = item_table[name]
+ item_type, progressive, item_id, special = item_table[name]
self.name: str = name
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
self.index: Optional[int] = item_id
self.price: Optional[int] = self.special.get('price', None)
self.bottle: bool = self.special.get('bottle', False)
@@ -201,11 +201,11 @@ def ItemFactory(items: str, world: Optional[World] = None, event: bool = False)
@overload
-def ItemFactory(items: Iterable[str], world: Optional[World] = None, event: bool = False) -> list[Item]:
+def ItemFactory(items: list[str] | tuple[str], world: Optional[World] = None, event: bool = False) -> list[Item]:
pass
-def ItemFactory(items: str | Iterable[str], world: Optional[World] = None, event: bool = False) -> Item | list[Item]:
+def ItemFactory(items: str | list[str] | tuple[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)
diff --git a/ItemList.py b/ItemList.py
index b46d4b45c..a1a9e3fda 100644
--- a/ItemList.py
+++ b/ItemList.py
@@ -330,19 +330,19 @@ class GetItemId(IntEnum):
# special "upgrade_ids" correspond to the item IDs in item_table.c for all of the upgrade tiers
# of that item.
#
-item_table: dict[str, tuple[str, Optional[bool], Optional[int], Optional[dict[str, Any]]]] = {
+item_table: dict[str, tuple[str, Optional[bool], Optional[int], dict[str, Any]]] = {
'Bombs (5)': ('Item', None, GetItemId.GI_BOMBS_5, {'junk': 8}),
'Deku Nuts (5)': ('Item', None, GetItemId.GI_DEKU_NUTS_5, {'junk': 5}),
- 'Bombchus (10)': ('Item', True, GetItemId.GI_BOMBCHUS_10, None),
- 'Boomerang': ('Item', True, GetItemId.GI_BOOMERANG, None),
+ 'Bombchus (10)': ('Item', True, GetItemId.GI_BOMBCHUS_10, {}),
+ 'Boomerang': ('Item', True, GetItemId.GI_BOOMERANG, {}),
'Deku Stick (1)': ('Item', None, GetItemId.GI_DEKU_STICKS_1, {'junk': 5}),
- 'Lens of Truth': ('Item', True, GetItemId.GI_LENS_OF_TRUTH, None),
- 'Megaton Hammer': ('Item', True, GetItemId.GI_HAMMER, None),
+ 'Lens of Truth': ('Item', True, GetItemId.GI_LENS_OF_TRUTH, {}),
+ 'Megaton Hammer': ('Item', True, GetItemId.GI_HAMMER, {}),
'Cojiro': ('Item', True, GetItemId.GI_COJIRO, {'trade': True}),
'Bottle': ('Item', True, GetItemId.GI_BOTTLE_EMPTY, {'bottle': float('Inf')}),
- 'Blue Potion': ('Item', True, GetItemId.GI_BOTTLE_POTION_BLUE, None), # distinct from shop item
+ 'Blue Potion': ('Item', True, GetItemId.GI_BOTTLE_POTION_BLUE, {}), # distinct from shop item
'Bottle with Milk': ('Item', True, GetItemId.GI_BOTTLE_MILK_FULL, {'bottle': float('Inf')}),
- 'Rutos Letter': ('Item', True, GetItemId.GI_BOTTLE_RUTOS_LETTER, None),
+ 'Rutos Letter': ('Item', True, GetItemId.GI_BOTTLE_RUTOS_LETTER, {}),
'Deliver Letter': ('Item', True, None, {'bottle': float('Inf')}),
'Sell Big Poe': ('Item', True, None, {'bottle': float('Inf')}),
'Magic Bean': ('Item', True, GetItemId.GI_MAGIC_BEAN, {'progressive': 10}),
@@ -362,22 +362,22 @@ class GetItemId(IntEnum):
'Eyeball Frog': ('Item', True, GetItemId.GI_EYEBALL_FROG, {'trade': True}),
'Eyedrops': ('Item', True, GetItemId.GI_EYE_DROPS, {'trade': True}),
'Claim Check': ('Item', True, GetItemId.GI_CLAIM_CHECK, {'trade': True}),
- 'Kokiri Sword': ('Item', True, GetItemId.GI_SWORD_KOKIRI, None),
- 'Giants Knife': ('Item', None, GetItemId.GI_SWORD_KNIFE, None),
- 'Deku Shield': ('Item', None, GetItemId.GI_SHIELD_DEKU, None),
- 'Hylian Shield': ('Item', None, GetItemId.GI_SHIELD_HYLIAN, None),
- 'Mirror Shield': ('Item', True, GetItemId.GI_SHIELD_MIRROR, None),
- 'Goron Tunic': ('Item', True, GetItemId.GI_TUNIC_GORON, None),
- 'Zora Tunic': ('Item', True, GetItemId.GI_TUNIC_ZORA, None),
- 'Iron Boots': ('Item', True, GetItemId.GI_BOOTS_IRON, None),
- 'Hover Boots': ('Item', True, GetItemId.GI_BOOTS_HOVER, None),
- 'Stone of Agony': ('Item', True, GetItemId.GI_STONE_OF_AGONY, None),
- 'Gerudo Membership Card': ('Item', True, GetItemId.GI_GERUDOS_CARD, None),
+ 'Kokiri Sword': ('Item', True, GetItemId.GI_SWORD_KOKIRI, {}),
+ 'Giants Knife': ('Item', None, GetItemId.GI_SWORD_KNIFE, {}),
+ 'Deku Shield': ('Item', None, GetItemId.GI_SHIELD_DEKU, {}),
+ 'Hylian Shield': ('Item', None, GetItemId.GI_SHIELD_HYLIAN, {}),
+ 'Mirror Shield': ('Item', True, GetItemId.GI_SHIELD_MIRROR, {}),
+ 'Goron Tunic': ('Item', True, GetItemId.GI_TUNIC_GORON, {}),
+ 'Zora Tunic': ('Item', True, GetItemId.GI_TUNIC_ZORA, {}),
+ 'Iron Boots': ('Item', True, GetItemId.GI_BOOTS_IRON, {}),
+ 'Hover Boots': ('Item', True, GetItemId.GI_BOOTS_HOVER, {}),
+ 'Stone of Agony': ('Item', True, GetItemId.GI_STONE_OF_AGONY, {}),
+ 'Gerudo Membership Card': ('Item', True, GetItemId.GI_GERUDOS_CARD, {}),
'Heart Container': ('Item', True, GetItemId.GI_HEART_CONTAINER, {'alias': ('Piece of Heart', 4), 'progressive': float('Inf')}),
'Piece of Heart': ('Item', True, GetItemId.GI_HEART_PIECE, {'progressive': float('Inf')}),
- 'Boss Key': ('BossKey', True, GetItemId.GI_BOSS_KEY, None),
- 'Compass': ('Compass', None, GetItemId.GI_COMPASS, None),
- 'Map': ('Map', None, GetItemId.GI_DUNGEON_MAP, None),
+ 'Boss Key': ('BossKey', True, GetItemId.GI_BOSS_KEY, {}),
+ 'Compass': ('Compass', None, GetItemId.GI_COMPASS, {}),
+ 'Map': ('Map', None, GetItemId.GI_DUNGEON_MAP, {}),
'Small Key': ('SmallKey', True, GetItemId.GI_SMALL_KEY, {'progressive': float('Inf')}),
'Weird Egg': ('Item', True, GetItemId.GI_WEIRD_EGG, {'trade': True}),
'Recovery Heart': ('Item', None, GetItemId.GI_RECOVERY_HEART, {'junk': 0}),
@@ -387,47 +387,47 @@ class GetItemId(IntEnum):
'Rupee (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN, {'junk': -1}),
'Rupees (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE, {'junk': 10}),
'Rupees (20)': ('Item', None, GetItemId.GI_RUPEE_RED, {'junk': 4}),
- 'Milk': ('Item', None, GetItemId.GI_MILK, None),
+ 'Milk': ('Item', None, GetItemId.GI_MILK, {}),
'Goron Mask': ('Item', None, GetItemId.GI_MASK_GORON, {'trade': True, 'object': 0x0150}),
'Zora Mask': ('Item', None, GetItemId.GI_MASK_ZORA, {'trade': True, 'object': 0x0151}),
'Gerudo Mask': ('Item', None, GetItemId.GI_MASK_GERUDO, {'trade': True, 'object': 0x0152}),
'Rupees (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE, {'junk': 1}),
'Rupees (200)': ('Item', None, GetItemId.GI_RUPEE_GOLD, {'junk': 0}),
- 'Biggoron Sword': ('Item', None, GetItemId.GI_SWORD_BIGGORON, None),
- 'Fire Arrows': ('Item', True, GetItemId.GI_ARROW_FIRE, None),
- 'Ice Arrows': ('Item', True, GetItemId.GI_ARROW_ICE, None),
- 'Blue Fire Arrows': ('Item', True, GetItemId.GI_ARROW_ICE, None),
- 'Light Arrows': ('Item', True, GetItemId.GI_ARROW_LIGHT, None),
+ 'Biggoron Sword': ('Item', None, GetItemId.GI_SWORD_BIGGORON, {}),
+ 'Fire Arrows': ('Item', True, GetItemId.GI_ARROW_FIRE, {}),
+ 'Ice Arrows': ('Item', True, GetItemId.GI_ARROW_ICE, {}),
+ 'Blue Fire Arrows': ('Item', True, GetItemId.GI_ARROW_ICE, {}),
+ 'Light Arrows': ('Item', True, GetItemId.GI_ARROW_LIGHT, {}),
'Gold Skulltula Token': ('Token', True, GetItemId.GI_SKULL_TOKEN, {'progressive': float('Inf')}),
- 'Dins Fire': ('Item', True, GetItemId.GI_DINS_FIRE, None),
- 'Farores Wind': ('Item', True, GetItemId.GI_FARORES_WIND, None),
- 'Nayrus Love': ('Item', True, GetItemId.GI_NAYRUS_LOVE, None),
+ 'Dins Fire': ('Item', True, GetItemId.GI_DINS_FIRE, {}),
+ 'Farores Wind': ('Item', True, GetItemId.GI_FARORES_WIND, {}),
+ 'Nayrus Love': ('Item', True, GetItemId.GI_NAYRUS_LOVE, {}),
'Deku Nuts (10)': ('Item', None, GetItemId.GI_DEKU_NUTS_10, {'junk': 0}),
'Bomb (1)': ('Item', None, GetItemId.GI_BOMBS_1, {'junk': -1}),
'Bombs (10)': ('Item', None, GetItemId.GI_BOMBS_10, {'junk': 2}),
'Bombs (20)': ('Item', None, GetItemId.GI_BOMBS_20, {'junk': 0}),
'Deku Seeds (30)': ('Item', None, GetItemId.GI_DEKU_SEEDS_30, {'junk': 5}),
- 'Bombchus (5)': ('Item', True, GetItemId.GI_BOMBCHUS_5, None),
- 'Bombchus (20)': ('Item', True, GetItemId.GI_BOMBCHUS_20, None),
+ 'Bombchus (5)': ('Item', True, GetItemId.GI_BOMBCHUS_5, {}),
+ 'Bombchus (20)': ('Item', True, GetItemId.GI_BOMBCHUS_20, {}),
'Small Key (Treasure Chest Game)': ('TCGSmallKey', True, GetItemId.GI_DOOR_KEY, {'progressive': float('Inf')}),
- 'Rupee (Treasure Chest Game) (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN_LOSE, None),
- 'Rupees (Treasure Chest Game) (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE_LOSE, None),
- 'Rupees (Treasure Chest Game) (20)': ('Item', None, GetItemId.GI_RUPEE_RED_LOSE, None),
- 'Rupees (Treasure Chest Game) (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE_LOSE, None),
+ 'Rupee (Treasure Chest Game) (1)': ('Item', None, GetItemId.GI_RUPEE_GREEN_LOSE, {}),
+ 'Rupees (Treasure Chest Game) (5)': ('Item', None, GetItemId.GI_RUPEE_BLUE_LOSE, {}),
+ 'Rupees (Treasure Chest Game) (20)': ('Item', None, GetItemId.GI_RUPEE_RED_LOSE, {}),
+ 'Rupees (Treasure Chest Game) (50)': ('Item', None, GetItemId.GI_RUPEE_PURPLE_LOSE, {}),
'Piece of Heart (Treasure Chest Game)': ('Item', True, GetItemId.GI_HEART_PIECE_WIN, {'alias': ('Piece of Heart', 1), 'progressive': float('Inf')}),
'Ice Trap': ('Item', None, GetItemId.GI_ICE_TRAP, {'junk': 0}),
'Progressive Hookshot': ('Item', True, GetItemId.GI_PROGRESSIVE_HOOKSHOT, {'progressive': 2}),
'Progressive Strength Upgrade': ('Item', True, GetItemId.GI_PROGRESSIVE_STRENGTH, {'progressive': 3}),
- 'Bomb Bag': ('Item', True, GetItemId.GI_PROGRESSIVE_BOMB_BAG, None),
- 'Bow': ('Item', True, GetItemId.GI_PROGRESSIVE_BOW, None),
- 'Slingshot': ('Item', True, GetItemId.GI_PROGRESSIVE_SLINGSHOT, None),
+ 'Bomb Bag': ('Item', True, GetItemId.GI_PROGRESSIVE_BOMB_BAG, {}),
+ 'Bow': ('Item', True, GetItemId.GI_PROGRESSIVE_BOW, {}),
+ 'Slingshot': ('Item', True, GetItemId.GI_PROGRESSIVE_SLINGSHOT, {}),
'Progressive Wallet': ('Item', True, GetItemId.GI_PROGRESSIVE_WALLET, {'progressive': 3}),
'Progressive Scale': ('Item', True, GetItemId.GI_PROGRESSIVE_SCALE, {'progressive': 2}),
- 'Deku Nut Capacity': ('Item', None, GetItemId.GI_PROGRESSIVE_NUT_CAPACITY, None),
- 'Deku Stick Capacity': ('Item', None, GetItemId.GI_PROGRESSIVE_STICK_CAPACITY, None),
- 'Bombchus': ('Item', True, GetItemId.GI_PROGRESSIVE_BOMBCHUS, None),
- 'Magic Meter': ('Item', True, GetItemId.GI_PROGRESSIVE_MAGIC_METER, None),
- 'Ocarina': ('Item', True, GetItemId.GI_PROGRESSIVE_OCARINA, None),
+ 'Deku Nut Capacity': ('Item', None, GetItemId.GI_PROGRESSIVE_NUT_CAPACITY, {}),
+ 'Deku Stick Capacity': ('Item', None, GetItemId.GI_PROGRESSIVE_STICK_CAPACITY, {}),
+ 'Bombchus': ('Item', True, GetItemId.GI_PROGRESSIVE_BOMBCHUS, {}),
+ 'Magic Meter': ('Item', True, GetItemId.GI_PROGRESSIVE_MAGIC_METER, {}),
+ 'Ocarina': ('Item', True, GetItemId.GI_PROGRESSIVE_OCARINA, {}),
'Bottle with Red Potion': ('Item', True, GetItemId.GI_BOTTLE_WITH_RED_POTION, {'bottle': True, 'shop_object': 0x0F}),
'Bottle with Green Potion': ('Item', True, GetItemId.GI_BOTTLE_WITH_GREEN_POTION, {'bottle': True, 'shop_object': 0x0F}),
'Bottle with Blue Potion': ('Item', True, GetItemId.GI_BOTTLE_WITH_BLUE_POTION, {'bottle': True, 'shop_object': 0x0F}),
@@ -437,32 +437,32 @@ class GetItemId(IntEnum):
'Bottle with Bugs': ('Item', True, GetItemId.GI_BOTTLE_WITH_BUGS, {'bottle': True, 'shop_object': 0x0F}),
'Bottle with Big Poe': ('Item', True, GetItemId.GI_BOTTLE_WITH_BIG_POE, {'shop_object': 0x0F}),
'Bottle with Poe': ('Item', True, GetItemId.GI_BOTTLE_WITH_POE, {'bottle': True, 'shop_object': 0x0F}),
- 'Boss Key (Forest Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_FOREST_TEMPLE, None),
- 'Boss Key (Fire Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_FIRE_TEMPLE, None),
- 'Boss Key (Water Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_WATER_TEMPLE, None),
- 'Boss Key (Spirit Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_SPIRIT_TEMPLE, None),
- 'Boss Key (Shadow Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_SHADOW_TEMPLE, None),
- 'Boss Key (Ganons Castle)': ('GanonBossKey', True, GetItemId.GI_BOSS_KEY_GANONS_CASTLE, None),
- 'Compass (Deku Tree)': ('Compass', False, GetItemId.GI_COMPASS_DEKU_TREE, None),
- 'Compass (Dodongos Cavern)': ('Compass', False, GetItemId.GI_COMPASS_DODONGOS_CAVERN, None),
- 'Compass (Jabu Jabus Belly)': ('Compass', False, GetItemId.GI_COMPASS_JABU_JABU, None),
- 'Compass (Forest Temple)': ('Compass', False, GetItemId.GI_COMPASS_FOREST_TEMPLE, None),
- 'Compass (Fire Temple)': ('Compass', False, GetItemId.GI_COMPASS_FIRE_TEMPLE, None),
- 'Compass (Water Temple)': ('Compass', False, GetItemId.GI_COMPASS_WATER_TEMPLE, None),
- 'Compass (Spirit Temple)': ('Compass', False, GetItemId.GI_COMPASS_SPIRIT_TEMPLE, None),
- 'Compass (Shadow Temple)': ('Compass', False, GetItemId.GI_COMPASS_SHADOW_TEMPLE, None),
- 'Compass (Bottom of the Well)': ('Compass', False, GetItemId.GI_COMPASS_BOTTOM_OF_THE_WELL, None),
- 'Compass (Ice Cavern)': ('Compass', False, GetItemId.GI_COMPASS_ICE_CAVERN, None),
- 'Map (Deku Tree)': ('Map', False, GetItemId.GI_MAP_DEKU_TREE, None),
- 'Map (Dodongos Cavern)': ('Map', False, GetItemId.GI_MAP_DODONGOS_CAVERN, None),
- 'Map (Jabu Jabus Belly)': ('Map', False, GetItemId.GI_MAP_JABU_JABU, None),
- 'Map (Forest Temple)': ('Map', False, GetItemId.GI_MAP_FOREST_TEMPLE, None),
- 'Map (Fire Temple)': ('Map', False, GetItemId.GI_MAP_FIRE_TEMPLE, None),
- 'Map (Water Temple)': ('Map', False, GetItemId.GI_MAP_WATER_TEMPLE, None),
- 'Map (Spirit Temple)': ('Map', False, GetItemId.GI_MAP_SPIRIT_TEMPLE, None),
- 'Map (Shadow Temple)': ('Map', False, GetItemId.GI_MAP_SHADOW_TEMPLE, None),
- 'Map (Bottom of the Well)': ('Map', False, GetItemId.GI_MAP_BOTTOM_OF_THE_WELL, None),
- 'Map (Ice Cavern)': ('Map', False, GetItemId.GI_MAP_ICE_CAVERN, None),
+ 'Boss Key (Forest Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_FOREST_TEMPLE, {}),
+ 'Boss Key (Fire Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_FIRE_TEMPLE, {}),
+ 'Boss Key (Water Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_WATER_TEMPLE, {}),
+ 'Boss Key (Spirit Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_SPIRIT_TEMPLE, {}),
+ 'Boss Key (Shadow Temple)': ('BossKey', True, GetItemId.GI_BOSS_KEY_SHADOW_TEMPLE, {}),
+ 'Boss Key (Ganons Castle)': ('GanonBossKey', True, GetItemId.GI_BOSS_KEY_GANONS_CASTLE, {}),
+ 'Compass (Deku Tree)': ('Compass', False, GetItemId.GI_COMPASS_DEKU_TREE, {}),
+ 'Compass (Dodongos Cavern)': ('Compass', False, GetItemId.GI_COMPASS_DODONGOS_CAVERN, {}),
+ 'Compass (Jabu Jabus Belly)': ('Compass', False, GetItemId.GI_COMPASS_JABU_JABU, {}),
+ 'Compass (Forest Temple)': ('Compass', False, GetItemId.GI_COMPASS_FOREST_TEMPLE, {}),
+ 'Compass (Fire Temple)': ('Compass', False, GetItemId.GI_COMPASS_FIRE_TEMPLE, {}),
+ 'Compass (Water Temple)': ('Compass', False, GetItemId.GI_COMPASS_WATER_TEMPLE, {}),
+ 'Compass (Spirit Temple)': ('Compass', False, GetItemId.GI_COMPASS_SPIRIT_TEMPLE, {}),
+ 'Compass (Shadow Temple)': ('Compass', False, GetItemId.GI_COMPASS_SHADOW_TEMPLE, {}),
+ 'Compass (Bottom of the Well)': ('Compass', False, GetItemId.GI_COMPASS_BOTTOM_OF_THE_WELL, {}),
+ 'Compass (Ice Cavern)': ('Compass', False, GetItemId.GI_COMPASS_ICE_CAVERN, {}),
+ 'Map (Deku Tree)': ('Map', False, GetItemId.GI_MAP_DEKU_TREE, {}),
+ 'Map (Dodongos Cavern)': ('Map', False, GetItemId.GI_MAP_DODONGOS_CAVERN, {}),
+ 'Map (Jabu Jabus Belly)': ('Map', False, GetItemId.GI_MAP_JABU_JABU, {}),
+ 'Map (Forest Temple)': ('Map', False, GetItemId.GI_MAP_FOREST_TEMPLE, {}),
+ 'Map (Fire Temple)': ('Map', False, GetItemId.GI_MAP_FIRE_TEMPLE, {}),
+ 'Map (Water Temple)': ('Map', False, GetItemId.GI_MAP_WATER_TEMPLE, {}),
+ 'Map (Spirit Temple)': ('Map', False, GetItemId.GI_MAP_SPIRIT_TEMPLE, {}),
+ 'Map (Shadow Temple)': ('Map', False, GetItemId.GI_MAP_SHADOW_TEMPLE, {}),
+ 'Map (Bottom of the Well)': ('Map', False, GetItemId.GI_MAP_BOTTOM_OF_THE_WELL, {}),
+ 'Map (Ice Cavern)': ('Map', False, GetItemId.GI_MAP_ICE_CAVERN, {}),
'Small Key (Forest Temple)': ('SmallKey', True, GetItemId.GI_SMALL_KEY_FOREST_TEMPLE, {'progressive': float('Inf')}),
'Small Key (Fire Temple)': ('SmallKey', True, GetItemId.GI_SMALL_KEY_FIRE_TEMPLE, {'progressive': float('Inf')}),
'Small Key (Water Temple)': ('SmallKey', True, GetItemId.GI_SMALL_KEY_WATER_TEMPLE, {'progressive': float('Inf')}),
@@ -472,14 +472,14 @@ class GetItemId(IntEnum):
'Small Key (Gerudo Training Ground)': ('SmallKey', True, GetItemId.GI_SMALL_KEY_GERUDO_TRAINING, {'progressive': float('Inf')}),
'Small Key (Thieves Hideout)': ('HideoutSmallKey', True, GetItemId.GI_SMALL_KEY_THIEVES_HIDEOUT, {'progressive': float('Inf')}),
'Small Key (Ganons Castle)': ('SmallKey', True, GetItemId.GI_SMALL_KEY_GANONS_CASTLE, {'progressive': float('Inf')}),
- 'Double Defense': ('Item', None, GetItemId.GI_DOUBLE_DEFENSE, None),
+ 'Double Defense': ('Item', None, GetItemId.GI_DOUBLE_DEFENSE, {}),
'Buy Magic Bean': ('Item', True, GetItemId.GI_MAGIC_BEAN, {'alias': ('Magic Bean', 10), 'progressive': 10}),
'Magic Bean Pack': ('Item', True, GetItemId.GI_MAGIC_BEAN_PACK, {'alias': ('Magic Bean', 10), 'progressive': 10}),
'Triforce Piece': ('Item', True, GetItemId.GI_TRIFORCE_PIECE, {'progressive': float('Inf')}),
'Zeldas Letter': ('Item', True, GetItemId.GI_ZELDAS_LETTER, {'trade': True}),
- 'Time Travel': ('Event', True, None, None),
- 'Scarecrow Song': ('Event', True, None, None),
- 'Triforce': ('Event', True, None, None),
+ 'Time Travel': ('Event', True, None, {}),
+ 'Scarecrow Song': ('Event', True, None, {}),
+ 'Triforce': ('Event', True, None, {}),
'Small Key Ring (Forest Temple)': ('SmallKeyRing', True, GetItemId.GI_SMALL_KEY_RING_FOREST_TEMPLE, {'alias': ('Small Key (Forest Temple)', 10), 'progressive': float('Inf')}),
'Small Key Ring (Fire Temple)': ('SmallKeyRing', True, GetItemId.GI_SMALL_KEY_RING_FIRE_TEMPLE, {'alias': ('Small Key (Fire Temple)', 10), 'progressive': float('Inf')}),
@@ -543,37 +543,37 @@ class GetItemId(IntEnum):
'Ocarina C down Button': ('Item', True, GetItemId.GI_OCARINA_BUTTON_C_DOWN, {'ocarina_button': True}),
'Ocarina C left Button': ('Item', True, GetItemId.GI_OCARINA_BUTTON_C_LEFT, {'ocarina_button': True}),
'Ocarina C right Button': ('Item', True, GetItemId.GI_OCARINA_BUTTON_C_RIGHT, {'ocarina_button': True}),
- 'Fairy Drop': ('Item', None, GetItemId.GI_FAIRY, None),
- 'Nothing': ('Item', None, GetItemId.GI_NOTHING, None),
+ 'Fairy Drop': ('Item', None, GetItemId.GI_FAIRY, {}),
+ 'Nothing': ('Item', None, GetItemId.GI_NOTHING, {}),
# Event items otherwise generated by generic event logic
# can be defined here to enforce their appearance in playthroughs.
- 'Water Temple Clear': ('Event', True, None, None),
- 'Forest Trial Clear': ('Event', True, None, None),
- 'Fire Trial Clear': ('Event', True, None, None),
- 'Water Trial Clear': ('Event', True, None, None),
- 'Shadow Trial Clear': ('Event', True, None, None),
- 'Spirit Trial Clear': ('Event', True, None, None),
- 'Light Trial Clear': ('Event', True, None, None),
- 'Epona': ('Event', True, None, None),
-
- 'Deku Stick Drop': ('Drop', True, None, None),
- 'Deku Nut Drop': ('Drop', True, None, None),
- 'Blue Fire': ('Drop', True, None, None),
- 'Fairy': ('Drop', True, None, None),
- 'Fish': ('Drop', True, None, None),
- 'Bugs': ('Drop', True, None, None),
- 'Big Poe': ('Drop', True, None, None),
- 'Bombchu Drop': ('Drop', True, None, None),
- 'Deku Shield Drop': ('Drop', True, None, None),
+ 'Water Temple Clear': ('Event', True, None, {}),
+ 'Forest Trial Clear': ('Event', True, None, {}),
+ 'Fire Trial Clear': ('Event', True, None, {}),
+ 'Water Trial Clear': ('Event', True, None, {}),
+ 'Shadow Trial Clear': ('Event', True, None, {}),
+ 'Spirit Trial Clear': ('Event', True, None, {}),
+ 'Light Trial Clear': ('Event', True, None, {}),
+ 'Epona': ('Event', True, None, {}),
+
+ 'Deku Stick Drop': ('Drop', True, None, {}),
+ 'Deku Nut Drop': ('Drop', True, None, {}),
+ 'Blue Fire': ('Drop', True, None, {}),
+ 'Fairy': ('Drop', True, None, {}),
+ 'Fish': ('Drop', True, None, {}),
+ 'Bugs': ('Drop', True, None, {}),
+ 'Big Poe': ('Drop', True, None, {}),
+ 'Bombchu Drop': ('Drop', True, None, {}),
+ 'Deku Shield Drop': ('Drop', True, None, {}),
# Consumable refills defined mostly to placate 'starting with' options
- 'Arrows': ('Refill', None, None, None),
- 'Bombs': ('Refill', None, None, None),
- 'Deku Seeds': ('Refill', None, None, None),
- 'Deku Sticks': ('Refill', None, None, None),
- 'Deku Nuts': ('Refill', None, None, None),
- 'Rupees': ('Refill', None, None, None),
+ 'Arrows': ('Refill', None, None, {}),
+ 'Bombs': ('Refill', None, None, {}),
+ 'Deku Seeds': ('Refill', None, None, {}),
+ 'Deku Sticks': ('Refill', None, None, {}),
+ 'Deku Nuts': ('Refill', None, None, {}),
+ 'Rupees': ('Refill', None, None, {}),
'Minuet of Forest': ('Song', True, GetItemId.GI_MINUET_OF_FOREST,
{
diff --git a/ItemPool.py b/ItemPool.py
index 3e67e91a8..80dcae2ec 100644
--- a/ItemPool.py
+++ b/ItemPool.py
@@ -440,7 +440,7 @@ def generate_itempool(world: World) -> None:
# set up item pool
(pool, placed_items) = get_pool_core(world)
- placed_items_count = {}
+ placed_items_count: dict[str, int] = {}
world.itempool = ItemFactory(pool, world)
world.initialize_items(world.itempool + list(placed_items.values()))
placed_locations = list(filter(lambda loc: loc.name in placed_items, world.get_locations()))
@@ -519,16 +519,16 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
if world.settings.shuffle_gerudo_card:
pending_junk_pool.append('Gerudo Membership Card')
if world.settings.shuffle_smallkeys in ('any_dungeon', 'overworld', 'keysanity', 'regional'):
- for dungeon in ('Forest Temple', 'Fire Temple', 'Water Temple', 'Shadow Temple', 'Spirit Temple',
+ for dungeon_name in ('Forest Temple', 'Fire Temple', 'Water Temple', 'Shadow Temple', 'Spirit Temple',
'Bottom of the Well', 'Gerudo Training Ground', 'Ganons Castle'):
- if world.keyring(dungeon):
- pending_junk_pool.append(f"Small Key Ring ({dungeon})")
+ if world.keyring(dungeon_name):
+ pending_junk_pool.append(f"Small Key Ring ({dungeon_name})")
else:
- pending_junk_pool.append(f"Small Key ({dungeon})")
+ pending_junk_pool.append(f"Small Key ({dungeon_name})")
if world.settings.shuffle_bosskeys in ('any_dungeon', 'overworld', 'keysanity', 'regional'):
- for dungeon in ('Forest Temple', 'Fire Temple', 'Water Temple', 'Shadow Temple', 'Spirit Temple'):
- if not world.keyring_give_bk(dungeon):
- pending_junk_pool.append(f"Boss Key ({dungeon})")
+ for dungeon_name in ('Forest Temple', 'Fire Temple', 'Water Temple', 'Shadow Temple', 'Spirit Temple'):
+ if not world.keyring_give_bk(dungeon_name):
+ pending_junk_pool.append(f"Boss Key ({dungeon_name})")
if world.settings.shuffle_ganon_bosskey in ('any_dungeon', 'overworld', 'keysanity', 'regional'):
pending_junk_pool.append('Boss Key (Ganons Castle)')
if world.settings.shuffle_silver_rupees in ('any_dungeon', 'overworld', 'anywhere', 'regional'):
@@ -557,7 +557,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
pending_junk_pool.append('Ocarina C right Button')
# Use the vanilla items in the world's locations when appropriate.
- vanilla_items_processed = Counter()
+ vanilla_items_processed: Counter[str] = Counter()
for location in world.get_locations():
if location.vanilla_item is None:
continue
@@ -573,9 +573,11 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
# Gold Skulltula Tokens
elif location.vanilla_item == 'Gold Skulltula Token':
- shuffle_item = (world.settings.tokensanity == 'all'
- or (world.settings.tokensanity == 'dungeons' and location.dungeon)
- or (world.settings.tokensanity == 'overworld' and not location.dungeon))
+ shuffle_item = (
+ world.settings.tokensanity == 'all'
+ or (world.settings.tokensanity == 'dungeons' and location.dungeon is not None)
+ or (world.settings.tokensanity == 'overworld' and location.dungeon is None)
+ )
# Shops
elif location.type == "Shop":
@@ -591,9 +593,11 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
elif world.settings.shuffle_scrubs == 'off':
shuffle_item = False
else:
- item = deku_scrubs_items[location.vanilla_item]
- if isinstance(item, list):
- item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
+ scrub_item = deku_scrubs_items[location.vanilla_item]
+ if isinstance(scrub_item, list):
+ item = random.choices([i[0] for i in scrub_item], weights=[i[1] for i in scrub_item], k=1)[0]
+ else:
+ item = scrub_item
shuffle_item = True
# Kokiri Sword
@@ -739,7 +743,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
location.disabled = DisableType.DISABLED
# Freestanding Rupees and Hearts
- elif location.type in ('ActorOverride', 'Freestanding', 'RupeeTower'):
+ elif location.type in ('Freestanding', 'RupeeTower'):
if world.settings.shuffle_freestanding_items == 'all':
shuffle_item = True
elif world.settings.shuffle_freestanding_items == 'dungeons' and location.dungeon is not None:
@@ -979,7 +983,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
remove_junk_pool, _ = zip(*junk_pool_base)
# Omits Rupees (200) and Deku Nuts (10)
- remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap']
+ remove_junk_pool = *remove_junk_pool, 'Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap'
junk_candidates = [item for item in pool if item in remove_junk_pool]
while pending_junk_pool:
diff --git a/JSONDump.py b/JSONDump.py
index 0c04c5f04..49b3bf56a 100644
--- a/JSONDump.py
+++ b/JSONDump.py
@@ -75,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[Sequence[int, int]] = None, ensure_ascii: bool = False) -> str:
+def dump_dict(obj: dict, current_indent: str = '', sub_width: Optional[tuple[int, int]] = None, ensure_ascii: bool = False) -> str:
entries = []
key_width = None
@@ -122,7 +122,7 @@ def dump_dict(obj: dict, current_indent: str = '', sub_width: Optional[Sequence[
return output
-def dump_obj(obj, current_indent: str = '', sub_width: Optional[Sequence[int, int]] = None, ensure_ascii: bool = False) -> str:
+def dump_obj(obj, current_indent: str = '', sub_width: Optional[tuple[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 fdca55f29..b28e00390 100644
--- a/Location.py
+++ b/Location.py
@@ -174,8 +174,12 @@ def LocationFactory(locations: str | list[str]) -> Location | list[Location]:
if location in location_table:
match_location = location
else:
- match_location = next(filter(lambda k: k.lower() == location.lower(), location_table), None)
- if match_location:
+ match_location = None
+ for k in location_table:
+ if k.lower() == location.lower():
+ match_location = k
+ break
+ if match_location is not None:
type, scene, default, addresses, vanilla_item, filter_tags = location_table[match_location]
if addresses is None:
addresses = (None, None)
diff --git a/LocationList.py b/LocationList.py
index e3376beec..777a08aff 100644
--- a/LocationList.py
+++ b/LocationList.py
@@ -59,8 +59,6 @@ def shop_address(shop_id: int, shelf_id: int) -> int:
# For cutscene/song/boss locations, the Scene is set to 0xFF. This matches the behavior of the push_delayed_item C function.
-# 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([
## Dungeon Rewards
@@ -2634,11 +2632,10 @@ def shop_address(shop_id: int, shelf_id: int) -> int:
'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'],
'Boss': [name for (name, data) in location_table.items() if data[0] == 'Boss'],
- 'ActorOverride': [name for (name, data) in location_table.items() if data[0] == 'ActorOverride'],
'BossHeart': [name for (name, data) in location_table.items() if data[0] == 'BossHeart'],
'CollectableLike': [name for (name, data) in location_table.items() if data[0] in ('Collectable', 'BossHeart', 'GS Token', 'SilverRupee')],
'CanSee': [name for (name, data) in location_table.items()
- if data[0] in ('Collectable', 'BossHeart', 'GS Token', 'Shop', 'MaskShop', 'Freestanding', 'ActorOverride', 'RupeeTower', 'Pot', 'Crate', 'FlyingPot', 'SmallCrate', 'Beehive', 'SilverRupee')
+ if data[0] in ('Collectable', 'BossHeart', 'GS Token', 'Shop', 'MaskShop', 'Freestanding', 'RupeeTower', 'Pot', 'Crate', 'FlyingPot', 'SmallCrate', 'Beehive', 'SilverRupee')
# Treasure Box Shop, Bombchu Bowling, Hyrule Field (OoT), Lake Hylia (RL/FA)
or data[0:2] in [('Chest', 0x10), ('NPC', 0x4B), ('NPC', 0x51), ('NPC', 0x57)]],
'Dungeon': [name for (name, data) in location_table.items() if data[5] is not None and any(dungeon in data[5] for dungeon in dungeons)],
diff --git a/MQ.py b/MQ.py
index 7bc8d8d0e..2f8b0f83e 100644
--- a/MQ.py
+++ b/MQ.py
@@ -46,7 +46,7 @@
from __future__ import annotations
import json
from struct import pack, unpack
-from typing import Optional, Any
+from typing import Optional, Any, TypedDict
from Rom import Rom
from Utils import data_path
@@ -54,6 +54,13 @@
SCENE_TABLE: int = 0xB71440
+class JsonFile(TypedDict):
+ Name: str
+ Start: Optional[str]
+ End: Optional[str]
+ RemapStart: Optional[str]
+
+
class File:
def __init__(self, name: str, start: int = 0, end: Optional[int] = None, remap: Optional[int] = None) -> None:
self.name: str = name
@@ -63,15 +70,18 @@ def __init__(self, name: str, start: int = 0, end: Optional[int] = None, remap:
self.from_file: int = self.start
# used to update the file's associated dmadata record
- self.dma_key: int = self.start
+ self.dma_key: Optional[int] = self.start
@classmethod
- def from_json(cls, file: dict[str, Optional[str]]) -> File:
+ def from_json(cls, file: JsonFile) -> File:
+ start = file.get('Start', None)
+ end = file.get('End', None)
+ remap_start = file.get('RemapStart', None)
return cls(
file['Name'],
- int(file['Start'], 16) if file.get('Start', None) is not None else 0,
- int(file['End'], 16) if file.get('End', None) is not None else None,
- int(file['RemapStart'], 16) if file.get('RemapStart', None) is not None else None
+ 0 if start is None else int(start, 16),
+ None if end is None else int(end, 16),
+ None if remap_start is None else int(remap_start, 16),
)
def __repr__(self) -> str:
@@ -113,16 +123,29 @@ def write_to_scene(self, rom: Rom, start: int) -> None:
rom.write_int32s(addr, [self.poly_addr, self.polytypes_addr, self.camera_data_addr])
+class JsonColDelta(TypedDict):
+ IsLarger: bool
+ Polys: list[dict[str, int]]
+ PolyTypes: list[dict[str, int]]
+ Cams: list[dict[str, int]]
+
+
class ColDelta:
- def __init__(self, delta: dict[str, bool | list[dict[str, int]]]) -> None:
+ def __init__(self, delta: JsonColDelta) -> 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']
+class JsonIcon(TypedDict):
+ Icon: int
+ Count: int
+ IconPoints: list[dict[str, int]]
+
+
class Icon:
- def __init__(self, data: dict[str, int | list[dict[str, int]]]) -> None:
+ def __init__(self, data: JsonIcon) -> None:
self.icon: int = data["Icon"]
self.count: int = data["Count"]
self.points: list[IconPoint] = [IconPoint(x) for x in data["IconPoints"]]
@@ -367,8 +390,15 @@ def append_path_data(self, rom: Rom) -> int:
return records_offset
+class JsonRoom(TypedDict):
+ File: JsonFile
+ Id: int
+ Objects: list[str]
+ Actors: list[str]
+
+
class Room:
- def __init__(self, room: dict[str, int | list[str] | dict[str, Optional[str]]]):
+ def __init__(self, room: JsonRoom):
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']]
@@ -600,25 +630,25 @@ def insert_space(rom: Rom, file: File, vram_start: int, insert_section: int, ins
(offset + insert_size))
# value contains the vram address
- value = rom.read_int32(address)
+ base_value = rom.read_int32(address)
reg = None
if type == 2:
# Data entry: value is the raw vram address
- pass
+ value = base_value
elif type == 4:
# Jump OP: Get the address from a Jump instruction
- value = 0x80000000 | (value & 0x03FFFFFF) << 2
+ value = 0x80000000 | (base_value & 0x03FFFFFF) << 2
elif type == 5:
# Load High: Upper half of an address load
- reg = (value >> 16) & 0x1F
- val_hi[reg] = (value & 0x0000FFFF) << 16
+ reg = (base_value >> 16) & 0x1F
+ val_hi[reg] = (base_value & 0x0000FFFF) << 16
adr_hi[reg] = address
# Do not process, wait until the lower half is read
value = None
elif type == 6:
# Load Low: Lower half of the address load
- reg = (value >> 21) & 0x1F
- val_low = value & 0x0000FFFF
+ reg = (base_value >> 21) & 0x1F
+ val_low = base_value & 0x0000FFFF
val_low = unpack('h', pack('H', val_low))[0]
# combine with previous load high
value = val_hi[reg] + val_low
@@ -641,6 +671,8 @@ def insert_space(rom: Rom, file: File, vram_start: int, insert_section: int, ins
new_value = op | new_value
rom.write_int32(address, new_value)
elif type == 6:
+ assert reg is not None
+
# Load Low: Lower half of the address load
op = rom.read_int32(address) & 0xFFFF0000
new_val_low = new_value & 0x0000FFFF
diff --git a/Messages.py b/Messages.py
index b0edf59dc..62e976355 100644
--- a/Messages.py
+++ b/Messages.py
@@ -128,7 +128,7 @@
GS_TOKEN_MESSAGES: list[int] = [0x00B4, 0x00B5] # Get Gold Skulltula Token messages
ERROR_MESSAGE: int = 0x0001
-new_messages = [] # Used to keep track of new/updated messages to prevent duplicates. Clear it at the start of patches
+new_messages: list[int] = [] # Used to keep track of new/updated messages to prevent duplicates. Clear it at the start of patches
# messages for shorter item messages
# ids are in the space freed up by move_shop_item_messages()
@@ -564,7 +564,7 @@
# convert byte array to an integer
-def bytes_to_int(data: bytes, signed: bool = False) -> int:
+def bytes_to_int(data: bytes | list[int], signed: bool = False) -> int:
return int.from_bytes(data, byteorder='big', signed=signed)
@@ -702,7 +702,7 @@ def write(self, rom: Rom, text_start: int, offset: int) -> int:
class Message:
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 = encode_text_string(raw_text)
+ raw_text = bytearray(encode_text_string(raw_text))
elif not isinstance(raw_text, bytearray):
raw_text = bytearray(raw_text)
@@ -929,7 +929,7 @@ def update_message_by_id(messages: list[Message], id: int, text: bytearray | str
if index >= 0:
update_message_by_index(messages, index, text, opts)
else:
- add_message(messages, text, id, opts)
+ add_message(messages, text, id, opts or 0x00)
# Gets the message by its ID. Returns None if the index does not exist
@@ -1007,7 +1007,7 @@ def display(self) -> str:
def write(self, rom: Rom, shop_table_address: int, index: int) -> None:
entry_offset = shop_table_address + 0x20 * index
- data = []
+ data: list[int] = []
data += int_to_bytes(self.object, 2)
data += int_to_bytes(self.model, 2)
data += int_to_bytes(self.func1, 4)
@@ -1143,14 +1143,14 @@ def make_player_message(text: str) -> str:
# make sure to call this AFTER move_shop_item_messages()
def update_item_messages(messages: list[Message], world: World) -> None:
new_item_messages = ITEM_MESSAGES + KEYSANITY_MESSAGES
- for id, text in new_item_messages:
+ for id, new_item_text in new_item_messages:
if world.settings.world_count > 1:
- update_message_by_id(messages, id, make_player_message(text), 0x23)
+ update_message_by_id(messages, id, make_player_message(new_item_text), 0x23)
else:
- update_message_by_id(messages, id, text, 0x23)
+ update_message_by_id(messages, id, new_item_text, 0x23)
- for id, (text, opt) in MISC_MESSAGES:
- update_message_by_id(messages, id, text, opt)
+ for id, (misc_text, opt) in MISC_MESSAGES:
+ update_message_by_id(messages, id, misc_text, opt)
# 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:
@@ -1205,7 +1205,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[Iterable[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)
@@ -1293,21 +1293,9 @@ def repack_messages(rom: Rom, messages: list[Message], permutation: Optional[lis
# shuffles the messages in the game, making sure to keep various message types in their own group
+SHOP_ITEM_MESSAGES: list[int] = []
+SCRUBS_MESSAGE_IDS: 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"):
- shuffle_messages.scrubs_message_ids = []
-
- hint_ids = (
- GOSSIP_STONE_MESSAGES + TEMPLE_HINTS_MESSAGES +
- [data['id'] for data in misc_item_hint_table.values()] +
- [data['id'] for data in misc_location_hint_table.values()] +
- [message_id for (message_id, message) in KEYSANITY_MESSAGES] + shuffle_messages.shop_item_messages +
- shuffle_messages.scrubs_message_ids +
- [0x5036, 0x70F5] # Chicken count and poe count respectively
- )
-
permutation = [i for i, _ in enumerate(messages)]
def is_exempt(m: Message) -> bool:
@@ -1315,9 +1303,8 @@ def is_exempt(m: Message) -> bool:
GOSSIP_STONE_MESSAGES + TEMPLE_HINTS_MESSAGES +
[data['id'] for data in misc_item_hint_table.values()] +
[data['id'] for data in misc_location_hint_table.values()] +
- [message_id for (message_id, message) in KEYSANITY_MESSAGES] +
- shuffle_messages.shop_item_messages +
- shuffle_messages.scrubs_message_ids +
+ [message_id for message_id, _ in KEYSANITY_MESSAGES] +
+ SHOP_ITEM_MESSAGES + SCRUBS_MESSAGE_IDS +
[0x5036, 0x70F5] # Chicken count and poe count respectively
)
shuffle_exempt = [
@@ -1325,9 +1312,9 @@ def is_exempt(m: Message) -> bool:
0x208D, # "One more lap!" for Cow in House race.
0xFFFC, # Character data from JP table used on title and file select screens
]
- is_hint = (except_hints and m.id in hint_ids)
- is_error_message = (m.id == ERROR_MESSAGE)
- is_shuffle_exempt = (m.id in shuffle_exempt)
+ is_hint = except_hints and m.id in hint_ids
+ is_error_message = m.id == ERROR_MESSAGE
+ is_shuffle_exempt = m.id in shuffle_exempt
return is_hint or is_error_message or m.is_id_message() or is_shuffle_exempt
have_goto = list(filter(lambda m: not is_exempt(m) and m.has_goto, messages))
@@ -1378,7 +1365,8 @@ def update_warp_song_text(messages: list[Message], world: World) -> None:
for id, entr in msg_list.items():
if 'warp_songs_and_owls' in world.settings.misc_hints or not world.settings.warp_songs:
destination = world.get_entrance(entr).connected_region
- destination_name = HintArea.at(destination)
+ assert destination is not None
+ destination_name: Any = HintArea.at(destination)
color = COLOR_MAP[destination_name.color]
if destination_name.preposition(True) is not None:
destination_name = f'to {destination_name}'
@@ -1393,6 +1381,7 @@ def update_warp_song_text(messages: list[Message], world: World) -> None:
for id, entr in owl_messages.items():
if 'warp_songs_and_owls' in world.settings.misc_hints:
destination = world.get_entrance(entr).connected_region
+ assert destination is not None
destination_name = HintArea.at(destination)
color = COLOR_MAP[destination_name.color]
if destination_name.preposition(True) is not None:
diff --git a/Models.py b/Models.py
index 4ac6f6486..3e02de39d 100644
--- a/Models.py
+++ b/Models.py
@@ -93,33 +93,35 @@ 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: bytearray | str, start: int = 0) -> int:
- databytes = data
+def scan(bytes_to_scan: bytearray, data: bytearray | str, start: int = 0) -> int:
+ databytes: bytearray | bytes
# If a string was passed, encode string as bytes
if isinstance(data, str):
databytes = data.encode()
+ else:
+ databytes = data
dataindex = 0
- for i in range(start, len(bytes)):
+ for i in range(start, len(bytes_to_scan)):
# Byte matches next byte in string
- if bytes[i] == databytes[dataindex]:
+ if bytes_to_scan[i] == databytes[dataindex]:
dataindex += 1
# Special case: Bottle, Bow, Slingshot, Fist.L, and Fist.R are subsets of
# Bottle.Hand.L, Bow.String, Slingshot.String, Gauntlet.Fist.L, and Gauntlet.Fist.R respectively
# And Hookshot which is a subset of Hookshot.Spike, Hookshot.Chain, Hookshot.Aiming.Reticule
# This leads to false positives. So if the next byte is . (0x2E) then reset the count.
- if isinstance(data, str) and data in ["Bottle", "Bow", "Slingshot", "Hookshot", "Fist.L", "Fist.R", "Blade.3"] and i < len(bytes) - 1 and bytes[i+1] == 0x2E:
+ if isinstance(data, str) and data in ["Bottle", "Bow", "Slingshot", "Hookshot", "Fist.L", "Fist.R", "Blade.3"] and i < len(bytes_to_scan) - 1 and bytes_to_scan[i+1] == 0x2E:
# Blade.3 is even wackier, as it is a subset of Blade.3.Break,
# and also a forward subset of Broken.Blade.3, and has a period in it
if data == "Blade.3":
resetCount = False
# If current byte is the "e" in "Blade.3", the period detected is the expected one- Carry on
# If it isn't, then reset the count
- if bytes[i] != 0x65:
+ if bytes_to_scan[i] != 0x65:
resetCount = True
# Make sure i is large enough, "Broken.Blad" is 11 chars (remember we're currently at the e)
if not resetCount and i > 10:
# Check if "Broken." immediately preceeds this string
- preceedingBytes = bytes[i-11:i-4]
+ preceedingBytes = bytes_to_scan[i-11:i-4]
if preceedingBytes == bytearray(b'Broken.'):
resetCount = True
if resetCount:
@@ -128,7 +130,7 @@ def scan(bytes: bytearray, data: bytearray | str, start: int = 0) -> int:
# "Gauntlet.Fis" is 12 chars (we are currently at the t)
elif data in ["Fist.L", "Fist.R"] and i > 11:
# Check if "Gauntlet." immediately preceeds this string
- preceedingBytes = bytes[i-12:i-3]
+ preceedingBytes = bytes_to_scan[i-12:i-3]
if preceedingBytes == bytearray(b'Gauntlet.'):
dataindex = 0
# Default case for Bottle, Bow, Slingshot, Hookshot, reset count
@@ -138,7 +140,7 @@ def scan(bytes: bytearray, data: bytearray | str, start: int = 0) -> int:
# (Blade.3 and fists can check in the previous stanza since a . will be encountered at some point)
if isinstance(data, str) and data == "Hookshot" and dataindex == 1 and i > 3:
# Check if "FPS." immediately preceeds this string
- preceedingBytes = bytes[i-4:i]
+ preceedingBytes = bytes_to_scan[i-4:i]
if preceedingBytes == bytearray(b'FPS.'):
dataindex = 0
# More special cases added by the new pipeline...
@@ -146,20 +148,20 @@ def scan(bytes: bytearray, data: bytearray | str, start: int = 0) -> int:
# And Hand.L specifically is a forward subset of Bottle.Hand.L
if isinstance(data, str) and data in ["Hand.L", "Hand.R"] and dataindex == 1:
if i > 8:
- preceedingBytes = bytes[i-9:i]
+ preceedingBytes = bytes_to_scan[i-9:i]
if preceedingBytes == bytearray(b'Gauntlet.'):
dataindex = 0
if dataindex == 1 and i > 3:
- preceedingBytes = bytes[i-4:i]
+ preceedingBytes = bytes_to_scan[i-4:i]
if preceedingBytes == bytearray(b'FPS.'):
dataindex = 0
if data == "Hand.L" and dataindex == 1 and i > 6:
- preceedingBytes = bytes[i-7:i]
+ preceedingBytes = bytes_to_scan[i-7:i]
if preceedingBytes == bytearray(b'Bottle.'):
dataindex = 0
# Forearm.L and Forearm.R are forward subsets of FPS.Forearm.X
if isinstance(data, str) and data in ["Forearm.L", "Forearm.R"] and dataindex == 1 and i > 3:
- preceedingBytes = bytes[i-4:i]
+ preceedingBytes = bytes_to_scan[i-4:i]
if preceedingBytes == bytearray(b'FPS.'):
dataindex = 0
# All bytes have been found, so a match
@@ -172,7 +174,7 @@ def scan(bytes: bytearray, data: bytearray | str, start: int = 0) -> int:
i += 2
offsetbytes = []
for j in range(4):
- offsetbytes.append(bytes[i + j])
+ offsetbytes.append(bytes_to_scan[i + j])
return int.from_bytes(offsetbytes, 'big')
# Match has been broken, reset to start of string
else:
@@ -209,9 +211,9 @@ def LoadVanilla(rom: Rom, missing: list[str], rebase: int, linkstart: int, links
for i in range(linksize):
vanillaData.append(rom.buffer[linkstart + i])
segment = 0x06
- vertices = {}
- matrices = {}
- textures = {}
+ vertices: dict[int, list[int]] = {}
+ matrices: dict[int, list[int]] = {}
+ textures: dict[int, list[int]] = {}
displayLists = {}
# For each missing piece, grab data from its vanilla display list
for item in missing:
@@ -283,7 +285,7 @@ def LoadVanilla(rom: Rom, missing: list[str], rebase: int, linkstart: int, links
# Grab the address from the low byte without the base offset
texOffset = lo & 0x00FFFFFF
numTexels = -1
- returnStack = []
+ returnStack: list[int] = []
j = i+8
# The point of this loop is just to find the number of texels
# so that it may be multiplied by the bytesPerTexel so we know
@@ -343,7 +345,7 @@ def LoadVanilla(rom: Rom, missing: list[str], rebase: int, linkstart: int, links
i += 8
displayLists[item] = (displayList, offset)
# Create vanilla zobj of the pieces from data collected during crawl
- vanillaZobj = []
+ vanillaZobj: list[int] = []
# Add textures, vertices, and matrices to the beginning of the zobj
# Textures
oldTex2New = {}
@@ -473,13 +475,13 @@ def CorrectSkeleton(zobj: bytearray, skeleton: list[list[int]], agestr: str) ->
hasVanillaSkeleton = True
for i in range(21):
offset = limb + i * 0x10
- bytes = []
- bytes.extend(int.to_bytes(skeleton[i][0], 2, 'big'))
- bytes.extend(int.to_bytes(skeleton[i][1], 2, 'big'))
- bytes.extend(int.to_bytes(skeleton[i][2], 2, 'big'))
+ skeleton_bytes: list[int] = []
+ skeleton_bytes.extend(int.to_bytes(skeleton[i][0], 2, 'big'))
+ skeleton_bytes.extend(int.to_bytes(skeleton[i][1], 2, 'big'))
+ skeleton_bytes.extend(int.to_bytes(skeleton[i][2], 2, 'big'))
# Overwrite the X, Y, Z bytes with their vanilla values
for j in range(6):
- zobj[offset+j] = bytes[j]
+ zobj[offset+j] = skeleton_bytes[j]
return hasVanillaSkeleton
@@ -530,7 +532,7 @@ def LoadModel(rom: Rom, model: str, age: int) -> int:
# Find which pieces, if any, are missing from this model
missing = []
present = {}
- DLOffsets = {}
+ DLOffsets: dict[str, int] = {}
for piece in pieces:
offset = scan(zobj, piece, footerstart)
if offset == -1:
diff --git a/Music.py b/Music.py
index 4d20d52b4..c2ae4b986 100644
--- a/Music.py
+++ b/Music.py
@@ -245,7 +245,7 @@ def rebuild_sequences(rom: Rom, sequences: list[Sequence], log: CosmeticsLog, sy
replacement_dict = {seq.replaces: seq for seq in sequences}
# List of sequences (actual sequence data objects) containing the vanilla sequence data
- old_sequences = []
+ old_sequences: list[SequenceData] = []
bgmlist = [sequence_id for title, sequence_id in bgm_sequence_ids]
fanfarelist = [sequence_id for title, sequence_id in fanfare_sequence_ids]
ocarinalist = [sequence_id for title, sequence_id in ocarina_sequence_ids]
diff --git a/N64Patch.py b/N64Patch.py
index 9c981370f..6d4953cf4 100644
--- a/N64Patch.py
+++ b/N64Patch.py
@@ -145,7 +145,7 @@ def create_patch_file(rom: Rom, file: str, xor_range: tuple[int, int] = (0x00B8A
# Write the address changes. We'll store the data with XOR so that
# the patch data won't be raw data from the patched rom.
- data = []
+ data: list[int] = []
block_start = block_end = None
BLOCK_HEADER_SIZE = 7 # this is used to break up gaps
for address in changed_addresses:
@@ -171,12 +171,11 @@ def create_patch_file(rom: Rom, file: str, xor_range: tuple[int, int] = (0x00B8A
xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
# compress the patch file
- patch_data = bytes(patch_data.buffer)
- patch_data = zlib.compress(patch_data)
+ compressed_patch_data = zlib.compress(patch_data.buffer)
# save the patch file
with open(file, 'wb') as outfile:
- outfile.write(patch_data)
+ outfile.write(compressed_patch_data)
# This will apply a patch file to a source rom to generate a patched rom.
@@ -188,13 +187,13 @@ def apply_patch_file(rom: Rom, settings: Settings, sub_file: Optional[str] = Non
with zipfile.ZipFile(file, 'r') as patch_archive:
try:
with patch_archive.open(sub_file, 'r') as stream:
- patch_data = stream.read()
+ compressed_patch_data = stream.read()
except KeyError as ex:
raise FileNotFoundError('Patch file missing from archive. Invalid Player ID.')
else:
with open(file, 'rb') as stream:
- patch_data = stream.read()
- patch_data = BigStream(bytearray(zlib.decompress(patch_data)))
+ compressed_patch_data = stream.read()
+ patch_data = BigStream(bytearray(zlib.decompress(compressed_patch_data)))
# make sure the header is correct
if patch_data.read_bytes(length=4) != b'ZPFv':
diff --git a/OcarinaSongs.py b/OcarinaSongs.py
index 4b616b89b..d1df37420 100644
--- a/OcarinaSongs.py
+++ b/OcarinaSongs.py
@@ -3,7 +3,7 @@
import sys
from collections.abc import Callable, Sequence
from itertools import chain
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, TypeVar
from Fill import ShuffleError
@@ -18,7 +18,8 @@
ActivationTransform: TypeAlias = "Callable[[list[int]], list[int]]"
PlaybackTransform: TypeAlias = "Callable[[list[dict[str, int]]], list[dict[str, int]]]"
-Transform: TypeAlias = "ActivationTransform | PlaybackTransform"
+P = TypeVar('P', "list[int]", "list[dict[str, int]]")
+T = TypeVar('T', ActivationTransform, PlaybackTransform)
PLAYBACK_START: int = 0xB781DC
PLAYBACK_LENGTH: int = 0xA0
@@ -137,7 +138,7 @@ 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[int | dict[str, int]]) -> list[int | dict[str, int]]:
+def identity(x: P) -> P:
return x
@@ -149,7 +150,7 @@ def invert_piece(piece: list[int]) -> list[int]:
return [4 - note for note in piece]
-def reverse_piece(piece: list[int | dict[str, int]]) -> list[int | dict[str, int]]:
+def reverse_piece(piece: P) -> P:
return piece[::-1]
@@ -163,7 +164,7 @@ def transpose(piece: list[int]) -> list[int]:
return transpose
-def compose(f: Transform, g: Transform) -> Transform:
+def compose(f: T, g: T) -> T:
return lambda x: f(g(x))
@@ -188,6 +189,7 @@ def __init__(self, rand_song: bool = True, piece_size: int = 3, extra_position:
self.activation_data: list[int] = []
self.playback_data: list[int] = []
self.total_duration: int = 0
+ self.difficulty: int = -1
if activation:
self.length = len(activation)
@@ -341,8 +343,8 @@ def get_random_song() -> Song:
rand_song = random.choices([True, False], [1, 9])[0]
piece_size = random.choices([3, 4], [5, 2])[0]
extra_position = random.choices(['none', 'start', 'middle', 'end'], [12, 1, 1, 1])[0]
- activation_transform = identity
- playback_transform = identity
+ activation_transform: ActivationTransform = identity
+ playback_transform: PlaybackTransform = identity
weight_damage = 0
should_transpose = random.choices([True, False], [1, 4])[0]
starting_range = range(0, 5)
@@ -403,7 +405,7 @@ def generate_song_list(world: World, frog: bool, warp: bool) -> dict[str, Song]:
for name2, song2 in fixed_songs.items():
if name1 != name2 and subsong(song1, song2):
raise ValueError(f'{name2} is unplayable because it contains {name1}')
- random_songs = []
+ random_songs: list[Song] = []
for _ in range(12 - len(fixed_songs)):
for _ in range(1000):
diff --git a/Patches.py b/Patches.py
index 19b46ba64..3131ee8b1 100644
--- a/Patches.py
+++ b/Patches.py
@@ -19,7 +19,8 @@
from ItemPool import reward_list, song_list, trade_items, child_trade_items
from Location import Location, DisableType
from LocationList import business_scrubs
-from Messages import read_messages, update_message_by_id, read_shop_items, update_warp_song_text, \
+import Messages
+from Messages import SCRUBS_MESSAGE_IDS, SHOP_ITEM_MESSAGES, read_messages, update_message_by_id, read_shop_items, update_warp_song_text, \
write_shop_items, remove_unused_messages, make_player_message, \
add_item_messages, repack_messages, shuffle_messages, \
get_message_by_id, TextCode, new_messages, COLOR_MAP
@@ -59,8 +60,8 @@ def patch_rom(spoiler: Spoiler, world: World, rom: Rom) -> Rom:
]
for (bin_path, write_address) in bin_patches:
- with open(bin_path, 'rb') as stream:
- bytes_compressed = stream.read()
+ with open(bin_path, 'rb') as bin_stream:
+ bytes_compressed = bin_stream.read()
bytes_diff = zlib.decompress(bytes_compressed)
original_bytes = rom.original.buffer[write_address: write_address + len(bytes_diff)]
new_bytes = bytearray([a ^ b for a, b in zip(bytes_diff, original_bytes)])
@@ -98,8 +99,8 @@ def patch_rom(spoiler: Spoiler, world: World, rom: Rom) -> Rom:
extended_objects_start = start_address = rom.dma.free_space()
for name, zobj_path, object_id in zobj_imports:
- with open(zobj_path, 'rb') as stream:
- obj_data = stream.read()
+ with open(zobj_path, 'rb') as zobj_stream:
+ obj_data = zobj_stream.read()
rom.write_bytes(start_address, obj_data)
# Add it to the extended object table
end_address = ((start_address + len(obj_data) + 0x0F) >> 4) << 4
@@ -266,7 +267,7 @@ def copy(addr, size):
extended_textures_start = start_address = rom.dma.free_space()
for texture_id, texture_name, rom_address_base, rom_address_palette, size, func, patch_file in crate_textures:
# Apply the texture patch. Resulting texture will be stored in texture_data as a bytearray
- texture_data = func(rom, rom_address_base, rom_address_palette, size, data_path(patch_file) if patch_file else None)
+ texture_data = func(rom, rom_address_base, rom_address_palette, size, None if patch_file is None else data_path(patch_file))
rom.write_bytes(start_address, texture_data) # write the bytes to our new file
end_address = ((start_address + len(texture_data) + 0x0F) >> 4) << 4
@@ -701,12 +702,15 @@ def set_entrance_updates(entrances: Iterable[Entrance]) -> None:
continue
new_entrance = entrance.data
replaced_entrance = (entrance.replaces or entrance).data
+ assert replaced_entrance is not None
# Fixup save/quit and death warping entrance IDs on bosses.
if 'savewarp_addresses' in replaced_entrance and entrance.reverse:
- if entrance.parent_region.savewarp:
+ if entrance.parent_region is not None and entrance.parent_region.savewarp is not None:
+ assert entrance.parent_region.savewarp.replaces is not None
+ assert entrance.parent_region.savewarp.replaces.data is not None
savewarp = entrance.parent_region.savewarp.replaces.data['index']
- elif 'savewarp_fallback' in entrance.reverse.data:
+ elif entrance.reverse.data is not None and 'savewarp_fallback' in entrance.reverse.data:
# Spawning outside a grotto crashes the game, so we use a nearby regular entrance instead.
if entrance.reverse.data['savewarp_fallback'] == 0x0117:
# We don't want savewarping in a boss room inside GV Octorok Grotto to allow out-of-logic access to Gerudo Valley,
@@ -1218,7 +1222,7 @@ def calculate_traded_flags(world):
# start with silver rupees
if world.settings.shuffle_silver_rupees == 'remove':
for puzzle in world.silver_rupee_puzzles():
- save_context.give_item(world, f'Silver Rupee ({puzzle})', float('inf'))
+ save_context.give_item(world, f'Silver Rupee ({puzzle})', 10)
if world.settings.shuffle_smallkeys == 'vanilla':
if world.dungeon_mq['Spirit Temple']:
@@ -1315,7 +1319,7 @@ def calculate_traded_flags(world):
insert_space(rom, shop_item_file, shop_item_vram_start, 1, 0x3C + (0x20 * 50), 0x20 * 50)
# Add relocation entries for shop item table
- new_relocations = []
+ new_relocations: list[int | tuple[int, int]] = []
for i in range(50, 100):
new_relocations.append(shop_item_file.start + 0x1DEC + (i * 0x20) + 0x04)
new_relocations.append(shop_item_file.start + 0x1DEC + (i * 0x20) + 0x14)
@@ -1381,7 +1385,6 @@ def calculate_traded_flags(world):
# Update Child Anju's dialogue
new_message = "\x08What should I do!?\x01My \x05\x41Cuccos\x05\x40 have all flown away!\x04You, little boy, please!\x01Please gather at least \x05\x41%d Cuccos\x05\x40\x01for me.\x02" % world.settings.chicken_count
update_message_by_id(messages, 0x5036, new_message)
-
# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
location = world.bigocto_location()
if location is None or location.item is None or location.item.name == 'Nothing':
@@ -1451,15 +1454,6 @@ def calculate_traded_flags(world):
if any(hint_type in world.settings.misc_hints for hint_type in ('10_skulltulas', '20_skulltulas', '30_skulltulas', '40_skulltulas', '50_skulltulas')):
rom.write_int16(0xEA185A, 0x44C8)
- # Patch freestanding items
- if world.settings.shuffle_freestanding_items:
- # Get freestanding item locations
- actor_override_locations = [location for location in world.get_locations() if location.disabled == DisableType.ENABLED and location.type == 'ActorOverride']
- rupeetower_locations = [location for location in world.get_locations() if location.disabled == DisableType.ENABLED and location.type == 'RupeeTower']
-
- for location in actor_override_locations:
- patch_actor_override(location, rom)
-
if world.shuffle_silver_rupees:
rom.write_byte(rom.sym('SHUFFLE_SILVER_RUPEES'), 1)
if world.settings.shuffle_silver_rupees != 'remove':
@@ -1549,10 +1543,12 @@ def calculate_traded_flags(world):
for location in world.get_filled_locations():
if location.type == 'Song' and not songs_as_items:
item = location.item
+ assert item is not None
special = item.special
locationaddress = location.address
secondaryaddress = location.address2
-
+ assert not isinstance(locationaddress, list)
+ assert not isinstance(secondaryaddress, list)
bit_mask_pointer = 0x8C34 + ((special['item_id'] - 0x65) * 4)
rom.write_byte(locationaddress, special['song_id'])
next_song_id = special['song_id'] + 0x0D
@@ -1616,7 +1612,7 @@ def calculate_traded_flags(world):
shop_items[0x000A].description_message = 0x80B5
shop_items[0x000A].purchase_message = 0x80BE
- shuffle_messages.shop_item_messages = []
+ Messages.SHOP_ITEM_MESSAGES = []
# kokiri shop
shop_locations = [location for location in world.get_region('KF Kokiri Shop').locations if location.type == 'Shop'] # Need to filter because of the freestanding item in KF Shop
@@ -1725,7 +1721,9 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
for (scrub_item, default_price, text_id, text_replacement) in business_scrubs:
if scrub_item not in single_item_scrubs.keys():
continue
- scrub_message_dict[text_id] = update_scrub_text(get_message_by_id(messages, text_id).raw_text, text_replacement, default_price, default_price)
+ message = get_message_by_id(messages, text_id)
+ assert message is not None
+ scrub_message_dict[text_id] = update_scrub_text(message.raw_text, text_replacement, default_price, default_price)
else:
# Rebuild Business Scrub Item Table
rom.seek_address(0xDF8684)
@@ -1737,22 +1735,25 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
rom.write_int32(None, 0x80A74FF8) # Can_Buy_Func
rom.write_int32(None, 0x80A75354) # Buy_Func
- scrub_message_dict[text_id] = update_scrub_text(get_message_by_id(messages, text_id).raw_text, text_replacement, default_price, price)
+ message = get_message_by_id(messages, text_id)
+ assert message is not None
+ scrub_message_dict[text_id] = update_scrub_text(message.raw_text, text_replacement, default_price, price)
# update actor IDs
set_deku_salesman_data(rom)
# Update scrub messages.
- shuffle_messages.scrubs_message_ids = []
- for text_id, message in scrub_message_dict.items():
- update_message_by_id(messages, text_id, message)
+ Messages.SCRUBS_MESSAGE_IDS = []
+ for text_id, text in scrub_message_dict.items():
+ update_message_by_id(messages, text_id, text)
if world.settings.shuffle_scrubs == 'random':
- shuffle_messages.scrubs_message_ids.append(text_id)
+ Messages.SCRUBS_MESSAGE_IDS.append(text_id)
if world.settings.shuffle_grotto_entrances:
# Build the Grotto Load Table based on grotto entrance data
for entrance in world.get_shuffled_entrances(type='Grotto'):
if entrance.primary:
+ assert entrance.data is not None
load_table_pointer = rom.sym('GROTTO_LOAD_TABLE') + 4 * entrance.data['grotto_id']
rom.write_int16(load_table_pointer, entrance.data['entrance'])
rom.write_byte(load_table_pointer + 2, entrance.data['content'])
@@ -1774,6 +1775,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
update_message_by_id(messages, 0x405E, "\x1AChomp chomp chomp...\x01We have... \x05\x41a mysterious item\x05\x40! \x01Do you want it...huh? Huh?\x04\x05\x41\x0860 Rupees\x05\x40 and it's yours!\x01Keyahahah!\x01\x1B\x05\x42Yes\x01No\x05\x40\x02")
else:
location = world.get_location("ZR Magic Bean Salesman")
+ assert location.item is not None
item_text = get_hint(get_item_generic_name(location.item), True).text
wrapped_item_text = line_wrap(item_text, False, False, False)
if wrapped_item_text != item_text:
@@ -1792,6 +1794,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
update_message_by_id(messages, 0x6077, "\x06\x41Well Come!\x04I am selling stuff, strange and \x01rare, from all over the world to \x01everybody.\x01Today's special is...\x04A mysterious item! \x01Intriguing! \x01I won't tell you what it is until \x01I see the money....\x04How about \x05\x41200 Rupees\x05\x40?\x01\x01\x1B\x05\x42Buy\x01Don't buy\x05\x40\x02")
else:
location = world.get_location("Wasteland Bombchu Salesman")
+ assert location.item is not None
item_text = get_hint(get_item_generic_name(location.item), True).text
wrapped_item_text = line_wrap(item_text, False, False, False)
if wrapped_item_text != item_text:
@@ -1808,6 +1811,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
update_message_by_id(messages, 0x304F, "How about buying this cool item for \x01200 Rupees?\x01\x1B\x05\x42Buy\x01Don't buy\x05\x40\x02")
else:
location = world.get_location("GC Medigoron")
+ assert location.item is not None
item_text = get_hint(get_item_generic_name(location.item), True).text
wrapped_item_text = line_wrap(item_text, False, False, False)
if wrapped_item_text != item_text:
@@ -1820,6 +1824,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
update_message_by_id(messages, 0x500C, "Mysterious item! How about\x01\x05\x41100 Rupees\x05\x40?\x01\x1B\x05\x42Buy\x01Don't buy\x05\x40\x02")
else:
location = world.get_location("Kak Granny Buy Blue Potion")
+ assert location.item is not None
item_text = get_hint(get_item_generic_name(location.item), True).text
wrapped_item_text = line_wrap(item_text, False, False, False)
if wrapped_item_text != item_text:
@@ -1839,6 +1844,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
update_message_by_id(messages, 0x6D, "I seem to have misplaced my\x01keys, but I have a fun item to\x01sell instead.\x04How about \x05\x4110 Rupees\x05\x40?\x01\x01\x1B\x05\x42Buy\x01Don't Buy\x05\x40\x02")
else:
location = world.get_location("Market Treasure Chest Game Salesman")
+ assert location.item is not None
item_text = get_hint(get_item_generic_name(location.item), True).text
wrapped_item_text = line_wrap(item_text, False, False, False)
if wrapped_item_text != item_text:
@@ -1869,43 +1875,43 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
HEART_CHEST_BIG = 17
if world.settings.shuffle_tcgkeys == 'vanilla':
# Force key chests in Treasure Chest Game to use the default chest texture when not shuffled
- item = read_rom_item(rom, 0x0071)
- item['chest_type'] = BROWN_CHEST
- write_rom_item(rom, 0x0071, item)
+ rom_item = read_rom_item(rom, 0x0071)
+ rom_item['chest_type'] = BROWN_CHEST
+ write_rom_item(rom, 0x0071, rom_item)
if world.settings.free_bombchu_drops or 'bombchus' in world.settings.minor_items_as_major_chest:
bombchu_ids = [0x006A, 0x0003, 0x006B]
for i in bombchu_ids:
- item = read_rom_item(rom, i)
- item['chest_type'] = GILDED_CHEST
- write_rom_item(rom, i, item)
+ rom_item = read_rom_item(rom, i)
+ rom_item['chest_type'] = GILDED_CHEST
+ write_rom_item(rom, i, rom_item)
if world.settings.bridge == 'tokens' or world.settings.lacs_condition == 'tokens' or world.settings.shuffle_ganon_bosskey == 'tokens':
- item = read_rom_item(rom, 0x005B)
- item['chest_type'] = SKULL_CHEST_BIG
- write_rom_item(rom, 0x005B, item)
+ rom_item = read_rom_item(rom, 0x005B)
+ rom_item['chest_type'] = SKULL_CHEST_BIG
+ write_rom_item(rom, 0x005B, rom_item)
if world.settings.bridge == 'hearts' or world.settings.lacs_condition == 'hearts' or world.settings.shuffle_ganon_bosskey == 'hearts':
heart_ids = [0x003D, 0x003E, 0x0076]
for i in heart_ids:
- item = read_rom_item(rom, i)
- item['chest_type'] = HEART_CHEST_BIG
- write_rom_item(rom, i, item)
+ rom_item = read_rom_item(rom, i)
+ rom_item['chest_type'] = HEART_CHEST_BIG
+ write_rom_item(rom, i, rom_item)
if 'shields' in world.settings.minor_items_as_major_chest:
# Deku
- item = read_rom_item(rom, 0x0029)
- item['chest_type'] = GILDED_CHEST
- write_rom_item(rom, 0x0029, item)
+ rom_item = read_rom_item(rom, 0x0029)
+ rom_item['chest_type'] = GILDED_CHEST
+ write_rom_item(rom, 0x0029, rom_item)
# Hylian
- item = read_rom_item(rom, 0x002A)
- item['chest_type'] = GILDED_CHEST
- write_rom_item(rom, 0x002A, item)
+ rom_item = read_rom_item(rom, 0x002A)
+ rom_item['chest_type'] = GILDED_CHEST
+ write_rom_item(rom, 0x002A, rom_item)
if 'capacity' in world.settings.minor_items_as_major_chest:
# Nuts
- item = read_rom_item(rom, 0x0087)
- item['chest_type'] = GILDED_CHEST
- write_rom_item(rom, 0x0087, item)
+ rom_item = read_rom_item(rom, 0x0087)
+ rom_item['chest_type'] = GILDED_CHEST
+ write_rom_item(rom, 0x0087, rom_item)
# Sticks
- item = read_rom_item(rom, 0x0088)
- item['chest_type'] = GILDED_CHEST
- write_rom_item(rom, 0x0088, item)
+ rom_item = read_rom_item(rom, 0x0088)
+ rom_item['chest_type'] = GILDED_CHEST
+ write_rom_item(rom, 0x0088, rom_item)
# Update chest type appearance
if world.settings.correct_chest_appearances == 'textures':
@@ -1932,8 +1938,9 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
if not world.dungeon_mq['Ganons Castle']:
chest_name = 'Ganons Castle Light Trial Lullaby Chest'
location = world.get_location(chest_name)
- item = read_rom_item(rom, (location.item.looks_like_item or location.item).index)
- if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG, HEART_CHEST_BIG):
+ assert location.item is not None
+ rom_item = read_rom_item(rom, (location.item.looks_like_item or location.item).index)
+ if rom_item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG, HEART_CHEST_BIG):
rom.write_int16(0x321B176, 0xFC40) # original 0xFC48
# Move Spirit Temple Compass Chest if it is a small chest so it is reachable with hookshot
@@ -1941,8 +1948,9 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
chest_name = 'Spirit Temple Compass Chest'
chest_address = 0x2B6B07C
location = world.get_location(chest_name)
- item = read_rom_item(rom, (location.item.looks_like_item or location.item).index)
- if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL, HEART_CHEST_SMALL):
+ assert location.item is not None
+ rom_item = read_rom_item(rom, (location.item.looks_like_item or location.item).index)
+ if rom_item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL, HEART_CHEST_SMALL):
rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
@@ -1952,8 +1960,9 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
chest_address_0 = 0x21A02D0 # Address in setup 0
chest_address_2 = 0x21A06E4 # Address in setup 2
location = world.get_location(chest_name)
- item = read_rom_item(rom, (location.item.looks_like_item or location.item).index)
- if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL, HEART_CHEST_SMALL):
+ assert location.item is not None
+ rom_item = read_rom_item(rom, (location.item.looks_like_item or location.item).index)
+ if rom_item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL, HEART_CHEST_SMALL):
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
@@ -2008,16 +2017,21 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
else:
dungeon_name, compass_id, map_id = dungeon_list[dungeon.name]
if world.entrance_rando_reward_hints:
+ assert dungeon.vanilla_boss_name is not None
vanilla_reward = world.get_location(dungeon.vanilla_boss_name).vanilla_item
+ assert vanilla_reward is not None
vanilla_reward_location = world.hinted_dungeon_reward_locations[vanilla_reward]
area = HintArea.at(vanilla_reward_location)
- area = GossipText(area.text(world.settings.clearer_hints, preposition=True, use_2nd_person=True), [area.color], prefix='', capitalize=False)
- compass_message = f"\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for {dungeon_name}\x05\x40!\x01The {vanilla_reward} can be found\x01{area}!\x09"
+ area_text = GossipText(area.text(world.settings.clearer_hints, preposition=True, use_2nd_person=True), [area.color], prefix='', capitalize=False)
+ compass_message = f"\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for {dungeon_name}\x05\x40!\x01The {vanilla_reward} can be found\x01{area_text}!\x09"
else:
if world.settings.logic_rules == 'glitched':
boss_location = world.get_location(dungeon.vanilla_boss_name)
else:
- boss_location = next(filter(lambda loc: loc.type == 'Boss', world.get_entrance(f'{dungeon} Before Boss -> {dungeon.vanilla_boss_name} Boss Room').connected_region.locations))
+ entrance = world.get_entrance(f'{dungeon} Before Boss -> {dungeon.vanilla_boss_name} Boss Room')
+ assert entrance.connected_region is not None
+ boss_location = next(filter(lambda loc: loc.type == 'Boss', entrance.connected_region.locations))
+ assert boss_location.item is not None
dungeon_reward = boss_location.item.name
compass_message = f"\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for {dungeon_name}\x05\x40!\x01It holds the \x05{COLOR_MAP[REWARD_COLORS[dungeon_reward]]}{dungeon_reward}\x05\x40!\x09"
if world.settings.shuffle_dungeon_rewards != 'dungeon':
@@ -2060,7 +2074,9 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
for message_id in (0x706F, 0x7091, 0x7092, 0x7093, 0x7094, 0x7095):
text_codes = []
chars_in_section = 1
- for code in get_message_by_id(messages, message_id).text_codes:
+ message = get_message_by_id(messages, message_id)
+ assert message is not None
+ for code in message.text_codes:
if code.code == 0x04: # box-break
text_codes.append(TextCode(0x0c, 80 + chars_in_section))
chars_in_section = 1
@@ -2096,8 +2112,8 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
bfa_message = make_player_message(bfa_message)
update_message_by_id(messages, 0x0071, bfa_message, 0x23, allow_duplicates=True)
- with open(data_path('blue_fire_arrow_item_name_eng.ia4'), 'rb') as stream:
- bfa_name_bytes = stream.read()
+ with open(data_path('blue_fire_arrow_item_name_eng.ia4'), 'rb') as bin_stream:
+ bfa_name_bytes = bin_stream.read()
rom.write_bytes(0x8a1c00, bfa_name_bytes)
repack_messages(rom, messages, permutation)
@@ -2324,14 +2340,17 @@ def get_override_table_bytes(override_table):
def get_override_entry(location: Location) -> Optional[OverrideEntry]:
scene = location.scene
default = location.default
+ assert location.item is not None
+ assert location.world is not None
item_id = location.item.index
if None in (scene, default, item_id):
return None
# Don't add freestanding items, pots/crates, beehives to the override table if they're disabled. We use this check to determine how to draw and interact with them
- if location.type in ('ActorOverride', 'Freestanding', 'RupeeTower', 'Pot', 'Crate', 'FlyingPot', 'SmallCrate', 'Beehive', 'Wonderitem') and location.disabled != DisableType.ENABLED:
+ if location.type in ('Freestanding', 'RupeeTower', 'Pot', 'Crate', 'FlyingPot', 'SmallCrate', 'Beehive', 'Wonderitem') and location.disabled != DisableType.ENABLED:
return None
+ assert location.item.world is not None
player_id = location.item.world.id + 1
if location.item.looks_like_item is not None:
looks_like_item_id = location.item.looks_like_item.index
@@ -2360,7 +2379,7 @@ def get_override_entry(location: Location) -> Optional[OverrideEntry]:
default = ((scene_setup & 0x1F) << 19) + ((room & 0x0F) << 15) + ((flag & 0x7F) << 8) + ((subflag & 0xFF)) #scene_setup = grotto_id
else:
default = (scene_setup << 22) + (room << 16) + (flag << 8) + (subflag)
- elif location.type in ('Collectable', 'ActorOverride'):
+ elif location.type == 'Collectable':
type = 2
elif location.type == 'GS Token':
type = 3
@@ -2502,10 +2521,14 @@ def remove_entrance_blockers_do(rom: Rom, actor_id: int, actor: int, scene: int)
def set_cow_id_data(rom: Rom, world: World) -> None:
+ last_actor = -1
+ last_scene = -1
+ cow_count = 1
+
def set_cow_id(rom: Rom, actor_id: int, actor: int, scene: int) -> None:
+ nonlocal last_actor
nonlocal last_scene
nonlocal cow_count
- nonlocal last_actor
if actor_id == 0x01C6: # Cow
if scene == last_scene and last_actor != actor:
@@ -2520,10 +2543,6 @@ def set_cow_id(rom: Rom, actor_id: int, actor: int, scene: int) -> None:
else:
rom.write_int16(actor + 0x8, cow_count)
- last_actor = -1
- last_scene = -1
- cow_count = 1
-
get_actor_list(rom, set_cow_id)
@@ -2721,7 +2740,7 @@ def place_shop_items(rom: Rom, world: World, shop_items, messages, locations, in
shop_item.description_message = 0x8100 + message_id
shop_item.purchase_message = 0x8100 + message_id + 1
- shuffle_messages.shop_item_messages.extend(
+ Messages.SHOP_ITEM_MESSAGES.extend(
[shop_item.description_message, shop_item.purchase_message])
if item_display.dungeonitem:
@@ -2794,7 +2813,7 @@ def configure_dungeon_info(rom: Rom, world: World) -> None:
area = HintArea.at(location)
dungeon_reward_areas += area.short_name.encode('ascii').ljust(0x16) + b'\0'
dungeon_reward_worlds.append(location.world.id + 1)
- if location.world.id == world.id and area.is_dungeon:
+ if location.world.id == world.id and area.dungeon_name is not None:
dungeon_rewards[codes.index(area.dungeon_name)] = boss_reward_index(location.item)
dungeon_is_mq = [1 if world.dungeon_mq.get(c) else 0 for c in codes]
@@ -2815,31 +2834,6 @@ def configure_dungeon_info(rom: Rom, world: World) -> None:
rom.write_bytes(rom.sym('CFG_DUNGEON_PRECOMPLETED'), dungeon_precompleted)
-# Overwrite an actor in rom w/ the actor data from LocationList
-def patch_actor_override(location: Location, rom: Rom) -> None:
- addresses = location.address
- patch = location.address2
- if addresses is not None and patch is not None:
- for address in addresses:
- rom.write_bytes(address, patch)
-
-
-# Patch rupee towers (circular patterns of rupees) to include their flag in their actor initialization data z rotation.
-# Also used for goron pot, shadow spinning pots
-def patch_rupee_tower(location: Location, rom: Rom) -> None:
- if isinstance(location.default, tuple):
- room, scene_setup, flag = location.default
- elif isinstance(location.default, list):
- room, scene_setup, flag = location.default[0]
- else:
- raise Exception(f"Location does not have compatible data for patch_rupee_tower: {location.name}")
-
- flag = flag | (room << 8) | (scene_setup << 14)
- if location.address:
- for address in location.address:
- rom.write_bytes(address + 12, flag.to_bytes(2, byteorder='big'))
-
-
# Patch the first boss key door in ganons tower that leads to the room w/ the pots
def patch_ganons_tower_bk_door(rom: Rom, flag: int) -> None:
var = (0x05 << 6) + (flag & 0x3F)
diff --git a/Plandomizer.py b/Plandomizer.py
index a01605610..3e6d16457 100644
--- a/Plandomizer.py
+++ b/Plandomizer.py
@@ -67,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) -> json:
return {k: getattr(self, k) for (k, d) in self.properties.items() if getattr(self, k) != d}
def __str__(self) -> str:
diff --git a/Region.py b/Region.py
index 799b76928..cf2bc17fd 100644
--- a/Region.py
+++ b/Region.py
@@ -83,6 +83,7 @@ def hint(self) -> Optional[HintArea]:
return HintArea[self.hint_name]
if self.dungeon:
return self.dungeon.hint
+ return None
@property
def alt_hint(self) -> Optional[HintArea]:
@@ -90,6 +91,7 @@ def alt_hint(self) -> Optional[HintArea]:
if self.alt_hint_name is not None:
return HintArea[self.alt_hint_name]
+ return None
def can_fill(self, item: Item, manual: bool = False) -> bool:
from Hints import HintArea
diff --git a/Rom.py b/Rom.py
index a9cf7ea81..1114f8fdb 100644
--- a/Rom.py
+++ b/Rom.py
@@ -129,7 +129,7 @@ def decompress_rom(self, input_file: str, output_file: str, verify_crc: bool = T
subprocess.call(subcall, **subprocess_args())
self.read_rom(output_file, verify_crc=verify_crc)
- def write_byte(self, address: int, value: int) -> None:
+ def write_byte(self, address: int | None, value: int) -> None:
super().write_byte(address, value)
self.changed_address[self.last_address-1] = value
diff --git a/RuleParser.py b/RuleParser.py
index 93f132219..d948982d2 100644
--- a/RuleParser.py
+++ b/RuleParser.py
@@ -138,12 +138,12 @@ def visit_Tuple(self, node: ast.Tuple) -> Any:
item, count = node.elts
if not isinstance(item, ast.Name) and not (isinstance(item, ast.Constant) and isinstance(item.value, str)):
- raise Exception('Parse Error: first value must be an item. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False))
+ raise Exception('Parse Error: first value must be an item. Got %s' % item.__class__.__name__, None if self.current_spot is None else self.current_spot.name, ast.dump(node, False))
if isinstance(item, ast.Constant) and isinstance(item.value, str):
item = ast.Name(id=escape_name(item.value), ctx=ast.Load())
if not (isinstance(count, ast.Name) or (isinstance(count, ast.Constant) and isinstance(count.value, int))):
- raise Exception('Parse Error: second value must be a number. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False))
+ raise Exception('Parse Error: second value must be a number. Got %s' % item.__class__.__name__, None if self.current_spot is None else self.current_spot.name, ast.dump(node, False))
if isinstance(count, ast.Name):
# Must be a settings constant
diff --git a/Rules.py b/Rules.py
index f6a736bff..23795ff3a 100644
--- a/Rules.py
+++ b/Rules.py
@@ -25,6 +25,7 @@ def set_rules(world: World) -> None:
is_child = world.parser.parse_rule('is_child')
for location in world.get_locations():
+ assert location.world is not None
if world.settings.shuffle_song_items == 'song':
if location.type == 'Song':
# allow junk items, but songs must still have matching world
diff --git a/SaveContext.py b/SaveContext.py
index e2f71cbca..94ddf207a 100644
--- a/SaveContext.py
+++ b/SaveContext.py
@@ -54,12 +54,12 @@ 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) -> 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[str | int] = value
+ self.value: Optional[int] = None
self.size: int = size
self.choices: Optional[dict[str, int]] = choices
self.mask: int = mask
diff --git a/SceneFlags.py b/SceneFlags.py
index 5f57e6abd..2be7bf0ee 100644
--- a/SceneFlags.py
+++ b/SceneFlags.py
@@ -8,7 +8,7 @@
# Loop through all of the locations in the world. Extract ones that use our flag system to start building our xflag tables
def build_xflags_from_world(world: World) -> tuple[dict[int, dict[tuple[int, int], list[tuple[int, int]]]], list[tuple[Location, tuple[int, int, int, int], tuple[int, int, int, int]]]]:
- scene_flags = {}
+ scene_flags: dict[int, dict[int, int]] = {}
alt_list = []
for i in range(0, 101):
scene_flags[i] = {}
diff --git a/Search.py b/Search.py
index 88519d041..1c3eb0263 100644
--- a/Search.py
+++ b/Search.py
@@ -127,7 +127,7 @@ def reset(self) -> None:
# 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]:
- failed = []
+ failed: list[Entrance] = []
for exit in exit_queue:
if exit.world and exit.connected_region and exit.connected_region not in regions:
# Evaluate the access rule directly, without tod
diff --git a/SettingTypes.py b/SettingTypes.py
index 9a6ac734b..1da99ea89 100644
--- a/SettingTypes.py
+++ b/SettingTypes.py
@@ -24,12 +24,14 @@ def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Option
# dictionary of options to their text names
choices = {} if choices is None else choices
+ self.choices: dict
+ self.choice_list: list
if isinstance(choices, list):
- self.choices: dict = {k: k for k in choices}
- self.choice_list: list = list(choices)
+ self.choices = {k: k for k in choices}
+ self.choice_list = list(choices)
else:
- self.choices: dict = dict(choices)
- self.choice_list: list = list(choices.keys())
+ self.choices = dict(choices)
+ self.choice_list = list(choices.keys())
self.reverse_choices: dict = {v: k for k, v in self.choices.items()}
# number of bits needed to store the setting, used in converting settings to a string
diff --git a/Settings.py b/Settings.py
index 1753d9c91..829015716 100644
--- a/Settings.py
+++ b/Settings.py
@@ -29,8 +29,9 @@
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action) -> Optional[str]:
- if action.help is not None:
+ if action.help is not None:
return textwrap.dedent(action.help)
+ return None
# 32 characters
diff --git a/SettingsList.py b/SettingsList.py
index 5d97f732c..aa11dd1f4 100644
--- a/SettingsList.py
+++ b/SettingsList.py
@@ -5457,7 +5457,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[list[str] | dict[str, list[Entrance]]] = None) -> str:
- source = []
+ source: Iterable[str] = []
if value_type == 'item':
source = ItemInfo.items.keys()
elif value_type == 'location':
diff --git a/SettingsListTricks.py b/SettingsListTricks.py
index ed536ba44..93a83a4c1 100644
--- a/SettingsListTricks.py
+++ b/SettingsListTricks.py
@@ -1,11 +1,17 @@
from __future__ import annotations
+from typing import TypedDict
+class TrickInfo(TypedDict):
+ name: str
+ tags: tuple[str, ...]
+ tooltip: str
+
# 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, str | tuple[str, ...]]] = {
+logic_tricks: dict[str, TrickInfo] = {
# General tricks
diff --git a/SettingsToJson.py b/SettingsToJson.py
index 0dddae3ef..1c1d6d940 100755
--- a/SettingsToJson.py
+++ b/SettingsToJson.py
@@ -69,7 +69,7 @@ def get_setting_json(setting: str, web_version: bool, as_array: bool = False) ->
'options': [],
'default': setting_info.default,
'text': setting_info.gui_text,
- 'tooltip': remove_trailing_lines('
'.join(line.strip() for line in setting_info.gui_tooltip.split('\n'))),
+ 'tooltip': remove_trailing_lines('
'.join(line.strip() for line in (setting_info.gui_tooltip or '').split('\n'))),
'type': setting_info.gui_type,
'shared': setting_info.shared,
}
diff --git a/Spoiler.py b/Spoiler.py
index 59721ea3e..90bfb6f1b 100644
--- a/Spoiler.py
+++ b/Spoiler.py
@@ -80,7 +80,7 @@ def __init__(self, worlds: list[World]) -> None:
self.password: list[int] = []
def build_file_hash(self) -> None:
- dist_file_hash = self.settings.distribution.file_hash
+ dist_file_hash = self.settings.distribution.file_hash or [None, None, None, None, None]
for i in range(5):
self.file_hash.append(random.randint(0, 31) if dist_file_hash[i] is None else HASH_ICONS.index(dist_file_hash[i]))
diff --git a/State.py b/State.py
index 52b17328b..2cfb58abe 100644
--- a/State.py
+++ b/State.py
@@ -103,10 +103,10 @@ def has_ocarina_buttons(self, count: int) -> bool:
return self.count_distinct(ItemInfo.ocarina_buttons_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: GoalItem) -> 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: GoalItem) -> 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
diff --git a/TextBox.py b/TextBox.py
index 6eb4ad515..693a36824 100644
--- a/TextBox.py
+++ b/TextBox.py
@@ -112,7 +112,7 @@ def replace_bytes(match: re.Match) -> str:
box_codes = []
# Arrange our words into lines.
- lines = []
+ lines: list[list[list[TextCode]]] = []
start_index = 0
end_index = 0
box_count = 1
diff --git a/Unittest.py b/Unittest.py
index d23d80c46..3f79a8a16 100644
--- a/Unittest.py
+++ b/Unittest.py
@@ -135,7 +135,7 @@ def get_actual_pool(spoiler: dict[str, Any]) -> dict[str, int]:
key: Item name
value: count in spoiler
"""
- actual_pool = {}
+ actual_pool: dict[str, int] = {}
for location, item in spoiler['locations'].items():
if isinstance(item, dict):
test_item = item['item']
diff --git a/Utils.py b/Utils.py
index a1c9688d9..c05655768 100644
--- a/Utils.py
+++ b/Utils.py
@@ -19,36 +19,36 @@ def is_bundled() -> bool:
return getattr(sys, 'frozen', False)
+CACHED_LOCAL_PATH: Optional[str] = None
def local_path(path: str = '') -> str:
- if not hasattr(local_path, "cached_path"):
- local_path.cached_path = None
+ global CACHED_LOCAL_PATH
- if local_path.cached_path is not None:
- return os.path.join(local_path.cached_path, path)
+ if CACHED_LOCAL_PATH is not None:
+ return os.path.join(CACHED_LOCAL_PATH, path)
if is_bundled():
# we are running in a bundle
- local_path.cached_path = os.path.dirname(os.path.realpath(sys.executable))
+ CACHED_LOCAL_PATH = os.path.dirname(os.path.realpath(sys.executable))
else:
# we are running in a normal Python environment
- local_path.cached_path = os.path.dirname(os.path.realpath(__file__))
+ CACHED_LOCAL_PATH = os.path.dirname(os.path.realpath(__file__))
- return os.path.join(local_path.cached_path, path)
+ return os.path.join(CACHED_LOCAL_PATH, path)
+CACHED_DATA_PATH: Optional[str] = None
def data_path(path: str = '') -> str:
- if not hasattr(data_path, "cached_path"):
- data_path.cached_path = None
+ global CACHED_DATA_PATH
- if data_path.cached_path is not None:
- return os.path.join(data_path.cached_path, path)
+ if CACHED_DATA_PATH is not None:
+ return os.path.join(CACHED_DATA_PATH, path)
# Even if it's bundled we use __file__
# if it's not bundled, then we want to use the source.py dir + Data
# if it's bundled, then we want to use the extraction dir + Data
- data_path.cached_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
+ CACHED_DATA_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
- return os.path.join(data_path.cached_path, path)
+ return os.path.join(CACHED_DATA_PATH, path)
def default_output_path(path: str) -> str:
diff --git a/World.py b/World.py
index effd18d28..81afebbe0 100644
--- a/World.py
+++ b/World.py
@@ -12,13 +12,13 @@
from Entrance import Entrance
from Goals import Goal, GoalCategory
from HintList import get_required_hints, misc_item_hint_table, misc_location_hint_table
-from Hints import HintArea, hint_dist_keys, hint_dist_files
+from Hints import HintArea, RegionRestriction, hint_dist_files, hint_dist_keys
from Item import Item, ItemFactory, ItemInfo, make_event_item
from ItemPool import reward_list
from Location import Location, LocationFactory
from LocationList import business_scrubs, location_groups, location_table
-from OcarinaSongs import generate_song_list, Song
-from Plandomizer import WorldDistribution, InvalidFileException
+from OcarinaSongs import Song, generate_song_list
+from Plandomizer import InvalidFileException, WorldDistribution
from Region import Region, TimeOfDay
from RuleParser import Rule_AST_Transformer
from Settings import Settings
@@ -49,6 +49,7 @@ def __init__(self, world_id: int, settings: Settings, resolve_randomized_setting
self.empty_areas: dict[HintArea, dict[str, Any]] = {}
self.barren_dungeon: int = 0
self.woth_dungeon: int = 0
+ self.get_barren_hint_prev: RegionRestriction = RegionRestriction.NONE
self.randomized_list: list[str] = []
self.cached_bigocto_location: Optional[Location] = None
@@ -137,7 +138,7 @@ def __init__(self):
self['Shadow Temple'] = self.EmptyDungeonInfo('Bongo Bongo')
for area in HintArea:
- if area.is_dungeon and area.dungeon_name in self:
+ if area.dungeon_name is not None and area.dungeon_name in self:
self[area.dungeon_name].hint_name = area
def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo:
@@ -318,7 +319,7 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo:
else:
cat = GoalCategory(category['category'], category['priority'], minimum_goals=category['minimum_goals'])
for goal in category['goals']:
- cat.add_goal(Goal(self, goal['name'], goal['hint_text'], goal['color'], items=list({'name': i['name'], 'quantity': i['quantity'], 'minimum': i['minimum'], 'hintable': i['hintable']} for i in goal['items'])))
+ cat.add_goal(Goal(self, goal['name'], goal['hint_text'], goal['color'], items=[{'name': i['name'], 'quantity': i['quantity'], 'minimum': i['minimum'], 'hintable': i['hintable']} for i in goal['items']]))
if 'count_override' in category:
cat.goal_count = category['count_override']
else:
@@ -1156,7 +1157,7 @@ def bigocto_location(self) -> Optional[Location]:
if region is not None and region.locations is not None
for loc in region.locations
if not loc.locked
- and loc.has_item()
+ and loc.item is not None
and not loc.item.event
and (loc.type != "Shop" or loc.name in self.shop_prices) # ignore regular shop items (but keep special deals)
]
@@ -1166,7 +1167,6 @@ def bigocto_location(self) -> Optional[Location]:
priority_types = (
"Wonderitem",
"Freestanding",
- "ActorOverride",
"RupeeTower",
"Pot",
"Crate",
@@ -1221,13 +1221,13 @@ def get_locations(self) -> list[Location]:
self._cached_locations.extend(region.locations)
return self._cached_locations
- def get_unfilled_locations(self) -> Iterable[Location]:
+ def get_unfilled_locations(self) -> Iterator[Location]:
return filter(Location.has_no_item, self.get_locations())
- def get_filled_locations(self) -> Iterable[Location]:
- return filter(Location.has_item, self.get_locations())
+ def get_filled_locations(self, item_filter: Callable[[Item], bool] = lambda item: True) -> Iterator[Location]:
+ return filter(lambda loc: loc.item is not None and item_filter(loc.item), self.get_locations())
- def get_progression_locations(self) -> Iterable[Location]:
+ def get_progression_locations(self) -> Iterator[Location]:
return filter(Location.has_progression_item, self.get_locations())
def get_entrances(self) -> list[Entrance]:
diff --git a/texture_util.py b/texture_util.py
index 9f7dcc2fb..d58f78b0d 100755
--- a/texture_util.py
+++ b/texture_util.py
@@ -58,9 +58,9 @@ 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]:
- if rgba16_patch is not None and (len(rgba16_texture) != len(rgba16_patch)):
- raise(Exception("OG Texture and Patch not the same length!"))
+def apply_rgba16_patch(rgba16_texture: list[int], rgba16_patch: Optional[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!")
new_texture = []
if not rgba16_patch:
@@ -139,7 +139,7 @@ def rgba16_from_file(rom: Rom, base_texture_address: int, base_palette_address:
# size - Size of the texture in PIXELS
# patchfile - file path of a rgba16 binary texture to patch
# returns - bytearray of the new texture
-def rgba16_patch(rom: Rom, base_texture_address: int, base_palette_address: int, size: int, patchfile: str) -> bytearray:
+def rgba16_patch(rom: Rom, base_texture_address: int, base_palette_address: Optional[int], size: int, patchfile: str) -> bytearray:
base_texture_rgba16 = load_rgba16_texture_from_rom(rom, base_texture_address, size)
patch_rgba16 = None
if patchfile:
@@ -158,11 +158,12 @@ def rgba16_patch(rom: Rom, base_texture_address: int, base_palette_address: int,
# size - Size of the texture in PIXELS
# patchfile - file path of a rgba16 binary texture to patch
# returns - bytearray of the new texture
-def ci4_rgba16patch_to_ci8(rom: Rom, base_texture_address: int, base_palette_address: int, size: int, patchfile: str) -> bytearray:
+def ci4_rgba16patch_to_ci8(rom: Rom, base_texture_address: int, base_palette_address: Optional[int], size: int, patchfile: Optional[str]) -> bytearray:
+ assert base_palette_address is not None
palette = load_palette(rom, base_palette_address, 16) # load the original palette from rom
base_texture_rgba16 = ci4_to_rgba16(rom, base_texture_address, size, palette) # load the original texture from rom and convert to ci8
patch_rgba16 = None
- if patchfile:
+ if patchfile is not None:
patch_rgba16 = load_rgba16_texture(patchfile, size)
new_texture_rgba16 = apply_rgba16_patch(base_texture_rgba16, patch_rgba16)
ci8_texture, ci8_palette = rgba16_to_ci8(new_texture_rgba16)