jak-project/decompiler/extractor/main.cpp
Tyler Wilding b4113dda67
Some checks failed
Build / 🖥️ Windows (push) Has been cancelled
Build / 🐧 Linux (push) Has been cancelled
Build / 🍎 MacOS (push) Has been cancelled
Lint / 📝 Formatting (push) Has been cancelled
Lint / 📝 Required Checks (push) Has been cancelled
Lint / 📝 Optional Checks (push) Has been cancelled
decompiler: Cleanup duplication in extractor/decompiler and make it easier to enable streamed audio ripping from CLI (#3560)
This centralizes the code that both `extractor` and the decompiler
executes. In the past this code was partially-duplicated, meaning that
the `extractor` could only do _some_ operations and not others (ie.
could not extract the audio files).

I also simplified the process to enable audio streaming in the
configuration. This is to support a new feature in the launcher that
allows you to enable these options for the decompiler:


![image](https://github.com/open-goal/jak-project/assets/13153231/8e6c20a1-8b5b-46f0-bceb-7644f713989f)
2024-06-29 01:18:39 -04:00

391 lines
15 KiB
C++

#include "common/log/log.h"
#include "common/util/Assert.h"
#include "common/util/FileUtil.h"
#include "common/util/read_iso_file.h"
#include "common/util/term_util.h"
#include "common/util/unicode_util.h"
#include "decompiler/config.h"
#include "decompiler/decompilation_process.h"
#include "decompiler/extractor/extractor_util.h"
#include "goalc/compiler/Compiler.h"
#include "third-party/CLI11.hpp"
// used for - decompiler_out/<jak1> and iso_data/<jak1>
const std::unordered_map<std::string, std::string> data_subfolders = {{"jak1", "jak1"},
{"jak2", "jak2"}};
IsoFile extract_files(fs::path input_file_path, fs::path extracted_iso_path) {
lg::info(
"Note: Provided game data path '{}' points to a file, not a directory. Assuming it's an ISO "
"file and attempting to extract!",
input_file_path.string());
fs::create_directories(extracted_iso_path);
auto fp = file_util::open_file(input_file_path, "rb");
ASSERT_MSG(fp, "failed to open input ISO file");
IsoFile iso = unpack_iso_files(fp, extracted_iso_path, true, true);
fclose(fp);
return iso;
}
std::tuple<std::optional<ISOMetadata>, ExtractorErrorCode> validate(
const fs::path& extracted_iso_path,
const uint64_t expected_hash,
const int expected_num_files) {
if (!fs::exists(extracted_iso_path / "DGO")) {
lg::error("input folder doesn't have a DGO folder. Is this the right input?");
return {std::nullopt, ExtractorErrorCode::VALIDATION_BAD_EXTRACTION};
}
const auto [serial, elf_hash] = findElfFile(extracted_iso_path);
if (!serial || !elf_hash) {
lg::error("Unable to locate a Serial/ELF file!");
// No point in continuing here
return {std::nullopt, ExtractorErrorCode::VALIDATION_CANT_LOCATE_ELF};
}
// Find the game in our tracking database
const auto& iso_database = extractor_iso_database();
auto dbEntry = iso_database.find(serial.value());
if (dbEntry == iso_database.end()) {
lg::error("Serial '{}' not found in the validation database", serial.value());
log_potential_new_db_entry(ExtractorErrorCode::VALIDATION_SERIAL_MISSING_FROM_DB,
serial.value(), elf_hash.value(), expected_num_files, expected_hash);
return {std::nullopt, ExtractorErrorCode::VALIDATION_SERIAL_MISSING_FROM_DB};
}
auto& metaMap = dbEntry->second;
auto meta_entry = metaMap.find(elf_hash.value());
if (meta_entry == metaMap.end()) {
lg::error(
"ELF Hash '{}' not found in the validation database, is this a new or "
"modified version of the same game?",
elf_hash.value());
log_potential_new_db_entry(ExtractorErrorCode::VALIDATION_ELF_MISSING_FROM_DB, serial.value(),
elf_hash.value(), expected_num_files, expected_hash);
return {std::nullopt, ExtractorErrorCode::VALIDATION_ELF_MISSING_FROM_DB};
}
auto& version_info = meta_entry->second;
// Print out some information
lg::info("Detected Game Metadata:");
lg::info("\tDetected - {}", version_info.canonical_name);
lg::info("\tRegion - {}", get_territory_name(version_info.region));
lg::info("\tSerial - {}", dbEntry->first);
lg::info("\tUses Decompiler Config Version - {}", version_info.decomp_config_version);
// - Number of Files
if (version_info.num_files != expected_num_files) {
lg::error("Extracted an unexpected number of files. Expected '{}', Actual '{}'",
version_info.num_files, expected_num_files);
return {std::nullopt, ExtractorErrorCode::VALIDATION_INCORRECT_EXTRACTION_COUNT};
}
// Check the ISO Hash
if (version_info.contents_hash.count(expected_hash) == 0) {
std::string all_expected;
for (const auto& hash : version_info.contents_hash) {
all_expected += fmt::format("{}, ", hash);
}
lg::error("Overall ISO content's hash does not match. Expected '{}', Actual '{}'", all_expected,
expected_hash);
return {std::nullopt, ExtractorErrorCode::VALIDATION_FILE_CONTENTS_UNEXPECTED};
}
return {
std::make_optional(version_info),
ExtractorErrorCode::SUCCESS,
};
}
ExtractorErrorCode decompile(const fs::path& in_folder,
const std::string& data_subfolder,
const std::string& config_override) {
// Determine which config to use from the database
const auto version_info = get_version_info_or_default(in_folder);
decompiler::Config config = decompiler::read_config_file(
file_util::get_jak_project_dir() / "decompiler" / "config" / version_info.game_name /
fmt::format("{}_config.jsonc", version_info.game_name),
version_info.decomp_config_version, config_override);
auto out_folder = file_util::get_jak_project_dir() / "decompiler_out" / data_subfolder;
const auto result = run_decompilation_process(config, in_folder, out_folder, true);
if (result != 0) {
return ExtractorErrorCode::DECOMPILATION_GENERIC_ERROR;
}
return ExtractorErrorCode::SUCCESS;
}
const std::unordered_map<std::string, GameIsoFlags> game_iso_flag_names = {
{"jak1-black-label", FLAG_JAK1_BLACK_LABEL}};
ExtractorErrorCode compile(const fs::path& iso_data_path, const std::string& data_subfolder) {
// Determine which config to use from the database
const auto version_info = get_version_info_or_default(iso_data_path);
Compiler compiler(game_name_to_version(version_info.game_name));
compiler.make_system().set_constant("*iso-data*", absolute(iso_data_path).string());
compiler.make_system().set_constant("*use-iso-data-path*", true);
file_util::set_iso_data_dir(absolute(iso_data_path));
lg::info("set iso_data_dir to {}", absolute(iso_data_path).string());
int flags = 0;
for (const auto& flag : version_info.flags) {
if (auto it = game_iso_flag_names.find(flag); it != game_iso_flag_names.end()) {
flags |= it->second;
}
}
compiler.get_goos().set_global_variable_to_int("*default-territory*", version_info.region);
if (version_info.game_name == "jak1") {
compiler.make_system().set_constant("*jak1-full-game*", !(flags & FLAG_JAK1_BLACK_LABEL));
compiler.get_goos().set_global_variable_to_symbol(
"*jak1-full-game*", !(flags & FLAG_JAK1_BLACK_LABEL) ? "#t" : "#f");
}
auto project_path = file_util::get_jak_project_dir() / "goal_src" / data_subfolder / "game.gp";
if (!fs::exists(project_path)) {
return ExtractorErrorCode::COMPILATION_BAD_PROJECT_PATH;
}
compiler.make_system().load_project_file(project_path.string());
compiler.run_front_end_on_string("(mi)");
return ExtractorErrorCode::SUCCESS;
}
void launch_game(const std::string& game_version) {
system(fmt::format("\"{}\" -g {}", (file_util::get_jak_project_dir() / "../gk").string(),
game_version)
.c_str());
}
int main(int argc, char** argv) {
ArgumentGuard u8_guard(argc, argv);
fs::path input_file_path;
fs::path project_path_override;
fs::path extraction_path;
bool flag_runall = false;
bool flag_extract = false;
bool flag_fail_on_validation = false;
bool flag_decompile = false;
bool flag_compile = false;
bool flag_play = false;
bool flag_folder = false;
std::string game_name = "jak1";
std::string decomp_config_override = "{}";
lg::initialize();
CLI::App app{"OpenGOAL Extractor (ISO Tools + Decompiler + Compiler)"};
app.add_option("game-files-path", input_file_path,
"The path to the folder with the ISO extracted or the ISO itself")
->required();
app.add_option("--proj-path", project_path_override,
"Explicitly set the location of the 'data/' folder");
app.add_option("--extract-path", extraction_path,
"Explicitly set the location for where the ISO should be extracted");
app.add_option("-g,--game", game_name, "Specify the game name, defaults to 'jak1'");
app.add_option(
"--decomp-config-override", decomp_config_override,
"JSON provided will be merged with the decompiler config, use to override options");
app.add_flag("-a,--all", flag_runall, "Run all steps, from extraction to playing the game");
app.add_flag("-e,--extract", flag_extract, "Extract the ISO");
app.add_flag("-v,--validate", flag_fail_on_validation,
"Fail on validation errors during extraction");
app.add_flag("-d,--decompile", flag_decompile, "Decompile the game data");
app.add_flag("-c,--compile", flag_compile, "Compile the game");
app.add_flag("-p,--play", flag_play, "Play the game");
app.add_flag("-f,--folder", flag_folder, "Take ISO input from a folder");
define_common_cli_arguments(app);
app.validate_positionals();
CLI11_PARSE(app, argc, argv);
lg::info("Working Directory - {}", fs::current_path().string());
// If no flag is set, we default to running everything
if (!flag_extract && !flag_decompile && !flag_compile && !flag_play) {
lg::info("Running all steps, no flags provided!");
flag_runall = true;
}
if (flag_runall) {
flag_extract = true;
flag_decompile = true;
flag_compile = true;
flag_play = true;
}
// - SETUP
if (!project_path_override.empty()) {
if (!fs::exists(project_path_override)) {
lg::error("Error: project path override '{}' does not exist", project_path_override.string());
return static_cast<int>(ExtractorErrorCode::INVALID_CLI_INPUT);
}
auto ok = file_util::setup_project_path(project_path_override);
if (!ok) {
lg::error("Could not setup project path!");
return 1;
}
} else {
auto ok = file_util::setup_project_path({});
if (!ok) {
lg::error("Could not setup project path!");
return 1;
}
}
try {
lg::set_file("extractor");
if (_cli_flag_disable_ansi) {
lg::disable_ansi_colors();
}
} catch (const std::exception& e) {
lg::error("Failed to setup logging: {}", e.what());
return 1;
}
fs::path iso_data_path;
// - INPUT VALIDATION
if (!fs::exists(input_file_path)) {
lg::error("Error: input game file path '{}' does not exist", input_file_path.string());
return static_cast<int>(ExtractorErrorCode::INVALID_CLI_INPUT);
}
if (data_subfolders.count(game_name) == 0) {
lg::error("Error: input game name '{}' is not valid", game_name);
return static_cast<int>(ExtractorErrorCode::INVALID_CLI_INPUT);
}
std::string data_subfolder = data_subfolders.at(game_name);
if (flag_extract) {
// we extract to a temporary location because we don't know what we're extracting yet!
fs::path temp_iso_extract_location = file_util::get_jak_project_dir() / "iso_data" / "_temp";
if (!extraction_path.empty()) {
temp_iso_extract_location = extraction_path / "_temp";
}
lg::info("Extracting ISO to temporary dir at: {}", temp_iso_extract_location.string());
if (input_file_path != temp_iso_extract_location) {
// in case input is also output, don't just wipe everything (weird)
fs::remove_all(temp_iso_extract_location);
}
fs::create_directories(temp_iso_extract_location);
if (fs::is_regular_file(input_file_path)) {
// If it's a file, then it better be an iso file
const auto [iso_ok, iso_code] = is_iso_file(input_file_path);
if (!iso_ok) {
return static_cast<int>(iso_code);
}
// Extract to the temporary location
const auto iso_file = extract_files(input_file_path, temp_iso_extract_location);
// Get hash and file count
const auto [hash, file_count] = calculate_extraction_hash(iso_file);
// Validate the result to determine the release
const auto [version_info, validate_code] =
validate(temp_iso_extract_location, hash, file_count);
if (validate_code == ExtractorErrorCode::VALIDATION_BAD_EXTRACTION ||
(flag_fail_on_validation && validate_code != ExtractorErrorCode::SUCCESS)) {
return static_cast<int>(validate_code);
}
// Finalize the folder name now that we know where it should go
if (!version_info) {
lg::error("could not verify release, so not finalizing iso_data, leaving in '_temp'");
iso_data_path = temp_iso_extract_location;
} else {
// We know the version since we just extracted it, so the user didn't need to provide this
// explicitly
data_subfolder = data_subfolders.at(version_info->game_name);
if (!extraction_path.empty()) {
iso_data_path = extraction_path / data_subfolder;
} else {
iso_data_path = file_util::get_jak_project_dir() / "iso_data" / data_subfolder;
}
if (fs::exists(iso_data_path) && iso_data_path != temp_iso_extract_location) {
fs::remove_all(iso_data_path);
}
// std::filesystem doesn't have a rename for dirs...
// NOTE - potential disaster here, don't do either if the directories are the same location
// or don't copy if the temp location is _inside_ the destination directory
if (!file_util::is_dir_in_dir(iso_data_path, temp_iso_extract_location)) {
fs::copy(temp_iso_extract_location, iso_data_path, fs::copy_options::recursive);
}
if (iso_data_path != temp_iso_extract_location) {
// in case input is also output, don't just wipe everything (weird)
fs::remove_all(temp_iso_extract_location);
}
}
} else if (fs::is_directory(input_file_path)) {
if (!flag_folder) {
// if we didn't request a folder explicitly, but we got one, assume something went wrong.
lg::error("got a folder, but didn't get folder flag");
return static_cast<int>(ExtractorErrorCode::INVALID_CLI_INPUT);
}
iso_data_path = input_file_path;
// Get hash and file count
const auto [hash, file_count] = calculate_extraction_hash(iso_data_path);
// Validate
auto [version_info, validate_code] = validate(iso_data_path, hash, file_count);
if (validate_code == ExtractorErrorCode::VALIDATION_BAD_EXTRACTION ||
(flag_fail_on_validation && validate_code != ExtractorErrorCode::SUCCESS)) {
return static_cast<int>(validate_code);
}
}
// write out a json file with some metadata for the game
if (fs::exists(iso_data_path / "buildinfo.json")) {
fs::remove(iso_data_path / "buildinfo.json");
}
const auto [serial, elf_hash] = findElfFile(iso_data_path);
BuildInfo build_info;
if (serial.has_value()) {
build_info.serial = serial.value();
}
if (elf_hash.has_value()) {
build_info.elf_hash = elf_hash.value();
}
const nlohmann::json json_data{build_info};
file_util::write_text_file((iso_data_path / "buildinfo.json").string(), json_data.dump(2));
} else {
// If we did not extract, we have no clue what game the user is trying to decompile / compile
// this is why the user has to specify this!
if (flag_folder) {
iso_data_path = input_file_path;
} else {
iso_data_path = file_util::get_jak_project_dir() / "iso_data" / data_subfolder;
}
}
if (flag_decompile) {
try {
const auto status_code = decompile(iso_data_path, data_subfolder, decomp_config_override);
if (status_code != ExtractorErrorCode::SUCCESS) {
return static_cast<int>(status_code);
}
} catch (std::exception& e) {
lg::error("Error during decompile: {}", e.what());
return static_cast<int>(ExtractorErrorCode::DECOMPILATION_GENERIC_ERROR);
}
}
if (flag_compile) {
const auto status_code = compile(iso_data_path, data_subfolder);
if (status_code != ExtractorErrorCode::SUCCESS) {
return static_cast<int>(status_code);
}
}
if (flag_play) {
launch_game(game_name);
}
return 0;
}