Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converts mods.txt to use mods.json #540

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions UE4SS/include/UE4SSProgram.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include <SettingsManager.hpp>
#include <Unreal/Core/Containers/Array.hpp>
#include <Unreal/UnrealVersion.hpp>
#include <glaze/glaze.hpp>
#include <glaze/core/macros.hpp>

// Used to set up ImGui context and allocator in DLL mods
#define UE4SS_ENABLE_IMGUI() \
Expand Down Expand Up @@ -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<ModData> global_mod_data_vector;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable ?
I can't find any usages in this PR.



auto init() -> void;
auto is_program_started() -> bool;
static auto read_mods_json(std::string mod_list_file, std::vector<ModData>& mod_data_vector) -> void;
static auto write_mods_json(std::string mod_list_file, std::vector<ModData>& mod_data_vector) -> void;
static auto convert_legacy_mods_file(StringType legacy_mod_list_file, std::vector<ModData>& 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;
Expand Down
158 changes: 109 additions & 49 deletions UE4SS/src/UE4SSProgram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1117,67 +1117,127 @@ namespace RC
}
}

template <typename ModType>
auto start_mods() -> std::string
auto UE4SSProgram::read_mods_json(std::string mod_list_file, std::vector<ModData>& 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<LogLevel::Error>(STR("{}\n\nError parsing mods.json file, please fix the file...\n"), converted);
Comment on lines +1126 to +1138
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this ?
Why aren't we using to_wstring ?

}
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<ModData>& mod_data_vector) -> void
{
std::string mod_data_buffer;
glz::write<glz::opts{.prettify = true}>(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<ModData>& 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;
}
Comment on lines +1153 to +1157
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bad, don't throw here, just let the exception fall through to main like we do with all fatal exceptions.
The way you've currently set this up will cause the error to not actually be logged, it will just be ignored and probably crash as you're still continuing to operate on the stream even though there was an error with it causing the stream to be invalid.

std::wstring current_line;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use StringType.

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);
Comment on lines +1179 to +1180
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use StringType, and STR('...') instead of L'...'.

bool mod_enabled_out = !mod_enabled.empty() && mod_enabled[0] == L'1';

auto mod = UE4SSProgram::find_mod_by_name<ModType>(mod_name, UE4SSProgram::IsInstalled::Yes);
if (!mod || !dynamic_cast<ModType*>(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<ModType, LuaMod> ? 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 <typename ModType>
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<UE4SSProgram::ModData> 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we're using the more complicated iterator method of looping here instead of the simpler
for (const auto& mod_data : mod_data_vector) ?

{
auto mod = UE4SSProgram::find_mod_by_name<ModType>(it->mod_name, UE4SSProgram::IsInstalled::Yes);
if (!mod || !dynamic_cast<ModType*>(mod))
{
continue;
}

if (it->mod_enabled)
{
Output::send(STR("Starting {} mod '{}'\n"), std::is_same_v<ModType, LuaMod> ? 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))
{
Expand Down
80 changes: 40 additions & 40 deletions assets/Mods/mods.json
Original file line number Diff line number Diff line change
@@ -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
}
]
2 changes: 2 additions & 0 deletions deps/first/File/include/File/File.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 53 additions & 1 deletion deps/first/File/src/File.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <codecvt>
#include <fstream>
#include <File/File.hpp>

namespace RC::File
Expand All @@ -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<char*>(&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<wchar_t>);
#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
} // namespace RC::File