From 55179c8d333cfada3183c0dc25763da77c60e4be Mon Sep 17 00:00:00 2001 From: Jon Feldman Date: Mon, 2 Oct 2017 16:47:53 -0400 Subject: [PATCH] Initial commit: I mean, it works. --- README.md | 47 ++++++++++++++++++++ build.sh | 3 ++ extract.c | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ inject.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 README.md create mode 100755 build.sh create mode 100644 extract.c create mode 100644 inject.c diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9d141f --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +Info +---------- + +Something something readme here. + +What? No Etrian Odyssey Untold 1 Undub? Not on my watch. + +These tools are capable of modifying "Etrian Odyssey Untold: The Millenium Girl" to use Japanese audio, and not a whole lot else. Some assembly required. and with caveats. + +The only reason this is possible is due to this guy: https://github.com/xdanieldzd and his tool UntoldUnpack, which served as documentation for implementing my own tool. + +How to use +---------- + +First, get a compiler and run `build.sh`. I've chosen not to use makefiles since nothing is complex enough to justify them. + +You'll need an english copy of EoU:TmG and a Japanese copy (as in, Sekai no Meikyuu: Millenium no Shoujo.) More specifically: You need the data files MORI1R.HPI and MORI1R.HPB from both. + +First, run the following: + +``` +./extract JPN_MORI1R.HPI JPN_MORI1R.HPB +``` + +Next, in the same directory, run the following: + +``` +./inject ENG_MORI1R.HPI ENG_MORI1R.HPB +``` + +This will generate two output files: out.hpi and out.hpb. Either rebuild the romfs using these as MORI1R.whatever or use Luma's layeredfs functionality. + +For a full undub, you'll also want to overwrite the video files that are not in the archive but the folder in the romfs' `Mobiclip` folder. + +How the hell does this work? +---------------------------- + +I abuse a few properties of the format. First: the archive is a blob of concatenated data and an index of files pointing to offsets in this blob. Voice files are not compressed in the archive, so recalculation is simple: change the offset and size. + +The result is not perfect. Sadly, some differences exist bewteen voiced content exist. There may be a few manual corrections needed; a few files exist in the japanese files and not english as well as vice versa. This does not rebuild archives. It patches them to use new appended data, therefore the archive grows by ~80MB. + +An additional note to people: A peculiarity may exist in the game's engine. The files in the `Mobiclip` folder exist inside the archive as zero-byte files. It's possible a zero-byte file causes the game to seek the file in the romfs instead, which means one may be able to pull some Oblivion archiveinvalidation-style shenanigans and make a "stub" HPI/HPB with all the files extracted outside the archive. I'll leave this for the next person to test, whoever that may be. + +Licensing +---------- + +You can use this under the terms of the WTFPL. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ec6fce7 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh +gcc -o inject inject.c +gcc -o extract extract.c diff --git a/extract.c b/extract.c new file mode 100644 index 0000000..c4a2ddc --- /dev/null +++ b/extract.c @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include + +typedef struct { + char magic[4]; // "HPIH" + uint32_t ukn1; + uint32_t ukn2; + uint32_t ukn3; + uint16_t ukn4; + uint16_t unknown_entry_count; + uint16_t file_entry_count; + uint16_t unknown; +} __attribute__((packed)) hpi_header_t; + +typedef struct { + uint16_t first_file_index; + uint16_t num_files; +} __attribute__((packed)) hpi_unknown_entry_t; + +typedef struct { + uint32_t filename_offset; + uint32_t file_offset; + uint32_t compressed_size; + uint32_t uncompressed_size; +} __attribute__((packed)) hpi_file_entry_t; + +void die(char *error_mesg) { + fprintf(stderr, "%s", error_mesg); + exit(1); +} + +void mkdir_rec(const char *dir) { + char tmp[256]; + char *p = NULL; + size_t len; + + snprintf(tmp, sizeof(tmp),"%s",dir); + len = strlen(tmp); + if(tmp[len - 1] == '/') + tmp[len - 1] = 0; + for(p = tmp + 1; *p; p++) + if(*p == '/') { + *p = 0; + mkdir(tmp, S_IRWXU); + *p = '/'; + } + mkdir(tmp, S_IRWXU); +} + +char* dir_name(char* file) { + int len = strlen(file); + for(; len > 0; len--) { + if (file[len] == '/') break; + } + char * copy = strdup(file); + copy[len] = 0; + return copy; +} + +char* name_map(hpi_file_entry_t *entries, int index, char* blob) { + return &blob[entries[index].filename_offset]; +} + +char* data_map(hpi_file_entry_t *entries, int index, char* data) { + return &data[entries[index].file_offset]; +} + +int main(int c, char** v) { + FILE* hpi = fopen(v[1], "rb"); + FILE* hpb = fopen(v[2], "rb"); + if (!hpi) + die("Failed to open HPI.\n"); + if (!hpb) + die("Failed to open HPB.\n"); + + fseek(hpi, 0, SEEK_END); + size_t hpi_file_size = ftell(hpi); + rewind(hpi); + + fseek(hpb, 0, SEEK_END); + size_t hpb_file_size = ftell(hpb); + rewind(hpb); + + hpi_header_t *header = malloc(sizeof(hpi_header_t)); + fread(header, 1, sizeof(hpi_header_t), hpi); + + if (strncmp(header->magic, "HPIH", 4)) + die("HPI magic invalid; is this an HPI file?\n"); + + hpi_unknown_entry_t *unknown_entries = malloc(sizeof(hpi_unknown_entry_t) * header->unknown_entry_count); + hpi_file_entry_t *file_entries = malloc(sizeof(hpi_file_entry_t) * header->file_entry_count); + + fread(unknown_entries, 1, sizeof(hpi_unknown_entry_t) * header->unknown_entry_count, hpi); + fread(file_entries, 1, sizeof(hpi_file_entry_t) * header->file_entry_count, hpi); + + size_t pos = ftell(hpi); + + char *filename_blob = malloc(hpi_file_size - pos); + fread(filename_blob, 1, hpi_file_size - pos, hpi); + + char *data = malloc(hpb_file_size); + fread(data, 1, hpb_file_size, hpb); + + fclose(hpi); + fclose(hpb); + + for (int i = 0; i < header->file_entry_count; i++) { + char *name = name_map(file_entries, i, filename_blob); + + // If you're poking around, comment this. Keep in mind no decompression is performed. + if (strncmp(name, "VOICE/", 6)) + continue; // Not voice files, keep going + + char* dir = dir_name(name); + mkdir_rec(dir); + free(dir); + + char *data_off = data_map(file_entries, i, data); + + printf("%s [%hu/%hu]\n", name, file_entries[i].compressed_size, file_entries[i].uncompressed_size); + + FILE *out = fopen(name, "wb"); + if (!out) + die("Failed to open file to write.\n"); + fwrite(data_off, 1, file_entries[i].compressed_size, out); + fclose(out); + } +} diff --git a/inject.c b/inject.c new file mode 100644 index 0000000..1b38e94 --- /dev/null +++ b/inject.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +typedef struct { + char magic[4]; // "HPIH" + uint32_t ukn1; + uint32_t ukn2; + uint32_t ukn3; + uint16_t ukn4; + uint16_t unknown_entry_count; + uint16_t file_entry_count; + uint16_t unknown; +} __attribute__((packed)) hpi_header_t; + +typedef struct { + uint16_t first_file_index; + uint16_t num_files; +} __attribute__((packed)) hpi_unknown_entry_t; + +typedef struct { + uint32_t filename_offset; + uint32_t file_offset; + uint32_t compressed_size; + uint32_t uncompressed_size; +} __attribute__((packed)) hpi_file_entry_t; + +void die(char *error_mesg) { + fprintf(stderr, "%s", error_mesg); + exit(1); +} + +char* name_map(hpi_file_entry_t *entries, int index, char* blob) { + return &blob[entries[index].filename_offset]; +} + +char* data_map(hpi_file_entry_t *entries, int index, char* data) { + return &data[entries[index].file_offset]; +} + +int main(int c, char** v) { + FILE* hpi = fopen(v[1], "rb"); + FILE* hpb = fopen(v[2], "rb"); + if (!hpi) + die("Failed to open HPI.\n"); + if (!hpb) + die("Failed to open HPB.\n"); + + fseek(hpi, 0, SEEK_END); + size_t hpi_file_size = ftell(hpi); + rewind(hpi); + + fseek(hpb, 0, SEEK_END); + size_t hpb_file_size = ftell(hpb); + rewind(hpb); + + hpi_header_t *header = malloc(sizeof(hpi_header_t)); + fread(header, 1, sizeof(hpi_header_t), hpi); + + if (strncmp(header->magic, "HPIH", 4)) + die("HPI magic invalid; is this an HPI file?\n"); + + hpi_unknown_entry_t *unknown_entries = malloc(sizeof(hpi_unknown_entry_t) * header->unknown_entry_count); + hpi_file_entry_t *file_entries = malloc(sizeof(hpi_file_entry_t) * header->file_entry_count); + + fread(unknown_entries, 1, sizeof(hpi_unknown_entry_t) * header->unknown_entry_count, hpi); + fread(file_entries, 1, sizeof(hpi_file_entry_t) * header->file_entry_count, hpi); + + size_t pos = ftell(hpi); + + char *filename_blob = malloc(hpi_file_size - pos); + fread(filename_blob, 1, hpi_file_size - pos, hpi); + + char *data = malloc(hpb_file_size); + fread(data, 1, hpb_file_size, hpb); + + fclose(hpi); + fclose(hpb); + + for (int i = 0; i < header->file_entry_count; i++) { + char *name = name_map(file_entries, i, filename_blob); + + FILE *out = fopen(name, "rb"); + if (!out) + continue; // This file does not exist; don't replace it. + + fseek(out, 0, SEEK_END); + size_t fs = ftell(out); + rewind(out); + + printf("%s [%lu]\n", name, fs); + + data = realloc(data, hpb_file_size + fs); + fread(&data[hpb_file_size], 1, fs, out); + fclose(out); + + file_entries[i].file_offset = hpb_file_size; + file_entries[i].compressed_size = fs; + + hpb_file_size += fs; + } + + FILE* out_hpb = fopen("out.hpb", "wb"); + fwrite(data, 1, hpb_file_size, out_hpb); + fclose(out_hpb); + + FILE* out_hpi = fopen("out.hpi", "wb"); + fwrite(header, 1, sizeof(hpi_header_t), out_hpi); + fwrite(unknown_entries, 1, sizeof(hpi_unknown_entry_t) * header->unknown_entry_count, out_hpi); + fwrite(file_entries, 1, sizeof(hpi_file_entry_t) * header->file_entry_count, out_hpi); + fwrite(filename_blob, 1, hpi_file_size - pos, out_hpi); + fclose(out_hpi); +} -- 2.39.5