diff --git a/Generate.py b/Generate.py index 6855a6aaae..ff76b3c649 100644 --- a/Generate.py +++ b/Generate.py @@ -585,7 +585,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads - picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] + picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + list(failed_world_loads.keys()), + limit=1)[0] if picks[0] in failed_world_loads: raise Exception(f"No functional world found to handle game {ret.game}. " f"Did you mean '{picks[0]}' ({picks[1]}% sure)? " diff --git a/Launcher.py b/Launcher.py index 0e7d4796c4..d1e076b4c3 100644 --- a/Launcher.py +++ b/Launcher.py @@ -36,6 +36,7 @@ if __name__ == "__main__": init_logging('Launcher') from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type +from worlds import failed_world_loads def open_host_yaml(): @@ -275,6 +276,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: search_box: MDTextField = ObjectProperty(None) cards: list[LauncherCard] current_filter: Sequence[str | Type] | None + failed_worlds: bool = bool(failed_world_loads) def __init__(self, ctx=None, components=None, args=None): self.title = self.base_title + " " + Utils.__version__ @@ -422,6 +424,39 @@ def run_gui(launch_components: list[Component], args: Any) -> None: MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5}, size_hint_x=0.5).open() + @staticmethod + def copy_to_clipboard(text): + from kivy.core.clipboard import Clipboard + Clipboard.copy(text) + MDSnackbar(MDSnackbarText(text="Copied to clipboard."), y=dp(24), pos_hint={"center_x": 0.5}, + size_hint_x=0.5).open() + + def display_failed(self): + """Display a dialog showing the exceptions produced by any world that failed to load during + initialization.""" + if not self.failed_worlds: + return + from kivymd.uix.dialog import MDDialog, MDDialogIcon, MDDialogHeadlineText, MDDialogContentContainer + from kivymd.uix.divider import MDDivider + from kivymd.uix.list import MDListItem, MDListItemHeadlineText, MDListItemSupportingText + entries = [] + for world, reason in failed_world_loads.items(): + entries.append(MDListItem( + MDListItemHeadlineText(text=world), + MDListItemSupportingText(text=reason), + on_release=lambda x, r=reason: self.copy_to_clipboard(r) + )) + dialog = MDDialog( + MDDialogIcon(icon="alert"), + MDDialogHeadlineText(text="Failed World Loads"), + MDDialogContentContainer( + MDDivider(), + *entries, + orientation="vertical", + ) + ) + dialog.open() + def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None: """ When a patch file is dropped into the window, run the associated component. """ file, component = identify(filename.decode()) diff --git a/data/launcher.kv b/data/launcher.kv index 1cb4e84ab5..a52214a7a4 100644 --- a/data/launcher.kv +++ b/data/launcher.kv @@ -140,6 +140,15 @@ MDFloatLayout: MDNavigationDrawerDivider: + MDBoxLayout: + orientation: "horizontal" + MDIconButton: + icon: "alert" if app.failed_worlds else "" + theme_text_color: "Custom" + text_color: "D23C42" + disabled: not app.failed_worlds + on_release: app.display_failed() + MDGridLayout: id: main_layout diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 0bc7b62d5b..0906852201 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -54,7 +54,7 @@ class TestImplemented(unittest.TestCase): def test_no_failed_world_loads(self): if failed_world_loads: - self.fail(f"The following worlds failed to load: {failed_world_loads}") + self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}") def test_prefill_items(self): """Test that every world can reach every location from allstate before pre_fill.""" diff --git a/worlds/__init__.py b/worlds/__init__.py index dd2d83a27e..1907100688 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -33,7 +33,7 @@ __all__ = [ ] -failed_world_loads: List[str] = [] +failed_world_loads: dict[str, str] = {} @dataclasses.dataclass(order=True) @@ -68,8 +68,9 @@ class WorldSource: print(f"Could not load world {self}:", file=file_like) traceback.print_exc(file=file_like) file_like.seek(0) - logging.exception(file_like.read()) - failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0]) + reason = file_like.read() + logging.exception(reason) + failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason return False @@ -128,7 +129,7 @@ if apworlds: def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None: if add_as_failed_to_load: - failed_world_loads.append(game_name) + failed_world_loads[game_name] = reason logging.warning(reason) for apworld_source in apworlds: