mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-06 12:48:22 +02:00
added Patch prototype implementation (untested and unfinished)
This commit is contained in:
parent
b34833397d
commit
5481b75cbf
3 changed files with 446 additions and 67 deletions
|
@ -2205,7 +2205,7 @@
|
||||||
"version":"v2.1",
|
"version":"v2.1",
|
||||||
"family":1079,
|
"family":1079,
|
||||||
"tags":[
|
"tags":[
|
||||||
|
"DKR"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,8 +2,8 @@ from pathlib import Path
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from source.mkw.ModConfig import ModConfig
|
from source.mkw.ModConfig import ModConfig
|
||||||
|
from source.mkw.Patch import Patch
|
||||||
from source.wt import szs
|
from source.wt import szs
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class ExtractedGame:
|
class ExtractedGame:
|
||||||
|
@ -31,70 +31,6 @@ class ExtractedGame:
|
||||||
yield {"description": "Installing MyStuff directory...", "determinate": False}
|
yield {"description": "Installing MyStuff directory...", "determinate": False}
|
||||||
...
|
...
|
||||||
|
|
||||||
def install_file(self, mod_config: ModConfig, patch_directory: Path | str, subfile: Path | str) \
|
|
||||||
-> Generator[dict, None, None]:
|
|
||||||
"""
|
|
||||||
Install a file into the game
|
|
||||||
:param patch_directory: patch_directory where the subfile is located
|
|
||||||
:param subfile: subfile to install
|
|
||||||
:param mod_config: the mod to install
|
|
||||||
"""
|
|
||||||
subfile = Path(subfile)
|
|
||||||
yield {"description": f"Patch {patch_directory.name}\nInstalling {subfile.name}...", "determinate": False}
|
|
||||||
|
|
||||||
'''
|
|
||||||
configuration = {
|
|
||||||
"base": "/files/test.json", # path a another json file to use as a base
|
|
||||||
|
|
||||||
"mode": "copy", # copy, replace, ignore, edit the subfile [default: copy]
|
|
||||||
# if edit is set, use the file in the extracted game as a source
|
|
||||||
# replace can't be used inside szs
|
|
||||||
|
|
||||||
"replace_regex": None, # regex expression to match the file on the game to replace
|
|
||||||
"if": "True", # safe eval expression to check if the file should be installed
|
|
||||||
|
|
||||||
"operation": { # other operation for the file
|
|
||||||
"img-generate": {
|
|
||||||
# width, height, default color, ... can be determined from the base file
|
|
||||||
"format": "RGB", # type of the image
|
|
||||||
"layers": [
|
|
||||||
{"type": "image", ...},
|
|
||||||
{"type": "text", ...}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
"tpl-encode": {"encoding": "TPL.RGB565"}, # encode an image to a tpl with the given format
|
|
||||||
|
|
||||||
"bmg-replace": {
|
|
||||||
"mode": "regex" # regex or id
|
|
||||||
"template": {
|
|
||||||
"CWF": "{{ ONLINE_SERVICE }}", # regex type expression
|
|
||||||
"0x203F": "{{ ONLINE_SERVICE }}" # id type expression
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
|
|
||||||
configuration = { # default configuration
|
|
||||||
"mode": "copy",
|
|
||||||
"if": "True",
|
|
||||||
}
|
|
||||||
configuration_path = subfile.with_suffix(subfile.suffix + ".json")
|
|
||||||
if configuration_path.exists(): configuration |= json.loads(configuration_path.read_text(encoding="utf8"))
|
|
||||||
|
|
||||||
def install_patch(self, mod_config: ModConfig, patch_directory: Path | str) -> Generator[dict, None, None]:
|
|
||||||
"""
|
|
||||||
Install a patch into the game
|
|
||||||
:param mod_config: the mod to install
|
|
||||||
:param patch_directory: directory containing the patch
|
|
||||||
"""
|
|
||||||
patch_directory = Path(patch_directory)
|
|
||||||
yield {"description": f"Installing Patch {patch_directory.parent.name}...", "determinate": False}
|
|
||||||
|
|
||||||
for subfile in filter(lambda sf: sf.suffix == ".json", patch_directory.glob("*")):
|
|
||||||
self.install_file(mod_config, patch_directory, subfile)
|
|
||||||
|
|
||||||
def install_all_patch(self, mod_config: ModConfig) -> Generator[dict, None, None]:
|
def install_all_patch(self, mod_config: ModConfig) -> Generator[dict, None, None]:
|
||||||
"""
|
"""
|
||||||
Install all patchs of the mod_config into the game
|
Install all patchs of the mod_config into the game
|
||||||
|
@ -107,4 +43,4 @@ class ExtractedGame:
|
||||||
# for all the subdirectory named "_PATCH", apply the patch
|
# for all the subdirectory named "_PATCH", apply the patch
|
||||||
for part_directory in mod_config.get_mod_directory().glob("[!_]*"):
|
for part_directory in mod_config.get_mod_directory().glob("[!_]*"):
|
||||||
for patch_directory in part_directory.glob("_PATCH/"):
|
for patch_directory in part_directory.glob("_PATCH/"):
|
||||||
self.install_patch(mod_config, patch_directory)
|
yield from Patch(patch_directory).install(self)
|
||||||
|
|
443
source/mkw/Patch.py
Normal file
443
source/mkw/Patch.py
Normal file
|
@ -0,0 +1,443 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
from abc import abstractmethod, ABC
|
||||||
|
from typing import Generator, IO
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
class PathOutsidePatch(Exception):
|
||||||
|
def __init__(self, forbidden_path: Path, allowed_range: Path):
|
||||||
|
super().__init__(f"Error : path {forbidden_path} outside of allowed range {allowed_range}")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPatchMode(Exception):
|
||||||
|
def __init__(self, mode: str):
|
||||||
|
super().__init__(f"Error : mode \"{mode}\" is not implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPatchOperation(Exception):
|
||||||
|
def __init__(self, operation: str):
|
||||||
|
super().__init__(f"Error : operation \"{operation}\" is not implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidImageLayerType(Exception):
|
||||||
|
def __init__(self, layer_type: str):
|
||||||
|
super().__init__(f"Error : layer type \"{layer_type}\" is not implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class Patch:
|
||||||
|
"""
|
||||||
|
Represent a patch object
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path: Path | str):
|
||||||
|
self.path = Path(path)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} {self.path}>"
|
||||||
|
|
||||||
|
def install(self, extracted_game: "ExtractedGame") -> Generator[dict, None, None]:
|
||||||
|
"""
|
||||||
|
patch a game with this Patch
|
||||||
|
"""
|
||||||
|
# take all the files in the root directory, and patch them into the game.
|
||||||
|
# Patch is not directly applied to the root to avoid custom configuration
|
||||||
|
# on the base directory.
|
||||||
|
for file in self.path.iterdir():
|
||||||
|
pathname = file.relative_to(self.path)
|
||||||
|
yield from PatchDirectory(self, str(pathname)).install(extracted_game, extracted_game.path / pathname)
|
||||||
|
|
||||||
|
|
||||||
|
class PatchOperation:
|
||||||
|
"""
|
||||||
|
Represent an operation that can be applied onto a patch to modify it before installing
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_operation_by_name(cls, name: str):
|
||||||
|
match name:
|
||||||
|
case "img-generate":
|
||||||
|
return cls.ImageGenerator
|
||||||
|
case "tpl-encode":
|
||||||
|
return cls.TplConverter
|
||||||
|
case "bmg-replace":
|
||||||
|
return cls.BmgEditor
|
||||||
|
case _:
|
||||||
|
raise InvalidPatchOperation(name)
|
||||||
|
|
||||||
|
class Operation(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def patch(self, patch: "Patch", file_name: str, file_content: IO) -> (str, IO):
|
||||||
|
"""
|
||||||
|
patch a file and return the new file_path (if changed) and the new content of the file
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ImageGenerator(Operation):
|
||||||
|
"""
|
||||||
|
generate a new image based on a file and apply a generator on it
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, layers: list[dict]):
|
||||||
|
self.layers: list["Layer"] = [self.Layer(layer) for layer in layers]
|
||||||
|
|
||||||
|
def patch(self, patch: "Patch", file_name: str, file_content: IO) -> (str, IO):
|
||||||
|
image = Image.open(file_content)
|
||||||
|
|
||||||
|
for layer in self.layers:
|
||||||
|
image = layer.patch_image(patch, image)
|
||||||
|
|
||||||
|
patch_content = BytesIO()
|
||||||
|
image.save(patch_content, format="PNG")
|
||||||
|
patch_content.seek(0)
|
||||||
|
|
||||||
|
return file_name, file_content
|
||||||
|
|
||||||
|
class Layer(ABC):
|
||||||
|
"""
|
||||||
|
represent a layer for a image generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, layer: dict):
|
||||||
|
match layer["type"]:
|
||||||
|
case "color":
|
||||||
|
obj = ColorLayer
|
||||||
|
case "image":
|
||||||
|
obj = ImageLayer
|
||||||
|
case "text":
|
||||||
|
obj = TextLayer
|
||||||
|
case _:
|
||||||
|
raise InvalidImageLayerType(layer["type"])
|
||||||
|
|
||||||
|
return obj(**layer)
|
||||||
|
|
||||||
|
def get_bbox(self, image: Image.Image) -> tuple:
|
||||||
|
"""
|
||||||
|
return a tuple of a bbox from x1, x2, y1, y2
|
||||||
|
if float, calculate the position like a percentage on the image
|
||||||
|
if int, use directly the position
|
||||||
|
"""
|
||||||
|
if isinstance(x1 := self.x1, float): x1 = int(x1 * image.width)
|
||||||
|
if isinstance(y1 := self.y1, float): y1 = int(y1 * image.height)
|
||||||
|
if isinstance(x2 := self.x2, float): x2 = int(x2 * image.width)
|
||||||
|
if isinstance(y2 := self.y2, float): y2 = int(y2 * image.height)
|
||||||
|
|
||||||
|
return x1, y1, x2, y2
|
||||||
|
|
||||||
|
def get_bbox_size(self, image: Image.Image) -> tuple:
|
||||||
|
"""
|
||||||
|
return the size that a layer use on the image
|
||||||
|
"""
|
||||||
|
x1, y1, x2, y2 = self.get_bbox(image)
|
||||||
|
return x2 - x1, y2 - y1
|
||||||
|
|
||||||
|
def get_font_size(self, image: Image.Image) -> int:
|
||||||
|
"""
|
||||||
|
return the font_size of a layer
|
||||||
|
"""
|
||||||
|
return int(self.font_size * image.height) if isinstance(self.font_size, float) else self.font_size
|
||||||
|
|
||||||
|
def get_layer_position(self, image: Image.Image) -> tuple:
|
||||||
|
"""
|
||||||
|
return a tuple of the x and y position
|
||||||
|
if x / y is a float, calculate the position like a percentage on the image
|
||||||
|
if x / y is an int, use directly the position
|
||||||
|
"""
|
||||||
|
if isinstance(x := self.x, float): x = int(x * image.width)
|
||||||
|
if isinstance(y := self.y, float): y = int(y * image.height)
|
||||||
|
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def patch_image(self, patch: "Patch", image: Image.Image) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Patch an image with the actual layer. Return the new image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class ColorLayer(Layer):
|
||||||
|
"""
|
||||||
|
Represent a layer that fill a rectangle with a certain color on the image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, color: tuple[int] = (0,), x1: int | float = 0, y1: int | float = 0, x2: int | float = 1,
|
||||||
|
y2: int | float = 1):
|
||||||
|
self.x1: int = x1
|
||||||
|
self.y1: int = y1
|
||||||
|
self.x2: int = x2
|
||||||
|
self.y2: int = y2
|
||||||
|
self.color: tuple[int] = tuple(color)
|
||||||
|
|
||||||
|
def patch_image(self, patch: "Patch", image: Image.Image):
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
draw.rectangle(self.get_bbox(image), self.color)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
class ImageLayer(Layer):
|
||||||
|
"""
|
||||||
|
Represent a layer that paste an image on the image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, image_path: str, x1: int | float = 0, y1: int | float = 0, x2: int | float = 1,
|
||||||
|
y2: int | float = 1):
|
||||||
|
self.x1: int = x1
|
||||||
|
self.y1: int = y1
|
||||||
|
self.x2: int = x2
|
||||||
|
self.y2: int = y2
|
||||||
|
self.image_path: str = image_path
|
||||||
|
|
||||||
|
def patch(self, patch: "Patch", image: Image.Image) -> Image.Image:
|
||||||
|
# check if the path is outside of the allowed directory
|
||||||
|
layer_image_path = patch.path / self.image_path
|
||||||
|
if not layer_image_path.is_relative_to(patch.path): raise PathOutsidePatch(layer_image_path, patch.path)
|
||||||
|
|
||||||
|
layer_image = Image.open(layer_image_path).resize(self.get_bbox_size(image)).convert("RGBA")
|
||||||
|
|
||||||
|
image.paste(
|
||||||
|
layer_image,
|
||||||
|
box=self.get_bbox(image),
|
||||||
|
mask=layer_image
|
||||||
|
)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
class TextLayer:
|
||||||
|
"""
|
||||||
|
Represent a layer that write a text on the image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str, font_path: str | None = None, font_size: int = 10, color: tuple[int] = (255,),
|
||||||
|
x: int | float = 0, y: int | float = 0):
|
||||||
|
self.x: int = x
|
||||||
|
self.y: int = y
|
||||||
|
self.font_path: str | None = font_path
|
||||||
|
self.font_size: int = font_size
|
||||||
|
self.color: tuple[int] = tuple(color)
|
||||||
|
self.text: str = text
|
||||||
|
|
||||||
|
def patch_image(self, patch: "Patch", image: Image.Image) -> Image.Image:
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
|
if self.font_path is not None:
|
||||||
|
font_image_path = patch.path / self.font_path
|
||||||
|
if not font_image_path.is_relative_to(patch.path):
|
||||||
|
raise PathOutsidePatch(font_image_path, patch.path)
|
||||||
|
else:
|
||||||
|
font_image_path = None
|
||||||
|
|
||||||
|
font = ImageFont.truetype(font=font_image_path, size=self.get_font_size(image))
|
||||||
|
draw.text(self.get_layer_position(layer), text=self.text, fill=self.color, font=font)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
class TplConverter(Operation):
|
||||||
|
"""
|
||||||
|
convert an image to a tpl file
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self): ...
|
||||||
|
|
||||||
|
def patch(self, patch: "Patch", file_name: str, file_content: IO) -> (str, IO): ...
|
||||||
|
|
||||||
|
class BmgEditor(Operation):
|
||||||
|
"""
|
||||||
|
edit a bmg
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self): ...
|
||||||
|
|
||||||
|
def patch(self, patch: "Patch", file_name: str, file_content: IO) -> (str, IO): ...
|
||||||
|
|
||||||
|
|
||||||
|
class PatchObject:
|
||||||
|
"""
|
||||||
|
Represent an object inside a patch
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, patch: Patch, subpath: str):
|
||||||
|
self.patch = patch
|
||||||
|
self.subpath = subpath
|
||||||
|
self._configuration = None
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} {self.full_path}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_path(self) -> Path:
|
||||||
|
return self.patch.path / self.subpath
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configuration(self) -> dict:
|
||||||
|
"""
|
||||||
|
return the configuration from the file
|
||||||
|
"""
|
||||||
|
if self._configuration is not None: return self._configuration
|
||||||
|
|
||||||
|
# default configuration
|
||||||
|
self._configuration = {
|
||||||
|
"mode": "copy",
|
||||||
|
"if": "True",
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration_path = self.full_path.with_suffix(self.full_path.suffix + ".json")
|
||||||
|
if not configuration_path.exists(): return self._configuration
|
||||||
|
|
||||||
|
self._configuration |= json.loads(configuration_path.read_text(encoding="utf8"))
|
||||||
|
|
||||||
|
# if configuration inherit from an another file, then load it from the patch root,
|
||||||
|
# keep this configuration keys over inherited one.
|
||||||
|
# pop "base" to avoid infinite loop
|
||||||
|
while "base" in self._configuration:
|
||||||
|
self._configuration |= json.loads(
|
||||||
|
(self.patch.path / self._configuration.pop("base")).read_text(encoding="utf8"))
|
||||||
|
|
||||||
|
return self._configuration
|
||||||
|
|
||||||
|
def subfile_from_path(self, path: Path) -> "PatchObject":
|
||||||
|
"""
|
||||||
|
return a PatchObject from a path
|
||||||
|
"""
|
||||||
|
obj = PatchDirectory if path.is_dir() else PatchFile
|
||||||
|
return obj(self.patch, str(path.relative_to(self.patch.path)))
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]:
|
||||||
|
"""
|
||||||
|
install the PatchObject into the game
|
||||||
|
yield the step of the process
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PatchFile(PatchObject):
|
||||||
|
"""
|
||||||
|
Represent a file from a patch
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_source_path(self, game_subpath: Path):
|
||||||
|
"""
|
||||||
|
Return the path of the file that the patch is applied on.
|
||||||
|
If the configuration mode is set to "edit", then return the path of the file inside the Game
|
||||||
|
Else, return the path of the file inside the Patch
|
||||||
|
"""
|
||||||
|
match self.configuration["mode"]:
|
||||||
|
case "edit":
|
||||||
|
return game_subpath
|
||||||
|
case _:
|
||||||
|
return self.full_path
|
||||||
|
|
||||||
|
def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]:
|
||||||
|
"""
|
||||||
|
patch a subfile of the game with the PatchFile
|
||||||
|
"""
|
||||||
|
yield {"description": f"Patching {self}"}
|
||||||
|
|
||||||
|
if self.full_path.name.startswith("#"):
|
||||||
|
print(f"special file : {self} [install to {game_subpath}]")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not safe_eval(self.configuration["if"]): return
|
||||||
|
|
||||||
|
# apply operation on the file
|
||||||
|
patch_source: Path = self.get_source_path(game_subpath)
|
||||||
|
patch_name: str = game_subpath.name
|
||||||
|
patch_content: IO = open(patch_source, "rb")
|
||||||
|
|
||||||
|
for operation_name, operation in self.configuration.get("operation", {}).items():
|
||||||
|
# process every operation and get the new patch_path (if the name is changed)
|
||||||
|
# and the new content of the patch
|
||||||
|
patch_name, patch_content = PatchOperation.get_operation_by_name(operation_name)(*operation).patch(
|
||||||
|
self.patch, patch_name, patch_content
|
||||||
|
)
|
||||||
|
|
||||||
|
match self.configuration["mode"]:
|
||||||
|
# if the mode is copy, replace the subfile in the game by the PatchFile
|
||||||
|
case "copy" | "edit":
|
||||||
|
print(f"[copy] copying {self} to {game_subpath}")
|
||||||
|
|
||||||
|
with open(game_subpath.parent / patch_name, "wb") as file:
|
||||||
|
file.write(patch_content.read())
|
||||||
|
|
||||||
|
# if the mode is match, replace all the subfiles that match match_regex by the PatchFile
|
||||||
|
case "match":
|
||||||
|
for game_subfile in game_subpath.parent.glob(self.configuration["match_regex"]):
|
||||||
|
# disallow patching files outside of the game
|
||||||
|
if not game_subfile.relative_to(extracted_game.path):
|
||||||
|
raise PathOutsidePatch(game_subfile, extracted_game.path)
|
||||||
|
# patch the game with the subpatch
|
||||||
|
print(f"[match] copying {self} to {game_subfile}")
|
||||||
|
|
||||||
|
with open(game_subfile.parent / patch_name, "wb") as file:
|
||||||
|
file.write(patch_content.read())
|
||||||
|
|
||||||
|
# else raise an error
|
||||||
|
case _:
|
||||||
|
raise InvalidPatchMode(self.configuration["mode"])
|
||||||
|
|
||||||
|
patch_content.close()
|
||||||
|
|
||||||
|
|
||||||
|
class PatchDirectory(PatchObject):
|
||||||
|
"""
|
||||||
|
Represent a directory from a patch
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subpatchs(self) -> Generator["PatchObject", None, None]:
|
||||||
|
"""
|
||||||
|
return all the subpatchs inside the PatchDirectory
|
||||||
|
"""
|
||||||
|
for subpath in self.full_path.iterdir():
|
||||||
|
if subpath.suffix == ".json": continue
|
||||||
|
yield self.subfile_from_path(subpath)
|
||||||
|
|
||||||
|
def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]:
|
||||||
|
"""
|
||||||
|
patch a subdirectory of the game with the PatchDirectory
|
||||||
|
"""
|
||||||
|
yield {"description": f"Patching {self}"}
|
||||||
|
|
||||||
|
if not safe_eval(self.configuration["if"]): return
|
||||||
|
|
||||||
|
match self.configuration["mode"]:
|
||||||
|
# if the mode is copy, then simply patch the subfile into the game with the same path
|
||||||
|
case "copy":
|
||||||
|
for subpatch in self.subpatchs:
|
||||||
|
# install the subfile in the directory with the same name as the PatchDirectory
|
||||||
|
yield from subpatch.install(extracted_game, game_subpath / subpatch.full_path.name)
|
||||||
|
|
||||||
|
# if the mode is replace, patch all the files that match the replace_regex
|
||||||
|
case "match":
|
||||||
|
for subpatch in self.subpatchs:
|
||||||
|
# install the subfile in all the directory that match the match_regex of the configuration
|
||||||
|
for game_subfile in game_subpath.parent.glob(self.configuration["match_regex"]):
|
||||||
|
# disallow patching files outside of the game
|
||||||
|
if not game_subfile.relative_to(extracted_game.path):
|
||||||
|
raise PathOutsidePatch(game_subfile, extracted_game.path)
|
||||||
|
# patch the game with the subpatch
|
||||||
|
yield from subpatch.install(extracted_game, game_subfile / subpatch.full_path.name)
|
||||||
|
|
||||||
|
# else raise an error
|
||||||
|
case _:
|
||||||
|
raise InvalidPatchMode(self.configuration["mode"])
|
||||||
|
|
||||||
|
|
||||||
|
# TODO : extract SZS
|
||||||
|
# TODO : implement TPL
|
||||||
|
# TODO : implement BMG
|
||||||
|
# TODO : safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
configuration_example = {
|
||||||
|
"operation": { # other operation for the file
|
||||||
|
"tpl-encode": {"encoding": "TPL.RGB565"}, # encode an image to a tpl with the given format
|
||||||
|
|
||||||
|
"bmg-replace": {
|
||||||
|
"mode": "regex", # regex or id
|
||||||
|
"template": {
|
||||||
|
"CWF": "{{ ONLINE_SERVICE }}", # regex type expression
|
||||||
|
"0x203F": "{{ ONLINE_SERVICE }}" # id type expression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue