diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/basic_games/games/game_sims4.py | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/basic_games/games/game_sims4.py')
| -rw-r--r-- | libs/basic_games/games/game_sims4.py | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/libs/basic_games/games/game_sims4.py b/libs/basic_games/games/game_sims4.py new file mode 100644 index 0000000..e5e7fa2 --- /dev/null +++ b/libs/basic_games/games/game_sims4.py @@ -0,0 +1,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) |
