diff --git a/games/game_borderlands1.py b/games/game_borderlands1.py new file mode 100644 index 0000000..4b90b51 --- /dev/null +++ b/games/game_borderlands1.py @@ -0,0 +1,153 @@ +import re + +import mobase +from mobase import FileTreeEntry, IFileTree, ModDataChecker + +from ..basic_features import BasicModDataChecker +from ..basic_game import BasicGame + +_extention_pattern = re.compile("\\.(upk|umap|u|int|dll|exe)$", re.I) +_mapslot_pattern = re.compile("^Mapslot\\d\\d?\\.umap$", re.I) + +_mod_dirs = { + "Binaries".casefold(): "/", + "WillowGame".casefold(): "/", + "CookedPC".casefold(): "WillowGame/", + "Localization".casefold(): "WillowGame/", + "Maps".casefold(): "WillowGame/CookedPC/", +} + +_slots_path = "WillowGame/CookedPC/Maps/MapSlots" + + +def _check_filetree( + filetree: IFileTree | FileTreeEntry, recurse: bool +) -> ModDataChecker.CheckReturn: + if filetree.isFile(): + if _extention_pattern.search(filetree.name()): + if _mapslot_pattern.search(filetree.name()): + return ModDataChecker.FIXABLE + else: + return ModDataChecker.INVALID + elif recurse and isinstance(filetree, IFileTree): + status = ModDataChecker.VALID + for child in filetree: + child_status = _check_filetree(child, True) + if child_status is ModDataChecker.INVALID: + return ModDataChecker.INVALID + elif child_status is ModDataChecker.FIXABLE: + status = ModDataChecker.FIXABLE + return status + + return ModDataChecker.VALID + + +def _get_nest(filetree: IFileTree) -> IFileTree | None: + children = tuple(filetree) + if ( + len(children) == 1 + and children[0].isDir() + and isinstance(children[0], IFileTree) + and _mod_dirs.get(children[0].name().casefold()) is None + ): + return children[0] + return None + + +def _get_slotstree(roottree: IFileTree) -> IFileTree: + slotstree: IFileTree | None = roottree.find(_slots_path) # type: ignore + if slotstree is None: + slotstree = roottree.addDirectory(_slots_path) + return slotstree + + +def _fix_mapslots(filetree: IFileTree, roottree: IFileTree) -> None: + for child in filetree: + if child.isDir() and isinstance(child, IFileTree): + _fix_mapslots(child, roottree) + elif _mapslot_pattern.search(child.name()): + child.moveTo(_get_slotstree(roottree)) + + +class Borderlands1ModDataChecker(BasicModDataChecker): + def dataLooksValid(self, filetree: IFileTree) -> ModDataChecker.CheckReturn: + parent = filetree.parent() + if parent is not None: + return self.dataLooksValid(parent) + + status = ModDataChecker.VALID + + nest = _get_nest(filetree) + if nest is not None: + status = ModDataChecker.FIXABLE + filetree = nest + + if _check_filetree(filetree, False) is ModDataChecker.INVALID: + return ModDataChecker.INVALID + + slotstree = filetree.find(_slots_path) + if slotstree is not None and not slotstree.isDir(): + return ModDataChecker.INVALID + + for child in filetree: + if child.isDir(): + destination = _mod_dirs.get(child.name().casefold()) + if destination is None: + child_status = _check_filetree(child, True) + if child_status is ModDataChecker.INVALID: + return ModDataChecker.INVALID + elif child_status is ModDataChecker.FIXABLE: + status = ModDataChecker.FIXABLE + elif destination != "/": + status = ModDataChecker.FIXABLE + elif _mapslot_pattern.search(child.name()): + status = ModDataChecker.FIXABLE + elif _extention_pattern.search(child.name()): + return ModDataChecker.INVALID + return status + + def fix(self, filetree: IFileTree) -> IFileTree: + nest = _get_nest(filetree) + if nest is not None: + conflict: FileTreeEntry | None = None + for child in tuple(nest): + if not child.moveTo(filetree): + conflict = child + conflict.detach() + filetree.remove(nest) + if conflict is not None: + conflict.moveTo(filetree) + + for child in tuple(filetree): + if child.isDir() and isinstance(child, IFileTree): + destination = _mod_dirs.get(child.name().casefold()) + if destination is None: + _fix_mapslots(child, filetree) + elif destination != "/": + filetree.move(child, destination) + elif _mapslot_pattern.search(child.name()): + child.moveTo(_get_slotstree(filetree)) + + return filetree + + +class Borderlands1Game(BasicGame): + Name = "Borderlands 1 Support Plugin" + Author = "Miner Of Worlds, RedxYeti, mopioid" + Version = "1.0.0" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(Borderlands1ModDataChecker()) + return True + + GameName = "Borderlands" + GameShortName = "Borderlands" + GameNexusName = "Borderlands GOTY" + GameSteamId = 8980 + GameBinary = "Binaries/Borderlands.exe" + GameDataPath = "." + GameSaveExtension = "sav" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/Borderlands/" + GameSavesDirectory = "%GAME_DOCUMENTS%/savedata" + GameIniFiles = "%GAME_DOCUMENTS%/WillowGame/Config"