Separating state out of the location

This commit is contained in:
Vladislav Khorev committed 2026-10-30 14:56:24 +06:00
1 parent 5e9a663812
commit 297bd7377f
45 files changed
+2114 -778

No files matched your search

+427
View File
@@ -0,0 +1,427 @@
#include "LocationDefinition.h"
#include "../external/nlohmann/json.hpp"
#include "../utils/Utils.h"
#include <Eigen/Geometry>
#include <cmath>
#include <cstring>
#include <stdexcept>
#include <unordered_set>
namespace FRG {
namespace {
using json = nlohmann::json;
constexpr float kPi = 3.14159265358979323846f;
json loadJson(const std::string& path, const std::string& zipFile)
{
if (path.empty()) return json::object();
std::string content;
if (zipFile.empty()) {
content = readTextFile(path);
} else {
const auto bytes = readFileFromZIP(path, zipFile);
content.assign(bytes.begin(), bytes.end());
}
if (content.empty()) throw std::runtime_error("Failed to read " + path);
try {
return json::parse(content);
} catch (const std::exception& error) {
throw std::runtime_error(
"JSON parse error in " + path + ": " + error.what());
}
}
GameObjectData parseGameObject(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.f);
data.rotationY = item.value("rotationY", 0.f);
data.rotationZ = item.value("rotationZ", 0.f);
data.positionX = item.value("positionX", 0.f);
data.positionY = item.value("positionY", 0.f);
data.positionZ = item.value("positionZ", 0.f);
data.scale = item.value("scale", 1.f);
data.instanced = item.value("instanced", true);
return data;
}
Eigen::Vector3f vector(
const json& item, const char* x, const char* y, const char* z,
const Eigen::Vector3f& fallback = Eigen::Vector3f::Zero())
{
return {item.value(x, fallback.x()), item.value(y, fallback.y()),
item.value(z, fallback.z())};
}
std::pair<int, int> readBinaryAnimationMetadata(
const std::string& path, const std::string& zipFile)
{
const std::vector<char> bytes = zipFile.empty()
? readFile(path) : readFileFromZIP(path, zipFile);
if (bytes.size() < 8)
throw std::runtime_error("Invalid animation file " + path);
const char* cursor = bytes.data();
const char* end = cursor + bytes.size();
auto skip = [&](std::size_t size) {
if (size > static_cast<std::size_t>(end - cursor))
throw std::runtime_error("Unexpected EOF in " + path);
cursor += size;
};
auto readU32 = [&]() {
std::uint32_t value = 0;
if (sizeof(value) > static_cast<std::size_t>(end - cursor))
throw std::runtime_error("Unexpected EOF in " + path);
std::memcpy(&value, cursor, sizeof(value));
cursor += sizeof(value);
return value;
};
auto readI32 = [&]() {
std::int32_t value = 0;
if (sizeof(value) > static_cast<std::size_t>(end - cursor))
throw std::runtime_error("Unexpected EOF in " + path);
std::memcpy(&value, cursor, sizeof(value));
cursor += sizeof(value);
return value;
};
auto skipString = [&]() { skip(readU32()); };
if (std::memcmp(cursor, "BSMF", 4) != 0)
throw std::runtime_error("Invalid animation file " + path);
skip(4);
if (readU32() != 2)
throw std::runtime_error("Unsupported animation version in " + path);
skip(16 * sizeof(float));
const std::uint32_t boneCount = readU32();
for (std::uint32_t bone = 0; bone < boneCount; ++bone) {
skip(3 * sizeof(float) + sizeof(float) + 9 * sizeof(float));
skip(sizeof(std::int32_t));
skip(static_cast<std::size_t>(readU32()) * sizeof(std::int32_t));
}
for (std::uint32_t bone = 0; bone < boneCount; ++bone) skipString();
const std::uint32_t meshCount = readU32();
for (std::uint32_t mesh = 0; mesh < meshCount; ++mesh) {
skipString();
const std::uint32_t vertexCount = readU32();
skip(static_cast<std::size_t>(vertexCount) * 3 * sizeof(float));
const std::uint32_t faceCount = readU32();
skip(static_cast<std::size_t>(faceCount) * 3 * 2 * sizeof(float));
skip(static_cast<std::size_t>(vertexCount) * 3 * sizeof(float));
const std::uint32_t triangleCount = readU32();
skip(static_cast<std::size_t>(triangleCount) * 3 * sizeof(std::int32_t));
for (std::uint32_t vertex = 0; vertex < vertexCount; ++vertex)
skip(static_cast<std::size_t>(readU32()) *
(sizeof(std::int32_t) + sizeof(float)));
}
const std::uint32_t keyframeCount = readU32();
int lastFrame = 0;
for (std::uint32_t frame = 0; frame < keyframeCount; ++frame) {
lastFrame = readI32();
skip(static_cast<std::size_t>(boneCount) *
(3 * sizeof(float) + 16 * sizeof(float)));
}
return {0, keyframeCount == 0 ? 1 : lastFrame + 1};
}
} // namespace
LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const
{
LoadedGameObjectState state;
state.texturePath = texturePath;
state.textureDarkandsPath = textureDarkandsPath;
state.meshPath = meshPath;
state.meshRotationX = rotationX;
state.meshRotationY = rotationY;
state.meshRotationZ = rotationZ;
state.meshScale = scale;
return state;
}
InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const
{
InteractiveObjectState state;
state.objectInfo = base.toLoadedGameObjectState();
state.interactionRadius = interactionRadius;
state.approachRadius = approachRadius;
state.castShadow = castShadow;
state.castShadowNight = castShadowNight;
state.isActive = isActive;
state.activateFunctionName = activateFunctionName;
state.pivot = pivot;
state.boundsMin = boundsMin;
state.boundsMax = boundsMax;
state.hasInteractionPosition = hasInteractionPosition;
state.interactionPosition = interactionPosition;
state.jsonPositionX = base.positionX;
state.jsonPositionY = base.positionY;
state.jsonPositionZ = base.positionZ;
state.position =
Eigen::Vector3f(base.positionX, base.positionY, base.positionZ);
return state;
}
CharacterState NpcData::toCharacterState() const
{
CharacterState state;
state.npcName = name;
state.position = {positionX, positionY, positionZ};
state.homePosition = {positionX, 0.f, positionZ};
state.stopInPlace();
state.facingAngle = facingAngle;
state.walkSpeed = walkSpeed;
state.rotationSpeed = rotationSpeed;
state.modelScale = modelScale;
state.setHp(hp);
state.initialHp = hp;
state.visibleRange = visibleRange;
state.canAttack = canAttack;
if (canAttack) state.attackTargetId = "player";
state.enabled = enabled;
state.interactionRadius = interactionRadius;
Eigen::Quaternionf correction = Eigen::Quaternionf::Identity();
if (modelCorrectionRotX != 0.f)
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
modelCorrectionRotX, Eigen::Vector3f::UnitX())) * correction;
if (modelCorrectionRotY != 0.f)
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
modelCorrectionRotY, Eigen::Vector3f::UnitY())) * correction;
if (modelCorrectionRotZ != 0.f)
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * correction;
state.modelCorrectionRotation = correction;
auto addAnimation = [&](AnimationState animation, const std::string& path) {
if (!path.empty())
state.creationInfo.animationPaths.emplace_back(animation, path);
};
addAnimation(AnimationState::STAND, animationIdlePath);
addAnimation(AnimationState::WALK, animationWalkPath);
addAnimation(AnimationState::ACTION_IDLE, animationActionIdlePath);
addAnimation(AnimationState::ACTION_ATTACK, animationActionAttackPath);
addAnimation(AnimationState::STAND_TO_ACTION, animationStandToActionPath);
addAnimation(AnimationState::ACTION_TO_STAND, animationActionToStandPath);
addAnimation(AnimationState::ACTION_TO_DEATH, animationActionToDeathPath);
addAnimation(AnimationState::DEATH_IDLE, animationDeathIdlePath);
state.creationInfo.meshTexturePaths = {
meshTextures.begin(), meshTextures.end()};
state.creationInfo.singleTexturePath = texturePath;
return state;
}
void loadCharacterAnimationMetadata(
CharacterState& state, const std::string& zipFile)
{
for (const auto& [animation, path] : state.creationInfo.animationPaths) {
if (path.size() < 5 || path.substr(path.size() - 5) != ".anim")
continue;
try {
const auto [startFrame, frameCount] =
readBinaryAnimationMetadata(path, zipFile);
state.setAnimationMetadata(animation, startFrame, frameCount);
} catch (const std::exception& error) {
logger() << "[content] Animation metadata failed for " << path
<< ": " << error.what() << '\n';
}
}
}
LocationDefinition LocationContentLoader::load(
const LocationConfig& config, const std::string& zipFile)
{
LocationDefinition result;
result.config = config;
if (!config.gameObjectsJsonPath.empty()) {
const json root = loadJson(config.gameObjectsJsonPath, zipFile);
for (const auto& item : root.value("objects", json::array())) {
GameObjectData data = parseGameObject(item);
if (!data.meshPath.empty())
result.staticObjects.push_back(std::move(data));
}
}
if (!config.interactiveObjectsJsonPath.empty()) {
const json root = loadJson(config.interactiveObjectsJsonPath, zipFile);
std::unordered_set<std::string> ids;
for (const auto& item : root.value("objects", json::array())) {
InteractiveObjectData data;
data.id = item.at("id").get<std::string>();
if (data.id.empty() || !ids.insert(data.id).second)
throw std::runtime_error(
"Missing or duplicate interactive-object id in " +
config.interactiveObjectsJsonPath);
data.base = parseGameObject(item);
data.interactionRadius = item.value("interactionRadius", 2.f);
data.approachRadius =
item.value("approachRadius", data.interactionRadius);
data.activateFunctionName =
item.value("activateFunction", "");
data.pivot = vector(item, "pivotX", "pivotY", "pivotZ");
data.boundsMin =
vector(item, "boundsMinX", "boundsMinY", "boundsMinZ");
data.boundsMax =
vector(item, "boundsMaxX", "boundsMaxY", "boundsMaxZ");
data.hasInteractionPosition =
item.contains("interactionPositionX");
if (data.hasInteractionPosition) {
data.interactionPosition = vector(
item, "interactionPositionX", "interactionPositionY",
"interactionPositionZ");
}
data.castShadow = item.value("castShadow", true);
data.castShadowNight = item.value("castShadowNight", true);
data.isActive = item.value("isActive", true);
result.interactiveObjects.push_back(std::move(data));
}
}
if (!config.npcsJsonPath.empty()) {
const json root = loadJson(config.npcsJsonPath, zipFile);
std::unordered_set<std::string> ids;
for (const auto& item : root.value("npcs", json::array())) {
NpcData data;
data.id = item.at("id").get<std::string>();
if (data.id.empty() || data.id == "player" ||
!ids.insert(data.id).second) {
throw std::runtime_error(
"Missing, duplicate, or reserved NPC id in " +
config.npcsJsonPath);
}
data.name = item.value("name", "Unknown NPC");
data.texturePath = item.value("texturePath", "");
if (item.contains("meshTextures") &&
item["meshTextures"].is_object()) {
for (const auto& [name, path] :
item["meshTextures"].items()) {
if (path.is_string())
data.meshTextures[name] = path.get<std::string>();
}
}
data.interactionRadius =
item.value("interactionRadius", 0.f);
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.f);
data.positionY = item.value("positionY", 0.f);
data.positionZ = item.value("positionZ", 0.f);
data.walkSpeed = item.value("walkSpeed", 1.5f);
data.rotationSpeed = item.value("rotationSpeed", 8.f);
data.modelScale = item.value("modelScale", 0.01f);
const float degrees = kPi / 180.f;
data.modelCorrectionRotX =
item.value("modelCorrectionRotX", 0.f) * degrees;
data.modelCorrectionRotY =
item.value("modelCorrectionRotY", 0.f) * degrees;
data.modelCorrectionRotZ =
item.value("modelCorrectionRotZ", 0.f) * degrees;
data.facingAngle =
item.value("facingAngle", 0.f) * degrees;
data.hp = item.value("hp", 100.f);
data.visibleRange =
item.value("visibleRange", VISIBLE_RANGE);
data.canAttack = item.value("canAttack", false);
data.enabled = item.value("enabled", true);
result.npcs.push_back(std::move(data));
}
}
if (!config.teleportsJsonPath.empty()) {
const json root = loadJson(config.teleportsJsonPath, zipFile);
std::unordered_set<std::string> ids;
for (const auto& item : root.value("teleports", json::array())) {
TeleportDefinition data;
data.id = item.at("id").get<std::string>();
if (data.id.empty() || !ids.insert(data.id).second)
throw std::runtime_error(
"Missing or duplicate teleport id in " +
config.teleportsJsonPath);
data.active = item.value("active", false);
data.position =
vector(item, "positionX", "positionY", "positionZ");
data.radius = item.value("radius", 0.f);
data.automatic = item.value("automatic", false);
data.automaticRadius =
item.value("automaticRadius", data.radius);
data.destinationLocation =
item.value("destinationLocation", "");
data.destinationPosition = vector(
item, "destinationPositionX", "destinationPositionY",
"destinationPositionZ");
data.destinationRotationY =
item.value("destinationRotationY", 0.f);
result.teleports.push_back(std::move(data));
}
}
if (!config.triggerZonesJsonPath.empty()) {
const json root = loadJson(config.triggerZonesJsonPath, zipFile);
std::unordered_set<std::string> ids;
for (const auto& item :
root.value("trigger_zones", json::array())) {
TriggerZoneDefinition data;
data.id = item.at("id").get<std::string>();
if (data.id.empty() || !ids.insert(data.id).second)
throw std::runtime_error(
"Missing or duplicate trigger-zone id in " +
config.triggerZonesJsonPath);
data.enabled = item.value("enabled", true);
data.radius = item.value("radius", 1.5f);
data.hysteresis = item.value("hysteresis", 0.3f);
data.position =
vector(item, "positionX", "positionY", "positionZ");
result.triggerZones.push_back(std::move(data));
}
}
if (!config.lightsJsonPath.empty()) {
const json root = loadJson(config.lightsJsonPath, zipFile);
for (const auto& item : root.value("lights", json::array())) {
PointLightDefinition data;
data.id = item.value("id", "");
data.position =
vector(item, "positionX", "positionY", "positionZ");
data.direction = vector(
item, "directionX", "directionY", "directionZ",
Eigen::Vector3f(0.f, -1.f, 0.f));
data.color = vector(
item, "colorR", "colorG", "colorB",
Eigen::Vector3f::Ones());
data.autoLight = item.value("autoLight", false);
data.autoLightDistance =
item.value("autoLightDistance", -1.f);
data.limitX = item.value("limitX", 99999.f);
data.limitY = item.value("limitY", 99999.f);
data.autoLightLimitX =
item.value("autoLightLimitX", -1.f);
data.autoLightLimitY =
item.value("autoLightLimitY", -1.f);
result.pointLights.push_back(std::move(data));
}
}
return result;
}
} // namespace FRG
+132
View File
@@ -0,0 +1,132 @@
#pragma once
#include "../Campaign.h"
#include "../CharacterState.h"
#include "../items/InteractiveObjectState.h"
#include <Eigen/Core>
#include <map>
#include <string>
#include <vector>
namespace FRG {
struct GameObjectData {
std::string name;
std::string texturePath;
std::string textureDarkandsPath;
std::string meshPath;
float rotationX = 0.f;
float rotationY = 0.f;
float rotationZ = 0.f;
float positionX = 0.f;
float positionY = 0.f;
float positionZ = 0.f;
float scale = 1.f;
bool instanced = true;
LoadedGameObjectState toLoadedGameObjectState() const;
};
struct InteractiveObjectData {
std::string id;
GameObjectData base;
float interactionRadius = 2.f;
float approachRadius = 2.f;
std::string activateFunctionName;
Eigen::Vector3f pivot = Eigen::Vector3f::Zero();
Eigen::Vector3f boundsMin = Eigen::Vector3f::Zero();
Eigen::Vector3f boundsMax = Eigen::Vector3f::Zero();
bool hasInteractionPosition = false;
Eigen::Vector3f interactionPosition = Eigen::Vector3f::Zero();
bool castShadow = true;
bool castShadowNight = true;
bool isActive = true;
InteractiveObjectState toInteractiveObjectState() const;
};
struct NpcData {
std::string id;
std::string name;
std::string texturePath;
std::map<std::string, std::string> meshTextures;
std::string animationIdlePath;
std::string animationWalkPath;
std::string animationActionIdlePath;
std::string animationActionAttackPath;
std::string animationStandToActionPath;
std::string animationActionToStandPath;
std::string animationActionToDeathPath;
std::string animationDeathIdlePath;
float positionX = 0.f;
float positionY = 0.f;
float positionZ = 0.f;
float walkSpeed = 1.5f;
float rotationSpeed = 8.f;
float modelScale = 0.01f;
float modelCorrectionRotX = 0.f;
float modelCorrectionRotY = 0.f;
float modelCorrectionRotZ = 0.f;
float facingAngle = 0.f;
float hp = 100.f;
float visibleRange = VISIBLE_RANGE;
bool canAttack = false;
bool enabled = true;
float interactionRadius = 0.f;
CharacterState toCharacterState() const;
};
struct TeleportDefinition {
std::string id;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
float radius = 0.f;
bool active = false;
std::string destinationLocation;
Eigen::Vector3f destinationPosition = Eigen::Vector3f::Zero();
float destinationRotationY = 0.f;
bool automatic = false;
float automaticRadius = 0.f;
};
struct TriggerZoneDefinition {
std::string id;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
float radius = 1.5f;
float hysteresis = 0.3f;
bool enabled = true;
};
struct PointLightDefinition {
std::string id;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
Eigen::Vector3f direction = Eigen::Vector3f(0.f, -1.f, 0.f);
Eigen::Vector3f color = Eigen::Vector3f::Ones();
bool autoLight = false;
float autoLightDistance = -1.f;
float limitX = 99999.f;
float limitY = 99999.f;
float autoLightLimitX = -1.f;
float autoLightLimitY = -1.f;
};
struct LocationDefinition {
LocationConfig config;
std::vector<GameObjectData> staticObjects;
std::vector<InteractiveObjectData> interactiveObjects;
std::vector<NpcData> npcs;
std::vector<TeleportDefinition> teleports;
std::vector<TriggerZoneDefinition> triggerZones;
std::vector<PointLightDefinition> pointLights;
};
class LocationContentLoader {
public:
static LocationDefinition load(
const LocationConfig& config, const std::string& zipFile = {});
};
void loadCharacterAnimationMetadata(
CharacterState& state, const std::string& zipFile = {});
} // namespace FRG