Working on translations for dialogs and cutscenes

This commit is contained in:
Vladislav Khorev committed 2026-07-05 21:52:37 +03:00
1 parent a98f71676d
commit 794a820be9
25 files changed
+3015 -123

No files matched your search

+6
View File
@@ -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);
+38
View 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
+23
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+11 -3
View File
@@ -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;
}
+5
View File
@@ -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;
+18 -10
View File
@@ -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;
+4
View File
@@ -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
+10
View File
@@ -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);
+4
View File
@@ -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;
+80
View File
@@ -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
+26
View File
@@ -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