cave-story-randomizer/caver/patcher.py

144 lines
5.5 KiB
Python
Raw Normal View History

2021-12-01 04:37:50 +00:00
from pathlib import Path
2021-12-01 07:28:01 +00:00
from typing import Callable, Optional
2021-12-01 04:37:50 +00:00
from lupa import LuaRuntime
import logging
import shutil
2021-12-01 08:44:05 +00:00
import re
2021-12-08 06:42:20 +00:00
import sys
2021-12-08 06:09:45 +00:00
import pre_edited_cs
2021-12-01 04:37:50 +00:00
CSVERSION = 3
2021-12-06 08:20:16 +00:00
class CaverException(Exception):
pass
2021-12-08 06:42:20 +00:00
def get_path() -> Path:
if getattr(sys, "frozen", False):
file_dir = Path(getattr(sys, "_MEIPASS"))
else:
file_dir = Path(__file__).parent.parent
return file_dir.joinpath("caver")
2021-12-08 06:42:20 +00:00
2021-12-01 07:28:01 +00:00
def patch_files(patch_data: dict, output_dir: Path, progress_update: Callable[[str, float], None]):
2021-12-08 06:42:20 +00:00
progress_update("Copying base files...", -1)
2021-12-01 04:37:50 +00:00
ensure_base_files_exist(output_dir)
2021-12-10 09:05:51 +00:00
total = len(patch_data["maps"].keys()) + len(patch_data["other_tsc"].keys()) + 2
2021-12-01 07:28:01 +00:00
2021-12-08 06:42:20 +00:00
lua_file = get_path().joinpath("tsc_file.lua").read_text()
TscFile = LuaRuntime().execute(lua_file)
2021-12-01 07:28:01 +00:00
for i, (mapname, mapdata) in enumerate(patch_data["maps"].items()):
2021-12-10 09:05:51 +00:00
progress_update(f"Patching {mapname}...", i/total)
2021-12-01 04:37:50 +00:00
patch_map(mapname, mapdata, TscFile, output_dir)
2021-12-10 09:05:51 +00:00
for filename, scripts in patch_data["other_tsc"].items():
i += 1
progress_update(f"Patching {filename}.tsc...", i/total)
patch_other(filename, scripts, TscFile, output_dir)
i += 1
progress_update("Copying MyChar...", i/total)
2021-12-01 04:37:50 +00:00
patch_mychar(patch_data["mychar"], output_dir)
2021-12-10 09:05:51 +00:00
i += 1
progress_update("Copying hash...", i/total)
2021-12-01 04:37:50 +00:00
patch_hash(patch_data["hash"], output_dir)
def ensure_base_files_exist(output_dir: Path):
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
2021-12-01 07:28:01 +00:00
def should_ignore(path: str, names: list[str]):
2021-12-08 06:42:20 +00:00
base = ["__init__.py", "__pycache__", "ScriptSource", "__pyinstaller"]
2021-12-01 07:28:01 +00:00
if keep_existing_files:
p = Path(path)
base.extend([p.joinpath(name) for name in names if p.joinpath(name).exists() and p.joinpath(name).is_file()])
return base
2021-12-06 08:20:16 +00:00
try:
shutil.copytree(internal_copy, output_dir, ignore=should_ignore, dirs_exist_ok=True)
2021-12-06 08:20:16 +00:00
except shutil.Error:
2021-12-10 07:32:28 +00:00
raise CaverException("Error copying base files. Ensure the directory is not read-only, and that Doukutsu.exe is closed")
2021-12-01 07:28:01 +00:00
output_dir.joinpath("data", "Plaintext").mkdir(exist_ok=True)
2021-12-01 04:37:50 +00:00
def patch_map(mapname: str, mapdata: dict[str, dict], TscFile, output_dir: Path):
mappath = output_dir.joinpath("data", "Stage", f"{mapname}.tsc")
2021-12-01 07:28:01 +00:00
tsc_file = TscFile.new(TscFile, mappath.read_bytes(), logging.getLogger("caver"))
2021-12-01 04:37:50 +00:00
for event, script in mapdata["pickups"].items():
2021-12-01 08:44:05 +00:00
TscFile.placeScriptAtEvent(tsc_file, script, event, mapname)
2021-12-01 04:37:50 +00:00
for event, song in mapdata["music"].items():
TscFile.placeSongAtCue(tsc_file, song["song_id"], event, song["original_id"], mapname)
for event, script in mapdata["entrances"].items():
2021-12-01 08:44:05 +00:00
needle = "<EVE...." # TODO: create a proper pattern
TscFile.placeScriptAtEvent(tsc_file, script, event, mapname, needle)
2021-12-01 07:28:01 +00:00
2021-12-01 08:44:05 +00:00
for event, hint in mapdata["hints"].items():
2021-12-06 08:20:16 +00:00
script = create_hint_script(hint["text"], hint.get("facepic", "0000") != "0000", hint.get("ending", "<END"))
2021-12-01 08:44:05 +00:00
TscFile.placeScriptAtEvent(tsc_file, script, event, mapname)
2021-12-01 04:37:50 +00:00
2021-12-01 07:28:01 +00:00
chars = TscFile.getText(tsc_file).values()
mappath.write_bytes(bytes(chars))
output_dir.joinpath("data", "Plaintext", f"{mapname}.txt").write_text(TscFile.getPlaintext(tsc_file))
2021-12-01 04:37:50 +00:00
2021-12-10 09:05:51 +00:00
def patch_other(filename: str, scripts: dict[str, str], TscFile, output_dir: Path):
filepath = output_dir.joinpath("data", f"{filename}.tsc")
tsc_file = TscFile.new(TscFile, filepath.read_bytes(), logging.getLogger("caver"))
for event, script in scripts.items():
TscFile.placeScriptAtEvent(tsc_file, script, event, filename)
chars = TscFile.getText(tsc_file).values()
filepath.write_bytes(bytes(chars))
output_dir.joinpath("data", "Plaintext", f"{filename}.txt").write_text(TscFile.getPlaintext(tsc_file))
2021-12-01 04:37:50 +00:00
def patch_mychar(mychar: Optional[str], output_dir: Path):
if mychar is None:
return
mychar_img = Path(mychar).read_bytes()
output_dir.joinpath("data", "MyChar.bmp").write_bytes(mychar_img)
def patch_hash(hash: list[int], output_dir: Path):
hash_strings = [f"{num:04d}" for num in hash]
hash_string = ",".join(hash_strings)
2021-12-01 08:44:05 +00:00
output_dir.joinpath("data", "hash.txt").write_text(hash_string)
2021-12-06 08:20:16 +00:00
def create_hint_script(text: str, facepic: bool, ending: str) -> str:
2021-12-01 08:44:05 +00:00
"""
A desperate attempt to generate valid <MSG text. Fills one text box (up to three lines). Attempts to wrap words elegantly.
"""
hard_limit = 35
2021-12-06 08:20:16 +00:00
msgbox_limit = 26 if facepic else hard_limit
2021-12-01 08:44:05 +00:00
pattern = r' [^ ]*$'
line1, line2, line3 = "", "", ""
split = 0
line1 = text[split:split+msgbox_limit]
match = next(re.finditer(pattern, line1), None)
if match is not None and len(text) > msgbox_limit:
2021-12-06 08:20:16 +00:00
line1 = line1[:match.start(0)]
split += match.start(0)+1
2021-12-01 08:44:05 +00:00
if split % hard_limit != 0:
line2 = "\r\n"
line2 += text[split:split+msgbox_limit]
match = next(re.finditer(pattern, line2), None)
if match is not None and len(text) > msgbox_limit*2:
2021-12-06 08:20:16 +00:00
line2 = line2[:match.start(0)]
2021-12-01 08:44:05 +00:00
if split % hard_limit != 0:
split -= 2
2021-12-06 08:20:16 +00:00
split += match.start(0)+1
2021-12-01 08:44:05 +00:00
if split % hard_limit != 0:
line3 = "\r\n"
line3 += text[split:split+msgbox_limit]
2021-12-06 08:20:16 +00:00
return f"<PRI<MSG<TUR{line1}{line2}{line3}<NOD{ending}"