Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 136 additions & 5 deletions games/game_sims4.py
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)