subtitle editor fixes + other smaller fixes (#1572)

* [extractor] validate files when extracted as folder

* jp text fixes

* move game text version to the text file and fix subtitle editor escape chars

* make bad subtitles not crash the game

* fix texscroll in lag

* fix mood, fix decomp of other versions, fix text decomp

* clang

* fix tests

* oops dammit

* new fixes

* shut up codacy

* fix nonexistant subtitles crashing the game

* fix text hacks and extractor re-use on folders
This commit is contained in:
ManDude 2022-06-30 00:43:23 +01:00 committed by GitHub
parent 870be7151a
commit 2649fd17ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 439 additions and 187 deletions

View file

@ -3,6 +3,7 @@
#include <algorithm>
#include <regex>
#include "common/serialization/subtitles/subtitles.h"
#include "common/util/FileUtil.h"
#include "third-party/fmt/core.h"
@ -31,6 +32,9 @@ bool write_subtitle_db_to_files(const GameSubtitleDB& db) {
std::string file_contents = "";
file_contents += fmt::format("(language-id {})\n", fmt::join(banks, " "));
auto file_ver = parse_text_only_version(bank->file_path);
auto font = get_font_bank(file_ver);
file_contents += fmt::format("(text-version {})\n", get_text_version_name(file_ver));
for (const auto& group_name : db.m_subtitle_groups->m_group_order) {
file_contents +=
@ -46,20 +50,19 @@ bool write_subtitle_db_to_files(const GameSubtitleDB& db) {
file_contents += fmt::format(" :hint #x{0:x}", scene_info.m_id);
}
file_contents += "\n";
for (const auto& line : scene_info.m_lines) {
for (auto& line : scene_info.lines()) {
// Clear screen entries
if (line.line_utf8.empty()) {
if (line.line.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 +=
fmt::format(" \"{}\"", font->convert_game_to_utf8(line.speaker.c_str()));
file_contents +=
fmt::format(" \"{}\")\n", font->convert_game_to_utf8(line.line.c_str()));
}
}
file_contents += " )\n";

View file

@ -7,37 +7,6 @@
#include "third-party/fmt/core.h"
static const std::unordered_map<std::string, GameTextVersion> s_text_ver_enum_map = {
{"jak1-v1", GameTextVersion::JAK1_V1},
{"jak1-v2", GameTextVersion::JAK1_V2}};
// 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;
if (s_text_ver_enum_map.count(ver) == 0) {
throw std::runtime_error(fmt::format("unknown text version {}", ver));
}
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;
@ -76,8 +45,8 @@ std::string get_string(const goos::Object& x) {
* 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);
void parse_text(const goos::Object& data, GameTextDB& db) {
const GameTextFontBank* font = nullptr;
std::vector<std::shared_ptr<GameTextBank>> banks;
std::string possible_group_name;
@ -170,9 +139,29 @@ void parse_text(const goos::Object& data, GameTextVersion text_ver, GameTextDB&
throw std::runtime_error(fmt::format("Non-string value in text id #x{:x}", id));
}
});
} else if (head.is_symbol("text-version")) {
if (font) {
throw std::runtime_error("text version is already set");
}
const auto& ver_name = car(cdr(obj));
if (!ver_name.is_symbol()) {
throw std::runtime_error("invalid text version entry");
}
if (auto it = sTextVerEnumMap.find(ver_name.as_symbol()->name);
it == sTextVerEnumMap.end()) {
throw std::runtime_error(
fmt::format("unknown text version {}", ver_name.as_symbol()->name));
} else {
font = get_font_bank(it->second);
}
}
else if (head.is_int()) {
if (!font) {
throw std::runtime_error("Text version must be set before defining entries.");
}
if (banks.size() == 0) {
throw std::runtime_error("At least one language must be set before defining entries.");
}
@ -214,11 +203,8 @@ void parse_text(const goos::Object& data, GameTextVersion text_ver, GameTextDB&
* 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);
void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::string& file_path) {
const GameTextFontBank* font = nullptr;
std::map<int, std::shared_ptr<GameSubtitleBank>> banks;
for_each_in_list(data.as_pair()->cdr, [&](const goos::Object& obj) {
@ -244,9 +230,29 @@ void parse_subtitle(const goos::Object& data,
banks[lang]->file_path = file_path;
}
});
} else if (head.is_symbol("text-version")) {
if (font) {
throw std::runtime_error("text version is already set");
}
const auto& ver_name = car(cdr(obj));
if (!ver_name.is_symbol()) {
throw std::runtime_error("invalid text version entry");
}
if (auto it = sTextVerEnumMap.find(ver_name.as_symbol()->name);
it == sTextVerEnumMap.end()) {
throw std::runtime_error(
fmt::format("unknown text version {}", ver_name.as_symbol()->name));
} else {
font = get_font_bank(it->second);
}
}
else if (head.is_string() || head.is_int()) {
if (!font) {
throw std::runtime_error("Text version must be set before defining entries.");
}
if (banks.size() == 0) {
throw std::runtime_error("At least one language must be set before defining scenes.");
}
@ -319,11 +325,9 @@ void parse_subtitle(const goos::Object& data,
}
}
});
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);
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(
fmt::format("{} | Each entry must be a non-empty list", scene.name()));
@ -349,6 +353,45 @@ void parse_subtitle(const goos::Object& data,
}
}
GameTextVersion parse_text_only_version(const std::string& filename) {
goos::Reader reader;
return parse_text_only_version(reader.read_from_file({filename}));
}
GameTextVersion parse_text_only_version(const goos::Object& data) {
const GameTextFontBank* font = nullptr;
for_each_in_list(data.as_pair()->cdr, [&](const goos::Object& obj) {
if (obj.is_pair()) {
auto& head = car(obj);
if (head.is_symbol("text-version")) {
if (font) {
throw std::runtime_error("text version is already set");
}
const auto& ver_name = car(cdr(obj));
if (!ver_name.is_symbol()) {
throw std::runtime_error("invalid text version entry");
}
if (auto it = sTextVerEnumMap.find(ver_name.as_symbol()->name);
it == sTextVerEnumMap.end()) {
throw std::runtime_error(
fmt::format("unknown text version {}", ver_name.as_symbol()->name));
} else {
font = get_font_bank(it->second);
}
}
}
});
if (!font) {
throw std::runtime_error("text version not found");
}
return font->version();
}
void GameSubtitleGroups::hydrate_from_asset_file() {
std::string file_path = (file_util::get_jak_project_dir() / "game" / "assets" / "jak1" /
"subtitle" / "subtitle-groups.json")
@ -416,21 +459,50 @@ void GameSubtitleGroups::add_scene(const std::string& group_name, const std::str
}
}
// 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::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() && o.as_pair()->cdr.is_pair()) {
auto& action = o.as_pair()->car.as_symbol()->name;
if (action == "file") {
auto& in = o.as_pair()->cdr.as_pair()->car.as_string()->data;
inputs.push_back(in);
} else {
throw std::runtime_error(fmt::format("unknown action {} in {} project", action, kind));
}
} else {
throw std::runtime_error(fmt::format("invalid entry in {} project", kind));
}
});
}
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) {
try {
goos::Reader reader;
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& filename : inputs) {
auto code = reader.read_from_file({filename});
parse_subtitle(code, ver, db, filename);
parse_subtitle(code, db, filename);
}
} catch (std::runtime_error& e) {
lg::error("error loading subtitle project: {}", e.what());
}
return db;
}

View file

@ -5,6 +5,7 @@
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include "common/goos/Object.h"
#include "common/log/log.h"
@ -74,30 +75,17 @@ 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) {}
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 line_utf8;
std::string speaker;
std::string speaker_utf8;
bool offscreen;
bool operator<(const SubtitleLine& other) const { return (frame < other.frame); }
};
GameSubtitleSceneInfo() {}
GameSubtitleSceneInfo(SubtitleSceneKind kind) : m_kind(kind) {}
const std::string& name() const { return m_name; }
@ -115,13 +103,8 @@ class GameSubtitleSceneInfo {
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));
void add_line(int frame, std::string line, std::string speaker, bool offscreen) {
m_lines.emplace_back(SubtitleLine(frame, line, speaker, offscreen));
std::sort(m_lines.begin(), m_lines.end());
}
@ -147,7 +130,7 @@ class GameSubtitleBank {
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;
m_scenes.insert({scene.name(), scene});
}
int m_lang_id;
@ -199,10 +182,13 @@ class GameSubtitleDB {
// 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);
void parse_text(const goos::Object& data, GameTextDB& db);
void parse_subtitle(const goos::Object& data, GameSubtitleDB& db, const std::string& file_path);
GameTextVersion parse_text_only_version(const std::string& filename);
GameTextVersion parse_text_only_version(const goos::Object& data);
void open_text_project(const std::string& kind,
const std::string& filename,
std::vector<std::string>& inputs);
GameSubtitleDB load_subtitle_project();

View file

@ -10,9 +10,36 @@
#include "FontUtils.h"
#include <algorithm>
#include <stdexcept>
#include "common/util/Assert.h"
#include "third-party/fmt/core.h"
namespace {
/*!
* Is this a valid character for a hex number?
*/
bool hex_char(char c) {
return !((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F'));
}
} // namespace
const std::unordered_map<std::string, GameTextVersion> sTextVerEnumMap = {
{"jak1-v1", GameTextVersion::JAK1_V1},
{"jak1-v2", GameTextVersion::JAK1_V2}};
const std::string& get_text_version_name(GameTextVersion version) {
for (auto& [name, ver] : sTextVerEnumMap) {
if (ver == version) {
return name;
}
}
throw std::runtime_error(fmt::format("invalid text version {}", version));
}
GameTextFontBank::GameTextFontBank(GameTextVersion version,
std::vector<EncodeInfo>* encode_info,
std::vector<ReplaceInfo>* replace_info,
@ -130,6 +157,51 @@ std::string GameTextFontBank::convert_utf8_to_game(std::string str) const {
return str;
}
/*!
* Turn a normal readable string into a string readable in the in-game font encoding and converts
* \cXX escape sequences
*/
std::string GameTextFontBank::convert_utf8_to_game_with_escape(const std::string& str) const {
std::string newstr;
for (int i = 0; i < str.size(); ++i) {
auto c = str.at(i);
if (c == '\\') {
if (i + 1 >= str.size()) {
throw std::runtime_error("incomplete string escape code");
}
auto p = str.at(i + 1);
if (p == 'c') {
if (i + 3 >= str.size()) {
throw std::runtime_error("incomplete string escape code");
}
auto first = str.at(i + 2);
auto second = str.at(i + 3);
if (!hex_char(first) || !hex_char(second)) {
throw std::runtime_error("invalid character escape hex number");
}
char hex_num[3] = {first, second, '\0'};
std::size_t end = 0;
auto value = std::stoul(hex_num, &end, 16);
if (end != 2) {
throw std::runtime_error("invalid character escape");
}
ASSERT(value < 256);
newstr.push_back(char(value));
i += 3;
} else {
throw std::runtime_error("unknown string escape code");
}
} else {
newstr.push_back(c);
}
}
replace_to_game(newstr);
encode_utf8_to_game(newstr);
return newstr;
}
/*!
* Convert a string from the game-text font encoding to something normal.
* Unprintable characters become escape sequences, including tab and newline.

View file

@ -11,6 +11,7 @@
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@ -26,6 +27,10 @@ enum class GameTextVersion {
JAKX = 40 // jak x
};
extern const std::unordered_map<std::string, GameTextVersion> sTextVerEnumMap;
const std::string& get_text_version_name(GameTextVersion version);
/*!
* What bytes a set of characters (UTF-8) correspond to. You can convert to and fro.
*/
@ -68,6 +73,9 @@ class GameTextFontBank {
const std::vector<ReplaceInfo>* replace_info() const { return m_replace_info; }
const std::unordered_set<char>* passthrus() const { return m_passthrus; }
GameTextVersion version() const { return m_version; }
std::string convert_utf8_to_game_with_escape(const std::string& str) const;
std::string convert_utf8_to_game(std::string str) const;
std::string convert_game_to_utf8(const char* in) const;
};

View file

@ -51,6 +51,9 @@
// this is a guess at where each symbol is first defined/used.
"generate_symbol_definition_map": false,
// genreate the all-types file
"generate_all_types" : false,
// debug option for instruction decoder
"write_hex_near_instructions": false,

View file

@ -1,6 +1,6 @@
{
"game_version": 1,
"text_version": 10,
"text_version": 11, // patched to 11
"game_name": "jak1",
"expected_elf_name": "SCUS_971.24",
@ -113,7 +113,7 @@
"write_patches": false,
// set to true to apply patch files
"apply_patches": true,
// what to patch an object to and the patch file is
// what to patch an object to and what the patch file is
"object_patches": {
"0COMMON": {
"crc32": "DD2CD7E2",

View file

@ -51,6 +51,9 @@
// this is a guess at where each symbol is first defined/used.
"generate_symbol_definition_map": false,
// genreate the all-types file
"generate_all_types" : false,
// debug option for instruction decoder
"write_hex_near_instructions": false,

View file

@ -51,6 +51,9 @@
// this is a guess at where each symbol is first defined/used.
"generate_symbol_definition_map": false,
// genreate the all-types file
"generate_all_types" : false,
// debug option for instruction decoder
"write_hex_near_instructions": false,

View file

@ -7,6 +7,7 @@
#include "common/goos/Reader.h"
#include "common/util/BitUtils.h"
#include "common/util/FontUtils.h"
#include "decompiler/ObjectFile/ObjectFileDB.h"
@ -187,6 +188,7 @@ std::string write_game_text(
result += fmt::format(" {}", lang);
}
result += ")\n";
result += fmt::format("(text-version {})\n\n", get_text_version_name(version));
for (auto& x : text_by_id) {
result += fmt::format("(#x{:04x}\n ", x.first);
for (auto& y : x.second) {

View file

@ -105,6 +105,119 @@ std::pair<std::optional<std::string>, std::optional<xxh::hash64_t>> findElfFile(
return {serial, elf_hash};
}
std::pair<ExtractorErrorCode, std::optional<ISOMetadata>> validate(
const std::filesystem::path& extracted_iso_path) {
if (!std::filesystem::exists(extracted_iso_path / "DGO")) {
fmt::print(stderr, "ERROR: input folder doesn't have a DGO folder. Is this the right input?\n");
return {ExtractorErrorCode::VALIDATION_BAD_EXTRACTION, std::nullopt};
}
std::optional<ExtractorErrorCode> error_code;
std::optional<std::string> serial = std::nullopt;
std::optional<xxh::hash64_t> elf_hash = std::nullopt;
std::tie(serial, elf_hash) = findElfFile(extracted_iso_path);
// - XOR all hashes together and hash the result. This makes the ordering of the hashes (aka
// files) irrelevant
xxh::hash64_t combined_hash = 0;
int filec = 0;
for (auto const& dir_entry : std::filesystem::recursive_directory_iterator(extracted_iso_path)) {
if (dir_entry.is_regular_file()) {
auto buffer = file_util::read_binary_file(dir_entry.path().string());
auto hash = xxh::xxhash<64>(buffer);
combined_hash ^= hash;
filec++;
}
}
xxh::hash64_t contents_hash = xxh::xxhash<64>({combined_hash});
if (!serial || !elf_hash) {
fmt::print(stderr, "ERROR: Unable to locate a Serial/ELF file!\n");
if (!error_code.has_value()) {
error_code = std::make_optional(ExtractorErrorCode::VALIDATION_CANT_LOCATE_ELF);
}
// No point in continuing here
return {*error_code, std::nullopt};
}
// Find the game in our tracking database
std::optional<ISOMetadata> meta_res = std::nullopt;
if (auto dbEntry = isoDatabase.find(serial.value()); dbEntry == isoDatabase.end()) {
fmt::print(stderr, "ERROR: Serial '{}' not found in the validation database\n", serial.value());
if (!error_code.has_value()) {
error_code = std::make_optional(ExtractorErrorCode::VALIDATION_SERIAL_MISSING_FROM_DB);
}
} else {
auto& metaMap = dbEntry->second;
auto meta_entry = metaMap.find(elf_hash.value());
if (meta_entry == metaMap.end()) {
fmt::print(stderr,
"ERROR: ELF Hash '{}' not found in the validation database, is this a new or "
"modified version of the same game?\n",
elf_hash.value());
if (!error_code.has_value()) {
error_code = std::make_optional(ExtractorErrorCode::VALIDATION_ELF_MISSING_FROM_DB);
}
} else {
meta_res = std::make_optional<ISOMetadata>(meta_entry->second);
const auto& meta = *meta_res;
// Print out some information
fmt::print("Detected Game Metadata:\n");
fmt::print("\tDetected - {}\n", meta.canonical_name);
fmt::print("\tRegion - {}\n", meta.region);
fmt::print("\tSerial - {}\n", dbEntry->first);
fmt::print("\tUses Decompiler Config - {}\n", meta.decomp_config);
// - Number of Files
if (meta.num_files != filec) {
fmt::print(stderr,
"ERROR: Extracted an unexpected number of files. Expected '{}', Actual '{}'\n",
meta.num_files, filec);
if (!error_code.has_value()) {
error_code =
std::make_optional(ExtractorErrorCode::VALIDATION_INCORRECT_EXTRACTION_COUNT);
}
}
// Check the ISO Hash
if (meta.contents_hash != contents_hash) {
fmt::print(stderr,
"ERROR: Overall ISO content's hash does not match. Expected '{}', Actual '{}'\n",
meta.contents_hash, contents_hash);
}
}
}
// Finally, return the result
if (error_code.has_value()) {
// Generate the map entry to make things simple, just convienance
if (error_code.value() == ExtractorErrorCode::VALIDATION_SERIAL_MISSING_FROM_DB) {
fmt::print(
"If this is a new release or version that should be supported, consider adding the "
"following serial entry to the database:\n");
fmt::print(
"\t'{{\"{}\", {{{{{}U, {{\"GAME_TITLE\", \"NTSC-U/PAL/NTSC-J\", {}, {}U, "
"\"DECOMP_CONFIF_FILENAME_NO_EXTENSION\"}}}}}}}}'\n",
serial.value(), elf_hash.value(), filec, contents_hash);
} else if (error_code.value() == ExtractorErrorCode::VALIDATION_ELF_MISSING_FROM_DB) {
fmt::print(
"If this is a new release or version that should be supported, consider adding the "
"following ELF entry to the database under the '{}' serial:\n",
serial.value());
fmt::print(
"\t'{{{}, {{\"GAME_TITLE\", \"NTSC-U/PAL/NTSC-J\", {}, {}U, "
"\"DECOMP_CONFIF_FILENAME_NO_EXTENSION\"}}}}'\n",
elf_hash.value(), filec, contents_hash);
} else {
fmt::print(stderr,
"Validation has failed to match with expected values, see the above errors for "
"specifics. This may be an error in the validation database!\n");
}
return {*error_code, std::nullopt};
}
return {ExtractorErrorCode::SUCCESS, meta_res};
}
std::pair<ExtractorErrorCode, std::optional<ISOMetadata>> validate(
const IsoFile& iso_file,
const std::filesystem::path& extracted_iso_path) {
@ -439,6 +552,7 @@ int main(int argc, char** argv) {
std::filesystem::file_size(data_dir_path));
return static_cast<int>(ExtractorErrorCode::EXTRACTION_ISO_UNEXPECTED_SIZE);
}
auto iso_file = extract_files(data_dir_path, path_to_iso_files);
auto validation_res = validate(iso_file, path_to_iso_files);
flags = validation_res.second->flags;
@ -455,6 +569,11 @@ int main(int argc, char** argv) {
return static_cast<int>(ExtractorErrorCode::VALIDATION_BAD_ISO_CONTENTS);
}
path_to_iso_files = data_dir_path;
if (std::filesystem::exists(path_to_iso_files / "buildinfo.json")) {
std::filesystem::remove(path_to_iso_files / "buildinfo.json");
}
auto validation_res = validate(path_to_iso_files);
flags = validation_res.second->flags;
}
// write out a json file with some metadata for the game

View file

@ -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.gd")
(jak1-v1 "game/assets/jak1/subtitle/game_subtitle_es.gd")
(file "game/assets/jak1/subtitle/game_subtitle_en.gd")
(file "game/assets/jak1/subtitle/game_subtitle_es.gd")
)

View file

@ -5,12 +5,12 @@
(text
;; NOTE : we compile using the fixed v2 encoding because it's what we use.
(jak1-v2 "assets/game_text.txt") ;; this is the decompiler-generated file!
(file "assets/game_text.txt") ;; this is the decompiler-generated file!
;; "patch" files so we can fix some errors and perhaps maintain consistency
(jak1-v2 "game/assets/jak1/text/text_patch_ja.gs")
(file "game/assets/jak1/text/text_patch_ja.gs")
;; add custom files down here
(jak1-v2 "game/assets/jak1/text/game_text_en.gs")
(jak1-v2 "game/assets/jak1/text/game_text_ja.gs")
(file "game/assets/jak1/text/game_text_en.gs")
(file "game/assets/jak1/text/game_text_ja.gs")
)

View file

@ -1,4 +1,5 @@
(language-id 0 6)
(text-version jak1-v2)
;; -----------------
;; intro
@ -2522,7 +2523,7 @@
)
("MTA-AM08" :hint #x0
(0 "WILLARD" "EH HEH... BIRDIE IS MY BEST FRIEND<til>")
(0 "WILLARD" "EH HEH... BIRDIE IS MY BEST FRIEND<TIL>")
)
("MTA-AM09" :hint #x0

View file

@ -1,4 +1,5 @@
(language-id 3)
(text-version jak1-v2)
;; -----------------
;; intro

View file

@ -1,5 +1,6 @@
(group-name "common")
(language-id 0 6)
(language-id 0 6) ;; english and uk-english
(text-version jak1-v2)
;; -----------------
;; progress menu (insanity)

View file

@ -1,5 +1,6 @@
(group-name "common")
(language-id 5)
(text-version jak1-v2)
;; -----------------
;; progress menu (insanity)
@ -20,13 +21,13 @@
(#x1088 "サカナとりゲームのテーマ")
(#x1089 "チャレンジーのテーマ")
(#x1090 "無限の青のエコ")
(#x1091 "無限の赤のエコ")
(#x1092 "無限の緑のエコ")
(#x1093 "無限の黄のエコ")
(#x1090 "むげんの青のエコ")
(#x1091 "むげんの赤のエコ")
(#x1092 "むげんの緑のエコ")
(#x1093 "むげんの黄のエコ")
(#x1094 "へんかのダックスター")
(#x1095 "インヴィンシブル")
(#x1096 "全てBGMへんかを エネーブルする")
(#x1096 "すべてBGMのへんかを エネーブルする")
(#x10c0 "BGMプレイヤー")

View file

@ -1,5 +1,6 @@
(group-name "common")
(language-id 5)
(text-version jak1-v2)
;; -----------------
;; fixes

View file

@ -416,7 +416,7 @@ GfxDisplayMode GLDisplay::get_fullscreen() {
int GLDisplay::get_screen_vmode_count() {
int count = 0;
auto vmodes = glfwGetVideoModes(glfwGetPrimaryMonitor(), &count);
glfwGetVideoModes(glfwGetPrimaryMonitor(), &count);
return count;
}

View file

@ -44,7 +44,7 @@ std::string SubtitleEditor::repl_get_process_string(const std::string_view& enti
void SubtitleEditor::repl_play_hint(const std::string_view& hint_name) {
repl_reset_game();
repl_set_continue_point("village1-hut");
// repl_set_continue_point("village1-hut");
// TODO - move into water fountain
m_repl.eval(
fmt::format("(level-hint-spawn (game-text-id zero) \"{}\" (the-as entity #f) *entity-pool* "
@ -102,7 +102,8 @@ void SubtitleEditor::repl_execute_cutscene_code(const SubtitleEditorDB::Entry& e
void SubtitleEditor::repl_rebuild_text() {
m_repl.eval("(make-text)");
// NOTE - still no clue how this doesn't switch languages lol
// increment the language id of the in-memory text file so that it won't match the current
// language and the game will want to reload it asap
m_repl.eval("(1+! (-> *subtitle-text* lang))");
}
@ -176,16 +177,15 @@ void SubtitleEditor::draw_window() {
if (!is_scene_in_current_lang(m_new_scene_name) && !m_new_scene_name.empty() &&
!m_new_scene_group.empty()) {
if (ImGui::Button("Add Scene")) {
GameSubtitleSceneInfo newScene;
GameSubtitleSceneInfo newScene(SubtitleSceneKind::Movie);
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_subtitle_db.m_subtitle_groups->add_scene(newScene.m_sorting_group, newScene.m_name);
if (m_add_new_scene_as_current) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->m_scenes;
auto& scene_info = scenes[m_new_scene_name];
auto& scene_info = scenes.at(m_new_scene_name);
m_current_scene = &scene_info;
}
m_new_scene_name = "";
@ -225,7 +225,7 @@ void SubtitleEditor::draw_window() {
if (!is_scene_in_current_lang(m_new_scene_name) && !m_new_scene_name.empty() &&
!m_new_scene_group.empty()) {
if (ImGui::Button("Add Scene")) {
GameSubtitleSceneInfo newScene;
GameSubtitleSceneInfo newScene(SubtitleSceneKind::Hint);
newScene.m_name = m_new_scene_name;
if (m_new_scene_id == "0") {
newScene.m_kind = SubtitleSceneKind::Hint;
@ -235,7 +235,7 @@ void SubtitleEditor::draw_window() {
newScene.m_id = strtoul(m_new_scene_id.c_str(), nullptr, 16);
}
// currently hints have no way in the editor to add a line, so give us one for free
newScene.add_line(0, "", "", "", "", false);
newScene.add_line(0, "", "", false);
newScene.m_sorting_group = m_new_scene_group;
m_subtitle_db.m_banks.at(m_current_language)->add_scene(newScene);
m_subtitle_db.m_subtitle_groups->add_scene(newScene.m_sorting_group, newScene.m_name);
@ -378,7 +378,7 @@ bool SubtitleEditor::any_cutscenes_in_group(const std::string& group_name) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->m_scenes;
auto scenes_in_group = m_subtitle_db.m_subtitle_groups->m_groups[group_name];
for (auto& scene_name : scenes_in_group) {
auto& scene_info = scenes[scene_name];
auto& scene_info = scenes.at(scene_name);
if (scene_info.m_kind == SubtitleSceneKind::Movie) {
return true;
}
@ -407,7 +407,7 @@ bool SubtitleEditor::any_hints_in_group(const std::string& group_name) {
auto& scenes = m_subtitle_db.m_banks.at(m_current_language)->m_scenes;
auto scenes_in_group = m_subtitle_db.m_subtitle_groups->m_groups[group_name];
for (auto& scene_name : scenes_in_group) {
auto& scene_info = scenes[scene_name];
auto& scene_info = scenes.at(scene_name);
if (scene_info.m_kind != SubtitleSceneKind::Movie) {
return true;
}
@ -440,7 +440,7 @@ void SubtitleEditor::draw_all_scenes(std::string group_name, bool base_cutscenes
if (scenes.count(scene_name) == 0) {
continue;
}
auto& scene_info = scenes[scene_name];
auto& scene_info = scenes.at(scene_name);
// Don't duplicate entries
if (base_cutscenes && is_scene_in_current_lang(scene_name)) {
continue;
@ -497,7 +497,7 @@ void SubtitleEditor::draw_all_hints(std::string group_name, bool base_cutscenes)
if (scenes.count(scene_name) == 0) {
continue;
}
auto& scene_info = scenes[scene_name];
auto& scene_info = scenes.at(scene_name);
// Don't duplicate entries
if (base_cutscenes && is_scene_in_current_lang(scene_name)) {
continue;
@ -587,31 +587,34 @@ void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool cu
if (current_scene) {
draw_new_cutscene_line_form();
}
auto font =
get_font_bank(parse_text_only_version(m_subtitle_db.m_banks[m_current_language]->file_path));
for (size_t i = 0; i < scene.m_lines.size(); i++) {
auto& subtitleLine = scene.m_lines.at(i);
auto linetext = font->convert_game_to_utf8(subtitleLine.line.c_str());
std::string summary;
if (subtitleLine.line_utf8.empty()) {
if (linetext.empty()) {
summary = fmt::format("[{}] Clear Screen", subtitleLine.frame);
} else if (subtitleLine.line_utf8.length() >= 30) {
summary = fmt::format("[{}] {} - '{}...'", subtitleLine.frame, subtitleLine.speaker_utf8,
subtitleLine.line_utf8.substr(0, 30));
} else if (linetext.length() >= 30) {
summary = fmt::format("[{}] {} - '{}...'", subtitleLine.frame, subtitleLine.speaker,
linetext.substr(0, 30));
} else {
summary = fmt::format("[{}] {} - '{}'", subtitleLine.frame, subtitleLine.speaker_utf8,
subtitleLine.line_utf8.substr(0, 30));
summary = fmt::format("[{}] {} - '{}'", subtitleLine.frame, subtitleLine.speaker,
linetext.substr(0, 30));
}
if (subtitleLine.line_utf8.empty()) {
if (linetext.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, m_disabled_text_color);
} else if (subtitleLine.offscreen) {
ImGui::PushStyleColor(ImGuiCol_Text, m_offscreen_text_color);
}
if (ImGui::TreeNode(fmt::format("{}", i).c_str(), "%s", summary.c_str())) {
if (subtitleLine.line_utf8.empty() || subtitleLine.offscreen) {
if (linetext.empty() || subtitleLine.offscreen) {
ImGui::PopStyleColor();
}
ImGui::InputInt("Starting Frame", &subtitleLine.frame,
ImGuiInputTextFlags_::ImGuiInputTextFlags_CharsDecimal);
ImGui::InputText("Speaker", &subtitleLine.speaker_utf8);
ImGui::InputText("Text", &subtitleLine.line_utf8);
ImGui::InputText("Speaker", &subtitleLine.speaker);
ImGui::InputText("Text", &linetext);
ImGui::Checkbox("Offscreen?", &subtitleLine.offscreen);
ImGui::PushStyleColor(ImGuiCol_Button, m_warning_color);
if (scene.m_lines.size() > 1) { // prevent creating an empty scene
@ -621,9 +624,11 @@ void SubtitleEditor::draw_subtitle_options(GameSubtitleSceneInfo& scene, bool cu
}
ImGui::PopStyleColor();
ImGui::TreePop();
} else if (subtitleLine.line_utf8.empty() || subtitleLine.offscreen) {
} else if (linetext.empty() || subtitleLine.offscreen) {
ImGui::PopStyleColor();
}
auto newtext = font->convert_utf8_to_game_with_escape(linetext);
subtitleLine.line = newtext;
}
}
@ -642,7 +647,7 @@ void SubtitleEditor::draw_new_cutscene_line_form() {
} 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->add_line(m_current_scene_frame, m_current_scene_text,
m_current_scene_speaker, m_current_scene_offscreen);
}
}
@ -655,7 +660,7 @@ void SubtitleEditor::draw_new_cutscene_line_form() {
ImGui::SameLine();
}
if (ImGui::Button("Add Clear Screen Entry")) {
m_current_scene->add_line(m_current_scene_frame, "", "", "", "", false);
m_current_scene->add_line(m_current_scene_frame, "", "", false);
}
}
ImGui::NewLine();

View file

@ -1538,9 +1538,9 @@
(let ((s3-2 (new 'stack-no-clear 'vector))
(s4-3 (new 'stack-no-clear 'vector))
(f26-3 (+ 0.75
(* 0.0625 (cos (the float (* 4000 (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter))))) ;; changed for high fps
(* 0.0625 (cos (the float (shl (the int (* (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter))) 11)))) ;; changed for high fps
(* 0.125 (cos (the float (* 1500 (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter))))) ;; changed for high fps
(* 0.0625 (cos (the float (* 4000 (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter))))) ;; changed for high fps
(* 0.0625 (cos (the float (shl (the int (* (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter))) 11)))) ;; changed for high fps
(* 0.125 (cos (the float (* 1500 (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter))))) ;; changed for high fps
)
)
)
@ -2696,9 +2696,9 @@
(let* ((f2-5 (* 0.000012207031 (+ -409600.0 f0-41)))
(f30-2 (- 1.0 (fmax 0.0 (fmin 1.0 f2-5))))
(f28-4 (+ 0.5
(* 0.125 (cos (the float (* 4000 (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter))))) ;; changed for high fps
(* 0.125 (cos (the float (shl (the int (* (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter))) 11)))) ;; changed for high fps
(* 0.25 (cos (the float (* 1500 (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter))))) ;; changed for high fps
(* 0.125 (cos (the float (* 4000 (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter))))) ;; changed for high fps
(* 0.125 (cos (the float (shl (the int (* (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter))) 11)))) ;; changed for high fps
(* 0.25 (cos (the float (* 1500 (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter))))) ;; changed for high fps
)
)
(s3-1 (new 'stack-no-clear 'vector))

View file

@ -115,7 +115,7 @@
(a1-1 (the-as mei-texture-scroll (+ (the-as uint a1-0) (* (-> a1-0 texture-scroll-offset) 16))))
)
(when (< v1-1 32)
(let* ((a3-1 (the int (* (-> *display* time-adjust-ratio) (-> *display* integral-frame-counter)))) ;; changed for high fps
(let* ((a3-1 (the int (* (/ (-> *display* time-factor) 5) (-> *display* integral-frame-counter)))) ;; changed for high fps
(a2-3 (-> a1-1 time-factor))
(t0-2 (+ (ash 1 a2-3) -1))
)

View file

@ -66,8 +66,8 @@
(set! (-> obj win-height) height)
)
(else
(format 0 "Nope! No changing fullscreen resolution for now!")
(format #t "Nope! No changing fullscreen resolution for now!")
(format 0 "Nope! No changing fullscreen resolution for now!~%")
(format #t "Nope! No changing fullscreen resolution for now!~%")
)
)
(none))

View file

@ -280,7 +280,8 @@
"rebuild and reload subtitles."
`(begin
(asm-text-file subtitle :files ("game/assets/game_subtitle.gp"))
(+! (-> *subtitle-text* lang) (the-as pc-subtitle-lang 1))))
(if *subtitle-text*
(+! (-> *subtitle-text* lang) (the-as pc-subtitle-lang 1)))))
@ -597,6 +598,7 @@
;; get a subtitle info that matches our current status
(let ((keyframe (the subtitle-keyframe #f)))
(when *subtitle-text*
(case (-> self cur-channel)
(((pc-subtitle-channel movie))
;; cutscenes. get our cutscene.
@ -637,7 +639,7 @@
)
)
)
)
))
;; save whatever subtitle we got.
(set! (-> self want-subtitle) keyframe))

View file

@ -85,7 +85,7 @@ Val* Compiler::compile_asm_text_file(const goos::Object& form, const goos::Objec
va_check(form, args, {goos::ObjectType::SYMBOL}, {{"files", {true, goos::ObjectType::PAIR}}});
// list of files per text version.
std::unordered_map<GameTextVersion, std::vector<std::string>> inputs;
std::vector<std::string> inputs;
// what kind of text file?
const auto kind = symbol_string(args.unnamed.at(0));
@ -101,15 +101,11 @@ Val* Compiler::compile_asm_text_file(const goos::Object& form, const goos::Objec
// compile files.
if (kind == "subtitle") {
for (auto& [ver, in] : inputs) {
GameSubtitleDB db;
compile_game_subtitle(in, ver, db);
}
GameSubtitleDB db;
compile_game_subtitle(inputs, db);
} else if (kind == "text") {
for (auto& [ver, in] : inputs) {
GameTextDB db;
compile_game_text(in, ver, db);
}
GameTextDB db;
compile_game_text(inputs, db);
} else {
throw_compiler_error(form, "The option {} was not recognized for asm-text-file.", kind);
}

View file

@ -148,26 +148,22 @@ void compile_subtitle(GameSubtitleDB& db) {
/*!
* Read a game text description file and generate GOAL objects.
*/
void compile_game_text(const std::vector<std::string>& filenames,
GameTextVersion text_ver,
GameTextDB& db) {
void compile_game_text(const std::vector<std::string>& filenames, GameTextDB& db) {
goos::Reader reader;
for (auto& filename : filenames) {
fmt::print("[Build Game Text] {}\n", filename.c_str());
auto code = reader.read_from_file({filename});
parse_text(code, text_ver, db);
parse_text(code, db);
}
compile_text(db);
}
void compile_game_subtitle(const std::vector<std::string>& filenames,
GameTextVersion text_ver,
GameSubtitleDB& db) {
void compile_game_subtitle(const std::vector<std::string>& filenames, GameSubtitleDB& db) {
goos::Reader reader;
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, filename);
parse_subtitle(code, db, filename);
}
compile_subtitle(db);
}

View file

@ -9,13 +9,5 @@
#include "common/util/Assert.h"
#include "common/util/FontUtils.h"
void compile_game_text(const std::vector<std::string>& filenames,
GameTextVersion text_ver,
GameTextDB& db);
void compile_game_subtitle(const std::vector<std::string>& filenames,
GameTextVersion text_ver,
GameSubtitleDB& db);
void open_text_project(const std::string& kind,
const std::string& filename,
std::unordered_map<GameTextVersion, std::vector<std::string>>& inputs);
void compile_game_text(const std::vector<std::string>& filenames, GameTextDB& db);
void compile_game_subtitle(const std::vector<std::string>& filenames, GameSubtitleDB& db);

View file

@ -136,23 +136,15 @@ bool TextTool::needs_run(const ToolInput& task) {
}
std::vector<std::string> deps;
std::unordered_map<GameTextVersion, std::vector<std::string>> inputs;
open_text_project("text", task.input.at(0), inputs);
for (auto& [ver, files] : inputs) {
for (auto& in : files) {
deps.push_back(in);
}
}
open_text_project("text", task.input.at(0), deps);
return Tool::needs_run({task.input, deps, task.output, task.arg});
}
bool TextTool::run(const ToolInput& task) {
GameTextDB db;
std::unordered_map<GameTextVersion, std::vector<std::string>> inputs;
std::vector<std::string> inputs;
open_text_project("text", task.input.at(0), inputs);
for (auto& [ver, in] : inputs) {
compile_game_text(in, ver, db);
}
compile_game_text(inputs, db);
return true;
}
@ -170,13 +162,7 @@ bool SubtitleTool::needs_run(const ToolInput& task) {
}
std::vector<std::string> deps;
std::unordered_map<GameTextVersion, std::vector<std::string>> inputs;
open_text_project("subtitle", task.input.at(0), inputs);
for (auto& [ver, files] : inputs) {
for (auto& in : files) {
deps.push_back(in);
}
}
open_text_project("subtitle", task.input.at(0), deps);
return Tool::needs_run({task.input, deps, task.output, task.arg});
}
@ -184,11 +170,9 @@ 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;
std::vector<std::string> inputs;
open_text_project("subtitle", task.input.at(0), inputs);
for (auto& [ver, in] : inputs) {
compile_game_subtitle(in, ver, db);
}
compile_game_subtitle(inputs, db);
return true;
}

View file

@ -1,2 +1,2 @@
(text
(jak1-v1 "test/test_data/test_game_text.txt"))
(file "test/test_data/test_game_text.txt"))

View file

@ -1,5 +1,6 @@
(group-name "test")
(language-id 0 1 2)
(text-version jak1-v2)
(#x123 "language 0"
"language 1"