From e2d9105b020163516adbbcceb2886e07e5378f75 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Tue, 7 Jul 2026 11:24:21 +0300 Subject: [PATCH] Working on translations --- resources/config/items.json | 9 +- resources/config/npcs_uni_interior.json | 2 +- resources/config/quests.json | 8 +- resources/config_en/items.json | 60 ++++++++++++++ resources/config_en/quests.json | 83 +++++++++++++++++++ resources/dialogue/translate_dialogues.json | 6 +- .../dialogue/uni_interior_dialogues_009.json | 4 +- src/Game.cpp | 2 +- src/Localization.cpp | 33 ++++++++ src/Localization.h | 9 ++ src/MenuManager.cpp | 8 ++ src/MenuManager.h | 1 + src/ScriptEngine.cpp | 2 +- src/items/Item.cpp | 59 +++++++------ src/items/Item.h | 14 ++-- src/items/ItemRegistry.cpp | 18 +--- src/quest/QuestJournal.cpp | 36 ++++---- src/quest/QuestJournal.h | 5 ++ 18 files changed, 271 insertions(+), 88 deletions(-) create mode 100644 resources/config_en/items.json create mode 100644 resources/config_en/quests.json diff --git a/resources/config/items.json b/resources/config/items.json index 095fbdc..a7d4022 100644 --- a/resources/config/items.json +++ b/resources/config/items.json @@ -20,13 +20,6 @@ "description": "Это ключ от учительской, который я получил от Айпери.", "icon": "resources/w/ui/img/inv/ItemKey001.png", "selectedIcon": "resources/w/ui/img/inv/ItemSelKey001.png" - }, - { - "id": "all_keys", - "name": "Ключи от универа", - "description": "Это ключи от всех комнат в универе, которые я получил от Айпери.", - "icon": "resources/w/ui/img/inv/ItemKey001.png", - "selectedIcon": "resources/w/ui/img/inv/ItemSelKey001.png" }, { "id": "knife", @@ -45,7 +38,7 @@ { "id": "note_spell", "name": "Записка", - "description": "Тут написано \"Прочитай вслух когда настанет ночь, сквозь двери сможешь ты пройти.\" и текст заклинания.", + "description": "Тут написано \"Привет собрату теней от собрата теней! Дарю тебе способ пройти через любую запертую дверь! Коснись рукой серебра, и прочитай вслух это заклинание. Заклинание работает только ночью.\" и текст заклинания.", "icon": "resources/w/ui/img/inv/ItemNote001.png", "selectedIcon": "resources/w/ui/img/inv/ItemSelNote001.png" }, diff --git a/resources/config/npcs_uni_interior.json b/resources/config/npcs_uni_interior.json index df6e78d..29b809d 100644 --- a/resources/config/npcs_uni_interior.json +++ b/resources/config/npcs_uni_interior.json @@ -2,7 +2,7 @@ "npcs": [ { "id": "npc_teacher01", - "name": "Аида Дженибекова", + "name": "Аида Джаныбековна", "animationIdlePath": "resources/w/new_anims/teacher_stand_idle001_small.anim", "animationWalkPath": "resources/w/new_anims/teacher_walk001_small.anim", "meshTextures": { diff --git a/resources/config/quests.json b/resources/config/quests.json index f70e6ea..10f163c 100644 --- a/resources/config/quests.json +++ b/resources/config/quests.json @@ -20,7 +20,7 @@ "objectives": [ { "id": "ghost_lore_aiperi", "text": "Поговорить с Айпери", "completed": false }, { "id": "ghost_lore_alik", "text": "Поговорить с Аликом", "completed": false, "visible": false }, - { "id": "ghost_lore_teacher", "text": "Поговорить с Аидой Джаныбековой", "completed": false } + { "id": "ghost_lore_teacher", "text": "Поговорить с Аидой Джаныбековной", "completed": false } ] }, { @@ -31,7 +31,7 @@ "description": "История Бегимай еще более загадочная. Во время сдачи курсовой работы в университете происходила генеральная уборка. По случайности, ее курсовая работа оказалась в стопке бумаг на выброс. В результате курсач оказался на помойке, а преподаватель, не обнаружив курсовую работу, поставил ей ноль баллов. По словам Алика, курсовая работа до сих пор лежит в куче мусора во дворе университета.", "objectives": [ { "id": "ghost_coursework_find", "text": "Найти курсовую работу Бегимай", "completed": false }, - { "id": "ghost_coursework_mark", "text": "Показать курсовую работу Аиде Джаныбековой", "completed": false } + { "id": "ghost_coursework_mark", "text": "Показать курсовую работу Аиде Джаныбековной", "completed": false } ] }, { @@ -51,7 +51,7 @@ "title": "Манасоведение", "status": "Available", "autoComplete": true, - "description": "Скоро будет модуль по Манасоведению, а я еще ни разу не ходил на лекции. Сегодня у нас лекция по Манасоведению, надо ее посетить и получить у преподавательницы Аиды Дженибековной задание на модуль.", + "description": "Скоро будет модуль по Манасоведению, а я еще ни разу не ходил на лекции. Сегодня у нас лекция по Манасоведению, надо ее посетить и получить у преподавательницы Аиды Джаныбековной задание на модуль.", "objectives": [ { "id": "study_beginning_lecture", "text": "Посетить лекцию", "completed": false }, { "id": "study_beginning_task", "text": "Получить задание на модуль", "completed": false } @@ -61,7 +61,7 @@ "id": "study_project", "title": "Эссе манасчи", "status": "Hidden", - "description": "Преподавательница по манасоведению Аида Джаныбекова дала мне задачу - найти в библиотеке книгу про манасчи Жусупа Мамая, и написать по этой книге эссе. Книгу нельзя выносить из библиотеки, но можно воспользоваться стоящим в библиотеке компьютером, чтобы написать эссе.", + "description": "Преподавательница по манасоведению Аида Джаныбековна дала мне задачу - найти в библиотеке книгу про манасчи Жусупа Мамая, и написать по этой книге эссе. Книгу нельзя выносить из библиотеки, но можно воспользоваться стоящим в библиотеке компьютером, чтобы написать эссе.", "objectives": [ { "id": "study_project_book", "text": "Найти книгу", "completed": false }, { "id": "study_project_write", "text": "Написать эссе", "completed": false }, diff --git a/resources/config_en/items.json b/resources/config_en/items.json new file mode 100644 index 0000000..1399d8c --- /dev/null +++ b/resources/config_en/items.json @@ -0,0 +1,60 @@ +{ + "items": [ + { + "id": "phone", + "name": "Phone", + "description": "I can't imagine life without my phone.", + "icon": "resources/w/ui/img/inv/ItemPhone001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelPhone001.png" + }, + { + "id": "journal", + "name": "Journal", + "description": "This is my journal where I write my notes.", + "icon": "resources/w/ui/img/inv/ItemJournal001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelJournal001.png" + }, + { + "id": "teacher_room_key", + "name": "Key to the teachers' room", + "description": "This is the key to the teachers' room that I got from Aiperi.", + "icon": "resources/w/ui/img/inv/ItemKey001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelKey001.png" + }, + { + "id": "knife", + "name": "Silver knife", + "description": "I borrowed this silver knife from Aiperi, and I must return it.", + "icon": "resources/w/ui/img/inv/ItemKnife001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelKnife001.png" + }, + { + "id": "book", + "name": "Book", + "description": "This is a book about the manaschi Zhusup Mamai.", + "icon": "resources/w/ui/img/inv/ItemBook001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelBook001.png" + }, + { + "id": "note_spell", + "name": "Note", + "description": "It says, \"Greetings to a fellow shadow from a fellow shadow! I give you a way to get through any locked door! Touch the silver with your hand and read this spell out loud! The spell only works at night.\" and the text of the spell.", + "icon": "resources/w/ui/img/inv/ItemNote001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelNote001.png" + }, + { + "id": "report_card", + "name": "Begimai's grade book", + "description": "This is Begimai's grade book. This is where you need to put your grade for your coursework.", + "icon": "resources/w/ui/img/inv/ItemReportCard001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelReportCard001.png" + }, + { + "id": "coursework", + "name": "Begimai's coursework", + "description": "This is Begimai's coursework, because of which she jumped out of the window.", + "icon": "resources/w/ui/img/inv/ItemCoursework001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelCoursework001.png" + } + ] +} diff --git a/resources/config_en/quests.json b/resources/config_en/quests.json new file mode 100644 index 0000000..c68cf6c --- /dev/null +++ b/resources/config_en/quests.json @@ -0,0 +1,83 @@ +{ + "quests": [ + { + "id": "tutorial_take_items", + "title": "The Phone and the Journal", + "status": "Available", + "autoComplete": true, + "description": "Whenever I go somewhere, I always take my phone and my journal with me.", + "objectives": [ + { "id": "take_phone", "text": "Take the phone", "completed": false }, + { "id": "take_journal", "text": "Take the journal", "completed": false } + ] + }, + { + "id": "ghost_lore", + "title": "The Ghost", + "status": "Hidden", + "autoComplete": true, + "description": "A ghostly female student appears at the university every night. She claims her name is Begimai and that she has a coursework due. I don't remember a student by that name, but maybe she's from a different year? I need to find out more.", + "objectives": [ + { "id": "ghost_lore_aiperi", "text": "Talk to Aiperi", "completed": false }, + { "id": "ghost_lore_alik", "text": "Talk to Alik", "completed": false, "visible": false }, + { "id": "ghost_lore_teacher", "text": "Talk to Aida Dzhanybekovna", "completed": false } + ] + }, + { + "id": "ghost_coursework", + "title": "The Ghost's coursework", + "status": "Hidden", + "autoComplete": true, + "description": "Begimai's story is even more mysterious. While her coursework was due, the university was undergoing a general cleaning. By chance, her coursework ended up in the trash bin. As a result, her coursework was thrown away, the professor was not able to find it, and gave her a zero grade. According to Alik, the coursework still lies in a pile of trash in the university courtyard.", + "objectives": [ + { "id": "ghost_coursework_find", "text": "Find Begimai's coursework", "completed": false }, + { "id": "ghost_coursework_mark", "text": "Show coursework to Aida Dzhanybekovna", "completed": false } + ] + }, + { + "id": "ghost_release", + "title": "Liberation", + "status": "Hidden", + "autoComplete": true, + "description": "A student named Begimai actually attended university last year. She tried to submit a coursework, but due to a series of circumstances, she failed and received a zero grade. This coursework shattered all her plans and hopes, and Begimai jumped out the window. Now she's returned as a ghost, and to be liberated, she must ensure she receives a grade for the coursework.", + "objectives": [ + { "id": "ghost_release_reportcard", "text": "Find Begimai's grade book", "completed": false }, + { "id": "ghost_release_mark", "text": "Put a grade for the coursework in the grade book", "completed": false }, + { "id": "ghost_release_show", "text": "Show the grade book to the ghost", "completed": false } + ] + }, + { + "id": "study_beginning", + "title": "Manas studies", + "status": "Available", + "autoComplete": true, + "description": "There's a Manas Studies module coming up soon, and I've never attended a lecture before. We have a Manas Studies lecture today, so I need to attend it and get the module assignment from my teacher, Aida Dzhanybekovna.", + "objectives": [ + { "id": "study_beginning_lecture", "text": "Attend the lecture", "completed": false }, + { "id": "study_beginning_task", "text": "Get an assignment for the module", "completed": false } + ] + }, + { + "id": "study_project", + "title": "Essay on Manaschi", + "status": "Hidden", + "description": "My Manas studies teacher, Aida Dzhanybekovna, gave me an assignment: find a book about Zhusup Mamai's Manaschi in the library and write an essay about it. The book isn't allowed out of the library, but I can use the library's computer to write the essay.", + "objectives": [ + { "id": "study_project_book", "text": "Find the book", "completed": false }, + { "id": "study_project_write", "text": "Write the essay", "completed": false }, + { "id": "study_project_give", "text": "Submit the essay", "completed": false } + ] + }, + { + "id": "aiperi_knife", + "title": "Silver knife", + "status": "Hidden", + "description": "Aiperi lent me the silver knife we used to cut the teachers' birthday cake. The knife is still somewhere in the staff room, and I have to get it and return it to Aiperi.", + "objectives": [ + { "id": "aiperi_knife_keys", "text": "Get the keys to the teachers' room", "completed": false }, + { "id": "aiperi_knife_take", "text": "Take the knife", "completed": false }, + { "id": "aiperi_knife_give", "text": "Give the knife to Aiperi", "completed": false } + ] + } + ] +} diff --git a/resources/dialogue/translate_dialogues.json b/resources/dialogue/translate_dialogues.json index 887714e..f3ea585 100644 --- a/resources/dialogue/translate_dialogues.json +++ b/resources/dialogue/translate_dialogues.json @@ -666,9 +666,9 @@ "en": "Bekzat, what do you need?" }, { - "key": "Сдать модуль", - "ru": "Сдать модуль", - "en": "Submit the module" + "key": "Задание на модуль", + "ru": "Задание на модуль", + "en": "Assignment for the module" }, { "key": "Ничего", diff --git a/resources/dialogue/uni_interior_dialogues_009.json b/resources/dialogue/uni_interior_dialogues_009.json index ebcffbc..6a9e317 100644 --- a/resources/dialogue/uni_interior_dialogues_009.json +++ b/resources/dialogue/uni_interior_dialogues_009.json @@ -478,7 +478,7 @@ { "id": "choice_1783179865107", "kind": "Main", - "text": "Сдать модуль", + "text": "Задание на модуль", "next": "line_14" }, { @@ -499,7 +499,7 @@ { "id": "choice_1783179745949", "kind": "Main", - "text": "Сдать модуль", + "text": "Задание на модуль", "next": "line_14" }, { diff --git a/src/Game.cpp b/src/Game.cpp index 87e56a2..1c8c574 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -316,7 +316,7 @@ namespace ZL gameState.chatPreviewMsg[i].clear(); gameState.chatHistory[i].clear(); } - gameState.inventory.items.clear(); + gameState.inventory.clear(); gameState.questJournal.loadFromFile("resources/config/quests.json", CONST_ZIP_FILE); createLocations(); }; diff --git a/src/Localization.cpp b/src/Localization.cpp index 81121f7..7ef445e 100644 --- a/src/Localization.cpp +++ b/src/Localization.cpp @@ -21,6 +21,39 @@ Language languageFromCode(const std::string& code) { return Language::Russian; } +std::string localizedConfigPath(const std::string& basePath, Language lang) { + static const std::string marker = "/config/"; + const size_t pos = basePath.find(marker); + if (pos == std::string::npos) return basePath; + return basePath.substr(0, pos) + "/config_" + languageToCode(lang) + "/" + basePath.substr(pos + marker.size()); +} + +std::string readLocalizedConfigFile(const std::string& basePath, const std::string& zipFile) { + const std::string localizedPath = localizedConfigPath(basePath, g_currentLanguage); + if (localizedPath != basePath) { + std::string content; + if (zipFile.empty()) { + content = readTextFile(localizedPath); + } else { + try { + const auto buf = readFileFromZIP(localizedPath, zipFile); + content.assign(buf.begin(), buf.end()); + } catch (const std::exception&) { + content.clear(); + } + } + if (!content.empty()) return content; + } + + if (zipFile.empty()) return readTextFile(basePath); + try { + const auto buf = readFileFromZIP(basePath, zipFile); + return std::string(buf.begin(), buf.end()); + } catch (const std::exception&) { + return ""; + } +} + void loadLanguageSetting() { const std::string content = readTextFile("settings.json"); if (content.empty()) return; diff --git a/src/Localization.h b/src/Localization.h index 78b3b76..7543112 100644 --- a/src/Localization.h +++ b/src/Localization.h @@ -14,6 +14,15 @@ extern Language g_currentLanguage; std::string languageToCode(Language lang); Language languageFromCode(const std::string& code); +// "resources/config/items.json" + English -> "resources/config_en/items.json". +// Returns basePath unchanged if it doesn't contain a "/config/" segment. +std::string localizedConfigPath(const std::string& basePath, Language lang); + +// Reads basePath, preferring the config_ variant for g_currentLanguage; +// falls back to basePath if the localized file/zip-entry is missing or empty. +// zipFile empty => loose files from disk; non-empty => entries inside that zip. +std::string readLocalizedConfigFile(const std::string& basePath, const std::string& zipFile); + // Reads the "language" field from settings.json (if present) and updates // g_currentLanguage. Safe to call even if settings.json doesn't exist yet. // Must run before any location/dialogue content is loaded, since some diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 2ebba10..4733694 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -1,6 +1,7 @@ #include "MenuManager.h" #include "Game.h" #include "Localization.h" +#include "items/ItemRegistry.h" #include "render/TextRenderer.h" #include "utils/Utils.h" #include @@ -414,12 +415,19 @@ namespace ZL { uiManager.setTextButtonCallback("languageRussianButton", [this](const std::string&) { ZL::g_currentLanguage = ZL::Language::Russian; + reloadLocalizedGameContent(); }); uiManager.setTextButtonCallback("languageEnglishButton", [this](const std::string&) { ZL::g_currentLanguage = ZL::Language::English; + reloadLocalizedGameContent(); }); } + void MenuManager::reloadLocalizedGameContent() { + ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE); + gameState_.questJournal.reloadDefinitionsPreservingState("resources/config/quests.json", CONST_ZIP_FILE); + } + void MenuManager::showLoadGameScreen() { uiManager.pushMenuFromSavedRoot(loadSavedGameScreenRoot); diff --git a/src/MenuManager.h b/src/MenuManager.h index eeef321..4cb8d9f 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -118,6 +118,7 @@ namespace ZL { void enterGameplay(bool isFromLoad = false); void applyHudForCurrentState(); void refreshQuestJournalUi(); + void reloadLocalizedGameContent(); void selectQuestByIndex(int index); void refreshItemPickupHud(); void setupStep5Callbacks(); diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index 64facf6..bf2cd65 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -124,7 +124,7 @@ namespace ZL { api.set_function("pickup_item", [inventory](const std::string& itemId) { const Item* item = ItemRegistry::instance().findById(itemId); if (item) { - inventory->addItem(*item); + inventory->addItem(item->id); std::cout << "[script] pickup_item: " << item->name << std::endl; } else { std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n"; diff --git a/src/items/Item.cpp b/src/items/Item.cpp index 604355c..3d691de 100644 --- a/src/items/Item.cpp +++ b/src/items/Item.cpp @@ -1,56 +1,55 @@ #include "Item.h" +#include "ItemRegistry.h" #include "external/nlohmann/json.hpp" #include #include namespace ZL { - void Inventory::addItem(const Item& item) { - items.push_back(item); - std::cout << "Item added to inventory: " << item.name << std::endl; - if (onItemAdded) onItemAdded(item.id); + void Inventory::addItem(const std::string& itemId) { + itemIds.push_back(itemId); + std::cout << "Item added to inventory: " << itemId << std::endl; + if (onItemAdded) onItemAdded(itemId); } void Inventory::removeItem(const std::string& itemId) { - auto it = std::find_if(items.begin(), items.end(), - [&itemId](const Item& item) { return item.id == itemId; }); - if (it != items.end()) { - std::cout << "Item removed from inventory: " << it->name << std::endl; + auto it = std::find(itemIds.begin(), itemIds.end(), itemId); + if (it != itemIds.end()) { + std::cout << "Item removed from inventory: " << itemId << std::endl; if (onItemRemoved) onItemRemoved(itemId); - items.erase(it); + itemIds.erase(it); } } bool Inventory::hasItem(const std::string& itemId) const { - return std::find_if(items.begin(), items.end(), - [&itemId](const Item& item) { return item.id == itemId; }) != items.end(); + return std::find(itemIds.begin(), itemIds.end(), itemId) != itemIds.end(); + } + + std::vector Inventory::getItems() const { + std::vector result; + result.reserve(itemIds.size()); + for (const auto& id : itemIds) { + const Item* def = ItemRegistry::instance().findById(id); + if (def) result.push_back(*def); + } + return result; } void Inventory::save(nlohmann::json& out) const { - nlohmann::json arr = nlohmann::json::array(); - for (const auto& item : items) { - arr.push_back({ - {"id", item.id}, - {"name", item.name}, - {"description", item.description}, - {"icon", item.icon}, - {"selectedIcon", item.selectedIcon} - }); - } - out["items"] = std::move(arr); + out["items"] = itemIds; } void Inventory::load(const nlohmann::json& in) { - items.clear(); + itemIds.clear(); if (!in.contains("items") || !in["items"].is_array()) return; for (const auto& j : in["items"]) { - Item item; - item.id = j.value("id", ""); - item.name = j.value("name", ""); - item.description = j.value("description", ""); - item.icon = j.value("icon", ""); - item.selectedIcon = j.value("selectedIcon", ""); - items.push_back(std::move(item)); + if (j.is_string()) { + itemIds.push_back(j.get()); + } else if (j.is_object()) { + // Backward-compat with old save files that stored full item structs. + const std::string id = j.value("id", ""); + if (!id.empty()) itemIds.push_back(id); + } } } diff --git a/src/items/Item.h b/src/items/Item.h index 551c06a..f72afe6 100644 --- a/src/items/Item.h +++ b/src/items/Item.h @@ -22,13 +22,14 @@ namespace ZL { class Inventory : public ISaveable { public: - std::vector items; - void addItem(const Item& item); + void addItem(const std::string& itemId); void removeItem(const std::string& itemId); - const std::vector& getItems() const { return items; } + // Resolves current items from ItemRegistry by id, so returned data + // always reflects the currently loaded (localized) definitions. + std::vector getItems() const; bool hasItem(const std::string& itemId) const; - void clear() { items.clear(); } - size_t getCount() const { return items.size(); } + void clear() { itemIds.clear(); } + size_t getCount() const { return itemIds.size(); } void save(nlohmann::json& out) const override; void load(const nlohmann::json& in) override; @@ -36,6 +37,9 @@ namespace ZL { // Called whenever an item is added/removed; receives the item id. std::function onItemAdded; std::function onItemRemoved; + + private: + std::vector itemIds; }; } // namespace ZL \ No newline at end of file diff --git a/src/items/ItemRegistry.cpp b/src/items/ItemRegistry.cpp index cf992a1..6465095 100644 --- a/src/items/ItemRegistry.cpp +++ b/src/items/ItemRegistry.cpp @@ -1,5 +1,6 @@ #include "ItemRegistry.h" #include "utils/Utils.h" +#include "Localization.h" #include "external/nlohmann/json.hpp" #include @@ -11,20 +12,9 @@ ItemRegistry& ItemRegistry::instance() { } void ItemRegistry::loadFromJson(const std::string& jsonPath, const std::string& zipPath) { - std::string content; - try { - if (zipPath.empty()) { - content = readTextFile(jsonPath); - } else { - auto buf = readFileFromZIP(jsonPath, zipPath); - if (buf.empty()) { - std::cerr << "[ItemRegistry] Failed to read " << jsonPath << " from zip\n"; - return; - } - content.assign(buf.begin(), buf.end()); - } - } catch (const std::exception& e) { - std::cerr << "[ItemRegistry] Failed to open " << jsonPath << ": " << e.what() << "\n"; + const std::string content = readLocalizedConfigFile(jsonPath, zipPath); + if (content.empty()) { + std::cerr << "[ItemRegistry] Failed to read " << jsonPath << "\n"; return; } diff --git a/src/quest/QuestJournal.cpp b/src/quest/QuestJournal.cpp index f18056d..36407a4 100644 --- a/src/quest/QuestJournal.cpp +++ b/src/quest/QuestJournal.cpp @@ -1,6 +1,7 @@ #include "quest/QuestJournal.h" #include "external/nlohmann/json.hpp" #include "utils/Utils.h" +#include "Localization.h" #include #include @@ -26,26 +27,9 @@ static QuestStatus parseQuestStatus(const std::string& value) { } bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipFile) { - quests.clear(); - questOrder.clear(); - - std::string content; - try { - if (zipFile.empty()) { - content = ZL::readTextFile(path); - } - else { - auto data = ZL::readFileFromZIP(path, zipFile); - content.assign(data.begin(), data.end()); - } - } - catch (const std::exception& e) { - std::cerr << "[quest] Failed to read " << path << ": " << e.what() << std::endl; - return false; - } - + const std::string content = ZL::readLocalizedConfigFile(path, zipFile); if (content.empty()) { - std::cerr << "[quest] Empty quest file: " << path << std::endl; + std::cerr << "[quest] Failed to read " << path << std::endl; return false; } @@ -63,6 +47,12 @@ bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipF return false; } + // Only clear existing definitions/state once the new file is confirmed + // valid, so a failed reload (e.g. a broken localized file) doesn't wipe + // the currently loaded quest journal. + quests.clear(); + questOrder.clear(); + for (const auto& item : root["quests"]) { QuestDefinition def; def.id = item.value("id", ""); @@ -101,6 +91,14 @@ bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipF return true; } +bool QuestJournal::reloadDefinitionsPreservingState(const std::string& path, const std::string& zipFile) { + json snapshot; + save(snapshot); + if (!loadFromFile(path, zipFile)) return false; + load(snapshot); + return true; +} + bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) { QuestState* quest = findQuest(questId); if (!quest) return false; diff --git a/src/quest/QuestJournal.h b/src/quest/QuestJournal.h index 99aa7fe..3e14854 100644 --- a/src/quest/QuestJournal.h +++ b/src/quest/QuestJournal.h @@ -14,6 +14,11 @@ class QuestJournal : public ZL::ISaveable { public: bool loadFromFile(const std::string& path, const std::string& zipFile = ""); + // Reloads quest definitions (e.g. after a language switch) while + // preserving current progress: snapshots status/objective state, reloads + // definitions from path, then re-applies the snapshot. + bool reloadDefinitionsPreservingState(const std::string& path, const std::string& zipFile = ""); + void save(nlohmann::json& out) const override; void load(const nlohmann::json& in) override;