Files
jak-project/common/serialization/subtitles2/subtitles2_ser.cpp
T
ManDude 18ddd1613c Jak 2 pc subtitle support (#2672)
Adds support for adding custom subtitles to Jak 2 audio. Comes with a
new editor for the new system and format. Compared to the Jak 1 system,
this is much simpler to make an editor for.

Comes with a few subtitles already made as an example.
Cutscenes are not officially supported but you can technically subtitle
those with editor, so please don't right now.

This new system supports multiple subtitles playing at once (even from a
single source!) and will smartly push the subtitles up if there's a
message already playing:

![image](https://github.com/open-goal/jak-project/assets/7569514/033e6374-a05a-4c31-b029-51868153a932)

![image](https://github.com/open-goal/jak-project/assets/7569514/5298aa6d-a183-446e-bdb6-61c4682df917)

Unlike in Jak 1, it will not hide the bottom HUD when subtitles are
active:

![image](https://github.com/open-goal/jak-project/assets/7569514/d466bfc0-55d0-4689-a6e1-b7784b9fff59)

Sadly this leaves us with not much space for the subtitle region (and
the subtitles are shrunk when the minimap is enabled) but when you have
guards and citizens talking all the time, hiding the HUD every time
anyone spoke would get really frustrating.

The subtitle speaker is also color-coded now, because I thought that
would be fun to do.

TODO:
- [x] proper cutscene support.
- [x] merge mode for cutscenes so we don't have to rewrite the script?

---------

Co-authored-by: Hat Kid <6624576+Hat-Kid@users.noreply.github.com>
2023-06-08 01:04:16 +01:00

193 lines
5.8 KiB
C++

#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;
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;
}