Skip to content

Commit 7e072cc

Browse files
authored
feat(sims4): add ModDataChecker, ModDataContent (#173)
* feat(sims4): add ModDataChecker, ModDataContent
1 parent 22920a0 commit 7e072cc

1 file changed

Lines changed: 136 additions & 5 deletions

File tree

games/game_sims4.py

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
import mobase
1+
from collections.abc import Callable
2+
from enum import IntEnum
3+
from re import match
4+
from typing import Any, List, Set, cast
5+
6+
from mobase import (
7+
FileTreeEntry,
8+
IFileTree,
9+
IOrganizer,
10+
ModDataChecker,
11+
ModDataContent,
12+
ReleaseType,
13+
VersionInfo,
14+
)
215

316
from ..basic_game import BasicGame
417

518

19+
class Content(IntEnum):
20+
PACKAGE = 0
21+
SCRIPT = 1
22+
23+
24+
class ValidationResult(IntEnum):
25+
INVALID = 0
26+
VALID = 1
27+
FIXABLE = 2
28+
29+
630
class TS4Game(BasicGame):
731
Name = "The Sims 4 Support Plugin"
8-
Author = "R3z Shark"
9-
32+
Author = "R3z Shark, xieve"
1033
GameName = "The Sims 4"
1134
GameShortName = "thesims4"
1235
GameBinary = "Game/Bin/TS4_x64.exe"
@@ -16,5 +39,113 @@ class TS4Game(BasicGame):
1639
GameOriginWatcherExecutables = ("TS4_x64.exe",)
1740

1841
def version(self):
19-
# Don't forget to import mobase!
20-
return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.FINAL)
42+
return VersionInfo(1, 0, 0, ReleaseType.FINAL)
43+
44+
def documentsDirectory(self):
45+
return self.dataDirectory()
46+
47+
def init(self, organizer: IOrganizer):
48+
if super().init(organizer):
49+
self._register_feature(TS4ModDataChecker())
50+
self._register_feature(TS4ModDataContent())
51+
return True
52+
return False
53+
54+
55+
class TS4ModDataChecker(ModDataChecker):
56+
# .package files are allowed at a maximum depth of 5 subfolders, script files can be at most one level deep
57+
# The first capturing group lazily captures any parent folders exceeding that depth, see below
58+
_fixableOrValid = r"(?i)^(.*?)((?:[^\\]+\\){0,5}[^\\]*\.package|(?:[^\\]+\\)?[^\\]*\.ts4script|(?:[^\\]+\\)scripts\\.*\.py)$"
59+
60+
def dataLooksValid(self, filetree: IFileTree) -> ModDataChecker.CheckReturn:
61+
return cast(
62+
ModDataChecker.CheckReturn,
63+
self._fixOrValidateTree(filetree, validateMode=True),
64+
)
65+
66+
def fix(self, filetree: IFileTree) -> IFileTree | None:
67+
return cast(IFileTree, self._fixOrValidateTree(filetree))
68+
69+
def _fixOrValidateTree(
70+
self,
71+
tree: IFileTree,
72+
validateMode: bool = False,
73+
) -> IFileTree | ModDataChecker.CheckReturn:
74+
validationResult = ValidationResult.INVALID
75+
checkReturn = ModDataChecker.INVALID
76+
walkReturn = IFileTree.CONTINUE
77+
78+
def setValidationResult(
79+
newResult: ValidationResult = ValidationResult.INVALID,
80+
actionCallback: Callable[[], Any] = lambda: None,
81+
):
82+
nonlocal validationResult
83+
nonlocal checkReturn
84+
nonlocal walkReturn
85+
if validateMode:
86+
validationResult = max(validationResult, newResult)
87+
match validationResult:
88+
case ValidationResult.INVALID:
89+
pass
90+
case ValidationResult.VALID:
91+
checkReturn = ModDataChecker.VALID
92+
case ValidationResult.FIXABLE:
93+
checkReturn = ModDataChecker.FIXABLE
94+
walkReturn = IFileTree.STOP
95+
else:
96+
actionCallback()
97+
98+
def fixOrValidateEntry(
99+
parentPath: str, entry: FileTreeEntry
100+
) -> IFileTree.WalkReturn:
101+
path = f"{parentPath}{entry.name()}"
102+
fixableOrValid = match(self._fixableOrValid, path)
103+
if fixableOrValid:
104+
match fixableOrValid.groups():
105+
case ["", _]:
106+
setValidationResult(ValidationResult.VALID)
107+
case [_, innerPath]:
108+
# Move files that are nested too deeply up, preserving the inner folder structure
109+
# E.g. a/b/c.ts4script will be moved to b/c.ts4script
110+
setValidationResult(
111+
ValidationResult.FIXABLE,
112+
lambda: tree.move(entry, innerPath),
113+
)
114+
return walkReturn
115+
116+
tree.walk(fixOrValidateEntry)
117+
118+
if validateMode:
119+
return checkReturn
120+
else:
121+
return tree
122+
123+
124+
class TS4ModDataContent(ModDataContent):
125+
def getAllContents(self: ModDataContent) -> List[ModDataContent.Content]:
126+
return [
127+
ModDataContent.Content(
128+
Content.PACKAGE, "Package", ":/MO/gui/content/plugin"
129+
),
130+
ModDataContent.Content(Content.SCRIPT, "Script", ":/MO/gui/content/script"),
131+
]
132+
133+
def getContentsFor(self: ModDataContent, filetree: IFileTree) -> List[int]:
134+
contents: Set[int] = set()
135+
136+
def getContentForEntry(path: str, entry: FileTreeEntry):
137+
nonlocal contents
138+
match entry.suffix():
139+
case "package":
140+
contents.add(Content.PACKAGE)
141+
case "ts4script" | "py":
142+
contents.add(Content.SCRIPT)
143+
case _:
144+
pass
145+
if len(contents) == 2:
146+
return IFileTree.STOP
147+
else:
148+
return IFileTree.CONTINUE
149+
150+
filetree.walk(getContentForEntry)
151+
return list(contents)

0 commit comments

Comments
 (0)