diff --git a/UE4SS/include/UE4SSProgram.hpp b/UE4SS/include/UE4SSProgram.hpp index 672e92ec6..3e9b60253 100644 --- a/UE4SS/include/UE4SSProgram.hpp +++ b/UE4SS/include/UE4SSProgram.hpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include // Used to set up ImGui context and allocator in DLL mods #define UE4SS_ENABLE_IMGUI() \ @@ -207,8 +209,20 @@ namespace RC auto fire_dll_load_for_cpp_mods(std::wstring_view dll_name) -> void; public: + struct ModData { + std::string mod_name; + bool mod_enabled; + + GLZ_LOCAL_META(ModData, mod_name, mod_enabled); + }; + std::vector global_mod_data_vector; + + auto init() -> void; auto is_program_started() -> bool; + static auto read_mods_json(std::string mod_list_file, std::vector& mod_data_vector) -> void; + static auto write_mods_json(std::string mod_list_file, std::vector& mod_data_vector) -> void; + static auto convert_legacy_mods_file(StringType legacy_mod_list_file, std::vector& mod_data_vector) -> void; auto reinstall_mods() -> void; auto get_object_dumper_output_directory() -> const File::StringType; RC_UE4SS_API auto get_module_directory() -> File::StringViewType; diff --git a/UE4SS/src/UE4SSProgram.cpp b/UE4SS/src/UE4SSProgram.cpp index e753c31d3..50ef52625 100644 --- a/UE4SS/src/UE4SSProgram.cpp +++ b/UE4SS/src/UE4SSProgram.cpp @@ -384,7 +384,7 @@ namespace RC std::filesystem::path game_exe_path = exe_path_buffer; std::filesystem::path game_directory_path = game_exe_path.parent_path(); m_legacy_root_directory = game_directory_path; - + m_working_directory = m_root_directory; m_mods_directory = m_working_directory / "Mods"; m_game_executable_directory = game_directory_path; @@ -1117,67 +1117,127 @@ namespace RC } } - template - auto start_mods() -> std::string + auto UE4SSProgram::read_mods_json(std::string mod_list_file, std::vector& mod_data_vector) -> void { - ProfilerScope(); - // Part #1: Start all mods that are enabled in mods.txt. - Output::send(STR("Starting mods (from mods.txt load order)...\n")); - - std::filesystem::path mods_directory = UE4SSProgram::get_program().get_mods_directory(); - std::wstring enabled_mods_file{mods_directory / "mods.txt"}; - if (!std::filesystem::exists(enabled_mods_file)) + std::string buffer{}; + glz::parse_error pe = glz::read_file_json(mod_data_vector, mod_list_file, buffer); + if (pe) { - Output::send(STR("No mods.txt file found...\n")); + std::string descriptive_error = glz::format_error(pe, buffer); + + size_t count = descriptive_error.size() + 1; + wchar_t* converted_method_name = new wchar_t[count]; + + size_t num_of_char_converted = 0; + mbstowcs_s(&num_of_char_converted, converted_method_name, count, descriptive_error.data(), count); + + auto converted = File::StringViewType(converted_method_name); + + delete[] converted_method_name; + + Output::send(STR("{}\n\nError parsing mods.json file, please fix the file...\n"), converted); } - else - { - // 'mods.txt' exists, lets parse it - std::wifstream mods_stream{enabled_mods_file}; + } - std::wstring current_line; - while (std::getline(mods_stream, current_line)) + auto UE4SSProgram::write_mods_json(std::string mod_list_file, std::vector& mod_data_vector) -> void + { + std::string mod_data_buffer; + glz::write(mod_data_vector, mod_data_buffer); + glz::error_code ec = glz::buffer_to_file(mod_data_buffer, mod_list_file); + } + + auto UE4SSProgram::convert_legacy_mods_file(StringType legacy_mod_list_file, std::vector& mod_data_vector) -> void + { + Output::send(STR("Converting legacy mods.txt to mods.json...\n")); + std::wifstream mods_stream = File::open_file_skip_BOM(legacy_mod_list_file); + try { + mods_stream = File::open_file_skip_BOM(legacy_mod_list_file); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + } + std::wstring current_line; + while (std::getline(mods_stream, current_line)) + { + + // Don't parse any lines with ';' + if (current_line.find(L";") != current_line.npos) { - // Don't parse any lines with ';' - if (current_line.find(L";") != current_line.npos) - { - continue; - } + continue; + } - // Don't parse if the line is impossibly short (empty lines for example) - if (current_line.size() <= 4) - { - continue; - } + // Don't parse if the line is impossibly short (empty lines for example) + if (current_line.size() <= 4) + { + continue; + } - // Remove all spaces - auto end = std::remove(current_line.begin(), current_line.end(), L' '); - current_line.erase(end, current_line.end()); + // Remove all spaces + auto end = std::remove(current_line.begin(), current_line.end(), L' '); + current_line.erase(end, current_line.end()); - // Parse the line into something that can be converted into proper data - std::wstring mod_name = explode_by_occurrence(current_line, L':', 1); - std::wstring mod_enabled = explode_by_occurrence(current_line, L':', ExplodeType::FromEnd); + // Parse the line into something that can be converted into proper data + std::wstring mod_name = explode_by_occurrence(current_line, L':', 1); + std::wstring mod_enabled = explode_by_occurrence(current_line, L':', ExplodeType::FromEnd); + bool mod_enabled_out = !mod_enabled.empty() && mod_enabled[0] == L'1'; - auto mod = UE4SSProgram::find_mod_by_name(mod_name, UE4SSProgram::IsInstalled::Yes); - if (!mod || !dynamic_cast(mod)) - { - continue; - } + auto it = std::find_if(mod_data_vector.begin(), mod_data_vector.end(), + [&mod_name](const ModData& element) { + return element.mod_name == to_string(mod_name); + }); - if (!mod_enabled.empty() && mod_enabled[0] == L'1') - { - Output::send(STR("Starting {} mod '{}'\n"), std::is_same_v ? STR("Lua") : STR("C++"), mod->get_name().data()); - mod->start_mod(); - } - else - { - Output::send(STR("Mod '{}' disabled in mods.txt.\n"), mod_name); - } + if (it == mod_data_vector.end()) { + mod_data_vector.push_back({to_string(mod_name), mod_enabled_out}); } } + + Output::send(STR("mods.txt entries converted to mods.json. Please delete mods.txt if no mod managers rely on it to avoid conflicting with changes to the json...\n")); + } + - // Part #2: Start all mods that have enabled.txt present in the mod directory. - Output::send(STR("Starting mods (from enabled.txt, no defined load order)...\n")); + template + auto start_mods() -> std::string + { + ProfilerScope(); + // Part #1: Start all mods that are enabled in mods.json. + std::filesystem::path mods_directory = UE4SSProgram::get_program().get_mods_directory(); + std::filesystem::path legacy_mod_list_file{mods_directory / "mods.txt"}; + std::filesystem::path mod_list_file{mods_directory / "mods.json"}; + std::vector mod_data_vector{}; + UE4SSProgram& program = UE4SSProgram::get_program(); + + if (std::filesystem::exists(mod_list_file)) + { + UE4SSProgram::read_mods_json(mod_list_file.string(), mod_data_vector); + } + + if (std::filesystem::exists(legacy_mod_list_file)) + { + UE4SSProgram::convert_legacy_mods_file(legacy_mod_list_file, mod_data_vector); + } + + UE4SSProgram::write_mods_json(mod_list_file.string(), mod_data_vector); + + + Output::send(STR("Starting mods (from mods.json load order)...\n")); + for (auto it = mod_data_vector.begin(); it != mod_data_vector.end(); ++it) + { + auto mod = UE4SSProgram::find_mod_by_name(it->mod_name, UE4SSProgram::IsInstalled::Yes); + if (!mod || !dynamic_cast(mod)) + { + continue; + } + + if (it->mod_enabled) + { + Output::send(STR("Starting {} mod '{}'\n"), std::is_same_v ? STR("Lua") : STR("C++"), mod->get_name().data()); + mod->start_mod(); + } + else + { + Output::send(STR("Mod '{}' disabled in mods.json.\n"), to_wstring(it->mod_name)); + } + } + for (const auto& mod_directory : std::filesystem::directory_iterator(mods_directory)) { diff --git a/assets/Mods/mods.json b/assets/Mods/mods.json index d79717fa9..b8c6dcc6c 100644 --- a/assets/Mods/mods.json +++ b/assets/Mods/mods.json @@ -1,42 +1,42 @@ [ - { - "mod_name": "CheatManagerEnablerMod", - "mod_enabled": true - }, - { - "mod_name": "ActorDumperMod", - "mod_enabled": false - }, - { - "mod_name": "ConsoleCommandsMod", - "mod_enabled": true - }, - { - "mod_name": "ConsoleEnablerMod", - "mod_enabled": true - }, - { - "mod_name": "SplitScreenMod", - "mod_enabled": false - }, - { - "mod_name": "LineTraceMod", - "mod_enabled": true - }, - { - "mod_name": "BPML_GenericFunctions", - "mod_enabled": true - }, - { - "mod_name": "BPModLoaderMod", - "mod_enabled": true - }, - { - "mod_name": "jsbLuaProfilerMod", - "mod_enabled": false - }, - { - "mod_name": "Keybinds", - "mod_enabled": true - } + { + "mod_name": "CheatManagerEnablerMod", + "mod_enabled": true + }, + { + "mod_name": "ActorDumperMod", + "mod_enabled": false + }, + { + "mod_name": "ConsoleCommandsMod", + "mod_enabled": true + }, + { + "mod_name": "ConsoleEnablerMod", + "mod_enabled": true + }, + { + "mod_name": "SplitScreenMod", + "mod_enabled": false + }, + { + "mod_name": "LineTraceMod", + "mod_enabled": true + }, + { + "mod_name": "BPML_GenericFunctions", + "mod_enabled": true + }, + { + "mod_name": "BPModLoaderMod", + "mod_enabled": true + }, + { + "mod_name": "jsbLuaProfilerMod", + "mod_enabled": false + }, + { + "mod_name": "Keybinds", + "mod_enabled": true + } ] \ No newline at end of file diff --git a/deps/first/File/include/File/File.hpp b/deps/first/File/include/File/File.hpp index c0e38bf13..14ec3c762 100644 --- a/deps/first/File/include/File/File.hpp +++ b/deps/first/File/include/File/File.hpp @@ -12,5 +12,7 @@ namespace RC::File OverwriteExistingFile = OverwriteExistingFile::No, CreateIfNonExistent = CreateIfNonExistent::No) -> Handle; + RC_FILE_API auto open_file_skip_BOM(const std::wstring& filename) -> StreamType; + RC_FILE_API auto delete_file(const std::filesystem::path& file_path_and_name) -> void; } // namespace RC::File diff --git a/deps/first/File/src/File.cpp b/deps/first/File/src/File.cpp index 6869475cb..ba1f8158b 100644 --- a/deps/first/File/src/File.cpp +++ b/deps/first/File/src/File.cpp @@ -1,3 +1,5 @@ +#include +#include #include namespace RC::File @@ -22,8 +24,58 @@ namespace RC::File return construct_handle(file_path_and_name, open_properties); } + // BOM sequences + const std::string BOM_UTF8 = "\xEF\xBB\xBF"; + const std::wstring BOM_UTF16_LE = L"\xFEFF"; + const std::wstring BOM_UTF16_BE = L"\xFFFE"; + + // Function to check for BOM and reopen the file as a wide stream + auto open_file_skip_BOM(const std::wstring& filename) -> StreamType + { + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) + { + throw std::runtime_error("Failed to open the file."); + } + + char bom[3] = {0}; + file.read(bom, 3); + if (std::string(bom, 3) == BOM_UTF8) + { + // UTF-8 BOM detected + file.close(); + StreamType wfile(filename); + wfile.seekg(3, std::ios::beg); // Skip BOM + return wfile; + } + else + { + // Check for wide BOM + wchar_t wbom; + file.seekg(0, std::ios::beg); // Rewind to start + file.read(reinterpret_cast(&wbom), sizeof(wchar_t)); + file.close(); + if (wbom == BOM_UTF16_LE[0] || wbom == BOM_UTF16_BE[0]) + { + StreamType wfile(filename); +#pragma warning(disable : 4996) + std::locale loc(wfile.getloc(), new std::codecvt_utf8_utf16); +#pragma warning(default : 4996) + wfile.imbue(loc); + wfile.seekg(sizeof(wchar_t), std::ios::beg); // Skip BOM + return wfile; + } + else + { + // No BOM detected + file.close(); + return StreamType(filename); + } + } + } + auto delete_file(const std::filesystem::path& file_path_and_name) -> void { Handle::FileType::delete_file(file_path_and_name); } -} // namespace RC::File \ No newline at end of file +} // namespace RC::File