From d2613fd484e0078593c981f617b2bdfa16fd2936 Mon Sep 17 00:00:00 2001 From: Miran Date: Fri, 13 Sep 2024 02:39:43 +0200 Subject: [PATCH] Introduced proper ModLoader support with plugin --- .github/workflows/main.yml | 35 +- .github/workflows/test.yml | 37 +- CHANGELOG.md | 1 + CLEO5.vcxproj | 2 + CLEO5.vcxproj.filters | 6 + README.md | 1 + cleo_plugins/MemoryOperations/AntiHacks.h | 32 ++ .../MemoryOperations/MemoryOperations.cpp | 44 ++- .../MemoryOperations/MemoryOperations.vcxproj | 1 + .../MemoryOperations.vcxproj.filters | 1 + modloader_plugin/CLEO_ModLoader_Plugin.sln | 25 ++ .../CLEO_ModLoader_Plugin.vcxproj | 143 +++++++ .../CLEO_ModLoader_Plugin.vcxproj.filters | 34 ++ modloader_plugin/Resource.rc | Bin 0 -> 1572 bytes modloader_plugin/source/ModLoaderProvider.cpp | 350 ++++++++++++++++++ modloader_plugin/source/ModLoaderProvider.h | 43 +++ modloader_plugin/source/main.cpp | 115 ++++++ modloader_plugin/source/modloader/modloader.h | 337 +++++++++++++++++ .../source/modloader/modloader.hpp | 290 +++++++++++++++ modloader_plugin/source/utils.h | 70 ++++ source/CCustomOpcodeSystem.cpp | 42 ++- source/CModLoaderSystem.cpp | 56 +++ source/CModLoaderSystem.h | 24 ++ source/CPluginSystem.cpp | 84 +++-- source/CPluginSystem.h | 6 +- source/CScriptEngine.cpp | 18 +- source/CleoBase.cpp | 68 +++- source/CleoBase.h | 2 + source/stdafx.h | 3 +- 29 files changed, 1777 insertions(+), 93 deletions(-) create mode 100644 cleo_plugins/MemoryOperations/AntiHacks.h create mode 100644 modloader_plugin/CLEO_ModLoader_Plugin.sln create mode 100644 modloader_plugin/CLEO_ModLoader_Plugin.vcxproj create mode 100644 modloader_plugin/CLEO_ModLoader_Plugin.vcxproj.filters create mode 100644 modloader_plugin/Resource.rc create mode 100644 modloader_plugin/source/ModLoaderProvider.cpp create mode 100644 modloader_plugin/source/ModLoaderProvider.h create mode 100644 modloader_plugin/source/main.cpp create mode 100644 modloader_plugin/source/modloader/modloader.h create mode 100644 modloader_plugin/source/modloader/modloader.hpp create mode 100644 modloader_plugin/source/utils.h create mode 100644 source/CModLoaderSystem.cpp create mode 100644 source/CModLoaderSystem.h diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fbc11198..987e1507 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,11 +69,34 @@ jobs: vt_api_key: ${{ secrets.VT_KEY }} files: './cleo_plugins/.output/*.cleo' + - name: ModLoader plugin - Build + shell: cmd + run: | + set PLUGIN_SDK_DIR=%GITHUB_WORKSPACE%\third-party\plugin-sdk + msbuild -m modloader_plugin/CLEO_ModLoader_Plugin.sln /property:Configuration=Release /property:Platform=x86 + + - name: ModLoader plugin - Sign + uses: x87/code-sign-action@develop + with: + certificate: '${{ secrets.DIG_KEY_CERT }}' + password: '${{ secrets.DIG_KEY_PWD }}' + certificatename: 'Seemann' + description: 'CLEO 5 ModLoader plugin' + timestampUrl: 'http://timestamp.digicert.com' + filename: './modloader_plugin/.output/Release/CLEO5_Provider.dll' + + - name: ModLoader plugin - VirusTotal Scan + uses: crazy-max/ghaction-virustotal@v4 + with: + vt_api_key: ${{ secrets.VT_KEY }} + files: './modloader_plugin/.output/Release/CLEO5_Provider.dll' + - name: Gather Output Files id: prepare_archive shell: cmd run: | - @REM create output directory + echo on + echo Creating output directories mkdir .output\Release\cleo mkdir .output\Release\cleo\.config mkdir .output\Release\cleo\cleo_modules @@ -82,19 +105,23 @@ jobs: mkdir .output\Release\cleo\cleo_text mkdir .output\Release\cleo_readme mkdir .output\Release\cleo_readme\examples + mkdir .output\Release\modloader + mkdir .output\Release\modloader\.data + mkdir .output\Release\modloader\.data\plugins - @REM copy files + echo Copying output files copy source\cleo_config.ini .output\Release\cleo\.cleo_config.ini copy cleo_plugins\.output\*.cleo .output\Release\cleo\cleo_plugins copy cleo_plugins\.output\*.ini .output\Release\cleo\cleo_plugins copy cleo_plugins\Audio\bass\bass.dll .output\Release\bass.dll + copy modloader_plugin\.output\Release\*.dll .output\Release\modloader\.data\plugins xcopy /E /I tests .output\Release\cleo xcopy /E /I examples .output\Release\cleo_readme\examples - @REM copy SDK + echo Copying CLEO SDK copy .output\Release\CLEO.lib cleo_sdk\CLEO.lib - @REM download Sanny Builder Library json + echo Downloading Sanny Builder Library json curl https://raw.githubusercontent.com/sannybuilder/library/master/sa/sa.json -o .output\Release\cleo\.config\sa.json - name: Convert Markdown to HTML diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ea22bac..ddc17357 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,25 +64,52 @@ jobs: with: vt_api_key: ${{ secrets.VT_KEY }} files: './cleo_plugins/.output/*.cleo' + + - name: ModLoader plugin - Build + shell: cmd + run: | + set PLUGIN_SDK_DIR=%GITHUB_WORKSPACE%\third-party\plugin-sdk + msbuild -m modloader_plugin/CLEO_ModLoader_Plugin.sln /property:Configuration=Release /property:Platform=x86 + + - name: ModLoader plugin - Sign + uses: x87/code-sign-action@develop + with: + certificate: '${{ secrets.DIG_KEY_CERT }}' + password: '${{ secrets.DIG_KEY_PWD }}' + certificatename: 'Seemann' + description: 'CLEO 5 ModLoader plugin' + timestampUrl: 'http://timestamp.digicert.com' + filename: './modloader_plugin/.output/Release/CLEO5_Provider.dll' + + - name: ModLoader plugin - VirusTotal Scan + uses: crazy-max/ghaction-virustotal@v4 + with: + vt_api_key: ${{ secrets.VT_KEY }} + files: './modloader_plugin/.output/Release/CLEO5_Provider.dll' - name: Gather Output Files id: prepare_archive shell: cmd run: | - @REM create output directory + echo on + echo Creating output directories mkdir .output\Release\cleo mkdir .output\Release\cleo\cleo_plugins - - @REM copy files - copy third-party\bass\bass.dll .output\Release\bass.dll + mkdir .output\Release\modloader + mkdir .output\Release\modloader\.data + mkdir .output\Release\modloader\.data\plugins + + echo Copying output files copy source\cleo_config.ini .output\Release\cleo\.cleo_config.ini copy cleo_plugins\.output\*.cleo .output\Release\cleo\cleo_plugins copy cleo_plugins\.output\*.ini .output\Release\cleo\cleo_plugins + copy cleo_plugins\Audio\bass\bass.dll .output\Release\bass.dll + copy modloader_plugin\.output\Release\*.dll .output\Release\modloader\.data\plugins - name: Upload Result uses: actions/upload-artifact@v4 with: - compression-level: 0 + compression-level: 6 #Default name: SA.CLEO5 path: | .output\Release\* diff --git a/CHANGELOG.md b/CHANGELOG.md index 258761a6..aa7d6ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 5.0.0 - support for CLEO modules feature https://github.com/sannybuilder/dev/issues/264 +- improved ModLoader support with CLEO plugin - new [Audio](https://github.com/cleolibrary/CLEO5/tree/master/cleo_plugins/Audio) plugin - audio related opcodes moved from CLEO core into separated plugin - CLEO's audio now obey game's volume settings diff --git a/CLEO5.vcxproj b/CLEO5.vcxproj index 3634d5bb..a4f3cdf2 100644 --- a/CLEO5.vcxproj +++ b/CLEO5.vcxproj @@ -45,6 +45,7 @@ + @@ -87,6 +88,7 @@ + diff --git a/CLEO5.vcxproj.filters b/CLEO5.vcxproj.filters index ca3d4e19..11f1c707 100644 --- a/CLEO5.vcxproj.filters +++ b/CLEO5.vcxproj.filters @@ -111,6 +111,9 @@ source\extensions + + source\extensions + third_party\plugin_sdk @@ -182,6 +185,9 @@ source\utils + + source\extensions + third_party\simdjson diff --git a/README.md b/README.md index 0632c761..e6a41fbe 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ CLEO itself does not replace any game file, however the following files and fold - cleo\cleo_plugins\SA.Text.cleo (text processing plugin) - cleo\cleo_saves\ (CLEO save directory) - cleo\cleo_text\ (CLEO text directory) +- modloader\.data\plugins\CLEO5_Provider.dll (CLEO5 ModLoader plugin) - cleo.asi (core library) - bass.dll (audio engine library) diff --git a/cleo_plugins/MemoryOperations/AntiHacks.h b/cleo_plugins/MemoryOperations/AntiHacks.h new file mode 100644 index 00000000..88842215 --- /dev/null +++ b/cleo_plugins/MemoryOperations/AntiHacks.h @@ -0,0 +1,32 @@ +#pragma once +#include "CLEO.h" +//#include "CFileMgr.h" +#include + +namespace AntiHacks +{ + // checks if specific function call should be allowed. Performs replacement action if needed + static bool CheckCall(CLEO::CRunningScript* thread, void* function, void* object, CLEO::SCRIPT_VAR* args, size_t argCount, DWORD& result) + { + switch ((size_t)function) + { + case 0x005387D0: // CFileMgr::SetDir(const char* relPath) // TODO: get the address from Plugin SDK + { + // some older mods use CFileMgr::SetDir directly instead of 0A99 opcode. In older CLEO versions 0A99 supported only few predefined locations + auto resolved = std::string(args[0].pcParam); + resolved.resize(511); + CLEO_ResolvePath(thread, resolved.data(), resolved.size()); + + CLEO::CLEO_SetScriptWorkDir(thread, resolved.c_str()); + return false; + } + break; + + default: + break; + } + + return true; // allow + } +}; + diff --git a/cleo_plugins/MemoryOperations/MemoryOperations.cpp b/cleo_plugins/MemoryOperations/MemoryOperations.cpp index d2ecb49d..40b4db43 100644 --- a/cleo_plugins/MemoryOperations/MemoryOperations.cpp +++ b/cleo_plugins/MemoryOperations/MemoryOperations.cpp @@ -1,5 +1,6 @@ #include "CLEO.h" #include "CLEO_Utils.h" +#include "AntiHacks.h" #include "plugin.h" #include "CTheScripts.h" #include @@ -159,26 +160,29 @@ class MemoryOperations } SCRIPT_VAR* arguments_end = arguments + numArg; - numPop *= 4; // bytes peer argument - DWORD result; - _asm - { - // transfer args to stack - lea ecx, arguments - call_func_loop : - cmp ecx, arguments_end - jae call_func_loop_end - push[ecx] - add ecx, 0x4 - jmp call_func_loop - call_func_loop_end : - - // call function - mov ecx, obj - xor eax, eax - call func - mov result, eax // get result - add esp, numPop // cleanup stack + numPop *= 4; // bytes peer argument + DWORD result = 0; + if (AntiHacks::CheckCall(thread, func, obj, scriptParams, numArg, result)) + { + _asm + { + // transfer args to stack + lea ecx, arguments + call_func_loop : + cmp ecx, arguments_end + jae call_func_loop_end + push[ecx] + add ecx, 0x4 + jmp call_func_loop + call_func_loop_end : + + // call function + mov ecx, obj + xor eax, eax + call func + mov result, eax // get result + add esp, numPop // cleanup stack + } } if (returnArg) diff --git a/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj b/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj index 8b6a4feb..86667c79 100644 --- a/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj +++ b/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj @@ -148,6 +148,7 @@ if defined GTA_SA_DIR ( + diff --git a/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj.filters b/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj.filters index 0b8bafb7..a7efc46d 100644 --- a/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj.filters +++ b/cleo_plugins/MemoryOperations/MemoryOperations.vcxproj.filters @@ -39,6 +39,7 @@ cleo_sdk + diff --git a/modloader_plugin/CLEO_ModLoader_Plugin.sln b/modloader_plugin/CLEO_ModLoader_Plugin.sln new file mode 100644 index 00000000..031388a3 --- /dev/null +++ b/modloader_plugin/CLEO_ModLoader_Plugin.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CLEO_ModLoader_Plugin", "CLEO_ModLoader_Plugin.vcxproj", "{E04BB51D-A37D-491E-BAA0-D95D0D5C1563}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E04BB51D-A37D-491E-BAA0-D95D0D5C1563}.Debug|x86.ActiveCfg = Debug|Win32 + {E04BB51D-A37D-491E-BAA0-D95D0D5C1563}.Debug|x86.Build.0 = Debug|Win32 + {E04BB51D-A37D-491E-BAA0-D95D0D5C1563}.Release|x86.ActiveCfg = Release|Win32 + {E04BB51D-A37D-491E-BAA0-D95D0D5C1563}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {27E214E3-AE89-40E1-9B76-0C5C9DB3452C} + EndGlobalSection +EndGlobal diff --git a/modloader_plugin/CLEO_ModLoader_Plugin.vcxproj b/modloader_plugin/CLEO_ModLoader_Plugin.vcxproj new file mode 100644 index 00000000..bcaf7cb9 --- /dev/null +++ b/modloader_plugin/CLEO_ModLoader_Plugin.vcxproj @@ -0,0 +1,143 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + + NotUsing + NotUsing + + + NotUsing + NotUsing + + + + + + + + + + + + + + + 16.0 + Win32Proj + {e04bb51d-a37d-491e-baa0-d95d0d5c1563} + modloaderplugin + 10.0 + CLEO_ModLoader_Plugin + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + $(ProjectDir)source\;$(IncludePath) + CLEO5_Provider + $(SolutionDir).output\$(Configuration)\ + $(SolutionDir).output\.obj\$(Configuration)\ + + + $(ProjectDir)source\;$(IncludePath) + CLEO5_Provider + $(SolutionDir).output\$(Configuration)\ + $(SolutionDir).output\.obj\$(Configuration)\ + + + + Level3 + true + _CRT_SECURE_NO_WARNINGS;WIN32;GTASA;TARGET_NAME=R"($(TargetName))";_DEBUG;MODLOADERPLUGIN_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreadedDebug + $(PLUGIN_SDK_DIR)\shared\;$(PLUGIN_SDK_DIR)\shared\game\;$(PLUGIN_SDK_DIR)\plugin_sa\game_sa\ + + + Windows + true + false + + + if defined GTA_SA_DIR ( +taskkill /IM gta_sa.exe /F /FI "STATUS eq RUNNING" +xcopy /Y "$(OutDir)$(TargetName).dll" "$(GTA_SA_DIR)\modloader\.data\plugins\" +xcopy /Y "$(OutDir)$(TargetName).pdb" "$(GTA_SA_DIR)\modloader\.data\plugins\" +) + + + TARGET_NAME=$(TargetFileName);%(PreprocessorDefinitions) + + + + + Level3 + true + true + true + _CRT_SECURE_NO_WARNINGS;WIN32;GTASA;TARGET_NAME=R"($(TargetName))";NDEBUG;MODLOADERPLUGIN_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + None + MultiThreaded + $(PLUGIN_SDK_DIR)\shared\;$(PLUGIN_SDK_DIR)\shared\game\;$(PLUGIN_SDK_DIR)\plugin_sa\game_sa\ + + + Windows + true + true + false + false + + + if defined GTA_SA_DIR ( +taskkill /IM gta_sa.exe /F /FI "STATUS eq RUNNING" +xcopy /Y "$(OutDir)$(TargetName).dll" "$(GTA_SA_DIR)\modloader\.data\plugins\" +) + + + TARGET_NAME=$(TargetFileName);%(PreprocessorDefinitions) + + + + + + \ No newline at end of file diff --git a/modloader_plugin/CLEO_ModLoader_Plugin.vcxproj.filters b/modloader_plugin/CLEO_ModLoader_Plugin.vcxproj.filters new file mode 100644 index 00000000..8a764a0a --- /dev/null +++ b/modloader_plugin/CLEO_ModLoader_Plugin.vcxproj.filters @@ -0,0 +1,34 @@ + + + + + + + + + + modloader + + + modloader + + + cleo_sdk + + + + cleo_sdk + + + + + {e1cecf56-63d9-4199-abc0-3bc243c35ae5} + + + {a91999b6-8f46-4d7d-bfde-64d0090de057} + + + + + + \ No newline at end of file diff --git a/modloader_plugin/Resource.rc b/modloader_plugin/Resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..e9c9f9b20eabc318bd515cfaf8870116be2192aa GIT binary patch literal 1572 zcmd6n&2H0B5QWb+NW6m;HXs#bsv-nJNFiyXLVl{o&8kwAG$yqOP8Fv>Umo~o634Mq z=zTyP)#vmYtH*kz zeeZYr?h(HcYw4En-O{YPYUorw?j})sU=^zz^%ArNT$bR+VD6$O>@7CP7H_Bl?1peU zXLaf^M~-m9I;fl&vhACv-6vRDK4t%4o8^Awoc{r_b1m+-`UuXs9Om{}nS+>;r1GU0?gE@!f%KQ=hzR23k=y>!umnCUjJcZK}`PnwdvALN!LHBZYdy zeWVG`b(hr~tLj-pp9Tyrh-qDW2Ks_dy`@W5C_C0y7}UKA^=-WZy~4WdmK~mB@A@Ts zH$DS1&!E7qqVzq-wbdApJkzv<7op3Do{XsHQ=SdpZFolP_W0_ue&!Xo+^QIP%ohAJ zvN2U}$SHwudQV1kP9|85x$nrt6t|4pj{KI@fU}f$gXfg9nD1;y-713)7Bg(-=(Yg+ z4xfabYgiPDR!;(Qy&|tbhtw|AYd!bXA=NA +#include + +using namespace std; +using namespace CLEO; + +const char* const ModLoaderProvider::Mod_Loader_Dir = "modloader\\"; +const char* const ModLoaderProvider::Cleo_Dir = "cleo\\"; + + +void ModLoaderProvider::Init(const char* gamePath) +{ + this->gamePath = gamePath; + NormalizePath(this->gamePath); + + modLoaderPath = this->gamePath + Mod_Loader_Dir; + cleoDirPath = this->gamePath + Cleo_Dir; +} + +bool ModLoaderProvider::IsHandled(const modloader::file& file) const +{ + if (file.is_dir()) return false; + + if (IsScript(file) || // all kind of CLEO scripts + file.is_ext("cleo") || // currently loaded by std.asi plugin anyway + file.is_ext("fxt")) // CLEO text dictionaries + { + return true; + } + + return false; +} + +bool ModLoaderProvider::IsCleoScript(const modloader::file& file) const +{ + if (file.is_dir()) return false; + + if (file.is_ext("cs") || + file.is_ext("cs3") || + file.is_ext("cs4")) + { + return true; + } + + return false; +} + +bool CLEO::ModLoaderProvider::IsScript(const modloader::file& file) const +{ + if (file.is_dir()) return false; + + if (IsCleoScript(file) || + file.is_ext("cm") || // CLEO mission + file.is_ext("s")) // used to store scripts that are not auto started + { + return true; + } + + return false; +} + +void ModLoaderProvider::AddFile(const modloader::file& file) +{ + files.insert(&file); +} + +void ModLoaderProvider::RemoveFile(const modloader::file& file) +{ + files.erase(&file); +} + +string ModLoaderProvider::ResolvePath(const char* scriptPath, const char* path) const +{ + if (files.empty() || path == nullptr || strlen(path) <= gamePath.length()) + { + return {}; + } + + string p = path; + NormalizePath(p); + + if (!utils::StrStartsWith(p, gamePath)) + { + return {}; // the path is not within game directory + } + + if (utils::StrStartsWith(p, modLoaderPath)) + { + return {}; // the path is already pointing inside 'modloader' directory, no need for resolving + } + + auto script = GetCleoFile(scriptPath); + if (script == nullptr) + { + return {}; // scriptPath is not part of any active mod + } + + // build moded path relative to that mod's directory + string modded; + if (utils::StrStartsWith(p, cleoDirPath)) // accessing cleo\* + { + // that script's cleo directory + modded = GetScriptsCleoDir(scriptPath); + + auto relative = p.c_str() + cleoDirPath.length(); // cut off cleo location from absolute path + if (strlen(relative) > 0) + { + modded += "\\"; + modded += relative; + } + } + else // any other path + { + modded = modLoaderPath; + modded += GetModName(*script); + + auto relative = p.c_str() + gamePath.length(); // cut off game location from absolute path + if (strlen(relative) > 0) + { + modded += "\\"; + modded += relative; + } + } + + if (GetFileAttributesA(modded.c_str()) != INVALID_FILE_ATTRIBUTES) + { + return modded; // file/dir found in mod's folder + } + + if (GetFileAttributesA(p.c_str()) == INVALID_FILE_ATTRIBUTES) + { + return modded; // both paths do not exists, so the mod perhaps tries to create a new file/directory + } + + return {}; +} + +const modloader::file* ModLoaderProvider::GetCleoFile(const char* filePath) const +{ + if (filePath == nullptr || strlen(filePath) <= modLoaderPath.length()) + return nullptr; // not located within 'modloader' directory + + string path = filePath; + NormalizePath(path); + + if (!utils::StrStartsWith(path, modLoaderPath)) + { + return nullptr; // not located inside 'modloader' directory + } + + path = path.substr(gamePath.length()); // cut game directory path prefix + auto scriptModFile = files.begin(); + while (true) + { + if (scriptModFile == files.end()) + { + return {}; // the script is not from ModLoader + } + + if (path == (*scriptModFile)->filebuffer()) + { + return *scriptModFile; // found + } + + scriptModFile++; + } + + return nullptr; +} + +string CLEO::ModLoaderProvider::GetScriptsCleoDir(const char* filePath) const +{ + auto script = GetCleoFile(filePath); + if (script == nullptr || !IsScript(*script)) + { + return {}; + } + + auto dir = string(utils::GetParentDirectory(script->filedir())); // relative path within mod's directory + + // By default ModLoader simply deep searched for any CLEO script files inside mod directory and mapped them as laying in game's main cleo directory. + // Do same thing, unless mod actually has some 'cleo' directory. This allows some more sophisticated mods to store subscripts in subdirectories, without MLoader autostarting them. + const size_t Cleo_Len = 4; + auto found = dir.find("cleo\\"); + while (found != string::npos) + { + // make sure it is full directory name, not just suffix + if (found == 0 || dir[found - 1] == '\\') + { + dir.resize(found + Cleo_Len); // cut after "cleo" + break; + } + + found = dir.find(dir.c_str() + found + Cleo_Len); // search for another + } + + // build absolute path + auto path = modLoaderPath; + path += GetModName(*script); + + if (!dir.empty()) + { + path += "\\"; + path += dir; + } + + return path; +} + +std::set ModLoaderProvider::ListFiles(const char* scriptPath, const char* searchPattern, bool listDirs, bool listFiles) const +{ + std::set files; + utils::ListFiles(searchPattern, files, listDirs, listFiles); // regular search + + if (scriptPath != nullptr) + { + string path = searchPattern; + NormalizePath(path); + if (utils::StrStartsWith(path, gamePath)) + { + auto script = GetCleoFile(scriptPath); // the script is ModLoader's mod? + if (script != nullptr) + { + // files inside mod directory will mask coresponding files in game + std::set masked; + for (const auto& file : files) + { + auto resolved = ResolvePath(scriptPath, file.c_str()); + masked.insert(resolved.empty() ? file : resolved); // select original or mod's file + } + files = masked; + + // search again, but now with mod location immitating game's directory + string moddedPattern = modLoaderPath; + moddedPattern += GetModName(*script); + moddedPattern += "\\"; + moddedPattern += searchPattern + gamePath.length(); // cut game directory path prefix + + utils::ListFiles(moddedPattern.c_str(), files, listDirs, listFiles); + } + } + } + + return files; +} + +set ModLoaderProvider::ListCleoFiles(const char* searchPattern) const +{ + set found; + if (files.empty()) return found; + + string prefix, suffix; + bool exact; + + auto pattern = string(searchPattern != nullptr ? searchPattern : ""); + auto pos = pattern.find('*'); // TODO: use real wildcards support + if (pos != string::npos) + { + prefix = pattern.substr(0, pos); + suffix = pattern.substr(pos + 1); + exact = false; + } + else + { + prefix = pattern; + exact = true; + } + + NormalizePath(prefix); + NormalizePath(suffix); + + for (const auto& file : files) + { + auto path = string(file->filedir()); + + if (exact) + { + if (path != prefix) // requires exact match + continue; + } + else + { + if (prefix.length() + suffix.length() > path.length()) + continue; // search pattern longer than actual file + + if (!prefix.empty() && !utils::StrStartsWith(path, prefix)) + continue; // prefix mismatch + + if (!suffix.empty() && strncmp(path.c_str() + path.length() - suffix.length(), suffix.c_str(), suffix.length()) != 0) + continue; // suffix mismatch + } + + found.insert(file->fullpath()); + } + + return found; +} + +set CLEO::ModLoaderProvider::ListCleoStartupScripts() const +{ + set found; + if (files.empty()) return found; + + for (const auto& file : files) + { + if (!IsCleoScript(*file)) + { + continue; + } + + string scriptPath = file->fullpath(); + NormalizePath(scriptPath); + + auto scriptDir = utils::GetParentDirectory(scriptPath); + auto cleoDir = GetScriptsCleoDir(scriptPath.c_str()); + + if (scriptDir.compare(cleoDir) == 0) + { + found.insert(file->fullpath()); + } + } + + return found; +} + +void ModLoaderProvider::NormalizePath(string& path) +{ + replace(path.begin(), path.end(), '/', '\\'); + transform(path.begin(), path.end(), path.begin(), [](unsigned char c) { return tolower(c); }); // to lower case +} + +string_view ModLoaderProvider::GetModName(const modloader::file& file) +{ + auto start = file.buffer + strlen(Mod_Loader_Dir); + + size_t length; + auto separator = strchr(start, '\\'); + if (separator != nullptr) + { + length = (size_t)separator - (size_t)start; // distance + } + else + { + length = strlen(start); + } + + return std::string_view(start, length); +} diff --git a/modloader_plugin/source/ModLoaderProvider.h b/modloader_plugin/source/ModLoaderProvider.h new file mode 100644 index 00000000..c46a74eb --- /dev/null +++ b/modloader_plugin/source/ModLoaderProvider.h @@ -0,0 +1,43 @@ +#include "modloader\modloader.hpp" +#include +#include + +namespace CLEO +{ + class CRunningScript; + + class ModLoaderProvider + { + public: + ModLoaderProvider() = default; + ~ModLoaderProvider() = default; + + void Init(const char* gamePath); + + bool IsHandled(const modloader::file&) const; // is this file interesting to the provider + bool IsCleoScript(const modloader::file&) const; // is CleoScript file? (*.cs or legacy variants) + bool IsScript(const modloader::file&) const; // is filetype any of known to store CLEO scripts? + void AddFile(const modloader::file&); + void RemoveFile(const modloader::file&); + + std::string ResolvePath(const char* scriptPath, const char* path) const; // resolve regular absolute path to actual ModLoader file. CLEO's virtual paths are not supported there! + const modloader::file* GetCleoFile(const char* filePath) const; // get descriptor of listed file + std::string GetScriptsCleoDir(const char* filePath) const; // get cleo directory location for specific script + + std::set ListFiles(const char* scriptPath, const char* searchPattern, bool listDirs = true, bool listFiles = true) const; + std::set ListCleoFiles(const char* searchPattern) const; + std::set ListCleoStartupScripts() const; // all CLEO scripts that appear to be in 'cleo' directory + + private: + static const char* const Mod_Loader_Dir; + static const char* const Cleo_Dir; + + std::string gamePath; + std::string modLoaderPath; + std::string cleoDirPath; + std::set files; + + static void NormalizePath(std::string& path); + static std::string_view GetModName(const modloader::file&); + }; +} diff --git a/modloader_plugin/source/main.cpp b/modloader_plugin/source/main.cpp new file mode 100644 index 00000000..a6442a31 --- /dev/null +++ b/modloader_plugin/source/main.cpp @@ -0,0 +1,115 @@ +#include "ModLoaderProvider.h" +#include "utils.h" +#include "..\..\cleo_sdk\CLEO.h" +#include "..\..\cleo_sdk\CLEO_Utils.h" +#include "modloader\modloader.h" +#include "modloader\modloader.hpp" +#include + +class ModLoaderPlugin : public modloader::basic_plugin +{ +public: + const info& GetInfo(); + bool OnStartup(); + bool OnShutdown(); + int GetBehaviour(modloader::file&); + bool InstallFile(const modloader::file&); + bool ReinstallFile(const modloader::file&); + bool UninstallFile(const modloader::file&); + + CLEO::ModLoaderProvider modloaderProvider; +} mlPlugin; +REGISTER_ML_PLUGIN(::mlPlugin); + +const ModLoaderPlugin::info& ModLoaderPlugin::GetInfo() +{ + static const char* extable[] = { "cleo", "cm", "cs", "cs3", "cs4", ".fxt", nullptr }; + static const info xinfo = { "CLEO5", CLEO_VERSION_STR, "Miran", -1, extable }; + return xinfo; +} + +bool ModLoaderPlugin::OnStartup() +{ + modloaderProvider.Init(loader->gamepath); + return true; +} + +bool ModLoaderPlugin::OnShutdown() +{ + return true; +} + +int ModLoaderPlugin::GetBehaviour(modloader::file& file) +{ + return modloaderProvider.IsHandled(file) ? MODLOADER_BEHAVIOUR_CALLME : MODLOADER_BEHAVIOUR_NO; +} + +bool ModLoaderPlugin::InstallFile(const modloader::file& file) +{ + modloaderProvider.AddFile(file); + return false; +} + +bool ModLoaderPlugin::ReinstallFile(const modloader::file& file) +{ + return false; +} + +bool ModLoaderPlugin::UninstallFile(const modloader::file& file) +{ + modloaderProvider.RemoveFile(file); + return false; +} + +// search in CLEO related type files registered by mods +extern "C" __declspec(dllexport) CLEO::StringList ListCleoFiles(const char* searchPattern) +{ + auto found = mlPlugin.modloaderProvider.ListCleoFiles(searchPattern); + return CLEO::CreateStringList(found); +} + +// search for all scripts that appear to be in 'cleo' directory +extern "C" __declspec(dllexport) CLEO::StringList ListCleoStartupScripts() +{ + auto found = mlPlugin.modloaderProvider.ListCleoStartupScripts(); + return CLEO::CreateStringList(found); +} + +// generic file search including files from loaded mods +// scriptPath can be null +extern "C" __declspec(dllexport) CLEO::StringList ListFiles(const char* scriptPath, const char* searchPattern, BOOL listDirs, BOOL listFiles) +{ + auto found = mlPlugin.modloaderProvider.ListFiles(scriptPath, searchPattern, listDirs, listFiles); + return CLEO::CreateStringList(found); +} + +// required as free can not be called from another module +extern "C" __declspec(dllexport) void StringListFree(CLEO::StringList list) +{ + if (list.count > 0 && list.strings != nullptr) + { + for (DWORD i = 0; i < list.count; i++) + { + free(list.strings[i]); + } + + free(list.strings); + } +} + +extern "C" __declspec(dllexport) BOOL ResolvePath(const char* scriptPath, char* path, DWORD pathMaxSize) +{ + auto resolved = mlPlugin.modloaderProvider.ResolvePath(scriptPath, path); + + if (!resolved.empty()) + { + if (resolved.length() >= pathMaxSize) + resolved.resize(pathMaxSize - 1); // and terminator character + + std::memcpy(path, resolved.c_str(), resolved.length() + 1); // with terminator + + return true; + } + + return false; +} diff --git a/modloader_plugin/source/modloader/modloader.h b/modloader_plugin/source/modloader/modloader.h new file mode 100644 index 00000000..43440e79 --- /dev/null +++ b/modloader_plugin/source/modloader/modloader.h @@ -0,0 +1,337 @@ +/* + * Mod Loader C Plugin Interface + * The interface is extremly simple and you don't have to link with modloader. + * The only thing you are requiered to do is export a 'GetPluginData' function (see below for the prototype). + * Put your plugin at '/modloader/.data/plugins' folder. + * + * + * This source code is offered for use in the public domain. You may + * use, modify or distribute it freely. + * + * This code is distributed in the hope that it will be useful but + * WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY + * DISCLAIMED. This includes but is not limited to warranties of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE + * + */ +#ifndef MODLOADER_H +#define MODLOADER_H +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Version */ +#define MODLOADER_VERSION_MAJOR 0 +#define MODLOADER_VERSION_MINOR 3 +#define MODLOADER_VERSION_REVISION 7 +#ifdef NDEBUG +#define MODLOADER_VERSION_ISDEV 0 +#else +#define MODLOADER_VERSION_ISDEV 1 +#endif + + +/************************************** + * CONSTANTS + **************************************/ + +/* Check file result */ +#define MODLOADER_BEHAVIOUR_NO 0 +#define MODLOADER_BEHAVIOUR_YES 1 +#define MODLOADER_BEHAVIOUR_CALLME 2 + +/* modloader_file_t flags */ +#define MODLOADER_FF_IS_DIRECTORY 1 + + +/************************************** + * COMMON DATA TYPES + **************************************/ + +/* Forwarding */ +struct modloader_t; +struct CLEO_ModLoader_Plugin_t; + + +/* + * modloader_mod_t + * This structure represents a mod + */ +typedef struct +{ + uint64_t id; // Unique mod id + uint32_t priority; // Mod priority + uint32_t _pad1; + +} modloader_mod_t; + + +/* + * modloader_file_t + * This structure represents a file + */ +typedef struct +{ + uint32_t flags; /* File flags */ + const char* buffer; /* Pointer to the file buffer... that's the file path relative to game dir */ + uint8_t pos_eos; /* The null terminator position (length of the string) */ + uint8_t pos_filedir; /* The position of the filepath relative to the mod folder (e.g. "modloader/my mod/stuff/a.dat" -> "stuff/a.dat") */ + uint8_t pos_filename; /* The position of the file name */ + uint8_t pos_filext; /* The position of the file extension */ + uint32_t hash; /* The filename hash (as in "modloader/util/hash.hpp" */ + uint32_t _rsv1; /* Reserved */ + modloader_mod_t* parent; /* The mod owner of this file */ + uint64_t size; /* Size of the file in bytes */ + uint64_t time; /* File modification time */ + /* (as FILETIME, 100-nanosecond intervals since January 1, 1601 UTC) */ + uint64_t behaviour; /* The file behaviour */ + +} modloader_file_t; + + + + + + +/************************************** + * THE LOADER EXPORTS + **************************************/ + +/* + * Shared data between plugins + */ +#define MODLOADER_SHDATA_ANY 0 /* Could be any data (default value) */ +#define MODLOADER_SHDATA_EMPTY 1 /* Empty variable */ +#define MODLOADER_SHDATA_FUNCTION 2 /* Function variable */ +#define MODLOADER_SHDATA_POINTER 3 /* Pointer variable */ +#define MODLOADER_SHDATA_INT 4 /* Signed 32 Bits Integer */ +#define MODLOADER_SHDATA_UINT 5 /* Unsigned 32 Bits Integer */ +/*#define MODLOADER_SHDATA_LIST 10 /* Linked list */ + +struct modloader_shdata_t +{ + uint32_t type; /* Type of the data as in MODLOADER_SHDATA_* constants */ + + /* Data */ + union { + void* p; /* MODLOADER_SHDATA_POINTER pointer */ + void* f; /* MODLOADER_SHDATA_FUNCTION function pointer */ + int32_t i; /* MODLOADER_SHDATA_INT */ + uint32_t u; /* MODLOADER_SHDATA_UINT */ + }; +}; + +/* + * modloader_fCreateSharedData -> Creates a shared data named @name. Returns a pointer to the created shared data or NULL on failure. + * modloader_fDeleteSharedData -> Frees a previosly @data created using CreateSharedData. + * modloader_fFindSharedData -> Gets the pointer of the shared data named @name. Returns the pointer to the shared data object or NULL on failure. + */ +typedef modloader_shdata_t* (*modloader_fCreateSharedData)(const char* name); +typedef void (*modloader_fDeleteSharedData)(modloader_shdata_t* data); +typedef modloader_shdata_t* (*modloader_fFindSharedData)(const char* name); + + +/* + * Log + * Logs something into the modloader log file + */ +typedef void (*modloader_fLog)(const char* msg, ...); +typedef void (*modloader_fvLog)(const char* msg, va_list va); + +/* + * Error + * Displays a message box with a error message + * Log may suit your needs. + */ +typedef void (*modloader_fError)(const char* errmsg, ...); + + +/* ---- Interface ---- */ +typedef struct modloader_t +{ + const char* gamepath; /* game path */ + const char* _rsvc; /* (deprecated - reserved) */ + const char* commonappdata; /* fullpath to a "modloader/" directory in the %ProgramData% directory */ + const char* localappdata; /* fullpath to a "modloader/" directory in the "%LocalAppData% directory */ + const char* _rsv0[2]; /* Reserved */ + + uint32_t _rsv1[4]; /* Reserved */ + uint8_t has_game_started; + uint8_t has_game_loaded; + uint8_t _rsv3; /* Reserved */ + uint8_t _rsv4; /* Reserved */ + + /* Interface */ + modloader_fLog Log; + modloader_fvLog vLog; + modloader_fError Error; + modloader_fCreateSharedData CreateSharedData; + modloader_fDeleteSharedData DeleteSharedData; + modloader_fFindSharedData FindSharedData; + +} modloader_t; + + + + + + +/************************************** + * THE PLUGIN EXPORTS + **************************************/ + +/* + You have to export this function! + Then you shall JUST (and JUST) fill 'major', 'minor' and 'revision' with the values of + MODLOADER_VERSION_MAJOR, MODLOADER_VERSION_MINOR, MODLOADER_VERSION_REVISION macros. + If the version checking goes okay, GetPluginData will get called. +*/ +typedef void (*modloader_fGetLoaderVersion)(uint8_t* major, uint8_t* minor, uint8_t* revision); + +/* + You have to export this function! + Then you shall JUST (and JUST) fill 'data' with the plugin information. + If everything goes okay, data->OnStartup will get called. + Note that data->modloader is already set here :) +*/ +typedef void (*modloader_fGetPluginData)(CLEO_ModLoader_Plugin_t* data); + +/* + * GetVersion + * Get plugin version string (e.g. "1.4") + * This will show up once, when the plugin get loaded + * @data: The plugin data + */ +typedef const char* (*modloader_fGetVersion)(CLEO_ModLoader_Plugin_t* data); + +/* + * GetAuthor + * Get plugin author (e.g. "My Name") + * This will show up once, when the plugin get loaded + * @data: The plugin data + */ +typedef const char* (*modloader_fGetAuthor)(CLEO_ModLoader_Plugin_t* data); + + +/* + * OnStartup + * Plugin started up, this happens before the the game engine starts. + * @data: The plugin shall fill 'data' parameter with it's information. + * @return 0 on success and 1 on failure; + */ +typedef int (*modloader_fOnStartup)(CLEO_ModLoader_Plugin_t* data); + +/* + * OnShutdown + * Plugin shut dowing + * @data: The plugin data + * @return 0 on success and 1 on failure; + */ +typedef int (*modloader_fOnShutdown)(CLEO_ModLoader_Plugin_t* data); + +/* + * GetBehaviour + * Gets the behaviour of this plugin in relation to the specified file + * @data: The plugin data + * @return MODLOADER_BEHAVIOUR_NO for no relationship + * MODLOADER_BEHAVIOUR_YES for strong relationship, this plugin will handle the file installation. Should set file->behaviour to a file-unique id. + * MODLOADER_BEHAVIOUR_CALLME for weak relationship, this plugin won't handle the file installation but will receive it at Install, Uninstall and so on. + * + * + */ +typedef int (*modloader_fGetBehaviour)(CLEO_ModLoader_Plugin_t* data, modloader_file_t* file); + +/* + * InstallFile + * Called to install a file previosly checked as MODLOADER_BEHAVIOUR_YES or MODLOADER_BEHAVIOUR_CALLME + * @data: The plugin data + * @return 0 on success and 1 on failure; + */ +typedef int (*modloader_fInstallFile)(CLEO_ModLoader_Plugin_t* data, const modloader_file_t* file); + + +/* + * ReinstallFile + * Called to reinstall a file previosly installed, the file was updated + * @data: The plugin data + * @return 0 on success and 1 on failure; + */ +typedef int (*modloader_fReinstallFile)(CLEO_ModLoader_Plugin_t* data, const modloader_file_t* file); + + +/* + * UninstallFile + * Called to uninstall a file previosly installed with 'InstallFile' + * @data: The plugin data + * @return 0 on success and 1 on failure; + */ +typedef int (*modloader_fUninstallFile)(CLEO_ModLoader_Plugin_t* data, const modloader_file_t* file); + + +/* + * Update + * Update is called after a serie of install/uninstalls, maybe you need a delayed refresh + * @data: The plugin data + */ +typedef void (*modloader_fUpdate)(CLEO_ModLoader_Plugin_t* data); + + + + +/* ---- Interface ---- Should be compatible with all versions of modloader.asi */ +typedef struct CLEO_ModLoader_Plugin_t +{ + // Data to be set by Mod Loader itself, read-only data for plugins! + struct + { + uint8_t major, minor, revision, _pad0; + void *pThis, *pModule; /* this pointer and HMODULE */ + const char *name, *author, *version; /* Plugin info */ + modloader_t* loader; /* Modloader pointer */ + uint8_t has_started; /* Determines whether the plugin has started up successfully */ + uint8_t _pad1[3]; /* Reserved */ + }; + + /* Userdata, set it to whatever you want */ + void* userdata; + + /* Extension table, set it to a pointer of extensions that this plugin handles */ + const char** extable; /* Can be null if extable_len is equal to zero */ + size_t extable_len; /* The length of the extension table */ + + /* + * The plugin priority, normally this is set outside the plugin in a config file, not recommend to touch this value. + * Mod Loader sets this to the default priority "50"; "0" means ignore this plugin. + */ + int priority; + + /* Callbacks */ + modloader_fGetAuthor GetAuthor; + modloader_fGetVersion GetVersion; + modloader_fOnStartup OnStartup; + modloader_fOnShutdown OnShutdown; + modloader_fGetBehaviour GetBehaviour; + modloader_fInstallFile InstallFile; + modloader_fReinstallFile ReinstallFile; + modloader_fUninstallFile UninstallFile; + modloader_fUpdate Update; + +} CLEO_ModLoader_Plugin_t; + + + + + + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/modloader_plugin/source/modloader/modloader.hpp b/modloader_plugin/source/modloader/modloader.hpp new file mode 100644 index 00000000..4bee38c2 --- /dev/null +++ b/modloader_plugin/source/modloader/modloader.hpp @@ -0,0 +1,290 @@ +/* + * Mod Loader C++ Plugin Interface + * + * Take a look at "doc/Creating Your Own Plugin.txt" + * + * This source code is offered for use in the public domain. You may + * use, modify or distribute it freely. + * + * This code is distributed in the hope that it will be useful but + * WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY + * DISCLAIMED. This includes but is not limited to warranties of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE + * + */ +#ifndef MODLOADER_HPP +#define MODLOADER_HPP +#pragma once +#include +//#include +#include +#include + +namespace modloader +{ + /* + modloader::plugin + Represents a loaded mod loader plugin + */ + struct plugin : public CLEO_ModLoader_Plugin_t + { + }; + + /* + modloader::mod + Represents a mod in mod loader directory tree + */ + struct mod : public modloader_mod_t + { + }; + + /* + modloader::file + Represents a handleable file in mod loader directory tree + */ + struct file : public modloader_file_t + { + // Checks if this file is a directory + bool is_dir() const { return (flags & MODLOADER_FF_IS_DIRECTORY) != 0; } + + // Checks if this file extension is the same as 'e' + bool is_ext(const char* e) const { return !_stricmp(e, filext()); } + + // Gets the full file path (including driver, etc) + std::string fullpath() const; + std::string& fullpath(std::string& out) const; + + // Gets the filebuffer that stores the underlying path + const char* filebuffer(size_t idx = 0) const { return &buffer[idx]; } + size_t filebuffer_len() const { return (size_t)(pos_eos); } + + // Gets the filepath relative to the game dir + const char* filepath() const { return filebuffer(0); } + // Gets the filepath relative to the mod folder + const char* filedir() const { return filebuffer(pos_filedir); } + // Gets the filename + const char* filename() const { return filebuffer(pos_filename); } + // Gets the file extension + const char* filext() const { return filebuffer(pos_filext); } + + // Checks if this file changed compared to 'c' + // Checks only for file size and file time, returns false for directories + bool has_changed(const file& c) const + { + return !is_dir() && !(time == c.time && size == c.size); + } + + // Helpers for working with behaviour bits + + template + static uint64_t set_mask(uint64_t mask, uint64_t umask, uint32_t shift, T value) + { + return ((mask & ~(umask << shift)) | (uint64_t(value) << shift)); + } + + template + static T get_mask(uint64_t mask, uint64_t umask, uint32_t shift) + { + return T((mask >> shift) & umask); + } + }; + + // Assert the size of the above wrappers + static_assert(sizeof(mod) == sizeof(modloader_mod_t), "Invalid mod inheritance size"); + static_assert(sizeof(file) == sizeof(modloader_file_t), "Invalid file inheritance size"); + static_assert(sizeof(plugin) == sizeof(CLEO_ModLoader_Plugin_t), "Invalid plugin inheritance size"); + + /* + modloader::basic_plugin + The base for any custom plugin + */ + class basic_plugin + { + public: + friend struct basic_plugin_wrapper; + + // Stores information about this plugin + struct info + { + const char* name; // Useless, name of the plugin + const char* version; // Plugin version + const char* author; // Plugin author + int default_priority; // Plugin default priority (or -1 for mod loader default) + const char** extable; // Extension table of possible files this plugin can handle, to speed up lookup + }; + + public: + const modloader_t* loader; // Pointer to the Mod Loader basic information + const plugin* data; // Pointer to this plugin data + modloader_fLog Log ; // Log(fmt, ...) + modloader_fvLog vLog; // vLog(fmt, va_list) + modloader_fError Error; // Error(fmt, ...) + + // Casts this base to a derived object + template + To& cast() { return *static_cast(this); } + + public: // Check out "doc/Creating Your Own Plugin.txt" for detailed information!!!!!!!!!! + virtual const info& GetInfo()=0; // Gets the plugin information such as version and author + virtual bool OnStartup() { return true; } // To startup the plugin + virtual bool OnShutdown() { return true; } // To shutdown the plugin + virtual int GetBehaviour(file&)=0; // Gets the behaviour of a specific file in relation to this plugin + virtual bool InstallFile(const file&)=0; // Installs a new file + virtual bool ReinstallFile(const file&)=0; // Reinstalls a file previosly installed + virtual bool UninstallFile(const file&)=0; // Uninstalls a file previosly installed + virtual void Update() {} // Updates the state of the plugin after a serie of install/uninstall/reinstall + }; + + + // Binding the C interface to the C++ interface + struct basic_plugin_wrapper + { + static basic_plugin& GetThis(CLEO_ModLoader_Plugin_t* data) + { + return *(basic_plugin*)(data->pThis); + } + + static file& GetFile(const modloader_file_t* f) + { + return *(const_cast(static_cast(f))); + } + + static const char* GetAuthor(CLEO_ModLoader_Plugin_t* data) + { + return GetThis(data).GetInfo().author; + } + + static const char* GetVersion(CLEO_ModLoader_Plugin_t* data) + { + return GetThis(data).GetInfo().version; + } + + static int OnStartup(CLEO_ModLoader_Plugin_t* data) + { + return !GetThis(data).OnStartup(); + } + + static int OnShutdown(CLEO_ModLoader_Plugin_t* data) + { + return !GetThis(data).OnShutdown(); + } + + static int GetBehaviour(CLEO_ModLoader_Plugin_t* data, modloader_file_t* file) + { + return GetThis(data).GetBehaviour(GetFile(file)); + } + + static int InstallFile(CLEO_ModLoader_Plugin_t* data, const modloader_file_t* file) + { + return !GetThis(data).InstallFile(GetFile(file)); + } + + static int ReinstallFile(CLEO_ModLoader_Plugin_t* data, const modloader_file_t* file) + { + return !GetThis(data).ReinstallFile(GetFile(file)); + } + + static int UninstallFile(CLEO_ModLoader_Plugin_t* data, const modloader_file_t* file) + { + return !GetThis(data).UninstallFile(GetFile(file)); + } + + static void Update(CLEO_ModLoader_Plugin_t* data) + { + return GetThis(data).Update(); + } + + // Attaches the 'interface' using the plugin 'data' + static void RegisterPluginData(basic_plugin& interfc, CLEO_ModLoader_Plugin_t* data) + { + int priority = interfc.GetInfo().default_priority; + + // Register version this plugin was built in + data->major = MODLOADER_VERSION_MAJOR; + data->minor = MODLOADER_VERSION_MINOR; + data->revision = MODLOADER_VERSION_REVISION; + + // Register interface this pointer + data->pThis = &interfc; + + // Callbacks + data->GetVersion = &basic_plugin_wrapper::GetVersion; + data->GetAuthor = &basic_plugin_wrapper::GetAuthor; + data->OnStartup = &basic_plugin_wrapper::OnStartup; + data->OnShutdown = &basic_plugin_wrapper::OnShutdown; + data->GetBehaviour = &basic_plugin_wrapper::GetBehaviour; + data->InstallFile = &basic_plugin_wrapper::InstallFile; + data->ReinstallFile = &basic_plugin_wrapper::ReinstallFile; + data->UninstallFile = &basic_plugin_wrapper::UninstallFile; + data->Update = &basic_plugin_wrapper::Update; + + // Custom priority + if(priority != -1) data->priority = priority; + + // Get Extension Table + if(data->extable = interfc.GetInfo().extable) + { + for(const char** extable = data->extable; *extable; ++extable) + ++data->extable_len; + } + else + { + data->extable_len = 0; + } + + // Mod Loader stuff + interfc.data = reinterpret_cast(data); + interfc.loader = data->loader; + interfc.Error = interfc.loader->Error; + interfc.Log = interfc.loader->Log; + interfc.vLog = interfc.loader->vLog; + } + }; + + /* + * Plugin object data + */ + extern basic_plugin* plugin_ptr; + + /* + Implementation of some modloader::file methods + */ + inline std::string& file::fullpath(std::string& out) const + { + out.assign(std::string(plugin_ptr->loader->gamepath)); + out.append(this->filepath()); + return out; + } + + inline std::string file::fullpath() const + { + std::string out; + this->fullpath(out); + return out; + } + + + // You need to use those to register the existence of your plugin + #define REGISTER_ML_PLUGIN(plugin) REGISTER_ML_PLUGIN_PTR(&plugin); + #define REGISTER_ML_NULL() REGISTER_ML_PLUGIN_PTR(nullptr) + + // Backend for REGISTER_ML_PLUGIN + #define REGISTER_ML_PLUGIN_PTR(ptr) namespace modloader {\ + modloader::basic_plugin* plugin_ptr = ptr;\ + extern "C" __declspec(dllexport) void GetPluginData(CLEO_ModLoader_Plugin_t* data)\ + {\ + if(plugin_ptr) basic_plugin_wrapper::RegisterPluginData(*plugin_ptr, data);\ + }\ + extern "C" __declspec(dllexport) void GetLoaderVersion(uint8_t* major, uint8_t* minor, uint8_t* revision)\ + {\ + *major = MODLOADER_VERSION_MAJOR;\ + *minor = MODLOADER_VERSION_MINOR;\ + *revision = MODLOADER_VERSION_REVISION;\ + }\ + } + + +}; + + +#endif diff --git a/modloader_plugin/source/utils.h b/modloader_plugin/source/utils.h new file mode 100644 index 00000000..d8d27cb0 --- /dev/null +++ b/modloader_plugin/source/utils.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace utils +{ + static void ListFiles(const char* searchPattern, std::set& output, bool listDirs = true, bool listFiles = true) + { + WIN32_FIND_DATAA wfd = { 0 }; + HANDLE hSearch = FindFirstFileA(searchPattern, &wfd); + if (hSearch == INVALID_HANDLE_VALUE) + { + return; + } + + do + { + if (!listDirs && (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + { + continue; // skip directories + } + + if (!listFiles && !(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + { + continue; // skip files + } + + auto path = std::filesystem::path(wfd.cFileName); + if (!path.is_absolute()) // keep absolute in case somebody hooked the APIs to return so + path = std::filesystem::path(searchPattern).parent_path() / path; + + output.insert(path.string()); + } + while (FindNextFileA(hSearch, &wfd)); + + FindClose(hSearch); + } + + static bool StrStartsWith(const std::string_view str, const std::string_view prefix, bool caseSensitive = true) + { + if (str.length() < prefix.length()) + { + return false; + } + + if (caseSensitive) + { + return strncmp(str.data(), prefix.data(), prefix.length()) == 0; + } + else + { + return _strnicmp(str.data(), prefix.data(), prefix.length()) == 0; + } + } + + static const std::string_view GetParentDirectory(const std::string_view str) + { + auto separatorPos = str.find_last_of('\\'); + + if (separatorPos == std::string::npos) + { + return {}; + } + + return std::string_view(str.data(), separatorPos); + } +} diff --git a/source/CCustomOpcodeSystem.cpp b/source/CCustomOpcodeSystem.cpp index 830f32d3..c199ae4d 100644 --- a/source/CCustomOpcodeSystem.cpp +++ b/source/CCustomOpcodeSystem.cpp @@ -837,7 +837,18 @@ namespace CLEO { OPCODE_READ_PARAM_STRING(path); - auto filename = reinterpret_cast(thread)->ResolvePath(path, DIR_CLEO); // legacy: default search location is game\cleo directory + std::string filename; + filename.reserve(MAX_PATH); + if (!FS::path(path).is_absolute()) + { + filename = DIR_CLEO; // legacy: default search location is game\cleo directory + filename += '\\'; + } + filename += path; + + CLEO_ResolvePath(thread, filename.data(), filename.capacity()); + + TRACE(""); // separator TRACE("[0A92] Starting new custom script %s from thread named %s", filename.c_str(), thread->GetName().c_str()); auto cs = new CCustomScript(filename.c_str(), false, thread); @@ -877,8 +888,19 @@ namespace CLEO { OPCODE_READ_PARAM_STRING(path); - auto filename = reinterpret_cast(thread)->ResolvePath(path, DIR_CLEO); // legacy: default search location is game\cleo directory - filename += ".cm"; // add custom mission extension + std::string filename; + filename.reserve(MAX_PATH); + if (!FS::path(path).is_absolute()) + { + filename = DIR_CLEO; // legacy: default search location is game\cleo directory + filename += '\\'; + } + filename += path; + filename += ".cm"; // custom mission filetype + + CLEO_ResolvePath(thread, filename.data(), filename.capacity()); + + TRACE(""); // separator TRACE("[0A94] Starting new custom mission %s from thread named %s", filename.c_str(), thread->GetName().c_str()); auto cs = new CCustomScript(filename.c_str(), true, thread); @@ -1830,10 +1852,20 @@ extern "C" return texture; } - CLEO::CRunningScript* WINAPI CLEO_CreateCustomScript(CLEO::CRunningScript* fromThread, const char *script_name, int label) + CLEO::CRunningScript* WINAPI CLEO_CreateCustomScript(CLEO::CRunningScript* fromThread, const char* script_name, int label) { - auto filename = reinterpret_cast(fromThread)->ResolvePath(script_name, DIR_CLEO); // legacy: default search location is game\cleo directory + std::string filename; + filename.reserve(MAX_PATH); + if (!FS::path(script_name).is_absolute()) + { + filename = DIR_CLEO; // legacy: default search location is game\cleo directory + filename += '\\'; + } + filename += script_name; + + CLEO_ResolvePath(fromThread, filename.data(), filename.capacity()); + TRACE(""); // separator if (label != 0) // create from label { TRACE("Starting new custom script from thread named %s label %i", filename.c_str(), label); diff --git a/source/CModLoaderSystem.cpp b/source/CModLoaderSystem.cpp new file mode 100644 index 00000000..6100467d --- /dev/null +++ b/source/CModLoaderSystem.cpp @@ -0,0 +1,56 @@ +#include "stdafx.h" +#include "CModLoaderSystem.h" +#include "CCodeInjector.h" + +using namespace std; +using namespace CLEO; + +extern const void (__cdecl* StringListFree)(StringList); +extern const BOOL (__cdecl* ResolvePath)(const char* scriptPath, char* path, DWORD pathMaxSize); +extern const StringList (__cdecl* ListFiles)(const char* scriptPath, const char* searchPattern, BOOL listDirs, BOOL listFiles); +extern const StringList (__cdecl* ListCleoFiles)(const char* directory, const char* filePattern); +extern const StringList(__cdecl* ListCleoStartupScripts)(); + + +void CModLoaderSystem::Init() +{ + if (initialized) return; + initialized = true; + + TRACE(""); // separator + TRACE("ModLoader system initialization..."); + + if (GetModuleHandleA("modloader.asi") == NULL) + { + TRACE("ModLoader not detected."); + return; + } + TRACE("ModLoader detected!"); + + auto provider = GetModuleHandleA("CLEO5_Provider.dll"); // CLEO's Mod Loader plugin + if (provider == NULL) + { + SHOW_ERROR("CLEO's ModLoader plugin was not detected!\nPlease make sure CLEO5_Provider.dll is present."); + return; + } + + StringListFree = memory_pointer(GetProcAddress(provider, "StringListFree")); + ResolvePath = memory_pointer(GetProcAddress(provider, "ResolvePath")); + ListFiles = memory_pointer(GetProcAddress(provider, "ListFiles")); + ListCleoFiles = memory_pointer(GetProcAddress(provider, "ListCleoFiles")); + ListCleoStartupScripts = memory_pointer(GetProcAddress(provider, "ListCleoStartupScripts")); + + if (StringListFree == nullptr || ResolvePath == nullptr || ListFiles == nullptr || ListCleoFiles == nullptr || ListCleoStartupScripts == nullptr) + { + SHOW_ERROR("CLEO's ModLoader plugin initialization error! Plugin corrupted or outdated."); + return; + } + + TRACE("ModLoader support active."); + active = true; +} + +bool CModLoaderSystem::IsActive() const +{ + return active; +} diff --git a/source/CModLoaderSystem.h b/source/CModLoaderSystem.h new file mode 100644 index 00000000..94887f10 --- /dev/null +++ b/source/CModLoaderSystem.h @@ -0,0 +1,24 @@ +namespace CLEO +{ + class CModLoaderSystem + { + bool initialized = false; + bool active = false; // ModLoader present? + + public: + CModLoaderSystem() = default; + CModLoaderSystem(const CModLoaderSystem&) = delete; // no copying + ~CModLoaderSystem() = default; + void Init(); + + bool IsActive() const; + + const void (__cdecl* StringListFree)(CLEO::StringList) = nullptr; + + const BOOL (__cdecl* ResolvePath)(const char* scriptPath, char* path, DWORD pathMaxSize) = nullptr; + + const CLEO::StringList (__cdecl* ListFiles)(const char* scriptPath, const char* searchPattern, BOOL listDirs, BOOL listFiles) = nullptr; + const CLEO::StringList (__cdecl* ListCleoFiles)(const char* searchPattern) = nullptr; + const CLEO::StringList(__cdecl* ListCleoStartupScripts)() = nullptr; + }; +} diff --git a/source/CPluginSystem.cpp b/source/CPluginSystem.cpp index 1a6a6dcc..23b4ea38 100644 --- a/source/CPluginSystem.cpp +++ b/source/CPluginSystem.cpp @@ -10,9 +10,16 @@ CPluginSystem::~CPluginSystem() UnloadPlugins(); } -void CPluginSystem::LoadPlugins() +void CPluginSystem::LoadPlugins(bool fromModLoader) { - if (pluginsLoaded) return; // already done + if (!fromModLoader && pluginsLoaded) return; // plugins already loaded + if (fromModLoader && mlPluginsLoaded) return; // plugins from ModLoader already loaded + + if (fromModLoader) mlPluginsLoaded = true; + else pluginsLoaded = true; + + auto& modLoader = GetInstance().ModLoaderSystem; + if (fromModLoader && !modLoader.IsActive()) return; // ModLoader not present std::set names; std::vector paths; @@ -22,7 +29,10 @@ void CPluginSystem::LoadPlugins() auto ScanPluginsDir = [&](std::string path, const std::string prefix, const std::string extension) { auto pattern = path + '\\' + prefix + '*' + extension; - auto files = CLEO_ListDirectory(nullptr, pattern.c_str(), false, true); + + auto files = fromModLoader ? + modLoader.ListCleoFiles(pattern.c_str()) : + CLEO_ListDirectory(nullptr, pattern.c_str(), false, true); for (DWORD i = 0; i < files.count; i++) { @@ -54,41 +64,55 @@ void CPluginSystem::LoadPlugins() } } - CLEO_StringListFree(files); + if (fromModLoader) modLoader.StringListFree(files); + else CLEO_StringListFree(files); }; - TRACE(""); // separator - TRACE("Listing CLEO plugins:"); - ScanPluginsDir(FS::path(Filepath_Cleo).append("cleo_plugins").string(), "SA.", ".cleo"); - ScanPluginsDir(FS::path(Filepath_Cleo).append("cleo_plugins").string(), "", ".cleo"); // legacy plugins in new location - ScanPluginsDir(Filepath_Cleo, "", ".cleo"); // legacy plugins in old location - - // reverse order, so opcodes from CLEO5 plugins can overwrite opcodes from legacy plugins - if (!paths.empty()) + if (fromModLoader) { - for (auto it = paths.crbegin(); it != paths.crend(); it++) - { - const auto filename = it->c_str(); - TRACE(""); // separator - TRACE("Loading plugin '%s'", filename); - - HMODULE hlib = LoadLibrary(filename); - if (!hlib) - { - LOG_WARNING(0, "Error loading plugin '%s'", filename); - continue; - } + TRACE(""); // separator + TRACE("Listing CLEO plugins from ModLoader:"); - plugins.emplace_back(filename, hlib); + // ModLoader loads *.cleo plugins itself + auto files = modLoader.ListCleoFiles("*.cleo"); + mlPluginCount = files.count; + modLoader.StringListFree(files); + if (mlPluginCount > 0) + { + TRACE(" - %d CLEO plugin(s) already loaded by ModLoader", mlPluginCount); + } + else + { + TRACE(" - nothing found"); } - TRACE(""); // separator } else { - TRACE(" - nothing found"); + TRACE(""); // separator + TRACE("Listing CLEO plugins:"); + ScanPluginsDir(FS::path(Filepath_Cleo).append("cleo_plugins").string(), "SA.", ".cleo"); + ScanPluginsDir(FS::path(Filepath_Cleo).append("cleo_plugins").string(), "", ".cleo"); // legacy plugins in new location + ScanPluginsDir(Filepath_Cleo, "", ".cleo"); // legacy plugins in old location + + if (paths.empty()) TRACE(" - nothing found"); } - pluginsLoaded = true; + // reverse order, so opcodes from CLEO5 plugins can overwrite opcodes from legacy plugins + for (auto it = paths.crbegin(); it != paths.crend(); it++) + { + const auto filename = it->c_str(); + TRACE(""); // separator + TRACE("Loading plugin '%s'", filename); + + HMODULE hlib = LoadLibrary(filename); + if (!hlib) + { + LOG_WARNING(0, "Error loading plugin '%s'", filename); + continue; + } + + plugins.emplace_back(filename, hlib); + } } void CPluginSystem::UnloadPlugins() @@ -106,10 +130,10 @@ void CPluginSystem::UnloadPlugins() plugins.clear(); pluginsLoaded = false; -} + } size_t CPluginSystem::GetNumPlugins() const { - return plugins.size(); + return plugins.size() + mlPluginCount; } diff --git a/source/CPluginSystem.h b/source/CPluginSystem.h index 7b99d4a5..47460926 100644 --- a/source/CPluginSystem.h +++ b/source/CPluginSystem.h @@ -22,12 +22,16 @@ namespace CLEO std::list plugins; bool pluginsLoaded = false; + // Mod Loader + bool mlPluginsLoaded = false; + size_t mlPluginCount = 0; // plugins loaded by ModLoader itself + public: CPluginSystem() = default; CPluginSystem(const CPluginSystem&) = delete; // no copying ~CPluginSystem(); - void LoadPlugins(); + void LoadPlugins(bool modloader = false); void UnloadPlugins(); size_t GetNumPlugins() const; }; diff --git a/source/CScriptEngine.cpp b/source/CScriptEngine.cpp index fe15d471..91ca8d2a 100644 --- a/source/CScriptEngine.cpp +++ b/source/CScriptEngine.cpp @@ -1012,20 +1012,18 @@ namespace CLEO } }; - auto searchPattern = Filepath_Cleo + "\\*" + cs_ext; + auto searchPattern = Filepath_Cleo + "\\*.*"; auto list = CLEO_ListDirectory(nullptr, searchPattern.c_str(), false, true); processFileList(list); CLEO_StringListFree(list); - searchPattern = Filepath_Cleo + "\\*" + cs3_ext; - list = CLEO_ListDirectory(nullptr, searchPattern.c_str(), false, true); - processFileList(list); - CLEO_StringListFree(list); - - searchPattern = Filepath_Cleo + "\\*" + cs4_ext; - list = CLEO_ListDirectory(nullptr, searchPattern.c_str(), false, true); - processFileList(list); - CLEO_StringListFree(list); + auto& modLoader = GetInstance().ModLoaderSystem; + if (modLoader.IsActive()) + { + list = modLoader.ListCleoStartupScripts(); + processFileList(list); + modLoader.StringListFree(list); + } if (!found.empty()) { diff --git a/source/CleoBase.cpp b/source/CleoBase.cpp index 96bf2752..0b1e1f15 100644 --- a/source/CleoBase.cpp +++ b/source/CleoBase.cpp @@ -232,6 +232,10 @@ namespace CLEO void __cdecl CCleoInstance::OnDrawingFinished() { + // delayed so modloader.asi has chance to start + GetInstance().ModLoaderSystem.Init(); + GetInstance().PluginSystem.LoadPlugins(true); // CLEO plugins from ModLoader + GetInstance().CallCallbacks(eCallbackId::DrawingFinished); // execute registered callbacks } @@ -249,6 +253,15 @@ namespace CLEO auto resolved = reinterpret_cast(thread)->ResolvePath(inOutPath); + auto& modLoader = GetInstance().ModLoaderSystem; + if (modLoader.IsActive()) + { + if (resolved.length() < (pathMaxLen - 1)) + resolved.resize(pathMaxLen); // expand buffer to max allowed size + + modLoader.ResolvePath(reinterpret_cast(thread)->GetScriptFileFullPath().c_str(), resolved.data(), resolved.length() + 1); // including terminator character + } + if (resolved.length() >= pathMaxLen) resolved.resize(pathMaxLen - 1); // and terminator character @@ -286,28 +299,51 @@ namespace CLEO fsSearchPath = workDir / fsSearchPath; } - WIN32_FIND_DATA wfd = { 0 }; - HANDLE hSearch = FindFirstFile(fsSearchPath.string().c_str(), &wfd); - if (hSearch == INVALID_HANDLE_VALUE) - return {}; // nothing found - std::set found; - do + + auto& modLoader = GetInstance().ModLoaderSystem; + if (modLoader.IsActive()) + { + const char* scriptPath = nullptr; + + std::string scriptFilePath; + if (thread != nullptr) + { + scriptFilePath = ((CCustomScript*)thread)->GetScriptFileFullPath(); + scriptPath = scriptFilePath.c_str(); + } + + auto list = modLoader.ListFiles(scriptPath, fsSearchPath.string().c_str(), listDirs, listFiles); + for (DWORD i = 0; i < list.count; i++) + { + found.emplace(list.strings[i]); + } + modLoader.StringListFree(list); + } + else { - if (!listDirs && (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) - continue; // skip directories + WIN32_FIND_DATA wfd = { 0 }; + HANDLE hSearch = FindFirstFile(fsSearchPath.string().c_str(), &wfd); + if (hSearch == INVALID_HANDLE_VALUE) + return {}; // nothing found + + do + { + if (!listDirs && (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + continue; // skip directories - if (!listFiles && !(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) - continue; // skip files + if (!listFiles && !(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + continue; // skip files - auto path = FS::path(wfd.cFileName); - if (!path.is_absolute()) // keep absolute in case somebody hooked the APIs to return so - path = fsSearchPath.parent_path() / path; + auto path = FS::path(wfd.cFileName); + if (!path.is_absolute()) // keep absolute in case somebody hooked the APIs to return so + path = fsSearchPath.parent_path() / path; - found.insert(path.string()); - } while (FindNextFile(hSearch, &wfd)); + found.insert(path.string()); + } while (FindNextFile(hSearch, &wfd)); - FindClose(hSearch); + FindClose(hSearch); + } return CreateStringList(found); } diff --git a/source/CleoBase.h b/source/CleoBase.h index 7c129e3e..14258fd7 100644 --- a/source/CleoBase.h +++ b/source/CleoBase.h @@ -5,6 +5,7 @@ #include "CDebug.h" #include "CDmaFix.h" #include "CGameMenu.h" +#include "CModLoaderSystem.h" #include "CModuleSystem.h" #include "CPluginSystem.h" #include "CScriptEngine.h" @@ -31,6 +32,7 @@ namespace CLEO CScriptEngine ScriptEngine; CCustomOpcodeSystem OpcodeSystem; CModuleSystem ModuleSystem; + CModLoaderSystem ModLoaderSystem; OpcodeInfoDatabase OpcodeInfoDb; int saveSlot = -1; // -1 if not loaded from save diff --git a/source/stdafx.h b/source/stdafx.h index 96603147..e9f92a59 100644 --- a/source/stdafx.h +++ b/source/stdafx.h @@ -31,8 +31,7 @@ static std::string GetApplicationDirectory() } static const std::string Filepath_Root = GetApplicationDirectory(); -//static const std::string Filepath_Cleo = FS::path(Filepath_Root).append("cleo").string(); // absolute path -static const std::string Filepath_Cleo = "cleo"; // relative path - allow mod loaders to affect it +static const std::string Filepath_Cleo = FS::path(Filepath_Root).append("cleo").string(); // absolute path static const std::string Filepath_Config = FS::path(Filepath_Cleo).append(".cleo_config.ini").string(); static const std::string Filepath_Log = FS::path(Filepath_Cleo).append(".cleo.log").string();