diff --git a/CMakeLists.txt b/CMakeLists.txt index ebc2eaf..aee8dd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,16 @@ set(CMAKE_C_STANDARD 99) include_directories(/usr/local/include) +find_package(Iconv REQUIRED) + find_library(SYLVERANT_LIBRARY sylverant REQUIRED) # decrypt_packets add_executable(decrypt_packets decrypt_packets/main.c) target_link_libraries(decrypt_packets ${SYLVERANT_LIBRARY}) +# gen_qst_header +add_executable(gen_qst_header gen_qst_header/main.c) +target_link_libraries(gen_qst_header ${SYLVERANT_LIBRARY} ${ICONV_LIBRARIES}) +target_compile_definitions(gen_qst_header PRIVATE ICONV_CONST=${ICONV_CONST}) +target_include_directories(gen_qst_header PRIVATE ${ICONV_INCLUDE_DIR}) diff --git a/gen_qst_header/main.c b/gen_qst_header/main.c new file mode 100644 index 0000000..03a55e1 --- /dev/null +++ b/gen_qst_header/main.c @@ -0,0 +1,210 @@ +#include +#include +#include +#include + +#include +#include + +typedef struct __attribute__((packed)) { + uint32_t object_code_offset; + uint32_t function_offset_table_offset; + uint32_t bin_size; + uint32_t xffffffff; + uint16_t language; + uint16_t quest_number; + char name[32]; + char short_description[128]; + char long_description[288]; +} QUEST_BIN_HEADER; + +typedef struct __attribute__((packed)) { + uint8_t pkt_id; + uint8_t pkt_flags; + uint16_t pkt_size; + char name[32]; + uint16_t unused; + uint16_t flags; + char filename[16]; + uint32_t size; +} QST_HEADER; + +int sjis_to_utf8(char *s, size_t length) { + iconv_t conv; + size_t in_size, out_size; + + char *outbuf = malloc(length); + + in_size = length; + out_size = length; + char *in = s; + char *out = outbuf; + conv = iconv_open("SHIFT_JIS", "UTF-8"); + iconv(conv, &in, &in_size, &out, &out_size); + iconv_close(conv); + + memset(s, 0, length); + memcpy(s, outbuf, length); + free(outbuf); + + return 0; +} + +int get_filesize(const char *filename, size_t *out_size) { + FILE *fp = fopen(filename, "rb"); + if (!fp) + return 1; + + fseek(fp, 0, SEEK_END); + *out_size = ftell(fp); + fclose(fp); + + return 0; +} + +const char* path_to_filename(const char *path) { + const char *pos = strrchr(path, '/'); + if (pos) { + return pos+1; + } else { + return path; + } +} + +char* append_file_extension(const char *filename, const char *extension) { + char *new_filename = malloc(strlen(filename) + strlen(extension)); + strcpy(new_filename, filename); + strcat(new_filename, extension); + return new_filename; +} + +void generate_qst_header(const char *src_file, size_t src_file_size, QUEST_BIN_HEADER *bin_header, QST_HEADER *out_header) { + memset(out_header, 0, sizeof(QST_HEADER)); + + // 0xA6 = download to memcard, 0x44 = download for online play + // (quest file data chunks must then be encoded accordingly. 0xA6 = use 0xA7, and 0x44 = use 0x13) + out_header->pkt_id = 0xa6; + out_header->pkt_size = sizeof(QST_HEADER); + + // khyller sets .dat header value to 0xC9, .bin header value to 0x88 + // newserv sets both to 0x00 + // sylverant appears to set it differently per quest, logic/reasoning is unknown to me + // ... so, this value is probably unimportant + out_header->pkt_flags = 0; + + // khyller sets .dat header value to 0x02, .bin header value to 0x00 + // newserv sets both to 0x02 + // sylverant sets both to 0x00 + // ... and so, this value is also probably unimportant + out_header->flags = 0; + + out_header->size = src_file_size; + + memcpy(out_header->name, bin_header->name, sizeof(out_header->name)); + memcpy(out_header->filename, src_file, strlen(src_file)); +} + +int write_qst_header(const char *src_file, QST_HEADER *header) { + char *header_file = append_file_extension(src_file, ".hdr"); + + FILE *fp = fopen(header_file, "wb"); + if (!fp) { + printf("Error creating header file: %s\n", header_file); + free(header_file); + return 1; + } + + fwrite(header, sizeof(QST_HEADER), 1, fp); + fclose(fp); + + free(header_file); +} + +int main(int argc, char *argv[]) { + if (argc != 3) { + printf("Usage: gen_qst_header quest.bin quest.dat\n"); + return 1; + } + + const char *bin_file = argv[1]; + const char *dat_file = argv[2]; + + const char *bin_base_filename = path_to_filename(bin_file); + if (strlen(bin_base_filename) > 16) { + printf("Bin filename is too long to fit in a QST header. Maximum length is 16 including file extension.\n"); + return 1; + } + + const char *dat_base_filename = path_to_filename(dat_file); + if (strlen(dat_base_filename) > 16) { + printf("Dat filename is too long to fit in a QST header. Maximum length is 16 including file extension.\n"); + return 1; + } + + size_t bin_compressed_size, dat_compressed_size; + + if (get_filesize(bin_file, &bin_compressed_size)) { + printf("Error getting size of bin file: %s\n", bin_file); + return 1; + } + if (get_filesize(dat_file, &dat_compressed_size)) { + printf("Error getting size of dat file: %s\n", dat_file); + return 1; + } + + uint8_t *bin_data; + size_t bin_decompressed_size = prs_decompress_file(bin_file, &bin_data); + if (bin_decompressed_size < 0) { + printf("Error opening and decompressing bin file: %s\n", bin_file); + return 1; + } + + uint8_t *dat_data; + size_t dat_decompressed_size = prs_decompress_file(dat_file, &dat_data); + if (dat_decompressed_size < 0) { + printf("Error opening and decompressing dat file: %s\n", dat_file); + return 1; + } + + + QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)bin_data; + + sjis_to_utf8(bin_header->name, sizeof(bin_header->name)); + sjis_to_utf8(bin_header->short_description, sizeof(bin_header->short_description)); + sjis_to_utf8(bin_header->long_description, sizeof(bin_header->long_description)); + + if (bin_header->object_code_offset != 468) { + printf("Quest bin file invalid (unexpected object_code_offset = %d).\n", bin_header->object_code_offset); + return 1; + } + if (bin_header->bin_size != bin_decompressed_size) { + printf("Quest bin file invalid (decompressed size does not match bin_size value: %d).\n", bin_header->bin_size); + return 1; + } + if (strlen(bin_header->name) == 0) { + printf("Quest bin file invalid or missing quest name.\n"); + return 1; + } + if (bin_header->quest_number == 0) { + printf("Quest bin file invalid (quest_number is zero?).\n"); + return 1; + } + + printf("Quest: id=%d, language=0x%04x, name=%s\n", bin_header->quest_number, bin_header->language, bin_header->name); + + QST_HEADER qst_bin_header, qst_dat_header; + generate_qst_header(bin_base_filename, bin_compressed_size, bin_header, &qst_bin_header); + generate_qst_header(dat_base_filename, dat_compressed_size, bin_header, &qst_dat_header); + + if (write_qst_header(bin_file, &qst_bin_header)) { + return 1; + } + if (write_qst_header(dat_file, &qst_dat_header)) { + return 1; + } + + free(bin_data); + free(dat_data); + + return 0; +}