#include "GameObjectLoader.h" #include #include #include #include "render/Renderer.h" #include "render/TextureManager.h" #include "utils/Utils.h" #include #include "../Character.h" #include #include namespace FRG { using json = nlohmann::json; // ----------------------------------------------------------------------- // Private helpers // ----------------------------------------------------------------------- json GameObjectLoader::loadJson(const std::string& jsonPath, const std::string& zipPath) { std::string content; try { if (zipPath.empty()) { content = readTextFile(jsonPath); } else { auto buf = readFileFromZIP(jsonPath, zipPath); if (buf.empty()) throw std::runtime_error("empty result from zip"); content.assign(buf.begin(), buf.end()); } } catch (const std::exception& e) { throw std::runtime_error("Failed to read " + jsonPath + ": " + e.what()); } try { return json::parse(content); } catch (const std::exception& e) { throw std::runtime_error("JSON parse error in " + jsonPath + ": " + e.what()); } } static GameObjectData parseGameObjectData(const json& item) { GameObjectData data; data.name = item.value("name", "Unknown"); data.texturePath = item.value("texturePath", ""); data.textureDarkandsPath = item.value("textureDarkandsPath", ""); data.meshPath = item.value("meshPath", ""); data.rotationX = item.value("rotationX", 0.0f); data.rotationY = item.value("rotationY", 0.0f); data.rotationZ = item.value("rotationZ", 0.0f); data.positionX = item.value("positionX", 0.0f); data.positionY = item.value("positionY", 0.0f); data.positionZ = item.value("positionZ", 0.0f); data.scale = item.value("scale", 1.0f); data.instanced = item.value("instanced", true); return data; } std::vector GameObjectLoader::loadFromJson(const std::string& jsonPath, const std::string& zipPath) { std::vector objects; json j = loadJson(jsonPath, zipPath); if (!j.contains("objects") || !j["objects"].is_array()) { std::cerr << "Warning: 'objects' array not found in " << jsonPath << std::endl; return objects; } for (const auto& item : j["objects"]) { GameObjectData data = parseGameObjectData(item); if (!data.meshPath.empty()) objects.push_back(std::move(data)); } logger() << "Loaded " << objects.size() << " static objects from " << jsonPath << std::endl; return objects; } std::vector GameObjectLoader::loadInteractiveFromJson(const std::string& jsonPath, const std::string& zipPath) { std::vector objects; json j = loadJson(jsonPath, zipPath); if (!j.contains("objects") || !j["objects"].is_array()) { std::cerr << "Warning: 'objects' array not found in " << jsonPath << std::endl; return objects; } for (const auto& item : j["objects"]) { InteractiveObjectData data; data.id = item.at("id").get(); if (data.id.empty()) throw std::runtime_error("Interactive object id must not be empty in " + jsonPath); data.base = parseGameObjectData(item); data.interactionRadius = item.value("interactionRadius", 2.0f); data.approachRadius = item.value("approachRadius", data.interactionRadius); data.castShadow = item.value("castShadow", true); data.castShadowNight = item.value("castShadowNight", true); data.isActive = item.value("isActive", true); data.activateFunctionName = item.value("activateFunction", ""); data.pivot = { item.value("pivotX", 0.0f), item.value("pivotY", 0.0f), item.value("pivotZ", 0.0f)}; data.boundsMin = { item.value("boundsMinX", 0.0f), item.value("boundsMinY", 0.0f), item.value("boundsMinZ", 0.0f)}; data.boundsMax = { item.value("boundsMaxX", 0.0f), item.value("boundsMaxY", 0.0f), item.value("boundsMaxZ", 0.0f)}; if (item.contains("interactionPositionX")) { data.hasInteractionPosition = true; data.interactionPosition = { item.value("interactionPositionX", 0.0f), item.value("interactionPositionY", 0.0f), item.value("interactionPositionZ", 0.0f)}; } if (!data.base.meshPath.empty()) objects.push_back(std::move(data)); } logger() << "Loaded " << objects.size() << " interactive objects from " << jsonPath << std::endl; return objects; } static VertexDataStruct loadMeshData(const std::string& meshPath, const std::string& zipPath) { if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0) return LoadModelFromBinFile(meshPath, zipPath); return LoadFromTextFile02(meshPath, zipPath); } static Eigen::Matrix3f makeRotationMatrix(const GameObjectData& data) { constexpr float kDeg2Rad = static_cast(M_PI) / 180.0f; Eigen::Quaternionf rot = Eigen::Quaternionf::Identity(); if (data.rotationX != 0.0f) rot = Eigen::Quaternionf(Eigen::AngleAxisf(data.rotationX * kDeg2Rad, Eigen::Vector3f::UnitX())) * rot; if (data.rotationY != 0.0f) rot = Eigen::Quaternionf(Eigen::AngleAxisf(data.rotationY * kDeg2Rad, Eigen::Vector3f::UnitY())) * rot; if (data.rotationZ != 0.0f) rot = Eigen::Quaternionf(Eigen::AngleAxisf(data.rotationZ * kDeg2Rad, Eigen::Vector3f::UnitZ())) * rot; return rot.toRotationMatrix(); } // Equivalent of what buildLoadedObject + Move() bake into the vertices: // RotateByMatrix computes v^T * R (= R^T * v), then Scale, then Move. static Eigen::Matrix4f makeInstanceMatrix(const GameObjectData& data) { Eigen::Matrix4f m = Eigen::Matrix4f::Identity(); m.block<3, 3>(0, 0) = data.scale * makeRotationMatrix(data).transpose(); m.block<3, 1>(0, 3) = Eigen::Vector3f(data.positionX, data.positionY, data.positionZ); return m; } LoadedGameObject GameObjectLoader::buildLoadedObject(const GameObjectData& data, Renderer& renderer, const std::string& zipPath) { LoadedGameObject obj; obj.name = data.name; obj.creationInfo = data.toLoadedGameObjectState(); obj.texture = renderer.textureManager.LoadFromPng(data.texturePath, zipPath); if (!data.textureDarkandsPath.empty()) obj.textureDarklands = renderer.textureManager.LoadFromPng(data.textureDarkandsPath, zipPath); obj.mesh.data = loadMeshData(data.meshPath, zipPath); obj.mesh.data.RotateByMatrix(makeRotationMatrix(data)); if (data.scale != 1.0f) obj.mesh.data.Scale(data.scale); return obj; } // ----------------------------------------------------------------------- // Public API // ----------------------------------------------------------------------- StaticObjectSet GameObjectLoader::createGameObjects( const std::vector& definitions, Renderer& renderer, const std::string& zipPath) { StaticObjectSet result; // Group objects sharing mesh + textures (in file order) so they can be drawn instanced. std::vector> groups; std::unordered_map groupIndexByKey; for (const auto& data : definitions) { if (!data.instanced) { groups.push_back({ &data }); continue; } std::string key = data.meshPath + '\n' + data.texturePath + '\n' + data.textureDarkandsPath; auto it = groupIndexByKey.find(key); if (it == groupIndexByKey.end()) { groupIndexByKey[key] = groups.size(); groups.push_back({ &data }); } else { groups[it->second].push_back(&data); } } for (const auto& group : groups) { // Service the OS message queue between loads so the window doesn't // get flagged as "not responding" during long location loads. SDL_PumpEvents(); const GameObjectData& first = *group.front(); if (group.size() == 1) { logger() << "Loading game object: " << first.name << std::endl; try { LoadedGameObject obj = buildLoadedObject(first, renderer, zipPath); if (first.positionX != 0.0f || first.positionY != 0.0f || first.positionZ != 0.0f) obj.mesh.data.Move({ first.positionX, first.positionY, first.positionZ }); obj.mesh.RefreshVBO(); result.single[first.name] = std::move(obj); logger() << "Successfully loaded: " << first.name << std::endl; } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load '" << first.name << "': " << e.what() << std::endl; } continue; } logger() << "Loading instanced batch: " << first.meshPath << " x" << group.size() << std::endl; try { InstancedBatch batch; batch.name = first.name; batch.texture = renderer.textureManager.LoadFromPng(first.texturePath, zipPath); if (!first.textureDarkandsPath.empty()) batch.textureDarklands = renderer.textureManager.LoadFromPng(first.textureDarkandsPath, zipPath); // Mesh stays in local space; rotation, scale and position live in the instance matrices. batch.mesh.data = loadMeshData(first.meshPath, zipPath); batch.mesh.RefreshVBO(); batch.instances.reserve(group.size()); for (const GameObjectData* d : group) batch.instances.push_back(makeInstanceMatrix(*d)); batch.RefreshInstanceVBO(); result.batches.push_back(std::move(batch)); logger() << "Successfully loaded instanced batch: " << first.meshPath << std::endl; } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load instanced batch '" << first.meshPath << "': " << e.what() << std::endl; } } logger() << "Total game objects loaded: " << result.single.size() << " single, " << result.batches.size() << " instanced batches" << std::endl; return result; } StaticObjectSet GameObjectLoader::loadAndCreateGameObjects( const std::string& jsonPath, Renderer& renderer, const std::string& zipPath) { return createGameObjects( loadFromJson(jsonPath, zipPath), renderer, zipPath); } std::vector GameObjectLoader::createInteractiveObjects( const std::vector& definitions, Renderer& renderer, std::unordered_map>& states, const std::string& zipPath) { std::vector interactiveObjects; for (const auto& data : definitions) { SDL_PumpEvents(); logger() << "Loading interactive object: " << data.base.name << std::endl; try { auto state = states.find(data.id); if (state == states.end() || !state->second) throw std::runtime_error( "Authoritative state missing for '" + data.id + "'"); InteractiveObject intObj = InteractiveObject::createFromState( *state->second, renderer, zipPath); intObj.id = data.id; intObj.loadedObject.name = data.base.name; if (!data.activateFunctionName.empty()) logger() << "Successfully loaded interactive: " << data.base.name << " (function: " << data.activateFunctionName << ")" << std::endl; else logger() << "Successfully loaded interactive: " << data.base.name << std::endl; interactiveObjects.push_back(std::move(intObj)); } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load interactive '" << data.base.name << "': " << e.what() << std::endl; } } logger() << "Total interactive objects loaded: " << interactiveObjects.size() << std::endl; return interactiveObjects; } std::vector GameObjectLoader::loadAndCreateInteractiveObjects( const std::string& jsonPath, Renderer& renderer, std::unordered_map>& states, const std::string& zipPath) { return createInteractiveObjects( loadInteractiveFromJson(jsonPath, zipPath), renderer, states, zipPath); } std::vector GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath) { std::vector npcs; json j = loadJson(jsonPath, zipPath); if (!j.contains("npcs") || !j["npcs"].is_array()) { std::cerr << "Warning: 'npcs' array not found in " << jsonPath << std::endl; return npcs; } try { for (const auto& item : j["npcs"]) { NpcData data; data.id = item.at("id").get(); if (data.id.empty()) throw std::runtime_error("NPC id must not be empty in " + jsonPath); data.name = item.value("name", "Unknown NPC"); data.texturePath = item.value("texturePath", ""); if (item.contains("meshTextures") && item["meshTextures"].is_object()) { for (auto it = item["meshTextures"].begin(); it != item["meshTextures"].end(); ++it) { if (it.value().is_string()) data.meshTextures[it.key()] = it.value().get(); } } data.interactionRadius = item.value("interactionRadius", 0.0f); data.animationIdlePath = item.value("animationIdlePath", ""); data.animationWalkPath = item.value("animationWalkPath", ""); data.animationActionIdlePath = item.value("animationActionIdlePath", ""); data.animationActionAttackPath = item.value("animationActionAttackPath", ""); data.animationStandToActionPath = item.value("animationStandToActionPath", ""); data.animationActionToStandPath = item.value("animationActionToStandPath", ""); data.animationActionToDeathPath = item.value("animationActionToDeathPath", ""); data.animationDeathIdlePath = item.value("animationDeathIdlePath", ""); data.positionX = item.value("positionX", 0.0f); data.positionY = item.value("positionY", 0.0f); data.positionZ = item.value("positionZ", 0.0f); data.walkSpeed = item.value("walkSpeed", 1.5f); data.rotationSpeed = item.value("rotationSpeed", 8.0f); data.modelScale = item.value("modelScale", 0.01f); data.modelCorrectionRotX = item.value("modelCorrectionRotX", 0.0f) * static_cast(M_PI) / 180.0f; data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast(M_PI) / 180.0f; data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast(M_PI) / 180.0f; data.facingAngle = item.value("facingAngle", 0.0f) * static_cast(M_PI) / 180.0f; data.hp = item.value("hp", 100.0f); data.visibleRange = item.value("visibleRange", VISIBLE_RANGE); data.canAttack = item.value("canAttack", false); data.enabled = item.value("enabled", true); npcs.push_back(std::move(data)); } logger() << "Successfully loaded " << npcs.size() << " NPCs from " << jsonPath << std::endl; } catch (const std::exception& e) { std::cerr << "Error loading NPCs from JSON: " << e.what() << std::endl; throw; } return npcs; } std::vector> GameObjectLoader::createNpcs( const std::vector& definitions, Renderer& renderer, const std::unordered_map>& states, IGameServer& server, const std::string& locationId, const std::string& zipPath) { std::vector> npcs; for (const auto& npcData : definitions) { SDL_PumpEvents(); logger() << "Loading NPC: " << npcData.name << std::endl; try { auto state = states.find(npcData.id); if (state == states.end() || !state->second) throw std::runtime_error( "Authoritative state missing for '" + npcData.id + "'"); auto npc = Character::createFromState( *state->second, server, locationId, npcData.id, renderer, zipPath.c_str()); if (npc) { logger() << "Successfully loaded NPC: " << npcData.name << " at (" << npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl; npcs.push_back(std::move(npc)); } } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load NPC '" << npcData.name << "': " << e.what() << std::endl; } } logger() << "Total NPCs loaded: " << npcs.size() << std::endl; return npcs; } std::vector> GameObjectLoader::loadAndCreate_Npcs( const std::string& jsonPath, Renderer& renderer, std::unordered_map>& states, IGameServer& server, const std::string& locationId, const std::string& zipPath) { return createNpcs( loadNpcsFromJson(jsonPath, zipPath), renderer, states, server, locationId, zipPath); } } // namespace FRG