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
316from ..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+
630class 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