def window_method(*args, **kwargs): try: func(*args, **kwargs) except SoulstructProjectError as e: window.CustomDialog( title="Runtime Manager Error", message=word_wrap(str(e), 50), ) except Exception as e: window.CustomDialog( title="Unknown Internal Error", message="Internal Error:\n\n" + word_wrap(str(e), 50) + "\n\nPlease report this error.", )
from soulstruct._logging import CONSOLE_HANDLER, FILE_HANDLER from soulstruct.utilities import word_wrap LOG_LEVELS = {"debug", "info", "warning", "error", "fatal", "critical"} _LOGGER = logging.getLogger("soulstruct") parser = argparse.ArgumentParser(prog="soulstruct", description="Launch Soulstruct programs or adjust settings.") parser.add_argument( "source", nargs="?", default=DEFAULT_PROJECT_PATH, help=word_wrap( "Source file or project directory to read from. Defaults to `DEFAULT_PROJECT_PATH` from `config.py`, which " "will be auto-generated the first time you run Soulstruct. If your project path is relative, it will be " "resolved relative to the executable directory (if running the frozen Soulstruct exe) or otherwise the working " "directory. If no additional arguments are given, the main Soulstruct GUI will launch. Alternatively, if " "`--console` is used, the `DarkSoulsProject` instance will be loaded into an interactive session (iPython " "package required)." ), ) parser.add_argument( "-c", "--console", action="store_true", default=False, help=word_wrap( "Open an interactive IPython console rather than using the Soulstruct GUI. IPython must be installed to use " "this option, but you can always import and use Soulstruct from directly within an existing Python session." ), ) parser.add_argument(
def __init__(self, project_path=None, master=None): super().__init__( master=master, toplevel=True, icon_data=SOULSTRUCT_ICON, window_title=f"{self.PROJECT_CLASS.GAME.name} Project Editor", ) self.withdraw() if not project_path: self.CustomDialog( title="Choose Soulstruct project directory", message="Navigate to your Soulstruct project directory.\n\n" + word_wrap( "If you want to create a new project, create an empty directory and select it. " "The name of the directory will be the name of the project.", 50, ), ) project_path = self.FileDialog.askdirectory( title="Choose Soulstruct project directory", initialdir=str(Path("~/Documents").expanduser()) ) if not project_path: self.CustomDialog(title="Project Error", message="No directory chosen. Quitting Soulstruct.") raise SoulstructProjectError("No directory chosen. Quitting Soulstruct.") self.toplevel.title(f"{self.PROJECT_CLASS.GAME.name} Project Editor: {Path(project_path)}") try: self.project = self.PROJECT_CLASS(project_path, with_window=self) _LOGGER.info(f"Opened project: {project_path}") except SoulstructProjectError as e: self.deiconify() msg = ( f"Fatal Soulstruct project error encountered (see log for full traceback):\n\n" f"{word_wrap(str(e), 50)}\n\nAborting startup." ) _LOGGER.exception("Fatal Soulstruct project error encountered. Aborting startup.") self.CustomDialog(title="Project Error", message=msg) raise except Exception as e: self.deiconify() _LOGGER.exception("Fatal internal error encountered. Aborting startup.") msg = ( f"Fatal internal error encountered (see log for full traceback):" f"\n\n{word_wrap(str(e), 50)}\n\nAborting startup." ) self.CustomDialog(title="Internal Error", message=msg) raise self.linker = WindowLinker(self) # TODO: Individual editors should have a lesser linker. with self.set_master(column_weights=[1], row_weights=[0, 1], auto_rows=0, sticky="nsew"): self.global_ribbon = self.Frame(row_weights=[1], column_weights=[1, 1, 1, 1], pady=(10, 5)) self.page_tabs = self.Notebook(name="project_notebook", sticky="nsew") self.maps_tab = None self.entities_tab = None self.params_tab = None self.text_tab = None self.lighting_tab = None self.events_tab = None self.ai_tab = None self.talk_tab = None self.runtime_tab = None self.save_all_button = None self.save_tab_button = None self.export_all_button = None self.export_tab_button = None self.toplevel.minsize(700, 500) self.alphanumeric_word_boundaries() if getattr(sys, "frozen", False): self.toplevel.protocol("WM_DELETE_WINDOW", self.confirm_quit) self.build() self.deiconify() self.maps_tab.check_for_repeated_entity_ids()
from soulstruct._config import DEFAULT_PROJECT_PATH from soulstruct.core import CONSOLE_HANDLER, FILE_HANDLER from soulstruct.utilities import word_wrap LOG_LEVELS = {'debug', 'info', 'warning', 'error', 'fatal', 'critical'} _LOGGER = logging.getLogger(__name__) parser = argparse.ArgumentParser(prog='soulstruct', description="Launch Soulstruct programs or adjust settings.") parser.add_argument( "source", nargs='?', default=DEFAULT_PROJECT_PATH, help=word_wrap( "Source file or directory to read from. Use 'live' to use the LIVE_GAME_PATH, 'temp' to use the " "TEMP_GAME_PATH, or 'default' (or no source) to use the DEFAULT_GAME_PATH. If no additional arguments are " "given, the main Soulstruct GUI will launch (in which case, 'source' must be either the game executable or its " "containing directory), or if '--console' is True, all possible structures will be loaded into an interactive " "console." ) ) parser.add_argument( "-c", "--console", action='store_true', default=False, help=word_wrap( "Open an interactive IPython console rather than using the Soulstruct GUI. IPython must be installed to use " "this option, but you can always import and use Soulstruct from directly within an existing Python session." ) ) parser.add_argument( "-t", "--text", action='store_true', help=word_wrap( "Open Soulstruct Text Editor with given source."
def _import_entities_module(self): """Reads '{map_id}_entities.py' file and loads names from it into map data. Also tries to read descriptions from inline comments with regex. (Messing too much with the formatting in the module file may interfere with this.) """ game_map = get_map(self.map_choice_id) msb = self.get_selected_msb() module_path = self.evs_directory / f"{game_map.emevd_file_stem}_entities.py" if not module_path.is_file(): return self.error_dialog( "No Entity Module", "Entity module not yet created in project 'events' folder.") evs_path = self.evs_directory / f"{game_map.emevd_file_stem}.evs.py" if not evs_path.is_file(): return self.error_dialog( "No EVS Script", "EVS script not yet imported into project 'events' folder.") sys.path.append(str(module_path.parent)) try: entity_module = import_module(module_path.stem) except Exception as e: return self.error_dialog( "Import Error", f"Could not import {module_path.name}. Error:\n\n{str(e)}") entries_by_entity_enum = {} found_map_entry_class_names = [] not_found = [] skipped = set() # will also skip description for attr_name, attr in [ m[0:2] for m in inspect.getmembers(entity_module, inspect.isclass) if m[1].__module__ == entity_module.__name__ ]: for entry_game_type in ENTITY_GAME_TYPES: if entry_game_type in attr.__bases__: break else: continue # ignore this class found_map_entry_class_names.append(attr_name) for entity_enum in attr: entry = msb.get_entry_with_entity_id(entity_enum.value, allow_multiple=True) if entry is None: not_found.append(entity_enum.value) continue if isinstance(entry, list): choice = self.CustomDialog( title="Multiple Entries with Same ID", message= f"Entity ID {entity_enum.value} in Python module '{module_path.stem}' appears " f"multiple times in MSB. This will rename only the first one found (type " f"{entry[0].ENTRY_SUBTYPE.name}). Is this OK?", button_kwargs=("YES", "NO", "NO"), button_names=("Yes, change it", "No, skip it", "No, abort import"), default_output=2, cancel_output=2, ) if choice == 2: return elif choice == 1: skipped.add(entity_enum.value) continue else: entry = entry[0] entry_type_name = entry.ENTRY_SUBTYPE.get_pluralized_type_name( ) entry_subtype_name = entry.ENTRY_SUBTYPE.name if entry_game_type.get_msb_entry_type_subtype() != ( entry_type_name, entry_subtype_name): choice = self.CustomDialog( title="Entry Type Mismatch", message= f"Entity ID {entity_enum.value} in Python module '{module_path.stem}' has type " f"'{attr_name}', but has different type '{entry_type_name}.{entry_subtype_name} in MSB. " f"Change name anyway?", button_kwargs=("YES", "NO", "NO"), button_names=("Yes, change it", "No, skip it", "No, abort import"), default_output=2, cancel_output=2, ) if choice == 2: return elif choice == 1: skipped.add(entity_enum.value) continue entries_by_entity_enum[entity_enum] = entry if not entries_by_entity_enum: return self.CustomDialog( "No IDs to Update", "No IDs in the Python entities module are present in the MSB.") if not_found: not_found_string = word_wrap(", ".join(not_found), 50) if (self.CustomDialog( title="Allow Missing IDs?", message= f"These entity IDs in the Python module could not be found in the MSB:" f"\n\n{not_found_string}" f"\n\nContinue updating {len(entries_by_entity_enum)} other IDs?", button_kwargs=("YES", "NO"), button_names=("Yes, continue", "No, abort import"), default_output=0, cancel_output=1, ) == 1): return # Find descriptions with regular expressions. skip_description = skipped.union(not_found) current_attr_name = "" descriptions_by_attr_and_id = {} with module_path.open("r", encoding="utf-8") as module: for line in module: class_match = _RE_ENTITIES_ENUM_CLASS.match(line) if class_match: attr_name = class_match.group(1) if attr_name in found_map_entry_class_names: current_attr_name = attr_name continue if current_attr_name: member_match = _RE_ENTITIES_ENUM_MEMBER.match(line) if member_match: entity_id, description = member_match.group(2, 3) if entity_id not in skip_description: descriptions_by_attr_and_id[ current_attr_name, int(entity_id)] = description.strip() for entity_enum, entry in entries_by_entity_enum.items(): entry.name = entity_enum.name for (attr_name, entity_id), description in descriptions_by_attr_and_id.items(): # attr_name not actually used, as entity ID uniqueness should have already been resolved entry = msb.get_entry_with_entity_id(entity_id, allow_multiple=True) if not entry: continue # shouldn't happen (as missing entity ID should be skipped) but just in case if isinstance(entry, list): entry = entry[0] # could happen entry.description = description self.refresh_entries() self.CustomDialog( "Import Successful", f"Entity names and descriptions imported successfully.")