Files
shadow-over-bishkek001/src/items/GameObjectLoader.cpp
T

429 lines
16 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"
#include <SDL.h>
#include <unordered_set>
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<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));
}
logger() << "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.id = item.at("id").get<std::string>();
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<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;
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<GameObjectData>& 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<std::vector<const GameObjectData*>> groups;
std::unordered_map<std::string, size_t> 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<InteractiveObject> GameObjectLoader::createInteractiveObjects(
const std::vector<InteractiveObjectData>& definitions,
Renderer& renderer,
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
const std::string& zipPath)
{
std::vector<InteractiveObject> 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<InteractiveObject>
GameObjectLoader::loadAndCreateInteractiveObjects(
const std::string& jsonPath, Renderer& renderer,
std::unordered_map<std::string,
std::shared_ptr<InteractiveObjectState>>& states,
const std::string& zipPath)
{
return createInteractiveObjects(
loadInteractiveFromJson(jsonPath, zipPath), renderer, states,
zipPath);
}
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.at("id").get<std::string>();
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<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.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<std::unique_ptr<Character>> GameObjectLoader::createNpcs(
const std::vector<NpcData>& definitions,
Renderer& renderer,
const std::unordered_map<std::string,
std::shared_ptr<CharacterState>>& states,
IGameServer& server,
const std::string& locationId,
const std::string& zipPath)
{
std::vector<std::unique_ptr<Character>> 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<std::unique_ptr<Character>>
GameObjectLoader::loadAndCreate_Npcs(
const std::string& jsonPath, Renderer& renderer,
std::unordered_map<std::string,
std::shared_ptr<CharacterState>>& states,
IGameServer& server, const std::string& locationId,
const std::string& zipPath)
{
return createNpcs(
loadNpcsFromJson(jsonPath, zipPath), renderer, states, server,
locationId, zipPath);
}
} // namespace FRG