diff --git a/data/fonts/noto_sans_arabic.ttf b/data/fonts/noto_sans_arabic.ttf new file mode 100644 index 00000000..d798a901 Binary files /dev/null and b/data/fonts/noto_sans_arabic.ttf differ diff --git a/data/fonts/noto_sans_kr.otf b/data/fonts/noto_sans_kr.otf new file mode 100644 index 00000000..7c5c2fae Binary files /dev/null and b/data/fonts/noto_sans_kr.otf differ diff --git a/data/fonts/noto_sans_sc.otf b/data/fonts/noto_sans_sc.otf new file mode 100644 index 00000000..d350ffa7 Binary files /dev/null and b/data/fonts/noto_sans_sc.otf differ diff --git a/data/ui_scripts/settings/__init__.lua b/data/ui_scripts/settings/__init__.lua index 26b798ac..093bd510 100644 --- a/data/ui_scripts/settings/__init__.lua +++ b/data/ui_scripts/settings/__init__.lua @@ -1,3 +1,7 @@ +if (Engine.InFrontend()) then + require("language") +end + game:addlocalizedstring("MENU_GENERAL", "GENERAL") game:addlocalizedstring("MENU_GENERAL_DESC", "Set the client's settings.") game:addlocalizedstring("LUA_MENU_AUTO_UPDATE", "Automatic updates") diff --git a/data/ui_scripts/settings/language.lua b/data/ui_scripts/settings/language.lua new file mode 100644 index 00000000..3d5e79e0 --- /dev/null +++ b/data/ui_scripts/settings/language.lua @@ -0,0 +1,136 @@ +game:addlocalizedstring("MENU_english", "English") +game:addlocalizedstring("MENU_french", "Français") +game:addlocalizedstring("MENU_german", "Deutsch") +game:addlocalizedstring("MENU_italian", "Italiano") +game:addlocalizedstring("MENU_spanish", "Español") +game:addlocalizedstring("MENU_russian", "Pyccкий") +game:addlocalizedstring("MENU_polish", "Polskie") +game:addlocalizedstring("MENU_portuguese", "Português") +game:addlocalizedstring("MENU_japanese_full", "日本") +game:addlocalizedstring("MENU_japanese_partial", "日本 (Partial)") +game:addlocalizedstring("MENU_traditional_chinese", "中国人") +game:addlocalizedstring("MENU_simplified_chinese", "简体中文") +game:addlocalizedstring("MENU_arabic", "عربى") +game:addlocalizedstring("MENU_czech", "Czech") -- ?? +game:addlocalizedstring("MENU_spanishna", "Español (2)") -- ?? +game:addlocalizedstring("MENU_korean", "한국어") +game:addlocalizedstring("MENU_english_safe", "English (Safe)") +game:addlocalizedstring("MENU_russian_partial", "Pyccкий (Partial)") + +LUI.addmenubutton("pc_controls", { + index = 4, + text = "@LUA_MENU_CHOOSE_LANGUAGE", + description = Engine.Localize("@LUA_MENU_CHOOSE_LANGUAGE_DESC"), + callback = function() + LUI.FlowManager.RequestAddMenu(nil, "choose_language_menu") + end +}) + +local factory = LUI.UIGenericButton.ButtonLabelFactory +local overrideyoffset = nil +LUI.UIGenericButton.ButtonLabelFactory = function(data, ...) + if (overrideyoffset) then + data.yOffset = overrideyoffset + overrideyoffset = nil + end + + return factory(data, ...) +end + +local languages = { + "english", + "french", + "german", + "italian", + "spanish", + "russian", + "polish", + "portuguese", + "japanese_full", + "japanese_partial", + "traditional_chinese", + "simplified_chinese", + "arabic", + "czech", + "spanishna", + "korean", + "english_safe", + "russian_partial", +} + +local function usingspeciallanguage() + local id = Engine.GetCurrentLanguage() + 1 + local lang = languages[id] or "english" + + local normalfontlangs = { + ["english"] = true, + ["french"] = true, + ["german"] = true, + ["italian"] = true, + ["spanish"] = true, + ["portuguese"] = true, + ["spanishna"] = true, + ["english_safe"] = true, + } + + return normalfontlangs[lang] ~= true +end + +LUI.MenuBuilder.registerType("choose_language_menu", function(a1) + local menu = LUI.MenuTemplate.new(a1, { + menu_title = "@LUA_MENU_CHOOSE_LANGUAGE", + menu_list_divider_top_offset = -(LUI.H1MenuTab.tabChangeHoldingElementHeight + H1MenuDims.spacing), + menu_width = 300, + uppercase_title = true + }) + + local languages = Engine.GetSupportedLanguages() + + for i = 1, #languages do + local prevfont = LUI.MenuGenericButtons.ButtonLabelFont.Font + local prevheight = LUI.MenuGenericButtons.ButtonLabelFont.Height + local id = languages[i].id + + if (not usingspeciallanguage()) then + if (id == 5 or (id >= 8 and id < 12) or id == 17) then + LUI.MenuGenericButtons.ButtonLabelFont.Font = RegisterFont("fonts/noto_sans_sc.otf", 30) + overrideyoffset = 0 + LUI.MenuGenericButtons.ButtonLabelFont.Height = 24 + elseif (id == 12) then + LUI.MenuGenericButtons.ButtonLabelFont.Font = RegisterFont("fonts/noto_sans_arabic.ttf", 30) + LUI.MenuGenericButtons.ButtonLabelFont.Height = 28 + overrideyoffset = 0 + elseif (id == 15) then + LUI.MenuGenericButtons.ButtonLabelFont.Font = RegisterFont("fonts/noto_sans_kr.otf", 30) + LUI.MenuGenericButtons.ButtonLabelFont.Height = 24 + overrideyoffset = 0 + end + end + + local button = menu:AddButton("", function() + if (languages[i].id == Engine.GetCurrentLanguage()) then + LUI.FlowManager.RequestLeaveMenu(nil, "choose_language_menu") + return + end + + LUI.FlowManager.RequestAddMenu(nil, "choose_language_confirm_popup", false, nil, true, { + language = languages[i].id + }) + end) + + overrideyoffset = nil + + LUI.MenuGenericButtons.ButtonLabelFont.Font = prevfont + LUI.MenuGenericButtons.ButtonLabelFont.Height = prevheight + + local label = button:getFirstDescendentById("text_label") + label:setText(Engine.ToUpperCase(languages[i].name)) + end + + LUI.Options.InitScrollingList(menu.list, nil, { + rows = 11 + }) + menu:AddBackButton() + + return menu +end) diff --git a/src/client/component/fonts.cpp b/src/client/component/fonts.cpp index 73b3dba9..1e799e47 100644 --- a/src/client/component/fonts.cpp +++ b/src/client/component/fonts.cpp @@ -4,6 +4,7 @@ #include "fonts.hpp" #include "console.hpp" #include "filesystem.hpp" +#include "command.hpp" #include "game/game.hpp" #include "game/dvars.hpp" @@ -205,6 +206,26 @@ namespace fonts utils::hook::jump(0x14037B390, utils::hook::assemble(get_hud_elem_info_stub), true); utils::hook::inject(0x1404C17A6, hudelem_fonts); utils::hook::set(0x1404C17B7, 13); // 13 hud elem fonts + + command::add("dumpFont", [](const command::params& params) + { + if (params.size() < 2) + { + return; + } + + const auto name = params.get(1); + const auto ttf = game::DB_FindXAssetHeader(game::XAssetType::ASSET_TYPE_TTF, name, false).ttf; + if (ttf == nullptr) + { + console::error("Font does not exist\n"); + return; + } + + const auto path = utils::string::va("dumps/%s", ttf->name); + utils::io::write_file(path, std::string(ttf->buffer, ttf->len), false); + console::info("Dumped to %s", path); + }); } }; } diff --git a/src/client/component/language.cpp b/src/client/component/language.cpp new file mode 100644 index 00000000..dbae1f90 --- /dev/null +++ b/src/client/component/language.cpp @@ -0,0 +1,59 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "language.hpp" +#include "localized_strings.hpp" + +#include +#include + +#define LANGUAGE_FILE "players2/default/language" + +namespace language +{ + namespace + { + const char* get_loc_language_string() + { + if (!utils::io::file_exists(LANGUAGE_FILE)) + { + return nullptr; + } + + static char language[0x200] = {0}; + const auto data = utils::io::read_file(LANGUAGE_FILE); + strcpy_s(language, data.data()); + return language; + } + } + + void set(const std::string& lang) + { + utils::io::write_file(LANGUAGE_FILE, lang, false); + } + + void set_from_index(const int index) + { + if (index < 0 || index > 17) + { + return; + } + + const auto language = game::languages[index]; + set(language.name); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::call(0x14060AFFB, get_loc_language_string); + } + }; +} + +REGISTER_COMPONENT(language::component) diff --git a/src/client/component/language.hpp b/src/client/component/language.hpp new file mode 100644 index 00000000..d3621634 --- /dev/null +++ b/src/client/component/language.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace language +{ + void set(const std::string& language); + void set_from_index(const int index); +} diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index e72cfd5a..8aabc443 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -14,6 +14,7 @@ #include "mods.hpp" #include "updater.hpp" #include "console.hpp" +#include "language.hpp" #include "game/ui_scripting/execution.hpp" #include "game/scripting/execution.hpp" @@ -260,6 +261,17 @@ namespace ui_scripting localized_strings::override(string, value); }; + game_type["setlanguage"] = [](const game&, const std::string& language) + { + language::set(language); + }; + + lua["Engine"]["SetLanguage"] = [](const int index) + { + language::set_from_index(index); + updater::relaunch(); + }; + using player = table; auto player_type = player(); lua["player"] = player_type; diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 7ad92df4..76a39ee6 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1194,6 +1194,16 @@ namespace game _OVERLAPPED overlapped; }; + struct language_values + { + const char* name; + const char* shortname; + const char* prefix1; + const char* prefix2; + const char* prefix3; + char __pad0[0x8]; + }; + namespace hks { struct lua_State; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index a1262a7e..fe0a18db 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -207,6 +207,8 @@ namespace game WEAK symbol maps{0x14097EE90}; + WEAK symbol languages{0x140BF9740}; + namespace hks { WEAK symbol lua_state{0x1419D83E8};