diff --git a/games/game_sims4.py b/games/game_sims4.py index a3c905b2..ca342768 100644 --- a/games/game_sims4.py +++ b/games/game_sims4.py @@ -1,12 +1,35 @@ -import mobase +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" - + Author = "R3z Shark, xieve" GameName = "The Sims 4" GameShortName = "thesims4" GameBinary = "Game/Bin/TS4_x64.exe" @@ -16,5 +39,113 @@ class TS4Game(BasicGame): GameOriginWatcherExecutables = ("TS4_x64.exe",) def version(self): - # Don't forget to import mobase! - return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.FINAL) + 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), + ) + 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)