From eb7afc3c8171d0a32d622ca89312facc07b6d082 Mon Sep 17 00:00:00 2001 From: Seemann Date: Mon, 21 Sep 2026 21:37:43 -0400 Subject: [PATCH 1/3] Improve compatibility with CLEO5 --- src/plugins/gta3/std.asi/ModuleInfo.cpp | 17 ++++++++++++++-- .../args_translator/hacks/FindCleoScripts.hpp | 6 ++++++ .../args_translator/xtranslator_path.hpp | 20 +++++++++++++++++++ src/plugins/gta3/std.asi/asi.h | 10 ++++++++-- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/plugins/gta3/std.asi/ModuleInfo.cpp b/src/plugins/gta3/std.asi/ModuleInfo.cpp index 5190b1b6..a8d3e3f8 100644 --- a/src/plugins/gta3/std.asi/ModuleInfo.cpp +++ b/src/plugins/gta3/std.asi/ModuleInfo.cpp @@ -243,10 +243,23 @@ void ThePlugin::LocateCleo() this->iCleoVersion = CLEO_GetVersion? CLEO_GetVersion() : 0; Log("CLEO library version %X found at \"%s\"", iCleoVersion, p); - if(this->bHasNoCleoFolder = !IsPath((std::string(loader->gamepath) + "./CLEO/").c_str())) - Log("Warning: No CLEO folder found, may cause problems"); this->asiList.emplace_back(p, nullptr, hCleo); + + // assume CLEO directory is located in root by default + this->cleoFolder = std::string(loader->gamepath) + "cleo"; + + // Try asking newer CLEO 5 where its folder it located + auto CLEO_GetCleoDirectory = (const char* (__stdcall*)()) GetProcAddress(hCleo, "_CLEO_GetCleoDirectory@0"); + if(CLEO_GetCleoDirectory) + { + if(const char* cleoDir = CLEO_GetCleoDirectory()) + this->cleoFolder = cleoDir; + } + + if(this->bHasNoCleoFolder = !IsPath((this->cleoFolder + "\\").c_str())) + Log("Warning: No CLEO folder found, may cause problems"); + this->asiList.back().PatchImports(); } } diff --git a/src/plugins/gta3/std.asi/args_translator/hacks/FindCleoScripts.hpp b/src/plugins/gta3/std.asi/args_translator/hacks/FindCleoScripts.hpp index 893cc4c5..3bc5adc5 100644 --- a/src/plugins/gta3/std.asi/args_translator/hacks/FindCleoScripts.hpp +++ b/src/plugins/gta3/std.asi/args_translator/hacks/FindCleoScripts.hpp @@ -171,6 +171,12 @@ namespace hacks // Get path in iterator compatible with what CLEO.asi expects to receive char* GetCleoCompatiblePath(char* buf) { + // CLEO 5 allows relocating the CLEO folder out of the game root + // (e.g. to scripts\cleo), which breaks the "..\" form, + // so always prefer the absolute path. + if(plugin_ptr->cast().IsCLEO5()) + return GetFullPath(buf); + // CLEO 4.3 needs a existing CLEO folder for the path relativity to work if(plugin_ptr->cast().iCleoVersion > 0x401011E && plugin_ptr->cast().bHasNoCleoFolder) return GetFullPath(buf); diff --git a/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp b/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp index b8625f3a..a8b24d21 100644 --- a/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp +++ b/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp @@ -142,12 +142,32 @@ inline void path_translator_base::CallInfo::TranslatePathForMainExecutable(const } }; + auto CheckCleoFolder = [&]() + { + auto& cleoDir = plugin_ptr->cast().cleoFolder; + + if(!bSet && !cleoDir.empty()) + { + struct PathBase { std::string translationPath; } cleo; + cleo.translationPath = cleoDir; + bSet = CxBuildPath(p, cleo, currdir, arg, build_path, false); + } + }; + // Run the tries by priority based on what is the caller if(asi->bIsCleo) { // Cleo scripts first CheckCleoScripts(); CheckASI(); + + // File operations worked only by accident in some legacy CLEO scripts, + // because modloader added CLEO folder containing .cleo plugins to search path. + // As CLEO5 moved them to cleo_plugins, we need to keep + // searching there to preserve backward compatibility. + // https://github.com/cleolibrary/CLEO5/issues/589 + if(plugin_ptr->cast().IsCLEO5()) + CheckCleoFolder(); } else { diff --git a/src/plugins/gta3/std.asi/asi.h b/src/plugins/gta3/std.asi/asi.h index 9035c348..6fc39350 100644 --- a/src/plugins/gta3/std.asi/asi.h +++ b/src/plugins/gta3/std.asi/asi.h @@ -122,8 +122,14 @@ class ThePlugin : public modloader::basic_plugin typedef std::list CsInfoList; // CLEO.ASI version - int iCleoVersion; - bool bHasNoCleoFolder; + int iCleoVersion = 0; + bool bHasNoCleoFolder = false; + + // CLEO data folder. Can be root\cleo or relative to cleo.asi (CLEO 5) + std::string cleoFolder; + + // CLEO 5.0 or newer (support for relocatable CLEO folder and cleo_plugins) + bool IsCLEO5() const { return iCleoVersion >= 0x05000000; } // Set to true when loading - do not translate relative paths, as we're chdir'd into modloader already // Set by the DLL Load Notification From e64811ce015fd10a84f40eb2856bd3861220ec77 Mon Sep 17 00:00:00 2001 From: Seemann Date: Tue, 22 Sep 2026 19:54:01 -0400 Subject: [PATCH 2/3] clarify cleoFolder path --- src/plugins/gta3/std.asi/asi.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/gta3/std.asi/asi.h b/src/plugins/gta3/std.asi/asi.h index 6fc39350..b9563f1a 100644 --- a/src/plugins/gta3/std.asi/asi.h +++ b/src/plugins/gta3/std.asi/asi.h @@ -125,7 +125,8 @@ class ThePlugin : public modloader::basic_plugin int iCleoVersion = 0; bool bHasNoCleoFolder = false; - // CLEO data folder. Can be root\cleo or relative to cleo.asi (CLEO 5) + // Absolute path to the CLEO data folder, without a trailing path separator. + // Can be ("\cleo") or relative to cleo.asi ("\scripts\cleo") std::string cleoFolder; // CLEO 5.0 or newer (support for relocatable CLEO folder and cleo_plugins) From baf230c1302be6cc9e34afe398ab3c8d0bbf3372 Mon Sep 17 00:00:00 2001 From: Seemann Date: Wed, 23 Sep 2026 17:39:42 -0400 Subject: [PATCH 3/3] Rewrite custom CLEO folder path to cleo --- include/modloader/util/path.hpp | 59 +++++++++++++++++++ src/core/loader.cpp | 7 --- .../args_translator/translator_basic.hpp | 49 +++++++++++++-- .../args_translator/xtranslator_path.hpp | 44 ++++++++++++-- 4 files changed, 144 insertions(+), 15 deletions(-) diff --git a/include/modloader/util/path.hpp b/include/modloader/util/path.hpp index 3ea30559..34ccc5ba 100644 --- a/include/modloader/util/path.hpp +++ b/include/modloader/util/path.hpp @@ -84,6 +84,65 @@ namespace modloader { return IsAbsolutePath(str.c_str()); } + /* + * strnicmp + * Case insensitive comparison of the first @num characters, for both character types + */ + inline int strnicmp(const char* a, const char* b, size_t num) { return ::_strnicmp(a, b, num); } + inline int strnicmp(const wchar_t* a, const wchar_t* b, size_t num) { return ::_wcsnicmp(a, b, num); } + + + /* + * ToWide + * Converts a narrow (ANSI) string to wide, using CP_ACP -- the same conversion the ANSI Win32 APIs perform. + */ + inline std::wstring ToWide(const std::string& narrow) + { + std::wstring result(narrow.size(), L'\0'); + if(!narrow.empty()) + MultiByteToWideChar(CP_ACP, 0, narrow.c_str(), (int) narrow.size(), &result[0], (int) result.size()); + return result; + } + + + /* + * GetPathRelativeTo + * If @path is inside @base, returns a pointer to the part after it, otherwise null. + * @base may or may not end with a path separator + * Both paths must be in the same form (both absolute, or both relative to the same base). + */ + template + inline const T* GetPathRelativeTo(const std::basic_string& path, const std::basic_string& base) + { + const size_t num = base.size(); + + if(!num || path.size() < num || strnicmp(path.c_str(), base.c_str(), num)) + return nullptr; + + // A base without a trailing separator needs @path to have one there, otherwise a folder would + // also be matched by a longer name starting with it ("cleo" matching "cleofoo") + const bool bSeparated = (base[num - 1] == T('\\') || base[num - 1] == T('/')); + if(!bSeparated && (path.size() == num || (path[num] != T('\\') && path[num] != T('/')))) + return nullptr; + + return path.c_str() + (bSeparated? num : num + 1); + } + + + /* + * MakePathRelativeTo + * Returns @path relative to @base, e.g. ("C:\\game\\cleo", "C:\\game\\") gives "cleo". + * If @path is not inside @base, the path is returned unchanged. + */ + template + inline std::basic_string MakePathRelativeTo(const std::basic_string& path, const std::basic_string& base) + { + if(const T* relative = GetPathRelativeTo(path, base)) + return relative; + return path; + } + + /* * MakeSureStringIsDirectory * Makes sure the string @dir is a directory path. If @touchEmpty is true, diff --git a/src/core/loader.cpp b/src/core/loader.cpp index 4fac96fa..6ef4c7b1 100644 --- a/src/core/loader.cpp +++ b/src/core/loader.cpp @@ -21,13 +21,6 @@ REGISTER_ML_NULL(); Loader loader; static HINSTANCE hLoaderModule = NULL; -static std::string MakePathRelativeTo(const std::string& path, const std::string& base) -{ - if(path.size() >= base.size() && !_strnicmp(path.c_str(), base.c_str(), base.size())) - return path.substr(base.size()); - return path; -} - /* * DllMain * Entry-point diff --git a/src/plugins/gta3/std.asi/args_translator/translator_basic.hpp b/src/plugins/gta3/std.asi/args_translator/translator_basic.hpp index 28756256..21dfb79c 100644 --- a/src/plugins/gta3/std.asi/args_translator/translator_basic.hpp +++ b/src/plugins/gta3/std.asi/args_translator/translator_basic.hpp @@ -378,6 +378,9 @@ struct path_translator_base + template + std::basic_string GetPathInsideCleoFolder(const T* arg); + template bool GetCurrentDir(const T*& arg, char type, const char*& currdir, char buffer[MAX_PATH]); @@ -401,14 +404,17 @@ struct path_translator_base // Gets the current working directory relative to the the game path // If working directory is not anywhere near the game path, return null // This is the manual version, where you send the current working directory (@fullpath) and game path (@gamepath) - static const char* GetCurrentDir(const char* fullpath, const char* gamePath, size_t max) + + // Both strings MUST have the same character type, otherwise their comparison fails + template + static const T* GetCurrentDir(const T* fullpath, const T* gamePath, size_t max) { - const char* currdir = 0; + const T* currdir = 0; // Iterate on the game path comparing it with the current working dir for(size_t i = 0; i < max; ++i) { - if(gamePath[i] == 0) // End of game path? + if(gamePath[i] == T(0)) // End of game path? { // Then here starts the relative part currdir = &fullpath[i]; @@ -417,7 +423,7 @@ struct path_translator_base else if(gamePath[i] != fullpath[i]) // Piece of gamepath not equal to the working dir? wow { // Let's calm down, if working directory ended and game path is ending, we're still 'equal' - if(fullpath[i] == 0 && gamePath[i] == '\\' && gamePath[i+1] == 0) + if(fullpath[i] == T(0) && gamePath[i] == T('\\') && gamePath[i+1] == T(0)) { // Point current directory to "\0" part of fullpath currdir = &fullpath[i]; @@ -429,6 +435,41 @@ struct path_translator_base return currdir; } + static const char* GetGamePathFor(const char*) + { + return plugin_ptr->loader->gamepath; + } + + static const wchar_t* GetGamePathFor(const wchar_t*) + { + static const std::wstring path = []() -> std::wstring + { + const char* narrow = plugin_ptr->loader->gamepath; + if(narrow == nullptr || narrow[0] == 0) return std::wstring(); + + // CP_ACP: the same conversion the ANSI Win32 APIs perform on this path + int length = MultiByteToWideChar(CP_ACP, 0, narrow, -1, nullptr, 0); // count includes the terminator + if(length <= 1) return std::wstring(); + + std::wstring wide(size_t(length) - 1, L'\0'); + MultiByteToWideChar(CP_ACP, 0, narrow, -1, &wide[0], length); // rewrites the terminator + return wide; + }(); + + return path.c_str(); + } + + static const char* GetCleoFolderFor(const char*) + { + return plugin_ptr->cast().cleoFolder.c_str(); + } + + static const wchar_t* GetCleoFolderFor(const wchar_t*) + { + static const std::wstring folder = ToWide(plugin_ptr->cast().cleoFolder); + return folder.c_str(); + } + }; diff --git a/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp b/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp index a8b24d21..88d3cb54 100644 --- a/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp +++ b/src/plugins/gta3/std.asi/args_translator/xtranslator_path.hpp @@ -44,6 +44,20 @@ inline bool path_translator_base::CallInfo::CxBuildPath(T* p, const M& module, } +/* + * If @arg is a path inside the CLEO folder, returns the part after the folder + * (e.g. "\scripts\cleo\foo\bar.ini" gives "foo\bar.ini"), otherwise returns an empty string + */ +template +inline std::basic_string path_translator_base::CallInfo::GetPathInsideCleoFolder(const T* arg) +{ + const std::basic_string absolute = IsAbsolutePath(arg)? std::basic_string(arg) + : std::basic_string(GetGamePathFor(arg)) + arg; + const T* const inside = GetPathRelativeTo(absolute, std::basic_string(GetCleoFolderFor(arg))); + return inside? std::basic_string(inside) : std::basic_string(); +} + + // Helper for proper GetCurrentDir template inline bool path_translator_base::CallInfo::GetCurrentDir(const T*& arg, char type, const char*& currdir, char buffer[MAX_PATH]) @@ -53,13 +67,13 @@ inline bool path_translator_base::CallInfo::GetCurrentDir(const T*& arg, char t if(this->bAbsolutePath) // This argument is an absolute path? { // Get current directory assuming argument is the fullpath - currdir = GetCurrentDir( (const char*) arg, plugin_ptr->loader->gamepath, -1); + const T* relative = GetCurrentDir(arg, GetGamePathFor(arg), -1); // If could get the currdir, set up some stuff and go ahead on the translation // For SetDir, if currdir is empty, that means it should chdir into base dir, so don't touch it - if(currdir && (!bSetDir || currdir[0] != 0)) + if(relative && (!bSetDir || relative[0] != 0)) { - arg = (T*) currdir; + arg = relative; currdir = 0; bCheckModules = true; } @@ -102,7 +116,9 @@ inline void path_translator_base::CallInfo::TranslatePathForMainExecutable(const // ...if not, try on the ASI paths or cleo paths if(type == AR_PATH_INE && !IsPath(arg)) { - // + // Remember the original path so it can be restored if no mod wants it. + const T* const originalArg = arg; + bool bCheckModules = GetCurrentDir(arg, type, currdir, buffer); // Check if currdir is alright @@ -168,6 +184,22 @@ inline void path_translator_base::CallInfo::TranslatePathForMainExecutable(const // https://github.com/cleolibrary/CLEO5/issues/589 if(plugin_ptr->cast().IsCLEO5()) CheckCleoFolder(); + + // CLEO folder is not necessarily in the game root anymore, so we strip "" + // from path and probe the rest relative to "cleo" folder inside the mod folder. + if(!bSet && plugin_ptr->cast().IsCLEO5()) + { + const std::basic_string cleoRelative = GetPathInsideCleoFolder(originalArg); + + if(!cleoRelative.empty()) + { + arg = cleoRelative.c_str(); + currdir = "cleo"; + + CheckCleoScripts(); + CheckASI(); + } + } } else { @@ -175,6 +207,10 @@ inline void path_translator_base::CallInfo::TranslatePathForMainExecutable(const CheckASI(); CheckCleoScripts(); } + + // Nothing wanted to translate this path, so put the original absolute path back. + // Leaving it relative makes the Win32 profile APIs resolve it against the Windows directory + if(!bSet) arg = originalArg; } } }