From 20e1cb86af2aa1248e93888ede22bd0ee5716ff4 Mon Sep 17 00:00:00 2001 From: "Ryan (Ryn)" <31045220+vmRyn@users.noreply.github.com> Date: Fri, 6 Nov 2026 07:59:35 +0000 Subject: [PATCH] Add Grand Theft Auto IV / Complete Edition basic game support. Enable MO2 instances for GTA IV with ASI force-load, FusionFix/Overloader-aware mod checking, and Complete Edition save detection. Co-authored-by: Cursor --- README.md | 1 + games/game_gta4.py | 211 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 games/game_gta4.py diff --git a/README.md b/README.md index 6fbed59b..01cd230f 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic | Divinity: Original Sin (Classic) — [STEAM](https://store.steampowered.com/app/230230/Divinity_Original_Sin_Classic/) | [LostDragonist](https://github.com/LostDragonist/) | [game_divinityoriginalsin.py](games/game_divinityoriginalsin.py) | | | Divinity: Original Sin (Enhanced Edition) — [STEAM](https://store.steampowered.com/app/373420/Divinity_Original_Sin__Enhanced_Edition/) | [LostDragonist](https://github.com/LostDragonist/) | [game_divinityoriginalsinee.py](games/game_divinityoriginalsinee.py) | | | Dragon's Dogma: Dark Arisen — [GOG](https://www.gog.com/game/dragons_dogma_dark_arisen) / [STEAM](https://store.steampowered.com/app/367500/Dragons_Dogma_Dark_Arisen/) | [EzioTheDeadPoet](https://github.com/EzioTheDeadPoet) | [game_dragonsdogmadarkarisen.py](games/game_dragonsdogmadarkarisen.py) | | +| Grand Theft Auto IV / The Complete Edition — [STEAM](https://store.steampowered.com/app/12210/Grand_Theft_Auto_IV_The_Complete_Edition/) | [vmRyn](https://github.com/vmRyn) | [game_gta4.py](games/game_gta4.py) | | | Dungeon Siege II — [GOG](https://www.gog.com/game/dungeon_siege_collection) / [STEAM](https://store.steampowered.com/app/39200/Dungeon_Siege_II/) | [Holt59](https://github.com/holt59/) | [game_dungeonsiege2.py](games/game_dungeonsiege2.py) | | | Kingdom Come: Deliverance — [GOG](https://www.gog.com/game/kingdom_come_deliverance) / [STEAM](https://store.steampowered.com/app/379430/Kingdom_Come_Deliverance/) / [Epic](https://store.epicgames.com/p/kingdom-come-deliverance) | [Silencer711](https://github.com/Silencer711) | [game_kingdomcomedeliverance.py](games/game_kingdomcomedeliverance.py) | | | METAL GEAR SOLID 2: Sons of Liberty — [STEAM](https://store.steampowered.com/app/2131640/METAL_GEAR_SOLID_2_Sons_of_Liberty__Master_Collection_Version/)|[AkiraJkr](https://github.com/AkiraJkr)|[game_metalgearsolid2mc.py](games/game_metalgearsolid2mc.py)| | diff --git a/games/game_gta4.py b/games/game_gta4.py new file mode 100644 index 00000000..41c79c4a --- /dev/null +++ b/games/game_gta4.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class GTA4ModDataChecker(BasicModDataChecker): + """ + Normalize common GTA IV mod layouts for Complete Edition / classic IV. + + Typical destinations (game root as MO data path): + - Ultimate ASI Loader / FusionFix: dinput8.dll (or xlive.dll), *.asi + - Fusion Overloader content: update/ + - ScriptHookDotNet scripts: scripts/*.net.dll + - IV-SDK .NET scripts: IVSDKDotNet/scripts/*.ivsdk.dll + - IV Tweaker (1.0.7.0 / 1.0.8.0): modloader/ + - Direct replacements: common/, pc/, TLAD/, TBoGT/ + """ + + def __init__(self, patterns: GlobPatterns | None = None): + super().__init__( + GlobPatterns( + unfold=[ + "Grand Theft Auto IV", + "GTA IV", + "GTAIV", + "GTA4", + "gta4", + ], + valid=[ + # Content / overloader + "update", + "common", + "pc", + "TLAD", + "TBoGT", + "movies", + "modloader", + # Script frameworks + "scripts", + "IVSDKDotNet", + "plugins", + "plugins_disabled", + # Root ASI / loader files + "*.asi", + "dinput8.dll", + "xlive.dll", + "dsound.dll", + # FusionFix / DXVK / graphics helpers + "d3d9.dll", + "d3d10.dll", + "d3d11.dll", + "dxgi.dll", + "vulkan.dll", + "bass.dll", + "ScriptHook.dll", + "ScriptHookDotNet.dll", + "ScriptHookDotNet.asi", + "AdvancedHook.dll", + "AdvancedHookInit.asi", + "aCompleteEditionHook.asi", + "ClrHook.dll", + # Common companion / config files + "*.ini", + "*.cfg", + "*.xml", + "*.dll", + ], + delete=[ + "*.md", + "*.txt", + "*.pdf", + "*.url", + "*.jpg", + "*.jpeg", + "*.png", + "*.gif", + "*.webp", + "readme*", + "read me*", + "License*", + "LICENSE*", + "fomod", + ], + move={ + # ScriptHookDotNet / IV-SDK scripts often ship loose + "*.net.dll": "scripts/", + "*.ivsdk.dll": "IVSDKDotNet/scripts/", + # Loose IMG packs for Fusion Overloader + "*.img": "update/", + }, + ).merge(patterns or GlobPatterns()) + ) + + +class GTA4Game(BasicGame): + Name = "Grand Theft Auto IV Support Plugin" + Author = "vmRyn" + Version = "1.0.0" + Description = ( + "Adds support for Grand Theft Auto IV / The Complete Edition " + "(ASI loaders, FusionFix/Overloader, ScriptHookDotNet, IV-SDK .NET)." + ) + + GameName = "Grand Theft Auto IV" + GameShortName = "gta4" + GameNexusName = "gta4" + GameNexusId = 253 + GameSteamId = [12210, 12220] # IV Complete Edition; legacy EFLC + GameBinary = "GTAIV.exe" + GameDataPath = "" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/Rockstar Games/GTA IV/Settings" + ) + GameSavesDirectory = "%DOCUMENTS%/Rockstar Games/GTA IV/Profiles" + GameIniFiles = ["SETTINGS.CFG"] + GameSupportURL = ( + r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/" + "Game:-Grand-Theft-Auto-IV" + ) + + # Ultimate ASI Loader / FusionFix. xlive.dll is the GFWL-removal rename of + # dinput8.dll used on older patches; force both so VFS injects them. + _forced_libraries = ["dinput8.dll", "xlive.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(GTA4ModDataChecker()) + # Older MO2 builds take QDir; current basic_games takes IPluginGame. + try: + local_saves = BasicLocalSavegames(self) + except AttributeError: + local_saves = BasicLocalSavegames(self.savesDirectory()) + self._register_feature(local_saves) + return True + + def looksValid(self, directory: QDir) -> bool: + # Complete Edition and classic IV both ship GTAIV.exe. Older Steam + # installs may also have LaunchGTAIV.exe as a secondary launcher. + return directory.exists(self.binaryName()) or directory.exists( + "LaunchGTAIV.exe" + ) + + def executables(self) -> list[mobase.ExecutableInfo]: + game_dir = self.gameDirectory() + execs = [ + mobase.ExecutableInfo( + "Grand Theft Auto IV", + QFileInfo(game_dir.absoluteFilePath(self.binaryName())), + ), + ] + # Legacy Steam launcher (pre–Complete Edition / some downgrades) + launch = game_dir.absoluteFilePath("LaunchGTAIV.exe") + if QFileInfo(launch).exists(): + execs.append( + mobase.ExecutableInfo( + "Launch GTA IV", + QFileInfo(launch), + ) + ) + return execs + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + loads: list[mobase.ExecutableForcedLoadSetting] = [] + for exe in self.executables(): + exe_name = exe.binary().fileName() + for lib in self._forced_libraries: + loads.append( + mobase.ExecutableForcedLoadSetting(exe_name, lib).withEnabled(True) + ) + return loads + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + """ + Complete Edition: Documents/Rockstar Games/GTA IV/Profiles//SGTA4XX + (no file extension). Also accept older extensionless/alternate layouts + under the configured saves directory. + """ + root = Path(folder.absolutePath()) + if not root.exists(): + return [] + + saves: list[mobase.ISaveGame] = [] + seen: set[Path] = set() + for pattern in ("**/SGTA4*", "**/SGTA4*.*", "**/savegames/**/*"): + for path in root.glob(pattern): + if not path.is_file(): + continue + # Skip obvious non-save profile clutter + name = path.name.casefold() + if name in { + "profilesettings", + "controlmap.dat", + "cloudsavedata.dat", + }: + continue + if path.suffix.casefold() in {".dat"} and not name.startswith("sgta"): + continue + resolved = path.resolve() + if resolved in seen: + continue + seen.add(resolved) + saves.append(BasicGameSaveGame(path)) + return saves