mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-03 11:18:26 +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": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"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": {
|
"operation": {
|
||||||
"img-encode": {"encoding": "TPL.RGB5A3"}
|
"img-encode": {"encoding": "TPL.RGB5A3"}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"bmgtxt-edit": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"bmgtxt-edit": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"str-edit": {
|
"str-edit": {
|
||||||
"region": "5500"
|
"region": "5500"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"mode": "match-copy",
|
"mode": "match",
|
||||||
"match_regex": "*/*.thp"
|
"match_regex": "*/*.thp"
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "overwrite",
|
"mode": "replace",
|
||||||
"operation": {
|
"operation": {
|
||||||
"img-edit": {
|
"img-edit": {
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "overwrite",
|
"mode": "replace",
|
||||||
"operation": {
|
"operation": {
|
||||||
"img-edit": {
|
"img-edit": {
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "overwrite",
|
"mode": "replace",
|
||||||
"operation": {
|
"operation": {
|
||||||
"img-edit": {
|
"img-edit": {
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "overwrite",
|
"mode": "replace",
|
||||||
"operation": {
|
"operation": {
|
||||||
"img-edit": {
|
"img-edit": {
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "overwrite",
|
"mode": "replace",
|
||||||
"operation": {
|
"operation": {
|
||||||
"img-edit": {
|
"img-edit": {
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "overwrite",
|
"mode": "replace",
|
||||||
"operation": {
|
"operation": {
|
||||||
"img-edit": {
|
"img-edit": {
|
||||||
"layers": [
|
"layers": [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
|
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"bmgtxt-edit": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"bmgtxt-edit": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"bmgtxt-edit": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"bmgtxt-edit": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"mode": "edit",
|
"source": "game",
|
||||||
"operation": {
|
"operation": {
|
||||||
"bmg-decode": {},
|
"bmg-decode": {},
|
||||||
"bmgtxt-edit": {
|
"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
|
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"])
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}")')
|
||||||
|
|
Loading…
Reference in a new issue