i18n: subtitle code cleanup and update new subtitle JSON files to be compatible with Crowdin (#2802)

The main thing that was done here was to slightly modify the new
subtitle-v2 JSON schema to be more similar to the existing one so that
it can properly be used in Crowdin.

Draft while I double-check the diff myself

Along the way the following was also done (among other things):
- got rid of as much duplication as was feasible in the serialization
and editor code
- separated the text serialization code from the subtitle code for
better organization
- simplified "base language" in the editor. The new subtitle format has
built-in support for defining a base language so the editor doesn't have
to be used as a crutch. Also, cutscenes only defined in the base come
first in the list now as that is generally the order you'd work from
(what you havn't done first)
- got rid of the GOAL subtitle format code completely
- switched jak 2 text translations to the JSON format as well
- found a few mistakes in the jak 1 subtitle metadata files
- added a couple minor features to the editor
- consolidate and removed complexity, ie. recently all jak 1 hints were
forced to the `named` type, so I got rid of the two types as there isn't
a need anymore.
- removed subtitle editor groups for jak 1, the only reason they existed
was so when the GOAL file was manually written out they were somewhat
organized, the editor has a decent filter control, there's no need for
them.
- removed the GOAL -> JSON python script helper, it's been a month or so
and no one has come forward with existing translations that they need
help with migrating. If they do need it, the script will be in the git
history.

I did some reasonably through testing in Jak1/Jak 2 and everything
seemed to work. But more testing is always a good idea.

---------

Co-authored-by: ManDude <7569514+ManDude@users.noreply.github.com>
This commit is contained in:
Tyler Wilding
2023-07-08 20:53:39 -05:00
committed by GitHub
parent d8ca594ee3
commit c87db7e670
81 changed files with 34822 additions and 40078 deletions
+1 -1
View File
@@ -131,7 +131,7 @@ Unfortunately you'll still need task runner on your local machine to run the gam
Install packages and init repository:
```sh
sudo apt install gcc make cmake build-essential g++ nasm clang-format libxrandr-dev libxinerama-dev libxcursor-dev libpulse-dev libxi-dev python
sudo apt install gcc make cmake build-essential g++ nasm clang-format libxrandr-dev libxinerama-dev libxcursor-dev libpulse-dev libxi-dev python libgl1-mesa-dev
sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
```
+8 -7
View File
@@ -32,10 +32,10 @@ add_library(common
dma/dma_copy.cpp
dma/dma.cpp
dma/gs.cpp
formatter/config/rule_config.cpp
formatter/formatter_tree.cpp
formatter/formatter.cpp
formatter/formatting_rules.cpp
formatter/formatter_tree.cpp
formatter/config/rule_config.cpp
global_profiler/GlobalProfiler.cpp
goos/Interpreter.cpp
goos/Object.cpp
@@ -51,10 +51,10 @@ add_library(common
repl/nrepl/ReplClient.cpp
repl/nrepl/ReplServer.cpp
repl/util.cpp
serialization/subtitles/subtitles_deser.cpp
serialization/subtitles/subtitles_ser.cpp
serialization/subtitles2/subtitles2_deser.cpp
serialization/subtitles2/subtitles2_ser.cpp
serialization/subtitles/subtitles_v1.cpp
serialization/subtitles/subtitles_v2.cpp
serialization/subtitles/subtitles.cpp
serialization/text/text_ser.cpp
sqlite/sqlite.cpp
type_system/defenum.cpp
type_system/deftype.cpp
@@ -84,7 +84,8 @@ add_library(common
util/term_util.cpp
util/Timer.cpp
util/unicode_util.cpp
versions/versions.cpp)
versions/versions.cpp
)
target_link_libraries(common fmt lzokay replxx libzstd_static tree-sitter sqlite3 libtinyfiledialogs)
@@ -0,0 +1,89 @@
#include "subtitles.h"
#include "common/goos/ParseHelpers.h"
#include "common/goos/Reader.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
#include "third-party/fmt/core.h"
void open_subtitle_project(const std::string& project_kind,
const std::string& file_path,
std::vector<GameSubtitleDefinitionFile>& subtitle_files) {
goos::Reader reader;
auto& proj = reader.read_from_file({file_path}).as_pair()->cdr.as_pair()->car;
if (!proj.is_pair() || !proj.as_pair()->car.is_symbol() ||
proj.as_pair()->car.as_symbol()->name != project_kind) {
throw std::runtime_error(fmt::format("invalid project '{}'", 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-json") {
auto new_file = GameSubtitleDefinitionFile();
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, project_kind));
}
} else {
throw std::runtime_error(fmt::format("invalid entry in {} project", project_kind));
}
});
}
const std::unordered_map<GameVersion, std::vector<std::string>> locale_lookup = {
{GameVersion::Jak1,
{"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"}},
{GameVersion::Jak2, {"en-US", "fr-FR", "de-DE", "es-ES", "it-IT", "jp-JP", "ko-KR", "en-GB"}}};
std::string lookup_locale_code(const GameVersion game_version, const int language_id) {
if (locale_lookup.find(game_version) == locale_lookup.end() ||
locale_lookup.at(game_version).size() < language_id) {
return "";
}
return locale_lookup.at(game_version).at(language_id);
}
const std::unordered_map<GameVersion, std::vector<int>> language_ids_with_audio = {
{GameVersion::Jak1, {0, 1, 2, 3, 4, 5, 6}},
{GameVersion::Jak2, {0, 1, 2, 3, 4, 5, 6, 7}}};
bool dump_language_with_duplicates_from_base(const GameVersion game_version,
const int language_id) {
if (language_ids_with_audio.find(game_version) == language_ids_with_audio.end()) {
return true;
}
if (std::find(language_ids_with_audio.at(game_version).begin(),
language_ids_with_audio.at(game_version).end(),
language_id) == language_ids_with_audio.at(game_version).end()) {
return true;
}
return false;
}
@@ -0,0 +1,29 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "common/versions/versions.h"
struct GameSubtitleDefinitionFile {
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;
};
void open_subtitle_project(const std::string& project_kind,
const std::string& file_path,
std::vector<GameSubtitleDefinitionFile>& inputs);
std::string lookup_locale_code(const GameVersion game_version, const int language_id);
// Languages that have audio tracks are not translated via Crowdin, therefore they must have
// a copy of the scenes from their base english counterpart no matter what (so it can be
// translated!)
//
// In contrast, languages like english-UK vs english-GB are almost entirely identical and cannot be
// translated via Crowdin (it has an audio track) so it makes sense to minimize the noise in the
// file by eliding the duplicated scenes that have no changes.
bool dump_language_with_duplicates_from_base(const GameVersion game_version, const int language_id);
@@ -1,137 +0,0 @@
#include "subtitles_deser.h"
#include <algorithm>
#include <regex>
#include <vector>
#include "common/util/FileUtil.h"
#include "third-party/fmt/core.h"
#include "third-party/fmt/ranges.h"
#include "third-party/json.hpp"
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);
}
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);
}
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;
}
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) {
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;
}
@@ -1,5 +0,0 @@
#pragma once
#include "common/serialization/subtitles/subtitles_ser.h"
bool write_subtitle_db_to_files(const GameSubtitleDB& db, const GameVersion game_version);
File diff suppressed because it is too large Load Diff
@@ -1,274 +0,0 @@
#pragma once
#include <algorithm>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
#include "common/goos/Object.h"
#include "common/log/log.h"
#include "common/util/Assert.h"
#include "common/util/FontUtils.h"
#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 = "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.
*/
class GameTextBank {
public:
GameTextBank(int lang_id) : m_lang_id(lang_id) {}
int lang() const { return m_lang_id; }
const std::map<int, std::string>& lines() const { return m_lines; }
bool line_exists(int id) const { return m_lines.find(id) != m_lines.end(); }
std::string line(int id) { return m_lines.at(id); }
void set_line(int id, std::string line) { m_lines[id] = line; }
private:
int m_lang_id;
std::map<int, std::string> m_lines;
};
/*!
* The text database contains a text bank for each language for each text group.
* Each text bank contains a list of text lines. Very simple.
*/
class GameTextDB {
public:
const std::unordered_map<std::string, std::map<int, std::shared_ptr<GameTextBank>>>& groups()
const {
return m_banks;
}
const std::map<int, std::shared_ptr<GameTextBank>>& banks(std::string group) const {
return m_banks.at(group);
}
bool bank_exists(std::string group, int id) const {
if (m_banks.find(group) == m_banks.end())
return false;
return m_banks.at(group).find(id) != m_banks.at(group).end();
}
std::shared_ptr<GameTextBank> add_bank(std::string group, std::shared_ptr<GameTextBank> bank) {
ASSERT(!bank_exists(group, bank->lang()));
m_banks[group][bank->lang()] = bank;
return bank;
}
std::shared_ptr<GameTextBank> bank_by_id(std::string group, int id) {
if (!bank_exists(group, id)) {
return nullptr;
}
return m_banks.at(group).at(id);
}
private:
std::unordered_map<std::string, std::map<int, std::shared_ptr<GameTextBank>>> m_banks;
};
/*!
* The subtitle scene info (accessed through the scene name) contains all lines and their timestamps
* and other settings.
*/
enum class SubtitleSceneKind { Invalid = -1, Movie = 0, Hint = 1, HintNamed = 2 };
class GameSubtitleSceneInfo {
public:
struct SubtitleLine {
SubtitleLine(int frame, std::string line, std::string speaker, bool offscreen)
: frame(frame), line(line), speaker(speaker), offscreen(offscreen) {}
int frame;
std::string line;
std::string speaker;
bool offscreen;
bool operator<(const SubtitleLine& other) const { return (frame < other.frame); }
};
GameSubtitleSceneInfo(SubtitleSceneKind kind) : m_id(0), m_kind(kind), m_sorting_group_idx(-1) {}
const std::string& name() const { return m_name; }
const std::vector<SubtitleLine>& lines() const { return m_lines; }
int id() const { return m_id; }
SubtitleSceneKind kind() const { return m_kind; }
void clear_lines() { m_lines.clear(); }
void set_name(const std::string& new_name) { m_name = new_name; }
void set_id(int new_id) { m_id = new_id; }
void from_other_scene(GameSubtitleSceneInfo& scene) {
m_name = scene.name();
m_lines = scene.lines();
m_kind = scene.kind();
m_id = scene.id();
}
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());
}
std::string m_name;
int m_id;
std::vector<SubtitleLine> m_lines;
SubtitleSceneKind m_kind;
std::string m_sorting_group;
int m_sorting_group_idx;
};
/*!
* The subtitle bank contains subtitles for all scenes in a language.
*/
class GameSubtitleBank {
public:
GameSubtitleBank(int lang_id) : m_lang_id(lang_id) {}
int lang() const { return m_lang_id; }
const std::map<std::string, GameSubtitleSceneInfo>& scenes() const { return m_scenes; }
bool scene_exists(const std::string& name) const { return m_scenes.find(name) != m_scenes.end(); }
GameSubtitleSceneInfo& scene_by_name(const std::string& name) { return m_scenes.at(name); }
void add_scene(GameSubtitleSceneInfo& scene) {
ASSERT(!scene_exists(scene.name()));
m_scenes.insert({scene.name(), scene});
}
int m_lang_id;
std::string m_text_version;
std::string m_file_path;
std::map<std::string, GameSubtitleSceneInfo> m_scenes;
};
class GameSubtitleGroups {
public:
std::vector<std::string> m_group_order;
std::map<std::string, std::vector<std::string>> m_groups;
void hydrate_from_asset_file();
std::string find_group(const std::string& scene_name);
int find_group_index(const std::string& group_name);
void remove_scene(const std::string& group_name, const std::string& scene_name);
void add_scene(const std::string& group_name, const std::string& scene_name);
std::string group_order_key = "_groups";
std::string uncategorized_group = "uncategorized";
};
/*!
* The subtitles database contains a subtitles bank for each language.
* Each subtitles bank contains a series of subtitle scene infos.
*/
class GameSubtitleDB {
public:
const std::map<int, std::shared_ptr<GameSubtitleBank>>& banks() const { return m_banks; }
bool bank_exists(int id) const { return m_banks.find(id) != m_banks.end(); }
std::shared_ptr<GameSubtitleBank> add_bank(std::shared_ptr<GameSubtitleBank> bank) {
ASSERT(!bank_exists(bank->lang()));
m_banks[bank->lang()] = bank;
return bank;
}
std::shared_ptr<GameSubtitleBank> bank_by_id(int id) {
if (!bank_exists(id)) {
return nullptr;
}
return m_banks.at(id);
}
std::map<int, std::shared_ptr<GameSubtitleBank>> m_banks;
std::unique_ptr<GameSubtitleGroups> m_subtitle_groups;
};
// TODO add docstrings
void parse_text(const goos::Object& data, GameTextDB& db, const GameTextDefinitionFile& file_info);
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);
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);
@@ -0,0 +1,302 @@
#include "subtitles_v1.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "subtitles_v2.h"
#include "third-party/fmt/core.h"
void to_json(json& j, const SubtitleCutsceneLineMetadataV1& obj) {
json_serialize(frame_start);
json_serialize(offscreen);
json_serialize(speaker);
json_serialize(clear);
}
void from_json(const json& j, SubtitleCutsceneLineMetadataV1& obj) {
json_deserialize_if_exists(frame_start);
json_deserialize_if_exists(offscreen);
json_deserialize_if_exists(speaker);
json_deserialize_if_exists(clear);
}
void to_json(json& j, const SubtitleHintLineMetadataV1& obj) {
json_serialize(frame_start);
json_serialize(speaker);
json_serialize(clear);
}
void from_json(const json& j, SubtitleHintLineMetadataV1& obj) {
json_deserialize_if_exists(frame_start);
json_deserialize_if_exists(speaker);
json_deserialize_if_exists(clear);
}
void to_json(json& j, const SubtitleHintMetadataV1& obj) {
json_serialize(id);
json_serialize(lines);
}
void from_json(const json& j, SubtitleHintMetadataV1& obj) {
json_deserialize_if_exists(id);
json_deserialize_if_exists(lines);
}
void to_json(json& j, const SubtitleMetadataFileV1& obj) {
json_serialize(cutscenes);
json_serialize(hints);
}
void from_json(const json& j, SubtitleMetadataFileV1& obj) {
json_deserialize_if_exists(cutscenes);
json_deserialize_if_exists(hints);
}
void to_json(json& j, const SubtitleFileV1& obj) {
json_serialize(speakers);
json_serialize(cutscenes);
json_serialize(hints);
}
void from_json(const json& j, SubtitleFileV1& obj) {
json_deserialize_if_exists(speakers);
json_deserialize_if_exists(cutscenes);
json_deserialize_if_exists(hints);
}
std::pair<SubtitleMetadataFile, SubtitleFile> convert_v1_to_v2(
const GameSubtitleDefinitionFile& file_info,
const SubtitleMetadataFileV1& v1_meta_file,
const SubtitleFileV1& v1_lines_file) {
// Convert the old format into the new
SubtitleMetadataFile meta_file;
SubtitleFile lines_file;
lines_file.speakers = v1_lines_file.speakers;
for (const auto& [cutscene_name, cutscene_lines] : v1_meta_file.cutscenes) {
std::vector<std::string> scene_lines;
SubtitleSceneMetadata new_scene_meta;
// 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_meta : cutscene_lines) {
SubtitleLineMetadata new_meta;
new_meta.frame_start = line_meta.frame_start;
new_meta.offscreen = line_meta.offscreen;
new_meta.speaker = line_meta.speaker;
new_meta.frame_end = 0; // v1 doesn't use frame_end
new_meta.merge = false; // or merge
new_scene_meta.lines.push_back(new_meta);
if (line_meta.clear) {
scene_lines.push_back("");
lines_added++;
} else {
if (v1_lines_file.speakers.find(line_meta.speaker) == v1_lines_file.speakers.end() ||
v1_lines_file.cutscenes.find(cutscene_name) == v1_lines_file.cutscenes.end() ||
int(v1_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_meta.speaker);
} else {
scene_lines.push_back(v1_lines_file.cutscenes.at(cutscene_name).at(line_idx));
lines_added++;
}
line_idx++;
}
}
// Verify we added the amount of lines we expected to
if (lines_added != int(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));
}
meta_file.cutscenes.emplace(cutscene_name, new_scene_meta);
lines_file.cutscenes.emplace(cutscene_name, scene_lines);
}
// Now hints
for (const auto& [hint_name, hint_info] : v1_meta_file.hints) {
std::vector<std::string> scene_lines;
SubtitleSceneMetadata new_scene_meta;
if (hint_info.id != "0") {
new_scene_meta.m_hint_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_meta : hint_info.lines) {
SubtitleLineMetadata new_meta;
new_meta.frame_start = line_meta.frame_start;
new_meta.frame_end = 0; // unused by v1
new_meta.offscreen = true;
new_meta.speaker = line_meta.speaker;
new_scene_meta.lines.push_back(new_meta);
if (line_meta.clear) {
scene_lines.push_back("");
lines_added++;
} else {
if (v1_lines_file.speakers.find(line_meta.speaker) == v1_lines_file.speakers.end() ||
v1_lines_file.hints.find(hint_name) == v1_lines_file.hints.end() ||
line_idx >= v1_lines_file.hints.at(hint_name).size()) {
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_meta.speaker);
} else {
scene_lines.push_back(v1_lines_file.hints.at(hint_name).at(line_idx));
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));
}
meta_file.other.emplace(hint_name, new_scene_meta);
lines_file.other.emplace(hint_name, scene_lines);
}
return {meta_file, lines_file};
}
GameSubtitlePackage read_json_files_v1(const GameSubtitleDefinitionFile& file_info) {
// Parse the files
SubtitleMetadataFileV1 v1_meta_base_file;
SubtitleMetadataFileV1 v1_meta_combined_file;
SubtitleFileV1 v1_lines_base_file;
SubtitleFileV1 v1_lines_lang_file;
SubtitleFileV1 v1_lines_combined_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");
v1_meta_base_file = base_data;
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"));
v1_meta_combined_file = base_data;
} else {
v1_meta_combined_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");
v1_lines_base_file = base_data;
auto data = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.lines_path),
"subtitle_line_path");
v1_lines_lang_file = data;
base_data.at("cutscenes").update(data.at("cutscenes"));
base_data.at("hints").update(data.at("hints"));
base_data.at("speakers").update(data.at("speakers"));
v1_lines_combined_file = base_data;
} else {
v1_lines_combined_file = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.lines_path),
"subtitle_line_path");
v1_lines_lang_file = v1_lines_combined_file;
}
GameSubtitlePackage package;
std::tie(package.base_meta, package.base_lines) =
convert_v1_to_v2(file_info, v1_meta_base_file, v1_lines_base_file);
std::tie(package.combined_meta, package.combined_lines) =
convert_v1_to_v2(file_info, v1_meta_combined_file, v1_lines_combined_file);
for (const auto& [scene_name, scene_info] : v1_lines_lang_file.cutscenes) {
package.scenes_defined_in_lang.insert(scene_name);
}
for (const auto& [scene_name, scene_info] : v1_lines_lang_file.hints) {
package.scenes_defined_in_lang.insert(scene_name);
}
return package;
} catch (std::exception& e) {
lg::error("Unable to parse subtitle json entry, couldn't successfully load files - {}",
e.what());
throw;
}
}
SubtitleMetadataFileV1 dump_bank_meta_v1(const GameVersion game_version,
std::shared_ptr<GameSubtitleBank> bank) {
auto meta_file = SubtitleMetadataFileV1();
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
// Avoid dumping duplicates
if (bank->m_base_scenes.find(scene_name) != bank->m_base_scenes.end() &&
scene_info.same_metadata_as_other(bank->m_base_scenes.at(scene_name))) {
continue;
}
if (scene_info.is_cutscene) {
std::vector<SubtitleCutsceneLineMetadataV1> lines;
for (const auto& line : scene_info.m_lines) {
auto line_meta = SubtitleCutsceneLineMetadataV1();
line_meta.frame_start = line.metadata.frame_start;
if (line.text.empty()) {
line_meta.clear = true;
} else {
line_meta.offscreen = line.metadata.offscreen;
line_meta.speaker = line.metadata.speaker;
}
lines.push_back(line_meta);
}
meta_file.cutscenes[scene_name] = lines;
} else {
SubtitleHintMetadataV1 hint;
hint.id = fmt::format("{:x}", scene_info.m_hint_id);
std::vector<SubtitleHintLineMetadataV1> lines;
for (const auto& line : scene_info.m_lines) {
auto line_meta = SubtitleHintLineMetadataV1();
line_meta.frame_start = line.metadata.frame_start;
if (line.text.empty()) {
line_meta.clear = true;
} else {
line_meta.speaker = line.metadata.speaker;
}
lines.push_back(line_meta);
}
hint.lines = lines;
meta_file.hints[scene_name] = hint;
}
}
return meta_file;
}
SubtitleFileV1 dump_bank_lines_v1(const GameVersion game_version,
std::shared_ptr<GameSubtitleBank> bank) {
const auto dump_with_duplicates =
dump_language_with_duplicates_from_base(game_version, bank->m_lang_id);
SubtitleFileV1 file;
file.speakers = bank->m_speakers;
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
// Avoid dumping duplicates if needed
if (!dump_with_duplicates &&
bank->m_base_scenes.find(scene_name) != bank->m_base_scenes.end() &&
scene_info.same_lines_as_other(bank->m_base_scenes.at(scene_name))) {
continue;
}
if (scene_info.is_cutscene) {
file.cutscenes[scene_name] = {};
for (const auto& scene_line : scene_info.m_lines) {
if (scene_line.text.empty()) {
continue;
}
file.cutscenes[scene_name].push_back(scene_line.text);
}
} else {
file.hints[scene_name] = {};
for (const auto& scene_line : scene_info.m_lines) {
if (scene_line.text.empty()) {
continue;
}
file.hints[scene_name].push_back(scene_line.text);
}
}
}
return file;
}
@@ -0,0 +1,54 @@
#pragma once
#include "common/serialization/subtitles/subtitles.h"
#include "common/serialization/subtitles/subtitles_v2.h"
#include "common/util/json_util.h"
struct SubtitleCutsceneLineMetadataV1 {
int frame_start;
bool offscreen;
std::string speaker;
bool clear;
};
void to_json(json& j, const SubtitleCutsceneLineMetadataV1& obj);
void from_json(const json& j, SubtitleCutsceneLineMetadataV1& obj);
struct SubtitleHintLineMetadataV1 {
int frame_start;
std::string speaker;
bool clear;
};
void to_json(json& j, const SubtitleHintLineMetadataV1& obj);
void from_json(const json& j, SubtitleHintLineMetadataV1& obj);
struct SubtitleHintMetadataV1 {
std::string id; // hex
std::vector<SubtitleHintLineMetadataV1> lines;
};
void to_json(json& j, const SubtitleHintMetadataV1& obj);
void from_json(const json& j, SubtitleHintMetadataV1& obj);
struct SubtitleMetadataFileV1 {
std::unordered_map<std::string, std::vector<SubtitleCutsceneLineMetadataV1>> cutscenes;
std::unordered_map<std::string, SubtitleHintMetadataV1> hints;
};
void to_json(json& j, const SubtitleMetadataFileV1& obj);
void from_json(const json& j, SubtitleMetadataFileV1& obj);
struct SubtitleFileV1 {
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 SubtitleFileV1& obj);
void from_json(const json& j, SubtitleFileV1& obj);
// These functions essentially convert to and from the V1/V2 formats to either load from disk, or
// persist to disk
//
// Returns the full file pair, then the base pair
GameSubtitlePackage read_json_files_v1(const GameSubtitleDefinitionFile& file_info);
SubtitleMetadataFileV1 dump_bank_meta_v1(const GameVersion game_version,
std::shared_ptr<GameSubtitleBank> bank);
SubtitleFileV1 dump_bank_lines_v1(const GameVersion game_version,
std::shared_ptr<GameSubtitleBank> bank);
@@ -0,0 +1,405 @@
#include "subtitles_v2.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "subtitles_v1.h"
#include "third-party/fmt/core.h"
void to_json(json& j, const SubtitleLineMetadata& obj) {
json_serialize(frame_start);
json_serialize(frame_end);
json_serialize(offscreen);
json_serialize(speaker);
json_serialize(merge);
}
void from_json(const json& j, SubtitleLineMetadata& obj) {
json_deserialize_if_exists(frame_start);
json_deserialize_if_exists(frame_end);
json_deserialize_if_exists(offscreen);
json_deserialize_if_exists(speaker);
json_deserialize_if_exists(merge);
}
void to_json(json& j, const SubtitleSceneMetadata& obj) {
json_serialize(lines);
}
void from_json(const json& j, SubtitleSceneMetadata& obj) {
json_deserialize_if_exists(lines);
}
void to_json(json& j, const SubtitleMetadataFile& obj) {
json_serialize(cutscenes);
json_serialize(other);
}
void from_json(const json& j, SubtitleMetadataFile& obj) {
json_deserialize_if_exists(cutscenes);
json_deserialize_if_exists(other);
}
void to_json(json& j, const SubtitleFile& obj) {
json_serialize(speakers);
json_serialize(cutscenes);
json_serialize(other);
}
void from_json(const json& j, SubtitleFile& obj) {
json_deserialize_if_exists(speakers);
json_deserialize_if_exists(cutscenes);
json_deserialize_if_exists(other);
}
// matches enum in `subtitle2.gc` with "none" (first) and "max" (last and removed)
const std::unordered_map<std::string, u16> jak2_speaker_name_to_enum_val = {
{"none", 0},
{"computer", 1},
{"jak", 2},
{"darkjak", 3},
{"daxter", 4},
{"samos", 5},
{"keira", 6},
{"keira-before-class-3", 7},
{"kid", 8},
{"kor", 9},
{"metalkor", 10},
{"baron", 11},
{"errol", 12},
{"torn", 13},
{"tess", 14},
{"guard", 15},
{"guard-a", 16},
{"guard-b", 17},
{"krew", 18},
{"sig", 19},
{"brutter", 20},
{"vin", 21},
{"youngsamos", 22},
{"youngsamos-before-rescue", 23},
{"pecker", 24},
{"onin", 25},
{"ashelin", 26},
{"jinx", 27},
{"mog", 28},
{"grim", 29},
{"agent", 30},
{"citizen-male", 31},
{"citizen-female", 32},
{"oracle", 33},
{"precursor", 34}};
GameSubtitlePackage read_json_files_v2(const GameSubtitleDefinitionFile& file_info) {
GameSubtitlePackage package;
SubtitleFile lang_lines;
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");
package.base_meta = base_data;
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("other").update(data.at("other"));
package.combined_meta = base_data;
} else {
package.combined_meta = 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");
package.base_lines = base_data;
auto data = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.lines_path),
"subtitle_line_path");
lang_lines = data;
base_data.at("cutscenes").update(data.at("cutscenes"));
base_data.at("other").update(data.at("other"));
base_data.at("speakers").update(data.at("speakers"));
package.combined_lines = base_data;
} else {
package.combined_lines = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.lines_path),
"subtitle_line_path");
lang_lines = package.combined_lines;
}
for (const auto& [scene_name, scene_info] : lang_lines.cutscenes) {
package.scenes_defined_in_lang.insert(scene_name);
}
for (const auto& [scene_name, scene_info] : lang_lines.other) {
package.scenes_defined_in_lang.insert(scene_name);
}
} catch (std::exception& e) {
lg::error("Unable to parse subtitle json entry, couldn't successfully load files - {}",
e.what());
throw;
}
return package;
}
void GameSubtitleDB::init_banks_from_file(const GameSubtitleDefinitionFile& file_info) {
// Init Settings
std::shared_ptr<GameSubtitleBank> bank;
if (!bank_exists(file_info.language_id)) {
// database has no lang yet
bank = add_bank(std::make_shared<GameSubtitleBank>(file_info.language_id));
} else {
bank = bank_by_id(file_info.language_id);
}
bank->m_text_version = get_text_version_from_name(file_info.text_version);
bank->m_file_path = file_info.lines_path;
bank->m_file_base_path = file_info.lines_base_path;
try {
if (m_subtitle_version == SubtitleFormat::V1) {
const auto package = read_json_files_v1(file_info);
bank->m_speakers = package.combined_lines.speakers;
bank->add_scenes_from_files(package);
} else {
const auto package = read_json_files_v2(file_info);
bank->m_speakers = package.combined_lines.speakers;
bank->m_speakers.emplace("none", "none");
bank->add_scenes_from_files(package);
}
} catch (std::exception& e) {
throw;
}
}
GameSubtitleSceneInfo GameSubtitleBank::new_scene_from_meta(
const std::string& scene_name,
const SubtitleSceneMetadata& scene_meta,
const std::unordered_map<std::string, std::vector<std::string>>& relevant_lines) {
GameSubtitleSceneInfo new_scene;
new_scene.m_name = scene_name;
new_scene.m_hint_id = scene_meta.m_hint_id;
new_scene.only_defined_in_base = false;
new_scene.is_cutscene = false;
int line_idx = 0;
int lines_added = 0;
for (const auto& line_meta : scene_meta.lines) {
// In V1, there was a concept of a "clear" line, you don't have to specify these in the "lines"
// file as they are just blank lines.
//
// In V2, there are no longer "clear" lines, but there are lines that are "merged" which
// essentially inherit the text from the base game (since Jak 2+ actually has subtitles!)
//
// In either case, we acknowledge that there is a line, but there is no text to retrieve at that
// index.
if (line_meta.merge || (relevant_lines.find(scene_name) != relevant_lines.end() &&
relevant_lines.at(scene_name).size() > line_idx &&
relevant_lines.at(scene_name).at(line_idx).empty())) {
new_scene.m_lines.push_back({"", line_meta});
lines_added++;
} else if (m_speakers.find(line_meta.speaker) == m_speakers.end() ||
relevant_lines.find(scene_name) == relevant_lines.end() ||
line_idx >= relevant_lines.at(scene_name).size()) {
lg::warn(
"{} Couldn't find {} in line file, or line list is too small, or speaker could not "
"be resolved {}!",
m_lang_id, scene_name, line_meta.speaker);
} else {
new_scene.m_lines.push_back({relevant_lines.at(scene_name).at(line_idx), line_meta});
lines_added++;
}
line_idx++;
}
// Verify we added the amount of lines we expected to
if (lines_added != int(scene_meta.lines.size())) {
throw std::runtime_error(
fmt::format("Cutscene: '{}' has a mismatch in metadata lines vs text lines. Expected {} "
"only added {} lines",
scene_name, scene_meta.lines.size(), lines_added));
}
return new_scene;
}
void GameSubtitleBank::add_scenes_from_files(const GameSubtitlePackage& package) {
// Save the base and lang specific file info separately for later context
for (const auto& [scene_name, scene_meta] : package.base_meta.cutscenes) {
auto new_scene = new_scene_from_meta(scene_name, scene_meta, package.base_lines.cutscenes);
new_scene.is_cutscene = true;
m_base_scenes.emplace(scene_name, new_scene);
}
for (const auto& [scene_name, scene_meta] : package.base_meta.other) {
auto new_scene = new_scene_from_meta(scene_name, scene_meta, package.base_lines.other);
m_base_scenes.emplace(scene_name, new_scene);
}
// Iterate through the metadata file as blank lines are now omitted from the lines file now
for (const auto& [scene_name, scene_meta] : package.combined_meta.cutscenes) {
auto new_scene = new_scene_from_meta(scene_name, scene_meta, package.combined_lines.cutscenes);
new_scene.is_cutscene = true;
// Check if the only place lines were defined was in the base file
if (package.scenes_defined_in_lang.find(scene_name) == package.scenes_defined_in_lang.end()) {
new_scene.only_defined_in_base = true;
}
m_scenes.emplace(scene_name, new_scene);
}
for (const auto& [scene_name, scene_meta] : package.combined_meta.other) {
auto new_scene = new_scene_from_meta(scene_name, scene_meta, package.combined_lines.other);
// Check if the only place lines were defined was in the base file
if (package.scenes_defined_in_lang.find(scene_name) == package.scenes_defined_in_lang.end()) {
new_scene.only_defined_in_base = true;
}
m_scenes.emplace(scene_name, new_scene);
}
}
// TODO - for jak 3+, this needs some game version context info (could infer from text version)
std::vector<std::string> GameSubtitleBank::speaker_names_ordered_by_enum_value() {
// Create a temporary vector of pairs (key, value)
std::vector<std::pair<std::string, u16>> temp_vec(jak2_speaker_name_to_enum_val.begin(),
jak2_speaker_name_to_enum_val.end());
// Sort the temporary vector based on the enum value in ascending order
std::sort(temp_vec.begin(), temp_vec.end(),
[](const auto& a, const auto& b) { return a.second < b.second; });
// Extract the sorted keys into a new vector
std::vector<std::string> sorted_names;
sorted_names.reserve(temp_vec.size());
for (const auto& pair : temp_vec) {
if (pair.second == 0) {
// we write #f for invalid entries, including the "none" at the start
sorted_names.push_back("#f");
} else {
sorted_names.push_back(m_speakers.at(pair.first));
}
}
return sorted_names;
}
u16 GameSubtitleBank::speaker_enum_value_from_name(const std::string& speaker_id) {
if (jak2_speaker_name_to_enum_val.find(speaker_id) == jak2_speaker_name_to_enum_val.end()) {
throw std::runtime_error(
fmt::format("'{}' speaker could not be found in the enum value mapping, update it or fix "
"the invalid speaker!",
speaker_id));
}
return u16(jak2_speaker_name_to_enum_val.at(speaker_id));
}
SubtitleMetadataFile dump_bank_meta_v2(const GameVersion game_version,
std::shared_ptr<GameSubtitleBank> bank) {
const auto dump_with_duplicates =
dump_language_with_duplicates_from_base(game_version, bank->m_lang_id);
auto meta_file = SubtitleMetadataFile();
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
// Avoid dumping duplicates
if (bank->m_base_scenes.find(scene_name) != bank->m_base_scenes.end() &&
scene_info.same_metadata_as_other(bank->m_base_scenes.at(scene_name))) {
continue;
}
SubtitleSceneMetadata scene_meta;
for (const auto& line : scene_info.m_lines) {
scene_meta.lines.push_back(line.metadata);
}
if (scene_info.is_cutscene) {
meta_file.cutscenes[scene_name] = scene_meta;
} else {
meta_file.other[scene_name] = scene_meta;
}
}
return meta_file;
}
SubtitleFile dump_bank_lines_v2(const GameVersion game_version,
std::shared_ptr<GameSubtitleBank> bank) {
const auto dump_with_duplicates =
dump_language_with_duplicates_from_base(game_version, bank->m_lang_id);
SubtitleFile file;
file.speakers = bank->m_speakers;
if (file.speakers.find("none") != file.speakers.end()) {
file.speakers.erase("none");
}
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
// Avoid dumping duplicates if needed
if (!dump_with_duplicates &&
bank->m_base_scenes.find(scene_name) != bank->m_base_scenes.end() &&
scene_info.same_lines_as_other(bank->m_base_scenes.at(scene_name))) {
continue;
}
for (const auto& scene_line : scene_info.m_lines) {
// Skip merged lines
if (scene_line.metadata.merge) {
continue;
}
if (scene_info.is_cutscene) {
file.cutscenes[scene_name].push_back(scene_line.text);
} else {
file.other[scene_name].push_back(scene_line.text);
}
}
}
return file;
}
bool GameSubtitleDB::write_subtitle_db_to_files(const GameVersion game_version) {
try {
for (const auto& [language_id, bank] : m_banks) {
json meta_file;
if (m_subtitle_version == SubtitleFormat::V1) {
meta_file = dump_bank_meta_v1(game_version, bank);
} else {
meta_file = dump_bank_meta_v2(game_version, 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", lookup_locale_code(game_version, language_id)))
.string();
file_util::write_text_file(dump_path, meta_file.dump(2));
// Now dump the actual subtitle lines
json lines_file;
if (m_subtitle_version == SubtitleFormat::V1) {
lines_file = dump_bank_lines_v1(game_version, bank);
} else {
lines_file = dump_bank_lines_v2(game_version, bank);
}
dump_path =
(file_util::get_jak_project_dir() / "game" / "assets" /
version_to_game_name(game_version) / "subtitle" /
fmt::format("subtitle_lines_{}.json", lookup_locale_code(game_version, language_id)))
.string();
file_util::write_text_file(dump_path, lines_file.dump(2));
}
} catch (std::exception& ex) {
lg::error(ex.what());
return false;
}
return true;
}
GameSubtitleDB load_subtitle_project(const GameSubtitleDB::SubtitleFormat format_version,
const GameVersion game_version) {
// Load the subtitle files
GameSubtitleDB db;
db.m_subtitle_version = format_version;
try {
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();
if (format_version == GameSubtitleDB::SubtitleFormat::V1) {
open_subtitle_project("subtitle", subtitle_project, files);
} else {
open_subtitle_project("subtitle-v2", subtitle_project, files);
}
for (auto& file : files) {
db.init_banks_from_file(file);
}
} catch (std::runtime_error& e) {
// TODO - these run in gk, all exceptions must go...not reliable
lg::error("error loading subtitle project: {}", e.what());
db.m_load_error = e.what();
}
return db;
}
@@ -0,0 +1,171 @@
#pragma once
#include "common/serialization/subtitles/subtitles.h"
#include "common/util/Assert.h"
#include "common/util/FontUtils.h"
#include "common/util/json_util.h"
struct SubtitleLineMetadata {
int frame_start;
int frame_end;
bool offscreen;
std::string speaker;
bool merge;
bool operator==(const SubtitleLineMetadata& other) const {
if (frame_start != other.frame_start || frame_end != other.frame_end ||
offscreen != other.offscreen || speaker != other.speaker || merge != other.merge) {
return false;
}
return true;
}
bool operator!=(const SubtitleLineMetadata& other) const { return !(*this == other); }
};
void to_json(json& j, const SubtitleLineMetadata& obj);
void from_json(const json& j, SubtitleLineMetadata& obj);
struct SubtitleSceneMetadata {
std::vector<SubtitleLineMetadata> lines;
int m_hint_id = 0; // used only for jak1, intentionally ignored in serialization/deserialization
};
void to_json(json& j, const SubtitleSceneMetadata& obj);
void from_json(const json& j, SubtitleSceneMetadata& obj);
struct SubtitleMetadataFile {
std::unordered_map<std::string, SubtitleSceneMetadata> cutscenes;
std::unordered_map<std::string, SubtitleSceneMetadata> other;
};
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>> other;
};
void to_json(json& j, const SubtitleFile& obj);
void from_json(const json& j, SubtitleFile& obj);
struct SubtitleLine {
std::string text;
SubtitleLineMetadata metadata;
bool operator<(const SubtitleLine& other) const {
return (metadata.frame_start < other.metadata.frame_start);
}
};
// Returns the individual components of each subtitle "package":
// - the base file
// - the combination of the two
// This allows for more context when dumping the files back out and you want
// to skip duplicate info
struct GameSubtitlePackage {
SubtitleMetadataFile base_meta;
SubtitleMetadataFile combined_meta;
SubtitleFile base_lines;
SubtitleFile combined_lines;
std::unordered_set<std::string> scenes_defined_in_lang;
};
struct GameSubtitleSceneInfo {
std::string m_name;
std::vector<SubtitleLine> m_lines;
bool is_cutscene;
bool only_defined_in_base = false;
int m_hint_id; // used only for jak1
void add_line(const std::string& text,
const int frame_start,
const int frame_end,
const bool offscreen,
const std::string& speaker,
const bool merge) {
m_lines.push_back({text, {frame_start, frame_end, offscreen, speaker, merge}});
std::sort(m_lines.begin(), m_lines.end());
}
bool same_lines_as_other(const GameSubtitleSceneInfo& other) const {
if (m_lines.size() != other.m_lines.size()) {
return false;
}
// Check each line
for (int i = 0; i < m_lines.size(); i++) {
if (m_lines.at(i).text != other.m_lines.at(i).text) {
return false;
}
}
return true;
}
bool same_metadata_as_other(const GameSubtitleSceneInfo& other) const {
if (m_name != other.m_name || m_lines.size() != other.m_lines.size() ||
is_cutscene != other.is_cutscene || m_hint_id != other.m_hint_id) {
return false;
}
// Check each line's metadata
for (int i = 0; i < m_lines.size(); i++) {
if (m_lines.at(i).metadata != other.m_lines.at(i).metadata) {
return false;
}
}
return true;
}
};
/*!
* The subtitle bank contains subtitles for all scenes in a language.
*/
class GameSubtitleBank {
public:
GameSubtitleBank(const int lang_id) : m_lang_id(lang_id) {}
int m_lang_id;
GameTextVersion m_text_version;
std::string m_file_path;
std::optional<std::string> m_file_base_path;
std::map<std::string, GameSubtitleSceneInfo> m_base_scenes;
std::map<std::string, GameSubtitleSceneInfo> m_lang_scenes;
std::map<std::string, GameSubtitleSceneInfo> m_scenes;
bool scene_exists(const std::string& name) const { return m_scenes.find(name) != m_scenes.end(); }
GameSubtitleSceneInfo new_scene_from_meta(
const std::string& scene_name,
const SubtitleSceneMetadata& scene_meta,
const std::unordered_map<std::string, std::vector<std::string>>& relevant_lines);
GameSubtitleSceneInfo& scene_by_name(const std::string& name) { return m_scenes.at(name); }
void add_scenes_from_files(const GameSubtitlePackage& package);
std::unordered_map<std::string, std::string> m_speakers;
std::vector<std::string> speaker_names_ordered_by_enum_value();
u16 speaker_enum_value_from_name(const std::string& speaker_id);
};
/*!
* The subtitles database contains a subtitles bank for each language.
* Each subtitles bank contains a series of subtitle scene infos.
*/
class GameSubtitleDB {
public:
enum class SubtitleFormat { V1, V2 };
std::optional<std::string> m_load_error = {};
std::map<int, std::shared_ptr<GameSubtitleBank>> m_banks;
SubtitleFormat m_subtitle_version = SubtitleFormat::V2;
bool bank_exists(int id) const { return m_banks.find(id) != m_banks.end(); }
std::shared_ptr<GameSubtitleBank> add_bank(std::shared_ptr<GameSubtitleBank> bank) {
ASSERT(!bank_exists(bank->m_lang_id));
m_banks[bank->m_lang_id] = bank;
return bank;
}
std::shared_ptr<GameSubtitleBank> bank_by_id(int id) {
if (!bank_exists(id)) {
return nullptr;
}
return m_banks.at(id);
}
void init_banks_from_file(const GameSubtitleDefinitionFile& file_info);
bool write_subtitle_db_to_files(const GameVersion game_version);
};
GameSubtitleDB load_subtitle_project(const GameSubtitleDB::SubtitleFormat format_version,
GameVersion game_version);
@@ -1,27 +0,0 @@
#include "subtitles2_deser.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "third-party/json.hpp"
const std::vector<std::string> locale_lookup = {"en-US", "fr-FR", "de-DE", "es-ES",
"it-IT", "jp-JP", "ko-KR", "en-GB"};
bool write_subtitle_db_to_files(const GameSubtitle2DB& db, const GameVersion game_version) {
try {
for (const auto& [language_id, bank] : db.m_banks) {
json data;
to_json(data, *bank);
std::string dump_path = (file_util::get_jak_project_dir() / "game" / "assets" /
version_to_game_name(game_version) / "subtitle" /
fmt::format("subtitle_{}.json", locale_lookup.at(language_id)))
.string();
file_util::write_text_file(dump_path, data.dump(2));
}
} catch (std::exception& ex) {
lg::error(ex.what());
return false;
}
return true;
}
@@ -1,5 +0,0 @@
#pragma once
#include "common/serialization/subtitles2/subtitles2_ser.h"
bool write_subtitle_db_to_files(const GameSubtitle2DB& db, const GameVersion game_version);
@@ -1,193 +0,0 @@
#include "subtitles2_ser.h"
#include "common/goos/ParseHelpers.h"
#include "common/goos/Reader.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
// matches enum in `subtitle2.gc` with "none" (first) and "max" (last) removed
const std::vector<std::string> s_speakers_jak2 = {
"computer",
"jak",
"darkjak",
"daxter",
"samos",
"keira",
"keira-before-class-3",
"kid",
"kor",
"metalkor",
"baron",
"errol",
"torn",
"tess",
"guard",
"guard-a",
"guard-b",
"krew",
"sig",
"brutter",
"vin",
"youngsamos",
"youngsamos-before-rescue",
"pecker",
"onin",
"ashelin",
"jinx",
"mog",
"grim",
"agent",
"citizen-male",
"citizen-female",
"oracle",
"precursor",
};
const std::vector<std::string> get_speaker_names(GameVersion version) {
switch (version) {
case GameVersion::Jak2:
return s_speakers_jak2;
default:
break;
}
throw std::runtime_error(
fmt::format("no speakers for game version {} project", version_to_game_name(version)));
}
void parse_subtitle2_json(GameSubtitle2DB& db, const GameSubtitle2DefinitionFile& file_info) {
// TODO - some validation
// Init Settings
std::shared_ptr<GameSubtitle2Bank> bank;
try {
if (!db.bank_exists(file_info.language_id)) {
// database has no lang yet
bank = db.add_bank(std::make_shared<GameSubtitle2Bank>(file_info.language_id));
} else {
bank = db.bank_by_id(file_info.language_id);
}
bank->text_version = file_info.text_version;
bank->file_path = file_info.file_path;
// Parse the file
auto file = parse_commented_json(
file_util::read_text_file(file_util::get_jak_project_dir() / file_info.file_path),
"subtitle2_json");
from_json(file, *bank);
} catch (std::exception& e) {
lg::error("Unable to parse subtitle json entry, couldn't successfully load files - {}",
e.what());
throw;
}
}
void to_json(json& j, const Subtitle2Line& obj) {
j = json{{"start", obj.start}, {"end", obj.end}, {"offscreen", obj.offscreen},
{"merge", obj.merge}, {"speaker", obj.speaker}, {"text", obj.text}};
}
void from_json(const json& j, Subtitle2Line& obj) {
json_deserialize_if_exists(start);
json_deserialize_if_exists(end);
json_deserialize_if_exists(offscreen);
json_deserialize_if_exists(merge);
json_deserialize_if_exists(speaker);
json_deserialize_if_exists(text);
}
void to_json(json& j, const Subtitle2Scene& obj) {
j = json{{"scene", obj.scene}};
json lines;
for (const auto& line : obj.lines) {
json l;
to_json(l, line);
lines.push_back(l);
}
j["lines"] = obj.lines.size() == 0 ? json::array({}) : lines;
}
void from_json(const json& j, Subtitle2Scene& obj) {
json_deserialize_if_exists(scene);
for (auto& kv : j.at("lines").items()) {
auto& line = obj.lines.emplace_back();
from_json(kv.value(), line);
}
}
void to_json(json& j, const GameSubtitle2Bank& obj) {
j = json{{"speakers", obj.speakers}, {"lang", obj.lang}};
json scenes = json::object({});
for (const auto& [name, scene] : obj.scenes) {
json s;
to_json(s, scene);
scenes[name] = s;
}
j["scenes"] = scenes;
}
void from_json(const json& j, GameSubtitle2Bank& obj) {
json_deserialize_if_exists(speakers);
for (auto& kv : j.at("scenes").items()) {
Subtitle2Scene scene;
from_json(kv.value(), scene);
obj.scenes[kv.key()] = scene;
}
json_deserialize_if_exists(lang);
}
void open_subtitle2_project(const std::string& kind,
const std::string& filename,
std::vector<GameSubtitle2DefinitionFile>& 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-json") {
GameSubtitle2DefinitionFile new_file;
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 = get_text_version_from_name(args->car.as_string()->data);
} else if (kwarg == ":data") {
new_file.file_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));
}
});
}
GameSubtitle2DB load_subtitle2_project(GameVersion game_version) {
// Load the subtitle files
GameSubtitle2DB db(game_version);
try {
goos::Reader reader;
std::vector<GameSubtitle2DefinitionFile> files;
std::string subtitle_project = (file_util::get_jak_project_dir() / "game" / "assets" /
version_to_game_name(game_version) / "game_subtitle.gp")
.string();
open_subtitle2_project("subtitle2", subtitle_project, files);
for (auto& file : files) {
parse_subtitle2_json(db, file);
}
} catch (std::runtime_error& e) {
lg::error("error loading subtitle project: {}", e.what());
}
return db;
}
@@ -1,107 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include "common/util/Assert.h"
#include "common/util/FileUtil.h"
#include "common/util/FontUtils.h"
#include "common/util/json_util.h"
#include "common/versions/versions.h"
const std::vector<std::string> get_speaker_names(GameVersion version);
struct Subtitle2Line {
Subtitle2Line() {}
Subtitle2Line(float start,
float end,
const std::string& text,
const std::string& speaker,
bool offscreen,
bool merge)
: start(start), end(end), text(text), speaker(speaker), offscreen(offscreen), merge(merge) {}
float start, end;
std::string text;
// name in enum. saved as int later.
std::string speaker;
bool offscreen, merge;
bool operator<(const Subtitle2Line& other) const {
return (start < other.start) || (start == other.start && end < other.end);
}
};
void to_json(json& j, const Subtitle2Line& obj);
void from_json(const json& j, Subtitle2Line& obj);
struct Subtitle2Scene {
bool scene = false;
std::vector<Subtitle2Line> lines;
};
void to_json(json& j, const Subtitle2Scene& obj);
void from_json(const json& j, Subtitle2Scene& obj);
struct GameSubtitle2Bank {
GameSubtitle2Bank(int lang) : lang(lang) {}
int lang;
GameTextVersion text_version = GameTextVersion::JAK2;
std::string file_path;
std::map<std::string, std::string> speakers;
std::map<std::string, Subtitle2Scene> scenes;
bool scene_exists(const std::string& name) const { return scenes.find(name) != scenes.end(); }
void add_scene(const std::string& name, Subtitle2Scene& scene) {
ASSERT(!scene_exists(name));
scenes.insert({name, scene});
}
};
void to_json(json& j, const GameSubtitle2Bank& obj);
void from_json(const json& j, GameSubtitle2Bank& obj);
class GameSubtitle2DB {
public:
GameSubtitle2DB(GameVersion version) : m_version(version) {}
const std::map<int, std::shared_ptr<GameSubtitle2Bank>>& banks() const { return m_banks; }
bool bank_exists(int id) const { return m_banks.find(id) != m_banks.end(); }
std::shared_ptr<GameSubtitle2Bank> add_bank(std::shared_ptr<GameSubtitle2Bank> bank) {
ASSERT(!bank_exists(bank->lang));
m_banks[bank->lang] = bank;
return bank;
}
std::shared_ptr<GameSubtitle2Bank> bank_by_id(int id) {
if (!bank_exists(id)) {
return nullptr;
}
return m_banks.at(id);
}
std::map<int, std::shared_ptr<GameSubtitle2Bank>> m_banks;
std::unique_ptr<GameSubtitle2Bank> m_subtitle_groups;
GameVersion version() const { return m_version; }
private:
GameVersion m_version;
};
struct GameSubtitle2DefinitionFile {
std::string file_path = "";
int language_id = -1;
GameTextVersion text_version = GameTextVersion::JAK2;
};
void parse_subtitle2_json(GameSubtitle2DB& db, const GameSubtitle2DefinitionFile& file_info);
void open_subtitle2_project(const std::string& kind,
const std::string& filename,
std::vector<GameSubtitle2DefinitionFile>& inputs);
GameSubtitle2DB load_subtitle2_project(GameVersion game_version);
+332
View File
@@ -0,0 +1,332 @@
#include "text_ser.h"
#include "common/goos/ParseHelpers.h"
#include "common/goos/Reader.h"
int64_t get_int(const goos::Object& obj) {
if (obj.is_int()) {
return obj.integer_obj.value;
}
throw std::runtime_error(obj.print() + " was supposed to be an integer, but isn't");
}
const goos::Object& car(const goos::Object& x) {
if (!x.is_pair()) {
throw std::runtime_error("car: invalid pair");
}
return x.as_pair()->car;
}
const goos::Object& cdr(const goos::Object& x) {
if (!x.is_pair()) {
throw std::runtime_error("cdr: invalid pair");
}
return x.as_pair()->cdr;
}
std::string get_string(const goos::Object& x) {
if (x.is_string()) {
return x.as_string()->data;
}
throw std::runtime_error(x.print() + " was supposed to be a string, but isn't");
}
/*!
* Parse a game text file (GOAL format).
* Information is added to the game text database.
*
* The file should begin with (language-id x y z...) with the given language IDs.
* Each entry should be (id "line for 1st language" "line for 2nd language" ...)
* This adds the text line to each of the specified languages.
*/
void parse_text_goal(const goos::Object& data,
GameTextDB& db,
const GameTextDefinitionFile& /*file_info*/) {
const GameTextFontBank* font = nullptr;
std::vector<std::shared_ptr<GameTextBank>> banks;
std::string possible_group_name;
for_each_in_list(data.as_pair()->cdr, [&](const goos::Object& obj) {
if (obj.is_pair()) {
auto& head = car(obj);
if (head.is_symbol("language-id")) {
if (banks.size() != 0) {
throw std::runtime_error("Languages have been set multiple times.");
}
if (cdr(obj).is_empty_list()) {
throw std::runtime_error("At least one language must be set.");
}
if (possible_group_name.empty()) {
throw std::runtime_error("Text group must be set before languages.");
}
for_each_in_list(cdr(obj), [&](const goos::Object& obj) {
auto lang = get_int(obj);
if (!db.bank_exists(possible_group_name, lang)) {
// database has no lang in this group yet
banks.push_back(db.add_bank(possible_group_name, std::make_shared<GameTextBank>(lang)));
} else {
banks.push_back(db.bank_by_id(possible_group_name, lang));
}
});
} else if (head.is_symbol("group-name")) {
if (!possible_group_name.empty()) {
throw std::runtime_error("group-name has been set multiple times.");
}
possible_group_name = get_string(car(cdr(obj)));
if (possible_group_name.empty()) {
throw std::runtime_error("invalid group-name.");
}
if (!cdr(cdr(obj)).is_empty_list()) {
throw std::runtime_error("group-name has too many arguments");
}
} else if (head.is_symbol("credits")) {
// parse a "credits" object. it's a list of lines where the ID automatically increments, and
// empty lines are skipped
if (banks.size() == 0) {
throw std::runtime_error("At least one language must be set before defining entries.");
}
if (cdr(obj).is_empty_list() || cdr(cdr(obj)).is_empty_list() ||
!car(cdr(obj)).is_symbol(":begin") || !car(cdr(cdr(obj))).is_int()) {
throw std::runtime_error("Invalid credits begin param");
}
const auto& it = cdr(cdr(obj));
int begin_id = car(it).as_int();
int id = begin_id - 1;
for_each_in_list(cdr(it), [&](const goos::Object& entry) {
++id;
if (entry.is_string()) {
if (entry.as_string()->data.empty()) {
// empty string! just advance
return;
}
auto line = font->convert_utf8_to_game(entry.as_string()->data);
// add to all langs
for (auto& bank : banks) {
bank->set_line(id, line);
}
} else if (entry.is_pair()) {
int b_i = 0;
for_each_in_list(entry, [&](const goos::Object& entry) {
if (entry.is_string()) {
if (b_i >= int(banks.size())) {
throw std::runtime_error(fmt::format("Too many strings in text id #x{:x}", id));
}
auto line = font->convert_utf8_to_game(entry.as_string()->data);
banks[b_i++]->set_line(id, line);
} else {
throw std::runtime_error(fmt::format("Non-string value in text id #x{:x}", id));
}
});
if (b_i != int(banks.size())) {
throw std::runtime_error(
fmt::format("Not enough strings specified in text id #x{:x}", id));
}
} else {
throw std::runtime_error(fmt::format("Non-string value in text id #x{:x}", id));
}
});
} else if (head.is_symbol("text-version")) {
if (font) {
throw std::runtime_error("text version is already set");
}
const auto& ver_name = car(cdr(obj));
if (!ver_name.is_symbol()) {
throw std::runtime_error("invalid text version entry");
}
font = get_font_bank(ver_name.as_symbol()->name);
}
else if (head.is_int()) {
if (!font) {
throw std::runtime_error("Text version must be set before defining entries.");
}
if (banks.size() == 0) {
throw std::runtime_error("At least one language must be set before defining entries.");
}
int i = 0;
int id = head.as_int();
for_each_in_list(cdr(obj), [&](const goos::Object& entry) {
if (entry.is_string()) {
if (i >= int(banks.size())) {
throw std::runtime_error(fmt::format("Too many strings in text id #x{:x}", id));
}
auto line = font->convert_utf8_to_game(entry.as_string()->data);
banks[i++]->set_line(id, line);
} else {
throw std::runtime_error(fmt::format("Non-string value in text id #x{:x}", id));
}
});
if (i != int(banks.size())) {
throw std::runtime_error(
fmt::format("Not enough strings specified in text id #x{:x}", id));
}
} else {
throw std::runtime_error("Invalid game text file entry: " + head.print());
}
} else {
throw std::runtime_error("Invalid game text file");
}
});
if (banks.size() == 0) {
throw std::runtime_error("At least one language must be set.");
}
}
/*!
* Parse a game text file (JSON format).
* Information is added to the game text database.
*
* Each single text entry should follow this format:
* <text_id>: <text_value>
* For example, for (display #x1043) you would add this entry:
* "1043": "DISPLAY"
*
* You can also set multiple sequential lines starting from an initial text_id:
* <text_id>: [<text_value_1>, <text_value_2>, etc]
* For example, Jak 1 credits start at #x0b00, so this would override the first 3 lines:
* "0b00": [
* "JAK AND DAXTER: CREDITS LINE 1",
* "CREDITS LINE 2",
* "CREDITS LINE 3"
* ]
*/
void parse_text_json(const nlohmann::json& json,
GameTextDB& db,
const GameTextDefinitionFile& file_info) {
// Verify we have all data that we need
if (!file_info.group_name.has_value()) {
throw std::runtime_error(
fmt::format("Can't parse {}, did not provide group_name", file_info.file_path));
}
if (file_info.language_id == -1) {
throw std::runtime_error(
fmt::format("Can't parse {}, did not provide language_id", file_info.file_path));
}
if (file_info.text_version.empty()) {
throw std::runtime_error(
fmt::format("Can't parse {}, did not provide text_version", file_info.file_path));
}
// Init Settings
std::shared_ptr<GameTextBank> bank;
if (!db.bank_exists(file_info.group_name.value(), file_info.language_id)) {
// database has no lang in this group yet
bank = db.add_bank(file_info.group_name.value(),
std::make_shared<GameTextBank>(file_info.language_id));
} else {
bank = db.bank_by_id(file_info.group_name.value(), file_info.language_id);
}
const GameTextFontBank* font = get_font_bank(file_info.text_version);
// Parse the file
for (const auto& [text_id, text_value] : json.items()) {
auto line_id = std::stoi(text_id, nullptr, 16);
if (text_value.is_string()) {
// single line replacement
auto line = font->convert_utf8_to_game(text_value);
// TODO - lint duplicate line definitions across text files
bank->set_line(line_id, line);
} else if (text_value.is_array()) {
// multi-line replacement starting from line_id
// (e.g. for Jak 1 credits, start from x0b00)
for (const auto& [idx, raw_line] : text_value.items()) {
if (!raw_line.is_string()) {
throw std::runtime_error(fmt::format(
"Non string provided for line {} / text id #x{} of _credits", idx, line_id));
}
auto line = font->convert_utf8_to_game(raw_line);
bank->set_line(line_id++, line); // increment line_id
}
} else {
// Unexpected value type
throw std::runtime_error(
fmt::format("Must provide string or array for text id #x{}", text_id));
}
}
}
GameTextVersion parse_text_only_version(const std::string& filename) {
goos::Reader reader;
return parse_text_only_version(reader.read_from_file({filename}));
}
GameTextVersion parse_text_only_version(const goos::Object& data) {
const GameTextFontBank* font = nullptr;
for_each_in_list(data.as_pair()->cdr, [&](const goos::Object& obj) {
if (obj.is_pair()) {
auto& head = car(obj);
if (head.is_symbol("text-version")) {
if (font) {
throw std::runtime_error("text version is already set");
}
const auto& ver_name = car(cdr(obj));
if (!ver_name.is_symbol()) {
throw std::runtime_error("invalid text version entry");
}
font = get_font_bank(ver_name.as_symbol()->name);
}
}
});
if (!font) {
throw std::runtime_error("text version not found");
}
return font->version();
}
void open_text_project(const std::string& kind,
const std::string& filename,
std::vector<GameTextDefinitionFile>& text_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 = GameTextDefinitionFile();
new_file.format = GameTextDefinitionFile::Format::GOAL;
new_file.file_path = file_path;
text_files.push_back(new_file);
} else if (action == "file-json") {
auto& language_id = args->car.as_int();
args = args->cdr.as_pair();
auto& text_version = args->car.as_symbol()->name;
args = args->cdr.as_pair();
std::optional<std::string> group_name = std::nullopt;
group_name = args->car.as_string()->data;
args = args->cdr.as_pair()->car.as_pair();
goos::for_each_in_list(args->cdr.as_pair()->car, [&](const goos::Object& o) {
text_files.push_back({GameTextDefinitionFile::Format::JSON, o.as_string()->data,
(int)language_id, text_version, group_name});
});
} 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));
}
});
}
+93
View File
@@ -0,0 +1,93 @@
#pragma once
#include <algorithm>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
#include "common/goos/Object.h"
#include "common/log/log.h"
#include "common/util/Assert.h"
#include "common/util/FontUtils.h"
#include "common/util/json_util.h"
#include "common/versions/versions.h"
struct GameTextDefinitionFile {
enum class Format { GOAL, JSON };
Format format;
std::string file_path = "";
int language_id = -1;
std::string text_version = "jak1-v2";
std::optional<std::string> group_name = std::nullopt;
};
/*!
* The text bank contains all lines (accessed with an ID) for a language.
*/
class GameTextBank {
public:
GameTextBank(int lang_id) : m_lang_id(lang_id) {}
int lang() const { return m_lang_id; }
const std::map<int, std::string>& lines() const { return m_lines; }
bool line_exists(int id) const { return m_lines.find(id) != m_lines.end(); }
std::string line(int id) { return m_lines.at(id); }
void set_line(int id, std::string line) { m_lines[id] = line; }
private:
int m_lang_id;
std::map<int, std::string> m_lines;
};
/*!
* The text database contains a text bank for each language for each text group.
* Each text bank contains a list of text lines. Very simple.
*/
class GameTextDB {
public:
const std::unordered_map<std::string, std::map<int, std::shared_ptr<GameTextBank>>>& groups()
const {
return m_banks;
}
const std::map<int, std::shared_ptr<GameTextBank>>& banks(std::string group) const {
return m_banks.at(group);
}
bool bank_exists(std::string group, int id) const {
if (m_banks.find(group) == m_banks.end())
return false;
return m_banks.at(group).find(id) != m_banks.at(group).end();
}
std::shared_ptr<GameTextBank> add_bank(std::string group, std::shared_ptr<GameTextBank> bank) {
ASSERT(!bank_exists(group, bank->lang()));
m_banks[group][bank->lang()] = bank;
return bank;
}
std::shared_ptr<GameTextBank> bank_by_id(std::string group, int id) {
if (!bank_exists(group, id)) {
return nullptr;
}
return m_banks.at(group).at(id);
}
private:
std::unordered_map<std::string, std::map<int, std::shared_ptr<GameTextBank>>> m_banks;
};
void parse_text_goal(const goos::Object& data,
GameTextDB& db,
const GameTextDefinitionFile& /*file_info*/);
void parse_text_json(const nlohmann::json& json,
GameTextDB& db,
const GameTextDefinitionFile& file_info);
GameTextVersion parse_text_only_version(const std::string& filename);
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);
+6 -12
View File
@@ -12,22 +12,13 @@
#include <algorithm>
#include <stdexcept>
#include "string_util.h"
#include "common/util/Assert.h"
#include "third-party/fmt/core.h"
#include "third-party/fmt/format.h"
namespace {
/*!
* Is this a valid character for a hex number?
*/
bool hex_char(char c) {
return !((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F'));
}
} // namespace
const std::unordered_map<std::string, GameTextVersion> sTextVerEnumMap = {
{"jak1-v1", GameTextVersion::JAK1_V1},
{"jak1-v2", GameTextVersion::JAK1_V2},
@@ -210,6 +201,9 @@ std::string GameTextFontBank::encode_utf8_to_game(std::string& str) const {
* Turn a normal readable string into a string readable in the in-game font encoding and converts
* \cXX escape sequences
*/
// NOTE - the convert_utf8_to_game function is really really slow (about 80-90% of the
// time loading the text files)
// TODO - improve that as a follow up sometime in the future
std::string GameTextFontBank::convert_utf8_to_game(std::string str, bool escape) const {
std::string newstr;
@@ -230,7 +224,7 @@ std::string GameTextFontBank::convert_utf8_to_game(std::string str, bool escape)
}
auto first = str.at(i + 2);
auto second = str.at(i + 3);
if (!hex_char(first) || !hex_char(second)) {
if (!str_util::hex_char(first) || !str_util::hex_char(second)) {
throw std::runtime_error("invalid character escape hex number");
}
char hex_num[3] = {first, second, '\0'};
+3
View File
@@ -86,6 +86,9 @@ class GameTextFontBank {
std::string convert_game_to_utf8(const char* in) const;
};
extern GameTextFontBank g_font_bank_jak1_v1;
extern GameTextFontBank g_font_bank_jak1_v2;
extern GameTextFontBank g_font_bank_jak2;
extern std::map<GameTextVersion, GameTextFontBank*> g_font_banks;
const GameTextFontBank* get_font_bank(GameTextVersion version);
+3
View File
@@ -207,4 +207,7 @@ std::string to_lower(const std::string& str) {
std::transform(str.begin(), str.end(), new_str.begin(), ::tolower);
return new_str;
}
bool hex_char(char c) {
return !((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F'));
}
} // namespace str_util
+2
View File
@@ -30,4 +30,6 @@ std::string current_local_timestamp_no_colons();
std::string current_isotimestamp();
std::string to_upper(const std::string& str);
std::string to_lower(const std::string& str);
/// Is this a valid character for a hex number?
bool hex_char(char c);
} // namespace str_util
+5 -4
View File
@@ -6,12 +6,12 @@ set(CMAKE_ASM_NASM_SOURCE_FILE_EXTENSIONS ${CMAKE_ASM_NASM_SOURCE_FILE_EXTENSION
set(CMAKE_ASM_NASM_COMPILE_OBJECT "<CMAKE_ASM_NASM_COMPILER> <INCLUDES> -f ${CMAKE_ASM_NASM_OBJECT_FORMAT} -o <OBJECT> <SOURCE>")
set_source_files_properties(kernel/asm_funcs.asm PROPERTIES COMPILE_FLAGS "-g")
set(RUNTIME_SOURCE
external/discord.cpp
external/discord_jak1.cpp
external/discord_jak2.cpp
external/discord.cpp
graphics/display.cpp
graphics/gfx.cpp
graphics/gfx_test.cpp
graphics/gfx.cpp
graphics/jak2_texture_remap.cpp
graphics/opengl_renderer/background/background_common.cpp
graphics/opengl_renderer/background/Shrub.cpp
@@ -217,8 +217,9 @@ set(RUNTIME_SOURCE
system/vm/dmac.cpp
system/vm/vm.cpp
tools/filter_menu/filter_menu.cpp
tools/subtitles/subtitle_editor.cpp
tools/subtitles2/subtitle2_editor.cpp
tools/subtitle_editor/subtitle_editor_db.cpp
tools/subtitle_editor/subtitle_editor_repl_client.cpp
tools/subtitle_editor/subtitle_editor.cpp
)
add_subdirectory(sound)
@@ -1,205 +0,0 @@
// 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"
}
}
@@ -1,125 +0,0 @@
# 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)
@@ -1,736 +0,0 @@
{
"_groups": [
"intro",
"sidekick",
"oracle",
"training",
"village1",
"beach",
"jungle",
"misty",
"firecanyon",
"village2",
"swamp",
"rolling",
"sunken",
"ogre",
"village3",
"snowy",
"spidercave",
"lavatube",
"citadel",
"finalboss",
"title",
"uncategorized"
],
"beach": [
"CHI-LO01",
"CHI-LO02",
"CHI-AM01",
"CHI-AM02",
"CHI-AM03",
"CHI-AM04",
"CHI-AM05",
"CHI-AM06",
"CHI-AM07",
"CHI-AM08",
"SCU-AM01",
"SCU-AM02",
"SCU-AM03",
"SCU-AM04",
"SCU-AM05",
"SCU-AM06",
"SCU-LO01",
"BIR-AM01",
"BIR-AM02",
"BIR-AM03",
"BIR-AM04",
"BIR-AM05",
"BIR-AM06",
"BIR-AM07",
"BIR-AM08",
"BIR-AM09",
"BIR-AM10",
"BIR-AM11",
"BIR-AM12",
"BIR-AM13",
"BIR-LO01",
"BIR-LO02",
"BIR-LO03",
"sksp0019",
"sksp0020",
"sksp0022",
"sksp0023",
"sksp0024",
"sksp0025",
"sksp0026",
"sksp0027",
"sksp0029",
"sksp0030",
"sksp0031",
"sksp0034",
"sksp0443",
"sagevb01",
"mayor-introduction",
"mayor-reminder-beams",
"mayor-reminder-donation",
"mayor-resolution-beams",
"mayor-resolution-donation",
"sculptor-introduction",
"sculptor-reminder-1",
"sculptor-resolution",
"bird-lady-introduction",
"bird-lady-reminder-1",
"bird-lady-reminder-2",
"bird-lady-beach-resolution",
"sksp0028",
"sksp0021",
"BIR-am08"
],
"citadel": [
"green-sagecage-introduction",
"redsage-resolution",
"bluesage-resolution",
"yellowsage-resolution",
"green-sagecage-resolution",
"green-sagecage-outro-preboss",
"BLU-AM01",
"BLU-AM02",
"BLU-AM03",
"RED-AM01",
"RED-AM02",
"RED-AM03",
"YEL-AM01",
"YEL-AM02",
"YEL-AM03",
"sksp0381",
"sksp0382",
"sksp0383",
"sksp0384",
"sksp0385",
"sksp0386",
"sksp0387",
"sksp0388",
"sksp0389",
"sksp0390",
"sksp0391",
"sksp0392",
"sksp0393",
"sksp0378"
],
"finalboss": [
"GOL-AM01",
"GOL-AM02",
"GOL-AM03",
"GOL-AM04",
"GOL-AM05",
"GOL-AM06",
"GOL-AM07",
"GOL-AM08",
"GOL-AM09",
"GOL-AM10",
"GOL-AM11",
"GOL-AM12",
"GOL-AM13",
"GOL-AM14",
"GOL-AM15",
"GOL-AM16",
"GOL-AM17",
"GOL-AM18",
"GOL-AM19",
"GOL-AM20",
"MAI-AM01",
"MAI-AM02",
"MAI-AM03",
"MAI-AM04",
"MAI-AM05",
"MAI-AM06",
"MAI-AM07",
"MAI-AM08",
"MAI-AM09",
"finalbosscam-white-eco",
"green-sagecage-daxter-sacrifice",
"green-sagecage-outro-beat-boss-a",
"green-sagecage-outro-beat-boss-b",
"green-sagecage-outro-beat-boss-need-cells",
"green-sagecage-outro-beat-boss-enough-cells",
"green-sagecage-outro-big-finish",
"sksp0394",
"sksp0395",
"sksp0396",
"sksp0397",
"sksp0398",
"sksp0399",
"sksp0400",
"sksp0401",
"sksp0402",
"sksp0403",
"sksp0404",
"sksp0405",
"sksp0406",
"sksp0407",
"sksp0408",
"sksp0409",
"sksp0410",
"sksp0411",
"sksp0412",
"sksp0413",
"sksp0414",
"sksp0415",
"sksp0416",
"sksp0417"
],
"firecanyon": [
"sksp0076",
"sksp0077",
"sksp0078",
"sksp0079",
"sksp0080",
"sksp0081",
"sksp0082",
"sksp0083",
"sksp0084",
"sksp0085",
"sksp0086",
"sksp0087",
"sksp0088",
"sksp0089",
"sksp0090",
"sksp0091",
"sksp0092",
"sksp0093",
"sksp0094",
"sksp0095",
"sksp0096",
"assistant-firecanyon-resolution"
],
"intro": [
"sage-intro-sequence-a",
"sidekick-human-intro-sequence-b",
"sidekick-human-intro-sequence-c",
"sage-intro-sequence-d1",
"sage-intro-sequence-d2",
"sage-intro-sequence-e"
],
"jungle": [
"FIS-AM01",
"FIS-AM02",
"FIS-AM03",
"FIS-AM04",
"FIS-AM05",
"FIS-AM06",
"FIS-LO01",
"FIS-LO03",
"FIS-LO04",
"FIS-LO05",
"FIS-TA01",
"FIS-TA02",
"FIS-TA03",
"FIS-TA04",
"FIS-TA05",
"FIS-TA06",
"FIS-TA07",
"FIS-TA08",
"FIS-TA09",
"FIS-TA10",
"FIS-TA11",
"FIS-TA1A",
"FIS-TA2A",
"asstvb02",
"sksp0011",
"sksp0035",
"sksp0018",
"sksp0037",
"sksp0038",
"sksp0039",
"sksp0040",
"sksp0041",
"sksp0049",
"sksp0050",
"sksp0051",
"sksp0052",
"sksp0053",
"sksp0054",
"sksp0b42",
"fisher-introduction",
"fisher-reminder-1",
"fisher-reject",
"fisher-accept",
"fisher-resolution"
],
"lavatube": [
"assistant-lavatube-start-resolution",
"assistant-lavatube-end-resolution",
"sksp0364",
"sksp0365",
"sksp0366",
"sksp0367",
"sksp0368",
"sksp0369",
"sksp0370",
"sksp0371",
"sksp0372",
"sksp0373",
"sksp0374",
"sksp0375",
"sksp0375",
"sksp0376",
"sksp0378",
"sksp0379",
"sksp0380"
],
"misty": [
"asstvb03",
"sagevb02",
"sksp0056",
"sksp0059",
"sksp0060",
"sksp0062",
"sksp0063",
"sksp0064",
"sksp0067",
"sksp0069",
"sksp0070",
"sksp0071",
"sksp0072",
"sksp0073",
"sksp0435",
"fishermans-boat-ride-to-village1-alt",
"evilbro-misty-end"
],
"ogre": [
"asstvb23",
"asstvb24",
"sksp0300",
"sksp0301",
"sksp0302",
"sksp0303",
"sksp0304",
"sksp0305",
"sksp0306",
"sksp0307",
"sksp0308",
"sksp0309",
"sksp0310",
"sksp0311",
"sksp0312",
"sksp0313",
"sksp0314",
"sksp0315",
"sksp0316",
"sksp0317",
"sksp0318",
"sksp0319",
"sksp0320",
"sksp0321",
"asstvb25"
],
"oracle": [
"oracle-intro-1",
"oracle-reminder-1",
"oracle-right-eye-1",
"oracle-left-eye-1",
"oracle-right-eye-2",
"oracle-left-eye-2",
"oracle-right-eye-3",
"oracle-left-eye-3",
"oracle-intro-2",
"oracle-reminder-2",
"oracle-intro-3",
"oracle-reminder-3"
],
"rolling": [
"sksp0109",
"sksp0110",
"sksp0111",
"sksp0112",
"sksp0113",
"sksp0114",
"sksp0115",
"sksp0116",
"sksp0117",
"sksp0118",
"sksp0119",
"sksp0120",
"sksp0121",
"sksp0122"
],
"sidekick": [
"death-0181",
"death-0182",
"death-0184",
"death-0186",
"death-0187",
"death-0191",
"death-0193",
"death-0195",
"death-0197",
"death-0199",
"death-0202",
"sksp0001",
"sksp0002",
"sksp0003",
"sksp0004",
"sksp0005",
"sksp0006",
"sksp0007",
"sksp0008",
"sksp0009",
"sksp0014",
"sksp009a",
"sksp009b",
"sksp009c",
"sksp009d",
"sksp009e",
"sksp009f",
"sksp009g",
"sksp009i",
"sksp009j",
"sksp009k"
],
"snowy": [
"sksp0345",
"sksp0346",
"sksp0347",
"sksp0348",
"sksp0349",
"sksp0350",
"sksp0351",
"sksp0352",
"sksp0359",
"sksp0360",
"sksp0362",
"sksp0363"
],
"spidercave": [
"sksp0327",
"sksp0328",
"sksp0329",
"sksp0330",
"sksp0331",
"sksp0332",
"sksp0333",
"sksp0334",
"sksp0335",
"sksp0336",
"sksp0337",
"sksp0338",
"sksp0339",
"sksp0340",
"sksp0341",
"sksp0342",
"sksp0343"
],
"sunken": [
"sksp0123",
"sksp0124",
"sksp0125",
"sksp0126",
"sksp0127",
"sksp0128",
"sksp0129",
"sksp0130",
"sksp0131",
"sksp0132",
"sksp0133",
"sksp0134",
"sksp0135",
"sksp0136"
],
"swamp": [
"billy-introduction",
"billy-reject",
"billy-accept",
"billy-resolution",
"billy-reminder-1",
"BIL-AM01",
"BIL-AM02",
"BIL-AM03",
"BIL-AM04",
"BIL-AM05",
"BIL-AM06",
"BIL-AM07",
"BIL-AM08",
"BIL-AM1A",
"BIL-AM2A",
"BIL-AM2B",
"BIL-LO01",
"BIL-LO02",
"BIL-LO03",
"BIL-LO1A",
"BIL-LO2A",
"BIL-LO2B",
"BIL-TA01",
"BIL-TA02",
"BIL-TA03",
"BIL-TA04",
"BIL-TA05",
"BIL-TA06",
"BIL-TA07",
"BIL-TA08",
"BIL-TA09",
"BIL-TA1A",
"BIL-TA2A",
"BIL-TA3A",
"BIL-TA4A",
"BIL-TA4B",
"BIL-TA5A",
"SKSP009F",
"sksp0137",
"sksp0138",
"sksp0139",
"sksp0140",
"sksp0141",
"sksp0142",
"sksp0143",
"sksp0144",
"sksp0145",
"sksp0146",
"sksp0147",
"sksp0148",
"sksp0149",
"sksp0150",
"sksp0151",
"sksp0152",
"sksp0153",
"sksp0154",
"sksp0155",
"sksp0156",
"sksp0157",
"sksp0158",
"sksp0159",
"sksp0160",
"sksp0161"
],
"title": [
"sksp0444",
"sksp0449",
"sksp0450",
"sksp0451",
"sksp0452",
"sksp0455",
"sksp0466"
],
"training": [
"asstvb40",
"asstvb41",
"asstvb42",
"asstvb44",
"asstvb45",
"asstvb46",
"asstvb47",
"asstvb48",
"sagevb21",
"sagevb22",
"sagevb23",
"sagevb24",
"sagevb25",
"sagevb26",
"sagevb27",
"sagevb28",
"sagevb29",
"sagevb30",
"sagevb31",
"sagevb32",
"sagevb33",
"sagevb34",
"sagevb35",
"sagevb36",
"sagevb37",
"sagevb38",
"sagevb39"
],
"village1": [
"ASSTLP01",
"ASSTLP02",
"ASSTLP03",
"ASSTLP04",
"ASSTLP05",
"EXP-AM01",
"EXP-AM02",
"EXP-AM03",
"EXP-AM04",
"EXP-AM05",
"EXP-LO02",
"FAR-AM01",
"FAR-AM02",
"FAR-AM2A",
"FAR-LO01",
"FAR-LO1A",
"SAGELP03",
"SAGELP04",
"SAGELP05",
"SAGELP06",
"SAGELP11",
"asstvb04",
"asstvb08",
"asstvb09",
"sksp0010",
"sksp0013",
"sksp0015",
"sksp0017",
"sksp0043",
"sksp018a",
"sage-introduction-misty-cannon",
"sage-reminder-1-misty-cannon",
"sage-reminder-1-ecorocks",
"sage-reminder-1-generic",
"sage-reminder-2-generic",
"assistant-introduction-blue-eco-switch",
"assistant-reminder-1-blue-eco-switch",
"assistant-introduction-race-bike",
"assistant-reminder-1-race-bike",
"assistant-reminder-1-generic",
"farmer-introduction",
"farmer-reminder-1",
"farmer-reminder-2",
"farmer-resolution",
"explorer-introduction",
"explorer-reminder-1",
"explorer-reminder-2",
"explorer-resolution",
"EXP-LO1A"
],
"village2": [
"sagevb03",
"sagevb04",
"asstvb28",
"asstvb29",
"asstvb30",
"ASSTLP23",
"ASSTLP24",
"SAGELP20",
"SAGELP21",
"SAGELP22",
"SAGELP23",
"SAGELP24",
"asstvb20",
"asstvb21",
"asstvb22",
"asstvb72",
"assistant-village2-introduction",
"sage-bluehut-introduction-crop-dusting",
"assistant-village2-introduction-room",
"sage-bluehut-reminder-1-crop-dusting",
"assistant-village2-reminder-1-room",
"sage-bluehut-introduction-prec-arm",
"sage-bluehut-reminder-1-prec-arm",
"assistant-village2-introduction-flutflut",
"assistant-village2-reminder-1-flutflut",
"assistant-village2-introduction-robbers",
"assistant-village2-reminder-1-robbers",
"asstvb71",
"assistant-village2-resolution",
"GEO-AM01",
"GEO-AM02",
"GEO-AM03",
"GEO-AM04",
"GEO-AM05",
"GEO-AM06",
"GEO-AM07",
"GEO-AM08",
"GEO-LO01",
"GEO-LO02",
"geologist-introduction",
"geologist-reminder-money",
"geologist-resolution-money",
"geologist-reminder-moles",
"geologist-resolution-moles",
"GAM-AM01",
"GAM-AM02",
"GAM-AM03",
"GAM-AM04",
"GAM-AM05",
"GAM-AM06",
"GAM-AM07",
"GAM-AM08",
"GAM-AM09",
"GAM-AM10",
"GAM-AM11",
"GAM-AM12",
"GAM-AM13",
"gambler-reminder-race",
"gambler-reminder-money",
"gambler-introduction-1",
"gambler-resolution-money",
"gambler-resolution-race",
"WAR-LO1A",
"WAR-LO1B",
"WAR-LO1C",
"warrior-introduction",
"warrior-reminder-1",
"warrior-resolution"
],
"village3": [
"asstv100",
"asstv101",
"asstv102",
"asstv103",
"asstva73",
"asstva74",
"asstvb75",
"asstvb76",
"sage-village3-introduction",
"ASSTLP30",
"ASSTLP31",
"ASSTLP32",
"ASSTLP33",
"ASSTLP34",
"ASSTLP35",
"ASSTLP36",
"ASSTLP37",
"SAGELP31",
"SAGELP32",
"SAGELP33",
"SAGELP34",
"SAGELP35",
"SAGELP36",
"SAGELP37",
"SAGELP38",
"assistant-village3-reminder",
"sage-village3-introduction-dark-eco",
"sage-village3-reminder-1-dark-eco",
"sage-village3-introduction-rams",
"sage-village3-reminder-1-rams",
"MSH-AM09",
"MSH-AM1A",
"minershort-introduction-orbs",
"minershort-resolution-1-orbs",
"minershort-reminder-2-orbs",
"minershort-reminder-1-orbs",
"minershort-resolution-2-orbs",
"minershort-introduction-gnawers",
"minershort-reminder-1-gnawers",
"minershort-introduction-switch",
"minershort-reminder-1-switch",
"MIN-LO01",
"MIN-LO03",
"MIN-LO04",
"MIN-LO05",
"MIN-LO06",
"MSH-AM01",
"MSH-AM02",
"MSH-AM03",
"MSH-AM04",
"MSH-AM05",
"MSH-AM06",
"MSH-AM07",
"MSH-AM08",
"MSH-AM10",
"MSH-AM11",
"MSH-AM12",
"MSH-AM2A",
"MSH-AM3A",
"MTA-AM01",
"MTA-AM02",
"MTA-AM03",
"MTA-AM04",
"MTA-AM05",
"MTA-AM06",
"MTA-AM07",
"MTA-AM08",
"MTA-AM09",
"asstv104",
"asstv105"
]
}
@@ -2850,6 +2850,7 @@
"GEOLOGIST": "GÉOLOGUE",
"GOL": "GOL",
"GORDY": "GORDY",
"JAK": "JAK",
"JAK'S UNCLE": "ONCLE DE JAK",
"KEIRA": "KEIRA",
"MAIA": "MAIA",
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
@@ -1,4 +1,4 @@
{
"cutscenes": {},
"hints": {}
}
}
+13 -8
View File
@@ -1,10 +1,15 @@
;; "project file" for subtitles make tool.
(subtitle2
(file-json :text-version "jak2" :language-id 0
:data "game/assets/jak2/subtitle/subtitle_en-US.json")
(file-json :text-version "jak2" :language-id 5
:data "game/assets/jak2/subtitle/subtitle_jp-JP.json")
)
(subtitle-v2
(file-json
:language-id 0
:text-version "jak2"
:lines "game/assets/jak2/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak2/subtitle/subtitle_meta_en-US.json")
(file-json
:language-id 5
:text-version "jak2"
:lines "game/assets/jak2/subtitle/subtitle_lines_jp-JP.json"
:lines-base "game/assets/jak2/subtitle/subtitle_lines_en-US.json"
:meta "game/assets/jak2/subtitle/subtitle_meta_jp-JP.json"
:meta-base "game/assets/jak2/subtitle/subtitle_meta_en-US.json"))
+7 -3
View File
@@ -1,13 +1,17 @@
;; "project file" for text make tool.
;; it's very simple... a list of (action args...)
;; currently the only action available is 'file'
;; and it takes 1 argument: input filename.
;; There are two actions:
;; - file-json (A path to a json file)
;; - args: (language_id text_version group_name '(filename list))
;; - file (A path to a GOAL data file)
;; - the same arguments are provided within the file itself
(text
;; NOTE : we compile using the fixed v2 encoding because it's what we use.
(file "$DECOMP/assets/game_text.txt") ;; this is the decompiler-generated file!
;; add custom files down here
(file "game/assets/jak2/text/game_custom_text_en.gs")
(file-json 0 jak2 "common" '("game/assets/jak2/text/game_custom_text_en-US.json"))
(file-json 7 jak2 "common" '("game/assets/jak2/text/game_custom_text_en-GB.json"))
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,35 +1,11 @@
{
"lang": 5,
"scenes": {
"vin013": {
"lines": [
{
"end": 74.0,
"merge": false,
"offscreen": true,
"speaker": "vin",
"start": 0.0,
"text": "ジャック...コールが..."
},
{
"end": 154.0,
"merge": false,
"offscreen": true,
"speaker": "vin",
"start": 75.0,
"text": "爆弾...建造...エリア..."
},
{
"end": 189.0,
"merge": false,
"offscreen": true,
"speaker": "vin",
"start": 155.0,
"text": "フゥー..."
}
],
"scene": false
}
"cutscenes": {},
"other": {
"vin013": [
"ジャック...コールが...",
"爆弾...建造...エリア...",
"フゥー..."
]
},
"speakers": {
"agent": "エイジェント",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
{
"cutscenes": {},
"other": {
"vin013": {
"lines": [
{
"frame_end": 74,
"frame_start": 0,
"merge": false,
"offscreen": true,
"speaker": "vin"
},
{
"frame_end": 154,
"frame_start": 75,
"merge": false,
"offscreen": true,
"speaker": "vin"
},
{
"frame_end": 189,
"frame_start": 155,
"merge": false,
"offscreen": true,
"speaker": "vin"
}
]
}
}
}
@@ -0,0 +1,10 @@
{
"1100": "Not in a mission",
"1101": "Playing a side mission",
"1102": "Playing Scatter Gun Training",
"1103": "Playing Blaster Training",
"1104": "Playing Vulcan Fury Training",
"1105": "Playing Peace Maker Training",
"1106": "Playing Onin's Game",
"1107": "Playing Metal Head Mash"
}
@@ -0,0 +1,10 @@
{
"1100": "Not in a mission",
"1101": "Playing a side mission",
"1102": "Playing Scatter Gun Training",
"1103": "Playing Blaster Training",
"1104": "Playing Vulcan Fury Training",
"1105": "Playing Peace Maker Training",
"1106": "Playing Onin's Game",
"1107": "Playing Metal Head Mash"
}
@@ -1,43 +0,0 @@
(group-name "common")
(language-id 0 7)
(text-version jak2)
(#x1100
"Not in a mission"
"Not in a mission"
)
(#x1101
"Playing a side mission"
"Playing a side mission"
)
(#x1102
"Playing Scatter Gun Training"
"Playing Scatter Gun Training"
)
(#x1103
"Playing Blaster Training"
"Playing Blaster Training"
)
(#x1104
"Playing Vulcan Fury Training"
"Playing Vulcan Fury Training"
)
(#x1105
"Playing Peace Maker Training"
"Playing Peace Maker Training"
)
(#x1106
"Playing Onin's Game"
"Playing Onin's Game"
)
(#x1107
"Playing Metal Head Mash"
"Playing Metal Head Mash"
)
@@ -777,12 +777,6 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) {
}
m_subtitle_editor->draw_window();
}
if (settings.draw_subtitle2_editor_window) {
if (m_subtitle2_editor == nullptr) {
m_subtitle2_editor = new Subtitle2Editor(m_version);
}
m_subtitle2_editor->draw_window();
}
if (settings.draw_filters_window) {
m_filters_menu.draw_window();
@@ -13,8 +13,7 @@
#include "game/graphics/opengl_renderer/foreground/Merc2.h"
#include "game/graphics/opengl_renderer/opengl_utils.h"
#include "game/tools/filter_menu/filter_menu.h"
#include "game/tools/subtitles/subtitle_editor.h"
#include "game/tools/subtitles2/subtitle2_editor.h"
#include "game/tools/subtitle_editor/subtitle_editor.h"
struct RenderOptions {
bool draw_render_debug_window = false;
@@ -22,7 +21,6 @@ struct RenderOptions {
bool draw_loader_window = false;
bool draw_small_profiler_window = false;
bool draw_subtitle_editor_window = false;
bool draw_subtitle2_editor_window = false;
bool draw_filters_window = false;
// internal rendering settings - The OpenGLRenderer will internally use this resolution/format.
@@ -148,7 +146,6 @@ class OpenGLRenderer {
Profiler m_profiler;
SmallProfiler m_small_profiler;
SubtitleEditor* m_subtitle_editor = nullptr;
Subtitle2Editor* m_subtitle2_editor = nullptr;
FiltersMenu m_filters_menu;
std::shared_ptr<Merc2> m_merc2;
+1 -7
View File
@@ -106,19 +106,13 @@ void OpenGlDebugGui::draw(const DmaStats& dma_stats) {
}
if (ImGui::BeginMenu("Tools")) {
if (m_version == GameVersion::Jak1) {
ImGui::MenuItem("Subtitle Editor", nullptr, &m_subtitle_editor);
} else {
ImGui::MenuItem("Subtitle2 Editor", nullptr, &m_subtitle2_editor);
}
if (ImGui::BeginMenu("Screenshot")) {
ImGui::MenuItem("Screenshot Next Frame!", nullptr, &m_want_screenshot);
ImGui::InputText("File", m_screenshot_save_name, 50);
ImGui::InputInt("Width", &screenshot_width);
ImGui::InputInt("Height", &screenshot_height);
ImGui::InputInt("MSAA", &screenshot_samples);
ImGui::Checkbox("Screenshot on f2", &screenshot_hotkey_enabled);
ImGui::Checkbox("Screenshot on F2", &screenshot_hotkey_enabled);
ImGui::EndMenu();
}
ImGui::MenuItem("Subtitle Editor", nullptr, &m_subtitle_editor);
@@ -48,7 +48,6 @@ class OpenGlDebugGui {
bool should_draw_render_debug() const { return master_enable && m_draw_debug; }
bool should_draw_profiler() const { return master_enable && m_draw_profiler; }
bool should_draw_subtitle_editor() const { return master_enable && m_subtitle_editor; }
bool should_draw_subtitle2_editor() const { return master_enable && m_subtitle2_editor; }
bool should_draw_filters_menu() const { return master_enable && m_filters_menu; }
bool should_draw_loader_menu() const { return master_enable && m_draw_loader; }
const char* screenshot_name() const { return m_screenshot_save_name; }
@@ -83,7 +82,6 @@ class OpenGlDebugGui {
bool m_draw_debug = false;
bool m_draw_loader = false;
bool m_subtitle_editor = false;
bool m_subtitle2_editor = false;
bool m_filters_menu = false;
bool m_want_screenshot = false;
char m_screenshot_save_name[256] = "screenshot.png";
-1
View File
@@ -385,7 +385,6 @@ void render_game_frame(int game_width,
options.draw_profiler_window = g_gfx_data->debug_gui.should_draw_profiler();
options.draw_loader_window = g_gfx_data->debug_gui.should_draw_loader_menu();
options.draw_subtitle_editor_window = g_gfx_data->debug_gui.should_draw_subtitle_editor();
options.draw_subtitle2_editor_window = g_gfx_data->debug_gui.should_draw_subtitle2_editor();
options.draw_filters_window = g_gfx_data->debug_gui.should_draw_filters_menu();
options.save_screenshot = false;
options.quick_screenshot = false;
+2
View File
@@ -122,6 +122,8 @@ void KeyboardDevice::process_event(const SDL_Event& event,
// modifiers are instead inspected on a KEYUP, however if it's one of the keys
// for triggering the binding assignment, and it's the first time we've seen it -- we ignore
// it
// TODO - `seen_confirm_up` should already be true if they didn't press that key
// this is preventing setting modifiers as keys
if (!bind_assignment->seen_confirm_up) {
for (const auto& confirm_bind : bind_assignment->keyboard_confirmation_binds) {
if (confirm_bind.sdl_idx == key_event.keysym.sym) {
@@ -0,0 +1,569 @@
#include "subtitle_editor.h"
#include <regex>
#include <string_view>
#include "common/serialization/text/text_ser.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
#include "common/util/string_util.h"
#include "game/runtime.h"
#include "third-party/fmt/core.h"
#include "third-party/imgui/imgui.h"
#include "third-party/imgui/imgui_stdlib.h"
SubtitleEditor::SubtitleEditor() {
m_filter_cutscenes = m_filter_placeholder;
m_filter_non_cutscenes = m_filter_placeholder;
if (g_game_version == GameVersion::Jak1) {
m_subtitle_version = GameSubtitleDB::SubtitleFormat::V1;
} else {
m_subtitle_version = GameSubtitleDB::SubtitleFormat::V2;
}
}
bool SubtitleEditor::is_v1_format() {
return m_subtitle_db.m_subtitle_version == GameSubtitleDB::SubtitleFormat::V1;
}
bool SubtitleEditor::is_scene_in_current_lang(const std::string& scene_name) {
return m_subtitle_db.m_banks.at(m_current_language)->m_scenes.find(scene_name) !=
m_subtitle_db.m_banks.at(m_current_language)->m_scenes.end();
}
void SubtitleEditor::draw_window() {
ImGui::Begin("Subtitle Editor");
// Lazily load the first time the window is displayed
if (!m_db_loaded && !m_db_failed_to_load) {
if (g_game_version == GameVersion::Jak1) {
m_jak1_editor_db.update();
}
m_subtitle_db = load_subtitle_project(m_subtitle_version, g_game_version);
if (m_subtitle_db.m_load_error) {
m_db_failed_to_load = true;
} else {
m_db_loaded = true;
}
} else if (m_db_failed_to_load) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text(fmt::format("Error Loading - {}!", m_subtitle_db.m_load_error.value()).c_str());
ImGui::PopStyleColor();
if (ImGui::Button("Try Again")) {
if (g_game_version == GameVersion::Jak1) {
m_jak1_editor_db.update();
}
m_subtitle_db = load_subtitle_project(m_subtitle_version, g_game_version);
if (m_subtitle_db.m_load_error) {
m_db_failed_to_load = true;
} else {
m_db_loaded = true;
}
}
}
if (ImGui::Button("Save Changes")) {
m_files_saved_successfully =
std::make_optional(m_subtitle_db.write_subtitle_db_to_files(g_game_version));
m_repl.rebuild_text();
// TODO - reloading the project would be a good idea because then cutscens that have since been
// modified would appear as such but that creates race conditions when the GUI is parsing at the
// same time it seems so, disabled for now Same Below
// m_subtitle_db = load_subtitle_project(m_subtitle_version, g_game_version);
}
if (m_files_saved_successfully.has_value()) {
ImGui::SameLine();
if (m_files_saved_successfully.value()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_success_text_color);
ImGui::Text("Saved!");
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Error!");
ImGui::PopStyleColor();
}
}
/*if (ImGui::Button("Reload Project")) {
m_subtitle_db = load_subtitle_project(m_subtitle_version, g_game_version);
}*/
draw_edit_options();
draw_repl_options();
draw_speaker_options();
if (!m_current_scene) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
}
if (ImGui::TreeNode("Currently Selected Cutscene")) {
ImGui::PopStyleColor();
if (m_current_scene) {
draw_subtitle_options(*m_current_scene, true);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255));
ImGui::Text("Select a Scene from Below!");
ImGui::PopStyleColor();
}
ImGui::TreePop();
} else {
ImGui::PopStyleColor();
}
if (ImGui::TreeNode("Cutscenes")) {
draw_scene_section_header(false);
ImGui::InputText("Filter", &m_filter_cutscenes,
ImGuiInputTextFlags_::ImGuiInputTextFlags_AutoSelectAll);
if (m_subtitle_db.m_banks[m_current_language]->m_file_base_path) {
draw_all_cutscenes(true);
}
draw_all_cutscenes(false);
ImGui::TreePop();
}
if (ImGui::TreeNode("Non-Cutscenes")) {
draw_scene_section_header(true);
ImGui::InputText("Filter", &m_filter_non_cutscenes,
ImGuiInputTextFlags_::ImGuiInputTextFlags_AutoSelectAll);
if (m_subtitle_db.m_banks[m_current_language]->m_file_base_path) {
draw_all_non_cutscenes(true);
}
draw_all_non_cutscenes(false);
ImGui::TreePop();
}
ImGui::End();
}
void SubtitleEditor::draw_edit_options() {
if (ImGui::TreeNode("Editing 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]->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->m_file_path).c_str(),
isSelected)) {
m_current_language = key;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (m_subtitle_db.m_banks.find(m_current_language) != m_subtitle_db.m_banks.end()) {
if (m_subtitle_db.m_banks.at(m_current_language)->m_file_base_path) {
ImGui::Text("Language Base - %s",
m_subtitle_db.m_banks.at(m_current_language)->m_file_base_path.value().c_str());
} else {
ImGui::Text("This language has no base language!");
}
}
ImGui::Checkbox("Truncate line summaries", &m_truncate_summaries);
if (g_game_version == GameVersion::Jak1) {
if (ImGui::Button("Update Editor DB")) {
m_jak1_editor_db.update();
}
}
ImGui::TreePop();
}
}
void SubtitleEditor::draw_repl_options() {
if (ImGui::TreeNode("REPL Options")) {
ImGui::TextWrapped(
"This tool requires a REPL connected to the game, with the game built. Run the following "
"to do so:");
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
ImGui::Text(" task repl");
ImGui::Text(" (lt)");
ImGui::Text(" (mi)");
ImGui::PopStyleColor();
if (m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_success_text_color);
ImGui::Text("REPL Connected, should be good to go!");
ImGui::PopStyleColor();
} else {
if (ImGui::Button("Connect to REPL")) {
m_repl.connect();
if (!m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Could not connect.");
ImGui::PopStyleColor();
}
}
}
ImGui::TreePop();
}
}
void SubtitleEditor::draw_speaker_options() {
if (ImGui::TreeNode("Speakers")) {
const auto& bank = m_subtitle_db.m_banks[m_current_language];
for (auto& [speaker_id, speaker_localized] : bank->m_speakers) {
if (speaker_id == "none") {
continue;
}
// Insertion or deletion not needed here as it has to be wired up in .gc and C++ code
// nothing would get persisted and there has to be a translation for all speakers (even if
// it's no translation at all)
ImGui::InputText(speaker_id.c_str(), &speaker_localized);
}
ImGui::TreePop();
}
}
void SubtitleEditor::draw_scene_section_header(const bool non_cutscenes) {
if (ImGui::TreeNode("Create New Scene Entry")) {
ImGui::InputText("New Scene Name", &m_new_scene_name);
if (non_cutscenes && g_game_version == GameVersion::Jak1) {
ImGui::InputText("New Scene ID (hex)", &m_new_scene_id);
}
if (is_scene_in_current_lang(m_new_scene_name)) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Scene already exists with that name, no!");
ImGui::PopStyleColor();
} else if (!m_new_scene_name.empty()) {
if (ImGui::Button("Add Scene")) {
GameSubtitleSceneInfo new_scene;
new_scene.is_cutscene = !non_cutscenes;
if (non_cutscenes && g_game_version == GameVersion::Jak1) {
new_scene.m_hint_id = strtoul(m_new_scene_id.c_str(), nullptr, 16);
} else {
new_scene.m_hint_id = 0;
}
m_subtitle_db.m_banks.at(m_current_language)->m_scenes.emplace(m_new_scene_name, new_scene);
m_new_scene_name = "";
}
ImGui::NewLine();
}
ImGui::TreePop();
}
}
void SubtitleEditor::draw_scene_node(const bool base_cutscenes,
const std::string& scene_name,
GameSubtitleSceneInfo& scene_info,
std::unordered_set<std::string>& scenes_to_delete) {
bool is_current_scene = m_current_scene && m_current_scene->m_name == scene_name;
bool pop_color = false;
if (!base_cutscenes && is_current_scene) {
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
pop_color = true;
} else if (base_cutscenes) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
pop_color = true;
} else if (g_game_version == GameVersion::Jak1 && scene_info.is_cutscene &&
m_jak1_editor_db.m_db.find(scene_name) == m_jak1_editor_db.m_db.end()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_warning_color);
pop_color = true;
}
if (ImGui::TreeNode(fmt::format("{}-{}", scene_name, base_cutscenes).c_str(), "%s",
scene_name.c_str())) {
if (pop_color) {
ImGui::PopStyleColor();
}
if (!is_current_scene) {
if (ImGui::Button("Select as Current Cutscene")) {
m_current_scene = &scene_info;
}
}
draw_subtitle_options(scene_info);
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
if (ImGui::Button("Delete")) {
if (scene_info.m_name == m_current_scene->m_name) {
m_current_scene = nullptr;
}
scenes_to_delete.insert(scene_name);
}
ImGui::PopStyleColor();
ImGui::TreePop();
} else if (pop_color) {
ImGui::PopStyleColor();
}
}
void SubtitleEditor::draw_all_cutscenes(bool base_cutscenes) {
std::unordered_set<std::string> scenes_to_delete;
for (auto& [scene_name, scene_info] : m_subtitle_db.m_banks.at(m_current_language)->m_scenes) {
if (!scene_info.is_cutscene || (base_cutscenes && !scene_info.only_defined_in_base) ||
(!base_cutscenes &&
m_subtitle_db.m_banks[m_current_language]->m_file_base_path.has_value() &&
scene_info.only_defined_in_base)) {
continue;
}
if ((!m_filter_cutscenes.empty() && m_filter_cutscenes != m_filter_placeholder) &&
str_util::to_lower(scene_name).find(str_util::to_lower(m_filter_cutscenes)) ==
std::string::npos) {
continue;
}
draw_scene_node(base_cutscenes, scene_name, scene_info, scenes_to_delete);
}
for (auto& scene_name : scenes_to_delete) {
if (m_subtitle_db.m_banks.at(m_current_language)->scene_exists(scene_name)) {
m_subtitle_db.m_banks.at(m_current_language)->m_scenes.erase(scene_name);
}
}
}
void SubtitleEditor::draw_all_non_cutscenes(bool base_cutscenes) {
std::unordered_set<std::string> scenes_to_delete;
for (auto& [scene_name, scene_info] : m_subtitle_db.m_banks.at(m_current_language)->m_scenes) {
if (scene_info.is_cutscene || (base_cutscenes && !scene_info.only_defined_in_base) ||
(!base_cutscenes &&
m_subtitle_db.m_banks[m_current_language]->m_file_base_path.has_value() &&
scene_info.only_defined_in_base)) {
continue;
}
if ((!m_filter_non_cutscenes.empty() && m_filter_non_cutscenes != m_filter_placeholder) &&
str_util::to_lower(scene_name).find(str_util::to_lower(m_filter_non_cutscenes)) ==
std::string::npos) {
continue;
}
draw_scene_node(base_cutscenes, scene_name, scene_info, scenes_to_delete);
}
for (auto& scene_name : scenes_to_delete) {
if (m_subtitle_db.m_banks.at(m_current_language)->scene_exists(scene_name)) {
m_subtitle_db.m_banks.at(m_current_language)->m_scenes.erase(scene_name);
}
}
}
std::string SubtitleEditor::subtitle_line_summary(const SubtitleLine& line,
const SubtitleLineMetadata& line_meta,
const std::shared_ptr<GameSubtitleBank> bank) {
// Truncate the text if it's too long, it's supposed to just be a summary at a glance
std::string line_text = "";
if (!line.text.empty()) {
if (m_truncate_summaries && line.text.size() > 30) {
line_text = line.text.substr(0, 30) + "...";
} else {
line_text = line.text;
}
} else {
if (is_v1_format()) {
line_text = "Clear Screen";
} else if (line_meta.merge) {
line_text = "<Merge>";
}
}
// Append important info about the frame / speaker to the front
std::string info_header = fmt::format("[{}", line_meta.frame_start);
// V1
if (is_v1_format()) {
if (line.text.empty()) {
return fmt::format("{}] {}", info_header, line_text);
}
return fmt::format("{}] {} - '{}'", info_header,
m_subtitle_db.m_banks[m_current_language]->m_speakers.at(line_meta.speaker),
line_text);
}
// V2
if (line_meta.merge) {
return fmt::format("{}-{}] {} - {}", info_header, line_meta.frame_end,
m_subtitle_db.m_banks[m_current_language]->m_speakers.at(line_meta.speaker),
line_text);
}
return fmt::format("{}-{}] {} - '{}'", info_header, line_meta.frame_end,
m_subtitle_db.m_banks[m_current_language]->m_speakers.at(line_meta.speaker),
line_text);
}
void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool current_scene) {
if (!m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("REPL not connected, can't play!");
ImGui::PopStyleColor();
} else {
bool play = false;
bool save_and_reload_text = false;
if (ImGui::Button("Play")) {
play = true;
}
ImGui::SameLine();
if (ImGui::Button("Save and Play")) {
play = true;
save_and_reload_text = true;
}
if (play) {
if (save_and_reload_text) {
m_subtitle_db.write_subtitle_db_to_files(g_game_version);
m_repl.rebuild_text();
}
if (g_game_version == GameVersion::Jak1) {
m_jak1_editor_db.update();
if (scene.is_cutscene) {
if (m_jak1_editor_db.m_db.find(scene.m_name) == m_jak1_editor_db.m_db.end()) {
lg::error("{} not defined in Jak 1's subtitle editor database!", scene.m_name);
} else {
m_repl.execute_jak1_cutscene_code(m_jak1_editor_db.m_db.at(scene.m_name));
}
} else {
m_repl.play_hint(scene.m_name);
}
} else {
m_repl.play_vag(scene.m_name, scene.is_cutscene);
}
}
}
if (current_scene) {
draw_new_scene_line_form();
}
int i = 0;
for (auto subtitle_line = scene.m_lines.begin(); subtitle_line != scene.m_lines.end();) {
auto& line_text = subtitle_line->text;
auto& line_speaker = subtitle_line->metadata.speaker;
auto& line_meta = subtitle_line->metadata;
int frames[2] = {line_meta.frame_start, line_meta.frame_end};
std::string summary =
subtitle_line_summary(*subtitle_line, line_meta, m_subtitle_db.m_banks[m_current_language]);
if (line_text.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else if (subtitle_line->metadata.offscreen) {
ImGui::PushStyleColor(ImGuiCol_Text, m_offscreen_text_color);
}
if (ImGui::TreeNode(fmt::format("{}", i).c_str(), "%s", summary.c_str())) {
if (line_text.empty() || subtitle_line->metadata.offscreen) {
ImGui::PopStyleColor();
}
bool hide_line_options = false;
if (is_v1_format()) {
if (line_text.empty()) {
hide_line_options = true;
} else {
ImGui::InputInt("Starting Frame", &subtitle_line->metadata.frame_start,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
}
} else {
ImGui::InputInt2("Start and End Frame", frames,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
}
if (!hide_line_options) {
if (ImGui::BeginCombo(
"Speaker",
m_subtitle_db.m_banks[m_current_language]->m_speakers.at(line_speaker).c_str())) {
for (const auto& [speaker_id, localized_name] :
m_subtitle_db.m_banks[m_current_language]->m_speakers) {
const bool is_selected = speaker_id == line_speaker;
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
if (ImGui::Selectable(localized_name.c_str(), is_selected)) {
subtitle_line->metadata.speaker = speaker_id;
}
}
ImGui::EndCombo();
}
ImGui::InputText("Text", &subtitle_line->text,
line_meta.merge ? ImGuiInputTextFlags_ReadOnly : 0);
ImGui::Checkbox("Offscreen?", &subtitle_line->metadata.offscreen);
if (!is_v1_format()) {
ImGui::SameLine();
if (ImGui::Checkbox("Merge Text?", &subtitle_line->metadata.merge)) {
// Clear text if they've checked it
if (subtitle_line->metadata.merge) {
subtitle_line->text = "";
}
}
}
}
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
if (ImGui::Button("Remove")) {
subtitle_line = scene.m_lines.erase(subtitle_line);
ImGui::PopStyleColor();
ImGui::TreePop();
continue;
}
ImGui::PopStyleColor();
ImGui::TreePop();
} else if (line_text.empty() || subtitle_line->metadata.offscreen) {
ImGui::PopStyleColor();
}
line_meta.frame_start = frames[0];
line_meta.frame_end = frames[1];
i++;
subtitle_line++;
}
}
void SubtitleEditor::draw_new_scene_line_form() {
if (is_v1_format()) {
ImGui::InputInt("Starting Frame", &m_current_scene_frames[0],
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
} else {
ImGui::InputInt2("Start and End Frame", m_current_scene_frames,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
}
std::string current_speaker = "";
if (m_subtitle_db.m_banks[m_current_language]->m_speakers.find(m_current_scene_speaker) !=
m_subtitle_db.m_banks[m_current_language]->m_speakers.end()) {
current_speaker =
m_subtitle_db.m_banks[m_current_language]->m_speakers.at(m_current_scene_speaker);
}
if (ImGui::BeginCombo("Speaker", current_speaker.c_str())) {
for (const auto& [speaker_id, localized_name] :
m_subtitle_db.m_banks[m_current_language]->m_speakers) {
const bool is_selected = speaker_id == m_current_scene_speaker;
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
if (ImGui::Selectable(localized_name.c_str(), is_selected)) {
m_current_scene_speaker = speaker_id;
}
}
ImGui::EndCombo();
}
ImGui::InputText("Text", &m_current_scene_text,
m_current_scene_merge ? ImGuiInputTextFlags_ReadOnly : 0);
ImGui::Checkbox("Offscreen", &m_current_scene_offscreen);
if (!is_v1_format()) {
ImGui::SameLine();
if (ImGui::Checkbox("Merge Text?", &m_current_scene_merge)) {
// Clear text if they've checked it
if (m_current_scene_merge) {
m_current_scene_text = "";
}
}
}
bool rendered_text_entry_btn = false;
// Validation:
// - start frame > 0
// - end frame > start_frame
// - non-empty text
// - pick a speaker
if (m_current_scene_frames[0] < 0 ||
(!is_v1_format() && m_current_scene_frames[1] < m_current_scene_frames[0]) ||
(m_current_scene_text.empty() && !m_current_scene_merge) || m_current_scene_speaker.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Can't add a new text entry with the current fields!");
ImGui::PopStyleColor();
} else {
rendered_text_entry_btn = true;
if (ImGui::Button("Add Text Entry")) {
m_current_scene->add_line(m_current_scene_text, m_current_scene_frames[0],
m_current_scene_frames[1], m_current_scene_offscreen,
m_current_scene_speaker, false);
}
}
if (is_v1_format()) {
if (m_current_scene_frames[0] < 0) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Can't add a clear screen entry with the current fields!");
ImGui::PopStyleColor();
} else {
if (rendered_text_entry_btn) {
ImGui::SameLine();
}
if (ImGui::Button("Add Clear Screen Entry")) {
m_current_scene->add_line("", m_current_scene_frames[0], 0, m_current_scene_offscreen, "",
false);
}
}
}
}
@@ -0,0 +1,77 @@
#pragma once
#include <optional>
#include "subtitle_editor_db.h"
#include "subtitle_editor_repl_client.h"
#include "common/serialization/subtitles/subtitles_v2.h"
#include "third-party/imgui/imgui.h"
class SubtitleEditor {
public:
SubtitleEditor();
void draw_window();
private:
bool is_v1_format();
void draw_edit_options();
void draw_repl_options();
void draw_speaker_options();
void draw_scene_section_header(const bool non_cutscenes);
void draw_scene_node(const bool base_cutscenes,
const std::string& scene_name,
GameSubtitleSceneInfo& scene_info,
std::unordered_set<std::string>& scenes_to_delete);
void draw_all_cutscenes(bool base_cutscenes = false);
void draw_all_non_cutscenes(bool base_cutscenes = false);
std::string subtitle_line_summary(const SubtitleLine& line,
const SubtitleLineMetadata& line_meta,
const std::shared_ptr<GameSubtitleBank> bank);
void draw_subtitle_options(GameSubtitleSceneInfo& scene, bool current_scene = false);
void draw_new_scene_line_form();
bool m_db_loaded = false;
bool m_db_failed_to_load = false;
GameSubtitleDB m_subtitle_db;
SubtitleEditorReplClient m_repl;
GameSubtitleDB::SubtitleFormat m_subtitle_version;
// Jak 1 Specifics
Jak1SubtitleEditorDB m_jak1_editor_db;
// GUI Controls
int m_current_language = 0;
bool m_truncate_summaries = false;
std::string m_filter_cutscenes;
std::string m_filter_non_cutscenes;
GameSubtitleSceneInfo* m_current_scene = nullptr;
// GUI Styling
ImVec4 m_normal_text_color = ImVec4(1.0f, 0.0f, 1.0f, 1.0f);
int m_selected_text_color = IM_COL32(89, 227, 225, 255);
ImVec4 m_success_text_color = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
ImVec4 m_error_text_color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
ImVec4 m_disabled_text_color = ImVec4(1.0f, 1.0f, 1.0f, 0.7f);
ImVec4 m_warning_color = ImVec4(0.619f, 0.443f, 0.0f, 1.0f);
int m_offscreen_text_color = IM_COL32(240, 242, 102, 255);
// State
int m_current_scene_frames[2] = {0, 0};
std::string m_current_scene_speaker = "";
std::string m_current_scene_text = "";
bool m_current_scene_offscreen = false;
bool m_current_scene_merge = false;
std::string m_new_scene_name = "";
std::string m_new_scene_id = "0";
std::string m_filter_placeholder = "Filter List...";
std::optional<bool> m_files_saved_successfully = {};
bool is_scene_in_current_lang(const std::string& scene_name);
};
@@ -0,0 +1,53 @@
#include "subtitle_editor_db.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
#include "game/runtime.h"
#include "third-party/fmt/core.h"
void Jak1SubtitleEditorDB::update() {
if (g_game_version != GameVersion::Jak1) {
return;
}
std::string db_path =
(file_util::get_jak_project_dir() / "game" / "assets" / "jak1" / "subtitle-editor-db.json")
.string();
auto config_str = file_util::read_text_file(db_path);
auto db_data = parse_commented_json(config_str, db_path);
for (const auto& [key, val] : db_data.items()) {
auto new_entry = Entry();
try {
new_entry.entity_type = val.at("entity_type").get<std::string>();
new_entry.process_name = val.at("process_name").get<std::string>();
new_entry.continue_name = val.at("continue_name").get<std::string>();
new_entry.move_to = val.at("move_to").get<std::vector<double>>();
if (val.contains("delay")) {
new_entry.delay_frames = val.at("delay").get<int>();
} else {
new_entry.delay_frames = 0;
}
if (val.contains("move_first")) {
new_entry.move_first = val.at("move_first").get<bool>();
} else {
new_entry.move_first = false;
}
if (new_entry.move_to.size() != 0 && new_entry.move_to.size() != 3) {
fmt::print("Bad subtitle db entry, provide 0 or 3 coordinates for 'move_to' - {}", key);
continue;
}
new_entry.execute_code = val.at("execute_code").get<std::string>();
new_entry.requirements = val.at("requirements").get<std::vector<std::string>>();
if (m_db.count(key) == 0) {
m_db.emplace(key, new_entry);
} else {
m_db[key] = new_entry;
}
} catch (std::exception& ex) {
fmt::print("Bad subtitle db entry - {} - {}", key, ex.what());
}
}
}
@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
// Only really needed for Jak 1 as it had no built-in way to easily
// replay cutscenes
class Jak1SubtitleEditorDB {
public:
struct Entry {
std::string entity_type;
std::string process_name;
std::string continue_name;
std::vector<double> move_to;
int delay_frames;
std::string execute_code;
bool move_first;
std::vector<std::string> requirements;
};
std::unordered_map<std::string, Entry> m_db = {};
void update();
};
@@ -0,0 +1,127 @@
#include "subtitle_editor_repl_client.h"
#include <regex>
#include "game/runtime.h"
#include "third-party/fmt/core.h"
SubtitleEditorReplClient::SubtitleEditorReplClient() {
int port = 8181;
if (g_game_version == GameVersion::Jak2) {
port = 8182;
}
m_repl = std::make_unique<ReplClient>(port);
}
void SubtitleEditorReplClient::set_continue_point(const std::string& continue_point) {
m_repl->eval(
fmt::format("(start 'play (get-continue-by-name *game-info* \"{}\"))", continue_point));
}
void SubtitleEditorReplClient::move_jak(const double x, const double y, const double z) {
m_repl->eval(
fmt::format("(move-to-point! (-> *target* control) (new 'static 'vector :x (meters {:.3f}) "
":y (meters {:.3f}) :z (meters {:.3f})))",
x, y, z));
m_repl->eval("(send-event *camera* 'teleport)");
}
void SubtitleEditorReplClient::reset_game() {
m_repl->eval("(set! (-> *game-info* mode) 'debug)");
m_repl->eval("(initialize! *game-info* 'game (the-as game-save #f) (the-as string #f))");
}
std::string SubtitleEditorReplClient::get_process_string(const std::string& entity_type,
const std::string& process_name) {
return fmt::format("(the-as {} (process-by-name \"{}\" *active-pool*))", entity_type,
process_name);
}
void SubtitleEditorReplClient::play_hint(const std::string& hint_name) {
reset_game();
m_repl->eval(
fmt::format("(level-hint-spawn (text-id zero) \"{}\" (the-as entity #f) *entity-pool* "
"(game-task none))",
hint_name));
}
void SubtitleEditorReplClient::execute_jak1_cutscene_code(
const Jak1SubtitleEditorDB::Entry& entry) {
// Reset the game first to get to a known state
reset_game();
if (entry.move_first) {
// Set Jak's Continue Point
if (!entry.continue_name.empty()) {
set_continue_point(entry.continue_name);
}
// Move Jak into position
if (!entry.move_to.empty()) {
move_jak(entry.move_to[0], entry.move_to[1], entry.move_to[2]);
}
}
// Run any requirements to setup the task state
if (!entry.requirements.empty()) {
// Replace __GET-PROCESS__
for (const auto& form : entry.requirements) {
std::string temp = form;
temp = std::regex_replace(temp, std::regex("__GET-PROCESS__"),
get_process_string(entry.entity_type, entry.process_name));
m_repl->eval(temp);
}
}
if (!entry.move_first) {
// Set Jak's Continue Point
if (!entry.continue_name.empty()) {
set_continue_point(entry.continue_name);
}
// Move Jak into position
if (!entry.move_to.empty()) {
move_jak(entry.move_to[0], entry.move_to[1], entry.move_to[2]);
}
}
// Execute the critical code - typically this means sending a 'play-anim event to the
// process-taskable in question
if (!entry.execute_code.empty()) {
std::string temp = entry.execute_code;
temp = std::regex_replace(temp, std::regex("__GET-PROCESS__"),
get_process_string(entry.entity_type, entry.process_name));
m_repl->eval("(send-event *camera* 'teleport)");
if (entry.delay_frames == 0) {
m_repl->eval(temp);
} else {
// We do this in a separate thread to introduce a delay -- allow the game to catch up before
// running the critical section
auto code =
fmt::format("(process-spawn-function process (lambda () (dotimes (i {}) (suspend)) {}))",
entry.delay_frames, temp);
m_repl->eval(code);
}
}
}
void SubtitleEditorReplClient::rebuild_text() {
if (g_game_version == GameVersion::Jak1) {
m_repl->eval("(make-text)");
// increment the language id of the in-memory text file so that it won't match the current
// language and the game will want to reload it asap
m_repl->eval("(1+! (-> *subtitle-text* lang))");
} else {
// reload subtitles immediately
m_repl->eval("(reload-subtitles)");
}
}
void SubtitleEditorReplClient::play_vag(const std::string& scene_name, bool is_cutscene) {
if (g_game_version != GameVersion::Jak1) {
if (is_cutscene) {
m_repl->eval(fmt::format("(scene-find-and-play \"{}\")", scene_name));
} else {
m_repl->eval(fmt::format("(vag-player-play-from-name \"{}\")", scene_name));
}
}
}
@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include "subtitle_editor_db.h"
#include "common/repl/nrepl/ReplClient.h"
#include "common/versions/versions.h"
class SubtitleEditorReplClient {
public:
SubtitleEditorReplClient();
std::unique_ptr<ReplClient> m_repl;
void connect() { m_repl->connect(); }
bool is_connected() { return m_repl->is_connected(); }
void set_continue_point(const std::string& continue_point);
void move_jak(const double x, const double y, const double z);
void reset_game();
std::string get_process_string(const std::string& entity_type, const std::string& process_name);
void execute_jak1_cutscene_code(const Jak1SubtitleEditorDB::Entry& entry);
void rebuild_text();
void play_hint(const std::string& hint_name);
void play_vag(const std::string& scene_name, bool is_cutscene);
};
-702
View File
@@ -1,702 +0,0 @@
#include "subtitle_editor.h"
#include <regex>
#include <string_view>
#include "common/serialization/subtitles/subtitles_deser.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
#include "game/runtime.h"
#include "third-party/fmt/core.h"
#include "third-party/imgui/imgui.h"
#include "third-party/imgui/imgui_stdlib.h"
SubtitleEditor::SubtitleEditor() : m_repl(8181) {
update_subtitle_editor_db();
m_filter = m_filter_placeholder;
m_filter_hints = m_filter_placeholder;
}
void SubtitleEditor::repl_set_continue_point(const std::string_view& continue_point) {
m_repl.eval(
fmt::format("(start 'play (get-continue-by-name *game-info* \"{}\"))", continue_point));
}
void SubtitleEditor::repl_move_jak(const double x, const double y, const double z) {
m_repl.eval(
fmt::format("(move-to-point! (-> *target* control) (new 'static 'vector :x (meters {:.3f}) "
":y (meters {:.3f}) :z (meters {:.3f})))",
x, y, z));
m_repl.eval("(send-event *camera* 'teleport)");
}
void SubtitleEditor::repl_reset_game() {
m_repl.eval("(set! (-> *game-info* mode) 'debug)");
m_repl.eval("(initialize! *game-info* 'game (the-as game-save #f) (the-as string #f))");
}
std::string SubtitleEditor::repl_get_process_string(const std::string_view& entity_type,
const std::string_view& process_name) {
return fmt::format("(the-as {} (process-by-name \"{}\" *active-pool*))", entity_type,
process_name);
}
void SubtitleEditor::repl_play_hint(const std::string_view& hint_name) {
repl_reset_game();
m_repl.eval(
fmt::format("(level-hint-spawn (text-id zero) \"{}\" (the-as entity #f) *entity-pool* "
"(game-task none))",
hint_name));
}
void SubtitleEditor::repl_execute_cutscene_code(const SubtitleEditorDB::Entry& entry) {
// Reset the game first to get to a known state
repl_reset_game();
if (entry.move_first) {
// Set Jak's Continue Point
if (!entry.continue_name.empty()) {
repl_set_continue_point(entry.continue_name);
}
// Move Jak into position
if (!entry.move_to.empty()) {
repl_move_jak(entry.move_to[0], entry.move_to[1], entry.move_to[2]);
}
}
// Run any requirements to setup the task state
if (!entry.requirements.empty()) {
// Replace __GET-PROCESS__
for (const auto& form : entry.requirements) {
std::string temp = form;
temp = std::regex_replace(temp, std::regex("__GET-PROCESS__"),
repl_get_process_string(entry.entity_type, entry.process_name));
m_repl.eval(temp);
}
}
if (!entry.move_first) {
// Set Jak's Continue Point
if (!entry.continue_name.empty()) {
repl_set_continue_point(entry.continue_name);
}
// Move Jak into position
if (!entry.move_to.empty()) {
repl_move_jak(entry.move_to[0], entry.move_to[1], entry.move_to[2]);
}
}
// Execute the critical code - typically this means sending a 'play-anim event to the
// process-taskable in question
if (!entry.execute_code.empty()) {
std::string temp = entry.execute_code;
temp = std::regex_replace(temp, std::regex("__GET-PROCESS__"),
repl_get_process_string(entry.entity_type, entry.process_name));
m_repl.eval("(send-event *camera* 'teleport)");
if (entry.delay_frames == 0) {
m_repl.eval(temp);
} else {
// We do this in a separate thread to introduce a delay -- allow the game to catch up before
// running the critical section
auto code =
fmt::format("(process-spawn-function process (lambda () (dotimes (i {}) (suspend)) {}))",
entry.delay_frames, temp);
m_repl.eval(code);
}
}
}
void SubtitleEditor::repl_rebuild_text() {
m_repl.eval("(make-text)");
// increment the language id of the in-memory text file so that it won't match the current
// language and the game will want to reload it asap
m_repl.eval("(1+! (-> *subtitle-text* lang))");
}
bool SubtitleEditor::is_scene_in_current_lang(const std::string& scene_name) {
return m_subtitle_db.m_banks.at(m_current_language)->m_scenes.count(scene_name) > 0;
}
void SubtitleEditor::draw_window() {
ImGui::Begin("Subtitle Editor");
if (!db_loaded) {
if (ImGui::Button("Load Subtitles")) {
m_subtitle_db = load_subtitle_project(g_game_version);
db_loaded = true;
}
ImGui::End();
return;
}
if (ImGui::Button("Save Changes")) {
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()) {
ImGui::SameLine();
if (m_files_saved_successfully.value()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_success_text_color);
ImGui::Text("Saved!");
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Error!");
ImGui::PopStyleColor();
}
}
draw_edit_options();
draw_repl_options();
if (!m_current_scene) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
}
if (ImGui::TreeNode("Currently Selected Cutscene")) {
ImGui::PopStyleColor();
if (m_current_scene) {
draw_subtitle_options(*m_current_scene, true);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255));
ImGui::Text("Select a Scene from Below!");
ImGui::PopStyleColor();
}
ImGui::TreePop();
} else {
ImGui::PopStyleColor();
}
if (ImGui::TreeNode("All Cutscenes")) {
ImGui::InputText("New Scene Name", &m_new_scene_name);
if (ImGui::BeginCombo("Sorting Group", m_new_scene_group.c_str())) {
for (size_t i = 0; i < m_subtitle_db.m_subtitle_groups->m_group_order.size(); ++i) {
if (ImGui::Selectable(m_subtitle_db.m_subtitle_groups->m_group_order[i].c_str())) {
m_new_scene_group = m_subtitle_db.m_subtitle_groups->m_group_order[i];
}
}
ImGui::EndCombo();
}
ImGui::InputText("Filter", &m_filter, ImGuiInputTextFlags_::ImGuiInputTextFlags_AutoSelectAll);
if (is_scene_in_current_lang(m_new_scene_name)) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Scene already exists with that name, no!");
ImGui::PopStyleColor();
}
if (m_new_scene_group.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("You must provide a group to sort the scene into!");
ImGui::PopStyleColor();
}
if (!is_scene_in_current_lang(m_new_scene_name) && !m_new_scene_name.empty() &&
!m_new_scene_group.empty()) {
if (ImGui::Button("Add Scene")) {
GameSubtitleSceneInfo newScene(SubtitleSceneKind::Movie);
newScene.m_name = m_new_scene_name;
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);
if (m_add_new_scene_as_current) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->m_scenes;
auto& scene_info = scenes.at(m_new_scene_name);
m_current_scene = &scene_info;
}
m_new_scene_name = "";
}
ImGui::SameLine();
ImGui::Checkbox("Add as Current Scene", &m_add_new_scene_as_current);
ImGui::NewLine();
}
draw_all_cutscene_groups();
ImGui::TreePop();
}
if (ImGui::TreeNode("All Hints")) {
ImGui::InputText("New Scene Name", &m_new_scene_name);
ImGui::InputText("New Scene ID (hex)", &m_new_scene_id);
if (ImGui::BeginCombo("Sorting Group", m_new_scene_group.c_str())) {
for (size_t i = 0; i < m_subtitle_db.m_subtitle_groups->m_group_order.size(); ++i) {
if (ImGui::Selectable(m_subtitle_db.m_subtitle_groups->m_group_order[i].c_str())) {
m_new_scene_group = m_subtitle_db.m_subtitle_groups->m_group_order[i];
}
}
ImGui::EndCombo();
}
ImGui::InputText("Filter", &m_filter_hints,
ImGuiInputTextFlags_::ImGuiInputTextFlags_AutoSelectAll);
if (is_scene_in_current_lang(m_new_scene_name)) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Scene already exists with that name, no!");
ImGui::PopStyleColor();
}
if (m_new_scene_group.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("You must provide a group to sort the scene into!");
ImGui::PopStyleColor();
}
if (!is_scene_in_current_lang(m_new_scene_name) && !m_new_scene_name.empty() &&
!m_new_scene_group.empty()) {
if (ImGui::Button("Add Scene")) {
GameSubtitleSceneInfo newScene(SubtitleSceneKind::Hint);
newScene.m_name = m_new_scene_name;
if (m_new_scene_id == "0") {
newScene.m_kind = SubtitleSceneKind::Hint;
newScene.m_id = strtoul(m_new_scene_id.c_str(), nullptr, 16);
} else {
newScene.m_kind = SubtitleSceneKind::HintNamed;
newScene.m_id = strtoul(m_new_scene_id.c_str(), nullptr, 16);
}
// currently hints have no way in the editor to add a line, so give us one for free
newScene.add_line(0, "", "", false);
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);
m_new_scene_name = "";
}
}
draw_all_hint_groups();
ImGui::TreePop();
}
ImGui::End();
}
void SubtitleEditor::update_subtitle_editor_db() {
std::string db_path = (file_util::get_jak_project_dir() / "game" / "assets" / "jak1" /
"subtitle" / "subtitle-editor-db.json")
.string();
auto config_str = file_util::read_text_file(db_path);
auto db_data = parse_commented_json(config_str, db_path);
for (const auto& [key, val] : db_data.items()) {
auto new_entry = SubtitleEditorDB::Entry();
try {
new_entry.entity_type = val.at("entity_type").get<std::string>();
new_entry.process_name = val.at("process_name").get<std::string>();
new_entry.continue_name = val.at("continue_name").get<std::string>();
new_entry.move_to = val.at("move_to").get<std::vector<double>>();
if (val.contains("delay")) {
new_entry.delay_frames = val.at("delay").get<int>();
} else {
new_entry.delay_frames = 0;
}
if (val.contains("move_first")) {
new_entry.move_first = val.at("move_first").get<bool>();
} else {
new_entry.move_first = false;
}
if (new_entry.move_to.size() != 0 && new_entry.move_to.size() != 3) {
fmt::print("Bad subtitle db entry, provide 0 or 3 coordinates for 'move_to' - {}", key);
continue;
}
new_entry.execute_code = val.at("execute_code").get<std::string>();
new_entry.requirements = val.at("requirements").get<std::vector<std::string>>();
if (m_db.count(key) == 0) {
m_db.emplace(key, new_entry);
} else {
m_db[key] = new_entry;
}
} catch (std::exception& ex) {
fmt::print("Bad subtitle db entry - {} - {}", key, ex.what());
}
}
}
void SubtitleEditor::draw_edit_options() {
if (ImGui::TreeNode("Editing 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]->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->m_file_path).c_str(),
isSelected)) {
m_current_language = key;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
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]->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->m_file_path).c_str(),
isSelected)) {
m_base_language = key;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Checkbox("Show missing cutscenes from base", &m_base_show_missing_cutscenes);
ImGui::InputText("New Subtitle Group Name", &m_new_scene_group_name);
if (!m_new_scene_group_name.empty()) {
if (m_new_scene_group_name == "_groups" ||
std::find(m_subtitle_db.m_subtitle_groups->m_group_order.begin(),
m_subtitle_db.m_subtitle_groups->m_group_order.end(), m_new_scene_group_name) !=
m_subtitle_db.m_subtitle_groups->m_group_order.end()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Invalid group name, has to be unique and not '_groups'");
ImGui::PopStyleColor();
} else if (ImGui::Button("Add New Group")) {
m_subtitle_db.m_subtitle_groups->m_group_order.push_back(m_new_scene_group_name);
m_new_scene_group_name = "";
}
}
if (ImGui::Button("Update Editor DB")) {
update_subtitle_editor_db();
}
ImGui::TreePop();
}
}
void SubtitleEditor::draw_repl_options() {
if (ImGui::TreeNode("REPL Options")) {
// TODO - the ReplServer should eventually be able to return statuses to make this easier:
// - Has the game been built before?
// - Is the repl connected?
ImGui::TextWrapped(
"This tool requires a REPL connected to the game, with the game built. Run the following "
"to do so:");
ImGui::Text(" - `task repl`");
ImGui::Text(" - `(lt)`");
ImGui::Text(" - `(mi)`");
ImGui::Text(" - Click Connect Below!");
if (m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_success_text_color);
ImGui::Text("REPL Connected, should be good to go!");
ImGui::PopStyleColor();
} else {
if (ImGui::Button("Connect to REPL")) {
m_repl.connect();
if (!m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Could not connect.");
ImGui::PopStyleColor();
}
}
}
ImGui::TreePop();
}
}
bool SubtitleEditor::any_cutscenes_in_group(const std::string& group_name) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->m_scenes;
auto scenes_in_group = m_subtitle_db.m_subtitle_groups->m_groups[group_name];
for (auto& scene_name : scenes_in_group) {
if (scenes.count(scene_name) != 0) {
auto& scene_info = scenes.at(scene_name);
if (scene_info.m_kind == SubtitleSceneKind::Movie) {
return true;
}
}
}
return false;
}
void SubtitleEditor::draw_all_cutscene_groups() {
for (auto& group_name : m_subtitle_db.m_subtitle_groups->m_group_order) {
if (!m_filter.empty() && m_filter != m_filter_placeholder) {
ImGui::SetNextItemOpen(true);
}
if (m_subtitle_db.m_subtitle_groups->m_groups.count(group_name) == 0 ||
!any_cutscenes_in_group(group_name)) {
continue;
}
if (ImGui::TreeNode(group_name.c_str())) {
draw_all_scenes(group_name, false);
draw_all_scenes(group_name, true);
ImGui::TreePop();
}
}
}
bool SubtitleEditor::any_hints_in_group(const std::string& group_name) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->m_scenes;
auto scenes_in_group = m_subtitle_db.m_subtitle_groups->m_groups[group_name];
for (auto& scene_name : scenes_in_group) {
if (scenes.count(scene_name) != 0) {
auto& scene_info = scenes.at(scene_name);
if (scene_info.m_kind != SubtitleSceneKind::Movie) {
return true;
}
}
}
return false;
}
void SubtitleEditor::draw_all_hint_groups() {
for (auto& group_name : m_subtitle_db.m_subtitle_groups->m_group_order) {
if (!m_filter_hints.empty() && m_filter_hints != m_filter_placeholder) {
ImGui::SetNextItemOpen(true);
}
if (m_subtitle_db.m_subtitle_groups->m_groups.count(group_name) == 0 ||
!any_hints_in_group(group_name)) {
continue;
}
if (ImGui::TreeNode(group_name.c_str())) {
draw_all_hints(group_name, false);
draw_all_hints(group_name, true);
ImGui::TreePop();
}
}
}
void SubtitleEditor::draw_all_scenes(std::string group_name, bool base_cutscenes) {
auto& scenes =
m_subtitle_db.m_banks.at(base_cutscenes ? m_base_language : m_current_language)->m_scenes;
auto scenes_in_group = m_subtitle_db.m_subtitle_groups->m_groups[group_name];
for (auto& scene_name : scenes_in_group) {
if (scenes.count(scene_name) == 0) {
continue;
}
auto& scene_info = scenes.at(scene_name);
// Don't duplicate entries
if (base_cutscenes && is_scene_in_current_lang(scene_name)) {
continue;
}
bool is_current_scene = m_current_scene && m_current_scene->m_name == scene_info.m_name;
if (scene_info.m_kind != SubtitleSceneKind::Movie) {
continue;
}
if ((!m_filter.empty() && m_filter != m_filter_placeholder) &&
scene_name.find(m_filter) == std::string::npos) {
continue;
}
if (!base_cutscenes && is_current_scene) {
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
} else if (base_cutscenes) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else if (m_db.count(scene_name) == 0) {
ImGui::PushStyleColor(ImGuiCol_Text, m_warning_color);
}
if (ImGui::TreeNode(
fmt::format("{}-{}", scene_name, base_cutscenes ? m_base_language : m_current_language)
.c_str(),
"%s", scene_name.c_str())) {
if (base_cutscenes || is_current_scene || m_db.count(scene_name) == 0) {
ImGui::PopStyleColor();
}
if (!base_cutscenes && !is_current_scene) {
if (ImGui::Button("Select as Current")) {
m_current_scene = &scene_info;
}
}
if (base_cutscenes) {
if (ImGui::Button("Copy from Base Language")) {
m_subtitle_db.m_banks.at(m_current_language)->add_scene(scene_info);
}
}
draw_subtitle_options(scene_info);
ImGui::TreePop();
} else if (base_cutscenes || is_current_scene || m_db.count(scene_name) == 0) {
ImGui::PopStyleColor();
}
}
}
void SubtitleEditor::draw_all_hints(std::string group_name, bool base_cutscenes) {
auto& scenes =
m_subtitle_db.m_banks.at(base_cutscenes ? m_base_language : m_current_language)->m_scenes;
auto scenes_in_group = m_subtitle_db.m_subtitle_groups->m_groups[group_name];
for (auto& scene_name : scenes_in_group) {
if (scenes.count(scene_name) == 0) {
continue;
}
auto& scene_info = scenes.at(scene_name);
// Don't duplicate entries
if (base_cutscenes && is_scene_in_current_lang(scene_name)) {
continue;
}
if (scene_info.m_kind != SubtitleSceneKind::Hint &&
scene_info.m_kind != SubtitleSceneKind::HintNamed) {
continue;
}
if ((!m_filter_hints.empty() && m_filter_hints != m_filter_placeholder) &&
scene_name.find(m_filter_hints) == std::string::npos) {
continue;
}
if (base_cutscenes) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
}
if (ImGui::TreeNode(
fmt::format("{}-{}", scene_name, base_cutscenes ? m_base_language : m_current_language)
.c_str(),
"%s", scene_name.c_str())) {
if (base_cutscenes) {
ImGui::PopStyleColor();
}
if (base_cutscenes) {
if (ImGui::Button("Copy from Base Language")) {
m_subtitle_db.m_banks.at(m_current_language)->add_scene(scene_info);
}
}
draw_subtitle_options(scene_info);
ImGui::TreePop();
} else if (base_cutscenes) {
ImGui::PopStyleColor();
}
}
}
void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool current_scene) {
if (!m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("REPL not connected, can't play!");
ImGui::PopStyleColor();
} else {
// Cutscenes
if (scene.m_kind == SubtitleSceneKind::Movie && m_db.count(scene.m_name) > 0) {
if (ImGui::Button("Play Scene")) {
update_subtitle_editor_db();
repl_execute_cutscene_code(m_db[scene.m_name]);
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
ImGui::TextWrapped("You may have to click twice, load times cause issues");
ImGui::PopStyleColor();
ImGui::NewLine();
}
// Hints
else if (scene.m_kind == SubtitleSceneKind::Hint ||
scene.m_kind == SubtitleSceneKind::HintNamed) {
if (ImGui::Button("Play Hint")) {
repl_play_hint(scene.m_name);
}
// TODO add "Remove Hint" button (if you named it wrong or something)
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
ImGui::TextWrapped("You may have to click twice, load times cause issues");
ImGui::PopStyleColor();
ImGui::NewLine();
}
}
if (ImGui::BeginCombo("Sorting Group", scene.m_sorting_group.c_str())) {
for (size_t i = 0; i < m_subtitle_db.m_subtitle_groups->m_group_order.size(); ++i) {
const bool isSelected = (scene.m_sorting_group_idx == (int)i);
if (ImGui::Selectable(m_subtitle_db.m_subtitle_groups->m_group_order[i].c_str(),
isSelected)) {
// Remove from current group
m_subtitle_db.m_subtitle_groups->remove_scene(scene.m_sorting_group, scene.m_name);
// Add to new group
scene.m_sorting_group_idx = i;
scene.m_sorting_group = m_subtitle_db.m_subtitle_groups->m_group_order.at(i);
m_subtitle_db.m_subtitle_groups->add_scene(scene.m_sorting_group, scene.m_name);
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (current_scene) {
draw_new_cutscene_line_form();
}
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 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, line_speaker,
linetext.substr(0, 30));
} else {
summary =
fmt::format("[{}] {} - '{}'", subtitleLine->frame, line_speaker, linetext.substr(0, 30));
}
if (linetext.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else if (subtitleLine->offscreen) {
ImGui::PushStyleColor(ImGuiCol_Text, m_offscreen_text_color);
}
if (ImGui::TreeNode(fmt::format("{}", i).c_str(), "%s", summary.c_str())) {
if (linetext.empty() || subtitleLine->offscreen) {
ImGui::PopStyleColor();
}
ImGui::InputInt("Starting Frame", &subtitleLine->frame,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
// 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
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
if (ImGui::Button("Remove")) {
subtitleLine = scene.m_lines.erase(subtitleLine);
ImGui::PopStyleColor();
ImGui::TreePop();
continue;
}
ImGui::PopStyleColor();
}
ImGui::TreePop();
} else if (linetext.empty() || subtitleLine->offscreen) {
ImGui::PopStyleColor();
}
auto newtext = font->convert_utf8_to_game(linetext, true);
auto newspkr = font->convert_utf8_to_game(line_speaker, true);
subtitleLine->line = newtext;
subtitleLine->speaker = newspkr;
i++;
subtitleLine++;
}
}
void SubtitleEditor::draw_new_cutscene_line_form() {
ImGui::InputInt("Frame Number", &m_current_scene_frame,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
ImGui::InputText("Speaker", &m_current_scene_speaker);
ImGui::InputText("Text", &m_current_scene_text);
ImGui::Checkbox("Offscreen", &m_current_scene_offscreen);
bool rendered_text_entry_btn = false;
if (m_current_scene_frame < 0 || m_current_scene_text.empty() ||
m_current_scene_speaker.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Can't add a new text entry with the current fields!");
ImGui::PopStyleColor();
} else {
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]->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);
}
}
if (m_current_scene_frame < 0) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Can't add a clear screen entry with the current fields!");
ImGui::PopStyleColor();
} else {
if (rendered_text_entry_btn) {
ImGui::SameLine();
}
if (ImGui::Button("Add Clear Screen Entry")) {
m_current_scene->add_line(m_current_scene_frame, "", "", false);
}
}
ImGui::NewLine();
}
-100
View File
@@ -1,100 +0,0 @@
#pragma once
#include <optional>
#include <string_view>
#include "common/repl/nrepl/ReplClient.h"
#include "common/serialization/subtitles/subtitles_ser.h"
#include "third-party/imgui/imgui.h"
class SubtitleEditorDB {
public:
struct Entry {
std::string entity_type;
std::string process_name;
std::string continue_name;
std::vector<double> move_to;
int delay_frames;
std::string execute_code;
bool move_first;
std::vector<std::string> requirements;
};
};
// TODO Later:
// - Hints, these seem less annoying but there are a lot of them
class SubtitleEditor {
public:
SubtitleEditor();
void draw_window();
private:
void update_subtitle_editor_db();
void draw_edit_options();
void draw_repl_options();
bool any_cutscenes_in_group(const std::string& group_name);
bool any_hints_in_group(const std::string& group_name);
void draw_all_cutscene_groups();
void draw_all_scenes(std::string group_name, bool base_cutscenes = false);
void draw_subtitle_options(GameSubtitleSceneInfo& scene, bool current_scene = false);
void draw_new_cutscene_line_form();
void draw_all_hint_groups();
void draw_all_hints(std::string group_name, bool base_cutscenes);
bool db_loaded = false;
GameSubtitleDB m_subtitle_db;
std::map<std::string, SubtitleEditorDB::Entry> m_db = {};
GameSubtitleSceneInfo* m_current_scene = nullptr;
std::string m_filter;
std::string m_filter_hints;
ReplClient m_repl;
int m_current_scene_frame = 0;
std::string m_current_scene_text = "";
std::string m_current_scene_speaker = "";
bool m_current_scene_offscreen = false;
bool m_add_new_scene_as_current = false;
std::string m_new_scene_name = "";
std::string m_new_scene_group = "";
std::string m_new_scene_id = "0";
std::string m_new_scene_group_name = "";
std::string m_filter_placeholder = "Filter List...";
std::optional<bool> m_files_saved_successfully = {};
int m_base_language = 0;
int m_current_language = 0;
// bool m_base_show_lines = false;
bool m_base_show_missing_cutscenes = true;
// TODO - let the user customize these colors
ImVec4 m_normal_text_color = ImVec4(1.0f, 0.0f, 1.0f, 1.0f);
int m_selected_text_color = IM_COL32(89, 227, 225, 255);
ImVec4 m_success_text_color = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
ImVec4 m_error_text_color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
ImVec4 m_disabled_text_color = ImVec4(1.0f, 1.0f, 1.0f, 0.7f);
ImVec4 m_warning_color = ImVec4(0.619f, 0.443f, 0.0f, 1.0f);
int m_offscreen_text_color = IM_COL32(240, 242, 102, 255);
// TODO - cycle speaker colors
void repl_set_continue_point(const std::string_view& continue_point);
void repl_move_jak(const double x, const double y, const double z);
void repl_reset_game();
std::string repl_get_process_string(const std::string_view& entity_type,
const std::string_view& process_name);
void repl_execute_cutscene_code(const SubtitleEditorDB::Entry& entry);
void repl_rebuild_text();
void repl_play_hint(const std::string_view& hint_name);
bool is_scene_in_current_lang(const std::string& scene_name);
};
-436
View File
@@ -1,436 +0,0 @@
#include "subtitle2_editor.h"
#include <algorithm>
#include "common/serialization/subtitles2/subtitles2_deser.h"
#include "common/util/FileUtil.h"
#include "common/util/json_util.h"
#include "common/util/string_util.h"
#include "game/runtime.h"
#include "third-party/fmt/core.h"
#include "third-party/imgui/imgui.h"
#include "third-party/imgui/imgui_stdlib.h"
// static constexpr size_t LINE_DISPLAY_MAX_LEN = 38;
Subtitle2Editor::Subtitle2Editor(GameVersion version)
: db_loaded(true),
m_subtitle_db(load_subtitle2_project(version)),
m_repl(8182),
m_speaker_names(get_speaker_names(version)) {
m_filter = m_filter_placeholder;
m_filter_hints = m_filter_placeholder;
}
bool Subtitle2Editor::is_scene_in_current_lang(const std::string& scene_name) {
return m_subtitle_db.m_banks.at(m_current_language)->scenes.count(scene_name) > 0;
}
void Subtitle2Editor::repl_rebuild_text() {
// reload subtitles immediately
m_repl.eval("(reload-subtitles)");
}
void Subtitle2Editor::repl_play_vag(const std::string& name, bool is_scene) {
if (is_scene) {
m_repl.eval(fmt::format("(scene-find-and-play \"{}\")", name));
} else {
m_repl.eval(fmt::format("(vag-player-play-from-name \"{}\")", name));
}
}
void Subtitle2Editor::draw_window() {
ImGui::Begin("Subtitle2 Editor");
if (!db_loaded) {
if (ImGui::Button("Load Subtitles")) {
m_subtitle_db = load_subtitle2_project(g_game_version);
db_loaded = true;
}
ImGui::End();
return;
}
if (ImGui::Button("Save Changes")) {
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()) {
ImGui::SameLine();
if (m_files_saved_successfully.value()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_success_text_color);
ImGui::Text("Saved!");
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Error!");
ImGui::PopStyleColor();
}
}
draw_edit_options();
draw_repl_options();
draw_speaker_options();
if (!m_current_scene) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
}
if (ImGui::TreeNode("Currently Selected Cutscene")) {
ImGui::Text("%s", m_current_scene_name.c_str());
ImGui::SameLine();
ImGui::Checkbox("Cutscene?", &m_current_scene->scene);
ImGui::PopStyleColor();
if (m_current_scene) {
draw_subtitle_options(*m_current_scene, m_current_scene_name, true);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255));
ImGui::Text("Select a Scene from Below!");
ImGui::PopStyleColor();
}
ImGui::TreePop();
} else {
ImGui::PopStyleColor();
}
if (ImGui::TreeNode("All Cutscenes")) {
ImGui::InputText("New Scene Name", &m_new_scene_name);
ImGui::InputText("Filter", &m_filter, ImGuiInputTextFlags_::ImGuiInputTextFlags_AutoSelectAll);
if (is_scene_in_current_lang(m_new_scene_name)) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Scene already exists with that name, no!");
ImGui::PopStyleColor();
}
if (!is_scene_in_current_lang(m_new_scene_name) && !m_new_scene_name.empty()) {
if (ImGui::Button("Add Scene")) {
Subtitle2Scene new_scene;
m_subtitle_db.m_banks.at(m_current_language)->add_scene(m_new_scene_name, new_scene);
if (m_add_new_scene_as_current) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->scenes;
auto& scene_info = scenes.at(m_new_scene_name);
m_current_scene = &scene_info;
m_current_scene_name = m_new_scene_name;
}
m_new_scene_name = "";
}
ImGui::SameLine();
ImGui::Checkbox("Add as Current Scene", &m_add_new_scene_as_current);
}
draw_all_scenes();
ImGui::TreePop();
}
ImGui::End();
}
void Subtitle2Editor::draw_edit_options() {
if (ImGui::TreeNode("Editing Options")) {
if (ImGui::BeginCombo("Editor Language ID",
fmt::format("[{}] {}", m_subtitle_db.m_banks[m_current_language]->lang,
m_subtitle_db.m_banks[m_current_language]->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->lang, value->file_path).c_str(),
isSelected)) {
m_current_language = key;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (ImGui::BeginCombo("Base Language ID",
fmt::format("[{}] {}", m_subtitle_db.m_banks[m_base_language]->lang,
m_subtitle_db.m_banks[m_base_language]->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->lang, value->file_path).c_str(),
isSelected)) {
m_base_language = key;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Checkbox("Show missing cutscenes from base", &m_base_show_missing_cutscenes);
ImGui::TreePop();
}
}
void Subtitle2Editor::draw_repl_options() {
if (ImGui::TreeNode("REPL Options")) {
// TODO - the ReplServer should eventually be able to return statuses to make this easier:
// - Has the game been built before?
// - Is the repl connected?
ImGui::TextWrapped(
"This tool requires a REPL connected to the game, with the game built. Run the following "
"to do so:");
ImGui::Text(" - `task repl`");
ImGui::Text(" - `(lt)`");
ImGui::Text(" - `(mi)`");
ImGui::Text(" - Click Connect Below!");
if (m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_success_text_color);
ImGui::Text("REPL Connected, should be good to go!");
ImGui::PopStyleColor();
} else {
if (ImGui::Button("Connect to REPL")) {
m_repl.connect();
if (!m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Could not connect.");
ImGui::PopStyleColor();
}
}
}
ImGui::TreePop();
}
}
void Subtitle2Editor::draw_speaker_options() {
if (ImGui::TreeNode("Speakers")) {
const auto bank = m_subtitle_db.m_banks[m_current_language];
for (auto& speaker_name : m_speaker_names) {
// ImGui::SameLine();
if (bank->speakers.count(speaker_name) == 0) {
// no speaker yet.
std::string input = "";
ImGui::InputText(speaker_name.c_str(), &input);
if (!input.empty()) {
// speaker got filled
bank->speakers.insert({speaker_name, input});
}
} else {
// existing speaker
std::string input = bank->speakers.at(speaker_name);
if (ImGui::InputText(speaker_name.c_str(), &input)) {
if (input.empty()) {
// speaker got deleted
bank->speakers.erase(speaker_name);
} else {
// speaker got changed
bank->speakers.at(speaker_name) = input;
}
}
}
}
ImGui::TreePop();
}
}
void Subtitle2Editor::draw_all_scenes(bool base_cutscenes) {
auto& scenes =
m_subtitle_db.m_banks.at(base_cutscenes ? m_base_language : m_current_language)->scenes;
std::unordered_set<std::string> to_delete;
for (auto& [name, scene] : scenes) {
// Don't duplicate entries
if (base_cutscenes && is_scene_in_current_lang(name)) {
continue;
}
bool is_current_scene = m_current_scene && m_current_scene_name == name;
if ((!m_filter.empty() && m_filter != m_filter_placeholder) &&
name.find(m_filter) == std::string::npos) {
continue;
}
bool color_pushed = false;
if (!base_cutscenes && is_current_scene) {
ImGui::PushStyleColor(ImGuiCol_Text, m_selected_text_color);
color_pushed = true;
} else if (base_cutscenes) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
color_pushed = true;
}
if (ImGui::TreeNode(
fmt::format("{}-{}", name, base_cutscenes ? m_base_language : m_current_language)
.c_str(),
"%s", name.c_str())) {
if (color_pushed) {
ImGui::PopStyleColor();
}
if (!base_cutscenes && !is_current_scene) {
if (ImGui::Button("Select as Current")) {
m_current_scene = &scene;
m_current_scene_name = name;
}
}
if (base_cutscenes) {
if (ImGui::Button("Copy from Base Language")) {
m_subtitle_db.m_banks.at(m_current_language)->add_scene(name, scene);
}
}
draw_subtitle_options(scene, name);
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
if (ImGui::Button("Delete")) {
if (&scene == m_current_scene || name == m_current_scene_name) {
m_current_scene = nullptr;
m_current_scene_name = "";
}
to_delete.insert(name);
}
ImGui::PopStyleColor();
ImGui::TreePop();
} else if (color_pushed) {
ImGui::PopStyleColor();
}
}
for (auto& name : to_delete) {
scenes.erase(name);
}
}
void Subtitle2Editor::draw_subtitle_options(Subtitle2Scene& scene,
const std::string& name,
bool current_scene) {
if (!m_repl.is_connected()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("REPL not connected, can't play!");
ImGui::PopStyleColor();
} else {
// Cutscenes
if (ImGui::Button("Play Scene")) {
repl_play_vag(name, scene.scene);
}
}
if (current_scene) {
draw_new_cutscene_line_form();
}
const auto bank = m_subtitle_db.m_banks[m_current_language];
int i = 0;
for (auto line = scene.lines.begin(); line != scene.lines.end();) {
float times[2] = {line->start, line->end};
bool speaker_exists = bank->speakers.count(line->speaker) != 0;
auto speaker_text = !speaker_exists ? "N/A" : bank->speakers.at(line->speaker);
std::string full_line = line->text;
if (speaker_exists) {
full_line = speaker_text + ": " + full_line;
}
auto summary = fmt::format("[{} - {}] {}", line->start, line->end, full_line);
if (line->text.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else if (line->offscreen) {
ImGui::PushStyleColor(ImGuiCol_Text, m_offscreen_text_color);
}
if (ImGui::TreeNode(fmt::format("{}", i).c_str(), "%s", summary.c_str())) {
if (line->text.empty() || line->offscreen) {
ImGui::PopStyleColor();
}
ImGui::InputFloat2("Start and End Frame", times, "%.0f",
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
if (ImGui::BeginCombo("Speaker",
fmt::format("{} ({})", speaker_text.c_str(), line->speaker).c_str())) {
const bool isSelected = line->speaker == "none";
if (ImGui::Selectable("none", isSelected)) {
line->speaker = "none";
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
for (auto& speaker_name : m_speaker_names) {
if (bank->speakers.count(speaker_name) == 0) {
continue;
}
const bool isSelected = line->speaker == speaker_name;
if (ImGui::Selectable(
fmt::format("{} ({})", bank->speakers.at(speaker_name), speaker_name).c_str(),
isSelected)) {
line->speaker = speaker_name;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::InputText("Text", &line->text);
ImGui::Checkbox("Offscreen?", &line->offscreen);
ImGui::SameLine();
ImGui::Checkbox("Merge text?", &line->merge);
if (scene.lines.size() > 1) { // prevent creating an empty scene
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
if (ImGui::Button("Delete")) {
line = scene.lines.erase(line);
ImGui::PopStyleColor();
ImGui::TreePop();
continue;
}
ImGui::PopStyleColor();
}
ImGui::TreePop();
} else if (line->text.empty() || line->offscreen) {
ImGui::PopStyleColor();
}
line->start = times[0];
line->end = times[1];
i++;
line++;
}
}
void Subtitle2Editor::draw_new_cutscene_line_form() {
auto bank = m_subtitle_db.m_banks[m_current_language];
ImGui::InputFloat2("Start and End Frame", m_current_scene_frame, "%.0f",
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
const auto& speakers = bank->speakers;
if (speakers.count(m_current_scene_speaker) == 0) {
// pick whatever the first one it finds is
m_current_scene_speaker = "none";
}
if (ImGui::BeginCombo("Speaker",
m_current_scene_speaker == "none"
? "none"
: fmt::format("{} ({})", speakers.at(m_current_scene_speaker),
m_current_scene_speaker)
.c_str())) {
const bool isSelected = m_current_scene_speaker == "none";
if (ImGui::Selectable("none", isSelected)) {
m_current_scene_speaker = "none";
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
for (auto& speaker_name : m_speaker_names) {
if (speakers.count(speaker_name) == 0) {
continue;
}
const bool isSelected = m_current_scene_speaker == speaker_name;
if (ImGui::Selectable(fmt::format("{} ({})", speakers.at(speaker_name), speaker_name).c_str(),
isSelected)) {
m_current_scene_speaker = speaker_name;
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::InputText("Text", &m_current_scene_text);
ImGui::Checkbox("Offscreen?", &m_current_scene_offscreen);
ImGui::SameLine();
ImGui::Checkbox("Merge text?", &m_current_scene_merge);
if (m_current_scene_frame[0] < 0 || m_current_scene_frame[1] < 0 ||
(m_current_scene_text.empty() && !m_current_scene_merge)) {
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
ImGui::Text("Can't add a new text entry with the current fields!");
ImGui::PopStyleColor();
} else {
if (ImGui::Button("Add Text Entry")) {
m_current_scene->lines.emplace_back(m_current_scene_frame[0], m_current_scene_frame[1],
m_current_scene_text, m_current_scene_speaker,
m_current_scene_offscreen, m_current_scene_merge);
// TODO - sorting after every insertion is slow, sort on the add scene instead
std::sort(m_current_scene->lines.begin(), m_current_scene->lines.end());
}
}
}
-75
View File
@@ -1,75 +0,0 @@
#pragma once
#include <optional>
#include <string_view>
#include "common/repl/nrepl/ReplClient.h"
#include "common/serialization/subtitles2/subtitles2_ser.h"
#include "third-party/imgui/imgui.h"
// TODO Later:
// - Hints, these seem less annoying but there are a lot of them
class Subtitle2Editor {
public:
Subtitle2Editor(GameVersion version);
void draw_window();
private:
void draw_edit_options();
void draw_repl_options();
void draw_speaker_options();
void draw_all_scenes(bool base_cutscenes = false);
void draw_subtitle_options(Subtitle2Scene& scene,
const std::string& name,
bool current_scene = false);
void draw_new_cutscene_line_form();
bool db_loaded = false;
GameSubtitle2DB m_subtitle_db;
Subtitle2Scene* m_current_scene = nullptr;
std::string m_current_scene_name = "";
std::string m_filter;
std::string m_filter_hints;
ReplClient m_repl;
float m_current_scene_frame[2] = {0, 0};
std::string m_current_scene_text = "";
std::string m_current_scene_speaker = "";
bool m_current_scene_offscreen = false;
bool m_current_scene_merge = false;
bool m_add_new_scene_as_current = false;
std::string m_new_scene_name = "";
std::string m_new_scene_id = "0";
std::string m_filter_placeholder = "Filter List...";
std::optional<bool> m_files_saved_successfully = {};
int m_base_language = 0;
int m_current_language = 0;
// bool m_base_show_lines = false;
bool m_base_show_missing_cutscenes = true;
// TODO - let the user customize these colors
ImVec4 m_normal_text_color = ImVec4(1.0f, 0.0f, 1.0f, 1.0f);
int m_selected_text_color = IM_COL32(89, 227, 225, 255);
ImVec4 m_success_text_color = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
ImVec4 m_error_text_color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
ImVec4 m_disabled_text_color = ImVec4(1.0f, 1.0f, 1.0f, 0.7f);
ImVec4 m_warning_color = ImVec4(0.619f, 0.443f, 0.0f, 1.0f);
int m_offscreen_text_color = IM_COL32(240, 242, 102, 255);
// TODO - cycle speaker colors
const std::vector<std::string> m_speaker_names;
void repl_rebuild_text();
void repl_play_vag(const std::string& name, bool is_scene);
bool is_scene_in_current_lang(const std::string& scene_name);
};
+4 -3
View File
@@ -737,9 +737,10 @@
(new 'static 'sound-id)
)
)
((subtitle-image)
(draw-subtitle-image (the-as subtitle-image s3-0) s2-0)
)
;; TODO - crashes the game
;; ((subtitle-image)
;; (draw-subtitle-image (the-as subtitle-image s3-0) s2-0)
;; )
(else
(if *debug-segment*
(format *stdcon* "unknown message ~A~%" s3-0)
+1 -1
View File
@@ -382,7 +382,7 @@
)
(defstep :in "game/assets/jak2/game_subtitle.gp"
:tool 'subtitle2
:tool 'subtitle-v2
:out '("$OUT/iso/0SUBTI2.TXT")
)
+1 -1
View File
@@ -172,7 +172,7 @@
(defmacro reload-subtitles ()
"rebuild and reload subtitles."
`(begin
(asm-text-file subtitle2 :files ("game/assets/jak2/game_subtitle.gp"))
(asm-text-file subtitle-v2 :files ("game/assets/jak2/game_subtitle.gp"))
(if *subtitle2-text*
(+! (-> *subtitle2-text* lang) 1))
(load-level-subtitle2-files 0)))
+6 -16
View File
@@ -89,7 +89,7 @@ Val* Compiler::compile_asm_text_file(const goos::Object& form, const goos::Objec
const auto kind = symbol_string(args.unnamed.at(0));
// compile files.
if (kind == "subtitle") {
if (kind == "subtitle" || kind == "subtitle-v2") {
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) {
@@ -100,21 +100,11 @@ Val* Compiler::compile_asm_text_file(const goos::Object& form, const goos::Objec
}
});
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 == "subtitle2") {
std::vector<GameSubtitle2DefinitionFile> 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_subtitle2_project(kind, o.as_string()->data, inputs);
} else {
throw_compiler_error(form, "Invalid object {} in asm-text-file files list.", o.print());
}
});
GameSubtitle2DB db(m_version);
compile_game_subtitle2(inputs, db, m_make.compiler_output_prefix());
if (kind == "subtitle") {
} else {
db.m_subtitle_version = GameSubtitleDB::SubtitleFormat::V2;
}
compile_game_subtitles(inputs, db, m_make.compiler_output_prefix());
} else if (kind == "text") {
std::vector<GameTextDefinitionFile> inputs;
// open all project files specified (usually one).
+58 -80
View File
@@ -24,10 +24,13 @@
#include "common/util/FontUtils.h"
#include "common/util/json_util.h"
#include "game/runtime.h"
#include "third-party/fmt/core.h"
namespace {
// TODO - replace with str_util::to_upper?
std::string uppercase(const std::string& in) {
std::string result;
result.reserve(in.size());
@@ -94,43 +97,40 @@ void compile_text(GameTextDB& db, const std::string& output_prefix) {
* Write game subtitle data to a file. Uses the V2 object format which is identical between GOAL and
* OpenGOAL.
*/
void compile_subtitle(GameSubtitleDB& db, const std::string& output_prefix) {
for (const auto& [lang, bank] : db.banks()) {
void compile_subtitles_v1(GameSubtitleDB& db, const std::string& output_prefix) {
for (const auto& [lang, bank] : db.m_banks) {
auto font = get_font_bank(bank->m_text_version);
DataObjectGenerator gen;
gen.add_type_tag("subtitle-text-info"); // type
gen.add_word(bank->scenes().size()); // length
gen.add_word(bank->m_scenes.size()); // length
gen.add_word(lang); // lang
gen.add_word(0); // dummy
// fifo queue for scene data arrays
std::queue<int> array_link_sources;
// now add all the scene infos
for (auto& [name, scene] : bank->scenes()) {
gen.add_word((u16)scene.kind() |
(scene.lines().size() << 16)); // kind (lower 16 bits), length (upper 16 bits)
for (auto& [name, scene] : bank->m_scenes) {
gen.add_word((u16)(scene.is_cutscene ? 0 : 2) |
(scene.m_lines.size() << 16)); // kind (lower 16 bits), length (upper 16 bits)
array_link_sources.push(gen.words());
gen.add_word(0); // keyframes (linked later)
if (scene.kind() == SubtitleSceneKind::Movie ||
scene.kind() == SubtitleSceneKind::HintNamed) {
gen.add_ref_to_string_in_pool(scene.name()); // name
} else if (scene.kind() == SubtitleSceneKind::Hint) {
gen.add_word(0); // nothing
}
gen.add_word(scene.id());
gen.add_ref_to_string_in_pool(name);
gen.add_word(scene.m_hint_id);
}
// now add all the scene *data!* (keyframes)
for (auto& [name, scene] : bank->scenes()) {
for (auto& [name, scene] : bank->m_scenes) {
// link inline-array with reference from earlier
gen.link_word_to_word(array_link_sources.front(), gen.words());
array_link_sources.pop();
for (auto& subtitle : scene.lines()) {
gen.add_word(subtitle.frame); // frame
gen.add_ref_to_string_in_pool(subtitle.line); // line
gen.add_ref_to_string_in_pool(subtitle.speaker); // speaker
gen.add_word(subtitle.offscreen); // offscreen
for (auto& subtitle : scene.m_lines) {
gen.add_word(subtitle.metadata.frame_start); // frame
gen.add_ref_to_string_in_pool(font->convert_utf8_to_game(subtitle.text)); // line
gen.add_ref_to_string_in_pool(
font->convert_utf8_to_game(subtitle.metadata.speaker)); // speaker
gen.add_word(subtitle.metadata.offscreen); // offscreen
}
}
@@ -148,67 +148,54 @@ void compile_subtitle(GameSubtitleDB& db, const std::string& output_prefix) {
* Write game subtitle2 data to a file. Uses the V2 object format which is identical between GOAL
* and OpenGOAL.
*/
void compile_subtitle2(GameSubtitle2DB& db, const std::string& output_prefix) {
auto& speaker_names = get_speaker_names(db.version());
for (const auto& [lang, bank] : db.banks()) {
auto font = get_font_bank(bank->text_version);
void compile_subtitles_v2(GameSubtitleDB& db, const std::string& output_prefix) {
for (const auto& [lang, bank] : db.m_banks) {
auto font = get_font_bank(bank->m_text_version);
DataObjectGenerator gen;
gen.add_type_tag("subtitle2-text-info"); // type
gen.add_word((bank->scenes.size() & 0xffff) | (1 << 16)); // length (lo) + version (hi)
gen.add_type_tag("subtitle2-text-info"); // type
gen.add_word((bank->m_scenes.size() & 0xffff) | (1 << 16)); // length (lo) + version (hi)
// note: we add 1 because "none" isn't included
gen.add_word((lang & 0xffff) | ((speaker_names.size() + 1) << 16)); // lang + speaker-length
gen.add_word((lang & 0xffff) | ((bank->m_speakers.size() + 1) << 16)); // lang + speaker-length
int speaker_array_link = gen.add_word(0); // speaker array (dummy for now)
auto speaker_index_by_name = [&speaker_names](const std::string& name) {
for (int i = 0; i < (int)speaker_names.size(); ++i) {
if (speaker_names.at(i) == name) {
return i + 1;
}
}
return 0;
};
// fifo queue for scene data arrays
std::queue<int> array_link_sources;
// now add all the scenes inline
for (auto& [name, scene] : bank->scenes) {
for (auto& [name, scene] : bank->m_scenes) {
gen.add_ref_to_string_in_pool(name); // scene name
gen.add_word(scene.lines.size()); // line amount
gen.add_word(scene.m_lines.size()); // line amount
array_link_sources.push(gen.words());
gen.add_word(0); // line array (linked later)
}
// now add all the line arrays and link them to their scene
for (auto& [name, scene] : bank->scenes) {
for (auto& [name, scene] : bank->m_scenes) {
// link inline-array with reference from earlier
gen.link_word_to_word(array_link_sources.front(), gen.words());
array_link_sources.pop();
for (auto& line : scene.lines) {
gen.add_word_float(line.start); // start frame
gen.add_word_float(line.end); // end frame
if (!line.merge) {
gen.add_ref_to_string_in_pool(font->convert_utf8_to_game(line.text)); // line text
} else {
for (auto& line : scene.m_lines) {
gen.add_word_float(static_cast<float>(line.metadata.frame_start)); // start frame
gen.add_word_float(static_cast<float>(line.metadata.frame_end)); // end frame
if (line.metadata.merge) {
gen.add_symbol_link("#f");
} else {
gen.add_ref_to_string_in_pool(font->convert_utf8_to_game(line.text)); // line text
}
u16 speaker = speaker_index_by_name(line.speaker);
u16 speaker = bank->speaker_enum_value_from_name(line.metadata.speaker);
u16 flags = 0;
flags |= line.offscreen << 0;
flags |= line.merge << 1;
flags |= line.metadata.offscreen << 0;
flags |= line.metadata.merge << 1;
gen.add_word(speaker | (flags << 16)); // speaker (lo) + flags (hi)
}
}
// now write the array of strings for the speakers
// key word array -- it has to be in the right order.
gen.link_word_to_word(speaker_array_link, gen.words());
// we write #f for invalid entries, including the "none" at the start
gen.add_symbol_link("#f");
for (auto& speaker_name : speaker_names) {
if (bank->speakers.count(speaker_name) == 0) {
// no speaker for this
gen.add_symbol_link("#f");
} else {
gen.add_ref_to_string_in_pool(font->convert_utf8_to_game(bank->speakers.at(speaker_name)));
}
const auto localized_speakers = bank->speaker_names_ordered_by_enum_value();
for (auto& speaker_localized : localized_speakers) {
// No need to check for invalid speakers here, they are checked at the scene line level above
// and throw an error
gen.add_ref_to_string_in_pool(font->convert_utf8_to_game(speaker_localized));
}
auto data = gen.generate_v2();
@@ -233,7 +220,7 @@ void compile_game_text(const std::vector<GameTextDefinitionFile>& files,
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);
parse_text_goal(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;
@@ -244,30 +231,21 @@ void compile_game_text(const std::vector<GameTextDefinitionFile>& files,
compile_text(db, output_prefix);
}
void compile_game_subtitle(const std::vector<GameSubtitleDefinitionFile>& files,
GameSubtitleDB& db,
const std::string& output_prefix) {
goos::Reader reader;
for (auto& file : files) {
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);
}
void compile_game_subtitle2(const std::vector<GameSubtitle2DefinitionFile>& files,
GameSubtitle2DB& db,
void compile_game_subtitles(const std::vector<GameSubtitleDefinitionFile>& files,
GameSubtitleDB& db,
const std::string& output_prefix) {
goos::Reader reader;
for (auto& file : files) {
lg::print("[Build Game Subtitle] JSON {}\n", file.file_path);
parse_subtitle2_json(db, file);
if (db.m_subtitle_version == GameSubtitleDB::SubtitleFormat::V1) {
for (auto& file : files) {
lg::print("[Build Game Subtitle V1] {}:{}\n", file.lines_path, file.meta_path);
db.init_banks_from_file(file);
}
compile_subtitles_v1(db, output_prefix);
} else {
for (auto& file : files) {
lg::print("[Build Game Subtitle V2] {}:{}\n", file.lines_path, file.meta_path);
db.init_banks_from_file(file);
}
compile_subtitles_v2(db, output_prefix);
}
compile_subtitle2(db, output_prefix);
}
+6 -7
View File
@@ -5,17 +5,16 @@
#include <unordered_map>
#include <unordered_set>
#include "common/serialization/subtitles/subtitles_ser.h"
#include "common/serialization/subtitles2/subtitles2_ser.h"
#include "common/serialization/subtitles/subtitles.h"
#include "common/serialization/subtitles/subtitles_v1.h"
#include "common/serialization/subtitles/subtitles_v2.h"
#include "common/serialization/text/text_ser.h"
#include "common/util/Assert.h"
#include "common/util/FontUtils.h"
void compile_game_text(const std::vector<GameTextDefinitionFile>& filenames,
GameTextDB& db,
const std::string& output_prefix);
void compile_game_subtitle(const std::vector<GameSubtitleDefinitionFile>& filenames,
GameSubtitleDB& db,
const std::string& output_prefix);
void compile_game_subtitle2(const std::vector<GameSubtitle2DefinitionFile>& filenames,
GameSubtitle2DB& db,
void compile_game_subtitles(const std::vector<GameSubtitleDefinitionFile>& filenames,
GameSubtitleDB& db,
const std::string& output_prefix);
+1 -1
View File
@@ -100,7 +100,7 @@ MakeSystem::MakeSystem(const std::optional<REPL::Config> repl_config, const std:
add_tool<GroupTool>();
add_tool<TextTool>();
add_tool<SubtitleTool>();
add_tool<Subtitle2Tool>();
add_tool<SubtitleV2Tool>();
add_tool<BuildLevelTool>();
}
+50 -47
View File
@@ -167,77 +167,80 @@ bool GroupTool::run(const ToolInput&, const PathMap& /*path_map*/) {
return true;
}
void enumerate_subtitle_project_files(const std::string& tool_name,
const std::string& file_path,
const PathMap& path_map,
std::vector<GameSubtitleDefinitionFile>& files,
std::vector<std::string>& deps) {
open_subtitle_project(tool_name, file_path, files);
for (auto& file : files) {
deps.push_back(path_map.apply_remaps(file.lines_path));
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()));
}
}
}
void run_subtitle_project_files(const std::string& tool_name,
const std::string& file_path,
const PathMap& path_map,
std::vector<GameSubtitleDefinitionFile>& files) {
open_subtitle_project(tool_name, file_path, files);
for (auto& file : files) {
file.lines_path = path_map.apply_remaps(file.lines_path);
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());
}
}
}
SubtitleTool::SubtitleTool() : Tool("subtitle") {}
bool SubtitleTool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
std::vector<std::string> deps;
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.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()));
}
}
}
std::vector<std::string> deps;
enumerate_subtitle_project_files(name(), task.input.at(0), path_map, files, deps);
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
}
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();
db.m_subtitle_version = GameSubtitleDB::SubtitleFormat::V1;
std::vector<GameSubtitleDefinitionFile> files;
open_subtitle_project("subtitle", task.input.at(0), files);
for (auto& file : files) {
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);
run_subtitle_project_files(name(), task.input.at(0), path_map, files);
compile_game_subtitles(files, db, path_map.output_prefix);
return true;
}
Subtitle2Tool::Subtitle2Tool() : Tool("subtitle2") {}
SubtitleV2Tool::SubtitleV2Tool() : Tool("subtitle-v2") {}
bool Subtitle2Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
bool SubtitleV2Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
std::vector<GameSubtitleDefinitionFile> files;
std::vector<std::string> deps;
std::vector<GameSubtitle2DefinitionFile> files;
open_subtitle2_project("subtitle2", task.input.at(0), files);
for (auto& file : files) {
deps.push_back(path_map.apply_remaps(file.file_path));
}
enumerate_subtitle_project_files(name(), task.input.at(0), path_map, files, deps);
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
}
bool Subtitle2Tool::run(const ToolInput& task, const PathMap& path_map) {
GameSubtitle2DB db(GameVersion::Jak2); // TODO game version param
std::vector<GameSubtitle2DefinitionFile> files;
open_subtitle2_project("subtitle2", task.input.at(0), files);
for (auto& file : files) {
file.file_path = path_map.apply_remaps(file.file_path);
}
compile_game_subtitle2(files, db, path_map.output_prefix);
bool SubtitleV2Tool::run(const ToolInput& task, const PathMap& path_map) {
GameSubtitleDB db;
db.m_subtitle_version = GameSubtitleDB::SubtitleFormat::V2;
std::vector<GameSubtitleDefinitionFile> files;
run_subtitle_project_files(name(), task.input.at(0), path_map, files);
compile_game_subtitles(files, db, path_map.output_prefix);
return true;
}
+2 -2
View File
@@ -65,9 +65,9 @@ class SubtitleTool : public Tool {
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};
class Subtitle2Tool : public Tool {
class SubtitleV2Tool : public Tool {
public:
Subtitle2Tool();
SubtitleV2Tool();
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};
+1
View File
@@ -10,6 +10,7 @@ parser.set_defaults(fix=False)
args = parser.parse_args()
# TODO - trim strings
# TODO - jak 2 files
# fmt: off
JAK1_ALLOWED_CHARACTERS = [