Working on translations for dialogs and cutscenes
This commit is contained in:
1 parent
a98f71676d
commit
794a820be9
25 files changed
+3015
-123
No files matched your search
@@ -1,5 +1,6 @@
|
||||
#include "Game.h"
|
||||
#include "AnimatedModel.h"
|
||||
#include "Localization.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <fstream>
|
||||
#include "items/ItemRegistry.h"
|
||||
@@ -150,6 +151,11 @@ namespace ZL
|
||||
{
|
||||
std::cout << "Load resurces step 12x1" << std::endl;
|
||||
|
||||
// Must happen before createLocations() below: some location scripts
|
||||
// (e.g. resources/start_dorm.lua) start a dialogue as soon as they're
|
||||
// loaded, so the language needs to be known before that first load.
|
||||
ZL::loadLanguageSetting();
|
||||
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "Localization.h"
|
||||
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "utils/Utils.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
Language g_currentLanguage = Language::Russian;
|
||||
|
||||
std::string languageToCode(Language lang) {
|
||||
switch (lang) {
|
||||
case Language::English: return "en";
|
||||
case Language::Russian:
|
||||
default: return "ru";
|
||||
}
|
||||
}
|
||||
|
||||
Language languageFromCode(const std::string& code) {
|
||||
if (code == "en") return Language::English;
|
||||
return Language::Russian;
|
||||
}
|
||||
|
||||
void loadLanguageSetting() {
|
||||
const std::string content = readTextFile("settings.json");
|
||||
if (content.empty()) return;
|
||||
|
||||
try {
|
||||
const nlohmann::json root = nlohmann::json::parse(content);
|
||||
if (root.contains("language")) {
|
||||
g_currentLanguage = languageFromCode(root["language"].get<std::string>());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZL
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
enum class Language {
|
||||
Russian,
|
||||
English
|
||||
};
|
||||
|
||||
// Currently selected language for in-game text. Defaults to Russian.
|
||||
extern Language g_currentLanguage;
|
||||
|
||||
std::string languageToCode(Language lang);
|
||||
Language languageFromCode(const std::string& code);
|
||||
|
||||
// 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
|
||||
// location scripts start dialogues eagerly as soon as they're loaded.
|
||||
void loadLanguageSetting();
|
||||
|
||||
} // namespace ZL
|
||||
+2
-1
@@ -164,12 +164,13 @@ namespace ZL
|
||||
std::cout << "Shadow map initialized" << std::endl;
|
||||
//#endif
|
||||
|
||||
|
||||
setupNavigation(params.navigationJsonPaths);
|
||||
|
||||
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
||||
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
|
||||
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes008.json");
|
||||
dialogueSystem.loadTranslations("resources/dialogue/translate_dialogues.json");
|
||||
dialogueSystem.loadCutsceneTranslations("resources/dialogue/translate_cutscenes.json");
|
||||
dialogueSystem.setQuestJournal(journal);
|
||||
|
||||
npcNameText = std::make_unique<TextRenderer>();
|
||||
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
#include "MenuManager.h"
|
||||
#include "Game.h"
|
||||
#include "Localization.h"
|
||||
#include "render/TextRenderer.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <iostream>
|
||||
@@ -412,10 +413,10 @@ namespace ZL {
|
||||
});
|
||||
|
||||
uiManager.setTextButtonCallback("languageRussianButton", [this](const std::string&) {
|
||||
|
||||
ZL::g_currentLanguage = ZL::Language::Russian;
|
||||
});
|
||||
uiManager.setTextButtonCallback("languageEnglishButton", [this](const std::string&) {
|
||||
|
||||
ZL::g_currentLanguage = ZL::Language::English;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1821,6 +1822,7 @@ namespace ZL {
|
||||
root["musicEnabled"] = audioPlayer_.isMusicEnabled();
|
||||
root["soundEnabled"] = audioPlayer_.isSoundEnabled();
|
||||
root["fullscreen"] = Environment::isFullscreen;
|
||||
root["language"] = ZL::languageToCode(ZL::g_currentLanguage);
|
||||
|
||||
std::ofstream file("settings.json");
|
||||
if (file.is_open()) {
|
||||
@@ -1835,6 +1837,11 @@ namespace ZL {
|
||||
// directly in the right fullscreen state (see WIN32 main() in main.cpp) to avoid
|
||||
// a windowed -> fullscreen flash. The block below just keeps Environment::isFullscreen
|
||||
// in sync in case this is ever called from a path that didn't pre-apply it.
|
||||
// Language is loaded separately, even earlier (see ZL::loadLanguageSetting()
|
||||
// callers), because some location scripts start a dialogue as soon as they're
|
||||
// loaded, before this function ever runs.
|
||||
ZL::loadLanguageSetting();
|
||||
|
||||
const std::string content = ZL::readTextFile("settings.json");
|
||||
if (content.empty()) return;
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,14 @@ void CutsceneRuntime::setDatabase(const CutsceneDatabase* value) {
|
||||
database = value;
|
||||
}
|
||||
|
||||
void CutsceneRuntime::setTranslationDatabase(const ZL::Dialogue::TranslationDatabase* value) {
|
||||
translations = value;
|
||||
}
|
||||
|
||||
std::string CutsceneRuntime::tr(const std::string& s) const {
|
||||
return translations ? translations->translate(s, ZL::g_currentLanguage) : s;
|
||||
}
|
||||
|
||||
void CutsceneRuntime::setOnFinished(std::function<void(const std::string&)> cb) {
|
||||
onFinished = std::move(cb);
|
||||
}
|
||||
@@ -329,9 +337,9 @@ void CutsceneRuntime::refreshPresentation() {
|
||||
}
|
||||
|
||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||
presentation.speaker = line.speaker;
|
||||
presentation.fullText = line.text;
|
||||
presentation.visibleText = line.text;
|
||||
presentation.speaker = tr(line.speaker);
|
||||
presentation.fullText = tr(line.text);
|
||||
presentation.visibleText = presentation.fullText;
|
||||
presentation.selectedChoice = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "cutscene/CutsceneDatabase.h"
|
||||
#include "dialogue/DialogueTypes.h"
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "Localization.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
@@ -10,6 +12,7 @@ namespace ZL::Cutscene {
|
||||
class CutsceneRuntime {
|
||||
public:
|
||||
void setDatabase(const CutsceneDatabase* value);
|
||||
void setTranslationDatabase(const ZL::Dialogue::TranslationDatabase* value);
|
||||
|
||||
void setOnFinished(std::function<void(const std::string&)> cb);
|
||||
void setOnLineStarted(std::function<void(const std::string&)> cb);
|
||||
@@ -27,6 +30,7 @@ public:
|
||||
|
||||
private:
|
||||
const CutsceneDatabase* database = nullptr;
|
||||
const ZL::Dialogue::TranslationDatabase* translations = nullptr;
|
||||
|
||||
const StaticCutsceneDefinition* activeCutscene = nullptr;
|
||||
std::string activeCutsceneId;
|
||||
@@ -50,6 +54,7 @@ private:
|
||||
void syncLineToElapsedTime();
|
||||
void advanceLine();
|
||||
void refreshPresentation();
|
||||
std::string tr(const std::string& s) const;
|
||||
|
||||
std::vector<PresentedCutsceneImage> evaluateImages() const;
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ void DialogueRuntime::setDatabase(const DialogueDatabase* value) {
|
||||
database = value;
|
||||
}
|
||||
|
||||
void DialogueRuntime::setTranslationDatabase(const TranslationDatabase* value) {
|
||||
translations = value;
|
||||
}
|
||||
|
||||
std::string DialogueRuntime::tr(const std::string& s) const {
|
||||
return translations ? translations->translate(s, g_currentLanguage) : s;
|
||||
}
|
||||
|
||||
bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
|
||||
if (!database) {
|
||||
std::cerr << "[dialogue] No database assigned to runtime\n";
|
||||
@@ -295,14 +303,14 @@ void DialogueRuntime::presentLine(const Node& node) {
|
||||
mode = Mode::PresentingLine;
|
||||
revealCharacters = 0.0f;
|
||||
presentation.mode = PresentationMode::Dialogue;
|
||||
presentation.speaker = node.speaker;
|
||||
presentation.fullText = node.text;
|
||||
presentation.speaker = tr(node.speaker);
|
||||
presentation.fullText = tr(node.text);
|
||||
presentation.visibleText.clear();
|
||||
presentation.portraitPath = node.portrait;
|
||||
|
||||
presentation.choices.clear();
|
||||
presentation.selectedChoice = -1;
|
||||
presentation.revealCompleted = node.text.empty();
|
||||
presentation.revealCompleted = presentation.fullText.empty();
|
||||
presentation.showCutsceneSubtitle = false;
|
||||
presentation.cutsceneSkippable = false;
|
||||
|
||||
@@ -311,8 +319,8 @@ void DialogueRuntime::presentLine(const Node& node) {
|
||||
presentation.cutsceneBlackAlpha = 0.0f;
|
||||
|
||||
if (presentation.revealCompleted) {
|
||||
presentation.visibleText = node.text;
|
||||
revealCharacters = static_cast<float>(node.text.size());
|
||||
presentation.visibleText = presentation.fullText;
|
||||
revealCharacters = static_cast<float>(presentation.fullText.size());
|
||||
}
|
||||
|
||||
applyQuestActions(node.questUnlock, node.questComplete, node.questFail,
|
||||
@@ -321,7 +329,7 @@ void DialogueRuntime::presentLine(const Node& node) {
|
||||
onDialogueLineStarted(node.luaCallback);
|
||||
}
|
||||
if (!node.chatBubble.empty() && onChatBubbleReady) {
|
||||
onChatBubbleReady(node.text, node.chatBubble == "in");
|
||||
onChatBubbleReady(presentation.fullText, node.chatBubble == "in");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +341,7 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
||||
if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) continue;
|
||||
if (!evaluateConditions(choice.conditions)) continue;
|
||||
visibleChoices.push_back(choice);
|
||||
presentation.choices.push_back({ choice.id, choice.text, choice.kind });
|
||||
presentation.choices.push_back({ choice.id, tr(choice.text), choice.kind });
|
||||
}
|
||||
|
||||
if (visibleChoices.empty()) {
|
||||
@@ -349,9 +357,9 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
||||
mode = Mode::WaitingForChoice;
|
||||
selectedChoice = -1;
|
||||
presentation.mode = PresentationMode::Choice;
|
||||
presentation.speaker = node.speaker;
|
||||
presentation.fullText = node.text;
|
||||
presentation.visibleText = node.text;
|
||||
presentation.speaker = tr(node.speaker);
|
||||
presentation.fullText = tr(node.text);
|
||||
presentation.visibleText = presentation.fullText;
|
||||
presentation.portraitPath = node.portrait;
|
||||
|
||||
presentation.selectedChoice = -1;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "dialogue/DialogueDatabase.h"
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
@@ -15,6 +16,7 @@ namespace ZL::Dialogue {
|
||||
class DialogueRuntime : public ZL::ISaveable {
|
||||
public:
|
||||
void setDatabase(const DialogueDatabase* value);
|
||||
void setTranslationDatabase(const TranslationDatabase* value);
|
||||
|
||||
bool startDialogue(const std::string& dialogueId);
|
||||
void stop();
|
||||
@@ -62,6 +64,7 @@ private:
|
||||
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
||||
|
||||
const DialogueDatabase* database = nullptr;
|
||||
const TranslationDatabase* translations = nullptr;
|
||||
Quest::QuestJournal* questJournal = nullptr;
|
||||
const DialogueDefinition* activeDialogue = nullptr;
|
||||
|
||||
@@ -86,6 +89,7 @@ private:
|
||||
bool enterNode(const std::string& nodeId);
|
||||
void presentLine(const Node& node);
|
||||
void presentChoices(const Node& node);
|
||||
std::string tr(const std::string& s) const;
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@@ -4,7 +4,9 @@ namespace ZL::Dialogue {
|
||||
|
||||
bool DialogueSystem::init(Renderer& renderer, const std::string& zipFile) {
|
||||
dialogueRuntime.setDatabase(&database);
|
||||
dialogueRuntime.setTranslationDatabase(&translationDatabase);
|
||||
cutsceneRuntime.setDatabase(&cutsceneDatabase);
|
||||
cutsceneRuntime.setTranslationDatabase(&cutsceneTranslationDatabase);
|
||||
|
||||
// When dialogue hits a CutsceneStart node, hand off to cutsceneRuntime.
|
||||
dialogueRuntime.setOnCutsceneStartNeeded([this](const std::string& cutsceneId, const std::string& nextNodeId) {
|
||||
@@ -30,6 +32,14 @@ bool DialogueSystem::loadCutsceneDatabase(const std::string& path) {
|
||||
return cutsceneDatabase.loadFromFile(path);
|
||||
}
|
||||
|
||||
bool DialogueSystem::loadTranslations(const std::string& path) {
|
||||
return translationDatabase.loadFromFile(path);
|
||||
}
|
||||
|
||||
bool DialogueSystem::loadCutsceneTranslations(const std::string& path) {
|
||||
return cutsceneTranslationDatabase.loadFromFile(path);
|
||||
}
|
||||
|
||||
void DialogueSystem::update(int deltaMs) {
|
||||
dialogueRuntime.update(deltaMs);
|
||||
cutsceneRuntime.update(deltaMs);
|
||||
|
||||
@@ -20,6 +20,8 @@ public:
|
||||
|
||||
bool loadDatabase(const std::string& path);
|
||||
bool loadCutsceneDatabase(const std::string& path);
|
||||
bool loadTranslations(const std::string& path);
|
||||
bool loadCutsceneTranslations(const std::string& path);
|
||||
|
||||
void update(int deltaMs);
|
||||
void draw(Renderer& renderer);
|
||||
@@ -64,10 +66,12 @@ public:
|
||||
|
||||
private:
|
||||
DialogueDatabase database;
|
||||
TranslationDatabase translationDatabase;
|
||||
DialogueRuntime dialogueRuntime;
|
||||
DialogueOverlay dialogueOverlay;
|
||||
|
||||
ZL::Cutscene::CutsceneDatabase cutsceneDatabase;
|
||||
TranslationDatabase cutsceneTranslationDatabase;
|
||||
ZL::Cutscene::CutsceneRuntime cutsceneRuntime;
|
||||
ZL::Cutscene::CutsceneOverlay cutsceneOverlay;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
|
||||
#include "utils/Utils.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace ZL
|
||||
{
|
||||
extern const char* CONST_ZIP_FILE;
|
||||
}
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
bool TranslationDatabase::loadFromFile(const std::string& path) {
|
||||
entries.clear();
|
||||
|
||||
std::string raw;
|
||||
try {
|
||||
if (strlen(CONST_ZIP_FILE) == 0) {
|
||||
raw = readTextFile(path);
|
||||
}
|
||||
else {
|
||||
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
|
||||
if (buf.empty()) {
|
||||
std::cerr << "[translation] Failed to read " << path << " from zip\n";
|
||||
return false;
|
||||
}
|
||||
raw.assign(buf.begin(), buf.end());
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "[translation] Failed to open " << path << ": " << e.what() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
json root;
|
||||
try {
|
||||
root = json::parse(raw);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "[translation] JSON parse error in " << path << ": " << e.what() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.contains("dialogue_translations") && root["dialogue_translations"].is_array()) {
|
||||
for (const auto& item : root["dialogue_translations"]) {
|
||||
const std::string key = item.value("key", "");
|
||||
if (key.empty()) continue;
|
||||
|
||||
std::unordered_map<std::string, std::string> langMap;
|
||||
for (auto it = item.begin(); it != item.end(); ++it) {
|
||||
if (it.key() == "key") continue;
|
||||
if (it.value().is_string()) {
|
||||
langMap[it.key()] = it.value().get<std::string>();
|
||||
}
|
||||
}
|
||||
entries[key] = std::move(langMap);
|
||||
}
|
||||
}
|
||||
|
||||
return !entries.empty();
|
||||
}
|
||||
|
||||
std::string TranslationDatabase::translate(const std::string& key, Language lang) const {
|
||||
if (key.empty()) return key;
|
||||
|
||||
auto it = entries.find(key);
|
||||
if (it == entries.end()) return key;
|
||||
|
||||
const auto& langMap = it->second;
|
||||
|
||||
auto langIt = langMap.find(languageToCode(lang));
|
||||
if (langIt != langMap.end() && !langIt->second.empty()) return langIt->second;
|
||||
|
||||
auto ruIt = langMap.find("ru");
|
||||
if (ruIt != langMap.end() && !ruIt->second.empty()) return ruIt->second;
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "Localization.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
// Loads a shared translation file mapping dialogue "key" strings (the literal
|
||||
// speaker/text/choice text found in dialogue config files) to per-language text.
|
||||
class TranslationDatabase {
|
||||
public:
|
||||
using json = nlohmann::json;
|
||||
|
||||
bool loadFromFile(const std::string& path);
|
||||
|
||||
// Returns the translated text for `key` in `lang`, falling back to Russian,
|
||||
// then to `key` itself if no entry or translation exists.
|
||||
std::string translate(const std::string& key, Language lang) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> entries;
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
Reference in new issue
Block a user