1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
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)
|