380 lines
15 KiB
C++
380 lines
15 KiB
C++
#include "GameObjectLoader.h"
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <cmath>
|
|
#include "render/Renderer.h"
|
|
#include "render/TextureManager.h"
|
|
#include "utils/Utils.h"
|
|
#include <Eigen/Geometry>
|
|
#include "../Character.h"
|
|
|
|
namespace ZL {
|
|
|
|
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);
|
|
return data;
|
|
}
|
|
|
|
std::vector<GameObjectData> GameObjectLoader::loadFromJson(const std::string& jsonPath, const std::string& zipPath)
|
|
{
|
|
std::vector<GameObjectData> 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));
|
|
}
|
|
|
|
std::cout << "Loaded " << objects.size() << " static objects from " << jsonPath << std::endl;
|
|
return objects;
|
|
}
|
|
|
|
std::vector<InteractiveObjectData> GameObjectLoader::loadInteractiveFromJson(const std::string& jsonPath, const std::string& zipPath)
|
|
{
|
|
std::vector<InteractiveObjectData> 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.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.pivotX = item.value("pivotX", 0.0f);
|
|
data.pivotY = item.value("pivotY", 0.0f);
|
|
data.pivotZ = item.value("pivotZ", 0.0f);
|
|
data.boundsMinX = item.value("boundsMinX", 0.0f);
|
|
data.boundsMinY = item.value("boundsMinY", 0.0f);
|
|
data.boundsMinZ = item.value("boundsMinZ", 0.0f);
|
|
data.boundsMaxX = item.value("boundsMaxX", 0.0f);
|
|
data.boundsMaxY = item.value("boundsMaxY", 0.0f);
|
|
data.boundsMaxZ = item.value("boundsMaxZ", 0.0f);
|
|
if (item.contains("interactionPositionX")) {
|
|
data.hasInteractionPosition = true;
|
|
data.interactionPositionX = item.value("interactionPositionX", 0.0f);
|
|
data.interactionPositionY = item.value("interactionPositionY", 0.0f);
|
|
data.interactionPositionZ = item.value("interactionPositionZ", 0.0f);
|
|
}
|
|
if (!data.base.meshPath.empty())
|
|
objects.push_back(std::move(data));
|
|
}
|
|
|
|
std::cout << "Loaded " << objects.size() << " interactive objects from " << jsonPath << std::endl;
|
|
return objects;
|
|
}
|
|
|
|
LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const {
|
|
LoadedGameObjectState st;
|
|
st.texturePath = texturePath;
|
|
st.textureDarkandsPath = textureDarkandsPath;
|
|
st.meshPath = meshPath;
|
|
st.meshRotationX = rotationX;
|
|
st.meshRotationY = rotationY;
|
|
st.meshRotationZ = rotationZ;
|
|
st.meshScale = scale;
|
|
return st;
|
|
}
|
|
|
|
InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const {
|
|
InteractiveObjectState st;
|
|
st.objectInfo = base.toLoadedGameObjectState();
|
|
st.interactionRadius = interactionRadius;
|
|
st.approachRadius = approachRadius;
|
|
st.castShadow = castShadow;
|
|
st.castShadowNight = castShadowNight;
|
|
st.isActive = isActive;
|
|
st.activateFunctionName = activateFunctionName;
|
|
st.pivot = Eigen::Vector3f(pivotX, pivotY, pivotZ);
|
|
st.boundsMin = Eigen::Vector3f(boundsMinX, boundsMinY, boundsMinZ);
|
|
st.boundsMax = Eigen::Vector3f(boundsMaxX, boundsMaxY, boundsMaxZ);
|
|
st.hasInteractionPosition = hasInteractionPosition;
|
|
if (hasInteractionPosition)
|
|
st.interactionPosition = Eigen::Vector3f(interactionPositionX, interactionPositionY, interactionPositionZ);
|
|
st.jsonPositionX = base.positionX;
|
|
st.jsonPositionY = base.positionY;
|
|
st.jsonPositionZ = base.positionZ;
|
|
// state.position is set by InteractiveObject::createFromState() after mesh-center computation
|
|
return st;
|
|
}
|
|
|
|
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);
|
|
|
|
if (data.meshPath.size() > 4 && data.meshPath.compare(data.meshPath.size() - 4, 4, ".bin") == 0)
|
|
obj.mesh.data = LoadModelFromBinFile(data.meshPath, zipPath);
|
|
else
|
|
obj.mesh.data = LoadFromTextFile02(data.meshPath, zipPath);
|
|
|
|
constexpr float kDeg2Rad = static_cast<float>(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;
|
|
obj.mesh.data.RotateByMatrix(rot.toRotationMatrix());
|
|
|
|
if (data.scale != 1.0f)
|
|
obj.mesh.data.Scale(data.scale);
|
|
|
|
return obj;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Public API
|
|
// -----------------------------------------------------------------------
|
|
|
|
std::unordered_map<std::string, LoadedGameObject> GameObjectLoader::loadAndCreateGameObjects(
|
|
const std::string& jsonPath,
|
|
Renderer& renderer,
|
|
const std::string& zipPath)
|
|
{
|
|
std::unordered_map<std::string, LoadedGameObject> gameObjects;
|
|
|
|
for (const auto& data : loadFromJson(jsonPath, zipPath)) {
|
|
std::cout << "Loading game object: " << data.name << std::endl;
|
|
try {
|
|
LoadedGameObject obj = buildLoadedObject(data, renderer, zipPath);
|
|
|
|
if (data.positionX != 0.0f || data.positionY != 0.0f || data.positionZ != 0.0f)
|
|
obj.mesh.data.Move({ data.positionX, data.positionY, data.positionZ });
|
|
|
|
obj.mesh.RefreshVBO();
|
|
gameObjects[data.name] = std::move(obj);
|
|
std::cout << "Successfully loaded: " << data.name << std::endl;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "GameObjectLoader: Failed to load '" << data.name << "': " << e.what() << std::endl;
|
|
}
|
|
}
|
|
|
|
std::cout << "Total game objects loaded: " << gameObjects.size() << std::endl;
|
|
return gameObjects;
|
|
}
|
|
|
|
std::vector<InteractiveObject> GameObjectLoader::loadAndCreateInteractiveObjects(
|
|
const std::string& jsonPath,
|
|
Renderer& renderer,
|
|
const std::string& zipPath)
|
|
{
|
|
std::vector<InteractiveObject> interactiveObjects;
|
|
|
|
if (jsonPath.empty())
|
|
return interactiveObjects;
|
|
|
|
for (const auto& data : loadInteractiveFromJson(jsonPath, zipPath)) {
|
|
std::cout << "Loading interactive object: " << data.base.name << std::endl;
|
|
try {
|
|
InteractiveObject intObj = InteractiveObject::createFromState(
|
|
data.toInteractiveObjectState(), renderer, zipPath);
|
|
intObj.loadedObject.name = data.base.name;
|
|
|
|
if (!data.activateFunctionName.empty())
|
|
std::cout << "Successfully loaded interactive: " << data.base.name
|
|
<< " (function: " << data.activateFunctionName << ")" << std::endl;
|
|
else
|
|
std::cout << "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;
|
|
}
|
|
}
|
|
|
|
std::cout << "Total interactive objects loaded: " << interactiveObjects.size() << std::endl;
|
|
return interactiveObjects;
|
|
}
|
|
|
|
std::vector<NpcData> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
|
|
{
|
|
std::vector<NpcData> 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.value("id", "npc_unknown");
|
|
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<std::string>();
|
|
}
|
|
}
|
|
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<float>(M_PI) / 180.0f;
|
|
data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
|
data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
|
data.facingAngle = item.value("facingAngle", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
|
data.hp = item.value("hp", 100.0f);
|
|
data.canAttack = item.value("canAttack", false);
|
|
data.enabled = item.value("enabled", true);
|
|
|
|
npcs.push_back(std::move(data));
|
|
}
|
|
std::cout << "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;
|
|
}
|
|
|
|
return npcs;
|
|
}
|
|
|
|
CharacterState NpcData::toCharacterState() const {
|
|
CharacterState st;
|
|
|
|
// Identity + game state
|
|
st.npcId = id;
|
|
st.npcName = name;
|
|
st.position = Eigen::Vector3f(positionX, positionY, positionZ);
|
|
st.facingAngle = facingAngle;
|
|
st.walkSpeed = walkSpeed;
|
|
st.rotationSpeed = rotationSpeed;
|
|
st.modelScale = modelScale;
|
|
st.setHp(hp);
|
|
st.initialHp = hp;
|
|
st.canAttack = canAttack;
|
|
st.enabled = enabled;
|
|
st.interactionRadius = interactionRadius;
|
|
|
|
Eigen::Quaternionf corrRot = Eigen::Quaternionf::Identity();
|
|
if (modelCorrectionRotX != 0.0f)
|
|
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotX, Eigen::Vector3f::UnitX())) * corrRot;
|
|
if (modelCorrectionRotY != 0.0f)
|
|
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotY, Eigen::Vector3f::UnitY())) * corrRot;
|
|
if (modelCorrectionRotZ != 0.0f)
|
|
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * corrRot;
|
|
st.modelCorrectionRotation = corrRot;
|
|
|
|
// Creation info: animation paths
|
|
CharacterCreationInfo& info = st.creationInfo;
|
|
auto addAnim = [&](AnimationState animState, const std::string& path) {
|
|
if (!path.empty())
|
|
info.animationPaths.emplace_back(animState, path);
|
|
};
|
|
addAnim(AnimationState::STAND, animationIdlePath);
|
|
addAnim(AnimationState::WALK, animationWalkPath);
|
|
addAnim(AnimationState::ACTION_IDLE, animationActionIdlePath);
|
|
addAnim(AnimationState::ACTION_ATTACK, animationActionAttackPath);
|
|
addAnim(AnimationState::STAND_TO_ACTION, animationStandToActionPath);
|
|
addAnim(AnimationState::ACTION_TO_STAND, animationActionToStandPath);
|
|
addAnim(AnimationState::ACTION_TO_DEATH, animationActionToDeathPath);
|
|
addAnim(AnimationState::DEATH_IDLE, animationDeathIdlePath);
|
|
|
|
// Creation info: textures
|
|
info.meshTexturePaths = std::unordered_map<std::string, std::string>(meshTextures.begin(), meshTextures.end());
|
|
info.singleTexturePath = texturePath;
|
|
|
|
return st;
|
|
}
|
|
|
|
std::vector<std::unique_ptr<Character>> GameObjectLoader::loadAndCreate_Npcs(
|
|
const std::string& jsonPath,
|
|
Renderer& renderer,
|
|
const std::string& zipPath)
|
|
{
|
|
std::vector<std::unique_ptr<Character>> npcs;
|
|
|
|
for (const auto& npcData : loadNpcsFromJson(jsonPath, zipPath)) {
|
|
std::cout << "Loading NPC: " << npcData.name << std::endl;
|
|
try {
|
|
auto npc = Character::createFromState(npcData.toCharacterState(), renderer, zipPath.c_str());
|
|
if (npc) {
|
|
std::cout << "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;
|
|
}
|
|
}
|
|
|
|
std::cout << "Total NPCs loaded: " << npcs.size() << std::endl;
|
|
return npcs;
|
|
}
|
|
|
|
} // namespace ZL
|