From 68ecdca929fd7fef35df1fbc78a2ba314915f0d9 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 12 Apr 2022 23:27:21 -0400 Subject: [PATCH] Add very experimental skeleton loading Added functions to extract Havok pak files, and extract some skeleton data from an externally processed Havok XML file. This still requires you to use the proprietary Havok SDK though, but I'll think of a solution in the future. --- CMakeLists.txt | 5 +- include/gamedata.h | 2 + include/havokxmlparser.h | 27 ++++++++++ src/gamedata.cpp | 56 ++++++++++++++++++- src/havokxmlparser.cpp | 113 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 include/havokxmlparser.h create mode 100644 src/havokxmlparser.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e373412..2d02ee0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,8 @@ add_library(libxiv STATIC src/installextract.cpp src/patch.cpp src/exlparser.cpp - src/mdlparser.cpp) + src/mdlparser.cpp + src/havokxmlparser.cpp) target_include_directories(libxiv PUBLIC include PRIVATE src) -target_link_libraries(libxiv PUBLIC ${LIBRARIES}) +target_link_libraries(libxiv PUBLIC ${LIBRARIES} pugixml) target_link_directories(libxiv PUBLIC ${LIB_DIRS}) \ No newline at end of file diff --git a/include/gamedata.h b/include/gamedata.h index 9490747..28458d7 100644 --- a/include/gamedata.h +++ b/include/gamedata.h @@ -26,6 +26,8 @@ public: */ void extractFile(std::string_view dataFilePath, std::string_view outPath); + void extractSkeleton(); + std::optional readExcelSheet(std::string_view name); std::vector getAllSheetNames(); diff --git a/include/havokxmlparser.h b/include/havokxmlparser.h new file mode 100644 index 0000000..50f502e --- /dev/null +++ b/include/havokxmlparser.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +struct Bone { + std::string name; + + Bone* parent = nullptr; + + std::array localTransform, finalTransform; + + std::array position; + std::array rotation; + std::array scale; +}; + +struct Skeleton { + std::vector bones; + Bone* root_bone = nullptr; +}; + +/* + * This reads a havok xml scene file, which is generated from your preferred assetcc.exe. + */ +Skeleton parseHavokXML(const std::string_view path); \ No newline at end of file diff --git a/src/gamedata.cpp b/src/gamedata.cpp index 985a407..7980fd5 100644 --- a/src/gamedata.cpp +++ b/src/gamedata.cpp @@ -106,7 +106,7 @@ void GameData::extractFile(std::string_view dataFilePath, std::string_view outPa // TODO: handle platforms other than win32 auto indexFilename = calculateFilename(categoryToID[category], getExpansionID(repository), 0, "win32", "index"); - fmt::print("calculated index filename: "); + fmt::print("calculated index filename: {}\n", indexFilename); // TODO: handle hashes in index2 files (we can read them but it's not setup yet.) auto indexFile = readIndexFile(dataDirectory + "/" + repository + "/" + indexFilename); @@ -445,4 +445,56 @@ std::optional GameData::readExcelSheet(std::string_view name) { } return {}; -} \ No newline at end of file +} + +void GameData::extractSkeleton() { + std::string path = fmt::format("chara/human/c0201/skeleton/base/b0001/skl_c0201b0001.sklb"); + + extractFile(path, "test.skel"); + + FILE* file = fopen("test.skel", "rb"); + + fseek(file, 0, SEEK_END); + size_t end = ftell(file); + fseek(file, 0, SEEK_SET); + + int32_t magic; + fread(&magic, sizeof(int32_t), 1, file); + + int32_t format; + fread(&format, sizeof(int32_t), 1, file); + + fseek(file, sizeof(uint16_t), SEEK_CUR); + + if(magic != 0x736B6C62) + fmt::print("INVALID SKLB magic"); + + size_t dataOffset = 0; + + switch(format) { + case 0x31323030: + fread(&dataOffset, sizeof(int16_t), 1, file); + break; + case 0x31333030: + case 0x31333031: + fseek(file, sizeof(uint16_t), SEEK_CUR); + fread(&dataOffset, sizeof(int16_t), 1, file); + break; + default: + fmt::print("INVALID SKLB format {}", format); + break; + } + + fseek(file, dataOffset, SEEK_SET); + + fmt::print("data offset: {}\n", dataOffset); + + std::vector havokData(end - dataOffset); + fread(havokData.data(), havokData.size(), 1, file); + + FILE* newFile = fopen("test.sklb.havok", "wb"); + fwrite(havokData.data(), havokData.size(), 1, newFile); + + fclose(newFile); + fclose(file); +} diff --git a/src/havokxmlparser.cpp b/src/havokxmlparser.cpp new file mode 100644 index 0000000..06a387b --- /dev/null +++ b/src/havokxmlparser.cpp @@ -0,0 +1,113 @@ +#include "havokxmlparser.h" + +#include +#include +#include +#include + +void walkSkeleton(Skeleton& skeleton, Bone *pBone, int level = 0); + +inline std::vector tokenize(const std::string_view string, const std::string_view& delimiters) { + std::vector tokens; + + const size_t length = string.length(); + size_t lastPos = 0; + + while(lastPos < length + 1) { + size_t pos = string.find_first_of(delimiters, lastPos); + if(pos == std::string_view::npos) + pos = length; + + if(pos != lastPos) + tokens.emplace_back(string.data() + lastPos, pos - lastPos); + + lastPos = pos + 1; + } + + return tokens; +} + +Skeleton parseHavokXML(const std::string_view path) { + Skeleton skeleton; + + pugi::xml_document doc; + doc.load_file(path.data()); + + pugi::xpath_node build_tool = doc.select_node("//hkobject[@name=\"#0052\"]/hkparam[@name=\"bones\"]"); + + auto bonesNode = build_tool.node(); + + fmt::print("num bones: {}\n", bonesNode.attribute("numelements").as_int()); + + skeleton.bones.reserve(bonesNode.attribute("numelements").as_int()); + + for(auto node : bonesNode.children()) { + pugi::xpath_node name_node = node.select_node("./hkparam[@name=\"name\"]"); + //fmt::print("{}\n", node.print()); + name_node.node().print(std::cout); + + Bone bone; + bone.name = name_node.node().text().as_string(); + + fmt::print("bone {}\n", bone.name); + + skeleton.bones.push_back(bone); + } + pugi::xpath_node parentNode = doc.select_node("//hkparam[@name=\"parentIndices\"]"); + + std::string text = parentNode.node().text().as_string(); + + auto parentIndices = tokenize(text, " "); + + for(int i = 0; i < parentIndices.size(); i++) { + auto indice = std::atoi(parentIndices[i].c_str()); + if(indice != -1) + skeleton.bones[i].parent = &skeleton.bones[indice]; + else + skeleton.root_bone = &skeleton.bones[i]; + } + + walkSkeleton(skeleton, skeleton.root_bone); + + pugi::xpath_node build_tool2 = doc.select_node("//hkobject[@name=\"#0052\"]/hkparam[@name=\"referencePose\"]"); + + fmt::print("num ref poses: {}\n", build_tool2.node().attribute("numelements").as_int()); + + auto matrices = tokenize(build_tool2.node().text().as_string(), "\n"); + int i = 0; + for(auto matrix : matrices) { + size_t firstParenthesis = matrix.find_first_of('('); + if(firstParenthesis != std::string::npos) { + matrix = matrix.substr(firstParenthesis); + std::replace(matrix.begin(), matrix.end(), ')', ' '); + matrix.erase(std::remove(matrix.begin(), matrix.end(), '('), matrix.end()); + fmt::print("{}\n", matrix); + + auto tokens = tokenize(matrix, " "); + std::array position = {std::stof(tokens[0]), std::stof(tokens[1]), std::stof(tokens[2])}; + std::array rotation = {std::stof(tokens[3]), std::stof(tokens[4]), std::stof(tokens[5]), std::stof(tokens[6])}; + std::array scale = {std::stof(tokens[7]), std::stof(tokens[8]), std::stof(tokens[9])}; + + skeleton.bones[i].position = position; + skeleton.bones[i].rotation = rotation; + skeleton.bones[i].scale = scale; + } + + i++; + } + + return skeleton; +} + +void walkSkeleton(Skeleton& skeleton, Bone *pBone, int level) { + for(int i = 0; i < level; i++) + fmt::print(" "); + + fmt::print("- {}\n", pBone->name); + + for(auto& bone : skeleton.bones) { + if(bone.parent == pBone) { + walkSkeleton(skeleton, &bone, level++); + } + } +}