diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 756ff7fa..170c61fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,7 @@ jobs: cp resources/text_data_info_i18n_zh_tw.json package/localized_data/config_schema/text_data_info_i18n_zh_tw.json cp resources/text_data_info_i18n_ja.json package/localized_data/config_schema/text_data_info_i18n_ja.json cp resources/legend_g_plugin.exe package/legend_g_plugin.exe.autoupdate + cp -r resources/eventHelper package/localized_data/eventHelper_examples - uses: actions/upload-artifact@v2 with: name: trainers-legend-g diff --git a/.gitignore b/.gitignore index 9f48d53d..b0c95b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -156,3 +156,4 @@ resources/text_dumper/bin .cache/ .vscode/ /backend +/utils/events.br diff --git a/resources/config.json b/resources/config.json index 315274e9..dcf3b0cc 100644 --- a/resources/config.json +++ b/resources/config.json @@ -110,5 +110,6 @@ "enableCustomPersistentDataPath": false, "customPersistentDataPath": "" }, - "uploadGachaHistory": false + "uploadGachaHistory": false, + "enableEventHelper": false } diff --git a/resources/config.schema.json b/resources/config.schema.json index 7f1e4161..562894bb 100644 --- a/resources/config.schema.json +++ b/resources/config.schema.json @@ -464,6 +464,11 @@ "description": "上传您的抽卡记录\n点击\"抽奖记录\"按钮上传\n您可以前往 uma.gacha.chinosk6.cn 查看抽卡记录", "type": "boolean", "default": false + }, + "enableEventHelper": { + "description": "启用事件簿", + "type": "boolean", + "default": false } }, "additionalProperties": false, diff --git a/resources/config_en.schema.json b/resources/config_en.schema.json index 456a184e..2e592503 100644 --- a/resources/config_en.schema.json +++ b/resources/config_en.schema.json @@ -464,6 +464,11 @@ "description": "Upload your gacha history\nClick on the \"Gacha history\" button to upload\nYou can go to uma.gacha.chinosk6.cn to view the gacha history", "type": "boolean", "default": false + }, + "enableEventHelper": { + "description": "Enable Events Helper", + "type": "boolean", + "default": false } }, "additionalProperties": false, diff --git a/resources/config_ja.schema.json b/resources/config_ja.schema.json index 8e3dd83f..5c450037 100644 --- a/resources/config_ja.schema.json +++ b/resources/config_ja.schema.json @@ -464,6 +464,11 @@ "description": "ガチャの履歴の情報をアップロード\n「Gacha Records」ボタンをクリックでアップロードします。\nガチャの履歴は、uma.gacha.chinosk6.cnから確認ができます。", "type": "boolean", "default": false + }, + "enableEventHelper": { + "description": "Enable Events Helper", + "type": "boolean", + "default": false } }, "additionalProperties": false, diff --git a/resources/config_zh_tw.schema.json b/resources/config_zh_tw.schema.json index 34762dc2..b7f248dc 100644 --- a/resources/config_zh_tw.schema.json +++ b/resources/config_zh_tw.schema.json @@ -464,6 +464,11 @@ "description": "上傳您的抽卡記錄\n點擊「轉蛋記錄」按鈕上傳\n您可以前往 uma.gacha.chinosk6.cn 查看抽卡記錄", "type": "boolean", "default": false + }, + "enableEventHelper": { + "description": "啟用事件簿", + "type": "boolean", + "default": false } }, "additionalProperties": false, diff --git a/resources/eventHelper/events_en.json b/resources/eventHelper/events_en.json new file mode 100644 index 00000000..ad1497bd --- /dev/null +++ b/resources/eventHelper/events_en.json @@ -0,0 +1,42 @@ +[ + { + "Id": 400001017, + "Name": "二人三脚", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "Wisdom +20、Skill Pt +20、「鋼の意志」 Hint Lv +3", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "Wisdom +20、Skill Pt +20、「鋼の意志」 Hint Lv +1", + "FailedEffect": "" + } + ] + ] + }, + { + "Id": 400001029, + "Name": "ハッピーな靴選び", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "Power +20", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "Power +10", + "FailedEffect": "" + } + ] + ] + } +] \ No newline at end of file diff --git a/resources/eventHelper/events_jp.json b/resources/eventHelper/events_jp.json new file mode 100644 index 00000000..567f1b7c --- /dev/null +++ b/resources/eventHelper/events_jp.json @@ -0,0 +1,42 @@ +[ + { + "Id": 400001017, + "Name": "二人三脚", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "賢さ +20、スキルPt +20、「鋼の意志」 ヒント +3", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "賢さ +20、スキルPt +20、「鋼の意志」 ヒント +1", + "FailedEffect": "" + } + ] + ] + }, + { + "Id": 400001029, + "Name": "ハッピーな靴選び", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "パワー +20", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "パワー +10", + "FailedEffect": "" + } + ] + ] + } +] \ No newline at end of file diff --git a/resources/eventHelper/events_scn.json b/resources/eventHelper/events_scn.json new file mode 100644 index 00000000..c1260f8b --- /dev/null +++ b/resources/eventHelper/events_scn.json @@ -0,0 +1,42 @@ +[ + { + "Id": 400001017, + "Name": "二人三脚", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "智力 +20、技能Pt +20、「鋼の意志」 Hint Lv +3", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "智力 +20、技能Pt +20、「鋼の意志」 Hint Lv +1", + "FailedEffect": "" + } + ] + ] + }, + { + "Id": 400001029, + "Name": "ハッピーな靴選び", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "力量 +20", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "力量 +10", + "FailedEffect": "" + } + ] + ] + } +] \ No newline at end of file diff --git a/resources/eventHelper/events_tcn.json b/resources/eventHelper/events_tcn.json new file mode 100644 index 00000000..c1260f8b --- /dev/null +++ b/resources/eventHelper/events_tcn.json @@ -0,0 +1,42 @@ +[ + { + "Id": 400001017, + "Name": "二人三脚", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "智力 +20、技能Pt +20、「鋼の意志」 Hint Lv +3", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "智力 +20、技能Pt +20、「鋼の意志」 Hint Lv +1", + "FailedEffect": "" + } + ] + ] + }, + { + "Id": 400001029, + "Name": "ハッピーな靴選び", + "Choices": [ + [ + { + "Option": "桐生院葵編成有り", + "SuccessEffect": "力量 +20", + "FailedEffect": "" + } + ], + [ + { + "Option": "桐生院葵編成無し", + "SuccessEffect": "力量 +10", + "FailedEffect": "" + } + ] + ] + } +] \ No newline at end of file diff --git a/resources/legend_g_plugin.exe b/resources/legend_g_plugin.exe index e53a6155..f74099d1 100644 Binary files a/resources/legend_g_plugin.exe and b/resources/legend_g_plugin.exe differ diff --git a/src/eventHelper/eventHelper.cpp b/src/eventHelper/eventHelper.cpp new file mode 100644 index 00000000..ca014ea2 --- /dev/null +++ b/src/eventHelper/eventHelper.cpp @@ -0,0 +1,173 @@ +#include +#include "eventHelper/eventHelper.hpp" + +extern std::unordered_set sChineseLangIds; +extern std::unordered_set tChineseLangIds; +extern std::unordered_set japaneseLangIds; + + +namespace EventHelper { + std::unordered_map events{}; + std::unordered_set serverExistsEventId{}; + + bool isServerMode = false; + std::wstring systemLang = L""; + std::wstring serverURL = L"https://umaevent.chinosk6.cn"; + + void loadServerData() { + try { + serverExistsEventId.clear(); + const auto resp = request_convert::send_post(serverURL, std::format(L"/get_all_events_id?l={}", systemLang), L""); + if (resp.status_code() != 200) { + printf("Load event faield: %d\n", resp.status_code()); + return; + } + const auto respStr = resp.extract_utf8string().get(); + auto serverIds = nlohmann::json::parse(respStr); + const bool isSuccess = serverIds["s"]; + if (isSuccess) { + for (auto& i : serverIds["d"]) { + const long id = i; + serverExistsEventId.insert(id); + } + } + } + catch (std::exception& e) { + printf("loadServerData error: %s\n", e.what()); + } + } + + /* + Thanks: + https://kamigame.jp/umamusume/page/152540608660042049.html + https://github.com/UmamusumeResponseAnalyzer/UmamusumeDeserializeDB5 + */ + void loadData() { + try { + events.clear(); + + const auto localLang = GetUserDefaultUILanguage(); + + std::filesystem::path dataPath = "localized_data/eventHelper"; + if (sChineseLangIds.contains(localLang)) { + systemLang = L"scn"; + dataPath /= "events_scn.json"; + } + else if (tChineseLangIds.contains(localLang)) { + systemLang = L"tcn"; + dataPath /= "events_tcn.json"; + } + else if (japaneseLangIds.contains(localLang)) { + systemLang = L"jp"; + dataPath /= "events_jp.json"; + } + else { + systemLang = L"en"; + dataPath /= "events_en.json"; + } + if (!std::filesystem::exists(dataPath)) { + isServerMode = true; + std::thread([]() { + loadServerData(); + }).detach(); + return; + } + isServerMode = false; + + std::ifstream file(dataPath); + if (!file.is_open()) { + printf("Load %ls failed: file not found.\n", dataPath.c_str()); + return; + } + std::string fileContent((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + auto jsonData = nlohmann::json::parse(fileContent); + + int loadCount = 0; + for (auto& i : jsonData) { + const long id = i["Id"]; + const std::string name = i["Name"]; + std::list choices{}; + + for (auto& choice : i["Choices"]) { + auto& currChoice = choice[0]; + const std::string option = currChoice["Option"]; + const std::string successEffect = currChoice["SuccessEffect"]; + const std::string failedEffect = currChoice["FailedEffect"]; + + choices.push_back(EventChoice{ + .Option=option, + .SuccessEffect = successEffect, + .FailedEffect = failedEffect + }); + loadCount++; + } + + events.emplace(id, EventInfo{ .Id = id, .Name = name, .Choices = choices }); + } + printf("%d events loaded.\n", loadCount); + + } + catch (std::exception& e) { + printf("Load event info error: %s\n", e.what()); + } + } + + bool getEventInfoFromServer(const long storyId, EventInfo* eventInfo) { + try { + if (!serverExistsEventId.contains(storyId)) { + return false; + } + const auto resp = request_convert::send_post(serverURL, std::format(L"/get_event?l={}&i={}", systemLang, storyId), L""); + if (resp.status_code() != 200) { + printf("Get event %ld faield: %d\n", storyId, resp.status_code()); + return false; + } + const auto respStr = resp.extract_utf8string().get(); + auto jsonData = nlohmann::json::parse(respStr); + const bool isSuccess = jsonData["s"]; + if (!isSuccess) { + return false; + } + + auto& i = jsonData["d"]; + + const long id = i["i"]; + const std::string name = i["n"]; + std::list choices{}; + + for (auto& choice : i["c"]) { + auto& currChoice = choice[0]; + const std::string option = currChoice["o"]; + const std::string successEffect = currChoice["s"]; + const std::string failedEffect = currChoice["f"]; + + choices.push_back(EventChoice{ + .Option = option, + .SuccessEffect = successEffect, + .FailedEffect = failedEffect + }); + } + + const EventInfo getEventInfo{ .Id = id, .Name = name, .Choices = choices }; + events.emplace(id, getEventInfo); + *eventInfo = getEventInfo; + return true; + } + catch (std::exception& e) { + printf("getEventInfoFromServer %ld error: %s\n", storyId, e.what()); + } + return false; + } + + bool getEventInfo(const long storyId, EventInfo* eventInfo) { + if (auto iter = events.find(storyId); iter != events.end()) { + *eventInfo = iter->second; + return true; + } + if (isServerMode) { + return getEventInfoFromServer(storyId, eventInfo); + } + return false; + } + +} diff --git a/src/eventHelper/eventHelper.hpp b/src/eventHelper/eventHelper.hpp new file mode 100644 index 00000000..4d9a4a6d --- /dev/null +++ b/src/eventHelper/eventHelper.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include + + +namespace EventHelper { + struct EventChoice { + std::string Option; + std::string SuccessEffect; + std::string FailedEffect; + }; + + struct EventInfo { + long Id; + std::string Name; + std::list Choices; + }; + + void loadData(); + bool getEventInfo(const long storyId, EventInfo* eventInfo); + +} diff --git a/src/hook.cpp b/src/hook.cpp index fa1e1fda..f92ed5d4 100644 --- a/src/hook.cpp +++ b/src/hook.cpp @@ -4271,6 +4271,77 @@ namespace return reinterpret_cast(WWWRequest_Post_orig)(_this, url, postData, headers); } + long getStoryId() { + + static auto StoryManager_klass = il2cpp_symbols::get_class("umamusume.dll", "Gallop", "StoryManager"); + static auto StoryManager_get_Instance_method = il2cpp_class_get_method_from_name(StoryManager_klass, "get_Instance", 0); + static auto StoryManager_get_Instance = reinterpret_cast( + StoryManager_get_Instance_method->methodPointer + ); + static auto get_StoryId = reinterpret_cast( + il2cpp_class_get_method_from_name(StoryManager_klass, "get_StoryId", 0)->methodPointer + ); + + auto storyManager = StoryManager_get_Instance(StoryManager_get_Instance_method); + if (!storyManager) return -1; + return get_StoryId(storyManager); + } + + void* StoryViewController_OpenChoiceButton_orig; + void StoryViewController_OpenChoiceButton_hook(void* _this, void* textClip) { + reinterpret_cast(StoryViewController_OpenChoiceButton_orig)(_this, textClip); + if (!g_enable_event_helper) return; + if (!guiStarting) { + startUmaGui(); + } + + static auto StoryViewController_klass = il2cpp_symbols::get_class_from_instance(_this); + static auto GetTitleText = reinterpret_cast( + il2cpp_class_get_method_from_name(StoryViewController_klass, "GetTitleText", 1)->methodPointer + ); + + const auto storyId = getStoryId(); + + eventInfoDisplay.currentGameStoryId = storyId; + + auto titleText = GetTitleText(_this, storyId); + if (titleText) { + eventInfoDisplay.currentGameStoryName = utility::conversions::to_utf8string(std::wstring(titleText->start_char)); + } + + static auto textClip_klass = il2cpp_symbols::get_class_from_instance(textClip); + static auto ChoiceDataList_field = il2cpp_class_get_field_from_name(textClip_klass, "ChoiceDataList"); + auto ChoiceDataList = il2cpp_symbols::read_field(textClip, ChoiceDataList_field); + + eventInfoDisplay.gameChoicesText.clear(); + il2cpp_symbols::iterate_list(ChoiceDataList, [&](size_t index, void* choice) { + static auto choiceData_klass = il2cpp_symbols::get_class_from_instance(choice); + static auto Text_field = il2cpp_class_get_field_from_name(choiceData_klass, "Text"); + static auto Index_field = il2cpp_class_get_field_from_name(choiceData_klass, "k__BackingField"); + + auto text = il2cpp_symbols::read_field(choice, Text_field); + auto choicecIndex = il2cpp_symbols::read_field(choice, Index_field); + // wprintf(L"[%d]text: %ls\n", choicecIndex, text->start_char); + const auto pushStr = utility::conversions::to_utf8string(std::wstring(text->start_char)); + if (auto it = std::find(eventInfoDisplay.gameChoicesText.begin(), eventInfoDisplay.gameChoicesText.end(), pushStr); it == eventInfoDisplay.gameChoicesText.end()) { + eventInfoDisplay.gameChoicesText.push_back(pushStr); + } + + }); + + std::thread([storyId]() { + eventInfoDisplay.hasInfo = false; + eventInfoDisplay.isLoading = true; + if (EventHelper::getEventInfo(storyId, &eventInfoDisplay.eventInfo)) { + eventInfoDisplay.hasInfo = true; + } + else { + eventInfoDisplay.hasInfo = false; + } + eventInfoDisplay.isLoading = false; + }).detach(); + } + void dump_all_entries() { // 0 is None @@ -5117,6 +5188,11 @@ namespace "SingleModeSceneController", "CreateModel", 3 ); + auto StoryViewController_OpenChoiceButton_addr = il2cpp_symbols::get_method_pointer( + "umamusume.dll", "Gallop", + "StoryViewController", "OpenChoiceButton", 1 + ); + // auto HomeCharacterCreator_klass = il2cpp_symbols::get_class("umamusume.dll", "Gallop", "HomeCharacterCreator"); // auto CreateInfo_klass = il2cpp_symbols::find_nested_class_from_name(HomeCharacterCreator_klass, "CreateInfo"); // auto CreateInfo_ctor_addr = il2cpp_class_get_method_from_name(CreateInfo_klass, ".ctor", 2)->methodPointer; @@ -5254,6 +5330,7 @@ namespace ADD_HOOK(SetSimpleTwoButtonMessagef, "SetSimpleTwoButtonMessagef at %p\n"); ADD_HOOK(StoryCharacter3D_LoadModel, "StoryCharacter3D_LoadModel at %p\n"); ADD_HOOK(SingleModeSceneController_CreateModel, "SingleModeSceneController_CreateModel at %p\n"); + ADD_HOOK(StoryViewController_OpenChoiceButton, "StoryViewController_OpenChoiceButton at %p\n"); //ADD_HOOK(camera_reset, "UnityEngine.Camera.Reset() at %p\n"); diff --git a/src/main.cpp b/src/main.cpp index 436a47e8..f2b6ad71 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -106,6 +106,7 @@ bool g_enable_live_dof_controller = false; bool g_enable_better60fps = false; bool g_upload_gacha_history = false; std::wstring g_upload_gacha_history_endpoint = L""; +bool g_enable_event_helper = false; constexpr const char LocalizedDataPath[] = "localized_data"; constexpr const char OldLocalizedDataPath[] = "old_localized_data"; @@ -827,6 +828,10 @@ namespace dumpGameAssemblyPath = document["dumpGameAssemblyPath"].GetString(); } } + + if (document.HasMember("enableEventHelper")) { + g_enable_event_helper = document["enableEventHelper"].GetBool(); + } } config_stream.close(); @@ -1063,6 +1068,9 @@ namespace { UmaDatabase::executeQueryRes(); } } + std::thread([]() { + EventHelper::loadData(); + }).detach(); } // 返回新的 static dict 路径 @@ -1898,6 +1906,7 @@ int __stdcall DllMain(HINSTANCE dllModule, DWORD reason, LPVOID) PluginLoader::loadDll(dllName); } SetConsoleTitleW(CONSOLE_TITLE); // 保持控制台标题 + EventHelper::loadData(); auto_update(); }); init_thread.detach(); diff --git a/src/stdinclude.hpp b/src/stdinclude.hpp index 733aac07..5206dfe8 100644 --- a/src/stdinclude.hpp +++ b/src/stdinclude.hpp @@ -43,6 +43,7 @@ #include "umadb/umadb.hpp" #include "pluginLoader/pluginLoader.hpp" #include +#include "eventHelper/eventHelper.hpp" #include "umagui/guiShowData.hpp" #include "umagui/umaguiMain.hpp" @@ -200,3 +201,4 @@ extern bool g_upload_gacha_history; extern std::wstring g_upload_gacha_history_endpoint; extern bool g_dump_sprite_tex; extern bool g_dump_bundle_tex; +extern bool g_enable_event_helper; diff --git a/src/umagui/guiLanguage.hpp b/src/umagui/guiLanguage.hpp index e547cfe3..d12132e8 100644 --- a/src/umagui/guiLanguage.hpp +++ b/src/umagui/guiLanguage.hpp @@ -1,17 +1,20 @@ #pragma once #include +std::unordered_set sChineseLangIds{ 0x0004, 0x0804, 0x1004 }; // zh-Hans, zh-CN, zh-SG +std::unordered_set tChineseLangIds{ 0x0404, 0x0c04, 0x1404, 0x048E }; // zh-TW, zh-HK, zh-MO, zh-yue-HK +std::unordered_set japaneseLangIds{ 0x0011, 0x0411 }; // ja, ja-JP + namespace GuiTrans { enum class GUILangType { ENGLISH, SCHINESE, - TCHINESE + TCHINESE, + JPN }; extern auto GuiLanguage = GUILangType::ENGLISH; - std::unordered_set sChineseLangIds { 0x0004, 0x0804, 0x1004 }; // zh-Hans, zh-CN, zh-SG - std::unordered_set tChineseLangIds { 0x0404, 0x0c04, 0x1404, 0x048E }; // zh-TW, zh-HK, zh-MO, zh-yue-HK const std::unordered_map guiSChineseTrans{ {"GateNo/CharaName", "闸号/角色名"}, @@ -101,6 +104,11 @@ namespace GuiTrans { {"RunningStyleTemptationOtherSelf", "RunningStyleTemptationOtherSelf"}, {"CharaId", "指定角色 ID"}, {"ActivateHealSkill", "激活治疗技能"}, + + {"Option", "选项"}, + {"Effects", "效果"}, + {"On Success:", "成功时:"}, + {"On Failed:", "失败时:"}, }; const std::unordered_map guiTChineseTrans{ @@ -191,6 +199,107 @@ namespace GuiTrans { {"RunningStyleTemptationOtherSelf", "RunningStyleTemptationOtherSelf"}, {"CharaId", "指定角色 ID"}, {"ActivateHealSkill", "激活治療技能"}, + + {"Option", "選項"}, + {"Effects", "效果"}, + {"On Success:", "成功時:"}, + {"On Failed:", "失敗時:"}, + }; + + + const std::unordered_map guiJPNTrans{ + {"GateNo/CharaName", "ゲート番号/ウマ娘の名前"}, + {"Rank/Distance", "順位/移動距離"}, + {"DistanceFrom Front/First", "先頭/1位からの距離"}, + {"InstantSpeed", "瞬間速度"}, + {"Rate", "スピードのレート"}, + {"HP Left", "残りのHP"}, + {"IsLastSpurt/Start Distance", "ラストスパート/スタートの距離"}, + {"LastSpeed", "直前のスピード"}, + {"Speed", "スピード"}, + {"Stamina", "スタミナ"}, + {"Pow", "パワー"}, + {"Power", "パワー"}, + {"Guts", "根性"}, + {"Wiz", "賢さ"}, + {"MinSpeed", "最小の速度"}, + {"RaceBaseSpeed", "レースの基本速度"}, + {"StartDashSpeedThreshold", "スタートダッシュ速度のしきい値"}, + {"BaseSpeed", "基本のスピード"}, + {"RawSpeed", "元のスピード"}, + {"BaseStamina", "基本のスタミナ"}, + {"RawStamina", "元のスタミナ"}, + {"BasePow", "基本のパワー"}, + {"RawPow", "元のパワー"}, + {"BaseGuts", "基本の根性"}, + {"RawGuts", "元の根性"}, + {"BaseWiz", "基本の賢さ"}, + {"RawWiz", "元の賢さ"}, + {"MoveDistance", "移動距離 (フレーム)"}, + {"RunMotionSpeed", "RunMotionSpeed"}, + {"RaceBaseSpeed", "レースの基本速度"}, + {"Keep Top", "ウィンドウを常に手前に表示"}, + {"Auto Close Window", "ウィンドウを自動で閉じる"}, + {"Show km/h", "速度をkm/hで表示"}, + {"Ignore Negative Speed", "ネガティブ移動速度を無視"}, + {"Gate/Name", "ゲート/名前"}, + {"Skill", "スキル"}, + {"AbilityType", "効果の種類"}, + {"Value", "値"}, + {"Targets", "ターゲット"}, + {"CoolDownTime", "クールダウンタイム"}, + {"RunningStyleExOonige", "RunningStyleExOonige"}, + {"HpDecRate", "HP減少率 (%)"}, + {"VisibleDistance", "距離の可視"}, + {"HpRate", "HPのパーセンテージ"}, + {"StartDash", "スタートダッシュ"}, + {"ForceOvertakeIn", "ForceOvertakeIn"}, + {"ForceOvertakeOut", "ForceOvertakeOut"}, + {"TemptationEndTime", "TemptationEndTime"}, + {"StartDelayFix", "StartDelayFix"}, + {"CurrentSpeed", "現在の速度"}, + {"CurrentSpeedWithNaturalDeceleration", "現在のスピードを上昇、徐々に降下"}, + {"TargetSpeed", "ターゲットの速度"}, + {"LaneMoveSpeed", "レーン変更速度"}, + {"TemptationPer", "TemptationPer"}, + {"PushPer", "PushPer"}, + {"Accel", "加速度"}, + {"AllStatus", "すべてのステータス向上"}, + {"TargetLane", "ターゲットのレーンを変更"}, + {"ActivateRandomNormalAndRareSkill", "ランダムで通常とレアスキルを発動"}, + {"ActivateRandomRareSkill", "ランダムでレアスキルを発動"}, + {"DebuffCancel", "デバフのキャンセル"}, + {"DebuffAbilityValueMultiply", "デバフ能力値倍増"}, + {"DebuffAbilityValueMultiplyOtherActivate", "DebuffAbilityValueMultiplyOtherActivate"}, + + {"Self", "自分"}, + {"All", "すべてのウマ娘"}, + {"AllOtherSelf", "自分以外のすべて"}, + {"Visible", "可視"}, + {"RandomOtherSelf", "自分以外ランダム"}, + {"Order", "指定の順位"}, + {"OrderInfront", "自分の前の順位"}, + {"OrderBehind", "自分の後の順位"}, + {"SelfInfront", "自分の前"}, + {"SelfBehind", "自分の後ろ"}, + {"TeamMember", "チームメンバー"}, + {"Near", "近く"}, + {"SelfAndBlockFront", "自分と前方をブロック"}, + {"BlockSide", "横方向にブロック"}, + {"NearInfront", "自分の前方付近"}, + {"NearBehind", "自分の後方付近"}, + {"RunningStyle", "指定された走行スタイル"}, + {"RunningStyleOtherSelf", "他の指定されたウマ娘の走法"}, + {"SelfInfrontTemptation", "SelfInfrontTemptation"}, + {"SelfBehindTemptation", "SelfBehindTemptation"}, + {"RunningStyleTemptationOtherSelf", "RunningStyleTemptationOtherSelf"}, + {"CharaId", "指定されたウマ娘のID"}, + {"ActivateHealSkill", "回復スキルの発動"}, + + {"Option", "選択肢"}, + {"Effects", "結果"}, + {"On Success:", "成功:"}, + {"On Failed:", "失敗:"}, }; const char* GetTrans(const char* text) { @@ -208,6 +317,11 @@ namespace GuiTrans { return iter->second.c_str(); } }; break; + case GuiTrans::GUILangType::JPN: { + if (const auto iter = guiJPNTrans.find(std::string_view(text)); iter != guiTChineseTrans.end()) { + return iter->second.c_str(); + } + }; break; default: break; } @@ -222,9 +336,10 @@ namespace GuiTrans { else if (tChineseLangIds.contains(localLang)) { return 2; } + else if (japaneseLangIds.contains(localLang)) { + return 3; + } return 0; } } - - diff --git a/src/umagui/guiShowData.cpp b/src/umagui/guiShowData.cpp index 6706952b..89a29787 100644 --- a/src/umagui/guiShowData.cpp +++ b/src/umagui/guiShowData.cpp @@ -163,3 +163,5 @@ void UmaGUiShowData::initGuiGlobalData() { } } } + +EventInfoDisplay eventInfoDisplay{ .eventInfo = EventHelper::EventInfo{.Choices = std::list{}} }; diff --git a/src/umagui/guiShowData.hpp b/src/umagui/guiShowData.hpp index c53c747a..6061fba3 100644 --- a/src/umagui/guiShowData.hpp +++ b/src/umagui/guiShowData.hpp @@ -498,3 +498,16 @@ namespace UmaGUiShowData { void initGuiGlobalData(); } + + +struct EventInfoDisplay { + EventHelper::EventInfo eventInfo; + std::vector gameChoicesText; + bool hasInfo = false; + + std::string currentGameStoryName; + long currentGameStoryId = -1; + bool isLoading = false; +}; + +extern EventInfoDisplay eventInfoDisplay; diff --git a/src/umagui/umaguiMain.cpp b/src/umagui/umaguiMain.cpp index 8dc98d87..e31fc82f 100644 --- a/src/umagui/umaguiMain.cpp +++ b/src/umagui/umaguiMain.cpp @@ -30,6 +30,14 @@ bool closeWhenRaceEnd = false; HWND hwnd; RECT cacheRect{ 100, 100, 1250, 1000 }; +ImFont* japaneseFont = NULL; +ImFont* chineseFont = NULL; +ImFont* defaultUIFont = NULL; + +extern std::unordered_set sChineseLangIds; +extern std::unordered_set tChineseLangIds; +extern std::unordered_set japaneseLangIds; + std::map umaRaceData{}; std::vector umaUsedSkillList{}; using namespace GuiTrans; @@ -521,7 +529,7 @@ umaData.BaseWiz, umaData.RawWiz ImGui::SameLine(); ImGui::Checkbox("###ignoreNegativeSpeed", &ignoreNegativeSpeed); - static const char* items[] = { "English", "简体中文", "繁體中文" }; + static const char* items[] = { "English", "简体中文", "繁體中文", "日本語"}; static int current_item = checkDefaultLang(); ImGui::Text("Language"); @@ -534,6 +542,8 @@ umaData.BaseWiz, umaData.RawWiz GuiLanguage = GUILangType::SCHINESE; break; case 2: GuiLanguage = GUILangType::TCHINESE; break; + case 3: + GuiLanguage = GUILangType::JPN; break; } changeTopState(); @@ -763,6 +773,85 @@ void imGuiRaceSkillInfoMainLoop() { } +bool guiLangInited = false; +void imGuiEventHelperLoop() { + if (!guiLangInited) { + guiLangInited = true; + GuiTrans::GuiLanguage = (GuiTrans::GUILangType)GuiTrans::checkDefaultLang(); + } + if (!g_enable_event_helper) return; + + if (ImGui::Begin("Event Helper")) { + ImGui::Text("Current Game Story: %s (%ld)", eventInfoDisplay.currentGameStoryName.c_str(), eventInfoDisplay.currentGameStoryId); + if (eventInfoDisplay.isLoading) { + ImGui::SameLine(); + ImGui::Text(" (Loading...)"); + } + + if (eventInfoDisplay.hasInfo) { + ImGui::BeginChild("EventInfo", ImVec2(0, 0), true); + + ImGui::Text("Event ID: %ld", eventInfoDisplay.eventInfo.Id); + ImGui::Text("Event Name: %s", eventInfoDisplay.eventInfo.Name.c_str()); + + ImGui::Separator(); + + ImGui::BeginTable("EventInfoTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders); + ImGui::TableSetupColumn(GuiTrans::GetTrans("Option")); + ImGui::TableSetupColumn(GuiTrans::GetTrans("Effects")); + ImGui::TableHeadersRow(); + + const bool displayLocalOption = eventInfoDisplay.eventInfo.Choices.size() == eventInfoDisplay.gameChoicesText.size(); + + int choiceIndex = 0; + for (const auto& choice : eventInfoDisplay.eventInfo.Choices) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (displayLocalOption && (choice.Option.compare(eventInfoDisplay.gameChoicesText[choiceIndex]) != 0)) { + ImGui::Text("%s\n%s", choice.Option.c_str(), eventInfoDisplay.gameChoicesText[choiceIndex].c_str()); + } + else { + ImGui::Text("%s", choice.Option.c_str()); + } + + ImGui::TableSetColumnIndex(1); + + if (choice.FailedEffect.empty()) { + ImGui::Text("%s", choice.SuccessEffect.c_str()); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", choice.SuccessEffect.c_str()); + ImGui::EndTooltip(); + } + } + else { + ImGui::Text(GuiTrans::GetTrans("On Success:")); + ImGui::Text("%s", choice.SuccessEffect.c_str()); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", choice.SuccessEffect.c_str()); + ImGui::EndTooltip(); + } + ImGui::Separator(); + ImGui::Text(GuiTrans::GetTrans("On Failed:")); + ImGui::Text("%s", choice.FailedEffect.c_str()); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", choice.FailedEffect.c_str()); + ImGui::EndTooltip(); + } + } + + choiceIndex++; + } + ImGui::EndTable(); + + ImGui::EndChild(); + } + } + ImGui::End(); +} + // Main code void guimain() @@ -867,16 +956,32 @@ void guimain() builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull()); builder.AddRanges(io.Fonts->GetGlyphRangesKorean()); builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); - builder.AddText("○◎△×☆"); + builder.AddText("○◯◎△×☆+−"); ImVector glyphRanges; builder.BuildRanges(&glyphRanges); config.GlyphRanges = glyphRanges.Data; - if (std::filesystem::exists("c:\\Windows\\Fonts\\msyh.ttc")) { - io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\msyh.ttc", 18.0f, &config); + const char* chineseFontPath = "c:/Windows/Fonts/msyhbd.ttc"; + const char* japaneseFontPath = "c:/Windows/Fonts/YuGothB.ttc"; + const char* defaultUIFontPath = "c:/Windows/Fonts/segoeui.ttf"; + + if (std::filesystem::exists(defaultUIFontPath)) { + defaultUIFont = io.Fonts->AddFontFromFileTTF(defaultUIFontPath, 18.0f, &config); + } + + if (std::filesystem::exists(chineseFontPath)) { + chineseFont = io.Fonts->AddFontFromFileTTF(chineseFontPath, 18.0f, &config); + } + else if (defaultUIFont != NULL) { + chineseFont = defaultUIFont; + } + + if (std::filesystem::exists(japaneseFontPath)) { + japaneseFont = io.Fonts->AddFontFromFileTTF(japaneseFontPath, 16.0f, &config); + } + else if (defaultUIFont != NULL) { + japaneseFont = defaultUIFont; } - else - io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf", 18.0f, &config); ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 0.00f); @@ -919,10 +1024,26 @@ void guimain() ImGui_ImplWin32_NewFrame(); ImGui::NewFrame(); + + switch (GuiLanguage) { + case GuiTrans::GUILangType::SCHINESE: + case GuiTrans::GUILangType::TCHINESE: { + ImGui::PushFont(chineseFont); + }; break; + case GuiTrans::GUILangType::JPN: { + ImGui::PushFont(japaneseFont); + }; break; + default: { + ImGui::PushFont(chineseFont); + } + } + imguiRaceMainLoop(io); imGuiRaceSkillInfoMainLoop(); LiveGUILoops::AllLoop(); + imGuiEventHelperLoop(); + ImGui::PopFont(); ImGui::Render(); const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w }; g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, NULL); diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 00000000..7f1a735d --- /dev/null +++ b/utils/README.md @@ -0,0 +1,39 @@ +# 启用事件簿功能 + +- 将 `config.json` 内 `enableEventHelper` 设置为 `true` 即可 +- 事件相关默认使用云端数据。若需要使用本地数据,请参考下方说明 + + + +# 使用本地数据 + +- 当插件存在 `localized_data/eventHelper` 目录,且该目录内存在和系统语言对应的 Json 文件时,将会启用本地数据。否则使用云端数据。 + +```javascript +events_scn.json // 简体中文系统 +events_tcn.json // 繁體中文系統 +events_jp.json // 日本語システム +events_en.json // The System of English and Other Languages +``` + + + +# 如何自行更新本地事件簿数据 + +- 事件簿数据位于 `resources/eventHelper` 文件夹内 +- 当数据更新后,可以使用 [UmamusumeDeserializeDB5](https://github.com/UmamusumeResponseAnalyzer/UmamusumeDeserializeDB5) 工具生成新的 `events.br`,也可以直接 [下载](https://github.com/UmamusumeResponseAnalyzer/UmamusumeResponseAnalyzer/raw/master/GameData/events.br) 现成的 +- 运行 `convert_db_data.py` + +```shell +python convert_db_data.py +``` + +- 完成后可以在上述文件夹内找到新文件 + + + +# 数据来源/鸣谢 + +- [https://kamigame.jp/umamusume/page/152540608660042049.html](https://kamigame.jp/umamusume/page/152540608660042049.html) +- [UmamusumeDeserializeDB5](https://github.com/UmamusumeResponseAnalyzer/UmamusumeDeserializeDB5) + diff --git a/utils/convert_db_data.py b/utils/convert_db_data.py new file mode 100644 index 00000000..1def546a --- /dev/null +++ b/utils/convert_db_data.py @@ -0,0 +1,117 @@ +import os +import brotli +import json +import requests +from pydantic import BaseModel +from typing import List, Optional, Union + + +class EffectValue(BaseModel): + Values: List[int] + SkillNames: List[str] + Extras: List[str] + BuffName: Optional[str] + + +class BaseChoice(BaseModel): + Option: str + SuccessEffect: str + FailedEffect: str + + def to_base_translated(self, lang): + print(self) + return self + + +class Choice(BaseChoice): + SuccessEffectValue: EffectValue + FailedEffectValue: Optional[EffectValue] + + def to_base_translated(self, lang=0): + trans_scn = ["速度", "耐力", "力量", "根性", "智力", "技能Pt", "Hint Lv", "体力", "羁绊", "干劲"] + trans_tcn = ["速度", "耐久力", "力量", "意志力", "智力", "技能Pt", "Hint Lv", "體力", "羈絆", "幹勁"] + trans_en = ["Speed", "Stamina", "Power", "Guts", "Wisdom", "Skill Pt", "Hint Lv", "Energy", "Bond Gauge", "Motivation"] + trans_jp = ["スピード", "スタミナ", "パワー", "根性", "賢さ", "スキルPt", "ヒント", "体力", "絆", "やる気"] + langs = [trans_scn, trans_tcn, trans_en, trans_jp] + + def value_to_string(value: int, is_skill=False): + if value > 0: + return f"+{value}" + elif value < 0: + return f"{value}" + if is_skill: + return "+1" + return value + + trans = langs[lang] + + success_effects = [] + failed_effects = [] + + for_steps = [[success_effects, self.SuccessEffectValue], [failed_effects, self.FailedEffectValue]] + + for eff_list, eff_value in for_steps: + if not eff_value: + continue + + for i in range(10): + if i == 6: + continue + curr_trans = trans[i] + curr_value = eff_value.Values[i] + if curr_value == 0: + continue + eff_list.append(f"{curr_trans} {value_to_string(curr_value)}") + + if eff_value.SkillNames: + for i in eff_value.SkillNames: + eff_list.append(f"「{i}」 {trans[6]} {value_to_string(eff_value.Values[6], is_skill=True)}") + + if eff_value.Extras: + eff_list += eff_value.Extras + if eff_value.BuffName: + eff_list.append(f"「{eff_value.BuffName}」獲得") + + return BaseChoice(Option=self.Option, + SuccessEffect=f"{'、'.join(success_effects)}", + FailedEffect=f"{'、'.join(failed_effects)}") + + +class DbDataItem(BaseModel): + Id: int + Name: str + TriggerName: str + Choices: List[List[Union[Choice, BaseChoice]]] + + +def main(): + with open(input("events.br path: ") or 'events.br', 'rb') as file: + decompressed_data = brotli.decompress(file.read()) + data_str = decompressed_data.decode() + + events = json.loads(data_str) + # with open("events.json", "w", encoding="utf8") as f: + # f.write(json.dumps(events, indent=4, ensure_ascii=False)) + + trans_index = ["scn", "tcn", "en", "jp"] + for n, lang in enumerate(trans_index): + events_data = [DbDataItem(**i) for i in events] + fmt_data = [] + + for i in events_data: + if len(i.Choices) <= 1: + continue + i.Choices = [[c.to_base_translated(lang=n) for c in choice] for choice in i.Choices] + fmt_data.append(i.dict(exclude={"TriggerName"})) + + base_path = "../resources/eventHelper" + if not os.path.isdir(base_path): + os.makedirs(base_path) + with open(f"{base_path}/events_{lang}.json", "w", encoding="utf8") as f: + f.write(json.dumps(fmt_data, ensure_ascii=False)) + print(f"Generated {base_path}/events_{lang}.json") + print("Generate done.") + + +if __name__ == "__main__": + main()