mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-02 18:58:27 +02:00
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:
parent
781e564dd2
commit
6ecf752b6d
30 changed files with 131 additions and 100 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"mode": "overwrite"
|
||||
"mode": "replace"
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"mode": "overwrite"
|
||||
"mode": "replace"
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"mode": "overwrite"
|
||||
"mode": "replace"
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"mode": "overwrite"
|
||||
"mode": "replace"
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"mode": "overwrite"
|
||||
"mode": "replace"
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"mode": "overwrite"
|
||||
"mode": "replace"
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-encode": {"encoding": "TPL.RGB5A3"}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"str-edit": {
|
||||
"region": "5500"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"mode": "match-copy",
|
||||
"mode": "match",
|
||||
"match_regex": "*/*.thp"
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-edit": {
|
||||
"layers": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-edit": {
|
||||
"layers": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-edit": {
|
||||
"layers": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-edit": {
|
||||
"layers": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-edit": {
|
||||
"layers": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "overwrite",
|
||||
"mode": "replace",
|
||||
"operation": {
|
||||
"img-edit": {
|
||||
"layers": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"mode": "edit",
|
||||
"source": "game",
|
||||
"operation": {
|
||||
"bmg-decode": {},
|
||||
"bmgtxt-edit": {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,12 +26,10 @@ class PatchDirectory(PatchObject):
|
|||
"""
|
||||
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(
|
||||
self.configuration["if"],
|
||||
env={"extracted_game": extracted_game}
|
||||
) is not True: return
|
||||
# check if the directory should be patched
|
||||
if not self.is_enabled(extracted_game): return
|
||||
|
||||
match self.configuration["mode"]:
|
||||
# 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
|
||||
case _:
|
||||
raise InvalidPatchMode(self.configuration["mode"])
|
||||
raise InvalidPatchMode(self, self.configuration["mode"])
|
||||
|
|
|
@ -2,7 +2,7 @@ from io import BytesIO
|
|||
from pathlib import Path
|
||||
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.PatchObject import PatchObject
|
||||
from source.wt.szs import SZSPath
|
||||
|
@ -16,46 +16,27 @@ class PatchFile(PatchObject):
|
|||
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.
|
||||
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
|
||||
"""
|
||||
match self.configuration["mode"]:
|
||||
case "edit":
|
||||
return game_subpath
|
||||
case _:
|
||||
return self.full_path
|
||||
match self.configuration["source"]:
|
||||
case "patch": return self.full_path
|
||||
case "game": return game_subpath
|
||||
case _: raise InvalidSourceMode(self, self.configuration["course"])
|
||||
|
||||
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_name: str = game_subpath.name
|
||||
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
|
||||
|
||||
for operation_name, operation in self.configuration.get("operation", {}).items():
|
||||
|
@ -65,42 +46,90 @@ class PatchFile(PatchObject):
|
|||
self.patch, patch_name, patch_content
|
||||
)
|
||||
|
||||
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.
|
||||
:param destination: file where the content will be written
|
||||
:param patch_content: content of the file to write
|
||||
"""
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(destination, "wb") as file:
|
||||
patch_content.seek(0)
|
||||
file.write(patch_content.read())
|
||||
return patch_name, patch_content
|
||||
|
||||
@staticmethod
|
||||
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.
|
||||
:param destination: file where the content will be written
|
||||
:param patch_content: content of the file to write
|
||||
"""
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(destination, "wb") as file:
|
||||
patch_content.seek(0)
|
||||
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"]:
|
||||
# if the mode is copy, replace the subfile in the game by the PatchFile
|
||||
case "copy" | "edit":
|
||||
write_patch(game_subpath.parent / patch_name, patch_content)
|
||||
case "copy":
|
||||
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
|
||||
case "overwrite":
|
||||
# if the mode is replace, only write if the file existed before
|
||||
case "replace":
|
||||
patch_name, patch_content = self.get_patched_file(game_subpath)
|
||||
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
|
||||
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"]):
|
||||
# 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
|
||||
write_patch(game_subfile, patch_content)
|
||||
yield {"description": f"Patching {game_subfile}"}
|
||||
|
||||
# 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
|
||||
case "ignore": pass
|
||||
|
||||
# else raise an error
|
||||
case _:
|
||||
raise InvalidPatchMode(self.configuration["mode"])
|
||||
|
||||
patch_content.close()
|
||||
case _: raise InvalidPatchMode(self, self.configuration["mode"])
|
||||
|
|
|
@ -34,8 +34,9 @@ class PatchObject(ABC):
|
|||
|
||||
# default configuration
|
||||
self._configuration = {
|
||||
"mode": "copy",
|
||||
"if": "True",
|
||||
"mode": "copy", # mode on how should the file be patched
|
||||
"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")
|
||||
|
@ -68,3 +69,12 @@ class PatchObject(ABC):
|
|||
install the PatchObject into the game
|
||||
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
|
||||
|
|
|
@ -3,13 +3,19 @@ from typing import TYPE_CHECKING
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from source.mkw.Patch import Patch
|
||||
from source.mkw.Patch.PatchObject import PatchObject
|
||||
|
||||
|
||||
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}")
|
||||
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")
|
||||
def __init__(self, patch: "PatchObject", mode: str):
|
||||
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}")')
|
||||
|
|
Loading…
Reference in a new issue