mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
154 lines
5.0 KiB
Python
154 lines
5.0 KiB
Python
from collections.abc import Callable
|
|
from enum import IntEnum
|
|
from re import match
|
|
from typing import Any, List, Set, cast
|
|
|
|
from mobase import (
|
|
FileTreeEntry,
|
|
IFileTree,
|
|
IOrganizer,
|
|
ModDataChecker,
|
|
ModDataContent,
|
|
ReleaseType,
|
|
VersionInfo,
|
|
)
|
|
|
|
from ..basic_game import BasicGame
|
|
|
|
|
|
class Content(IntEnum):
|
|
PACKAGE = 0
|
|
SCRIPT = 1
|
|
|
|
|
|
class ValidationResult(IntEnum):
|
|
INVALID = 0
|
|
VALID = 1
|
|
FIXABLE = 2
|
|
|
|
|
|
class TS4Game(BasicGame):
|
|
Name = "The Sims 4 Support Plugin"
|
|
Author = "R3z Shark, xieve"
|
|
GameName = "The Sims 4"
|
|
GameShortName = "thesims4"
|
|
GameBinary = "Game/Bin/TS4_x64.exe"
|
|
GameDataPath = "%DOCUMENTS%/Electronic Arts/The Sims 4/Mods"
|
|
GameSteamId = 1222670
|
|
GameOriginManifestIds = ["OFB-EAST:109552677"]
|
|
GameOriginWatcherExecutables = ("TS4_x64.exe",)
|
|
|
|
def version(self):
|
|
return VersionInfo(1, 0, 0, ReleaseType.FINAL)
|
|
|
|
def documentsDirectory(self):
|
|
return self.dataDirectory()
|
|
|
|
def init(self, organizer: IOrganizer):
|
|
if super().init(organizer):
|
|
self._register_feature(TS4ModDataChecker())
|
|
self._register_feature(TS4ModDataContent())
|
|
return True
|
|
return False
|
|
|
|
|
|
class TS4ModDataChecker(ModDataChecker):
|
|
# .package files are allowed at a maximum depth of 5 subfolders, script files can be at most one level deep
|
|
# The first capturing group lazily captures any parent folders exceeding that depth, see below
|
|
_fixableOrValid = r"(?i)^(.*?)((?:[^\\]+\\){0,5}[^\\]*\.package|(?:[^\\]+\\)?[^\\]*\.ts4script|(?:[^\\]+\\)scripts\\.*\.py)$"
|
|
|
|
def dataLooksValid(self, filetree: IFileTree) -> ModDataChecker.CheckReturn:
|
|
return cast(
|
|
ModDataChecker.CheckReturn,
|
|
self._fixOrValidateTree(filetree, validateMode=True),
|
|
)
|
|
|
|
def fix(self, filetree: IFileTree) -> IFileTree | None:
|
|
return cast(IFileTree, self._fixOrValidateTree(filetree))
|
|
|
|
def _fixOrValidateTree(
|
|
self,
|
|
tree: IFileTree,
|
|
validateMode: bool = False,
|
|
) -> IFileTree | ModDataChecker.CheckReturn:
|
|
validationResult = ValidationResult.INVALID
|
|
checkReturn = ModDataChecker.INVALID
|
|
walkReturn = IFileTree.CONTINUE
|
|
|
|
def setValidationResult(
|
|
newResult: ValidationResult = ValidationResult.INVALID,
|
|
actionCallback: Callable[[], Any] = lambda: None,
|
|
):
|
|
nonlocal validationResult
|
|
nonlocal checkReturn
|
|
nonlocal walkReturn
|
|
if validateMode:
|
|
validationResult = max(validationResult, newResult)
|
|
match validationResult:
|
|
case ValidationResult.INVALID:
|
|
pass
|
|
case ValidationResult.VALID:
|
|
checkReturn = ModDataChecker.VALID
|
|
case ValidationResult.FIXABLE:
|
|
checkReturn = ModDataChecker.FIXABLE
|
|
walkReturn = IFileTree.STOP
|
|
else:
|
|
actionCallback()
|
|
|
|
def fixOrValidateEntry(
|
|
parentPath: str, entry: FileTreeEntry
|
|
) -> IFileTree.WalkReturn:
|
|
path = f"{parentPath}{entry.name()}"
|
|
fixableOrValid = match(self._fixableOrValid, path)
|
|
if fixableOrValid:
|
|
match fixableOrValid.groups():
|
|
case ["", _]:
|
|
setValidationResult(ValidationResult.VALID)
|
|
case [_, innerPath]:
|
|
# Move files that are nested too deeply up, preserving the inner folder structure
|
|
# E.g. a/b/c.ts4script will be moved to b/c.ts4script
|
|
setValidationResult(
|
|
ValidationResult.FIXABLE,
|
|
lambda: tree.move(entry, innerPath),
|
|
)
|
|
case _:
|
|
pass
|
|
return walkReturn
|
|
|
|
tree.walk(fixOrValidateEntry)
|
|
|
|
if validateMode:
|
|
return checkReturn
|
|
else:
|
|
return tree
|
|
|
|
|
|
class TS4ModDataContent(ModDataContent):
|
|
def getAllContents(self: ModDataContent) -> List[ModDataContent.Content]:
|
|
return [
|
|
ModDataContent.Content(
|
|
Content.PACKAGE, "Package", ":/MO/gui/content/plugin"
|
|
),
|
|
ModDataContent.Content(Content.SCRIPT, "Script", ":/MO/gui/content/script"),
|
|
]
|
|
|
|
def getContentsFor(self: ModDataContent, filetree: IFileTree) -> List[int]:
|
|
contents: Set[int] = set()
|
|
|
|
def getContentForEntry(path: str, entry: FileTreeEntry):
|
|
nonlocal contents
|
|
match entry.suffix():
|
|
case "package":
|
|
contents.add(Content.PACKAGE)
|
|
case "ts4script" | "py":
|
|
contents.add(Content.SCRIPT)
|
|
case _:
|
|
pass
|
|
if len(contents) == 2:
|
|
return IFileTree.STOP
|
|
else:
|
|
return IFileTree.CONTINUE
|
|
|
|
filetree.walk(getContentForEntry)
|
|
return list(contents)
|