mirror of
https://github.com/open-goal/jak-project
synced 2026-06-18 15:36:25 -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:
@@ -84,6 +84,13 @@
|
||||
"name" : "Run - Decompiler - Jak 1",
|
||||
"args" : [ "${workspaceRoot}/decompiler/config/jak1_ntsc_black_label.jsonc", "${workspaceRoot}/iso_data", "${workspaceRoot}/decompiler_out"]
|
||||
},
|
||||
{
|
||||
"type" : "default",
|
||||
"project" : "CMakeLists.txt",
|
||||
"projectTarget" : "decompiler.exe (bin\\decompiler.exe)",
|
||||
"name" : "Run - Decompiler - Jak 1 - Data Only",
|
||||
"args" : [ "${workspaceRoot}/decompiler/config/jak1_ntsc_black_label.jsonc", "${workspaceRoot}/iso_data", "${workspaceRoot}/decompiler_out", "decompile_code=false"]
|
||||
},
|
||||
{
|
||||
"type" : "default",
|
||||
"project" : "CMakeLists.txt",
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ tasks:
|
||||
cmds:
|
||||
- cmd: python ./third-party/run-clang-format/run-clang-format.py -r common decompiler game goalc test -i
|
||||
# npm install -g prettier
|
||||
- cmd: prettier --write ./decompiler/config/jak1_ntsc_black_label/*.jsonc
|
||||
- cmd: npx prettier --write ./decompiler/config/jak1_ntsc_black_label/*.jsonc
|
||||
ignore_error: true
|
||||
run-game-headless:
|
||||
cmds:
|
||||
|
||||
@@ -3,7 +3,9 @@ add_library(common
|
||||
cross_os_debug/xdbg.cpp
|
||||
cross_sockets/XSocket.cpp
|
||||
cross_sockets/XSocketServer.cpp
|
||||
cross_sockets/XSocketClient.cpp
|
||||
custom_data/TFrag3Data.cpp
|
||||
deserialization/subtitles/subtitles.cpp
|
||||
dma/dma.cpp
|
||||
dma/dma_copy.cpp
|
||||
dma/gs.cpp
|
||||
@@ -17,8 +19,11 @@ add_library(common
|
||||
goos/Reader.cpp
|
||||
goos/TextDB.cpp
|
||||
goos/ReplUtils.cpp
|
||||
math/geometry.cpp
|
||||
log/log.cpp
|
||||
math/geometry.cpp
|
||||
nrepl/ReplClient.cpp
|
||||
nrepl/ReplServer.cpp
|
||||
serialization/subtitles/subtitles.cpp
|
||||
type_system/defenum.cpp
|
||||
type_system/deftype.cpp
|
||||
type_system/state.cpp
|
||||
@@ -43,9 +48,7 @@ add_library(common
|
||||
util/print_float.cpp
|
||||
util/FontUtils.cpp
|
||||
util/FrameLimiter.cpp
|
||||
util/image_loading.cpp
|
||||
goos/Printer.cpp
|
||||
goos/PrettyPrinter2.cpp)
|
||||
util/image_loading.cpp)
|
||||
|
||||
target_link_libraries(common fmt lzokay replxx libzstd_static)
|
||||
|
||||
|
||||
@@ -34,6 +34,14 @@ int open_socket(int af, int type, int protocol) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int connect_socket(int socket, sockaddr* addr, int nameLen) {
|
||||
int result = connect(socket, addr, nameLen);
|
||||
if (result == -1) {
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#ifdef __linux
|
||||
int accept_socket(int socket, sockaddr* addr, socklen_t* addrLen) {
|
||||
return accept(socket, addr, addrLen);
|
||||
|
||||
@@ -25,6 +25,7 @@ const int TCP_SOCKET_LEVEL = IPPROTO_TCP;
|
||||
#endif
|
||||
|
||||
int open_socket(int af, int type, int protocol);
|
||||
int connect_socket(int socket, sockaddr* addr, int nameLen);
|
||||
#ifdef __linux
|
||||
int accept_socket(int socket, sockaddr* addr, socklen_t* addrLen);
|
||||
int select_and_accept_socket(int socket, sockaddr* addr, socklen_t* addrLen, int microSeconds);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "XSocketClient.h"
|
||||
|
||||
#include "common/cross_sockets/XSocket.h"
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <WinSock2.h>
|
||||
#include <WS2tcpip.h>
|
||||
#endif
|
||||
#include "common/nrepl/ReplServer.h"
|
||||
#include "third-party/fmt/core.h"
|
||||
|
||||
XSocketClient::XSocketClient(int _tcp_port) {
|
||||
tcp_port = _tcp_port;
|
||||
}
|
||||
|
||||
XSocketClient::~XSocketClient() {
|
||||
disconnect();
|
||||
client_socket = -1;
|
||||
}
|
||||
|
||||
void XSocketClient::disconnect() {
|
||||
close_socket(client_socket);
|
||||
client_socket = -1;
|
||||
}
|
||||
|
||||
bool XSocketClient::connect() {
|
||||
// Open Socket
|
||||
client_socket = open_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (client_socket < 0) {
|
||||
// TODO - log
|
||||
disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
addr.sin_port = htons(tcp_port);
|
||||
|
||||
// Connect to server
|
||||
int result = connect_socket(client_socket, (sockaddr*)&addr, sizeof(addr));
|
||||
if (result == -1) {
|
||||
// TODO - log and close
|
||||
disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/cross_sockets/XSocket.h"
|
||||
|
||||
#include <thread>
|
||||
#include "common/common_types.h"
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
/// @brief A cross platform generic socket client implementation
|
||||
class XSocketClient {
|
||||
public:
|
||||
XSocketClient(int _tcp_port);
|
||||
~XSocketClient();
|
||||
|
||||
XSocketClient(const XSocketClient&) = delete;
|
||||
XSocketClient& operator=(const XSocketClient&) = delete;
|
||||
|
||||
bool connect();
|
||||
void disconnect();
|
||||
|
||||
bool is_connected() { return client_socket != -1; }
|
||||
|
||||
protected:
|
||||
int tcp_port;
|
||||
struct sockaddr_in addr = {};
|
||||
int client_socket = -1;
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "subtitles.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "third-party/fmt/ranges.h"
|
||||
#include <regex>
|
||||
#include "common/util/FileUtil.h"
|
||||
#include "third-party/json.hpp"
|
||||
|
||||
bool write_subtitle_db_to_files(const GameSubtitleDB& db) {
|
||||
// Write the subtitles out
|
||||
std::vector<int> completed_banks = {};
|
||||
for (const auto& [id, bank] : db.m_banks) {
|
||||
// If we've done the bank before, skip it
|
||||
auto it = find(completed_banks.begin(), completed_banks.end(), bank->m_lang_id);
|
||||
if (it != completed_banks.end()) {
|
||||
continue;
|
||||
}
|
||||
// Check to see if this bank is shared by any other, if so do it at the same time
|
||||
// and skip it
|
||||
// This is basically just to deal with US/UK english in a not so hacky way
|
||||
std::vector<int> banks = {};
|
||||
for (const auto& [_id, _bank] : db.m_banks) {
|
||||
if (_bank->file_path == bank->file_path) {
|
||||
banks.push_back(_bank->m_lang_id);
|
||||
completed_banks.push_back(_bank->m_lang_id);
|
||||
}
|
||||
}
|
||||
|
||||
std::string file_contents = "";
|
||||
file_contents += fmt::format("(language-id {})\n", fmt::join(banks, " "));
|
||||
|
||||
for (const auto& group_name : db.m_subtitle_groups->m_group_order) {
|
||||
file_contents +=
|
||||
fmt::format("\n;; -----------------\n;; {}\n;; -----------------\n", group_name);
|
||||
for (const auto& [scene_name, scene_info] : bank->m_scenes) {
|
||||
if (scene_info.m_sorting_group != group_name) {
|
||||
continue;
|
||||
}
|
||||
file_contents += fmt::format("\n(\"{}\"", scene_name);
|
||||
if (scene_info.m_kind == SubtitleSceneKind::Hint) {
|
||||
file_contents += " :hint 0";
|
||||
} else if (scene_info.m_kind == SubtitleSceneKind::HintNamed) {
|
||||
file_contents += fmt::format(" :hint #x{0:x}", scene_info.m_id);
|
||||
}
|
||||
file_contents += "\n";
|
||||
for (const auto& line : scene_info.m_lines) {
|
||||
// Clear screen entries
|
||||
if (line.line_utf8.empty()) {
|
||||
file_contents += fmt::format(" ({})\n", line.frame);
|
||||
} else {
|
||||
file_contents += fmt::format(" ({}", line.frame);
|
||||
if (line.offscreen && scene_info.m_kind == SubtitleSceneKind::Movie) {
|
||||
file_contents += " :offscreen";
|
||||
}
|
||||
file_contents += fmt::format(" \"{}\"", line.speaker_utf8);
|
||||
// escape quotes
|
||||
std::string temp = line.line_utf8;
|
||||
temp = std::regex_replace(temp, std::regex("\""), "\\\"");
|
||||
file_contents += fmt::format(" \"{}\")\n", temp);
|
||||
}
|
||||
}
|
||||
file_contents += " )\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Commit it to the file
|
||||
std::string full_path =
|
||||
(file_util::get_jak_project_dir() / std::filesystem::path(bank->file_path)).string();
|
||||
file_util::write_text_file(full_path, file_contents);
|
||||
}
|
||||
|
||||
// Write the subtitle group info out
|
||||
nlohmann::json json(db.m_subtitle_groups->m_groups);
|
||||
json[db.m_subtitle_groups->group_order_key] = nlohmann::json(db.m_subtitle_groups->m_group_order);
|
||||
std::string file_path = (file_util::get_jak_project_dir() / "game" / "assets" / "jak1" /
|
||||
"subtitle" / "subtitle-groups.json")
|
||||
.string();
|
||||
file_util::write_text_file(file_path, json.dump(2));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/serialization/subtitles/subtitles.h"
|
||||
|
||||
bool write_subtitle_db_to_files(const GameSubtitleDB& db);
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "ReplClient.h"
|
||||
|
||||
#include "common/cross_sockets/XSocket.h"
|
||||
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "common/versions.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <WinSock2.h>
|
||||
#include <WS2tcpip.h>
|
||||
#endif
|
||||
|
||||
void ReplClient::eval(std::string form) {
|
||||
if (!is_connected()) {
|
||||
return;
|
||||
}
|
||||
// TODO - split this up into two writes
|
||||
u32 dataLength = form.length();
|
||||
ReplServerHeader header = {dataLength, ReplServerMessageType::EVAL};
|
||||
|
||||
auto const ptr = reinterpret_cast<char*>(&header);
|
||||
std::vector<char> buffer(ptr, ptr + sizeof header);
|
||||
|
||||
buffer.insert(buffer.end(), form.begin(), form.end());
|
||||
|
||||
int result = write_to_socket(client_socket, buffer.data(), buffer.size());
|
||||
if (result == -1) {
|
||||
// TODO - log
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/cross_sockets/XSocketClient.h"
|
||||
#include "ReplServer.h"
|
||||
|
||||
class ReplClient : public XSocketClient {
|
||||
public:
|
||||
using XSocketClient::XSocketClient;
|
||||
virtual ~ReplClient() = default;
|
||||
|
||||
ReplClient& operator=(const ReplClient&) { return *this; }
|
||||
|
||||
// TODO - just void for now :(
|
||||
void eval(std::string form);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/cross_sockets/XSocketServer.h"
|
||||
|
||||
#include "goalc/compiler/Compiler.h"
|
||||
#include <set>
|
||||
#include <optional>
|
||||
|
||||
enum ReplServerMessageType { PING = 0, EVAL = 10, SHUTDOWN = 20 };
|
||||
|
||||
@@ -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();
|
||||
@@ -9,18 +9,18 @@
|
||||
// NOTE: it's fine to have a function and its file both in here. the function takes priority.
|
||||
|
||||
"files": {
|
||||
"target":"eichar-ag",
|
||||
"target2":"eichar-ag",
|
||||
"target-death":"eichar-ag",
|
||||
"powerups":"eichar-ag"
|
||||
"target": "eichar-ag",
|
||||
"target2": "eichar-ag",
|
||||
"target-death": "eichar-ag",
|
||||
"powerups": "eichar-ag"
|
||||
},
|
||||
|
||||
"functions": {
|
||||
"(code target-warp-out)":"eichar-ag",
|
||||
"(code mistycannon-missile-idle)":"sack-ag",
|
||||
"(code billy-snack-eat)":"farthy-snack-ag",
|
||||
"(code plunger-lurker-plunge)":"plunger-lurker-ag",
|
||||
"(code plunger-lurker-flee)":"plunger-lurker-ag",
|
||||
"(code plunger-lurker-idle)":"plunger-lurker-ag"
|
||||
"(code target-warp-out)": "eichar-ag",
|
||||
"(code mistycannon-missile-idle)": "sack-ag",
|
||||
"(code billy-snack-eat)": "farthy-snack-ag",
|
||||
"(code plunger-lurker-plunge)": "plunger-lurker-ag",
|
||||
"(code plunger-lurker-flee)": "plunger-lurker-ag",
|
||||
"(code plunger-lurker-idle)": "plunger-lurker-ag"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,8 +616,6 @@
|
||||
"generic-no-light-dproc",
|
||||
"generic-envmap-dproc",
|
||||
"generic-tie-convert"
|
||||
|
||||
|
||||
],
|
||||
|
||||
"mips2c_jump_table_functions": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -126,7 +126,9 @@ set(RUNTIME_SOURCE
|
||||
graphics/texture/TexturePool.cpp
|
||||
graphics/pipelines/opengl.cpp
|
||||
system/vm/dmac.cpp
|
||||
system/vm/vm.cpp)
|
||||
system/vm/vm.cpp
|
||||
tools/subtitles/subtitle_editor.h
|
||||
tools/subtitles/subtitle_editor.cpp)
|
||||
|
||||
find_package(Git)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
;; you can find the game-text-version parsing in .cpp and an enum in goal-lib.gc
|
||||
|
||||
(subtitle
|
||||
(jak1-v1 "game/assets/jak1/subtitle/game_subtitle_en.gs")
|
||||
(jak1-v1 "game/assets/jak1/subtitle/game_subtitle_es.gs")
|
||||
(jak1-v1 "game/assets/jak1/subtitle/game_subtitle_en.gd")
|
||||
(jak1-v1 "game/assets/jak1/subtitle/game_subtitle_es.gd")
|
||||
)
|
||||
|
||||
|
||||
|
||||
+1218
-555
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
(language-id 3)
|
||||
|
||||
;; -----------------
|
||||
;; intro
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; sidekick
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; village1
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; oracle
|
||||
;; -----------------
|
||||
|
||||
("oracle-left-eye-1"
|
||||
(0 "ORÁCULO" "HABÉIS DEMOSTRADO SER HONESTOS. É AQUÍ UNA BATERÍA.")
|
||||
)
|
||||
|
||||
;; -----------------
|
||||
;; beach
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; jungle
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; misty
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; firecanyon
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; swamp
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; citadel
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; finalboss
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; training
|
||||
;; -----------------
|
||||
|
||||
;; -----------------
|
||||
;; uncategorized
|
||||
;; -----------------
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
(language-id 3)
|
||||
|
||||
;; -----------------
|
||||
;; oracle
|
||||
|
||||
("oracle-left-eye-1" (0 "ORÁCULO" "HABÉIS DEMOSTRADO SER HONESTOS. É AQUÍ UNA BATERÍA."))
|
||||
@@ -0,0 +1,643 @@
|
||||
{
|
||||
"assistant-firecanyon-resolution": {
|
||||
"entity_type": "assistant-firecanyon",
|
||||
"process_name": "assistant-firecanyon-1",
|
||||
"continue_name": "firecanyon-start",
|
||||
"move_to": [-27.0, 31.0, -183.0],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": ["(dm-lavabike-ready 0 (debug-menu-msg press))"]
|
||||
},
|
||||
"assistant-introduction-blue-eco-switch": {
|
||||
"entity_type": "assistant",
|
||||
"process_name": "assistant-11",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
]
|
||||
},
|
||||
"assistant-reminder-1-blue-eco-switch": {
|
||||
"entity_type": "assistant",
|
||||
"process_name": "assistant-11",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"assistant-introduction-race-bike": {
|
||||
"entity_type": "assistant",
|
||||
"process_name": "assistant-11",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": ["(dm-give-cell (game-task jungle-eggtop))"]
|
||||
},
|
||||
"assistant-reminder-1-generic": {
|
||||
"entity_type": "assistant",
|
||||
"process_name": "assistant-11",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-resolution 16 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 216 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"assistant-reminder-1-race-bike": {
|
||||
"entity_type": "assistant",
|
||||
"process_name": "assistant-11",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-give-cell (game-task jungle-eggtop))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"billy-accept": {
|
||||
"entity_type": "billy",
|
||||
"process_name": "billy-2",
|
||||
"continue_name": "swamp-game",
|
||||
"move_to": [606.1, 0.45, -2024.81],
|
||||
"execute_code": "(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"requirements": []
|
||||
},
|
||||
"billy-introduction": {
|
||||
"entity_type": "billy",
|
||||
"process_name": "billy-2",
|
||||
"continue_name": "swamp-game",
|
||||
"move_to": [606.1, 0.45, -2024.81],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"billy-reject": {
|
||||
"entity_type": "billy",
|
||||
"process_name": "billy-2",
|
||||
"continue_name": "swamp-game",
|
||||
"move_to": [606.1, 0.45, -2024.81],
|
||||
"execute_code": "(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"requirements": []
|
||||
},
|
||||
"billy-reminder-1": {
|
||||
"entity_type": "billy",
|
||||
"process_name": "billy-2",
|
||||
"continue_name": "swamp-game",
|
||||
"move_to": [606.1, 0.45, -2024.81],
|
||||
"execute_code": "(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"requirements": []
|
||||
},
|
||||
"billy-resolution": {
|
||||
"entity_type": "billy",
|
||||
"process_name": "billy-2",
|
||||
"continue_name": "swamp-game",
|
||||
"move_to": [606.1, 0.45, -2024.81],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": ["(dm-task-reminder 288 (debug-menu-msg press))"]
|
||||
},
|
||||
"bird-lady-beach-resolution": {
|
||||
"entity_type": "bird-lady-beach",
|
||||
"process_name": "bird-lady-beach-1",
|
||||
"continue_name": "beach-start",
|
||||
"move_to": [-76.90, 22.09, -270.7],
|
||||
"move_first": true,
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(run-now-in-process (process-by-name \"flutflutegg-1\" *active-pool*) (lambda () (go flutflutegg-physics-fall)))"
|
||||
]
|
||||
},
|
||||
"bird-lady-introduction": {
|
||||
"entity_type": "bird-lady",
|
||||
"process_name": "bird-lady-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-51, 10, -7],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"bird-lady-reminder-1": {
|
||||
"entity_type": "bird-lady",
|
||||
"process_name": "bird-lady-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-51, 10, -7],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"bird-lady-reminder-2": {
|
||||
"entity_type": "bird-lady",
|
||||
"process_name": "bird-lady-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-51, 10, -7],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(save-reminder (-> __GET-PROCESS__ tasks) 1 0)"
|
||||
]
|
||||
},
|
||||
"lrocklrg-falling": {
|
||||
"entity_type": "beach-rock",
|
||||
"process_name": "lrocklrg-1",
|
||||
"continue_name": "beach-start",
|
||||
"move_to": [-244, 32, -332],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'trigger)",
|
||||
"requirements": []
|
||||
},
|
||||
"bluesage-resolution": {
|
||||
"entity_type": "blue-sagecage",
|
||||
"process_name": "blue-sagecage-1",
|
||||
"continue_name": "citadel-plat-end",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-specific-task! (game-task citadel-sage-blue) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-red) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-green) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-yellow) (task-status need-hint))"
|
||||
]
|
||||
},
|
||||
"explorer-introduction": {
|
||||
"entity_type": "explorer",
|
||||
"process_name": "explorer-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"explorer-reminder-1": {
|
||||
"entity_type": "explorer",
|
||||
"process_name": "explorer-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"explorer-reminder-2": {
|
||||
"entity_type": "explorer",
|
||||
"process_name": "explorer-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(save-reminder (-> __GET-PROCESS__ tasks) 1 0)"
|
||||
]
|
||||
},
|
||||
"explorer-resolution": {
|
||||
"entity_type": "explorer",
|
||||
"process_name": "explorer-4",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"farmer-introduction": {
|
||||
"entity_type": "farmer",
|
||||
"process_name": "farmer-3",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-3.16, 1.77, -63.1],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"farmer-reminder-1": {
|
||||
"entity_type": "farmer",
|
||||
"process_name": "farmer-3",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-3.16, 1.77, -63.1],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"farmer-reminder-2": {
|
||||
"entity_type": "farmer",
|
||||
"process_name": "farmer-3",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-3.16, 1.77, -63.1],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(save-reminder (-> __GET-PROCESS__ tasks) 1 0)"
|
||||
]
|
||||
},
|
||||
"farmer-resolution": {
|
||||
"entity_type": "farmer",
|
||||
"process_name": "farmer-3",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-3.16, 1.77, -63.1],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": ["(dm-task-reminder 80 (debug-menu-msg press))"]
|
||||
},
|
||||
"fisher-accept": {
|
||||
"entity_type": "fisher",
|
||||
"process_name": "fisher-1",
|
||||
"continue_name": "jungle-start",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"requirements": []
|
||||
},
|
||||
"fisher-introduction": {
|
||||
"entity_type": "fisher",
|
||||
"process_name": "fisher-1",
|
||||
"continue_name": "jungle-start",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"fisher-reject": {
|
||||
"entity_type": "fisher",
|
||||
"process_name": "fisher-1",
|
||||
"continue_name": "jungle-start",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"requirements": []
|
||||
},
|
||||
"fisher-reminder-1": {
|
||||
"entity_type": "fisher",
|
||||
"process_name": "fisher-1",
|
||||
"continue_name": "jungle-start",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"requirements": []
|
||||
},
|
||||
"fisher-resolution": {
|
||||
"entity_type": "fisher",
|
||||
"process_name": "fisher-1",
|
||||
"continue_name": "jungle-start",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": ["(dm-task-reminder 40 (debug-menu-msg press))"]
|
||||
},
|
||||
"geologist-introduction": {
|
||||
"entity_type": "geologist",
|
||||
"process_name": "geologist-1",
|
||||
"continue_name": "village2-start",
|
||||
"move_to": [-56.6, 11.0, 32.9],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"green-sagecage-introduction": {
|
||||
"entity_type": "green-sagecage",
|
||||
"process_name": "green-sagecage-1",
|
||||
"continue_name": "citadel-start",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"green-sagecage-resolution": {
|
||||
"entity_type": "green-sagecage",
|
||||
"process_name": "green-sagecage-1",
|
||||
"continue_name": "citadel-elevator",
|
||||
"move_to": [262.35, 2.28, -231.83],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-specific-task! (game-task citadel-sage-blue) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-red) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-green) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-yellow) (task-status need-hint))",
|
||||
"(dm-give-all-cells 0 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"mayor-introduction": {
|
||||
"entity_type": "mayor",
|
||||
"process_name": "mayor-5",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"mayor-reminder-beams": {
|
||||
"entity_type": "mayor",
|
||||
"process_name": "mayor-5",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-introduction 24 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 88 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"mayor-reminder-donation": {
|
||||
"entity_type": "mayor",
|
||||
"process_name": "mayor-5",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-introduction 24 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 88 (debug-menu-msg press))",
|
||||
"(save-reminder (-> __GET-PROCESS__ tasks) 1 0)"
|
||||
]
|
||||
},
|
||||
"mayor-resolution-beams": {
|
||||
"entity_type": "mayor",
|
||||
"process_name": "mayor-5",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-introduction 24 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 88 (debug-menu-msg press))",
|
||||
"(dm-task-reminder 24 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"mayor-resolution-donation": {
|
||||
"entity_type": "mayor",
|
||||
"process_name": "mayor-5",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-87.04, 9.39, 43.64],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-introduction 24 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 88 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"oracle-intro-1": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-1",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [82.8, 18.2, 14.3],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"oracle-left-eye-1": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-1",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [82.8, 18.2, 14.3],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-give-cell (game-task village1-oracle-money1))"
|
||||
]
|
||||
},
|
||||
"oracle-left-eye-2": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-2",
|
||||
"continue_name": "village2-start",
|
||||
"move_to": [238.48, 11.9, -1532.6],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-give-cell (game-task village2-oracle-money1))"
|
||||
]
|
||||
},
|
||||
"oracle-left-eye-3": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-3",
|
||||
"continue_name": "village3-start",
|
||||
"move_to": [1024.94, 40.07, -3507.05],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-give-cell (game-task village3-oracle-money1))"
|
||||
]
|
||||
},
|
||||
"oracle-reminder-1": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-1",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [82.8, 18.2, 14.3],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"oracle-right-eye-1": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-1",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [82.8, 18.2, 14.3],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"oracle-right-eye-2": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-2",
|
||||
"continue_name": "village2-start",
|
||||
"move_to": [238.48, 11.9, -1532.6],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"oracle-right-eye-3": {
|
||||
"entity_type": "oracle",
|
||||
"process_name": "oracle-3",
|
||||
"continue_name": "village3-start",
|
||||
"move_to": [1024.94, 40.07, -3507.05],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-hint 104 (debug-menu-msg press))",
|
||||
"(dm-task-hint 112 (debug-menu-msg press))",
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))",
|
||||
"(dm-task-get-money 0 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"redsage-resolution": {
|
||||
"entity_type": "red-sagecage",
|
||||
"process_name": "red-sagecage-1",
|
||||
"continue_name": "citadel-generator-end",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-specific-task! (game-task citadel-sage-blue) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-red) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-green) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-yellow) (task-status need-hint))"
|
||||
]
|
||||
},
|
||||
"sage-intro-sequence-a": {
|
||||
"entity_type": "",
|
||||
"process_name": "",
|
||||
"continue_name": "intro-start",
|
||||
"move_to": [],
|
||||
"execute_code": "",
|
||||
"requirements": []
|
||||
},
|
||||
"sage-intro-sequence-d1": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"sage-intro-sequence-d2": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"sage-intro-sequence-e": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-resolution 736 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 744 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 752 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 760 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 872 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"sage-introduction-misty-cannon": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-resolution 736 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 744 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 752 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 760 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 872 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 120 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"sage-reminder-1-ecorocks": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-resolution 736 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 744 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 752 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 760 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 872 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 120 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 208 (debug-menu-msg press))",
|
||||
"(dm-task-reminder 208 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"sage-reminder-1-generic": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-resolution 736 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 744 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 752 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 760 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 872 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 120 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 208 (debug-menu-msg press))",
|
||||
"(dm-task-reminder 208 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 120 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"sage-reminder-1-misty-cannon": {
|
||||
"entity_type": "sage",
|
||||
"process_name": "sage-23",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(dm-task-resolution 736 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 744 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 752 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 760 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 872 (debug-menu-msg press))",
|
||||
"(dm-task-resolution 120 (debug-menu-msg press))",
|
||||
"(dm-task-introduction 208 (debug-menu-msg press))"
|
||||
]
|
||||
},
|
||||
"sculptor-introduction": {
|
||||
"entity_type": "sculptor",
|
||||
"process_name": "sculptor-6",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-110.43, 9.75, -3.46],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": []
|
||||
},
|
||||
"sculptor-reminder-1": {
|
||||
"entity_type": "sculptor",
|
||||
"process_name": "sculptor-6",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-110.43, 9.75, -3.46],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-status! (-> __GET-PROCESS__ tasks) (task-status need-introduction))"
|
||||
]
|
||||
},
|
||||
"sculptor-resolution": {
|
||||
"entity_type": "sculptor",
|
||||
"process_name": "sculptor-6",
|
||||
"continue_name": "village1-hut",
|
||||
"move_to": [-110.43, 9.75, -3.46],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": ["(dm-task-reminder 184 (debug-menu-msg press))"]
|
||||
},
|
||||
"sidekick-human-intro-sequence-b": {
|
||||
"entity_type": "",
|
||||
"process_name": "",
|
||||
"continue_name": "intro-start",
|
||||
"move_to": [],
|
||||
"execute_code": "",
|
||||
"requirements": []
|
||||
},
|
||||
"sidekick-human-intro-sequence-c": {
|
||||
"entity_type": "",
|
||||
"process_name": "",
|
||||
"continue_name": "intro-start",
|
||||
"move_to": [],
|
||||
"execute_code": "",
|
||||
"requirements": []
|
||||
},
|
||||
"yellowsage-resolution": {
|
||||
"entity_type": "yellow-sagecage",
|
||||
"process_name": "yellow-sagecage-1",
|
||||
"continue_name": "citadel-launch-end",
|
||||
"move_to": [],
|
||||
"execute_code": "(send-event __GET-PROCESS__ 'play-anim)",
|
||||
"requirements": [
|
||||
"(close-specific-task! (game-task citadel-sage-blue) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-red) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-green) (task-status need-hint))",
|
||||
"(close-specific-task! (game-task citadel-sage-yellow) (task-status need-hint))"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"_groups": [
|
||||
"intro",
|
||||
"sidekick",
|
||||
"village1",
|
||||
"oracle",
|
||||
"beach",
|
||||
"jungle",
|
||||
"misty",
|
||||
"firecanyon",
|
||||
"swamp",
|
||||
"citadel",
|
||||
"finalboss",
|
||||
"training",
|
||||
"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",
|
||||
"sksp0026",
|
||||
"sksp0030",
|
||||
"sksp0034",
|
||||
"sksp0020",
|
||||
"sksp0022",
|
||||
"sksp0023",
|
||||
"sksp0024",
|
||||
"sksp0025",
|
||||
"sksp0027",
|
||||
"sksp0029",
|
||||
"sagevb01",
|
||||
"sksp0443",
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"firecanyon": [
|
||||
"asstvb09",
|
||||
"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-TA1A",
|
||||
"FIS-TA2A",
|
||||
"FIS-TA03",
|
||||
"FIS-TA04",
|
||||
"FIS-TA05",
|
||||
"FIS-TA06",
|
||||
"FIS-TA07",
|
||||
"FIS-TA08",
|
||||
"FIS-TA09",
|
||||
"FIS-TA10",
|
||||
"FIS-TA11",
|
||||
"sksp0038",
|
||||
"sksp0040",
|
||||
"sksp0041",
|
||||
"sksp0011",
|
||||
"sksp0039",
|
||||
"sksp0018",
|
||||
"sksp0037",
|
||||
"sksp0b42",
|
||||
"asstvb02",
|
||||
"sksp0049",
|
||||
"sksp0050",
|
||||
"sksp0051",
|
||||
"sksp0052",
|
||||
"sksp0053",
|
||||
"sksp0054",
|
||||
"fisher-introduction",
|
||||
"fisher-reminder-1",
|
||||
"fisher-reject",
|
||||
"fisher-accept",
|
||||
"fisher-resolution"
|
||||
],
|
||||
"misty": [
|
||||
"sksp0031",
|
||||
"sksp0064",
|
||||
"sksp0067",
|
||||
"sksp0059",
|
||||
"sksp0060",
|
||||
"sksp0056",
|
||||
"sksp0062",
|
||||
"sksp0063",
|
||||
"sagevb02",
|
||||
"asstvb03",
|
||||
"sksp0069",
|
||||
"sksp0070",
|
||||
"sksp0435"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"sidekick": [
|
||||
"sksp0014",
|
||||
"sksp0009",
|
||||
"sksp0035",
|
||||
"sksp0001",
|
||||
"sksp0002",
|
||||
"sksp0003",
|
||||
"sksp0004",
|
||||
"sksp0005",
|
||||
"sksp0006",
|
||||
"sksp0007",
|
||||
"sksp0008",
|
||||
"sksp009a",
|
||||
"sksp009b",
|
||||
"sksp009c",
|
||||
"sksp009d",
|
||||
"sksp009e",
|
||||
"sksp009f",
|
||||
"sksp009g",
|
||||
"sksp009i",
|
||||
"sksp009j",
|
||||
"sksp009k",
|
||||
"sksp0071",
|
||||
"sksp0072",
|
||||
"sksp0073",
|
||||
"sksp0145"
|
||||
],
|
||||
"swamp": [
|
||||
"billy-introduction",
|
||||
"billy-reject",
|
||||
"billy-accept",
|
||||
"billy-resolution",
|
||||
"billy-reminder-1"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"uncategorized": [],
|
||||
"village1": [
|
||||
"SAGELP03",
|
||||
"SAGELP04",
|
||||
"SAGELP05",
|
||||
"SAGELP06",
|
||||
"SAGELP11",
|
||||
"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",
|
||||
"sksp0010",
|
||||
"sksp0013",
|
||||
"sksp0015",
|
||||
"sksp0017",
|
||||
"sksp0043",
|
||||
"sksp018a",
|
||||
"asstvb04",
|
||||
"asstvb08",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -362,6 +362,10 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) {
|
||||
m_small_profiler.draw(m_render_state.load_status_debug, stats);
|
||||
}
|
||||
|
||||
if (settings.draw_subtitle_editor_window) {
|
||||
m_subtitle_editor.draw_window();
|
||||
}
|
||||
|
||||
if (settings.save_screenshot) {
|
||||
finish_screenshot(settings.screenshot_path, settings.window_width_px, settings.window_height_px,
|
||||
settings.lbox_width_px, settings.lbox_height_px);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "game/graphics/opengl_renderer/Profiler.h"
|
||||
#include "game/graphics/opengl_renderer/opengl_utils.h"
|
||||
#include "game/graphics/opengl_renderer/CollideMeshRenderer.h"
|
||||
#include "game/tools/subtitles/subtitle_editor.h"
|
||||
|
||||
struct RenderOptions {
|
||||
int window_height_px = 0;
|
||||
@@ -18,6 +19,7 @@ struct RenderOptions {
|
||||
bool draw_render_debug_window = false;
|
||||
bool draw_profiler_window = false;
|
||||
bool draw_small_profiler_window = false;
|
||||
bool draw_subtitle_editor_window = false;
|
||||
|
||||
bool save_screenshot = false;
|
||||
std::string screenshot_path;
|
||||
@@ -65,6 +67,7 @@ class OpenGLRenderer {
|
||||
SharedRenderState m_render_state;
|
||||
Profiler m_profiler;
|
||||
SmallProfiler m_small_profiler;
|
||||
SubtitleEditor m_subtitle_editor;
|
||||
|
||||
std::array<std::unique_ptr<BucketRenderer>, (int)BucketId::MAX_BUCKETS> m_bucket_renderers;
|
||||
std::array<BucketCategory, (int)BucketId::MAX_BUCKETS> m_bucket_categories;
|
||||
|
||||
@@ -97,6 +97,11 @@ void OpenGlDebugGui::draw(const DmaStats& dma_stats) {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Tools")) {
|
||||
ImGui::MenuItem("Subtitle Editor", nullptr, &m_subtitle_editor);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Screenshot")) {
|
||||
ImGui::MenuItem("Screenshot Next Frame!", nullptr, &m_want_screenshot);
|
||||
ImGui::InputText("File", m_screenshot_save_name, 50);
|
||||
|
||||
@@ -44,6 +44,7 @@ class OpenGlDebugGui {
|
||||
void draw(const DmaStats& dma_stats);
|
||||
bool should_draw_render_debug() const { return m_draw_debug; }
|
||||
bool should_draw_profiler() const { return m_draw_profiler; }
|
||||
bool should_draw_subtitle_editor() const { return m_subtitle_editor; }
|
||||
const char* screenshot_name() const { return m_screenshot_save_name; }
|
||||
|
||||
bool should_advance_frame() { return m_frame_timer.should_advance_frame(); }
|
||||
@@ -73,6 +74,7 @@ class OpenGlDebugGui {
|
||||
bool m_draw_frame_time = false;
|
||||
bool m_draw_profiler = false;
|
||||
bool m_draw_debug = false;
|
||||
bool m_subtitle_editor = false;
|
||||
bool m_want_screenshot = false;
|
||||
char m_screenshot_save_name[256] = "screenshot.png";
|
||||
bool m_vsync = true;
|
||||
|
||||
@@ -219,6 +219,11 @@ std::string make_output_file_name(const std::string& file_name) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static bool endsWith(std::string_view str, std::string_view suffix) {
|
||||
return str.size() >= suffix.size() &&
|
||||
0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix);
|
||||
}
|
||||
|
||||
void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
|
||||
// wait for a copied chain.
|
||||
bool got_chain = false;
|
||||
@@ -240,11 +245,17 @@ void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
|
||||
options.lbox_width_px = lbox_width;
|
||||
options.draw_render_debug_window = g_gfx_data->debug_gui.should_draw_render_debug();
|
||||
options.draw_profiler_window = g_gfx_data->debug_gui.should_draw_profiler();
|
||||
options.draw_subtitle_editor_window = g_gfx_data->debug_gui.should_draw_subtitle_editor();
|
||||
options.save_screenshot = g_gfx_data->debug_gui.get_screenshot_flag();
|
||||
options.draw_small_profiler_window = g_gfx_data->debug_gui.small_profiler;
|
||||
options.pmode_alp_register = g_gfx_data->pmode_alp;
|
||||
if (options.save_screenshot) {
|
||||
options.screenshot_path = make_output_file_name(g_gfx_data->debug_gui.screenshot_name());
|
||||
// ensure the screenshot has an extension
|
||||
std::string temp_path = g_gfx_data->debug_gui.screenshot_name();
|
||||
if (endsWith(temp_path, ".png")) {
|
||||
temp_path += ".png";
|
||||
}
|
||||
options.screenshot_path = make_output_file_name(temp_path);
|
||||
}
|
||||
if constexpr (run_dma_copy) {
|
||||
auto& chain = g_gfx_data->dma_copier.get_last_result();
|
||||
|
||||
@@ -197,6 +197,11 @@ void DefaultMapping(MappingInfo& mapping) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - these are different from the analog bindings above and cause
|
||||
// the keyboard to be bound to controls regardless
|
||||
//
|
||||
// Need someway to toggle off -- where do we have access to the game's settings?
|
||||
|
||||
// R1 / L1
|
||||
MapButton(mapping, Button::L1, 0, GLFW_KEY_Q);
|
||||
MapButton(mapping, Button::R1, 0, GLFW_KEY_O);
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
#include "subtitle_editor.h"
|
||||
|
||||
#include "third-party/imgui/imgui.h"
|
||||
#include "third-party/imgui/imgui_stdlib.h"
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "common/util/FileUtil.h"
|
||||
#include "common/util/json_util.h"
|
||||
#include <regex>
|
||||
#include <string_view>
|
||||
#include "common/deserialization/subtitles/subtitles.h"
|
||||
|
||||
SubtitleEditor::SubtitleEditor() : m_repl(8181) {
|
||||
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("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>>();
|
||||
m_db.emplace(key, new_entry);
|
||||
} catch (std::exception& ex) {
|
||||
fmt::print("Bad subtitle db entry - {} - {}", key, ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
m_subtitle_db = load_subtitle_project();
|
||||
m_filter = m_filter_placeholder;
|
||||
m_filter_hints = m_filter_placeholder;
|
||||
m_repl.connect();
|
||||
}
|
||||
|
||||
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 {:.1f}) "
|
||||
":y (meters {:.1f}) :z (meters {:.1f})))",
|
||||
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_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(temp);
|
||||
}
|
||||
}
|
||||
|
||||
void SubtitleEditor::repl_rebuild_text() {
|
||||
m_repl.eval("(make-text)");
|
||||
// NOTE - still no clue how this doesn't switch languages lol
|
||||
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 (ImGui::Button("Save Changes")) {
|
||||
m_files_saved_successfully = std::make_optional(write_subtitle_db_to_files(m_subtitle_db));
|
||||
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_success_text_color);
|
||||
ImGui::Text("Error!");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
draw_edit_options();
|
||||
draw_repl_options();
|
||||
|
||||
draw_current_cutscene();
|
||||
|
||||
if (ImGui::TreeNode("All Cutscenes")) {
|
||||
ImGui::InputText("New Scene Name", &m_new_scene_name);
|
||||
// TODO - make this a dropdown
|
||||
ImGui::InputText("New Scene Group", &m_new_scene_group);
|
||||
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_group.empty()) {
|
||||
if (ImGui::Button("Add Scene")) {
|
||||
GameSubtitleSceneInfo newScene;
|
||||
newScene.m_name = m_new_scene_name;
|
||||
newScene.m_kind = SubtitleSceneKind::Movie;
|
||||
newScene.m_id = 0; // TODO - id is always zero, bug in subtitles.cpp?
|
||||
newScene.m_sorting_group = m_new_scene_group;
|
||||
m_subtitle_db.m_banks.at(m_current_language)->add_scene(newScene);
|
||||
m_new_scene_name = "";
|
||||
}
|
||||
}
|
||||
|
||||
draw_all_cutscene_groups();
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// TODO - hints
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void SubtitleEditor::draw_edit_options() {
|
||||
if (ImGui::TreeNode("Editing Options")) {
|
||||
ImGui::InputInt("Editing language ID", &m_current_language);
|
||||
ImGui::InputInt("Base language ID", &m_base_language);
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void SubtitleEditor::draw_all_cutscene_groups() {
|
||||
for (auto& group_name : m_subtitle_db.m_subtitle_groups->m_group_order) {
|
||||
ImGui::SetNextItemOpen(true);
|
||||
if (ImGui::TreeNode(group_name.c_str())) {
|
||||
draw_all_scenes(group_name, false);
|
||||
draw_all_scenes(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[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);
|
||||
}
|
||||
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(),
|
||||
scene_name.c_str())) {
|
||||
if (base_cutscenes || is_current_scene) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (!m_repl.is_connected()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, m_error_text_color);
|
||||
ImGui::Text("REPL not connected, can't play!");
|
||||
ImGui::PopStyleColor();
|
||||
} else if (m_db.count(scene_info.m_name) > 0) {
|
||||
if (ImGui::Button("Play Scene")) {
|
||||
if (m_db.count(scene_info.m_name) == 1) {
|
||||
repl_execute_cutscene_code(m_db[scene_info.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();
|
||||
}
|
||||
if (ImGui::BeginCombo("Sorting Group", scene_info.m_sorting_group.c_str())) {
|
||||
for (int i = 0; i < m_subtitle_db.m_subtitle_groups->m_group_order.size(); ++i) {
|
||||
const bool isSelected = (scene_info.m_sorting_group_idx == 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_info.m_sorting_group,
|
||||
scene_info.m_name);
|
||||
// Add to new group
|
||||
scene_info.m_sorting_group_idx = i;
|
||||
scene_info.m_sorting_group = m_subtitle_db.m_subtitle_groups->m_group_order.at(i);
|
||||
m_subtitle_db.m_subtitle_groups->add_scene(scene_info.m_sorting_group,
|
||||
scene_info.m_name);
|
||||
}
|
||||
if (isSelected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
for (int i = 0; i < scene_info.m_lines.size(); i++) {
|
||||
auto& subtitleLine = scene_info.m_lines.at(i);
|
||||
std::string summary;
|
||||
if (subtitleLine.line_utf8.empty()) {
|
||||
summary = fmt::format("[{}] Clear Screen", subtitleLine.frame);
|
||||
} else {
|
||||
summary = fmt::format("[{}] {} - '{}...'", subtitleLine.frame, subtitleLine.speaker_utf8,
|
||||
subtitleLine.line_utf8.substr(0, 30));
|
||||
}
|
||||
if (subtitleLine.line_utf8.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(), summary.c_str())) {
|
||||
if (subtitleLine.line_utf8.empty() || subtitleLine.offscreen) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::InputInt("Starting Frame", &subtitleLine.frame,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::InputText("Speaker", &subtitleLine.speaker_utf8,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsUppercase);
|
||||
ImGui::InputText("Text", &subtitleLine.line_utf8,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsUppercase);
|
||||
ImGui::Checkbox("Offscreen?", &subtitleLine.offscreen);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
|
||||
if (ImGui::Button("Remove Line")) {
|
||||
scene_info.m_lines.erase(scene_info.m_lines.begin() + i);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::TreePop();
|
||||
} else if (subtitleLine.line_utf8.empty() || subtitleLine.offscreen) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
} else if (base_cutscenes || is_current_scene) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SubtitleEditor::draw_current_cutscene() {
|
||||
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 Movie")) {
|
||||
ImGui::PopStyleColor();
|
||||
if (m_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 if (m_db.count(m_current_scene->m_name) > 0) {
|
||||
if (ImGui::Button("Play Scene")) {
|
||||
if (m_db.count(m_current_scene->m_name) == 1) {
|
||||
repl_execute_cutscene_code(m_db[m_current_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();
|
||||
}
|
||||
if (ImGui::BeginCombo("Sorting Group", m_current_scene->m_sorting_group.c_str())) {
|
||||
for (int i = 0; i < m_subtitle_db.m_subtitle_groups->m_group_order.size(); ++i) {
|
||||
const bool isSelected = (m_current_scene->m_sorting_group_idx == 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(m_current_scene->m_sorting_group,
|
||||
m_current_scene->m_name);
|
||||
// Add to new group
|
||||
m_current_scene->m_sorting_group_idx = i;
|
||||
m_current_scene->m_sorting_group = m_subtitle_db.m_subtitle_groups->m_group_order.at(i);
|
||||
m_subtitle_db.m_subtitle_groups->add_scene(m_current_scene->m_sorting_group,
|
||||
m_current_scene->m_name);
|
||||
}
|
||||
if (isSelected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::InputInt("Frame Number", &m_current_scene_frame,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::InputText("Text", &m_current_scene_text,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsUppercase);
|
||||
ImGui::InputText("Speaker", &m_current_scene_speaker,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsUppercase);
|
||||
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")) {
|
||||
m_current_scene->add_line(m_current_scene_frame, "", m_current_scene_text, "",
|
||||
m_current_scene_speaker, 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();
|
||||
for (int i = 0; i < m_current_scene->m_lines.size(); i++) {
|
||||
auto& subtitleLine = m_current_scene->m_lines.at(i);
|
||||
std::string summary;
|
||||
if (subtitleLine.line_utf8.empty()) {
|
||||
summary = fmt::format("[{}] Clear Screen", subtitleLine.frame);
|
||||
} else {
|
||||
summary = fmt::format("[{}] {} - '{}...'", subtitleLine.frame, subtitleLine.speaker_utf8,
|
||||
subtitleLine.line_utf8.substr(0, 30));
|
||||
}
|
||||
if (subtitleLine.line_utf8.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(), summary.c_str())) {
|
||||
if (subtitleLine.line_utf8.empty() || subtitleLine.offscreen) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::InputInt("Starting Frame", &subtitleLine.frame,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::InputText("Speaker", &subtitleLine.speaker_utf8,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsUppercase);
|
||||
ImGui::InputText("Text", &subtitleLine.line_utf8,
|
||||
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsUppercase);
|
||||
ImGui::Checkbox("Offscreen?", &subtitleLine.offscreen);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
|
||||
if (ImGui::Button("Remove Line")) {
|
||||
m_current_scene->m_lines.erase(m_current_scene->m_lines.begin() + i);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::TreePop();
|
||||
} else if (subtitleLine.line_utf8.empty() || subtitleLine.offscreen) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/serialization/subtitles/subtitles.h"
|
||||
#include <optional>
|
||||
#include "common/nrepl/ReplClient.h"
|
||||
#include "third-party/imgui/imgui.h"
|
||||
#include <string_view>
|
||||
|
||||
class SubtitleEditorDB {
|
||||
public:
|
||||
struct Entry {
|
||||
std::string entity_type;
|
||||
std::string process_name;
|
||||
std::string continue_name;
|
||||
std::vector<double> move_to;
|
||||
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 draw_edit_options();
|
||||
void draw_repl_options();
|
||||
|
||||
void draw_all_cutscene_groups();
|
||||
void draw_all_scenes(std::string group_name, bool base_cutscenes = false);
|
||||
void draw_current_cutscene();
|
||||
|
||||
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;
|
||||
|
||||
std::string m_new_scene_name = "";
|
||||
std::string m_new_scene_group = "";
|
||||
|
||||
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();
|
||||
|
||||
bool is_scene_in_current_lang(const std::string& scene_name);
|
||||
};
|
||||
@@ -915,6 +915,11 @@
|
||||
`(make-group "engine")
|
||||
)
|
||||
|
||||
(defmacro make-text ()
|
||||
"Make Text"
|
||||
`(make-group "text")
|
||||
)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;
|
||||
;; enum stuff
|
||||
;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -26,12 +26,11 @@ add_library(compiler
|
||||
compiler/compilation/Type.cpp
|
||||
compiler/compilation/State.cpp
|
||||
compiler/compilation/Static.cpp
|
||||
compiler/nrepl/ReplServer.cpp
|
||||
compiler/Util.cpp
|
||||
data_compiler/game_text_common.cpp
|
||||
data_compiler/dir_tpages.cpp
|
||||
data_compiler/DataObjectGenerator.cpp
|
||||
data_compiler/game_count.cpp
|
||||
data_compiler/DataObjectGenerator.cpp
|
||||
debugger/Debugger.cpp
|
||||
debugger/DebugInfo.cpp
|
||||
listener/Listener.cpp
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#include "nrepl/ReplServer.h" // this import has to come first because WinSock sucks
|
||||
|
||||
#include "Compiler.h"
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
@@ -32,4 +32,4 @@ void compile_dir_tpages(const std::string& filename) {
|
||||
file_util::create_dir_if_needed(file_util::get_file_path({"out", "obj"}));
|
||||
file_util::write_binary_file(file_util::get_file_path({"out", "obj", "dir-tpages.go"}),
|
||||
data.data(), data.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,4 +74,4 @@ void compile_game_count(const std::string& filename) {
|
||||
file_util::create_dir_if_needed(file_util::get_file_path({"out", "obj"}));
|
||||
file_util::write_binary_file(file_util::get_file_path({"out", "obj", "game-cnt.go"}),
|
||||
result.data(), result.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,37 +20,9 @@
|
||||
#include "common/util/FontUtils.h"
|
||||
#include "common/goos/ParseHelpers.h"
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "common/serialization/subtitles/subtitles.h"
|
||||
|
||||
namespace {
|
||||
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");
|
||||
}
|
||||
|
||||
std::string uppercase(const std::string& in) {
|
||||
std::string result;
|
||||
@@ -64,94 +36,6 @@ std::string uppercase(const std::string& in) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
(deftype game-text (structure)
|
||||
((id uint32 :offset-assert 0)
|
||||
@@ -202,139 +86,6 @@ void compile_text(GameTextDB& db) {
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* 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) {
|
||||
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));
|
||||
} else {
|
||||
banks[lang] = db.bank_by_id(lang);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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_str = font->convert_utf8_to_game(line ? line->data : "");
|
||||
auto speaker_str = font->convert_utf8_to_game(speaker ? speaker->data : "");
|
||||
scene.add_line(time, line_str, speaker_str, 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* Write game subtitle data to a file. Uses the V2 object format which is identical between GOAL and
|
||||
* OpenGOAL.
|
||||
@@ -412,32 +163,7 @@ void compile_game_subtitle(const std::vector<std::string>& filenames,
|
||||
for (auto& filename : filenames) {
|
||||
fmt::print("[Build Game Subtitle] {}\n", filename.c_str());
|
||||
auto code = reader.read_from_file({filename});
|
||||
parse_subtitle(code, text_ver, db);
|
||||
parse_subtitle(code, text_ver, db, filename);
|
||||
}
|
||||
compile_subtitle(db);
|
||||
}
|
||||
|
||||
static const std::unordered_map<std::string, GameTextVersion> s_text_ver_enum_map = {
|
||||
{"jak1-v1", GameTextVersion::JAK1_V1}};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,156 +6,7 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <memory>
|
||||
|
||||
/*!
|
||||
* 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;
|
||||
};
|
||||
|
||||
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 speaker, bool offscreen) {
|
||||
m_lines.emplace_back(frame, line, speaker, offscreen);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
int m_id;
|
||||
std::vector<SubtitleLine> m_lines;
|
||||
SubtitleSceneKind m_kind;
|
||||
};
|
||||
|
||||
/*!
|
||||
* 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;
|
||||
}
|
||||
|
||||
private:
|
||||
int m_lang_id;
|
||||
|
||||
std::map<std::string, GameSubtitleSceneInfo> m_scenes;
|
||||
};
|
||||
|
||||
/*!
|
||||
* 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);
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<int, std::shared_ptr<GameSubtitleBank>> m_banks;
|
||||
};
|
||||
#include "common/serialization/subtitles/subtitles.h"
|
||||
|
||||
void compile_game_text(const std::vector<std::string>& filenames,
|
||||
GameTextVersion text_ver,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
#include "common/goos/ReplUtils.h"
|
||||
#include <regex>
|
||||
#include <goalc/compiler/nrepl/ReplServer.h>
|
||||
#include "common/nrepl/ReplServer.h"
|
||||
|
||||
void setup_logging(bool verbose) {
|
||||
lg::set_file(file_util::get_file_path({"log/compiler.txt"}));
|
||||
|
||||
@@ -177,6 +177,8 @@ bool SubtitleTool::needs_run(const ToolInput& task) {
|
||||
|
||||
bool SubtitleTool::run(const ToolInput& task) {
|
||||
GameSubtitleDB db;
|
||||
db.m_subtitle_groups = std::make_unique<GameSubtitleGroups>();
|
||||
db.m_subtitle_groups->hydrate_from_asset_file();
|
||||
std::unordered_map<GameTextVersion, std::vector<std::string>> inputs;
|
||||
open_text_project("subtitle", task.input.at(0), inputs);
|
||||
for (auto& [ver, in] : inputs) {
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ add_library(imgui
|
||||
imgui_tables.cpp
|
||||
imgui_widgets.cpp
|
||||
imgui_impl_glfw.cpp
|
||||
imgui_impl_opengl3.cpp)
|
||||
imgui_impl_opengl3.cpp "imgui_stdlib.cpp" "imgui_stdlib.h")
|
||||
|
||||
target_link_libraries(imgui glfw)
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.)
|
||||
// This is also an example of how you may wrap your own similar types.
|
||||
|
||||
// Changelog:
|
||||
// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_stdlib.h"
|
||||
|
||||
struct InputTextCallback_UserData {
|
||||
std::string* Str;
|
||||
ImGuiInputTextCallback ChainCallback;
|
||||
void* ChainCallbackUserData;
|
||||
};
|
||||
|
||||
static int InputTextCallback(ImGuiInputTextCallbackData* data) {
|
||||
InputTextCallback_UserData* user_data = (InputTextCallback_UserData*)data->UserData;
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||
// Resize string callback
|
||||
// If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to
|
||||
// set them back to what we want.
|
||||
std::string* str = user_data->Str;
|
||||
IM_ASSERT(data->Buf == str->c_str());
|
||||
str->resize(data->BufTextLen);
|
||||
data->Buf = (char*)str->c_str();
|
||||
} else if (user_data->ChainCallback) {
|
||||
// Forward to user callback, if any
|
||||
data->UserData = user_data->ChainCallbackUserData;
|
||||
return user_data->ChainCallback(data);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ImGui::InputText(const char* label,
|
||||
std::string* str,
|
||||
ImGuiInputTextFlags flags,
|
||||
ImGuiInputTextCallback callback,
|
||||
void* user_data) {
|
||||
IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0);
|
||||
flags |= ImGuiInputTextFlags_CallbackResize;
|
||||
|
||||
InputTextCallback_UserData cb_user_data;
|
||||
cb_user_data.Str = str;
|
||||
cb_user_data.ChainCallback = callback;
|
||||
cb_user_data.ChainCallbackUserData = user_data;
|
||||
return InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback,
|
||||
&cb_user_data);
|
||||
}
|
||||
|
||||
bool ImGui::InputTextMultiline(const char* label,
|
||||
std::string* str,
|
||||
const ImVec2& size,
|
||||
ImGuiInputTextFlags flags,
|
||||
ImGuiInputTextCallback callback,
|
||||
void* user_data) {
|
||||
IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0);
|
||||
flags |= ImGuiInputTextFlags_CallbackResize;
|
||||
|
||||
InputTextCallback_UserData cb_user_data;
|
||||
cb_user_data.Str = str;
|
||||
cb_user_data.ChainCallback = callback;
|
||||
cb_user_data.ChainCallbackUserData = user_data;
|
||||
return InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags,
|
||||
InputTextCallback, &cb_user_data);
|
||||
}
|
||||
|
||||
bool ImGui::InputTextWithHint(const char* label,
|
||||
const char* hint,
|
||||
std::string* str,
|
||||
ImGuiInputTextFlags flags,
|
||||
ImGuiInputTextCallback callback,
|
||||
void* user_data) {
|
||||
IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0);
|
||||
flags |= ImGuiInputTextFlags_CallbackResize;
|
||||
|
||||
InputTextCallback_UserData cb_user_data;
|
||||
cb_user_data.Str = str;
|
||||
cb_user_data.ChainCallback = callback;
|
||||
cb_user_data.ChainCallbackUserData = user_data;
|
||||
return InputTextWithHint(label, hint, (char*)str->c_str(), str->capacity() + 1, flags,
|
||||
InputTextCallback, &cb_user_data);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.)
|
||||
// This is also an example of how you may wrap your own similar types.
|
||||
|
||||
// Changelog:
|
||||
// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ImGui {
|
||||
// ImGui::InputText() with std::string
|
||||
// Because text input needs dynamic resizing, we need to setup a callback to grow the capacity
|
||||
IMGUI_API bool InputText(const char* label,
|
||||
std::string* str,
|
||||
ImGuiInputTextFlags flags = 0,
|
||||
ImGuiInputTextCallback callback = NULL,
|
||||
void* user_data = NULL);
|
||||
IMGUI_API bool InputTextMultiline(const char* label,
|
||||
std::string* str,
|
||||
const ImVec2& size = ImVec2(0, 0),
|
||||
ImGuiInputTextFlags flags = 0,
|
||||
ImGuiInputTextCallback callback = NULL,
|
||||
void* user_data = NULL);
|
||||
IMGUI_API bool InputTextWithHint(const char* label,
|
||||
const char* hint,
|
||||
std::string* str,
|
||||
ImGuiInputTextFlags flags = 0,
|
||||
ImGuiInputTextCallback callback = NULL,
|
||||
void* user_data = NULL);
|
||||
} // namespace ImGui
|
||||
Reference in New Issue
Block a user