aboutsummaryrefslogtreecommitdiff
path: root/libs/basic_games/games/game_sims4.py
blob: e5e7fa21812840b8041531149f8692aa553f85fd (plain)
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)