reorganised PatchObject, PatchFile and PatchDirectory to make them more readable. mode="edit" have been replaced by source="game" | "patch". "overwrite" -> "replace".

This commit is contained in:
Faraphel 2022-08-14 21:50:00 +02:00
parent 781e564dd2
commit 6ecf752b6d
30 changed files with 131 additions and 100 deletions

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,3 +1,3 @@
{ {
"mode": "overwrite" "mode": "replace"
} }

View file

@ -1,3 +1,3 @@
{ {
"mode": "overwrite" "mode": "replace"
} }

View file

@ -1,3 +1,3 @@
{ {
"mode": "overwrite" "mode": "replace"
} }

View file

@ -1,3 +1,3 @@
{ {
"mode": "overwrite" "mode": "replace"
} }

View file

@ -1,3 +1,3 @@
{ {
"mode": "overwrite" "mode": "replace"
} }

View file

@ -1,3 +1,3 @@
{ {
"mode": "overwrite" "mode": "replace"
} }

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-encode": {"encoding": "TPL.RGB5A3"} "img-encode": {"encoding": "TPL.RGB5A3"}
} }

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"str-edit": { "str-edit": {
"region": "5500" "region": "5500"

View file

@ -1,4 +1,4 @@
{ {
"mode": "match-copy", "mode": "match",
"match_regex": "*/*.thp" "match_regex": "*/*.thp"
} }

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-edit": { "img-edit": {
"layers": [ "layers": [

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-edit": { "img-edit": {
"layers": [ "layers": [

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-edit": { "img-edit": {
"layers": [ "layers": [

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-edit": { "img-edit": {
"layers": [ "layers": [

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-edit": { "img-edit": {
"layers": [ "layers": [

View file

@ -1,5 +1,5 @@
{ {
"mode": "overwrite", "mode": "replace",
"operation": { "operation": {
"img-edit": { "img-edit": {
"layers": [ "layers": [

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,5 +1,5 @@
{ {
"mode": "edit", "source": "game",
"operation": { "operation": {
"bmg-decode": {}, "bmg-decode": {},
"bmgtxt-edit": { "bmgtxt-edit": {

View file

@ -1,12 +0,0 @@
{
"mode": "match-edit",
"match_regex": "*.szs",
"operation": {
"szs-edit": {
"scale": {"x": 2, "y": 2, "z": 2},
"speed": 1.5,
"laps": 9
}
}
}

View file

@ -26,12 +26,10 @@ class PatchDirectory(PatchObject):
""" """
patch a subdirectory of the game with the PatchDirectory patch a subdirectory of the game with the PatchDirectory
""" """
yield {"description": f"Patching {self}"} yield {"description": f"Patching {game_subpath}"}
if self.patch.mod_config.safe_eval( # check if the directory should be patched
self.configuration["if"], if not self.is_enabled(extracted_game): return
env={"extracted_game": extracted_game}
) is not True: return
match self.configuration["mode"]: match self.configuration["mode"]:
# if the mode is copy, then simply patch the subfile into the game with the same path # if the mode is copy, then simply patch the subfile into the game with the same path
@ -59,4 +57,4 @@ class PatchDirectory(PatchObject):
# else raise an error # else raise an error
case _: case _:
raise InvalidPatchMode(self.configuration["mode"]) raise InvalidPatchMode(self, self.configuration["mode"])

View file

@ -2,7 +2,7 @@ from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Generator, IO, TYPE_CHECKING from typing import Generator, IO, TYPE_CHECKING
from source.mkw.Patch import PathOutsidePatch, InvalidPatchMode from source.mkw.Patch import PathOutsidePatch, InvalidPatchMode, InvalidSourceMode
from source.mkw.Patch.PatchOperation import AbstractPatchOperation from source.mkw.Patch.PatchOperation import AbstractPatchOperation
from source.mkw.Patch.PatchObject import PatchObject from source.mkw.Patch.PatchObject import PatchObject
from source.wt.szs import SZSPath from source.wt.szs import SZSPath
@ -16,46 +16,27 @@ class PatchFile(PatchObject):
Represent a file from a patch Represent a file from a patch
""" """
def get_source_path(self, game_subpath: Path): def get_source_path(self, game_subpath: Path) -> Path:
""" """
Return the path of the file that the patch is applied on. 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 If the configuration mode is set to "game", then return the path of the file inside the Game
Else, return the path of the file inside the Patch Else, return the path of the file inside the Patch
""" """
match self.configuration["mode"]: match self.configuration["source"]:
case "edit": case "patch": return self.full_path
return game_subpath case "game": return game_subpath
case _: case _: raise InvalidSourceMode(self, self.configuration["course"])
return self.full_path
def install(self, extracted_game: "ExtractedGame", game_subpath: Path) -> Generator[dict, None, None]: def get_patched_file(self, game_subpath: Path) -> (str, BytesIO):
""" """
patch a subfile of the game with the PatchFile Return the name and the content of the patched file
:param game_subpath: path to the game subfile
:return: the name and the content of the patched file
""" """
yield {"description": f"Patching {self}"}
# check if the file should be patched considering the "if" configuration
if self.patch.mod_config.safe_eval(
self.configuration["if"],
env={"extracted_game": extracted_game}
) is not True: return
# check if the path to the game_subpath is inside a szs, and if yes extract it
for szs_subpath in filter(lambda path: path.suffix == ".d",
game_subpath.relative_to(extracted_game.path).parents):
szs_path = extracted_game.path / szs_subpath
# if the archive is already extracted, ignore
if not szs_path.exists():
# if the szs file in the game exists, extract it
if szs_path.with_suffix(".szs").exists():
SZSPath(szs_path.with_suffix(".szs")).extract_all(szs_path)
# apply operation on the file
patch_source: Path = self.get_source_path(game_subpath) patch_source: Path = self.get_source_path(game_subpath)
patch_name: str = game_subpath.name patch_name: str = game_subpath.name
patch_content: BytesIO = BytesIO(open(patch_source, "rb").read()) patch_content: BytesIO = BytesIO(open(patch_source, "rb").read())
# the file is converted into a BytesIO because if the mode is "edit", # the file is converted into a BytesIO because if the source is "game",
# the file is overwritten and if the content is untouched, the file will only be lost # the file is overwritten and if the content is untouched, the file will only be lost
for operation_name, operation in self.configuration.get("operation", {}).items(): for operation_name, operation in self.configuration.get("operation", {}).items():
@ -65,6 +46,9 @@ class PatchFile(PatchObject):
self.patch, patch_name, patch_content self.patch, patch_name, patch_content
) )
return patch_name, patch_content
@staticmethod
def write_patch(destination: Path, patch_content: IO) -> None: def write_patch(destination: Path, patch_content: IO) -> None:
""" """
Write a patch content to the destination. Automatically create the directory and seek to the start. Write a patch content to the destination. Automatically create the directory and seek to the start.
@ -76,31 +60,76 @@ class PatchFile(PatchObject):
patch_content.seek(0) patch_content.seek(0)
file.write(patch_content.read()) file.write(patch_content.read())
@staticmethod
def check_szs(extracted_game: "ExtractedGame", game_subpath: Path) -> None:
"""
Check if game path is inside a szs archive. If yes, extract it.
:param extracted_game: the extracted game object
:param game_subpath: path to the game file that is being patched
"""
for szs_subpath in filter(lambda path: path.suffix == ".d",
game_subpath.relative_to(extracted_game.path).parents):
szs_path = extracted_game.path / szs_subpath
# if the archive is not already extracted and the szs file in the game exists, extract it
if not szs_path.exists() and szs_path.with_suffix(".szs").exists():
SZSPath(szs_path.with_suffix(".szs")).extract_all(szs_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 {game_subpath}"}
# check if the file should be patched
if not self.is_enabled(extracted_game): return
# check if it is patching a szs archive. If yes, extract it.
self.check_szs(extracted_game, game_subpath)
# apply operation on the file
match self.configuration["mode"]: match self.configuration["mode"]:
# if the mode is copy, replace the subfile in the game by the PatchFile # if the mode is copy, replace the subfile in the game by the PatchFile
case "copy" | "edit": case "copy":
write_patch(game_subpath.parent / patch_name, patch_content) patch_name, patch_content = self.get_patched_file(game_subpath)
self.write_patch(game_subpath.parent / patch_name, patch_content)
patch_content.close()
# if the mode is overwrite, only write if the file existed before # if the mode is replace, only write if the file existed before
case "overwrite": case "replace":
patch_name, patch_content = self.get_patched_file(game_subpath)
if (game_subpath.parent / patch_name).exists(): if (game_subpath.parent / patch_name).exists():
write_patch(game_subpath.parent / patch_name, patch_content) self.write_patch(game_subpath.parent / patch_name, patch_content)
patch_content.close()
# if the mode is match, replace all the subfiles that match match_regex by the PatchFile # if the mode is match, replace all the subfiles that match match_regex by the PatchFile
case "match": case "match":
patch_content: BytesIO = BytesIO()
# if the source is the patch, then directly calculate the patch content
# so that it is not recalculated for every file with no reason
if self.configuration["source"] == "patch":
_, patch_content = self.get_patched_file(game_subpath)
for game_subfile in game_subpath.parent.glob(self.configuration["match_regex"]): for game_subfile in game_subpath.parent.glob(self.configuration["match_regex"]):
# disallow patching files outside of the game # disallow patching files outside of the game
if not game_subfile.relative_to(extracted_game.path): if not game_subfile.relative_to(extracted_game.path):
raise PathOutsidePatch(game_subfile, extracted_game.path) raise PathOutsidePatch(game_subfile, extracted_game.path)
# patch the game with the subpatch yield {"description": f"Patching {game_subfile}"}
write_patch(game_subfile, patch_content)
# if the source is the game, then recalculate the content for every game subfile
if self.configuration["source"] == "game":
_, patch_content = self.get_patched_file(game_subfile)
# patch the game with the patch content
self.write_patch(game_subfile, patch_content)
patch_content.close()
# ignore if mode is "ignore", useful if the file is used as a resource for an operation # ignore if mode is "ignore", useful if the file is used as a resource for an operation
case "ignore": pass case "ignore": pass
# else raise an error # else raise an error
case _: case _: raise InvalidPatchMode(self, self.configuration["mode"])
raise InvalidPatchMode(self.configuration["mode"])
patch_content.close()

View file

@ -34,8 +34,9 @@ class PatchObject(ABC):
# default configuration # default configuration
self._configuration = { self._configuration = {
"mode": "copy", "mode": "copy", # mode on how should the file be patched
"if": "True", "source": "patch", # source of the file data that will be modified
"if": "True", # condition for the patch to be applied
} }
configuration_path = self.full_path.with_suffix(self.full_path.suffix + ".json") configuration_path = self.full_path.with_suffix(self.full_path.suffix + ".json")
@ -68,3 +69,12 @@ class PatchObject(ABC):
install the PatchObject into the game install the PatchObject into the game
yield the step of the process yield the step of the process
""" """
...
def is_enabled(self, extracted_game: "ExtractedGame") -> bool:
"""
Return if the patch object is actually enabled
:param extracted_game: the extracted game object
:return: should the patch be applied ?
"""
return self.patch.mod_config.safe_eval(self.configuration["if"], env={"extracted_game": extracted_game}) is True

View file

@ -3,13 +3,19 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from source.mkw.Patch import Patch from source.mkw.Patch import Patch
from source.mkw.Patch.PatchObject import PatchObject
class PathOutsidePatch(Exception): class PathOutsidePatch(Exception):
def __init__(self, forbidden_path: Path, allowed_range: Path): def __init__(self, forbidden_path: Path, allowed_range: Path):
super().__init__(f"Error : path {forbidden_path} outside of allowed range {allowed_range}") super().__init__(f'Error : path "{forbidden_path}" outside of allowed range {allowed_range}')
class InvalidPatchMode(Exception): class InvalidPatchMode(Exception):
def __init__(self, mode: str): def __init__(self, patch: "PatchObject", mode: str):
super().__init__(f"Error : mode \"{mode}\" is not implemented") super().__init__(f'Error : mode "{mode}" is not implemented (in patch : "{patch.full_path}")')
class InvalidSourceMode(Exception):
def __init__(self, patch: "PatchObject", source: str):
super().__init__(f'Error : source "{source}" is not implemented (in patch : "{patch.full_path}")')