mirror of
https://github.com/open-goal/jak-project
synced 2026-08-21 14:54:53 -04:00
tools: Add cutscene player / subtitle editor window (#1429)
* stash * temp * tools: subtitle tool works! just gotta fill out the db / polish UX * tools: added configuration for every subtitle we have so far * tools: add some colors to the editor, time for repl controls and make it run the code! * tools: continuing polish of tool, getting very close * tools: finished UX polish, just need to write deserializers * tools: added deserializer for subtitle data * tools: exported subtitle files, all data appears intact * tools: more UX polish and test all the cutscenes, majority work * assets: update subtitle files * lint: formatting and cleanup * lint: codacy lints
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
#include "subtitles.h"
|
||||
#include "common/goos/ParseHelpers.h"
|
||||
#include "common/goos/Reader.h"
|
||||
#include "common/util/FileUtil.h"
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "common/util/json_util.h"
|
||||
|
||||
static const std::unordered_map<std::string, GameTextVersion> s_text_ver_enum_map = {
|
||||
{"jak1-v1", GameTextVersion::JAK1_V1}};
|
||||
|
||||
// TODO - why not just return the inputs instead of passing in an empty one?
|
||||
void open_text_project(const std::string& kind,
|
||||
const std::string& filename,
|
||||
std::unordered_map<GameTextVersion, std::vector<std::string>>& inputs) {
|
||||
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()) {
|
||||
throw std::runtime_error(fmt::format("invalid entry in {} project", kind));
|
||||
}
|
||||
|
||||
auto& ver = o.as_pair()->car.as_symbol()->name;
|
||||
auto& in = o.as_pair()->cdr.as_pair()->car.as_string()->data;
|
||||
|
||||
inputs[s_text_ver_enum_map.at(ver)].push_back(in);
|
||||
});
|
||||
}
|
||||
|
||||
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("invalid pair");
|
||||
}
|
||||
|
||||
return x.as_pair()->car;
|
||||
}
|
||||
|
||||
const goos::Object& cdr(const goos::Object& x) {
|
||||
if (!x.is_pair()) {
|
||||
throw std::runtime_error("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.
|
||||
* 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(const goos::Object& data, GameTextVersion text_ver, GameTextDB& db) {
|
||||
auto font = get_font_bank(text_ver);
|
||||
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() && head.as_symbol()->name == "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() && head.as_symbol()->name == "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_int()) {
|
||||
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 subtitle file.
|
||||
* Information is added to the game subtitles database.
|
||||
*
|
||||
* The file should begin with (language-id x y z...) for the given language IDs.
|
||||
* Each scene should be (scene-name <entry 1> <entry 2> ... )
|
||||
* This adds the subtitle to each of the specified languages.
|
||||
*/
|
||||
void parse_subtitle(const goos::Object& data,
|
||||
GameTextVersion text_ver,
|
||||
GameSubtitleDB& db,
|
||||
const std::string& file_path) {
|
||||
auto font = get_font_bank(text_ver);
|
||||
std::map<int, std::shared_ptr<GameSubtitleBank>> banks;
|
||||
|
||||
for_each_in_list(data.as_pair()->cdr, [&](const goos::Object& obj) {
|
||||
if (obj.is_pair()) {
|
||||
auto& head = car(obj);
|
||||
if (head.is_symbol() && head.as_symbol()->name == "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.");
|
||||
}
|
||||
|
||||
for_each_in_list(cdr(obj), [&](const goos::Object& obj) {
|
||||
auto lang = get_int(obj);
|
||||
if (!db.bank_exists(lang)) {
|
||||
// database has no lang yet
|
||||
banks[lang] = db.add_bank(std::make_shared<GameSubtitleBank>(lang));
|
||||
banks[lang]->file_path = file_path;
|
||||
} else {
|
||||
banks[lang] = db.bank_by_id(lang);
|
||||
banks[lang]->file_path = file_path;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
else if (head.is_string() || head.is_int()) {
|
||||
if (banks.size() == 0) {
|
||||
throw std::runtime_error("At least one language must be set before defining scenes.");
|
||||
}
|
||||
auto kind = SubtitleSceneKind::Movie;
|
||||
int id = 0;
|
||||
auto entries = cdr(obj);
|
||||
if (head.is_int()) {
|
||||
kind = SubtitleSceneKind::Hint;
|
||||
} else if (car(entries).is_symbol()) {
|
||||
const auto& parm = car(entries).as_symbol()->name;
|
||||
if (parm == ":hint") {
|
||||
entries = cdr(entries);
|
||||
id = car(entries).as_int();
|
||||
kind = SubtitleSceneKind::HintNamed;
|
||||
} else {
|
||||
throw std::runtime_error("Unknown parameter for subtitle scene");
|
||||
}
|
||||
entries = cdr(entries);
|
||||
}
|
||||
|
||||
GameSubtitleSceneInfo scene(kind);
|
||||
if (kind == SubtitleSceneKind::Movie || kind == SubtitleSceneKind::HintNamed) {
|
||||
scene.set_name(head.as_string()->data);
|
||||
} else if (kind == SubtitleSceneKind::Hint) {
|
||||
id = head.as_int();
|
||||
}
|
||||
scene.set_id(id);
|
||||
scene.m_sorting_group = db.m_subtitle_groups->find_group(scene.name());
|
||||
scene.m_sorting_group_idx = db.m_subtitle_groups->find_group_index(scene.m_sorting_group);
|
||||
|
||||
for_each_in_list(entries, [&](const goos::Object& entry) {
|
||||
if (entry.is_pair()) {
|
||||
// expected formats:
|
||||
// (time <args>)
|
||||
// all arguments have default values. the arguments are:
|
||||
// "speaker" "line" - two strings. one for the speaker's name and one for the actual
|
||||
// line. speaker can be empty. default is just empty string.
|
||||
// :offscreen - speaker is offscreen. default is not offscreen.
|
||||
|
||||
if (!car(entry).is_int()) {
|
||||
throw std::runtime_error("Each entry must start with a timestamp (number)");
|
||||
}
|
||||
|
||||
auto time = car(entry).as_int();
|
||||
goos::StringObject *speaker = nullptr, *line = nullptr;
|
||||
bool offscreen = false;
|
||||
if (scene.kind() == SubtitleSceneKind::Hint ||
|
||||
scene.kind() == SubtitleSceneKind::HintNamed) {
|
||||
offscreen = true;
|
||||
}
|
||||
for_each_in_list(cdr(entry), [&](const goos::Object& arg) {
|
||||
if (arg.is_string()) {
|
||||
if (!speaker) {
|
||||
speaker = arg.as_string();
|
||||
} else if (!line) {
|
||||
line = arg.as_string();
|
||||
} else {
|
||||
throw std::runtime_error("Invalid string in subtitle entry");
|
||||
}
|
||||
} else if (speaker && !line) {
|
||||
throw std::runtime_error(
|
||||
"Invalid object in subtitle entry, expecting actual line string after speaker");
|
||||
} else if (arg.is_symbol()) {
|
||||
if (scene.kind() == SubtitleSceneKind::Movie &&
|
||||
arg.as_symbol()->name == ":offscreen") {
|
||||
offscreen = true;
|
||||
} else {
|
||||
throw std::runtime_error(
|
||||
fmt::format("Unknown parameter {} in subtitle", arg.as_symbol()->name));
|
||||
}
|
||||
}
|
||||
});
|
||||
auto line_utf8 = line ? line->data : "";
|
||||
auto line_str = font->convert_utf8_to_game(line_utf8);
|
||||
auto speaker_utf8 = speaker ? speaker->data : "";
|
||||
auto speaker_str = font->convert_utf8_to_game(speaker_utf8);
|
||||
scene.add_line(time, line_str, line_utf8, speaker_str, speaker_utf8, offscreen);
|
||||
} else {
|
||||
throw std::runtime_error("Each entry must be a list");
|
||||
}
|
||||
});
|
||||
for (auto& [lang, bank] : banks) {
|
||||
if (!bank->scene_exists(scene.name())) {
|
||||
bank->add_scene(scene);
|
||||
} else {
|
||||
auto& old_scene = bank->scene_by_name(scene.name());
|
||||
old_scene.from_other_scene(scene);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("Invalid game subtitles file entry: " + head.print());
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("Invalid game subtitles file");
|
||||
}
|
||||
});
|
||||
if (banks.size() == 0) {
|
||||
throw std::runtime_error("At least one language must be set.");
|
||||
}
|
||||
}
|
||||
|
||||
void GameSubtitleGroups::hydrate_from_asset_file() {
|
||||
std::string file_path = (file_util::get_jak_project_dir() / "game" / "assets" / "jak1" /
|
||||
"subtitle" / "subtitle-groups.json")
|
||||
.string();
|
||||
auto config_str = file_util::read_text_file(file_path);
|
||||
auto group_data = parse_commented_json(config_str, file_path);
|
||||
|
||||
for (const auto& [key, val] : group_data.items()) {
|
||||
try {
|
||||
if (key == group_order_key) {
|
||||
m_group_order = val.get<std::vector<std::string>>();
|
||||
} else {
|
||||
m_groups[key] = val.get<std::vector<std::string>>();
|
||||
}
|
||||
} catch (std::exception& ex) {
|
||||
fmt::print("Bad subtitle group entry - {} - {}", key, ex.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string GameSubtitleGroups::find_group(const std::string& scene_name) {
|
||||
for (auto const& [group, scenes] : m_groups) {
|
||||
for (auto const& name : scenes) {
|
||||
if (name == scene_name) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add to the uncategorized group if it wasn't found
|
||||
m_groups[uncategorized_group].push_back(scene_name);
|
||||
return uncategorized_group;
|
||||
}
|
||||
|
||||
int GameSubtitleGroups::find_group_index(const std::string& group_name) {
|
||||
auto it = find(m_group_order.begin(), m_group_order.end(), group_name);
|
||||
if (it != m_group_order.end()) {
|
||||
return it - m_group_order.begin();
|
||||
} else {
|
||||
return m_group_order.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void GameSubtitleGroups::remove_scene(const std::string& group_name,
|
||||
const std::string& scene_name) {
|
||||
// TODO - validate group_name
|
||||
m_groups[group_name].erase(
|
||||
std::remove(m_groups[group_name].begin(), m_groups[group_name].end(), scene_name),
|
||||
m_groups[group_name].end());
|
||||
}
|
||||
void GameSubtitleGroups::add_scene(const std::string& group_name, const std::string& scene_name) {
|
||||
// TODO - validate group_name
|
||||
// TODO - don't add duplicates
|
||||
m_groups[group_name].push_back(scene_name);
|
||||
}
|
||||
|
||||
GameSubtitleDB load_subtitle_project() {
|
||||
// Load the subtitle files
|
||||
GameSubtitleDB db;
|
||||
db.m_subtitle_groups = std::make_unique<GameSubtitleGroups>();
|
||||
db.m_subtitle_groups->hydrate_from_asset_file();
|
||||
goos::Reader reader;
|
||||
std::unordered_map<GameTextVersion, std::vector<std::string>> inputs;
|
||||
std::string subtitle_project =
|
||||
(file_util::get_jak_project_dir() / "game" / "assets" / "game_subtitle.gp").string();
|
||||
open_text_project("subtitle", subtitle_project, inputs);
|
||||
for (auto& [ver, in] : inputs) {
|
||||
for (auto& filename : in) {
|
||||
auto code = reader.read_from_file({filename});
|
||||
parse_subtitle(code, ver, db, filename);
|
||||
}
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
// TODO - write a deserializer, the compiler still can do the compiling!
|
||||
@@ -0,0 +1,206 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/util/FontUtils.h"
|
||||
#include "common/util/Assert.h"
|
||||
#include "common/goos/Object.h"
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
|
||||
/*!
|
||||
* 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 line_utf8,
|
||||
std::string speaker,
|
||||
std::string speaker_utf8,
|
||||
bool offscreen)
|
||||
: frame(frame),
|
||||
line(line),
|
||||
line_utf8(line_utf8),
|
||||
speaker(speaker),
|
||||
speaker_utf8(speaker_utf8),
|
||||
offscreen(offscreen) {}
|
||||
|
||||
int frame;
|
||||
std::string line;
|
||||
std::string line_utf8;
|
||||
std::string speaker;
|
||||
std::string speaker_utf8;
|
||||
bool offscreen;
|
||||
|
||||
bool operator<(const SubtitleLine& line) const { return (frame < line.frame); }
|
||||
};
|
||||
|
||||
GameSubtitleSceneInfo() {}
|
||||
GameSubtitleSceneInfo(SubtitleSceneKind kind) : m_kind(kind) {}
|
||||
|
||||
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 line_utf8,
|
||||
std::string speaker,
|
||||
std::string speaker_utf8,
|
||||
bool offscreen) {
|
||||
m_lines.emplace_back(SubtitleLine(frame, line, line_utf8, speaker, speaker_utf8, offscreen));
|
||||
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[scene.name()] = scene;
|
||||
}
|
||||
|
||||
int m_lang_id;
|
||||
std::string 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, GameTextVersion text_ver, GameTextDB& db);
|
||||
void parse_subtitle(const goos::Object& data,
|
||||
GameTextVersion text_ver,
|
||||
GameSubtitleDB& db,
|
||||
const std::string& file_path);
|
||||
|
||||
GameSubtitleDB load_subtitle_project();
|
||||
Reference in New Issue
Block a user