commit 998313be3c94c2fc430b29857508464afd60584c Author: cdemeyer-teachx Date: Wed Nov 12 06:29:59 2025 +0900 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bc3772 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +CBAssets.rar + +CBAssets + + + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7b20027 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.14) +project(cursebreakerParser) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Collect all source files +file(GLOB_RECURSE SOURCES "src/*.cpp") + +# Include FetchContent +include(FetchContent) + +message(STATUS "Using rapidyaml") +message(STATUS "Using GLM") +message(STATUS "Using TinyXML2") + +FetchContent_Declare( + rapidyaml + GIT_REPOSITORY https://github.com/biojppm/rapidyaml.git + GIT_TAG master +) + +FetchContent_Declare( + glm + GIT_REPOSITORY https://github.com/g-truc/glm.git + GIT_TAG master +) + +FetchContent_Declare( + tinyxml2 + GIT_REPOSITORY https://github.com/leethomason/tinyxml2.git + GIT_TAG master +) + +# Enable exceptions in rapidyaml +set(RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS ON CACHE BOOL "" FORCE) +set(RYML_DBG OFF CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(rapidyaml glm tinyxml2) + +# Create the main executable +add_executable(${PROJECT_NAME} ${SOURCES}) + +# Link libraries to our executable +target_link_libraries(${PROJECT_NAME} PUBLIC ryml::ryml) +target_link_libraries(${PROJECT_NAME} PUBLIC tinyxml2::tinyxml2) + +# Add include directories for rapidyaml +target_include_directories(${PROJECT_NAME} PRIVATE ${rapidyaml_SOURCE_DIR}/src) + +# Add include directories for GLM +target_include_directories(${PROJECT_NAME} PRIVATE ${glm_SOURCE_DIR}) + +# Include directories +target_include_directories(${PROJECT_NAME} PRIVATE include) \ No newline at end of file diff --git a/include/assets/asset_base.hpp b/include/assets/asset_base.hpp new file mode 100644 index 0000000..f1c95b1 --- /dev/null +++ b/include/assets/asset_base.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +struct AssetBase +{ +public: + int64_t ID; +}; + +struct AssetGUID +{ + AssetGUID() = default; + AssetGUID(const char* data) : AssetGUID(std::string_view(data, 32)) {}; + AssetGUID(std::string_view data) + { + for (size_t i = 0; i < 16; ++i) Data[0] = (Data[0] << 4) | hexToVal(data[i]); + for (size_t i = 16; i < 32; ++i) Data[1] = (Data[1] << 4) | hexToVal(data[i]); + } + + static uint64_t hexToVal(char c) + { + return (c >= '0' && c <= '9') ? (c - '0') : (c - 'a' + 10); + } + + bool operator==(const AssetGUID& other) const + { + return Data[0] == other.Data[0] && Data[1] == other.Data[1]; + } + + static constexpr uint64_t DefaultVal = 0xe000000000000000; + + std::array Data{ 0, DefaultVal }; +}; + + +namespace std +{ + template <> + struct hash { + size_t operator()(const AssetGUID& v) const noexcept + { + auto h1 = std::hash{}(v.Data[0]); + auto h2 = std::hash{}(v.Data[1]); + return h1 ^ (h2 << 1); + } + }; +} + +template +T* ParseAssetRef(const ryml::ConstNodeRef& node) +{ + T* asset; + node["fileID"] >> asset; + return asset; +} + +template +bool LinkAssetRef(const std::unordered_map& assetsMap, T*& asset) +{ + auto it = assetsMap.find(reinterpret_cast(asset)); + if (it != assetsMap.end()) + { + asset = reinterpret_cast(it->second); + return true; + } + asset = nullptr; + return false; +} + +inline AssetGUID ParseAssetGUID(const ryml::ConstNodeRef& node) +{ + auto guidStringSpan2 = node["guid"].val(); + + assert(guidStringSpan2.size() == 32); + + return AssetGUID{std::string_view(guidStringSpan2.data(), guidStringSpan2.size())}; +} \ No newline at end of file diff --git a/include/assets/custom_assets.hpp b/include/assets/custom_assets.hpp new file mode 100644 index 0000000..1596a4c --- /dev/null +++ b/include/assets/custom_assets.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "asset_base.hpp" + +struct Drop +{ + int itemId; + int minAmount; + int maxAmount; + int dropChance; +}; + +struct Interactable_Resource : public AssetBase +{ + int maxHealth; + int respawnTime; + std::vector drops; + std::vector requiredTools; + int xp; + int typeId; +}; \ No newline at end of file diff --git a/include/assets/game_object.hpp b/include/assets/game_object.hpp new file mode 100644 index 0000000..2aa9fb4 --- /dev/null +++ b/include/assets/game_object.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include + +#include "asset_base.hpp" +#include "transform.hpp" + +struct GameObjectAsset : public AssetBase +{ + std::string name; + uint8_t layer{ 0 }; + uint8_t namMeshLayer{ 0 }; + std::string tag; + + TransformAsset* transform{ nullptr }; + std::vector Components; +}; + +inline GameObjectAsset ParseGameObject(const ryml::ConstNodeRef& node) +{ + GameObjectAsset gameObject; + + node["m_Name"] >> gameObject.name; + node["m_Layer"] >> gameObject.layer; + node["m_TagString"] >> gameObject.tag; + + auto components = node["m_Component"]; + auto it = components.begin(); + + gameObject.transform = ParseAssetRef((*it)["component"]); + ++it; + for (; it != components.end(); ++it) + { + gameObject.Components.push_back(ParseAssetRef((*it)["component"])); + } + + return gameObject; +} + +inline void LinkGameObject(const std::unordered_map& assetsMap, GameObjectAsset& gameObject) +{ + LinkAssetRef(assetsMap, gameObject.transform); + for (int i = static_cast(gameObject.Components.size()) - 1; i >= 0; i--) + { + if (!LinkAssetRef(assetsMap, gameObject.Components[i])) + { + std::swap(gameObject.Components[i], gameObject.Components.back()); + gameObject.Components.pop_back(); + } + } +} \ No newline at end of file diff --git a/include/assets/mesh_filter.hpp b/include/assets/mesh_filter.hpp new file mode 100644 index 0000000..81ac966 --- /dev/null +++ b/include/assets/mesh_filter.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "asset_base.hpp" + + +struct GameObjectAsset; + +struct MeshFilterAsset : AssetBase +{ + AssetGUID Mesh; + GameObjectAsset* GameObject{ nullptr }; +}; + +inline MeshFilterAsset ParseMeshFilter(const ryml::ConstNodeRef& node) +{ + MeshFilterAsset meshFilter; + + meshFilter.Mesh = ParseAssetGUID(node["m_Mesh"]); + meshFilter.GameObject = ParseAssetRef(node["m_GameObject"]); + + return meshFilter; +} + +inline void LinkMeshFilter(const std::unordered_map& assetsMap, MeshFilterAsset& meshFilter) +{ + LinkAssetRef(assetsMap, meshFilter.GameObject); +} \ No newline at end of file diff --git a/include/assets/prefab_instance.hpp b/include/assets/prefab_instance.hpp new file mode 100644 index 0000000..3819d75 --- /dev/null +++ b/include/assets/prefab_instance.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "asset_base.hpp" + +//struct PrefabInstanceAsset : AssetBase +//{ +// AssetGUID prefabGUID; +// ryml::Tree Data{}; +//}; + +struct PrefabAsset +{ + std::filesystem::path path; + std::string name; + + std::unordered_map Data; +}; + +//inline PrefabInstanceAsset ParsePrefabInstance(const ryml::ConstNodeRef& node) +//{ +// PrefabInstanceAsset prefabInstance; +// prefabInstance.prefabGUID = ParseAssetGUID(node["m_SourcePrefab"]); +// // prefabInstance.Data = node; +// return prefabInstance; +//} \ No newline at end of file diff --git a/include/assets/scene.hpp b/include/assets/scene.hpp new file mode 100644 index 0000000..731cc39 --- /dev/null +++ b/include/assets/scene.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#include "game_object.hpp" +#include "transform.hpp" +#include "mesh_filter.hpp" +#include "prefab_instance.hpp" + +struct SceneAsset +{ +public: + std::filesystem::path path; + std::string name; + + std::vector gameObjects; + std::vector transforms; + std::vector meshFilters; + // std::vector prefabInstances; + + std::unordered_map IDtoAsset; + + +}; \ No newline at end of file diff --git a/include/assets/transform.hpp b/include/assets/transform.hpp new file mode 100644 index 0000000..4bc43f3 --- /dev/null +++ b/include/assets/transform.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include +#include + +#include "asset_base.hpp" + +struct GameObjectAsset; + +struct TransformAsset : public AssetBase +{ + glm::quat Rotation; + glm::vec3 Position; + glm::vec3 Scale; + + glm::mat4 GlobalMatrix; + + GameObjectAsset* GameObject{ nullptr }; + std::vector Children; + TransformAsset* Parent{ nullptr }; +}; + +inline glm::vec3 ParseVector3(const ryml::ConstNodeRef& node) +{ + glm::vec3 vector; + node["x"] >> vector.x; + node["y"] >> vector.y; + node["z"] >> vector.z; + return vector; +} + +inline glm::quat ParseQuaternion(const ryml::ConstNodeRef& node) +{ + glm::quat quaternion; + node["x"] >> quaternion.x; + node["y"] >> quaternion.y; + node["z"] >> quaternion.z; + node["w"] >> quaternion.w; + return quaternion; +} + +inline TransformAsset ParseTransform(const ryml::ConstNodeRef& node) +{ + TransformAsset transform; + transform.Rotation = ParseQuaternion(node["m_LocalRotation"]); + transform.Position = ParseVector3(node["m_LocalPosition"]); + transform.Scale = ParseVector3(node["m_LocalScale"]); + + transform.Parent = ParseAssetRef(node["m_Father"]); + transform.GameObject = ParseAssetRef(node["m_GameObject"]); + + auto children = node["m_Children"]; + for (const auto& child : children) + { + transform.Children.push_back(ParseAssetRef(child)); + } + + return transform; +} + +inline void LinkTransform(const std::unordered_map& assetsMap, TransformAsset& transform) +{ + LinkAssetRef(assetsMap, transform.Parent); + for (int i = static_cast(transform.Children.size()) - 1; i >= 0; i--) + { + if (!LinkAssetRef(assetsMap, transform.Children[i])) + { + std::swap(transform.Children[i], transform.Children.back()); + transform.Children.pop_back(); + } + } + LinkAssetRef(assetsMap, transform.GameObject); +} \ No newline at end of file diff --git a/include/configs/constants.h b/include/configs/constants.h new file mode 100644 index 0000000..e59e4dc --- /dev/null +++ b/include/configs/constants.h @@ -0,0 +1,208 @@ +#pragma once + +#include + +namespace cursebreaker { + + enum class SkillType : uint8_t + { + woodcutting, + fishing, + swordsmanship, + mining, + archery, + magic, + defence, + blacksmithy, + tailoring, + carpentry, + alchemy, + cooking, + none, + }; + + enum class WorkbenchType : uint8_t + { + none, + anvil, + oven, + cooking, + carpenter, + tailor, + forge, + alchemist, + mystic, + }; + + enum class StatType : uint8_t + { + health, + accuracyPhysical, + accuracyMagical, + accuracyRanged, + damagePhysical, + damageMagical, + damageRanged, + resistancePhysical, + resistanceMagical, + resistanceRanged, + healing, + movementSpeed, + mana, + manaregen, + healthregen, + power, + critical, + DamageVsUndead, + DamageVsBeasts, + CritterSlaying, + none, + harvestingSpeedWoodcutting + }; + + enum class ItemType : uint8_t + { + weapon, + shield, + armor, + head, + resource, + consumable, + trinket, + bracelet + }; + + enum class ItemCategory : uint8_t + { + none, + bone, + bow, + crossbow, + constructable, + torch, + blacksmithhammer, + questitem, + heavyArmor, + warhammer, + shield, + hatchet, + blade, + armor, + pickaxe, + fish, + fishingrod, + shears, + hammer, + battleaxe, + morningstar, + wand, + staff, + dagger + }; + + enum class Tool : uint8_t + { + hatchet, + pickaxe, + broom, + fishingrod, + none + }; + + enum class SpellBookType : uint8_t + { + none, + spells, + abilities + }; + + enum class ActionType : uint8_t + { + NpcDeath, + PlayerDeath, + PlayerRespawn, + NpcInteract, + QuestUpdate, + QuestTimerEnd, + UseItemOnItem, + UseItemOnNpc, + ConsumeItem, + NpcCombatInteract, + none, + NpcKilledByPlayer, + EnterMap, + VarUpdated, + GrantAchievement, + PlayerKilledByNpc, + UseItem, + CraftItem, + HarvestItem, + BuyItem, + NpcTagKilledByPlayer, + UseAbility, + LootItem, + UseItemCategoryOnItem, + }; + + enum class Material : uint8_t + { + none, + copper, + iron, + imbersteel, + titanium, + spruce, + oak, + evark, + deadwood, + sheep, + troll, + ogre, + demon, + wool, + cotton, + flax, + jute, + }; + + enum class GenstatType : uint8_t + { + none, + dagger, + broadsword, + battleaxe, + greatsword, + morningstar, + hammer, + spear, + bow, + staff, + wand, + crossbow, + woodenshield, + wizardhat, + wizardrobe, + grandwizardhat, + grandwizardrobe, + leatherhood, + leatherbracelet, + leatherarmor, + studdedleatherhood, + studdedleatherbracelet, + studdedleatherarmor, + helmet, + shield, + armor, + platehelmet, + kiteshield, + platearmor, + }; + + struct StatValue + { + StatType type; + uint8_t value; + bool isPercentage; + }; + +}; \ No newline at end of file diff --git a/include/configs/items.hpp b/include/configs/items.hpp new file mode 100644 index 0000000..f2c47ef --- /dev/null +++ b/include/configs/items.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include + +#include + +#include "constants.h" + +namespace cursebreaker { + +struct CraftingItemEntry +{ + uint16_t itemId{}; + uint16_t amount{}; +}; + +// Struct for item crafting requirements +struct ItemCrafting +{ + WorkbenchType workbench; + SkillType craftingskill; + std::vector craftingitems; + std::string checks; + + ItemCrafting() = default; +}; + +// Main item struct +struct Item { + uint16_t id = 0; + uint16_t price = 0; + uint16_t abilityid = 0; + uint16_t learnabilityid = 0; + + SkillType skill = SkillType::none; + ItemCategory category = ItemCategory::none; + ItemType slot = ItemType::resource; + Tool tool = Tool::none; + GenstatType generation = GenstatType::none; + uint8_t level = 0; + uint8_t foodlevel = 0; + uint8_t foodamount = 0; + uint8_t foodfrequency = 0; + uint8_t foodtime = 0; + uint8_t maxstack = 0; + uint8_t book = 0; + + bool stackable : 1; + bool twohanded : 1; + bool undroppable : 1; + bool hidemilestone : 1; + + std::string name; + std::string description; + std::string comment; + + std::vector stats; + std::vector craftings; + + Item() = default; +}; + +// Helper functions for enum conversions +StatType stringToStatType(const std::string& str); +ItemCategory stringToItemCategory(const std::string& str); +ItemType stringToItemType(const std::string& str); +Tool stringToTool(const std::string& str); +SkillType stringToSkillType(const std::string& str); +WorkbenchType stringToWorkbenchType(const std::string& str); +GenstatType stringToGenstatType(const std::string& str); + +// Items configuration manager +class ItemsConfig { +public: + static ItemsConfig& getInstance() { + static ItemsConfig instance; + return instance; + } + + bool loadFromXML(const std::string& filepath); + const Item* getItemById(int id) const; + const std::unordered_map& getAllItems() const { return m_items; } + +private: + ItemsConfig() = default; + ~ItemsConfig() = default; + ItemsConfig(const ItemsConfig&) = delete; + ItemsConfig& operator=(const ItemsConfig&) = delete; + + std::unordered_map m_items; + + // Helper methods for parsing + void parseItem(tinyxml2::XMLElement* itemElement); + void parseStats(tinyxml2::XMLElement* itemElement, Item& item); + void parseCraftings(tinyxml2::XMLElement* itemElement, Item& item); + std::string getAttributeValue(tinyxml2::XMLElement* element, const char* attribute, const std::string& defaultValue = ""); + int getAttributeValueInt(tinyxml2::XMLElement* element, const char* attribute, int defaultValue = 0); + bool getAttributeValueBool(tinyxml2::XMLElement* element, const char* attribute, bool defaultValue = false); +}; + +} // namespace cursebreaker diff --git a/include/project_parser.h b/include/project_parser.h new file mode 100644 index 0000000..eb19d95 --- /dev/null +++ b/include/project_parser.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "assets/asset_base.hpp" +#include "assets/scene.hpp" + +struct AssetPath +{ + std::filesystem::path path{}; + AssetGUID GUID{}; +}; + +struct ParsedProject +{ + std::filesystem::path m_projectRoot; + + std::vector m_scriptFiles; + std::vector m_imageFiles; + std::vector m_meshFiles; + std::vector m_sceneFiles; + std::vector m_prefabFiles; + + std::vector m_prefabAssets; + std::vector m_sceneAssets; + + std::unordered_map m_prefabsMap; + std::unordered_map m_scriptToClassHash; + +}; + +void ParseProject(const std::filesystem::path& projectRoot); + +extern ParsedProject g_parsedProject; \ No newline at end of file diff --git a/include/tree_builder.h b/include/tree_builder.h new file mode 100644 index 0000000..95923b9 --- /dev/null +++ b/include/tree_builder.h @@ -0,0 +1,5 @@ +#pragma once + +#include "assets/scene.hpp" + +void BuildTree(SceneAsset& sceneAsset); \ No newline at end of file diff --git a/src/configs/items.cpp b/src/configs/items.cpp new file mode 100644 index 0000000..cd68b3d --- /dev/null +++ b/src/configs/items.cpp @@ -0,0 +1,334 @@ +#include "configs/items.hpp" +#include +#include +#include +#include + +namespace cursebreaker { + +// Helper functions to convert strings to enums +StatType stringToStatType(const std::string& str) { + static const std::unordered_map statMap = { + {"damagephysical", StatType::damagePhysical}, + {"damagemagical", StatType::damageMagical}, + {"damageranged", StatType::damageRanged}, + {"accuracyphysical", StatType::accuracyPhysical}, + {"accuracymagical", StatType::accuracyMagical}, + {"accuracyranged", StatType::accuracyRanged}, + {"resistancephysical", StatType::resistancePhysical}, + {"resistancemagical", StatType::resistanceMagical}, + {"resistanceranged", StatType::resistanceRanged}, + {"health", StatType::health}, + {"mana", StatType::mana}, + {"manaregen", StatType::manaregen}, + {"healthregen", StatType::healthregen}, + {"movementSpeed", StatType::movementSpeed}, + {"power", StatType::power}, + {"critical", StatType::critical}, + {"healing", StatType::healing}, + {"DamageVsUndead", StatType::DamageVsUndead}, + {"DamageVsBeasts", StatType::DamageVsBeasts}, + {"CritterSlaying", StatType::CritterSlaying}, + {"harvestingSpeedWoodcutting", StatType::harvestingSpeedWoodcutting} + }; + + auto it = statMap.find(str); + return (it != statMap.end()) ? it->second : StatType::none; +} + +ItemCategory stringToItemCategory(const std::string& str) { + static const std::unordered_map categoryMap = { + {"bone", ItemCategory::bone}, + {"bow", ItemCategory::bow}, + {"crossbow", ItemCategory::crossbow}, + {"constructable", ItemCategory::constructable}, + {"torch", ItemCategory::torch}, + {"blacksmithhammer", ItemCategory::blacksmithhammer}, + {"questitem", ItemCategory::questitem}, + {"heavyArmor", ItemCategory::heavyArmor}, + {"warhammer", ItemCategory::warhammer}, + {"shield", ItemCategory::shield}, + {"hatchet", ItemCategory::hatchet}, + {"blade", ItemCategory::blade}, + {"armor", ItemCategory::armor}, + {"pickaxe", ItemCategory::pickaxe}, + {"fish", ItemCategory::fish}, + {"fishingrod", ItemCategory::fishingrod}, + {"shears", ItemCategory::shears}, + {"hammer", ItemCategory::hammer}, + {"battleaxe", ItemCategory::battleaxe}, + {"morningstar", ItemCategory::morningstar}, + {"wand", ItemCategory::wand}, + {"staff", ItemCategory::staff}, + {"dagger", ItemCategory::dagger} + }; + + auto it = categoryMap.find(str); + return (it != categoryMap.end()) ? it->second : ItemCategory::none; +} + +ItemType stringToItemType(const std::string& str) { + static const std::unordered_map typeMap = { + {"weapon", ItemType::weapon}, + {"shield", ItemType::shield}, + {"armor", ItemType::armor}, + {"head", ItemType::head}, + {"resource", ItemType::resource}, + {"consumable", ItemType::consumable}, + {"trinket", ItemType::trinket}, + {"bracelet", ItemType::bracelet} + }; + + auto it = typeMap.find(str); + return (it != typeMap.end()) ? it->second : ItemType::resource; +} + +Tool stringToTool(const std::string& str) { + static const std::unordered_map toolMap = { + {"hatchet", Tool::hatchet}, + {"pickaxe", Tool::pickaxe}, + {"broom", Tool::broom}, + {"fishingrod", Tool::fishingrod} + }; + + auto it = toolMap.find(str); + return (it != toolMap.end()) ? it->second : Tool::none; +} + +SkillType stringToSkillType(const std::string& str) { + static const std::unordered_map skillMap = { + {"woodcutting", SkillType::woodcutting}, + {"fishing", SkillType::fishing}, + {"swordsmanship", SkillType::swordsmanship}, + {"mining", SkillType::mining}, + {"archery", SkillType::archery}, + {"magic", SkillType::magic}, + {"defence", SkillType::defence}, + {"blacksmithy", SkillType::blacksmithy}, + {"tailoring", SkillType::tailoring}, + {"carpentry", SkillType::carpentry}, + {"alchemy", SkillType::alchemy}, + {"cooking", SkillType::cooking} + }; + + auto it = skillMap.find(str); + return (it != skillMap.end()) ? it->second : SkillType::none; +} + +WorkbenchType stringToWorkbenchType(const std::string& str) { + static const std::unordered_map workbenchMap = { + {"none", WorkbenchType::none}, + {"anvil", WorkbenchType::anvil}, + {"oven", WorkbenchType::oven}, + {"cooking", WorkbenchType::cooking}, + {"carpenter", WorkbenchType::carpenter}, + {"tailor", WorkbenchType::tailor}, + {"forge", WorkbenchType::forge}, + {"alchemist", WorkbenchType::alchemist}, + {"mystic", WorkbenchType::mystic} + }; + + auto it = workbenchMap.find(str); + return (it != workbenchMap.end()) ? it->second : WorkbenchType::none; +} + +GenstatType stringToGenstatType(const std::string& str) { + static const std::unordered_map genstatMap = { + {"dagger", GenstatType::dagger}, + {"broadsword", GenstatType::broadsword}, + {"battleaxe", GenstatType::battleaxe}, + {"greatsword", GenstatType::greatsword}, + {"morningstar", GenstatType::morningstar}, + {"hammer", GenstatType::hammer}, + {"spear", GenstatType::spear}, + {"bow", GenstatType::bow}, + {"staff", GenstatType::staff}, + {"wand", GenstatType::wand}, + {"crossbow", GenstatType::crossbow}, + {"woodenshield", GenstatType::woodenshield}, + {"wizardhat", GenstatType::wizardhat}, + {"wizardrobe", GenstatType::wizardrobe}, + {"grandwizardhat", GenstatType::grandwizardhat}, + {"grandwizardrobe", GenstatType::grandwizardrobe}, + {"leatherhood", GenstatType::leatherhood}, + {"leatherbracelet", GenstatType::leatherbracelet}, + {"leatherarmor", GenstatType::leatherarmor}, + {"studdedleatherhood", GenstatType::studdedleatherhood}, + {"studdedleatherbracelet", GenstatType::studdedleatherbracelet}, + {"studdedleatherarmor", GenstatType::studdedleatherarmor}, + {"helmet", GenstatType::helmet}, + {"shield", GenstatType::shield}, + {"armor", GenstatType::armor}, + {"platehelmet", GenstatType::platehelmet}, + {"kiteshield", GenstatType::kiteshield}, + {"platearmor", GenstatType::platearmor} + }; + + auto it = genstatMap.find(str); + return (it != genstatMap.end()) ? it->second : GenstatType::none; +} + +bool ItemsConfig::loadFromXML(const std::string& filepath) { + tinyxml2::XMLDocument doc; + tinyxml2::XMLError result = doc.LoadFile(filepath.c_str()); + + if (result != tinyxml2::XML_SUCCESS) { + std::cerr << "Failed to load items XML file: " << filepath << std::endl; + return false; + } + + tinyxml2::XMLElement* root = doc.FirstChildElement("items"); + if (!root) { + std::cerr << "Invalid XML structure: missing 'items' root element" << std::endl; + return false; + } + + m_items.clear(); + + // Parse all items + for (tinyxml2::XMLElement* itemElement = root->FirstChildElement("item"); + itemElement != nullptr; + itemElement = itemElement->NextSiblingElement("item")) { + parseItem(itemElement); + } + + std::cout << "Loaded " << m_items.size() << " items from XML" << std::endl; + return true; +} + +void ItemsConfig::parseItem(tinyxml2::XMLElement* itemElement) { + Item item; + + // Parse basic attributes + item.id = static_cast(getAttributeValueInt(itemElement, "id")); + item.name = getAttributeValue(itemElement, "name"); + item.description = getAttributeValue(itemElement, "description"); + item.category = stringToItemCategory(getAttributeValue(itemElement, "category")); + item.slot = stringToItemType(getAttributeValue(itemElement, "slot")); + item.tool = stringToTool(getAttributeValue(itemElement, "tool")); + item.skill = stringToSkillType(getAttributeValue(itemElement, "skill")); + item.comment = getAttributeValue(itemElement, "comment"); + + // Parse numeric attributes + item.price = static_cast(getAttributeValueInt(itemElement, "price")); + item.abilityid = static_cast(getAttributeValueInt(itemElement, "abilityid")); + item.learnabilityid = static_cast(getAttributeValueInt(itemElement, "learnabilityid")); + item.level = static_cast(getAttributeValueInt(itemElement, "level")); + item.foodlevel = static_cast(getAttributeValueInt(itemElement, "foodlevel")); + item.foodamount = static_cast(getAttributeValueInt(itemElement, "foodamount")); + item.foodfrequency = static_cast(getAttributeValueInt(itemElement, "foodfrequency")); + item.foodtime = static_cast(getAttributeValueInt(itemElement, "foodtime")); + item.maxstack = static_cast(getAttributeValueInt(itemElement, "maxstack")); + item.book = static_cast(getAttributeValueInt(itemElement, "book")); + + // Parse boolean attributes + item.stackable = getAttributeValueBool(itemElement, "stackable"); + item.twohanded = getAttributeValueBool(itemElement, "twohanded"); + item.undroppable = getAttributeValueBool(itemElement, "undroppable"); + item.hidemilestone = getAttributeValueBool(itemElement, "hidemilestone"); + + // Parse child elements + parseStats(itemElement, item); + parseCraftings(itemElement, item); + + m_items[item.id] = std::move(item); +} + +void ItemsConfig::parseStats(tinyxml2::XMLElement* itemElement, Item& item) { + for (tinyxml2::XMLElement* statElement = itemElement->FirstChildElement("stat"); + statElement != nullptr; + statElement = statElement->NextSiblingElement("stat")) { + + // Get all attributes of the stat element + const tinyxml2::XMLAttribute* attribute = statElement->FirstAttribute(); + while (attribute) { + StatType statType = stringToStatType(attribute->Name()); + std::string valueStr = attribute->Value(); + bool isPercentage = false; + uint8_t value = 0; + + // Check if value ends with '%' to indicate percentage + if (!valueStr.empty() && valueStr.back() == '%') { + isPercentage = true; + valueStr = valueStr.substr(0, valueStr.size() - 1); + } + + // Parse the numeric value + try { + value = static_cast(std::stoi(valueStr)); + } catch (const std::exception&) { + value = 0; // Default to 0 if parsing fails + } + + item.stats.push_back({statType, value, isPercentage}); + attribute = attribute->Next(); + } + } +} + + +void ItemsConfig::parseCraftings(tinyxml2::XMLElement* itemElement, Item& item) { + for (tinyxml2::XMLElement* craftElement = itemElement->FirstChildElement("crafting"); + craftElement != nullptr; + craftElement = craftElement->NextSiblingElement("crafting")) { + + ItemCrafting craft; + craft.workbench = stringToWorkbenchType(getAttributeValue(craftElement, "workbench")); + craft.craftingskill = stringToSkillType(getAttributeValue(craftElement, "craftingskill")); + craft.checks = getAttributeValue(craftElement, "checks"); + + // Parse crafting items (format: "itemId-amount,itemId-amount,..." + std::string craftingItemsStr = getAttributeValue(craftElement, "craftingitems"); + if (!craftingItemsStr.empty()) { + std::stringstream ss(craftingItemsStr); + std::string itemEntry; + while (std::getline(ss, itemEntry, ',')) { + // Remove any whitespace + itemEntry.erase(itemEntry.begin(), std::find_if(itemEntry.begin(), itemEntry.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + itemEntry.erase(std::find_if(itemEntry.rbegin(), itemEntry.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), itemEntry.end()); + + // Parse "itemId-amount" + size_t dashPos = itemEntry.find('-'); + if (dashPos != std::string::npos) { + try { + uint16_t itemId = static_cast(std::stoi(itemEntry.substr(0, dashPos))); + uint16_t amount = static_cast(std::stoi(itemEntry.substr(dashPos + 1))); + craft.craftingitems.push_back({itemId, amount}); + } catch (const std::exception&) { + // Skip invalid entries + } + } + } + } + + item.craftings.push_back(std::move(craft)); + } +} + +const Item* ItemsConfig::getItemById(int id) const { + auto it = m_items.find(id); + return (it != m_items.end()) ? &it->second : nullptr; +} + +std::string ItemsConfig::getAttributeValue(tinyxml2::XMLElement* element, const char* attribute, const std::string& defaultValue) { + const char* value = element->Attribute(attribute); + return value ? std::string(value) : defaultValue; +} + +int ItemsConfig::getAttributeValueInt(tinyxml2::XMLElement* element, const char* attribute, int defaultValue) { + int value = defaultValue; + element->QueryIntAttribute(attribute, &value); + return value; +} + +bool ItemsConfig::getAttributeValueBool(tinyxml2::XMLElement* element, const char* attribute, bool defaultValue) { + bool value = defaultValue; + element->QueryBoolAttribute(attribute, &value); + return value; +} + +} // namespace cursebreaker diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..fb55fb1 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,41 @@ +#include +#include + + +#include "project_parser.h" +#include "tree_builder.h" +#include "assets/scene.hpp" +#include "configs/items.hpp" + +int main() { + + ParseProject("../../CBAssets/_GameAssets/"); + + // build tree + for (auto& scene : g_parsedProject.m_sceneAssets) + { + BuildTree(scene); + } + + // Test items parser + std::cout << "Testing items parser..." << std::endl; + auto& itemsConfig = cursebreaker::ItemsConfig::getInstance(); + bool success = itemsConfig.loadFromXML("../../CBAssets/Data/XMLs/Items/Items.xml"); + if (success) { + std::cout << "Successfully loaded items XML!" << std::endl; + + // Test getting a specific item + const auto* item = itemsConfig.getItemById(150); + if (item) { + std::cout << "Found item: " << item->name << " (ID: " << item->id << ")" << std::endl; + std::cout << "Description: " << item->description << std::endl; + std::cout << "Level: " << item->level << ", Price: " << item->price << std::endl; + } else { + std::cout << "Item with ID 150 not found" << std::endl; + } + } else { + std::cout << "Failed to load items XML!" << std::endl; + } + + return 0; +} diff --git a/src/project_parser.cpp b/src/project_parser.cpp new file mode 100644 index 0000000..dcef951 --- /dev/null +++ b/src/project_parser.cpp @@ -0,0 +1,463 @@ +#include "project_parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +#include "assets/asset_base.hpp" +#include "assets/game_object.hpp" +#include "assets/transform.hpp" +#include "assets/mesh_filter.hpp" +#include "assets/prefab_instance.hpp" + +constexpr uint32_t fnv1a_hash(const char* str, uint32_t hash = 2166136261u) +{ + return (*str == '\0') ? hash : fnv1a_hash(str + 1, (hash ^ static_cast(*str)) * 16777619u); +} + +uint32_t fnv1a_hash(c4::csubstr substr) +{ + uint32_t hash = 2166136261u; + for (const auto& c : substr) + { + hash = (hash ^ static_cast(c)) * 16777619u; + } + return hash; +} + +uint32_t fnv1a_hash(std::string_view substr) +{ + uint32_t hash = 2166136261u; + for (const auto& c : substr) + { + hash = (hash ^ static_cast(c)) * 16777619u; + } + return hash; +} + + +AssetGUID GetAssetGUIDFromFile(const std::filesystem::path& path) +{ + /* + // read file as yaml and get "guid" + std::ifstream file(path); + std::string content{ std::istreambuf_iterator(file), std::istreambuf_iterator() }; + + try + { + ryml::Tree tree = ryml::parse_in_place(c4::substr{ content.data(), content.size() }); + ryml::ConstNodeRef root = tree.crootref(); + return ParseAssetGUID(root); + } + catch (const std::runtime_error& e) + { + std::cerr << "Error parsing asset GUID: " << e.what() << std::endl; + return AssetGUID{}; + } + */ + + // regex match the GUID from the contents of the file + static auto pattern = std::regex(R"(guid: (\w+))"); + std::smatch match{}; + std::ifstream file(path); + std::string content{ std::istreambuf_iterator(file), std::istreambuf_iterator() }; + if (std::regex_search(content, match, pattern)) + { + assert(match[1].length() == 32); + AssetGUID guid{ std::string_view{ &*match[1].first, static_cast(match[1].length()) } }; + return guid; + } + return AssetGUID{}; +} + +std::unordered_map ParseYamlFile(std::string& content, bool in_place) +{ + std::unordered_map Data{}; + + std::string::size_type start = content.find("---", 0); + std::string::size_type end = content.find("---", start + 1); + + while (end != std::string::npos) + { + auto document = std::string_view(content.data() + start, content.data() + end); + + // if the document contains "stripped", skip it + auto str_view = std::string_view(document.data(), document.size()); + if (str_view.find("stripped") != std::string_view::npos) + goto skip; + + { + auto secondLine = str_view.find('\n') + 1; + auto classStr = std::string_view(document.data() + secondLine, str_view.find(':', secondLine) - secondLine); + uint32_t classHash = fnv1a_hash(classStr); + + constexpr std::array availableClasses = { + fnv1a_hash("GameObject"), + fnv1a_hash("Transform"), + fnv1a_hash("MeshFilter"), + fnv1a_hash("PrefabInstance"), + fnv1a_hash("MonoBehaviour") + }; + if (std::ranges::find(availableClasses, classHash) == availableClasses.end()) goto skip; + } + + try + { + ryml::Tree tree{}; + + if (in_place) + { + tree = ryml::parse_in_place(c4::substr{ const_cast(document.data()), document.size() }); + } + else + { + tree = ryml::parse_in_arena(c4::csubstr{ const_cast(document.data()), document.size() }); + } + auto anchorData = tree.crootref().child(0).val_anchor(); + int64_t ID{ }; + auto [ptr, ec] = std::from_chars(anchorData.data(), anchorData.data() + anchorData.size(), ID); + assert(ec == std::errc{}); + + Data.emplace(ID, std::move(tree)); + } + catch (const std::runtime_error& e) + { + std::cerr << "Error parsing scene asset: " << e.what() << std::endl; + std::cerr << std::string_view(document.data(), document.size()) << std::endl; + } + + skip:; + + start = end; + end = content.find("---", start + 1); + + } + + return Data; +} + +int64_t GetNewID(const SceneAsset& scene, int64_t lastID = 0) +{ + for (;; ++lastID) + { + if (scene.IDtoAsset.find(lastID) == scene.IDtoAsset.end()) + return lastID; + } +} + +template +void MergeComponents(SceneAsset& scene, std::unordered_map& idMap, std::vector& to, const std::vector& from, int64_t& lastID) +{ + for (const auto& asset : from) + { + // create a copy of the asset + auto assetCopy = asset; + + // get a new ID + lastID = GetNewID(scene, lastID); + assetCopy.ID = lastID; + + // register the new ID to the scene + scene.IDtoAsset.emplace(assetCopy.ID, nullptr); + + // add the asset to the scene + to.emplace_back(assetCopy); + } +} + +void MergeScenes(SceneAsset& to, const SceneAsset& from) +{ + std::unordered_map newIDMap{}; + + // randomize the IDs to avoid conflicts + int64_t lastID = std::random_device{}(); + MergeComponents(to, newIDMap, to.gameObjects, from.gameObjects, lastID); + MergeComponents(to, newIDMap, to.transforms, from.transforms, lastID); + MergeComponents(to, newIDMap, to.meshFilters, from.meshFilters, lastID); + + auto& IDMapLinked = reinterpret_cast&>(newIDMap); + for (auto& gameObject : to.gameObjects) LinkGameObject(IDMapLinked, gameObject); + for (auto& transform : to.transforms) LinkTransform(IDMapLinked, transform); + for (auto& meshFilter : to.meshFilters) LinkMeshFilter(IDMapLinked, meshFilter); +} + +void ParseScene(SceneAsset& asset, const std::unordered_map& documents); + +void InstantiatePrefab(SceneAsset& parentScene, const ryml::Tree& instance) +{ + auto instanceData = instance.crootref().child(0).child(0); + + // parse the prebad UID + AssetGUID prefabAssetGUID = ParseAssetGUID(instanceData["m_SourcePrefab"]); + auto prefabAssetIt = g_parsedProject.m_prefabsMap.find(prefabAssetGUID); + if (prefabAssetIt == g_parsedProject.m_prefabsMap.end()) + { + std::cerr << "Prefab asset not found: " << std::endl; + return; + } + const PrefabAsset& prefabAsset = *prefabAssetIt->second; + auto prefabAssetTree = prefabAsset.Data; + + // apply the modifications to the prefab asset tree + const auto& overrides = instanceData["m_Modification"]["m_Modifications"]; + for (auto override : overrides) + { + int64_t ref = reinterpret_cast(ParseAssetRef(override["target"])); + auto prefabAssetTreeIt = prefabAssetTree.find(ref); + if (prefabAssetTreeIt == prefabAssetTree.end()) + { + std::cerr << "Prefab asset tree not found: " << ref << std::endl; + continue; + } + auto& targetTree = prefabAssetTreeIt->second; + + const auto propertyPath{ override["propertyPath"].val() }; + const auto& overrideValue = override["value"]; + + // Use lookup_path_or_modify to set the value at the property path + // Start from the root of the document (child(0) is the document root) + auto rootId = targetTree.crootref().child(0).id(); + targetTree.lookup_path_or_modify(&instance, overrideValue.id(), propertyPath, rootId); + } + + // parse the prefab asset tree into a scene + SceneAsset scene{}; + ParseScene(scene, prefabAssetTree); + + // get the index of the added transform + size_t addedTransformIndex = scene.transforms.size(); + + // merge the scenes + MergeScenes(parentScene, scene); + + // find the parent transform and add a the instantiated prebab to the children + auto parentID = ParseAssetRef(instanceData["m_TransformParent"]); + scene.transforms[addedTransformIndex].Parent = parentID; + auto parentTransform = std::find_if(scene.transforms.begin(), scene.transforms.end(), [parentID](const TransformAsset& asset) + { + return asset.ID == reinterpret_cast(parentID); + }); + if (parentTransform != scene.transforms.end()) + { + parentTransform->Children.push_back(reinterpret_cast(scene.transforms[addedTransformIndex].ID)); + } + +} + +void LinkAssets(SceneAsset& sceneAsset) +{ + std::unordered_map assetsMap; + + for (auto& gameObject : sceneAsset.gameObjects) assetsMap.emplace(gameObject.ID, &gameObject); + for (auto& transform : sceneAsset.transforms) assetsMap.emplace(transform.ID, &transform); + for (auto& meshFilter : sceneAsset.meshFilters) assetsMap.emplace(meshFilter.ID, &meshFilter); + + for (auto& gameObject : sceneAsset.gameObjects) LinkGameObject(assetsMap, gameObject); + for (auto& transform : sceneAsset.transforms) LinkTransform(assetsMap, transform); + for (auto& meshFilter : sceneAsset.meshFilters) LinkMeshFilter(assetsMap, meshFilter); +} + +void ParseScene(SceneAsset& asset, const std::unordered_map& documents) +{ + for (auto& [ID, tree] : documents) + { + try + { + ryml::ConstNodeRef root = tree.crootref(); + + // Each document should have one root node + if (root.num_children() > 0) + { + auto node = root.child(0).child(0); // The document root + auto assetType = node.key(); + + switch (fnv1a_hash(assetType)) + { + case fnv1a_hash("GameObject"): asset.gameObjects.emplace_back(ParseGameObject(node)).ID = ID; break; + case fnv1a_hash("Transform"): asset.transforms.emplace_back(ParseTransform(node)).ID = ID; break; + case fnv1a_hash("MeshFilter"): asset.meshFilters.emplace_back(ParseMeshFilter(node)).ID = ID; break; + case fnv1a_hash("PrefabInstance"): + { + InstantiatePrefab(asset, tree); + break; + } + case fnv1a_hash("MonoBehaviour"): + { + auto scriptGuid = ParseAssetGUID(node["m_Script"]); + auto it = g_parsedProject.m_scriptToClassHash.find(scriptGuid); + if (it != g_parsedProject.m_scriptToClassHash.end()) + { + switch (it->second) + { + case fnv1a_hash("Interactable_Resource"): + default: + break; + } + } + } + default: + std::cerr << assetType << '\n'; + } + + asset.IDtoAsset.emplace(ID, nullptr); + } + + } + catch (const std::exception& e) + { + std::cerr << e.what(); + } + } + + LinkAssets(asset); +} + +SceneAsset ParseSceneAsset(const AssetPath& path) +{ + SceneAsset sceneAsset; + sceneAsset.path = path.path; + sceneAsset.name = path.path.stem().string(); + + // read the file + std::ifstream file(path.path); + std::string content{ std::istreambuf_iterator(file), std::istreambuf_iterator() }; + + auto objects = ParseYamlFile(content, true); + + ParseScene(sceneAsset, objects); + + return sceneAsset; +} + + +std::vector ParseScenes(const std::vector& sceneFiles) +{ + std::vector sceneAssets; + for (const auto& sceneFile : sceneFiles) + { + sceneAssets.emplace_back(ParseSceneAsset(sceneFile)); + std::cout << "Parsed: " << sceneFile.path.filename() << '\n'; + } + return sceneAssets; +} + +PrefabAsset ParsePrefabAsset(const AssetPath& path) +{ + PrefabAsset prefabAsset; + prefabAsset.path = path.path; + prefabAsset.name = path.path.stem().string(); + + { + // read the file + std::ifstream file(path.path); + std::string content{ std::istreambuf_iterator(file), std::istreambuf_iterator() }; + + prefabAsset.Data = ParseYamlFile(content, false); + } + + return prefabAsset; +} + +std::vector ParsePrefabs(const std::vector& prefabFiles, std::unordered_map& prefabMap) +{ + std::vector prefabAssets; + for (const auto& prefabFile : prefabFiles) + { + prefabAssets.emplace_back(ParsePrefabAsset(prefabFile)); + } + + prefabMap.reserve(prefabAssets.size()); + for (size_t i = 0; i < prefabAssets.size(); ++i) + { + prefabMap.emplace(prefabFiles[i].GUID, &prefabAssets[i]); + } + return prefabAssets; +} + +void ParseDirectory(ParsedProject& project) +{ + for (const auto& entry : std::filesystem::recursive_directory_iterator(project.m_projectRoot)) + { + std::string extension = entry.path().extension().string(); + + if (!entry.is_directory()) + { + if (extension == ".unity") + { + project.m_sceneFiles.push_back(AssetPath{ entry.path(), GetAssetGUIDFromFile(entry.path().string() + ".meta") }); + } + else if (extension == ".prefab") + { + project.m_prefabFiles.push_back(AssetPath{ entry.path(), GetAssetGUIDFromFile(entry.path().string() + ".meta") }); + } + else if (extension == ".cs") + { + auto guid = GetAssetGUIDFromFile(entry.path().string() + ".meta"); + project.m_scriptFiles.push_back(AssetPath{ entry.path(), guid }); + project.m_scriptToClassHash.emplace(guid, fnv1a_hash(entry.path().filename().string())); + } + continue; + if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".bmp" || extension == ".tiff" || extension == ".webp") + { + project.m_imageFiles.push_back(AssetPath{ entry.path(), GetAssetGUIDFromFile(entry.path().string() + ".meta") }); + } + else if (extension == ".fbx" || extension == ".obj" || extension == ".glb" || extension == ".gltf") + { + project.m_meshFiles.push_back(AssetPath{ entry.path(), GetAssetGUIDFromFile(entry.path().string() + ".meta") }); + } + } + } +} + +ParsedProject g_parsedProject; + +void ParseProject(const std::filesystem::path& projectRoot) +{ + g_parsedProject = ParsedProject{}; + ParsedProject& project = g_parsedProject; + project.m_projectRoot = projectRoot; + + ParseDirectory(project); + + auto tilesFilesFilter = std::views::filter([](const AssetPath& path) + { +#ifdef _DEBUG + static auto pattern = std::regex(R"(Tiles\/10_3)"); +#else + static auto pattern = std::regex(R"(Tiles\/\d+_\d+)"); +#endif + return std::regex_search(path.path.generic_string(), pattern); + }); + auto tilesFiles = project.m_sceneFiles | tilesFilesFilter; + + auto prefabFilter = std::views::filter([](const AssetPath& path) + { + constexpr std::string_view prefabPath = "_GameAssets/Prefabs"; + return path.path.generic_string().find(prefabPath) != std::string::npos; + }); + auto prefabFiles = project.m_prefabFiles | prefabFilter; + + project.m_prefabAssets = ParsePrefabs(std::vector(prefabFiles.begin(), prefabFiles.end()), project.m_prefabsMap); + + project.m_sceneAssets = ParseScenes(std::vector(tilesFiles.begin(), tilesFiles.end())); +} + diff --git a/src/tree_builder.cpp b/src/tree_builder.cpp new file mode 100644 index 0000000..1088032 --- /dev/null +++ b/src/tree_builder.cpp @@ -0,0 +1,54 @@ +#include "tree_builder.h" +#include "assets/scene.hpp" +#include "assets/transform.hpp" + +#include + +#include +#include +#include + +glm::mat4 composeMatrix(const glm::vec3& pos, const glm::vec3& scale, const glm::quat& rotation) { + // Create translation matrix + glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), pos); + + // Convert quaternion to rotation matrix + glm::mat4 rotationMatrix = glm::mat4_cast(rotation); + + // Create scale matrix + glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale); + + // Combine: translation * rotation * scale + return translationMatrix * rotationMatrix * scaleMatrix; +} + +void BuildTreeHelper(TransformAsset& transform, const glm::mat4& parentMatrix) +{ + for (auto& child : transform.Children) + { + auto childMatrix = composeMatrix(transform.Position, transform.Scale, transform.Rotation); + transform.GlobalMatrix = parentMatrix * childMatrix; + + BuildTreeHelper(*child, transform.GlobalMatrix); + } +} + +void BuildTree(SceneAsset& sceneAsset) +{ + // find transforms with no parent + std::vector rootTransforms; + + for (auto& transform : sceneAsset.transforms) + { + if (transform.Parent == nullptr) + { + rootTransforms.push_back(&transform); + } + } + + // build tree + for (auto rootTransform : rootTransforms) + { + BuildTreeHelper(*rootTransform, glm::mat4(1.0f)); + } +}