diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8bb3555 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.2.2 + hooks: + - id: ruff + args: [ --fix, --exit-non-zero-on-fix ] + - id: ruff-format + +- repo: https://github.com/henriquegemignani/jsonschema-to-typeddict + rev: v1.1.1 + hooks: + - id: jsonschema-to-typeddict + files: caver/schema/schema.json + args: [ --output-path, caver/schema/types.py, --root-name, CaverData ] diff --git a/caver/patcher.py b/caver/patcher.py index 5a7f4dc..b5731ce 100644 --- a/caver/patcher.py +++ b/caver/patcher.py @@ -1,26 +1,46 @@ +from __future__ import annotations + +import json +import logging +import platform as pl +import shutil +import sys +import textwrap +import typing from enum import Enum from pathlib import Path -from typing import Callable, Optional -from randovania_lupa import LuaRuntime # type: ignore -import logging -import shutil -import textwrap -import sys -import platform as pl +from uuid import UUID import pre_edited_cs +from randovania_lupa import LuaRuntime # type: ignore +from caver.schema.validator_with_default import DefaultValidatingDraft7Validator +LuaFile = typing.Any + +if typing.TYPE_CHECKING: + from collections.abc import Callable + + from caver.schema import ( + CaverData, + CaverdataMaps, + CaverdataOtherTsc, + EventNumber, + MapName, + ) CSVERSION = 5 + class CaverException(Exception): pass + class CSPlatform(Enum): FREEWARE = "freeware" TWEAKED = "tweaked" + def get_path() -> Path: if getattr(sys, "frozen", False): file_dir = Path(getattr(sys, "_MEIPASS")) @@ -28,34 +48,46 @@ def get_path() -> Path: file_dir = Path(__file__).parent.parent return file_dir.joinpath("caver") -def patch_files(patch_data: dict, output_dir: Path, platform: CSPlatform, progress_update: Callable[[str, float], None]): + +def validate(patch_data: dict) -> None: + with Path(__file__).parent.joinpath("schema/schema.json").open() as f: + schema = json.load(f) + DefaultValidatingDraft7Validator(schema).validate(patch_data) + + +def patch_files( + patch_data: CaverData, output_dir: Path, platform: CSPlatform, progress_update: Callable[[str, float], None] +) -> None: + progress_update("Validating schema...", -1) + validate(typing.cast(dict, patch_data)) + progress_update("Copying base files...", -1) ensure_base_files_exist(platform, output_dir) total = len(patch_data["maps"].keys()) + len(patch_data["other_tsc"].keys()) + 3 lua_file = get_path().joinpath("tsc_file.lua").read_text() - TscFile = LuaRuntime().execute(lua_file) + TscFile = typing.cast(LuaFile, LuaRuntime().execute(lua_file)) for i, (mapname, mapdata) in enumerate(patch_data["maps"].items()): - progress_update(f"Patching {mapname}...", i/total) + progress_update(f"Patching {mapname}...", i / total) patch_map(mapname, mapdata, TscFile, output_dir) for filename, scripts in patch_data["other_tsc"].items(): i += 1 - progress_update(f"Patching {filename}.tsc...", i/total) + progress_update(f"Patching {filename}.tsc...", i / total) patch_other(filename, scripts, TscFile, output_dir) i += 1 - progress_update("Copying MyChar...", i/total) + progress_update("Copying MyChar...", i / total) patch_mychar(patch_data["mychar"], output_dir, platform is CSPlatform.TWEAKED) i += 1 - progress_update("Copying hash...", i/total) + progress_update("Copying hash...", i / total) patch_hash(patch_data["hash"], output_dir) i += 1 - progress_update("Copying UUID...", i/total) + progress_update("Copying UUID...", i / total) patch_uuid(patch_data["uuid"], output_dir) if platform == CSPlatform.TWEAKED: @@ -64,27 +96,35 @@ def patch_files(patch_data: dict, output_dir: Path, platform: CSPlatform, progre else: output_dir.joinpath("CSTweaked").unlink() -def ensure_base_files_exist(platform: CSPlatform, output_dir: Path): + +def ensure_base_files_exist(platform: CSPlatform, output_dir: Path) -> None: internal_copy = pre_edited_cs.get_path() version = output_dir.joinpath("data", "Stage", "_version.txt") keep_existing_files = version.exists() and int(version.read_text()) >= CSVERSION - def should_ignore(path: str, names: list[str]): + def should_ignore(path: str, names: list[str]) -> list[str]: base = ["__init__.py", "__pycache__", "ScriptSource", "__pyinstaller"] if keep_existing_files: p = Path(path) - base.extend([str(p.joinpath(name)) for name in names if p.joinpath(name).exists() and p.joinpath(name).is_file()]) + base.extend( + [str(p.joinpath(name)) for name in names if p.joinpath(name).exists() and p.joinpath(name).is_file()] + ) return base try: shutil.copytree(internal_copy.joinpath(platform.value), output_dir, ignore=should_ignore, dirs_exist_ok=True) - shutil.copytree(internal_copy.joinpath("data"), output_dir.joinpath("data"), ignore=should_ignore, dirs_exist_ok=True) + shutil.copytree( + internal_copy.joinpath("data"), output_dir.joinpath("data"), ignore=should_ignore, dirs_exist_ok=True + ) except shutil.Error: - raise CaverException("Error copying base files. Ensure the directory is not read-only, and that Doukutsu.exe is closed") + raise CaverException( + "Error copying base files. Ensure the directory is not read-only, and that Doukutsu.exe is closed" + ) output_dir.joinpath("data", "Plaintext").mkdir(exist_ok=True) -def patch_map(mapname: str, mapdata: dict[str, dict], TscFile, output_dir: Path): + +def patch_map(mapname: MapName, mapdata: CaverdataMaps, TscFile: LuaFile, output_dir: Path) -> None: mappath = output_dir.joinpath("data", "Stage", f"{mapname}.tsc") tsc_file = TscFile.new(TscFile, mappath.read_bytes(), logging.getLogger("caver")) @@ -95,7 +135,7 @@ def patch_map(mapname: str, mapdata: dict[str, dict], TscFile, output_dir: Path) TscFile.placeSongAtCue(tsc_file, song["song_id"], event, song["original_id"], mapname) for event, script in mapdata["entrances"].items(): - needle = " None: filepath = output_dir.joinpath("data", f"{filename}.tsc") tsc_file = TscFile.new(TscFile, filepath.read_bytes(), logging.getLogger("caver")) @@ -117,7 +160,8 @@ def patch_other(filename: str, scripts: dict[str, dict[str, str]], TscFile, outp filepath.write_bytes(bytes(chars)) output_dir.joinpath("data", "Plaintext", f"{filename}.txt").write_text(TscFile.getPlaintext(tsc_file)) -def patch_mychar(mychar: Optional[str], output_dir: Path, add_upscale: bool): + +def patch_mychar(mychar: str | None, output_dir: Path, add_upscale: bool) -> None: if mychar is None: return mychar_img = Path(mychar).read_bytes() @@ -129,15 +173,18 @@ def patch_mychar(mychar: Optional[str], output_dir: Path, add_upscale: bool): output_dir.joinpath("data", "sprites_up", "MyChar.bmp").write_bytes(mychar_up_img) -def patch_hash(hash: list[int], output_dir: Path): +def patch_hash(hash: list[int], output_dir: Path) -> None: hash_strings = [f"{num:04d}" for num in hash] hash_string = ",".join(hash_strings) output_dir.joinpath("data", "hash.txt").write_text(hash_string) -def patch_uuid(uuid: str, output_dir: Path): + +def patch_uuid(uuid: str, output_dir: Path) -> None: + uuid = f"{{{UUID(uuid)}}}" output_dir.joinpath("data", "uuid.txt").write_text(uuid) -def wrap_msg_text(text: str, facepic: bool, *, ending: str = " str: + +def wrap_msg_text(text: str, facepic: bool, *, ending: str = " str: hard_limit = 35 msgbox_limit = 26 if facepic else hard_limit @@ -145,19 +192,21 @@ def wrap_msg_text(text: str, facepic: bool, *, ending: str = " str: """ - A desperate attempt to generate valid =1.10 + jsonschema>=4.0.0 include_package_data = True zip_safe = False -python_requires = >=3.9 +python_requires = >=3.10 # # Entry Points for PyInstaller