#include #include "loader/component_loader.hpp" #include "localized_strings.hpp" #include "game_console.hpp" #include "filesystem.hpp" #include "console.hpp" #include "game/game.hpp" #include #include #include #include namespace localized_strings { namespace { utils::hook::detour seh_string_ed_get_string_hook; struct localize_entry { std::string value{}; bool volatile_{}; }; using localized_map = std::unordered_map; utils::concurrency::container localized_overrides; const char* seh_string_ed_get_string(const char* reference) { return localized_overrides.access([&](const localized_map& map) { const auto entry = map.find(reference); if (entry != map.end()) { return utils::string::va("%s", entry->second.value.data()); } return seh_string_ed_get_string_hook.invoke(reference); }); } game::XAssetHeader db_find_localize_entry_stub(game::XAssetType type, const char* name, int allow_create_default) { const auto value = localized_overrides.access([&](const localized_map& map) -> const char* { const auto entry = map.find(name); if (entry != map.end()) { return utils::string::va("%s", entry->second.value.data()); } return nullptr; }); if (value == nullptr) { return game::DB_FindXAssetHeader(type, name, allow_create_default); } static game::LocalizeEntry entry{}; entry.value = value; entry.name = name; return static_cast(&entry); } bool parse_localized_strings_file(const std::string& data) { rapidjson::Document j; j.Parse(data.data()); if (!j.IsObject()) { return false; } localized_overrides.access([&](localized_map& map) { const auto obj = j.GetObj(); for (const auto& [key, value] : obj) { if (!key.IsString() || !value.IsString()) { continue; } const auto name = key.GetString(); const auto str = value.GetString(); const auto entry = map.find(name); if (entry == map.end() || entry->second.volatile_) { map[name] = {str, true}; } } }); return true; } bool try_load_file(const std::string& path, const std::string& language) { const auto file = utils::string::va("%s/localizedstrings/%s.json", path.data(), language.data()); if (!utils::io::file_exists(file)) { return false; } console::info("[Localized strings] Parsing %s\n", file); const auto data = utils::io::read_file(file); if (!parse_localized_strings_file(data)) { console::error("[Localized strings] Invalid language json file\n"); return false; } return true; } void load_localized_strings() { bool found = false; const auto search_paths = filesystem::get_search_paths(); const auto language = game::SEH_GetCurrentLanguageName(); for (const auto& path : search_paths) { if (!try_load_file(path, language)) { if (try_load_file(path, "english")) { found = true; console::warn("[Localized strings] No valid language file found for '%s' in '%s', falling back to 'english'\n", language, path.data()); } } else { found = true; } } if (!found) { console::warn("[Localized strings] No valid language file found!\n"); } } } void override(const std::string& key, const std::string& value, bool volatile_) { localized_overrides.access([&](localized_map& map) { map[key] = {value, volatile_}; }); } void clear() { localized_overrides.access([&](localized_map& map) { for (auto i = map.begin(); i != map.end();) { if (i->second.volatile_) { i = map.erase(i); } else { ++i; } } }); load_localized_strings(); } class component final : public component_interface { public: void post_unpack() override { // Change some localized strings seh_string_ed_get_string_hook.create(0x1405E5FD0, &seh_string_ed_get_string); utils::hook::call(0x1405E5AB9, db_find_localize_entry_stub); } }; } REGISTER_COMPONENT(localized_strings::component)