i18n: Create a JSON subtitle format for translating via Crowdin (#2644)

This commit is contained in:
Tyler Wilding 2023-05-18 19:54:59 -05:00 committed by GitHub
parent 70cb7e3171
commit 288c093913
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 92567 additions and 15906 deletions

View file

@ -10,124 +10,128 @@
#include "third-party/fmt/ranges.h"
#include "third-party/json.hpp"
bool write_subtitle_db_to_files(const GameSubtitleDB& db) {
// Write the subtitles out
std::vector<int> completed_banks = {};
for (const auto& [id, bank] : db.m_banks) {
// If we've done the bank before, skip it
auto it = find(completed_banks.begin(), completed_banks.end(), bank->m_lang_id);
if (it != completed_banks.end()) {
continue;
}
// Check to see if this bank is shared by any other, if so do it at the same time
// and skip it
// This is basically just to deal with US/UK english in a not so hacky way
std::vector<int> banks = {};
for (const auto& [_id, _bank] : db.m_banks) {
if (_bank->file_path == bank->file_path) {
banks.push_back(_bank->m_lang_id);
completed_banks.push_back(_bank->m_lang_id);
SubtitleMetadataFile dump_bank_as_meta_json(std::shared_ptr<GameSubtitleBank> bank) {
auto meta_file = SubtitleMetadataFile();
auto font = get_font_bank(bank->m_text_version);
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
if (scene_info.m_kind == SubtitleSceneKind::Movie) {
std::vector<SubtitleCutsceneLineMetadata> lines;
for (const auto& line : scene_info.m_lines) {
auto line_meta = SubtitleCutsceneLineMetadata();
line_meta.frame = line.frame;
if (line.line.empty()) {
line_meta.clear = true;
} else {
auto line_speaker = font->convert_game_to_utf8(line.speaker.c_str());
line_meta.offscreen = line.offscreen;
line_meta.speaker = line_speaker;
}
lines.push_back(line_meta);
}
}
std::string file_contents = "";
file_contents += fmt::format("(language-id {})\n", fmt::join(banks, " "));
auto file_ver = parse_text_only_version(bank->file_path);
auto font = get_font_bank(file_ver);
file_contents += fmt::format("(text-version {})\n", get_text_version_name(file_ver));
for (const auto& group_name : db.m_subtitle_groups->m_group_order) {
bool last_was_single = false;
file_contents +=
fmt::format("\n;; -----------------\n;; {}\n;; -----------------\n", group_name);
std::vector<GameSubtitleSceneInfo> all_scenes;
for (const auto& [scene_name, scene] : bank->scenes()) {
all_scenes.push_back(scene);
meta_file.cutscenes[scene_name] = lines;
} else if (scene_info.m_kind == SubtitleSceneKind::Hint ||
scene_info.m_kind == SubtitleSceneKind::HintNamed) {
SubtitleHintMetadata hint;
hint.id = fmt::format("{:x}", scene_info.m_id);
std::vector<SubtitleHintLineMetadata> lines;
for (const auto& line : scene_info.m_lines) {
auto line_meta = SubtitleHintLineMetadata();
line_meta.frame = line.frame;
if (line.line.empty()) {
line_meta.clear = true;
} else {
auto line_speaker = font->convert_game_to_utf8(line.speaker.c_str());
line_meta.speaker = line_speaker;
}
lines.push_back(line_meta);
}
std::sort(all_scenes.begin(), all_scenes.end(),
[](const GameSubtitleSceneInfo& a, const GameSubtitleSceneInfo& b) {
if (a.kind() != b.kind()) {
return a.kind() < b.kind();
}
if (a.kind() == SubtitleSceneKind::Movie) {
return a.name() < b.name();
} else if (a.kind() == SubtitleSceneKind::HintNamed) {
if (a.id() == b.id()) {
return a.name() < b.name();
} else {
return a.id() < b.id();
}
} else if (a.kind() == SubtitleSceneKind::Hint) {
return a.id() < b.id();
}
return false;
});
for (const auto& scene : all_scenes) {
if (scene.m_sorting_group != group_name) {
hint.lines = lines;
meta_file.hints[scene_name] = hint;
}
}
return meta_file;
}
SubtitleFile dump_bank_as_json(std::shared_ptr<GameSubtitleBank> bank) {
SubtitleFile file;
auto font = get_font_bank(bank->m_text_version);
// Figure out speakers
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
for (const auto& line : scene_info.m_lines) {
if (line.line.empty()) {
continue;
}
auto line_speaker = font->convert_game_to_utf8(line.speaker.c_str());
file.speakers[line_speaker] = line_speaker;
}
}
// Hints
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
if (scene_info.m_kind == SubtitleSceneKind::Hint ||
scene_info.m_kind == SubtitleSceneKind::HintNamed) {
file.hints[scene_name] = {};
for (const auto& scene_line : scene_info.m_lines) {
if (scene_line.line.empty()) {
continue;
}
if (last_was_single && scene.lines().size() == 1) {
file_contents += fmt::format("(\"{}\"", scene.name());
} else {
file_contents += fmt::format("\n(\"{}\"", scene.name());
}
if (scene.kind() == SubtitleSceneKind::Hint) {
file_contents += " :hint #x0";
} else if (scene.kind() == SubtitleSceneKind::HintNamed) {
file_contents += fmt::format(" :hint #x{0:x}", scene.id());
}
// more compact formatting for single-line entries
if (scene.lines().size() == 1) {
const auto& line = scene.lines().at(0);
if (line.line.empty()) {
file_contents += fmt::format(" ({})", line.frame);
} else {
file_contents += fmt::format(" ({}", line.frame);
if (line.offscreen && scene.kind() == SubtitleSceneKind::Movie) {
file_contents += " :offscreen";
}
file_contents +=
fmt::format(" \"{}\"", font->convert_game_to_utf8(line.speaker.c_str()));
file_contents += fmt::format(" \"{}\")", font->convert_game_to_utf8(line.line.c_str()));
}
file_contents += ")\n";
last_was_single = true;
} else {
file_contents += "\n";
for (auto& line : scene.lines()) {
// Clear screen entries
if (line.line.empty()) {
file_contents += fmt::format(" ({})\n", line.frame);
} else {
file_contents += fmt::format(" ({}", line.frame);
if (line.offscreen && scene.kind() == SubtitleSceneKind::Movie) {
file_contents += " :offscreen";
}
file_contents +=
fmt::format(" \"{}\"", font->convert_game_to_utf8(line.speaker.c_str()));
file_contents +=
fmt::format(" \"{}\")\n", font->convert_game_to_utf8(line.line.c_str()));
}
}
file_contents += " )\n";
last_was_single = false;
}
auto line_utf8 = font->convert_game_to_utf8(scene_line.line.c_str());
file.hints[scene_name].push_back(line_utf8);
}
}
// Commit it to the file
std::string full_path = (file_util::get_jak_project_dir() / fs::path(bank->file_path)).string();
file_util::write_text_file(full_path, file_contents);
}
// Write the subtitle group info out
nlohmann::json json(db.m_subtitle_groups->m_groups);
json[db.m_subtitle_groups->group_order_key] = nlohmann::json(db.m_subtitle_groups->m_group_order);
std::string file_path = (file_util::get_jak_project_dir() / "game" / "assets" / "jak1" /
"subtitle" / "subtitle-groups.json")
.string();
file_util::write_text_file(file_path, json.dump(2));
// Cutscenes
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
if (scene_info.m_kind == SubtitleSceneKind::Movie) {
file.cutscenes[scene_name] = {};
for (const auto& scene_line : scene_info.m_lines) {
if (scene_line.line.empty()) {
continue;
}
auto line_utf8 = font->convert_game_to_utf8(scene_line.line.c_str());
file.cutscenes[scene_name].push_back(line_utf8);
}
}
}
return file;
}
const std::vector<std::string> locale_lookup = {
"en-US", "fr-FR", "de-DE", "es-ES", "it-IT", "jp-JP", "en-GB", "pt-PT", "fi-FI",
"sv-SE", "da-DK", "no-NO", "nl-NL", "pt-BR", "hu-HU", "ca-ES", "is-IS"};
bool write_subtitle_db_to_files(const GameSubtitleDB& db, const GameVersion game_version) {
try {
for (const auto& [language_id, bank] : db.m_banks) {
auto meta_file = dump_bank_as_meta_json(bank);
std::string dump_path = (file_util::get_jak_project_dir() / "game" / "assets" /
version_to_game_name(game_version) / "subtitle" /
fmt::format("subtitle_meta_{}.json", locale_lookup.at(language_id)))
.string();
json data = meta_file;
file_util::write_text_file(dump_path, data.dump(2));
// Now dump the actual subtitles
auto subtitle_file = dump_bank_as_json(bank);
dump_path = (file_util::get_jak_project_dir() / "game" / "assets" /
version_to_game_name(game_version) / "subtitle" /
fmt::format("subtitle_lines_{}.json", locale_lookup.at(language_id)))
.string();
data = subtitle_file;
file_util::write_text_file(dump_path, data.dump(2));
}
// Write the subtitle group info out
nlohmann::json json(db.m_subtitle_groups->m_groups);
json[db.m_subtitle_groups->group_order_key] =
nlohmann::json(db.m_subtitle_groups->m_group_order);
std::string file_path =
(file_util::get_jak_project_dir() / "game" / "assets" / version_to_game_name(game_version) /
"subtitle" / "subtitle-groups.json")
.string();
file_util::write_text_file(file_path, json.dump(2));
} catch (std::exception& ex) {
lg::error(ex.what());
return false;
}
return true;
}

View file

@ -2,4 +2,4 @@
#include "common/serialization/subtitles/subtitles_ser.h"
bool write_subtitle_db_to_files(const GameSubtitleDB& db);
bool write_subtitle_db_to_files(const GameSubtitleDB& db, const GameVersion game_version);

View file

@ -289,10 +289,8 @@ void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::str
if (!db.bank_exists(lang)) {
// database has no lang yet
banks[lang] = db.add_bank(std::make_shared<GameSubtitleBank>(lang));
banks[lang]->file_path = file_path;
} else {
banks[lang] = db.bank_by_id(lang);
banks[lang]->file_path = file_path;
}
});
} else if (head.is_symbol("text-version")) {
@ -304,7 +302,6 @@ void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::str
if (!ver_name.is_symbol()) {
throw std::runtime_error("invalid text version entry");
}
font = get_font_bank(ver_name.as_symbol()->name);
}
@ -339,8 +336,6 @@ void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::str
id = head.as_int();
}
scene.set_id(id);
scene.m_sorting_group = db.m_subtitle_groups->find_group(scene.name());
scene.m_sorting_group_idx = db.m_subtitle_groups->find_group_index(scene.m_sorting_group);
for_each_in_list(entries, [&](const goos::Object& entry) {
if (entry.is_pair()) {
@ -412,6 +407,172 @@ void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::str
}
}
void parse_subtitle_json(GameSubtitleDB& db, const GameSubtitleDefinitionFile& file_info) {
// TODO - some validation
// Init Settings
std::shared_ptr<GameSubtitleBank> bank;
if (!db.bank_exists(file_info.language_id)) {
// database has no lang yet
bank = db.add_bank(std::make_shared<GameSubtitleBank>(file_info.language_id));
} else {
bank = db.bank_by_id(file_info.language_id);
}
bank->m_text_version = file_info.text_version;
bank->m_file_path = file_info.lines_path;
const GameTextFontBank* font = get_font_bank(file_info.text_version);
// Parse the file
SubtitleMetadataFile meta_file;
SubtitleFile lines_file;
try {
// If we have a base file defined, load that and merge it
if (file_info.meta_base_path) {
auto base_data =
parse_commented_json(file_util::read_text_file(file_util::get_jak_project_dir() /
file_info.meta_base_path.value()),
"subtitle_meta_base_path");
auto data = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.meta_path),
"subtitle_meta_path");
base_data.at("cutscenes").update(data.at("cutscenes"));
base_data.at("hints").update(data.at("hints"));
meta_file = base_data;
} else {
meta_file = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.meta_path),
"subtitle_meta_path");
}
if (file_info.lines_base_path) {
auto base_data =
parse_commented_json(file_util::read_text_file(file_util::get_jak_project_dir() /
file_info.lines_base_path.value()),
"subtitle_line_base_path");
auto data = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.lines_path),
"subtitle_line_path");
base_data.at("cutscenes").update(data.at("cutscenes"));
base_data.at("hints").update(data.at("hints"));
base_data.at("speakers").update(data.at("speakers"));
auto test = base_data.dump();
lines_file = base_data;
} else {
lines_file = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.lines_path),
"subtitle_line_path");
}
} catch (std::exception& e) {
lg::error("Unable to parse subtitle json entry, couldn't successfully load files - {}",
e.what());
throw;
}
// Iterate through the metadata file as blank lines are no omitted from the lines file now
// Cutscenes First
for (const auto& [cutscene_name, cutscene_lines] : meta_file.cutscenes) {
GameSubtitleSceneInfo scene(SubtitleSceneKind::Movie);
scene.set_name(cutscene_name);
scene.m_sorting_group = db.m_subtitle_groups->find_group(cutscene_name);
scene.m_sorting_group_idx = db.m_subtitle_groups->find_group_index(scene.m_sorting_group);
// Iterate the lines, grab the actual text from the lines file if it's not a clear screen entry
int line_idx = 0;
int lines_added = 0;
for (const auto& line : cutscene_lines) {
if (line.clear) {
scene.add_clear_entry(line.frame);
lines_added++;
} else {
if (lines_file.speakers.find(line.speaker) == lines_file.speakers.end() ||
lines_file.cutscenes.find(cutscene_name) == lines_file.cutscenes.end() ||
lines_file.cutscenes.at(cutscene_name).size() < line_idx) {
lg::warn(
"{} Couldn't find {} in line file, or line list is too small, or speaker could not "
"be resolved {}!",
file_info.language_id, cutscene_name, line.speaker);
} else {
// NOTE - the convert_utf8_to_game function is really really slow (about 80-90% of the
// time loading the subtitle files)
// TODO - improve that as a follow up sometime in the future
scene.add_line(
line.frame,
font->convert_utf8_to_game(lines_file.cutscenes.at(cutscene_name).at(line_idx)),
font->convert_utf8_to_game(lines_file.speakers.at(line.speaker)), line.offscreen);
lines_added++;
}
line_idx++;
}
}
// Verify we added the amount of lines we expected to
if (lines_added != cutscene_lines.size()) {
throw std::runtime_error(
fmt::format("Cutscene: '{}' has a mismatch in metadata lines vs text lines. Expected {} "
"only added {} lines",
cutscene_name, cutscene_lines.size(), lines_added));
}
// TODO - add scene, can't we just use an emplace here?
if (!bank->scene_exists(scene.name())) {
bank->add_scene(scene);
} else {
auto& old_scene = bank->scene_by_name(scene.name());
old_scene.from_other_scene(scene);
}
}
// Now hints
for (const auto& [hint_name, hint_info] : meta_file.hints) {
GameSubtitleSceneInfo scene(SubtitleSceneKind::Hint);
scene.set_name(hint_name);
/*scene.m_sorting_group = db.m_subtitle_groups->find_group(hint_name);
scene.m_sorting_group_idx = db.m_subtitle_groups->find_group_index(scene.m_sorting_group);*/
if (hint_info.id == "0") {
scene.m_kind = SubtitleSceneKind::HintNamed;
} else {
scene.set_id(std::stoi(hint_info.id, nullptr, 16));
}
// Iterate the lines, grab the actual text from the lines file if it's not a clear screen entry
int line_idx = 0;
int lines_added = 0;
for (const auto& line : hint_info.lines) {
if (line.clear) {
scene.add_clear_entry(line.frame);
lines_added++;
} else {
if (lines_file.speakers.find(line.speaker) == lines_file.speakers.end() ||
lines_file.hints.find(hint_name) == lines_file.hints.end() ||
lines_file.hints.at(hint_name).size() < line_idx) {
lg::warn(
"{} Couldn't find {} in line file, or line list is too small, or speaker could not "
"be resolved {}!",
file_info.language_id, hint_name, line.speaker);
} else {
// NOTE - the convert_utf8_to_game function is really really slow (about 80-90% of the
// time loading the subtitle files)
// TODO - improve that as a follow up sometime in the future
scene.add_line(line.frame,
font->convert_utf8_to_game(lines_file.hints.at(hint_name).at(line_idx)),
font->convert_utf8_to_game(lines_file.speakers.at(line.speaker)), true);
lines_added++;
}
line_idx++;
}
}
// Verify we added the amount of lines we expected to
if (lines_added != hint_info.lines.size()) {
throw std::runtime_error(
fmt::format("Hint: '{}' has a mismatch in metadata lines vs text lines. Expected {} "
"only added {} lines",
hint_name, hint_info.lines.size(), lines_added));
}
// TODO - add scene, can't we just use an emplace here?
if (!bank->scene_exists(scene.name())) {
bank->add_scene(scene);
} else {
auto& old_scene = bank->scene_by_name(scene.name());
old_scene.from_other_scene(scene);
}
}
}
GameTextVersion parse_text_only_version(const std::string& filename) {
goos::Reader reader;
return parse_text_only_version(reader.read_from_file({filename}));
@ -555,6 +716,244 @@ void open_text_project(const std::string& kind,
});
}
void open_subtitle_project(const std::string& kind,
const std::string& filename,
std::vector<GameSubtitleDefinitionFile>& subtitle_files) {
goos::Reader reader;
auto& proj = reader.read_from_file({filename}).as_pair()->cdr.as_pair()->car;
if (!proj.is_pair() || !proj.as_pair()->car.is_symbol() ||
proj.as_pair()->car.as_symbol()->name != kind) {
throw std::runtime_error(fmt::format("invalid {} project", kind));
}
goos::for_each_in_list(proj.as_pair()->cdr, [&](const goos::Object& o) {
if (o.is_pair() && o.as_pair()->cdr.is_pair()) {
auto args = o.as_pair();
auto& action = args->car.as_symbol()->name;
args = args->cdr.as_pair();
if (action == "file") {
auto& file_path = args->car.as_string()->data;
auto new_file = GameSubtitleDefinitionFile();
new_file.format = GameSubtitleDefinitionFile::Format::GOAL;
new_file.lines_path = file_path;
subtitle_files.push_back(new_file);
} else if (action == "file-json") {
auto new_file = GameSubtitleDefinitionFile();
new_file.format = GameSubtitleDefinitionFile::Format::JSON;
while (true) {
const auto& kwarg = args->car.as_symbol()->name;
args = args->cdr.as_pair();
if (kwarg == ":language-id") {
new_file.language_id = args->car.as_int();
} else if (kwarg == ":text-version") {
new_file.text_version = args->car.as_string()->data;
} else if (kwarg == ":lines") {
new_file.lines_path = args->car.as_string()->data;
} else if (kwarg == ":meta") {
new_file.meta_path = args->car.as_string()->data;
} else if (kwarg == ":lines-base") {
new_file.lines_base_path = args->car.as_string()->data;
} else if (kwarg == ":meta-base") {
new_file.meta_base_path = args->car.as_string()->data;
}
if (args->cdr.is_empty_list()) {
break;
}
args = args->cdr.as_pair();
}
subtitle_files.push_back(new_file);
} else {
throw std::runtime_error(fmt::format("unknown action {} in {} project", action, kind));
}
} else {
throw std::runtime_error(fmt::format("invalid entry in {} project", kind));
}
});
}
void to_json(json& j, const SubtitleCutsceneLineMetadata& obj) {
j = json{{"frame", obj.frame},
{"offscreen", obj.offscreen},
{"speaker", obj.speaker},
{"clear", obj.clear}};
}
void from_json(const json& j, SubtitleCutsceneLineMetadata& obj) {
json_deserialize_if_exists(frame);
json_deserialize_if_exists(offscreen);
json_deserialize_if_exists(speaker);
json_deserialize_if_exists(clear);
}
void to_json(json& j, const SubtitleHintLineMetadata& obj) {
j = json{{"frame", obj.frame}, {"speaker", obj.speaker}, {"clear", obj.clear}};
}
void from_json(const json& j, SubtitleHintLineMetadata& obj) {
json_deserialize_if_exists(frame);
json_deserialize_if_exists(speaker);
json_deserialize_if_exists(clear);
}
void to_json(json& j, const SubtitleHintMetadata& obj) {
j = json{{"id", obj.id}, {"lines", obj.lines}};
}
void from_json(const json& j, SubtitleHintMetadata& obj) {
json_deserialize_if_exists(id);
json_deserialize_if_exists(lines);
}
void to_json(json& j, const SubtitleMetadataFile& obj) {
j = json{{"cutscenes", obj.cutscenes}, {"hints", obj.hints}};
}
void from_json(const json& j, SubtitleMetadataFile& obj) {
json_deserialize_if_exists(cutscenes);
json_deserialize_if_exists(hints);
}
void to_json(json& j, const SubtitleFile& obj) {
j = json{{"speakers", obj.speakers}, {"cutscenes", obj.cutscenes}, {"hints", obj.hints}};
}
void from_json(const json& j, SubtitleFile& obj) {
json_deserialize_if_exists(speakers);
json_deserialize_if_exists(cutscenes);
json_deserialize_if_exists(hints);
}
// TODO - temporary code for migration
SubtitleMetadataFile dump_bank_as_meta_json(
std::shared_ptr<GameSubtitleBank> bank,
std::unordered_map<std::string, std::string> speaker_lookup) {
auto meta_file = SubtitleMetadataFile();
auto font = get_font_bank("jak1-v2");
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
if (scene_info.m_kind == SubtitleSceneKind::Movie) {
std::vector<SubtitleCutsceneLineMetadata> lines;
for (const auto& line : scene_info.m_lines) {
auto line_meta = SubtitleCutsceneLineMetadata();
line_meta.frame = line.frame;
if (line.line.empty()) {
line_meta.clear = true;
} else {
auto line_speaker = font->convert_game_to_utf8(line.speaker.c_str());
for (const auto& [speaker, speaker_localized] : speaker_lookup) {
if (line_speaker == speaker_localized) {
line_speaker = speaker;
}
}
line_meta.offscreen = line.offscreen;
line_meta.speaker = line_speaker;
}
lines.push_back(line_meta);
}
meta_file.cutscenes[scene_name] = lines;
} else if (scene_info.m_kind == SubtitleSceneKind::Hint ||
scene_info.m_kind == SubtitleSceneKind::HintNamed) {
SubtitleHintMetadata hint;
hint.id = fmt::format("{:x}", scene_info.m_id);
std::vector<SubtitleHintLineMetadata> lines;
for (const auto& line : scene_info.m_lines) {
auto line_meta = SubtitleHintLineMetadata();
line_meta.frame = line.frame;
if (line.line.empty()) {
line_meta.clear = true;
} else {
auto line_speaker = font->convert_game_to_utf8(line.speaker.c_str());
for (const auto& [speaker, speaker_localized] : speaker_lookup) {
if (line_speaker == speaker_localized) {
line_speaker = speaker;
}
}
line_meta.speaker = line_speaker;
}
lines.push_back(line_meta);
}
hint.lines = lines;
meta_file.hints[scene_name] = hint;
}
}
return meta_file;
}
// TODO - temporary code for migration
SubtitleFile dump_bank_as_json(std::shared_ptr<GameSubtitleBank> bank,
std::shared_ptr<GameSubtitleBank> base_bank,
std::unordered_map<std::string, std::string> speaker_lookup) {
SubtitleFile file;
file.speakers = speaker_lookup;
auto font = get_font_bank("jak1-v2");
// Figure out speakers
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
for (const auto& line : scene_info.m_lines) {
if (line.line.empty()) {
continue;
}
auto line_speaker = font->convert_game_to_utf8(line.speaker.c_str());
bool new_speaker = true;
for (const auto& [speaker, speaker_localized] : file.speakers) {
if (line_speaker == speaker_localized) {
new_speaker = false;
break;
}
}
if (new_speaker) {
// if the speaker is in the english speaker map, append it
if (speaker_lookup.find(line_speaker) != speaker_lookup.end()) {
file.speakers[line_speaker] = line_speaker;
} else {
// otherwise, go figure it out manually, most names are the same so this isn't worth
// writing code for
file.speakers[fmt::format("unknown-{}", scene_info.m_name)] = line_speaker;
}
}
}
}
// Hints
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
if (scene_info.m_kind == SubtitleSceneKind::Hint ||
scene_info.m_kind == SubtitleSceneKind::HintNamed) {
// Check if the number of hints in the translated language match that of the base language
if (base_bank->m_scenes.find(scene_name) == base_bank->m_scenes.end()) {
lg::warn("scene not found in base language - {}:{}", bank->m_lang_id, scene_name);
} else {
if (scene_info.m_lines.size() > base_bank->m_scenes.at(scene_name).m_lines.size()) {
lg::info("hint - translation has more lines than base - {}:{}", bank->m_lang_id,
scene_name);
}
file.hints[scene_name] = {};
for (const auto& scene_line : scene_info.m_lines) {
if (scene_line.line.empty()) {
continue;
}
auto line_utf8 = font->convert_game_to_utf8(scene_line.line.c_str());
file.hints[scene_name].push_back(line_utf8);
}
}
}
}
// Cutscenes
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
if (scene_info.m_kind == SubtitleSceneKind::Movie) {
// Check if the number of hints in the translated language match that of the base language
if (base_bank->m_scenes.find(scene_name) == base_bank->m_scenes.end()) {
lg::warn("scene not found in base language - {}:{}", bank->m_lang_id, scene_name);
} else {
if (scene_info.m_lines.size() > base_bank->m_scenes.at(scene_name).m_lines.size()) {
lg::info("cutscene - translation has more lines than base - {}:{}", bank->m_lang_id,
scene_name);
}
file.cutscenes[scene_name] = {};
for (const auto& scene_line : scene_info.m_lines) {
if (scene_line.line.empty()) {
continue;
}
auto line_utf8 = font->convert_game_to_utf8(scene_line.line.c_str());
file.cutscenes[scene_name].push_back(line_utf8);
}
}
}
}
return file;
}
GameSubtitleDB load_subtitle_project(GameVersion game_version) {
// Load the subtitle files
GameSubtitleDB db;
@ -562,22 +961,70 @@ GameSubtitleDB load_subtitle_project(GameVersion game_version) {
db.m_subtitle_groups->hydrate_from_asset_file();
try {
goos::Reader reader;
std::vector<GameTextDefinitionFile> files;
std::vector<GameSubtitleDefinitionFile> files;
std::string subtitle_project = (file_util::get_jak_project_dir() / "game" / "assets" /
version_to_game_name(game_version) / "game_subtitle.gp")
.string();
open_text_project("subtitle", subtitle_project, files);
open_subtitle_project("subtitle", subtitle_project, files);
for (auto& file : files) {
if (file.format != GameTextDefinitionFile::Format::GOAL) {
continue; // non-GOAL formats are not supported for subtitles
if (file.format == GameSubtitleDefinitionFile::Format::GOAL) {
auto code = reader.read_from_file({file.lines_path});
parse_subtitle(code, db, file.lines_path);
} else if (file.format == GameSubtitleDefinitionFile::Format::JSON) {
parse_subtitle_json(db, file);
}
auto code = reader.read_from_file({file.file_path});
parse_subtitle(code, db, file.file_path);
}
} catch (std::runtime_error& e) {
lg::error("error loading subtitle project: {}", e.what());
}
// Dump new JSON format (uncomment if you need it)
// TODO -- TEMPORARY CODE FOR MIGRATION -- REMOVE LATER
// auto speaker_json = parse_commented_json(
// file_util::read_text_file((file_util::get_jak_project_dir() / "game" / "assets" /
// version_to_game_name(game_version) / "subtitle" /
// "_speaker_lookup.jsonc")),
// "_speaker_lookup.jsonc");
// auto speaker_lookup =
// speaker_json
// .get<std::unordered_map<std::string, std::unordered_map<std::string, std::string>>>();
// std::vector<std::string> locale_lookup = {"en-US", "fr-FR", "de-DE", "es-ES", "it-IT",
// "jp-JP", "en-GB", "pt-PT", "fi-FI", "sv-SE",
// "da-DK", "no-NO", "nl-NL", "pt-BR", "hu-HU", "ca-ES",
// "is-IS"};
// for (const auto& [language_id, bank] : db.m_banks) {
// auto meta_file =
// dump_bank_as_meta_json(bank, speaker_lookup.at(fmt::format("{}", language_id)));
// std::string dump_path =
// (file_util::get_jak_project_dir() / "game" / "assets" /
// version_to_game_name(game_version) /
// "subtitle" / fmt::format("subtitle_meta_{}.json", locale_lookup.at(language_id)))
// .string();
// json data = meta_file;
// try {
// std::string str = data.dump(2);
// file_util::write_text_file(dump_path, str);
// } catch (std::exception& ex) {
// lg::error(ex.what());
// }
// // Now dump the actual subtitles
// auto subtitle_file = dump_bank_as_json(bank, db.m_banks.at(0),
// speaker_lookup.at(fmt::format("{}", language_id)));
// dump_path =
// (file_util::get_jak_project_dir() / "game" / "assets" /
// version_to_game_name(game_version) /
// "subtitle" / fmt::format("subtitle_lines_{}.json", locale_lookup.at(language_id)))
// .string();
// data = subtitle_file;
// try {
// std::string str = data.dump(2);
// file_util::write_text_file(dump_path, str);
// } catch (std::exception& ex) {
// lg::error(ex.what());
// }
// }
return db;
}
// TODO - write a deserializer, the compiler still can do the compiling!

View file

@ -15,15 +15,69 @@
#include "common/util/json_util.h"
#include "common/versions/versions.h"
struct SubtitleCutsceneLineMetadata {
// Always required
int frame;
// Actual lines
bool offscreen;
std::string speaker;
// Clear entries
bool clear;
};
void to_json(json& j, const SubtitleCutsceneLineMetadata& obj);
void from_json(const json& j, SubtitleCutsceneLineMetadata& obj);
struct SubtitleHintLineMetadata {
int frame;
std::string speaker;
// Clear entries
bool clear;
};
void to_json(json& j, const SubtitleHintLineMetadata& obj);
void from_json(const json& j, SubtitleHintLineMetadata& obj);
struct SubtitleHintMetadata {
std::string id; // hex
std::vector<SubtitleHintLineMetadata> lines;
};
void to_json(json& j, const SubtitleHintMetadata& obj);
void from_json(const json& j, SubtitleHintMetadata& obj);
struct SubtitleMetadataFile {
std::unordered_map<std::string, std::vector<SubtitleCutsceneLineMetadata>> cutscenes;
std::unordered_map<std::string, SubtitleHintMetadata> hints;
};
void to_json(json& j, const SubtitleMetadataFile& obj);
void from_json(const json& j, SubtitleMetadataFile& obj);
struct SubtitleFile {
std::unordered_map<std::string, std::string> speakers;
std::unordered_map<std::string, std::vector<std::string>> cutscenes;
std::unordered_map<std::string, std::vector<std::string>> hints;
};
void to_json(json& j, const SubtitleFile& obj);
void from_json(const json& j, SubtitleFile& obj);
struct GameTextDefinitionFile {
enum class Format { GOAL, JSON };
Format format;
std::string file_path = "";
int language_id = -1;
std::string text_version = "";
std::string text_version = "jak1-v2";
std::optional<std::string> group_name = std::nullopt;
};
struct GameSubtitleDefinitionFile {
enum class Format { GOAL, JSON };
Format format;
int language_id = -1;
std::string text_version = "jak1-v2";
std::string lines_path = "";
std::optional<std::string> lines_base_path = std::nullopt;
std::string meta_path = "";
std::optional<std::string> meta_base_path = std::nullopt;
};
/*!
* The text bank contains all lines (accessed with an ID) for a language.
*/
@ -117,6 +171,13 @@ class GameSubtitleSceneInfo {
void add_line(int frame, std::string line, std::string speaker, bool offscreen) {
m_lines.emplace_back(SubtitleLine(frame, line, speaker, offscreen));
// TODO - sorting after every insertion is slow, sort on the add scene instead
std::sort(m_lines.begin(), m_lines.end());
}
void add_clear_entry(int frame) {
m_lines.emplace_back(SubtitleLine(frame, "", "", false));
// TODO - sorting after every insertion is slow, sort on the add scene instead
std::sort(m_lines.begin(), m_lines.end());
}
@ -146,8 +207,8 @@ class GameSubtitleBank {
}
int m_lang_id;
std::string file_path;
std::string m_text_version;
std::string m_file_path;
std::map<std::string, GameSubtitleSceneInfo> m_scenes;
};
@ -199,6 +260,7 @@ void parse_text_json(const nlohmann::json& json,
GameTextDB& db,
const GameTextDefinitionFile& file_info);
void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::string& file_path);
void parse_subtitle_json(GameSubtitleDB& db, const GameSubtitleDefinitionFile& file_info);
GameTextVersion parse_text_only_version(const std::string& filename);
GameTextVersion parse_text_only_version(const goos::Object& data);
@ -206,4 +268,7 @@ GameTextVersion parse_text_only_version(const goos::Object& data);
void open_text_project(const std::string& kind,
const std::string& filename,
std::vector<GameTextDefinitionFile>& inputs);
void open_subtitle_project(const std::string& kind,
const std::string& filename,
std::vector<GameSubtitleDefinitionFile>& inputs);
GameSubtitleDB load_subtitle_project(GameVersion game_version);

View file

@ -6,6 +6,13 @@
#include "third-party/json.hpp"
using json = nlohmann::json;
std::string strip_cpp_style_comments(const std::string& input);
nlohmann::json parse_commented_json(const std::string& input, const std::string& source_name);
Range<int> parse_json_optional_integer_range(const nlohmann::json& json);
#define json_deserialize_if_exists(field_name) \
if (j.contains(#field_name)) { \
j.at(#field_name).get_to(obj.field_name); \
}

View file

@ -1,3 +1,12 @@
files:
- source: /game/assets/jak1/text/game_custom_text_en-US.json
translation: /game/assets/jak1/text/game_custom_text_%locale%.json
- source: /game/assets/jak1/subtitle/subtitle_lines_en-US.json
translation: /game/assets/jak1/subtitle/subtitle_lines_%locale%.json
excluded_target_languages:
- "fr"
- "de"
- "es-ES"
- "it"
- "ja"
- "en-GB"

View file

@ -1,17 +1,123 @@
;; "project file" for subtitles make tool.
;; it's very simple... a list of (action args...)
;; There is one supported action:
;; - file (A path to a GOAL data file)
;; - the same arguments are provided within the file itself
(subtitle
(file "game/assets/jak1/subtitle/game_subtitle_en.gd")
(file "game/assets/jak1/subtitle/game_subtitle_fr.gd")
(file "game/assets/jak1/subtitle/game_subtitle_en-uk.gd")
(file "game/assets/jak1/subtitle/game_subtitle_de.gd")
(file "game/assets/jak1/subtitle/game_subtitle_es.gd")
(file "game/assets/jak1/subtitle/game_subtitle_ptbr.gd")
(file "game/assets/jak1/subtitle/game_subtitle_it.gd")
(file-json
:language-id 0
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 1
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_fr-FR.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_fr-FR.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 2
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_de-DE.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_de-DE.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 3
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_es-ES.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_es-ES.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 4
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_it-IT.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_it-IT.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 5
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_jp-JP.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_jp-JP.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 6
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_en-GB.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_en-GB.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 7
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_pt-PT.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_pt-PT.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 8
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_fi-FI.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_fi-FI.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 9
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_sv-SE.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_sv-SE.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 10
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_da-DK.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_da-DK.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 11
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_no-NO.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_no-NO.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 12
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_nl-NL.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_nl-NL.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 13
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_pt-BR.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_pt-BR.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 14
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_hu-HU.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_hu-HU.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 15
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_ca-ES.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_ca-ES.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 16
:text-version "jak1-v2"
:lines "game/assets/jak1/subtitle/subtitle_lines_is-IS.json"
:lines-base "game/assets/jak1/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak1/subtitle/subtitle_meta_is-IS.json"
:meta-base "game/assets/jak1/subtitle/subtitle_meta_en-US.json")
)

View file

@ -0,0 +1,205 @@
// This file is just here to aid in the translating of the old format to the new
// once we are done needing that, this can be deleted
{
"0": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "BIRDWATCHER",
"BLUE SAGE": "BLUE SAGE",
"DAXTER": "DAXTER",
"FARMER": "FARMER",
"FISHERMAN": "FISHERMAN",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "GAMBLER",
"GEOLOGIST": "GEOLOGIST",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "JAK'S UNCLE",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "MAYOR",
"OLD MAN": "OLD MAN",
"ORACLE": "ORACLE",
"RED SAGE": "RED SAGE",
"SAMOS": "SAMOS",
"SCULPTOR": "SCULPTOR",
"WARRIOR": "WARRIOR",
"WILLARD": "WILLARD",
"WOMAN": "WOMAN",
"YELLOW SAGE": "YELLOW SAGE",
"MINER": "MINER",
"JAK": "JAK"
},
"1": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "ORNITHOLOGUE",
"BLUE SAGE": "SAGE BLEU",
"DAXTER": "DAXTER",
"FARMER": "FERMIER",
"FISHERMAN": "PÊCHEUR",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "PARIEUR",
"GEOLOGIST": "GÉOLOGUE",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "ONCLE DE JAK",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "MAIRE",
"OLD MAN": "VIEIL HOMME",
"ORACLE": "ORACLE",
"RED SAGE": "SAGE ROUGE",
"SAMOS": "SAMOS",
"SCULPTOR": "SCULPTEUR",
"WARRIOR": "GUERRIER",
"WILLARD": "WILLARD",
"WOMAN": "FEMME",
"YELLOW SAGE": "SAGE JAUNE",
"MINER": "MINER"
},
"2": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "VOGEL-BEOBACHTERIN",
"BLUE SAGE": "BLAUER WEISE",
"DAXTER": "DAXTER",
"FARMER": "FARMER",
"FISHERMAN": "FISCHER",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "GLÜCKSSPIELER",
"GEOLOGIST": "GEOLOGIN",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "JAKS ONKEL",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "BÜRGERMEISTER",
"OLD MAN": "FRAU",
"ORACLE": "ORAKEL",
"RED SAGE": "ROTER WEISE",
"SAMOS": "SAMOS",
"SCULPTOR": "BILDHAUER",
"WARRIOR": "KRIEGER",
"WILLARD": "WILLARD",
"WOMAN": "ALTER MANN",
"YELLOW SAGE": "GELBER WEISE",
"MINER": "MINER",
"JAK": "JAK"
},
"3": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "MUJER PÁJARO",
"BLUE SAGE": "SABIO AZUL",
"DAXTER": "DAXTER",
"FARMER": "GRANJERO",
"FISHERMAN": "PESCADOR",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "JUGADOR",
"GEOLOGIST": "GEÓLOGA",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "EXPLORADOR",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "ALCALDE",
"OLD MAN": "MUJER",
"ORACLE": "ORÁCULO",
"RED SAGE": "SABIO ROJO",
"SAMOS": "SAMOS",
"SCULPTOR": "ESCULTOR",
"WARRIOR": "GUERRERO",
"WILLARD": "WILLARD",
"WOMAN": "ANCIANO",
"YELLOW SAGE": "SABIO AMARILLO",
"MINER": "MINER",
"JAK": "JAK"
},
"4": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "ORNITOLOGA",
"BLUE SAGE": "SAGGIO BLU",
"DAXTER": "DAXTER",
"FARMER": "CONTADINO",
"FISHERMAN": "PESCATORE",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "GIOCATORE D'AZZARDO",
"GEOLOGIST": "GEOLOGA",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "ZIO",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "SINDACO",
"OLD MAN": "VOCE MASCHILE",
"ORACLE": "ORACOLO",
"RED SAGE": "SAGGIO ROSSO",
"SAMOS": "SAMOS",
"SCULPTOR": "SCULTORE",
"WARRIOR": "SOLDATO",
"WILLARD": "WILLARD",
"WOMAN": "VOCE FEMMINILE",
"YELLOW SAGE": "SAGGIO GIALLO",
"JAK": "JAK",
"MINER": "MINER"
},
"6": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "BIRDWATCHER",
"BLUE SAGE": "BLUE SAGE",
"DAXTER": "DAXTER",
"FARMER": "FARMER",
"FISHERMAN": "FISHERMAN",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "GAMBLER",
"GEOLOGIST": "GEOLOGIST",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "JAK'S UNCLE",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "MAYOR",
"OLD MAN": "OLD MAN",
"ORACLE": "ORACLE",
"RED SAGE": "RED SAGE",
"SAMOS": "SAMOS",
"SCULPTOR": "SCULPTOR",
"WARRIOR": "WARRIOR",
"WILLARD": "WILLARD",
"WOMAN": "WOMAN",
"YELLOW SAGE": "YELLOW SAGE",
"MINER": "MINER",
"JAK": "JAK"
},
"13": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "SRA.PÁSSARO",
"BLUE SAGE": "SÁBIO AZUL",
"DAXTER": "DAXTER",
"FARMER": "FAZENDEIRO",
"FISHERMAN": "PESCADOR",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "APOSTADOR",
"GEOLOGIST": "GEOLOGISTA",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK'S UNCLE": "TIO DO JAK",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "PREFEITO",
"OLD MAN": "VELHO",
"ORACLE": "ORÁCULO",
"RED SAGE": "SÁBIO VERMELHO",
"SAMOS": "SAMOS",
"SCULPTOR": "ESCULTOR",
"WARRIOR": "GUERREIRO",
"WILLARD": "WILLARD",
"WOMAN": "MULHER",
"YELLOW SAGE": "SÁBIO AMARELO",
"MINER": "MINER"
}
}

View file

@ -0,0 +1,200 @@
# Simple script that cleans up and de-duplicates the subtitle files
# Most of the duplication happens in the metadata files, as most cutscenes share the same timing
# for some languages though (ie. en-US and en-GB) the actual translation files are heavily duplicated.
# Also this is a nice place to cleanup anything that C++ did not
# This file can also die once any active (but not commited) translation efforts actually committed
import os
import json
import shutil
def clean_lines(lines):
new_lines = []
for line in lines:
new_lines.append(line.replace('\\"', '"'))
return new_lines
# For the purposes of this script, it's assumed the base files are en-US
english_meta = None
english_lines = None
with open("./subtitle_meta_en-US.json", "r", encoding="utf-8") as f:
english_meta = json.load(f)
with open("./subtitle_lines_en-US.json", "r", encoding="utf-8") as f:
english_lines = json.load(f)
for name, info in english_lines["cutscenes"].items():
english_lines["cutscenes"][name] = clean_lines(info)
for name, info in english_lines["hints"].items():
english_lines["hints"][name] = clean_lines(info)
with open("./subtitle_lines_en-US.json", "w", encoding="utf-8") as line_file:
json.dump(english_lines, line_file, indent=2, ensure_ascii=False)
# I'm lazy, uncomment this to make the other language base files
locales = ["jp-JP", "hu-HU", "da-DK", "fi-FI", "nl-NL", "no-NO", "pt-PT", "sv-SE", "ca-ES", "is-IS"]
for locale in locales:
# duplicate the english files with the locale
shutil.copy("./subtitle_meta_en-US.json", "./subtitle_meta_" + locale + ".json")
# Now, let's iterate through all the other files and remove any near-top level duplication.
# this is a very similar strategy to the cast file cleanup effort.
for f in os.listdir("./"):
if not f.endswith(".json") or f.endswith("en-US.json"):
continue
# Check if it's a meta file, or a line file
if "meta" in f:
new_meta = {
"cutscenes": {},
"hints": {}
}
with open(f, "r", encoding="utf-8") as meta_file:
print(f)
meta = json.load(meta_file)
# Iterate through every thing, if its the same as the base file, it can be removed from this file
# otherwise, leave it!
for name, info in meta["cutscenes"].items():
if name not in english_meta["cutscenes"]:
print(f"{name} not in english_meta['cutscenes']")
new_meta["cutscenes"][name] = info
continue
# easy equality check since order matters and the files are machine generated, this should be good enough
if json.dumps(info) != json.dumps(english_meta["cutscenes"][name]):
new_meta["cutscenes"][name] = info
for name, info in meta["hints"].items():
if name not in english_meta["hints"]:
print(f"{name} not in english_meta['hints']")
new_meta["hints"][name] = info
continue
# easy equality check since order matters and the files are machine generated, this should be good enough
if json.dumps(info) != json.dumps(english_meta["hints"][name]):
new_meta["hints"][name] = info
# write out the new file
with open(f, "w", encoding="utf-8") as meta_file:
json.dump(new_meta, meta_file, indent=2, ensure_ascii=False)
if "lines" in f:
new_lines = {
"cutscenes": {},
"hints": {},
"speakers": {}
}
# now lines files
with open(f, "r", encoding="utf-8") as line_file:
print(f)
lines = json.load(line_file)
new_lines["speakers"] = lines["speakers"]
# Iterate through every thing, if its the same as the base file, it can be removed from this file
# otherwise, leave it!
for name, info in lines["cutscenes"].items():
if name not in english_lines["cutscenes"]:
print(f"{name} not in english_lines['cutscenes']")
new_lines["cutscenes"][name] = clean_lines(info)
continue
# easy equality check since order matters and the files are machine generated, this should be good enough
if json.dumps(info) != json.dumps(english_lines["cutscenes"][name]):
new_lines["cutscenes"][name] = clean_lines(info)
for name, info in lines["hints"].items():
if name not in english_lines["hints"]:
print(f"{name} not in english_lines['hints']")
new_lines["hints"][name] = clean_lines(info)
continue
# easy equality check since order matters and the files are machine generated, this should be good enough
if json.dumps(info) != json.dumps(english_lines["hints"][name]):
new_lines["hints"][name] = clean_lines(info)
# write out the new file
with open(f, "w", encoding="utf-8") as line_file:
json.dump(new_lines, line_file, indent=2, ensure_ascii=False)
# Lines get copied after because we actually don't want duplication to be removed (it needs to be translated!)
for locale in locales:
shutil.copy("./subtitle_lines_en-US.json", "./subtitle_lines_" + locale + ".json")
# Special case for portuguese brazilian
# it was done based off the spanish timings, but there is no portuguese audio
# so manually find the cutscenes that don't match so they can be adjusted...manually...
with open("./subtitle_lines_pt-BR.json", "r", encoding="utf-8") as f:
port_lines = json.load(f)
for cutscene_name, cutscene_lines in port_lines["cutscenes"].items():
if len(cutscene_lines) != len(english_lines["cutscenes"][cutscene_name]):
print(cutscene_name)
for hint_name, hint_lines in port_lines["hints"].items():
if len(hint_lines) != len(english_lines["hints"][hint_name]):
print(hint_name)
# assistant-lavatube-end-resolution
# assistant-reminder-1-generic
# billy-accept
# billy-introduction
# billy-reject
# billy-resolution
# bird-lady-beach-resolution
# bird-lady-introduction
# bird-lady-reminder-2
# bluesage-resolution
# explorer-introduction
# explorer-resolution
# farmer-introduction
# farmer-reminder-1
# fisher-accept
# fisher-introduction
# fisher-reject
# fisher-resolution
# green-sagecage-daxter-sacrifice
# green-sagecage-introduction
# green-sagecage-outro-beat-boss-b
# green-sagecage-outro-preboss
# green-sagecage-resolution
# oracle-intro-1
# oracle-intro-2
# oracle-intro-3
# oracle-reminder-1
# oracle-reminder-2
# oracle-reminder-3
# redsage-resolution
# sage-intro-sequence-a
# sage-intro-sequence-d1
# sage-intro-sequence-d2
# sage-intro-sequence-e
# sage-village3-introduction
# sage-village3-introduction-dark-eco
# yellowsage-resolution
# ASSTLP24
# ASSTLP36
# CHI-AM03
# CHI-AM04
# EXP-AM01
# EXP-AM05
# FAR-AM01
# FIS-AM01
# FIS-AM02
# MSH-AM12
# SAGELP05
# asstv100
# asstv101
# asstva73
# asstvb02
# asstvb04
# asstvb08
# asstvb21
# asstvb23
# asstvb24
# asstvb25
# asstvb45
# asstvb47
# sagevb01
# sagevb02
# sagevb03
# sagevb23
# sagevb24
# sagevb25
# sksp0013
# sksp0017
# sksp0059
# sksp0060
# sksp0067
# sksp0116
# sksp0145
# sksp0b42

File diff suppressed because it is too large Load diff

View file

@ -1,370 +0,0 @@
(language-id 6)
(text-version jak1-v2)
;; -----------------
;; intro
;; -----------------
("sidekick-human-intro-sequence-b"
(547 "OLD MAN" "CONTINUE YOUR SEARCH FOR ARTEFACTS AND ECO.")
(664)
(674 "OLD MAN" "IF THE LOCALS POSSESS PRECURSOR ITEMS, YOU KNOW WHAT TO DO.")
(819)
(839 "WOMAN" "DEAL HARSHLY WITH ANYBODY WHO STRAYS FROM THE VILLAGE.")
(937 "WOMAN" "WE WILL ATTACK IT IN DUE TIME.")
(1027)
)
;; -----------------
;; sidekick
;; -----------------
("sksp009c" :hint #x28f (0 "DAXTER" "DO ME A FAVOUR AND KEEP AWAY FROM THOSE DARK ECO BOXES!"))
;; -----------------
;; oracle
;; -----------------
;; -----------------
;; training
;; -----------------
("asstvb42" :hint #x902
(0 "KEIRA" "THIS IS A POWER CELL, THE MOST IMPORTANT PRECURSOR ARTEFACT YOU CAN FIND!")
(327)
(333 "KEIRA" "YOU NEED TO COLLECT 20 OF THESE SO I CAN POWER THE HEAT SHIELD")
(507 "KEIRA" "FOR YOUR A-GRAV ZOOMER.")
)
("sagevb22" :hint #x909
(0 "SAGE" "THAT'S BLUE ECO, WHICH CONTAINS THE ENERGY OF MOTION.")
(291 "SAGE" "BLUE ECO ALLOWS YOU TO RUN FAST, BREAK BOXES, AND EVEN ACTIVATE SOME PRECURSOR")
(640 "SAGE" "ARTEFACTS WHEN YOU GET NEAR THEM.")
)
("sagevb25" :hint #x90c
(3 "SAGE" "GOOD WORK, THE BLUE ECO CAUSED THE DOOR TO OPEN.")
(250 "SAGE" "WITH BLUE ECO, YOU CAN BREATHE ENERGY INTO ALL KINDS")
(476 "SAGE" "OF PRECURSOR ARTEFACTS THAT HAVE LAIN DORMANT FOR YEARS.")
)
;; -----------------
;; village1
;; -----------------
;; -----------------
;; beach
;; -----------------
("bird-lady-beach-resolution"
(30 "BIRDWATCHER" "OH MY, I HOPE THE POOR DEAR'S OKAY.")
(148)
(157 "BIRDWATCHER" "HERE'S A POWER CELL FOR YOUR VALOUR.")
(237)
(306 "FLUT FLUT" "MAMA!")
(351)
(406 "FLUT FLUT" "MAMA!")
(430 "DAXTER" "OH NO! NO, NO, NO, NO!")
(533)
(535 "BIRDWATCHER" "LOOK... ISN'T THAT CUTE? IT THINKS YOU'RE ITS MAMA.")
(696)
(699 "DAXTER" "EH? I'M NOT YOUR MOM! YOU SEE ANY FEATHERS HERE?")
(803)
(807 "BIRDWATCHER" "OH, LOVE AT FIRST SIGHT! AH...")
(936)
(939 "BIRDWATCHER" "LISTEN, BOYS, I'LL TAKE THIS LITTLE CHICK BACK TO THE VILLAGE WITH ME")
(1054 "BIRDWATCHER" "AND WORK WITH THE SAGE TO TAKE CARE OF HER.")
)
("bird-lady-introduction"
(125 "BIRDWATCHER" "OH MY, WHAT A HORRIBLY SICK LITTLE BIRD.")
(245)
(251 "DAXTER" "HUH! YOU DON'T LOOK SO GOOD YOURSELF, LADY!")
(328)
(331 "BIRDWATCHER" "OH, SORRY. I THOUGHT YOU WERE A SPOTTED")
(408 "BIRDWATCHER" "ORANGE-BELLIED RAIN FRAY.")
(454)
(457 "BIRDWATCHER" "YOU KNOW, YESTERDAY I SAW SOME")
(536 "BIRDWATCHER" "TERRIBLY VICIOUS CREATURES CAPTURE")
(590 "BIRDWATCHER" "A MOTHER FLUT FLUT NEAR THE BEACH.")
(648)
(654 :offscreen "BIRDWATCHER" "NOW THERE'S THIS POOR LITTLE ORPHAN EGG")
(730 :offscreen "BIRDWATCHER" "SITTING IN A NEST AT THE TOP OF THE CLIFF")
(794 :offscreen "BIRDWATCHER" "AND I CAN'T GET TO IT.")
(854 :offscreen "BIRDWATCHER" "IF YOU COULD CLIMB UP THERE AND PUSH IT OFF, I'VE PILED")
(946 :offscreen "BIRDWATCHER" "SOME HAY DOWN AT THE BASE TO CATCH IT SAFELY.")
(1041)
(1044 "BIRDWATCHER" "DO AN OLD LADY A FAVOUR, AND I'LL GIVE YOU A POWER CELL.")
(1198)
)
;; -----------------
;; jungle
;; -----------------
;; -----------------
;; misty
;; -----------------
;; -----------------
;; firecanyon
;; -----------------
;; -----------------
;; village2
;; -----------------
("sage-bluehut-introduction-prec-arm"
(0 "SAGE" "WELL, I HOPE YOU'VE PACKED A LUNCH. 'CAUSE WE'RE JUST GETTING STARTED.")
(130)
(140 "SAGE" "ACCORDING TO THE BLUE SAGE'S NOTES,")
(208 "SAGE" "LURKERS HAVE INFESTED THE SWAMP ACROSS THE BAY.")
(300)
(304 :offscreen "SAGE" "APPARENTLY, THEY'RE PLANNING TO USE A DIRIGIBLE")
(417 :offscreen "SAGE" "TO LIFT AN IMPORTANT PRECURSOR ARTEFACT FROM THE MUCK.")
(531)
(549 :offscreen "SAGE" "YOU'RE GOING TO HAVE TO GET OVER THERE TO DISLODGE THEIR TETHERS.")
(675)
(685 "SAGE" "WHO KNOWS WHAT THEY MIGHT WANT WITH THE ARTEFACT,")
(764 "SAGE" "BUT LIKE ORANGE STUFF HERE'S BREATH, IT JUST CAN'T BE GOOD.")
)
("warrior-introduction"
(24 "WARRIOR" "OHH... MY ACHING HEAD.")
(128)
(137 "DAXTER" "I DOUBT THAT'S ONE OF YOUR VITAL ORGANS!")
(212 "DAXTER" "WALK IT OFF, TOUGH GUY!")
(264 "WARRIOR" "OH, SURE, I WAS TOUGH ONCE.")
(337 "WARRIOR" "MAYBE EVEN THE TOUGHEST OF THEM ALL.")
(402 "WARRIOR" "I SINGLE-HANDEDLY DEFENDED THIS VILLAGE FROM THOSE HORRID CREATURES FOR ALMOST A YEAR!")
(594 :offscreen "WARRIOR" "THEN THAT HORRIBLE MONSTER ARRIVED AND COMMENCED THE BOULDER BOMBARDMENT.")
(760 :offscreen "WARRIOR" "SO, FULL OF VALOUR, ARMOR SHINING IN THE SUN...")
(928 "WARRIOR" "I CLIMBED THE HILL TO TAKE HIM ON...!")
(1033)
(1044 "WARRIOR" "BUT HE POUNDED ME LIKE ONE TENDERIZES A YAKOW STEAK.")
(1172)
(1178 "DAXTER" "HAVE YOU TRIED ATTACKING HIM WITH YOUR MELODRAMA?")
(1255 "DAXTER" "'CAUSE IT'S KILLIN' ME!")
(1303)
(1310 :offscreen "WARRIOR" "AFTER MY LAST STUNNING FAILURE,")
(1395 :offscreen "WARRIOR" "HE SEALED THE PASSAGEWAY TO HIS ROOST WITH A 30-TON BOULDER,")
(1515 :offscreen "WARRIOR" "LEAVING NO WAY FOR ANYONE TO CHALLENGE HIM AGAIN.")
(1640)
(1669 "WARRIOR" "SO, OUR SAGE, A MASTER OF BLUE ECO AND A MECHANICAL GENIUS, DEVISED A MACHINE")
(1905 "WARRIOR" "CAPABLE OF LIFTING THE BOULDER OUT OF THE WAY...!")
(2030)
(2040 "WARRIOR" "BUT ALAS, HE DISAPPEARED BEFORE WE HAD A CHANCE TO TURN IT ON.")
(2200)
(2222 "WARRIOR" "AND HE TOOK ALL OF HIS POWER CELLS WITH HIM.")
(2330)
(2360 :offscreen "WARRIOR" "AT LEAST I WAS ABLE TO PULL ENOUGH PONTOONS OUT OF OUR BRIDGE TO PREVENT")
(2465 :offscreen "WARRIOR" "THAT MONSTER FROM COMING DOWN HERE TO DO ME HARM.")
(2561)
(2566 "DAXTER" "YEAH, GOOD, GOOD JOB, TOUGH GUY. BUT, UM...")
(2659 "DAXTER" "WE'RE GONNA NEED YOU TO, UH... PUT 'EM BACK, AND STUFF.")
(2766)
(2770 "WARRIOR" "OH, SURE! AND SEAL MY DOOM?")
(2880 "WARRIOR" "(SIGHS)")
(2943)
(2949 "WARRIOR" "ALRIGHT. FINE.")
(3036 "WARRIOR" "BRING ME 90 PRECURSOR ORBS AND I'LL LET THE PONTOONS LOOSE.")
(3173)
(3197 "WARRIOR" "BUT I'M NOT GOING TO FIGHT THAT MONSTER AGAIN!")
)
;; -----------------
;; swamp
;; -----------------
("billy-introduction"
(6 "BILLY" "HOWDY, FRIENDS! ENJOYIN' MY BEAUTIFUL SWAMP?")
(123 "BILLY" "I OWN THESE HERE PARTS. EVERYTHING THAT DOESN'T SINK INTO THE MUD, THAT IS! HA HA HA...")
(349)
(369 "DAXTER" "JUDGING BY THE SMELL, I'D WAGER YOUR BATHTUB SANK IN THE MUD LONG AGO.")
(497)
(519 "BILLY" "WHAT'S A BATHTUB? ANYWAY I GOT BIGGER PROBLEMS NOW...")
(680)
(691 "BILLY" "SEEMS SOME NASTY LURKER VARMINTS ARE GROUSIN' ABOUTS,")
(798 "BILLY" "SNATCHIN' EVERYTHING THEY CAN GET THEIR GRUBBY LITTLE PAWS ON.")
(910 "BILLY" "AND SCARIN' AWAY MY PET HIPHOG, FARTHY.")
(1017)
(1021 "BILLY" "HE'S BEEN MISSIN' FOR NIGH ON TO A COON'S AGE.")
(1133)
(1136 :offscreen "BILLY" "I'VE BEEN PUTTIN' OUT HIS FAVOURITE SNACK, BUT THOSE ORNERY SWAMP RATS KEEP STEALIN' EM!")
(1318 :offscreen "BILLY" "IF YOU COULD KEEP THOSE PESKY CRITTERS AWAY LONG ENOUGH,")
(1408 :offscreen "BILLY" "I JUST KNOW FARTHY WOULD SMELL THEM VITTLES AND COME BACK!")
(1519)
(1530 "BILLY" "WILL YA HELP ME OUT?")
)
("sksp0153" :hint #x361
(0 "DAXTER" "HEY, THAT MUST BE THE PRECURSOR ARTEFACT THE LURKERS ARE AFTER!")
(175 "DAXTER" "IT LOOKS LIKE A GIANT ROBOT ARM!")
)
;; -----------------
;; rolling
;; -----------------
;; -----------------
;; sunken
;; -----------------
("sksp0135" :hint #x34f (0 "DAXTER" "THAT WATER LOOKS DANGEROUS WHEN IT CHANGES COLOUR!"))
("sksp0136" :hint #x350 (0 "DAXTER" "YOU GOTTA GET OUT OF THE WATER BEFORE IT CHANGES COLOUR!"))
;; -----------------
;; ogre
;; -----------------
;; -----------------
;; village3
;; -----------------
("minershort-introduction-gnawers"
(5 "GORDY" "WHY DON'T YOU TWO MAKE YOURSELVES USEFUL?")
(89 "GORDY" "LURKERS HAVE BEEN EXCAVATIN' THE DARK CAVES OVER THERE.")
(191 :offscreen "GORDY" "SEEMS THEY'RE LOOKIN' FOR PRECURSOR ARTEFACTS.")
(265)
(271 :offscreen "GORDY" "THEY CAN HAVE THE ARTEFACTS, FOR ALL I CARE.")
(357)
(360 :offscreen "WILLARD" "FOR ALL WE CARE!")
(441)
(444 "GORDY" "WILLARD, FEED YOUR BIRD.")
(490)
(496 "GORDY" "ALL I CARE ABOUT ARE GEMS!")
(566 "GORDY" "BUT I AIN'T GONNA BE ABLE TO GET THE CAVE'S GEMS")
(625 "GORDY" "BECAUSE WHEN THEY'RE THROUGH, THEY'RE GONNA COLLAPSE THE PLACE!")
(715)
(721 "GORDY" "IF YOU TAKE OUT THE LURKERS CHEWIN' AT THE SUPPORT BEAMS,")
(827 "GORDY" "YOU COULD SAVE THE CAVE FOR ME.")
(875)
(884 "GORDY" "NOW BEAT IT!")
)
("sage-village3-introduction"
(6 "SAGE" "OW! I ALWAYS WONDER IF I'M LOSING BODY PARTS IN THOSE THINGS!")
(153)
(183 "SAGE" "HOLY YAKOW! THE RED SAGE'S LAB LOOKS WORSE THAN THE BLUE'S!")
(321)
(321 "KEIRA" "WELL, IT DEFINITELY LOOKS AS THOUGH THERE'S BEEN A STRUGGLE HERE.")
(418)
(441 "OLD MAN" "HA HA HA HA HA!")
(500)
(528 "OLD MAN" "I'D HARDLY CALL IT \"STRUGGLE.\"")
(618 "OLD MAN" "WOULD YOU, DEAR SISTER?")
(672 "WOMAN" "CERTAINLY NOT. THE RED SAGE GAVE UP WITH SO LITTLE EFFORT.")
(800)
(805 "WOMAN" "NO FUN AT ALL.")
(860)
(884 "SAGE" "GOL? IS THAT YOU?")
(965)
(969 "SAGE" "YOU'VE FINALLY GONE OFF THE DEEP END, EH?")
(1060 "SAGE" "AND, MAIA! I TOLD YOU THE DARK ECO WOULD AFFECT YOU BOTH!")
(1180 "SAGE" "HNG, NOBODY EVER LISTENS TO OLD SAMOS...")
(1265)
(1282 "SAGE" "WHAT HAVE YOU TWO DONE WITH THE BLUE AND RED SAGES?")
(1372 "GOL" "DON'T WORRY ABOUT YOUR COLOURFUL FRIENDS, YOU OLD FOOL.")
(1516 "GOL" "THEY'RE PERFECTLY SAFE IN OUR CITADEL. OUR SPECIAL GUESTS.")
(1680 "MAIA" "THEY HAVE GRACIOUSLY AGREED TO HELP US ON A LITTLE PROJECT.")
(1813)
(1828 "GOL" "YOU WERE WRONG, SAMOS. DARK ECO CAN BE CONTROLLED!")
(1990)
(1996 "GOL" "WE'VE LEARNED ITS SECRETS, AND NOW WE CAN RESHAPE THE WORLD TO OUR LIKING.")
(2212)
(2223 "SAGE" "YOU CAN'T CONTROL DARK ECO BY ITSELF! EVEN THE PRECURSORS COULDN'T-")
(2357 "MAIA" "UNTIL NOW, WE'VE HAD TO SCRAPE BY WITH WHAT LITTLE DARK ECO")
(2452 "MAIA" "WE COULD FIND NEAR THE SURFACE.")
(2508)
(2520 "MAIA" "BUT SOON, WE WILL HAVE ACCESS TO THE VAST STORES")
(2611 "MAIA" "OF DARK ECO HIDDEN DEEP UNDERGROUND.")
(2715 "SAGE" "NOT THE SILOS!")
(2758)
(2768 "GOL" "YES, THE SILOS!")
(2841 "GOL" "THEY WILL BE OPENED, AND ALL THE DARK ECO IN THE WORLD WILL BE OURS!")
(3028)
(3034 "SAGE" "BUT THAT'S IMPOSSIBLE! ONLY A PRECURSOR ROBOT-")
(3128 "MAIA" "OH, DON'T LOOK SO UPSET, SAMOS.")
(3210 "MAIA" "WE'VE GOT BIG PLANS FOR YOU.")
(3300)
(3303 :offscreen "MAIA" "AH HA HA HA HA HA HA! AHH...")
(3460)
(3495 "DAXTER" "WAIT A MINUTE!")
(3538)
(3541 "DAXTER" "THAT WAS GOL?")
(3586)
(3595 "DAXTER" "THE SAME GOL WHO'S SUPPOSED TO CHANGE ME BACK?")
(3690 "DAXTER" "GOL IS THE GUY TRYING TO KILL US?!")
(3772)
(3785 "DAXTER" "I'M DOOMED.")
(3822)
(3828 "SAGE" "WE MAY ALL BE DOOMED.")
(3897)
(3903 "SAGE" "IF THEY OPEN THE SILOS, THE DARK ECO WILL")
(3995 "SAGE" "TWIST AND DESTROY EVERYTHING IT TOUCHES!")
(4080 "SAGE" "WE SIMPLY MUST GET TO THEIR CITADEL, TO STOP THEM!")
(4189)
(4192 "KEIRA" "THE FASTEST WAY THERE IS THROUGH THE LAVA TUBE")
(4276 "KEIRA" "AT THE BOTTOM OF THIS CRATER.")
(4316)
(4324 "KEIRA" "A FEW MORE POWER CELLS, AND YOUR ZOOMER'S HEAT SHIELD")
(4398 "KEIRA" "SHOULD GET YOU ACROSS THE LAVA SAFELY.")
(4460)
(4464 "SAGE" "ALL RIGHT, MY BOY. YOU KNOW WHAT TO DO.")
(4545 "SAGE" "TAKE THE FLEABAG AND GO ROUND UP MORE POWER CELLS.")
(4664)
)
("asstv103" :hint #x452
(0 "KEIRA" "DON'T FORGET TO TURN ON THE TELEPORT GATE TO LET US THROUGH.")
(160 "KEIRA" "YOU'VE GOT TO GO INTO THE RED SAGE'S LAB")
(280 "KEIRA" "IN THE CENTRE OF THE VOLCANIC CRATER TO TURN IT ON.")
(450 "KEIRA" "WE CAN'T COME THROUGH UNTIL IT'S BACK ONLINE.")
)
;; -----------------
;; snowy
;; -----------------
;; -----------------
;; spidercave
;; -----------------
;; -----------------
;; lavatube
;; -----------------
;; -----------------
;; citadel
;; -----------------
("green-sagecage-introduction"
(128 "SAGE" "IT'S ABOUT TIME YOU TWO DECIDED TO SHOW UP!")
(207 "DAXTER" "NICE TO SEE YOU TOO!")
(269)
(275 "DAXTER" "DO THEY HAVE YOU MOPPING THE FLOORS NOW?")
(343 "SAGE" "THERE'S NO TIME FOR JOKES, DAXTER. GOL AND MAIA KIDNAPPED US")
(477 "SAGE" "TO SAP OUR ENERGIES TO POWER THEIR ABOMINABLE MACHINE.")
(574)
(583 :offscreen "SAGE" "IT APPEARS THEY HAVE COMBINED THE FUNCTIONAL REMAINS OF A PRECURSOR ROBOT")
(709 :offscreen "SAGE" "WITH SCAVENGED ARTEFACTS FROM ACROSS THE LAND.")
(800)
(816 :offscreen "SAGE" "THEN THEY ADDED A FEW DIABOLICAL ADDITIONS OF THEIR OWN,")
(926 :offscreen "SAGE" "CREATING THE ONE THING CAPABLE OF OPENING THE DARK ECO SILOS.")
(1060 "SAGE" "IF YOU CAN FREE THE FOUR OF US, WE CAN USE OUR COMBINED POWERS")
(1180 "SAGE" "TO BREAK THE FORCE SHIELD SURROUNDING THE ROBOT")
(1265 "SAGE" "BEFORE THEY USE IT TO DESTROY THE WORLD.")
)
;; -----------------
;; finalboss
;; -----------------
;; -----------------
;; title
;; -----------------
;; -----------------
;; uncategorized
;; -----------------

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -295,7 +295,8 @@
"sksp0072",
"sksp0073",
"sksp0435",
"fishermans-boat-ride-to-village1-alt"
"fishermans-boat-ride-to-village1-alt",
"evilbro-misty-end"
],
"ogre": [
"asstvb23",
@ -539,9 +540,6 @@
"sagevb38",
"sagevb39"
],
"uncategorized": [
"evilbro-misty-end"
],
"village1": [
"ASSTLP01",
"ASSTLP02",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,217 @@
{
"cutscenes": {
"bird-lady-beach-resolution": [
"OH MY, I HOPE THE POOR DEAR'S OKAY.",
"HERE'S A POWER CELL FOR YOUR VALOUR.",
"MAMA!",
"MAMA!",
"OH NO! NO, NO, NO, NO!",
"LOOK... ISN'T THAT CUTE? IT THINKS YOU'RE ITS MAMA.",
"EH? I'M NOT YOUR MOM! YOU SEE ANY FEATHERS HERE?",
"OH, LOVE AT FIRST SIGHT! AH...",
"LISTEN, BOYS, I'LL TAKE THIS LITTLE CHICK BACK TO THE VILLAGE WITH ME",
"AND WORK WITH THE SAGE TO TAKE CARE OF HER."
],
"bird-lady-introduction": [
"OH MY, WHAT A HORRIBLY SICK LITTLE BIRD.",
"HUH! YOU DON'T LOOK SO GOOD YOURSELF, LADY!",
"OH, SORRY. I THOUGHT YOU WERE A SPOTTED",
"ORANGE-BELLIED RAIN FRAY.",
"YOU KNOW, YESTERDAY I SAW SOME",
"TERRIBLY VICIOUS CREATURES CAPTURE",
"A MOTHER FLUT-FLUT NEAR THE BEACH.",
"NOW THERE'S THIS POOR LITTLE ORPHAN EGG",
"SITTING IN A NEST AT THE TOP OF THE CLIFF",
"AND I CAN'T GET TO IT.",
"IF YOU COULD CLIMB UP THERE AND PUSH IT OFF, I'VE PILED",
"SOME HAY DOWN AT THE BASE TO CATCH IT SAFELY.",
"DO AN OLD LADY A FAVOUR, AND I'LL GIVE YOU A POWER CELL."
],
"green-sagecage-introduction": [
"IT'S ABOUT TIME YOU TWO DECIDED TO SHOW UP!",
"NICE TO SEE YOU TOO!",
"DO THEY HAVE YOU MOPPING THE FLOORS NOW?",
"THERE'S NO TIME FOR JOKES, DAXTER. GOL AND MAIA KIDNAPPED US",
"TO SAP OUR ENERGIES TO POWER THEIR ABOMINABLE MACHINE.",
"IT APPEARS THEY HAVE COMBINED THE FUNCTIONAL REMAINS OF A PRECURSOR ROBOT",
"WITH SCAVENGED ARTEFACTS FROM ACROSS THE LAND.",
"THEN THEY ADDED A FEW DIABOLICAL ADDITIONS OF THEIR OWN,",
"CREATING THE ONE THING CAPABLE OF OPENING THE DARK ECO SILOS.",
"IF YOU CAN FREE THE FOUR OF US, WE CAN USE OUR COMBINED POWERS",
"TO BREAK THE FORCE SHIELD SURROUNDING THE ROBOT",
"BEFORE THEY USE IT TO DESTROY THE WORLD."
],
"minershort-introduction-gnawers": [
"WHY DON'T YOU TWO MAKE YOURSELVES USEFUL?",
"LURKERS HAVE BEEN EXCAVATIN' THE DARK CAVES OVER THERE.",
"SEEMS THEY'RE LOOKIN' FOR PRECURSOR ARTEFACTS.",
"THEY CAN HAVE THE ARTEFACTS, FOR ALL I CARE.",
"FOR ALL WE CARE!",
"WILLARD, FEED YOUR BIRD.",
"ALL I CARE ABOUT ARE GEMS!",
"BUT I AIN'T GONNA BE ABLE TO GET THE CAVE'S GEMS",
"BECAUSE WHEN THEY'RE THROUGH, THEY'RE GONNA COLLAPSE THE PLACE!",
"IF YOU TAKE OUT THE LURKERS CHEWIN' AT THE SUPPORT BEAMS,",
"YOU COULD SAVE THE CAVE FOR ME.",
"NOW BEAT IT!"
],
"sage-bluehut-introduction-prec-arm": [
"WELL, I HOPE YOU'VE PACKED A LUNCH. 'CAUSE WE'RE JUST GETTING STARTED.",
"ACCORDING TO THE BLUE SAGE'S NOTES,",
"LURKERS HAVE INFESTED THE SWAMP ACROSS THE BAY.",
"APPARENTLY, THEY'RE PLANNING TO USE A DIRIGIBLE",
"TO LIFT AN IMPORTANT PRECURSOR ARTEFACT FROM THE MUCK.",
"YOU'RE GOING TO HAVE TO GET OVER THERE TO DISLODGE THEIR TETHERS.",
"WHO KNOWS WHAT THEY MIGHT WANT WITH THE ARTEFACT,",
"BUT LIKE ORANGE STUFF HERE'S BREATH, IT JUST CAN'T BE GOOD."
],
"sage-village3-introduction": [
"OW! I ALWAYS WONDER IF I'M LOSING BODY PARTS IN THOSE THINGS!",
"HOLY YAKOW! THE RED SAGE'S LAB LOOKS WORSE THAN THE BLUE'S!",
"WELL, IT DEFINITELY LOOKS AS THOUGH THERE'S BEEN A STRUGGLE HERE.",
"HA HA HA HA HA!",
"I'D HARDLY CALL IT \"STRUGGLE.\"",
"WOULD YOU, DEAR SISTER?",
"CERTAINLY NOT. THE RED SAGE GAVE UP WITH SO LITTLE EFFORT.",
"NO FUN AT ALL.",
"GOL? IS THAT YOU?",
"YOU'VE FINALLY GONE OFF THE DEEP END, EH?",
"AND, MAIA! I TOLD YOU THE DARK ECO WOULD AFFECT YOU BOTH!",
"HNG, NOBODY EVER LISTENS TO OLD SAMOS...",
"WHAT HAVE YOU TWO DONE WITH THE BLUE AND RED SAGES?",
"DON'T WORRY ABOUT YOUR COLOURFUL FRIENDS, YOU OLD FOOL.",
"THEY'RE PERFECTLY SAFE IN OUR CITADEL. OUR SPECIAL GUESTS.",
"THEY HAVE GRACIOUSLY AGREED TO HELP US ON A LITTLE PROJECT.",
"YOU WERE WRONG, SAMOS. DARK ECO CAN BE CONTROLLED!",
"WE'VE LEARNED ITS SECRETS, AND NOW WE CAN RESHAPE THE WORLD TO OUR LIKING.",
"YOU CAN'T CONTROL DARK ECO BY ITSELF! EVEN THE PRECURSORS COULDN'T-",
"UNTIL NOW, WE'VE HAD TO SCRAPE BY WITH WHAT LITTLE DARK ECO",
"WE COULD FIND NEAR THE SURFACE.",
"BUT SOON, WE WILL HAVE ACCESS TO THE VAST STORES",
"OF DARK ECO HIDDEN DEEP UNDERGROUND.",
"NOT THE SILOS!",
"YES, THE SILOS!",
"THEY WILL BE OPENED, AND ALL THE DARK ECO IN THE WORLD WILL BE OURS!",
"BUT THAT'S IMPOSSIBLE! ONLY A PRECURSOR ROBOT-",
"OH, DON'T LOOK SO UPSET, SAMOS.",
"WE'VE GOT BIG PLANS FOR YOU.",
"AH HA HA HA HA HA HA! AHH...",
"WAIT A MINUTE!",
"THAT WAS GOL?",
"THE SAME GOL WHO'S SUPPOSED TO CHANGE ME BACK?",
"GOL IS THE GUY TRYING TO KILL US?!",
"I'M DOOMED.",
"WE MAY ALL BE DOOMED.",
"IF THEY OPEN THE SILOS, THE DARK ECO WILL",
"TWIST AND DESTROY EVERYTHING IT TOUCHES!",
"WE SIMPLY MUST GET TO THEIR CITADEL, TO STOP THEM!",
"THE FASTEST WAY THERE IS THROUGH THE LAVA TUBE",
"AT THE BOTTOM OF THIS CRATER.",
"A FEW MORE POWER CELLS, AND YOUR ZOOMER'S HEAT SHIELD",
"SHOULD GET YOU ACROSS THE LAVA SAFELY.",
"ALL RIGHT, MY BOY. YOU KNOW WHAT TO DO.",
"TAKE THE FLEABAG AND GO ROUND UP MORE POWER CELLS."
],
"sidekick-human-intro-sequence-b": [
"CONTINUE YOUR SEARCH FOR ARTEFACTS AND ECO.",
"IF THE LOCALS POSSESS PRECURSOR ITEMS, YOU KNOW WHAT TO DO.",
"DEAL HARSHLY WITH ANYBODY WHO STRAYS FROM THE VILLAGE.",
"WE WILL ATTACK IT IN DUE TIME."
],
"warrior-introduction": [
"OHH... MY ACHING HEAD.",
"I DOUBT THAT'S ONE OF YOUR VITAL ORGANS!",
"WALK IT OFF, TOUGH GUY!",
"OH, SURE, I WAS TOUGH ONCE.",
"MAYBE EVEN THE TOUGHEST OF THEM ALL.",
"I SINGLE-HANDEDLY DEFENDED THIS VILLAGE FROM THOSE HORRID CREATURES FOR ALMOST A YEAR!",
"THEN THAT HORRIBLE MONSTER ARRIVED AND COMMENCED THE BOULDER BOMBARDMENT.",
"SO, FULL OF VALOUR, ARMOR SHINING IN THE SUN...",
"I CLIMBED THE HILL TO TAKE HIM ON...!",
"BUT HE POUNDED ME LIKE ONE TENDERIZES A YAKOW STEAK.",
"HAVE YOU TRIED ATTACKING HIM WITH YOUR MELODRAMA?",
"'CAUSE IT'S KILLIN' ME!",
"AFTER MY LAST STUNNING FAILURE,",
"HE SEALED THE PASSAGEWAY TO HIS ROOST WITH A 30-TON BOULDER,",
"LEAVING NO WAY FOR ANYONE TO CHALLENGE HIM AGAIN.",
"SO, OUR SAGE, A MASTER OF BLUE ECO AND A MECHANICAL GENIUS, DEVISED A MACHINE",
"CAPABLE OF LIFTING THE BOULDER OUT OF THE WAY...!",
"BUT ALAS, HE DISAPPEARED BEFORE WE HAD A CHANCE TO TURN IT ON.",
"AND HE TOOK ALL OF HIS POWER CELLS WITH HIM.",
"AT LEAST I WAS ABLE TO PULL ENOUGH PONTOONS OUT OF OUR BRIDGE TO PREVENT",
"THAT MONSTER FROM COMING DOWN HERE TO DO ME HARM.",
"YEAH, GOOD, GOOD JOB, TOUGH GUY. BUT, UM...",
"WE'RE GONNA NEED YOU TO, UH... PUT 'EM BACK, AND STUFF.",
"OH, SURE! AND SEAL MY DOOM?",
"(SIGHS)",
"ALRIGHT. FINE.",
"BRING ME 90 PRECURSOR ORBS AND I'LL LET THE PONTOONS LOOSE.",
"BUT I'M NOT GOING TO FIGHT THAT MONSTER AGAIN!"
]
},
"hints": {
"asstv103": [
"DON'T FORGET TO TURN ON THE TELEPORT GATE TO LET US THROUGH.",
"YOU'VE GOT TO GO INTO THE RED SAGE'S LAB",
"IN THE CENTRE OF THE VOLCANIC CRATER TO TURN IT ON.",
"WE CAN'T COME THROUGH UNTIL IT'S BACK ONLINE."
],
"asstvb42": [
"THIS IS A POWER CELL, THE MOST IMPORTANT PRECURSOR ARTEFACT YOU CAN FIND!",
"YOU NEED TO COLLECT 20 OF THESE SO I CAN POWER THE HEAT SHIELD",
"FOR YOUR A-GRAV ZOOMER."
],
"sagevb22": [
"THAT'S BLUE ECO, WHICH CONTAINS THE ENERGY OF MOTION.",
"BLUE ECO ALLOWS YOU TO RUN FAST, BREAK BOXES, AND EVEN ACTIVATE SOME PRECURSOR",
"ARTEFACTS WHEN YOU GET NEAR THEM."
],
"sagevb25": [
"GOOD WORK, THE BLUE ECO CAUSED THE DOOR TO OPEN.",
"WITH BLUE ECO, YOU CAN BREATHE ENERGY INTO ALL KINDS",
"OF PRECURSOR ARTEFACTS THAT HAVE LAIN DORMANT FOR YEARS."
],
"sksp009c": [
"DO ME A FAVOUR AND KEEP AWAY FROM THOSE DARK ECO BOXES!"
],
"sksp0135": [
"THAT WATER LOOKS DANGEROUS WHEN IT CHANGES COLOUR!"
],
"sksp0136": [
"YOU GOTTA GET OUT OF THE WATER BEFORE IT CHANGES COLOUR!"
],
"sksp0153": [
"HEY, THAT MUST BE THE PRECURSOR ARTEFACT THE LURKERS ARE AFTER!",
"IT LOOKS LIKE A GIANT ROBOT ARM!"
]
},
"speakers": {
"???": "???",
"BILLY": "BILLY",
"BIRDWATCHER": "BIRDWATCHER",
"BLUE SAGE": "BLUE SAGE",
"DAXTER": "DAXTER",
"FARMER": "FARMER",
"FISHERMAN": "FISHERMAN",
"FLUT-FLUT": "FLUT-FLUT",
"GAMBLER": "GAMBLER",
"GEOLOGIST": "GEOLOGIST",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK": "JAK",
"JAK'S UNCLE": "JAK'S UNCLE",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
"MAYOR": "MAYOR",
"MINER": "MINER",
"OLD MAN": "OLD MAN",
"ORACLE": "ORACLE",
"RED SAGE": "RED SAGE",
"SAMOS": "SAMOS",
"SCULPTOR": "SCULPTOR",
"WARRIOR": "WARRIOR",
"WILLARD": "WILLARD",
"WOMAN": "WOMAN",
"YELLOW SAGE": "YELLOW SAGE"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -0,0 +1,4 @@
{
"cutscenes": {},
"hints": {}
}

View file

@ -132,7 +132,8 @@ void SubtitleEditor::draw_window() {
}
if (ImGui::Button("Save Changes")) {
m_files_saved_successfully = std::make_optional(write_subtitle_db_to_files(m_subtitle_db));
m_files_saved_successfully =
std::make_optional(write_subtitle_db_to_files(m_subtitle_db, g_game_version));
repl_rebuild_text();
}
if (m_files_saved_successfully.has_value()) {
@ -196,7 +197,7 @@ void SubtitleEditor::draw_window() {
if (ImGui::Button("Add Scene")) {
GameSubtitleSceneInfo newScene(SubtitleSceneKind::Movie);
newScene.m_name = m_new_scene_name;
newScene.m_id = 0; // TODO - id is always zero, bug in subtitles.cpp?
newScene.m_id = 0; // id's are only used for non-named hints
newScene.m_sorting_group = m_new_scene_group;
m_subtitle_db.m_banks.at(m_current_language)->add_scene(newScene);
m_subtitle_db.m_subtitle_groups->add_scene(newScene.m_sorting_group, newScene.m_name);
@ -314,11 +315,11 @@ void SubtitleEditor::draw_edit_options() {
if (ImGui::BeginCombo(
"Editing Language ID",
fmt::format("[{}] {}", m_subtitle_db.m_banks[m_current_language]->m_lang_id,
m_subtitle_db.m_banks[m_current_language]->file_path)
m_subtitle_db.m_banks[m_current_language]->m_file_path)
.c_str())) {
for (const auto& [key, value] : m_subtitle_db.m_banks) {
const bool isSelected = m_current_language == key;
if (ImGui::Selectable(fmt::format("[{}] {}", value->m_lang_id, value->file_path).c_str(),
if (ImGui::Selectable(fmt::format("[{}] {}", value->m_lang_id, value->m_file_path).c_str(),
isSelected)) {
m_current_language = key;
}
@ -330,11 +331,11 @@ void SubtitleEditor::draw_edit_options() {
}
if (ImGui::BeginCombo("Base Language ID",
fmt::format("[{}] {}", m_subtitle_db.m_banks[m_base_language]->m_lang_id,
m_subtitle_db.m_banks[m_base_language]->file_path)
m_subtitle_db.m_banks[m_base_language]->m_file_path)
.c_str())) {
for (const auto& [key, value] : m_subtitle_db.m_banks) {
const bool isSelected = m_base_language == key;
if (ImGui::Selectable(fmt::format("[{}] {}", value->m_lang_id, value->file_path).c_str(),
if (ImGui::Selectable(fmt::format("[{}] {}", value->m_lang_id, value->m_file_path).c_str(),
isSelected)) {
m_base_language = key;
}
@ -610,21 +611,20 @@ void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool cu
if (current_scene) {
draw_new_cutscene_line_form();
}
auto font =
get_font_bank(parse_text_only_version(m_subtitle_db.m_banks[m_current_language]->file_path));
auto font = get_font_bank(m_subtitle_db.m_banks[m_current_language]->m_text_version);
int i = 0;
for (auto subtitleLine = scene.m_lines.begin(); subtitleLine != scene.m_lines.end();) {
auto linetext = font->convert_game_to_utf8(subtitleLine->line.c_str());
auto linespkr = font->convert_game_to_utf8(subtitleLine->speaker.c_str());
auto line_speaker = font->convert_game_to_utf8(subtitleLine->speaker.c_str());
std::string summary;
if (linetext.empty()) {
summary = fmt::format("[{}] Clear Screen", subtitleLine->frame);
} else if (linetext.length() >= 30) {
summary =
fmt::format("[{}] {} - '{}...'", subtitleLine->frame, linespkr, linetext.substr(0, 30));
summary = fmt::format("[{}] {} - '{}...'", subtitleLine->frame, line_speaker,
linetext.substr(0, 30));
} else {
summary =
fmt::format("[{}] {} - '{}'", subtitleLine->frame, linespkr, linetext.substr(0, 30));
fmt::format("[{}] {} - '{}'", subtitleLine->frame, line_speaker, linetext.substr(0, 30));
}
if (linetext.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
@ -637,7 +637,8 @@ void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool cu
}
ImGui::InputInt("Starting Frame", &subtitleLine->frame,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
ImGui::InputText("Speaker", &linespkr);
// TODO - speaker dropdown instead
ImGui::InputText("Speaker", &line_speaker);
ImGui::InputText("Text", &linetext);
ImGui::Checkbox("Offscreen?", &subtitleLine->offscreen);
if (scene.m_lines.size() > 1) { // prevent creating an empty scene
@ -655,7 +656,7 @@ void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool cu
ImGui::PopStyleColor();
}
auto newtext = font->convert_utf8_to_game(linetext, true);
auto newspkr = font->convert_utf8_to_game(linespkr, true);
auto newspkr = font->convert_utf8_to_game(line_speaker, true);
subtitleLine->line = newtext;
subtitleLine->speaker = newspkr;
i++;
@ -679,7 +680,7 @@ void SubtitleEditor::draw_new_cutscene_line_form() {
rendered_text_entry_btn = true;
if (ImGui::Button("Add Text Entry")) {
auto font = get_font_bank(
parse_text_only_version(m_subtitle_db.m_banks[m_current_language]->file_path));
parse_text_only_version(m_subtitle_db.m_banks[m_current_language]->m_file_path));
m_current_scene->add_line(
m_current_scene_frame, font->convert_utf8_to_game(m_current_scene_text, true),
font->convert_utf8_to_game(m_current_scene_speaker, true), m_current_scene_offscreen);

View file

@ -423,7 +423,7 @@
(define *text-languages* (static-text-list-array
english uk-english french german spanish italian japanese portuguese br-portuguese swedish finnish danish norwegian dutch hungarian))
(define *subtitle-languages* (static-text-list-array
english french german spanish italian br-portuguese))
english uk-english french german spanish italian br-portuguese))

View file

@ -85,28 +85,34 @@ Val* Compiler::compile_asm_text_file(const goos::Object& form, const goos::Objec
auto args = get_va(form, rest);
va_check(form, args, {goos::ObjectType::SYMBOL}, {{"files", {true, goos::ObjectType::PAIR}}});
// list of files per text version.
std::vector<GameTextDefinitionFile> inputs;
// what kind of text file?
const auto kind = symbol_string(args.unnamed.at(0));
// open all project files specified (usually one).
for_each_in_list(args.named.at("files"), [this, &inputs, &form, &kind](const goos::Object& o) {
if (o.is_string()) {
open_text_project(kind, o.as_string()->data, inputs);
} else {
throw_compiler_error(form, "Invalid object {} in asm-text-file files list.", o.print());
}
});
// compile files.
if (kind == "subtitle") {
std::vector<GameSubtitleDefinitionFile> inputs;
// open all project files specified (usually one).
for_each_in_list(args.named.at("files"), [this, &inputs, &form, &kind](const goos::Object& o) {
if (o.is_string()) {
open_subtitle_project(kind, o.as_string()->data, inputs);
} else {
throw_compiler_error(form, "Invalid object {} in asm-text-file files list.", o.print());
}
});
GameSubtitleDB db;
db.m_subtitle_groups = std::make_unique<GameSubtitleGroups>();
db.m_subtitle_groups->hydrate_from_asset_file();
compile_game_subtitle(inputs, db, m_make.compiler_output_prefix());
} else if (kind == "text") {
std::vector<GameTextDefinitionFile> inputs;
// open all project files specified (usually one).
for_each_in_list(args.named.at("files"), [this, &inputs, &form, &kind](const goos::Object& o) {
if (o.is_string()) {
open_text_project(kind, o.as_string()->data, inputs);
} else {
throw_compiler_error(form, "Invalid object {} in asm-text-file files list.", o.print());
}
});
GameTextDB db;
compile_game_text(inputs, db, m_make.compiler_output_prefix());
} else {

View file

@ -22,6 +22,7 @@
#include "common/goos/Reader.h"
#include "common/util/FileUtil.h"
#include "common/util/FontUtils.h"
#include "common/util/json_util.h"
#include "third-party/fmt/core.h"
@ -144,8 +145,6 @@ void compile_subtitle(GameSubtitleDB& db, const std::string& output_prefix) {
}
} // namespace
#include "common/util/json_util.h"
/*!
* Read a game text description file and generate GOAL objects.
*/
@ -154,11 +153,12 @@ void compile_game_text(const std::vector<GameTextDefinitionFile>& files,
const std::string& output_prefix) {
goos::Reader reader;
for (auto& file : files) {
lg::print("[Build Game Text] {}\n", file.file_path.c_str());
if (file.format == GameTextDefinitionFile::Format::GOAL) {
lg::print("[Build Game Text] GOAL {}\n", file.file_path);
auto code = reader.read_from_file({file.file_path});
parse_text(code, db, file);
} else if (file.format == GameTextDefinitionFile::Format::JSON) {
lg::print("[Build Game Text] JSON {}\n", file.file_path);
auto file_path = file_util::get_jak_project_dir() / file.file_path;
auto json = parse_commented_json(file_util::read_text_file(file_path), file.file_path);
parse_text_json(json, db, file);
@ -167,14 +167,19 @@ void compile_game_text(const std::vector<GameTextDefinitionFile>& files,
compile_text(db, output_prefix);
}
void compile_game_subtitle(const std::vector<GameTextDefinitionFile>& files,
void compile_game_subtitle(const std::vector<GameSubtitleDefinitionFile>& files,
GameSubtitleDB& db,
const std::string& output_prefix) {
goos::Reader reader;
for (auto& file : files) {
lg::print("[Build Game Subtitle] {}\n", file.file_path.c_str());
auto code = reader.read_from_file({file.file_path});
parse_subtitle(code, db, file.file_path);
if (file.format == GameSubtitleDefinitionFile::Format::GOAL) {
lg::print("[Build Game Subtitle] GOAL {}\n", file.lines_path);
auto code = reader.read_from_file({file.lines_path});
parse_subtitle(code, db, file.lines_path);
} else if (file.format == GameSubtitleDefinitionFile::Format::JSON) {
lg::print("[Build Game Subtitle] JSON {}:{}\n", file.lines_path, file.meta_path);
parse_subtitle_json(db, file);
}
}
compile_subtitle(db, output_prefix);
}

View file

@ -12,6 +12,6 @@
void compile_game_text(const std::vector<GameTextDefinitionFile>& filenames,
GameTextDB& db,
const std::string& output_prefix);
void compile_game_subtitle(const std::vector<GameTextDefinitionFile>& filenames,
void compile_game_subtitle(const std::vector<GameSubtitleDefinitionFile>& filenames,
GameSubtitleDB& db,
const std::string& output_prefix);

View file

@ -175,10 +175,19 @@ bool SubtitleTool::needs_run(const ToolInput& task, const PathMap& path_map) {
}
std::vector<std::string> deps;
std::vector<GameTextDefinitionFile> files;
open_text_project("subtitle", task.input.at(0), files);
std::vector<GameSubtitleDefinitionFile> files;
open_subtitle_project("subtitle", task.input.at(0), files);
for (auto& file : files) {
deps.push_back(path_map.apply_remaps(file.file_path));
deps.push_back(path_map.apply_remaps(file.lines_path));
if (file.format == GameSubtitleDefinitionFile::Format::JSON) {
deps.push_back(path_map.apply_remaps(file.meta_path));
if (file.lines_base_path) {
deps.push_back(path_map.apply_remaps(file.lines_base_path.value()));
}
if (file.meta_base_path) {
deps.push_back(path_map.apply_remaps(file.meta_base_path.value()));
}
}
}
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
}
@ -187,10 +196,19 @@ bool SubtitleTool::run(const ToolInput& task, const PathMap& path_map) {
GameSubtitleDB db;
db.m_subtitle_groups = std::make_unique<GameSubtitleGroups>();
db.m_subtitle_groups->hydrate_from_asset_file();
std::vector<GameTextDefinitionFile> files;
open_text_project("subtitle", task.input.at(0), files);
std::vector<GameSubtitleDefinitionFile> files;
open_subtitle_project("subtitle", task.input.at(0), files);
for (auto& file : files) {
file.file_path = path_map.apply_remaps(file.file_path);
file.lines_path = path_map.apply_remaps(file.lines_path);
if (file.format == GameSubtitleDefinitionFile::Format::JSON) {
file.meta_path = path_map.apply_remaps(file.meta_path);
if (file.lines_base_path) {
file.lines_base_path = path_map.apply_remaps(file.lines_base_path.value());
}
if (file.meta_base_path) {
file.meta_base_path = path_map.apply_remaps(file.meta_base_path.value());
}
}
}
compile_game_subtitle(files, db, path_map.output_prefix);
return true;