예제 #1
0
 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.",
         )
예제 #2
0
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(
예제 #3
0
    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()
예제 #4
0
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."
예제 #5
0
    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.")