From 8cde7983a646b7de63872ac393a15cb1579b045c Mon Sep 17 00:00:00 2001 From: gered Date: Thu, 25 Mar 2021 14:12:24 -0400 Subject: [PATCH] add quest_info tool --- CMakeLists.txt | 4 + quest_info.c | 439 +++++++++++++++++++++++++++++++++++++++++++++++++ utils.c | 14 ++ utils.h | 2 + 4 files changed, 459 insertions(+) create mode 100644 quest_info.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bb59c1..67da998 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,3 +26,7 @@ target_link_libraries(bindat_to_gcdl ${SYLVERANT_LIBRARY}) # gci_extract add_executable(gci_extract gci_extract.c quests.c fuzziqer_prs.c utils.c) target_link_libraries(gci_extract ${SYLVERANT_LIBRARY}) + +# quest_info +add_executable(quest_info quest_info.c quests.c fuzziqer_prs.c utils.c) +target_link_libraries(quest_info ${SYLVERANT_LIBRARY}) diff --git a/quest_info.c b/quest_info.c new file mode 100644 index 0000000..188b841 --- /dev/null +++ b/quest_info.c @@ -0,0 +1,439 @@ +#include +#include +#include +#include + +#include +#include "fuzziqer_prs.h" + +#include "retvals.h" +#include "utils.h" +#include "quests.h" + +typedef struct _PACKED_ { + uint8_t pkt_id; + uint8_t pkt_flags; + uint16_t pkt_size; +} PACKET_HEADER; + +#define PACKET_TYPE_ERROR 0 +#define PACKET_TYPE_HEADER 1 +#define PACKET_TYPE_DATA 2 +#define PACKET_TYPE_EOF 4 // not really a packet type, lol + +#define QST_TYPE_NONE 0 +#define QST_TYPE_ONLINE 1 +#define QST_TYPE_DOWNLOAD 2 + +const char* get_area_string(int area, int episode) { + if (episode == 0) { + switch (area) { + case 0: return "Pioneer 2"; + case 1: return "Forest 1"; + case 2: return "Forest 2"; + case 3: return "Caves 1"; + case 4: return "Caves 2"; + case 5: return "Caves 3"; + case 6: return "Mines 1"; + case 7: return "Mines 2"; + case 8: return "Ruins 1"; + case 9: return "Ruins 2"; + case 10: return "Ruins 3"; + case 11: return "Under the Dome"; + case 12: return "Underground Channel"; + case 13: return "Monitor Room"; + case 14: return "????"; + case 15: return "Visual Lobby"; + case 16: return "VR Spaceship Alpha"; + case 17: return "VR Temple Alpha"; + default: return "Invalid Area"; + } + } else if (episode == 1) { + switch (area) { + case 0: return "Lab"; + case 1: return "VR Temple Alpha"; + case 2: return "VR Temple Beta"; + case 3: return "VR Spaceship Alpha"; + case 4: return "VR Spaceship Beta"; + case 5: return "Central Control Area"; + case 6: return "Jungle North"; + case 7: return "Jungle East"; + case 8: return "Mountain"; + case 9: return "Seaside"; + case 10: return "Seabed Upper"; + case 11: return "Seabed Lower"; + case 12: return "Cliffs of Gal Da Val"; + case 13: return "Test Subject Disposal Area"; + case 14: return "VR Temple Final"; + case 15: return "VR Spaceship Final"; + case 16: return "Seaside Night"; + case 17: return "Control Tower"; + default: return "Invalid Area"; + } + } else { + return "Invalid Episode"; + } +} + +void display_info(uint8_t *bin_data, size_t bin_length, uint8_t *dat_data, size_t dat_length, int qst_type) { + int validation_result; + int32_t result; + uint8_t *decompressed_bin_data = NULL; + uint8_t *decompressed_dat_data = NULL; + size_t decompressed_bin_length, decompressed_dat_length; + + printf("Decompressing .bin data ...\n"); + result = fuzziqer_prs_decompress_buf(bin_data, &decompressed_bin_data, bin_length); + if (result < 0) { + printf("Error code %d decompressing .bin data.\n", result); + goto error; + } + decompressed_bin_length = result; + + printf("Decompressing .dat data ...\n"); + result = fuzziqer_prs_decompress_buf(dat_data, &decompressed_dat_data, dat_length); + if (result < 0) { + printf("Error code %d decompressing .dat data.\n", result); + goto error; + } + decompressed_dat_length = result; + + + printf("Validating .bin data ...\n"); + QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)decompressed_bin_data; + validation_result = validate_quest_bin(bin_header, decompressed_bin_length, true); + if (validation_result == QUESTBIN_ERROR_SMALLER_BIN_SIZE) { + printf("WARNING: Decompressed .bin data is larger than expected. Proceeding using the smaller .bin header bin_size value ...\n"); + decompressed_bin_length = bin_header->bin_size; + } else if (validation_result == QUESTBIN_ERROR_LARGER_BIN_SIZE) { + if ((decompressed_bin_length + 1) == bin_header->bin_size) { + printf("WARNING: Decompressed .bin data is 1 byte smaller than the .bin header bin_size specifies. Correcting by adding a null byte ...\n"); + ++decompressed_bin_length; + decompressed_bin_data = realloc(decompressed_bin_data, decompressed_bin_length); + decompressed_bin_data[decompressed_bin_length - 1] = 0; + } + } else if (validation_result) { + printf("Aborting due to invalid quest .bin data.\n"); + goto error; + } + + + printf("Validating .dat data ...\n"); + validation_result = validate_quest_dat(decompressed_dat_data, decompressed_dat_length, true); + if (validation_result != QUESTDAT_ERROR_EOF_EMPTY_TABLE) { + printf("Aborting due to invalid quest .dat data.\n"); + goto error; + } + + printf("\n\n"); + + printf("QUEST FILE FORMAT: "); + switch (qst_type) { + case QST_TYPE_NONE: printf("raw .bin/.dat\n"); break; + case QST_TYPE_DOWNLOAD: printf("download/offline .qst (0x%02X)\n", PACKET_ID_QUEST_INFO_DOWNLOAD); break; + case QST_TYPE_ONLINE: printf("online .qst (0x%02X)\n", PACKET_ID_QUEST_INFO_ONLINE); break; + default: printf("unknown\n"); + } + printf("\n"); + + + printf("QUEST .BIN FILE\n"); + printf("======================================================================\n"); + printf("name: %s\n", bin_header->name); + printf("download flag: %d\n", bin_header->download); + printf("quest_number: as byte: %d as word: %d\n", bin_header->quest_number_byte, bin_header->quest_number_word); + printf("episode: %d (%d)\n", bin_header->episode, bin_header->episode + 1); + printf("xffffffff: 0x%08x\n", bin_header->xffffffff); + printf("unknown: 0x%02x\n", bin_header->unknown); + printf("\n"); + printf("short_description:\n%s\n\n", bin_header->short_description); + printf("long_description:\n%s\n", bin_header->long_description); + printf("object_code_offset: %d\n", bin_header->object_code_offset); + printf("function_offset_table_offset: %d\n", bin_header->function_offset_table_offset); + printf("object_code_size: %d\n", (bin_header->function_offset_table_offset - bin_header->object_code_offset)); + printf("function_offset_table_size: %d\n", (bin_header->bin_size - bin_header->function_offset_table_offset)); + + + printf("\n\n"); + printf("QUEST .DAT FILE\n"); + printf("======================================================================\n"); + + int table_index = 0; + uint32_t offset = 0; + while (offset < decompressed_dat_length) { + QUEST_DAT_TABLE_HEADER *table_header = (QUEST_DAT_TABLE_HEADER*)(decompressed_dat_data + offset); + + printf("Table index %d - ", table_index); + switch (table_header->type) { + case 1: + printf("Object\n"); + printf("table_body_size: %d\n", table_header->table_body_size); + printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area); + printf("object count: %d\n", table_header->table_body_size / 68); + break; + case 2: + printf("NPC\n"); + printf("table_body_size: %d\n", table_header->table_body_size); + printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area); + printf("npc count: %d\n", table_header->table_body_size / 72); + break; + case 3: + printf("Wave\n"); + printf("table_body_size: %d\n", table_header->table_body_size); + printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area); + break; + case 4: + printf("Challenge Mode Spawn Points\n"); + printf("table_body_size: %d\n", table_header->table_body_size); + printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area); + break; + case 5: + printf("Challenge Mode (?)\n"); + printf("table_body_size: %d\n", table_header->table_body_size); + printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area); + break; + default: + if (table_header->type == 0 && table_header->table_size == 0 && table_header->area == 0 && table_header->table_body_size == 0) + printf("EOF marker\n"); + else { + printf("Unknown\n"); + printf("type: %d\n", table_header->type); + printf("table_body_size: %d\n", table_header->table_body_size); + printf("area: %d\n", table_header->area); + } + break; + } + printf("\n"); + + offset += sizeof(QUEST_DAT_TABLE_HEADER); + offset += table_header->table_body_size; + ++table_index; + } + +error: + free(decompressed_bin_data); + free(decompressed_dat_data); +} + +int read_next_qst_packet(FILE *fp, QST_HEADER *out_header_packet, QST_DATA_CHUNK *out_data_packet) { + size_t bytes_read; + PACKET_HEADER packet_header; + + bytes_read = fread(&packet_header, 1, sizeof(PACKET_HEADER), fp); + if (bytes_read == 0 && feof(fp)) + return PACKET_TYPE_EOF; + if (bytes_read != sizeof(PACKET_HEADER)) + return PACKET_TYPE_ERROR; + + if (packet_header.pkt_size == sizeof(QST_HEADER) && + (packet_header.pkt_id == PACKET_ID_QUEST_INFO_ONLINE || + packet_header.pkt_id == PACKET_ID_QUEST_INFO_DOWNLOAD)) { + memcpy(out_header_packet, &packet_header, sizeof(PACKET_HEADER)); + size_t remaining_bytes = sizeof(QST_HEADER) - sizeof(PACKET_HEADER); + bytes_read = fread((uint8_t*)out_header_packet + sizeof(PACKET_HEADER), 1, remaining_bytes, fp); + if (bytes_read != remaining_bytes) + return PACKET_TYPE_ERROR; + else + return PACKET_TYPE_HEADER; + + } else if (packet_header.pkt_size == sizeof(QST_DATA_CHUNK) && + (packet_header.pkt_id == PACKET_ID_QUEST_CHUNK_ONLINE || + packet_header.pkt_id == PACKET_ID_QUEST_CHUNK_DOWNLOAD)) { + memcpy(out_data_packet, &packet_header, sizeof(PACKET_HEADER)); + size_t remaining_bytes = sizeof(QST_DATA_CHUNK) - sizeof(PACKET_HEADER); + bytes_read = fread((uint8_t*)out_data_packet + sizeof(PACKET_HEADER), 1, remaining_bytes, fp); + if (bytes_read != remaining_bytes) + return PACKET_TYPE_ERROR; + else + return PACKET_TYPE_DATA; + + } else + return PACKET_TYPE_ERROR; +} + +int load_quest_from_qst(const char *filename, uint8_t **out_bin_data, size_t *out_bin_length, uint8_t **out_dat_data, size_t *out_dat_length, int *out_qst_type) { + int returncode; + FILE *fp = NULL; + uint8_t *bin_data = NULL; + uint8_t *dat_data = NULL; + int qst_type; + + fp = fopen(filename, "rb"); + if (!fp) { + returncode = ERROR_FILE_NOT_FOUND; + goto error; + } + + char bin_filename[QUEST_FILENAME_MAX_LENGTH] = ""; + char dat_filename[QUEST_FILENAME_MAX_LENGTH] = ""; + size_t bin_data_length, dat_data_length; + size_t bin_data_pos, dat_data_pos; + + while (!feof(fp)) { + QST_HEADER header; + QST_DATA_CHUNK data; + int type = read_next_qst_packet(fp, &header, &data); + + if (type == PACKET_TYPE_EOF && bin_data && dat_data) + break; + + if (type == PACKET_TYPE_ERROR) { + returncode = ERROR_BAD_DATA; + goto error; + + } else if (type == PACKET_TYPE_HEADER) { + //CRYPT_PrintData(&header, sizeof(QST_HEADER)); + if (string_ends_with(header.filename, ".bin")) { + strncpy(bin_filename, header.filename, QUEST_FILENAME_MAX_LENGTH); + bin_data_length = header.size; + bin_data_pos = 0; + bin_data = malloc(bin_data_length); + } else if (string_ends_with(header.filename, ".dat")) { + strncpy(dat_filename, header.filename, QUEST_FILENAME_MAX_LENGTH); + dat_data_length = header.size; + dat_data_pos = 0; + dat_data = malloc(dat_data_length); + } else { + returncode = ERROR_BAD_DATA; + goto error; + } + + if (header.pkt_id == PACKET_ID_QUEST_INFO_ONLINE) + qst_type = QST_TYPE_ONLINE; + else + qst_type = QST_TYPE_DOWNLOAD; + + } else if (type == PACKET_TYPE_DATA) { + //CRYPT_PrintData(&data, sizeof(QST_DATA_CHUNK)); + if (strncmp(data.filename, bin_filename, QUEST_FILENAME_MAX_LENGTH) == 0) { + memcpy(bin_data + bin_data_pos, data.data, data.size); + bin_data_pos += data.size; + + } else if (strncmp(data.filename, dat_filename, QUEST_FILENAME_MAX_LENGTH) == 0) { + memcpy(dat_data + dat_data_pos, data.data, data.size); + dat_data_pos += data.size; + + } else { + returncode = ERROR_BAD_DATA; + goto error; + } + } + } + + fclose(fp); + + *out_bin_length = bin_data_length; + *out_dat_length = dat_data_length; + *out_bin_data = bin_data; + *out_dat_data = dat_data; + *out_qst_type = qst_type; + + return SUCCESS; + +error: + fclose(fp); + free(bin_data); + free(dat_data); + return returncode; +} + +int decrypt_qst_bindat(uint8_t *bin_data, size_t *bin_length, uint8_t *dat_data, size_t *dat_length) { + DOWNLOAD_QUEST_CHUNKS_HEADER *bin_dl_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)bin_data; + DOWNLOAD_QUEST_CHUNKS_HEADER *dat_dl_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)dat_data; + + CRYPT_SETUP bin_cs, dat_cs; + CRYPT_CreateKeys(&bin_cs, &bin_dl_header->crypt_key, CRYPT_PC); + CRYPT_CreateKeys(&dat_cs, &dat_dl_header->crypt_key, CRYPT_PC); + + uint8_t *actual_bin_data = bin_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER); + uint8_t *actual_dat_data = dat_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER); + size_t decrypted_bin_length = *bin_length - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER); + size_t decrypted_dat_length = *dat_length - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER); + CRYPT_CryptData(&bin_cs, bin_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), decrypted_bin_length, 0); + CRYPT_CryptData(&dat_cs, dat_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), decrypted_dat_length, 0); + + memmove(bin_data, actual_bin_data, decrypted_bin_length); + memmove(dat_data, actual_dat_data, decrypted_dat_length); + + *bin_length = decrypted_bin_length; + *dat_length = decrypted_dat_length; + + return SUCCESS; +} + +int load_quest_from_bindat(const char *bin_filename, const char *dat_filename, uint8_t **out_bin_data, size_t *out_bin_length, uint8_t **out_dat_data, size_t *out_dat_length) { + int returncode; + uint8_t *bin_data = NULL; + uint8_t *dat_data = NULL; + uint32_t bin_data_length, dat_data_length; + + returncode = read_file(bin_filename, &bin_data, &bin_data_length); + if (returncode) + goto error; + + returncode = read_file(dat_filename, &dat_data, &dat_data_length); + if (returncode) + goto error; + + *out_bin_length = bin_data_length; + *out_dat_length = dat_data_length; + *out_bin_data = bin_data; + *out_dat_data = dat_data; + + return SUCCESS; + +error: + free(bin_data); + free(dat_data); + return returncode; +} + +int main(int argc, char *argv[]) { + int returncode; + + if (argc != 2 && argc != 3) { + printf("Usage: quest_info quest.bin quest.dat\n"); + printf(" quest_info quest.qst\n"); + return 1; + } + + uint8_t *bin_data = NULL; + uint8_t *dat_data = NULL; + size_t bin_data_size, dat_data_size; + int qst_type = QST_TYPE_NONE; + + if (argc == 2) { + printf("Reading .qst file: %s\n", argv[1]); + returncode = load_quest_from_qst(argv[1], &bin_data, &bin_data_size, &dat_data, &dat_data_size, &qst_type); + if (returncode) { + printf("Error code %d (%s) loading quest: %s\n", returncode, get_error_message(returncode), argv[1]); + goto error; + } + if (qst_type == QST_TYPE_DOWNLOAD) { + printf("Decrypting download .qst data ...\n"); + returncode = decrypt_qst_bindat(bin_data, &bin_data_size, dat_data, &dat_data_size); + if (returncode) { + printf("Error code %d (%s) while decrypting .qst contents", returncode, get_error_message(returncode)); + goto error; + } + } + } else { + printf("Reading .bin file %s and .dat file %s ... \n", argv[1], argv[2]); + returncode = load_quest_from_bindat(argv[1], argv[2], &bin_data, &bin_data_size, &dat_data, &dat_data_size); + if (returncode) { + printf("Error code %d (%s) loading quest files %s and %s\n", returncode, get_error_message(returncode), argv[1], argv[2]); + goto error; + } + } + + display_info(bin_data, bin_data_size, dat_data, dat_data_size, qst_type); + + returncode = 0; + goto quit; +error: + returncode = 1; +quit: + free(bin_data); + free(dat_data); + return returncode; +} diff --git a/utils.c b/utils.c index b4c6c01..133d183 100644 --- a/utils.c +++ b/utils.c @@ -101,6 +101,20 @@ char* append_string(const char *a, const char *b) { return result; } +bool string_ends_with(const char *s, const char *suffix) { + if (!s || !suffix) + return false; + + size_t s_len = strlen(s); + size_t suffix_len = strlen(suffix); + if (suffix_len > s_len) + return false; + else { + const char *end_of_s = s + (s_len - suffix_len); + return strncmp(end_of_s, suffix, suffix_len) == 0; + } +} + const char* get_error_message(int retvals_error_code) { retvals_error_code = abs(retvals_error_code); diff --git a/utils.h b/utils.h index 68f86ab..587457a 100644 --- a/utils.h +++ b/utils.h @@ -2,6 +2,7 @@ #define UTILS_H_INCLUDED #include +#include #include "retvals.h" @@ -10,6 +11,7 @@ int write_file(const char *filename, const void *data, size_t size); int get_filesize(const char *filename, size_t *out_size); const char* path_to_filename(const char *path); char* append_string(const char *a, const char *b); +bool string_ends_with(const char *s, const char *suffix); const char* get_error_message(int retvals_error_code);