diff --git a/CMakeLists.txt b/CMakeLists.txt index 98e384e..e599ca1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,5 +5,7 @@ add_library(libxiv STATIC src/indexparser.cpp src/crc32.cpp src/gamedata.cpp - src/compression.cpp) + src/compression.cpp + src/exhparser.cpp + src/exdparser.cpp) target_include_directories(libxiv PUBLIC include PRIVATE src) \ No newline at end of file diff --git a/include/exdparser.h b/include/exdparser.h new file mode 100644 index 0000000..16cb415 --- /dev/null +++ b/include/exdparser.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +struct EXH; +struct ExcelDataPagination; + +void readEXD(EXH& exh, ExcelDataPagination& page); \ No newline at end of file diff --git a/include/exhparser.h b/include/exhparser.h new file mode 100644 index 0000000..157ff19 --- /dev/null +++ b/include/exhparser.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +// taken from https://xiv.dev/game-data/file-formats/excel +struct ExhHeader { + char magic[0x4]; + uint16_t unknown; + uint16_t dataOffset; + uint16_t columnCount; + uint16_t pageCount; + uint16_t languageCount; + uint16_t unknown1; + uint8_t u2; + uint8_t variant; + uint16_t u3; + uint32_t rowCount; + uint32_t u4[2]; +}; + +enum ExcelColumnDataType : uint16_t { + String = 0x0, + Bool = 0x1, + Int8 = 0x2, + UInt8 = 0x3, + Int16 = 0x4, + UInt16 = 0x5, + Int32 = 0x6, + UInt32 = 0x7, + Float32 = 0x9, + Int64 = 0xA, + UInt64 = 0xB, + + // 0 is read like data & 1, 1 is like data & 2, 2 = data & 4, etc... + PackedBool0 = 0x19, + PackedBool1 = 0x1A, + PackedBool2 = 0x1B, + PackedBool3 = 0x1C, + PackedBool4 = 0x1D, + PackedBool5 = 0x1E, + PackedBool6 = 0x1F, + PackedBool7 = 0x20, +}; + +struct ExcelColumnDefinition { + ExcelColumnDataType type; + uint16_t offset; +}; + +struct ExcelDataPagination { + uint32_t startId; + uint32_t rowCount; +}; + +enum Language : uint16_t { + None, + // ja + Japanese, + // en + English, + // de + German, + // fr + French, + // chs + ChineseSimplified, + // cht + ChineseTraditional, + // ko + Korean +}; + +struct EXH { + ExhHeader header; + + std::vector columnDefinitions; + std::vector pages; + std::vector language; +}; + +EXH readEXH(std::string_view path); \ No newline at end of file diff --git a/include/utility.h b/include/utility.h new file mode 100644 index 0000000..9b19d6e --- /dev/null +++ b/include/utility.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +template +void endianSwap(T *objp) { + auto memp = reinterpret_cast(objp); + std::reverse(memp, memp + sizeof(T)); +} \ No newline at end of file diff --git a/src/exdparser.cpp b/src/exdparser.cpp new file mode 100644 index 0000000..7f41e1b --- /dev/null +++ b/src/exdparser.cpp @@ -0,0 +1,89 @@ +#include "exdparser.h" + +#include +#include +#include +#include + +#include "exhparser.h" +#include "utility.h" + +struct ExcelDataHeader { + char magic[0x4]; + uint16_t version; + uint16_t unknown1; + uint32_t indexSize; + uint32_t unknown2[5]; +}; + +struct ExcelDataOffset { + uint32_t rowId; + uint32_t offset; +}; + +struct ExcelDataRowHeader { + uint32_t dataSize; + uint16_t rowCount; +}; + +void readEXD(EXH& exh, ExcelDataPagination& page) { + auto path = fmt::format("{}_{}.exd", "map", page.startId); + + FILE* file = fopen(path.data(), "rb"); + if(file == nullptr) { + throw std::runtime_error("Failed to open exd file " + std::string(path)); + } + + ExcelDataHeader header; + fread(&header, sizeof(ExcelDataHeader), 1, file); + + endianSwap(&header.indexSize); + + std::vector dataOffsets; + dataOffsets.resize(header.indexSize / sizeof( ExcelDataOffset )); + + fread(dataOffsets.data(), sizeof(ExcelDataOffset) * dataOffsets.size(), 1, file); + + for(auto& offset : dataOffsets) { + endianSwap(&offset.offset); + endianSwap(&offset.rowId); + } + + for(auto& offset : dataOffsets) { + fseek(file, exh.header.dataOffset + offset.offset, SEEK_SET); + + ExcelDataRowHeader rowHeader; + fread(&rowHeader, sizeof(ExcelDataRowHeader), 1, file); + + endianSwap(&rowHeader.dataSize); + endianSwap(&rowHeader.rowCount); + + const int rowOffset = offset.offset + 6; + + for(auto column : exh.columnDefinitions) { + switch(column.type) { + case String: + { + fseek(file, rowOffset + column.offset, SEEK_SET); + + uint32_t stringLength; + fread(&stringLength, sizeof(uint32_t), 1, file); + + fseek(file, rowOffset + exh.header.dataOffset + stringLength, SEEK_SET); + + std::string string; + + int ch; + while ((ch = fgetc(file)) != '\0') { + string.push_back((char)ch); + } + + fmt::print("{}\n", string.data()); + } + break; + default: + break; + } + } + } +} \ No newline at end of file diff --git a/src/exhparser.cpp b/src/exhparser.cpp new file mode 100644 index 0000000..758127c --- /dev/null +++ b/src/exhparser.cpp @@ -0,0 +1,47 @@ +#include "exhparser.h" +#include "utility.h" + +#include +#include +#include +#include + +EXH readEXH(const std::string_view path) { + EXH exh; + + FILE* file = fopen(path.data(), "rb"); + if(file == nullptr) { + throw std::runtime_error("Failed to open exh file " + std::string(path)); + } + + fread(&exh.header, sizeof(ExhHeader), 1, file); + + endianSwap(&exh.header.dataOffset); + endianSwap(&exh.header.columnCount); + endianSwap(&exh.header.pageCount); + endianSwap(&exh.header.languageCount); + endianSwap(&exh.header.rowCount); + + exh.columnDefinitions.resize(exh.header.columnCount); + + fread(exh.columnDefinitions.data(), sizeof(ExcelColumnDefinition) * exh.header.columnCount, 1, file); + + exh.pages.resize(exh.header.pageCount); + + fread(exh.pages.data(), sizeof(ExcelDataPagination) * exh.header.pageCount, 1, file); + + exh.language.resize(exh.header.languageCount); + fread(exh.language.data(), sizeof(Language) * exh.header.languageCount, 1, file); + + for(auto& columnDef : exh.columnDefinitions) { + endianSwap(&columnDef.offset); + endianSwap(&columnDef.type); + } + + for(auto& page : exh.pages) { + endianSwap(&page.rowCount); + endianSwap(&page.startId); + } + + return exh; +} \ No newline at end of file diff --git a/src/gamedata.cpp b/src/gamedata.cpp index 61e0dea..b670bb7 100644 --- a/src/gamedata.cpp +++ b/src/gamedata.cpp @@ -53,7 +53,7 @@ std::tuple GameData::calculateRepositoryCategory(std:: std::string repository, category; auto tokens = tokenize(path, "/"); - if(stringContains(tokens[1], "ex") && !stringContains(tokens[0], "exd")) { + if(stringContains(tokens[1], "ex") && !stringContains(tokens[0], "exd") && !stringContains(tokens[0], "exh")) { repository = tokens[1]; } else { repository = "ffxiv"; @@ -79,6 +79,9 @@ void GameData::extractFile(std::string_view dataFilePath, std::string_view outPa const uint64_t hash = calculateHash(dataFilePath); auto [repository, category] = calculateRepositoryCategory(dataFilePath); + fmt::print("repository = {}\n", repository); + fmt::print("category = {}\n", category); + // TODO: handle platforms other than win32 auto indexFilename = calculateFilename(categoryToID[category], getExpansionID(repository), 0, "win32", "index"); @@ -114,6 +117,8 @@ void GameData::extractFile(std::string_view dataFilePath, std::string_view outPa fread(&info, sizeof(FileInfo), 1, file); + fmt::print("file size = {}\n", info.fileSize); + if(info.fileType != FileType::Standard) { throw std::runtime_error("File type is not handled yet for " + std::string(dataFilePath)); } @@ -179,5 +184,5 @@ void GameData::extractFile(std::string_view dataFilePath, std::string_view outPa } } - fmt::print("Extracted {} to {}", dataFilePath, outPath); + fmt::print("Extracted {} to {}\n", dataFilePath, outPath); }