Files
SDL3CPlusPlus/src/services/impl/mesh_service.cpp

906 lines
32 KiB
C++

#include "mesh_service.hpp"
#include <utility>
#include <assimp/DefaultIOSystem.h>
#include <assimp/Importer.hpp>
#include <assimp/material.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <lua.hpp>
#include <system_error>
#include <zip.h>
namespace sdl3cpp::services::impl {
namespace {
constexpr unsigned int kAssimpLoadFlags =
aiProcess_Triangulate |
aiProcess_JoinIdenticalVertices |
aiProcess_PreTransformVertices |
aiProcess_GenNormals;
struct ZipArchiveDeleter {
void operator()(zip_t* archive) const {
if (archive) {
zip_close(archive);
}
}
};
struct ZipFileDeleter {
void operator()(zip_file_t* file) const {
if (file) {
zip_fclose(file);
}
}
};
std::string BuildZipErrorMessage(int errorCode) {
zip_error_t zipError;
zip_error_init_with_code(&zipError, errorCode);
std::string message = zip_error_strerror(&zipError);
zip_error_fini(&zipError);
return message;
}
std::string BuildZipArchiveErrorMessage(zip_t* archive) {
if (!archive) {
return "unknown zip archive error";
}
zip_error_t* zipError = zip_get_error(archive);
if (!zipError) {
return "unknown zip archive error";
}
return zip_error_strerror(zipError);
}
std::string GetExtensionHint(const std::string& entryPath, const std::string& fallback) {
std::filesystem::path entry(entryPath);
std::string ext = entry.extension().string();
if (!ext.empty() && ext.front() == '.') {
ext.erase(ext.begin());
}
if (!ext.empty()) {
return ext;
}
return fallback;
}
std::string NormalizeExtension(std::string extension) {
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
return extension;
}
class ArchiveMapAwareIOSystem : public Assimp::DefaultIOSystem {
public:
bool Exists(const char* pFile) const override {
const std::string trimmed = TrimArchiveSuffix(pFile);
return Assimp::DefaultIOSystem::Exists(trimmed.c_str());
}
Assimp::IOStream* Open(const char* pFile, const char* pMode = "rb") override {
const std::string trimmed = TrimArchiveSuffix(pFile);
return Assimp::DefaultIOSystem::Open(trimmed.c_str(), pMode);
}
private:
static std::string TrimArchiveSuffix(const char* path) {
if (!path) {
return std::string();
}
std::string trimmed(path);
const std::string::size_type comma = trimmed.find(',');
if (comma != std::string::npos) {
trimmed.erase(comma);
}
return trimmed;
}
};
bool ReadArchiveEntry(zip_t* archive,
const std::string& entryPath,
const zip_stat_t& entryStat,
std::vector<uint8_t>& buffer,
std::string& outError) {
if (!archive) {
outError = "Archive handle is null";
return false;
}
if (entryStat.size == 0) {
outError = "Archive entry is empty: " + entryPath;
return false;
}
if (entryStat.size > std::numeric_limits<size_t>::max()) {
outError = "Archive entry exceeds addressable size: " + entryPath;
return false;
}
std::unique_ptr<zip_file_t, ZipFileDeleter> file(
zip_fopen(archive, entryPath.c_str(), ZIP_FL_ENC_GUESS));
if (!file) {
outError = "Failed to open archive entry: " + BuildZipArchiveErrorMessage(archive);
return false;
}
size_t entrySize = static_cast<size_t>(entryStat.size);
buffer.assign(entrySize, 0);
zip_int64_t totalRead = 0;
while (static_cast<size_t>(totalRead) < entrySize) {
zip_int64_t bytesRead = zip_fread(file.get(),
buffer.data() + totalRead,
entrySize - static_cast<size_t>(totalRead));
if (bytesRead < 0) {
outError = "Failed to read archive entry: " + BuildZipArchiveErrorMessage(archive);
return false;
}
if (bytesRead == 0) {
break;
}
totalRead += bytesRead;
}
if (static_cast<size_t>(totalRead) != entrySize) {
outError = "Archive entry read incomplete: " + entryPath;
return false;
}
return true;
}
#pragma pack(push, 1)
struct BspHeader {
char id[4];
int32_t version;
};
struct BspLump {
int32_t offset;
int32_t length;
};
struct BspVertex {
float position[3];
float texCoord[2];
float lightmapCoord[2];
float normal[3];
uint8_t color[4];
};
struct BspFace {
int32_t textureId;
int32_t effect;
int32_t type;
int32_t vertexIndex;
int32_t numVertices;
int32_t meshVertIndex;
int32_t numMeshVerts;
int32_t lightmapId;
int32_t lightmapCorner[2];
int32_t lightmapSize[2];
float lightmapPos[3];
float lightmapVecs[2][3];
float normal[3];
int32_t patchSize[2];
};
#pragma pack(pop)
static_assert(sizeof(BspHeader) == 8, "Unexpected BSP header size");
static_assert(sizeof(BspLump) == 8, "Unexpected BSP lump size");
static_assert(sizeof(BspVertex) == 44, "Unexpected BSP vertex size");
static_assert(sizeof(BspFace) == 104, "Unexpected BSP face size");
bool BuildPayloadFromBspBuffer(const std::vector<uint8_t>& buffer,
MeshPayload& outPayload,
std::string& outError,
const std::shared_ptr<ILogger>& logger) {
constexpr int32_t kBspExpectedVersion = 46;
constexpr size_t kBspLumpCount = 17;
constexpr size_t kBspVerticesLump = 10;
constexpr size_t kBspMeshVertsLump = 11;
constexpr size_t kBspFacesLump = 13;
constexpr int32_t kBspFacePolygon = 1;
constexpr int32_t kBspFaceMesh = 3;
if (buffer.size() < sizeof(BspHeader) + kBspLumpCount * sizeof(BspLump)) {
outError = "BSP buffer is too small to contain header and lumps";
return false;
}
BspHeader header{};
std::memcpy(&header, buffer.data(), sizeof(BspHeader));
if (std::string(header.id, sizeof(header.id)) != "IBSP") {
outError = "BSP header mismatch: expected IBSP";
return false;
}
if (header.version != kBspExpectedVersion) {
outError = "Unsupported BSP version: " + std::to_string(header.version);
return false;
}
std::array<BspLump, kBspLumpCount> lumps{};
std::memcpy(lumps.data(), buffer.data() + sizeof(BspHeader),
kBspLumpCount * sizeof(BspLump));
auto validateLump = [&](size_t index,
size_t elementSize,
size_t& outCount,
const std::string& label) -> bool {
const BspLump& lump = lumps[index];
if (lump.offset < 0 || lump.length < 0) {
outError = "BSP " + label + " lump has negative bounds";
return false;
}
size_t offset = static_cast<size_t>(lump.offset);
size_t length = static_cast<size_t>(lump.length);
if (offset + length > buffer.size()) {
outError = "BSP " + label + " lump exceeds buffer size";
return false;
}
if (length % elementSize != 0) {
outError = "BSP " + label + " lump size is not aligned";
return false;
}
outCount = length / elementSize;
return true;
};
size_t vertexCount = 0;
size_t meshVertCount = 0;
size_t faceCount = 0;
if (!validateLump(kBspVerticesLump, sizeof(BspVertex), vertexCount, "vertex")) {
return false;
}
if (!validateLump(kBspMeshVertsLump, sizeof(int32_t), meshVertCount, "meshvert")) {
return false;
}
if (!validateLump(kBspFacesLump, sizeof(BspFace), faceCount, "face")) {
return false;
}
if (vertexCount == 0) {
outError = "BSP contains no vertices";
return false;
}
if (meshVertCount == 0 || faceCount == 0) {
outError = "BSP contains no face indices";
return false;
}
const uint8_t* vertexData = buffer.data() + lumps[kBspVerticesLump].offset;
const uint8_t* meshVertData = buffer.data() + lumps[kBspMeshVertsLump].offset;
const uint8_t* faceData = buffer.data() + lumps[kBspFacesLump].offset;
std::vector<BspVertex> vertices(vertexCount);
std::memcpy(vertices.data(), vertexData, vertexCount * sizeof(BspVertex));
std::vector<int32_t> meshVerts(meshVertCount);
std::memcpy(meshVerts.data(), meshVertData, meshVertCount * sizeof(int32_t));
outPayload.positions.resize(vertexCount);
outPayload.normals.resize(vertexCount);
outPayload.colors.resize(vertexCount);
outPayload.texcoords.resize(vertexCount);
outPayload.indices.clear();
for (size_t i = 0; i < vertexCount; ++i) {
const BspVertex& vertex = vertices[i];
outPayload.positions[i] = {vertex.position[0], vertex.position[1], vertex.position[2]};
outPayload.normals[i] = {vertex.normal[0], vertex.normal[1], vertex.normal[2]};
outPayload.colors[i] = {
static_cast<float>(vertex.color[0]) / 255.0f,
static_cast<float>(vertex.color[1]) / 255.0f,
static_cast<float>(vertex.color[2]) / 255.0f
};
outPayload.texcoords[i] = {vertex.texCoord[0], vertex.texCoord[1]};
}
size_t trianglesBuilt = 0;
size_t trianglesSkipped = 0;
for (size_t faceIndex = 0; faceIndex < faceCount; ++faceIndex) {
BspFace face{};
std::memcpy(&face, faceData + faceIndex * sizeof(BspFace), sizeof(BspFace));
if (face.type != kBspFacePolygon && face.type != kBspFaceMesh) {
continue;
}
if (face.numMeshVerts < 3) {
continue;
}
for (int32_t i = 0; i + 2 < face.numMeshVerts; i += 3) {
int32_t meshIndex = face.meshVertIndex + i;
if (meshIndex < 0 || static_cast<size_t>(meshIndex + 2) >= meshVerts.size()) {
++trianglesSkipped;
continue;
}
int32_t index0 = face.vertexIndex + meshVerts[static_cast<size_t>(meshIndex)];
int32_t index1 = face.vertexIndex + meshVerts[static_cast<size_t>(meshIndex + 1)];
int32_t index2 = face.vertexIndex + meshVerts[static_cast<size_t>(meshIndex + 2)];
if (index0 < 0 || index1 < 0 || index2 < 0) {
++trianglesSkipped;
continue;
}
if (static_cast<size_t>(index0) >= vertexCount ||
static_cast<size_t>(index1) >= vertexCount ||
static_cast<size_t>(index2) >= vertexCount) {
++trianglesSkipped;
continue;
}
outPayload.indices.push_back(static_cast<uint32_t>(index0));
outPayload.indices.push_back(static_cast<uint32_t>(index1));
outPayload.indices.push_back(static_cast<uint32_t>(index2));
++trianglesBuilt;
}
}
if (logger) {
logger->Trace("MeshService", "BuildPayloadFromBspBuffer",
"vertexCount=" + std::to_string(vertexCount) +
", meshVertCount=" + std::to_string(meshVertCount) +
", faceCount=" + std::to_string(faceCount) +
", trianglesBuilt=" + std::to_string(trianglesBuilt) +
", texcoordCount=" + std::to_string(outPayload.texcoords.size()) +
", trianglesSkipped=" + std::to_string(trianglesSkipped));
}
if (outPayload.indices.empty()) {
outError = "BSP contains no triangle faces";
return false;
}
if (outPayload.positions.size() > std::numeric_limits<uint16_t>::max()) {
outError = "Mesh vertex count exceeds uint16_t index range: " +
std::to_string(outPayload.positions.size());
return false;
}
return true;
}
bool WriteArchiveEntryToTempFile(const std::vector<uint8_t>& buffer,
const std::string& entryPath,
const std::string& extensionHint,
std::filesystem::path& outPath,
std::string& outError) {
std::error_code tempError;
std::filesystem::path tempDirectory = std::filesystem::temp_directory_path(tempError);
if (tempError) {
outError = "Failed to resolve temp directory: " + tempError.message();
return false;
}
std::filesystem::path entryName(entryPath);
std::string baseName = entryName.stem().string();
if (baseName.empty()) {
baseName = "archive_entry";
}
auto timestampMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
std::string fileName = "sdl3cpp_" + baseName + "_" + std::to_string(timestampMs);
if (!extensionHint.empty()) {
fileName += "." + extensionHint;
}
outPath = tempDirectory / fileName;
std::ofstream output(outPath, std::ios::binary | std::ios::trunc);
if (!output) {
outError = "Failed to create temp file: " + outPath.string();
return false;
}
output.write(reinterpret_cast<const char*>(buffer.data()),
static_cast<std::streamsize>(buffer.size()));
if (!output) {
outError = "Failed to write temp file: " + outPath.string();
return false;
}
return true;
}
aiColor3D ResolveMaterialColor(const aiScene* scene, const aiMesh* mesh) {
aiColor3D defaultColor(0.6f, 0.8f, 1.0f);
if (!scene || !mesh) {
return defaultColor;
}
if (mesh->mMaterialIndex < scene->mNumMaterials) {
const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
aiColor4D diffuse;
if (material && material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS) {
return aiColor3D(diffuse.r, diffuse.g, diffuse.b);
}
}
return defaultColor;
}
struct MeshBounds {
aiVector3D min;
aiVector3D max;
};
MeshBounds ComputeMeshBounds(const aiMesh* mesh) {
MeshBounds bounds{};
bounds.min = aiVector3D(std::numeric_limits<float>::max());
bounds.max = aiVector3D(std::numeric_limits<float>::lowest());
if (!mesh || !mesh->mNumVertices) {
return bounds;
}
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
const aiVector3D& v = mesh->mVertices[i];
bounds.min.x = std::min(bounds.min.x, v.x);
bounds.min.y = std::min(bounds.min.y, v.y);
bounds.min.z = std::min(bounds.min.z, v.z);
bounds.max.x = std::max(bounds.max.x, v.x);
bounds.max.y = std::max(bounds.max.y, v.y);
bounds.max.z = std::max(bounds.max.z, v.z);
}
return bounds;
}
float NormalizeCoord(float value, float minValue, float maxValue) {
float range = maxValue - minValue;
if (range == 0.0f) {
return 0.0f;
}
return (value - minValue) / range;
}
std::array<float, 2> ComputeFallbackTexcoord(const aiVector3D& position,
const aiVector3D& normal,
const MeshBounds& bounds) {
float ax = std::fabs(normal.x);
float ay = std::fabs(normal.y);
float az = std::fabs(normal.z);
if (ax >= ay && ax >= az) {
return {NormalizeCoord(position.z, bounds.min.z, bounds.max.z),
NormalizeCoord(position.y, bounds.min.y, bounds.max.y)};
}
if (ay >= ax && ay >= az) {
return {NormalizeCoord(position.x, bounds.min.x, bounds.max.x),
NormalizeCoord(position.z, bounds.min.z, bounds.max.z)};
}
return {NormalizeCoord(position.x, bounds.min.x, bounds.max.x),
NormalizeCoord(position.y, bounds.min.y, bounds.max.y)};
}
bool AppendMeshPayload(const aiScene* scene,
const aiMesh* mesh,
MeshPayload& outPayload,
std::string& outError,
size_t& outIndicesAdded) {
outIndicesAdded = 0;
if (!mesh || !mesh->mNumVertices) {
outError = "Mesh contains no vertices";
return false;
}
size_t vertexOffset = outPayload.positions.size();
if (vertexOffset > std::numeric_limits<uint32_t>::max()) {
outError = "Mesh vertex count exceeds uint32_t index range";
return false;
}
aiColor3D materialColor = ResolveMaterialColor(scene, mesh);
const MeshBounds bounds = ComputeMeshBounds(mesh);
const bool hasTexcoords = mesh->HasTextureCoords(0) && mesh->mTextureCoords[0];
size_t positionsStart = outPayload.positions.size();
size_t normalsStart = outPayload.normals.size();
size_t colorsStart = outPayload.colors.size();
size_t texcoordsStart = outPayload.texcoords.size();
size_t indicesStart = outPayload.indices.size();
outPayload.positions.reserve(positionsStart + mesh->mNumVertices);
outPayload.normals.reserve(normalsStart + mesh->mNumVertices);
outPayload.colors.reserve(colorsStart + mesh->mNumVertices);
outPayload.texcoords.reserve(texcoordsStart + mesh->mNumVertices);
outPayload.indices.reserve(indicesStart + mesh->mNumFaces * 3);
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
const aiVector3D& vertex = mesh->mVertices[i];
outPayload.positions.push_back({vertex.x, vertex.y, vertex.z});
aiVector3D normal(0.0f, 0.0f, 1.0f);
if (mesh->HasNormals()) {
normal = mesh->mNormals[i];
}
outPayload.normals.push_back({normal.x, normal.y, normal.z});
aiColor3D color = materialColor;
if (mesh->HasVertexColors(0) && mesh->mColors[0]) {
const aiColor4D& vertexColor = mesh->mColors[0][i];
color = aiColor3D(vertexColor.r, vertexColor.g, vertexColor.b);
}
outPayload.colors.push_back({color.r, color.g, color.b});
if (hasTexcoords) {
const aiVector3D& uv = mesh->mTextureCoords[0][i];
outPayload.texcoords.push_back({uv.x, uv.y});
} else {
outPayload.texcoords.push_back(ComputeFallbackTexcoord(vertex, normal, bounds));
}
}
for (unsigned faceIndex = 0; faceIndex < mesh->mNumFaces; ++faceIndex) {
const aiFace& face = mesh->mFaces[faceIndex];
if (face.mNumIndices != 3) {
continue;
}
outPayload.indices.push_back(static_cast<uint32_t>(face.mIndices[0]) +
static_cast<uint32_t>(vertexOffset));
outPayload.indices.push_back(static_cast<uint32_t>(face.mIndices[1]) +
static_cast<uint32_t>(vertexOffset));
outPayload.indices.push_back(static_cast<uint32_t>(face.mIndices[2]) +
static_cast<uint32_t>(vertexOffset));
}
outIndicesAdded = outPayload.indices.size() - indicesStart;
if (outIndicesAdded == 0) {
outPayload.positions.resize(positionsStart);
outPayload.normals.resize(normalsStart);
outPayload.colors.resize(colorsStart);
outPayload.indices.resize(indicesStart);
outError = "Mesh contains no triangle faces";
return false;
}
return true;
}
bool BuildPayloadFromScene(const aiScene* scene,
bool combineMeshes,
MeshPayload& outPayload,
std::string& outError,
const std::shared_ptr<ILogger>& logger) {
if (!scene) {
outError = "Assimp scene is null";
return false;
}
if (scene->mNumMeshes == 0) {
outError = "Scene contains no meshes";
return false;
}
outPayload.positions.clear();
outPayload.normals.clear();
outPayload.colors.clear();
outPayload.indices.clear();
if (!combineMeshes) {
size_t indicesAdded = 0;
if (!AppendMeshPayload(scene, scene->mMeshes[0], outPayload, outError, indicesAdded)) {
return false;
}
return true;
}
size_t totalIndicesAdded = 0;
for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) {
const aiMesh* mesh = scene->mMeshes[meshIndex];
std::string meshError;
size_t indicesAdded = 0;
if (!AppendMeshPayload(scene, mesh, outPayload, meshError, indicesAdded)) {
if (logger) {
logger->Trace("MeshService", "BuildPayloadFromScene",
"Skipping mesh " + std::to_string(meshIndex) + ": " + meshError);
}
continue;
}
totalIndicesAdded += indicesAdded;
}
if (totalIndicesAdded == 0) {
outError = "Scene contains no triangle faces";
return false;
}
return true;
}
} // namespace
MeshService::MeshService(std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger)
: configService_(std::move(configService)),
logger_(std::move(logger)) {
if (logger_) {
logger_->Trace("MeshService", "MeshService",
"configService=" + std::string(configService_ ? "set" : "null"));
}
}
bool MeshService::LoadFromFile(const std::string& requestedPath,
MeshPayload& outPayload,
std::string& outError) {
if (logger_) {
logger_->Trace("MeshService", "LoadFromFile",
"requestedPath=" + requestedPath);
}
std::filesystem::path resolved;
if (!ResolvePath(requestedPath, resolved, outError)) {
return false;
}
if (!std::filesystem::exists(resolved)) {
outError = "Mesh file not found: " + resolved.string();
return false;
}
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(resolved.string(), kAssimpLoadFlags);
if (!scene) {
outError = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load mesh";
return false;
}
return BuildPayloadFromScene(scene, false, outPayload, outError, logger_);
}
bool MeshService::LoadFromArchive(const std::string& archivePath,
const std::string& entryPath,
MeshPayload& outPayload,
std::string& outError) {
if (logger_) {
logger_->Trace("MeshService", "LoadFromArchive",
"archivePath=" + archivePath +
", entryPath=" + entryPath);
}
std::filesystem::path resolvedArchive;
if (!ResolvePath(archivePath, resolvedArchive, outError)) {
return false;
}
if (!std::filesystem::exists(resolvedArchive)) {
outError = "Archive file not found: " + resolvedArchive.string();
return false;
}
if (entryPath.empty()) {
outError = "Archive entry path is empty";
return false;
}
std::string extensionHint = GetExtensionHint(entryPath, "bsp");
std::string normalizedExtension = NormalizeExtension(extensionHint);
auto buildPayload = [&](const aiScene* loadedScene, std::string& error) -> bool {
if (!BuildPayloadFromScene(loadedScene, true, outPayload, error, logger_)) {
return false;
}
if (outPayload.positions.size() > std::numeric_limits<uint16_t>::max()) {
error = "Mesh vertex count exceeds uint16_t index range: " +
std::to_string(outPayload.positions.size());
return false;
}
return true;
};
int errorCode = 0;
std::unique_ptr<zip_t, ZipArchiveDeleter> archive(
zip_open(resolvedArchive.string().c_str(), ZIP_RDONLY, &errorCode));
if (!archive) {
outError = "Failed to open archive: " + BuildZipErrorMessage(errorCode);
return false;
}
zip_stat_t entryStat;
if (zip_stat(archive.get(), entryPath.c_str(), ZIP_FL_ENC_GUESS, &entryStat) != 0) {
outError = "Archive entry not found: " + entryPath;
return false;
}
if (normalizedExtension == "bsp") {
if (entryStat.size == 0) {
outError = "Archive entry is empty: " + entryPath;
return false;
}
if (logger_) {
logger_->Trace("MeshService", "LoadFromArchive",
"Detected BSP entry; using archive importer. archive=" +
resolvedArchive.string() + ", entry=" + entryPath);
logger_->Trace("MeshService", "LoadFromArchive",
"BSP entry size=" + std::to_string(entryStat.size));
}
std::string importName = resolvedArchive.string() + "," + entryPath;
Assimp::Importer importer;
importer.SetIOHandler(new ArchiveMapAwareIOSystem());
const aiScene* scene = importer.ReadFile(importName, kAssimpLoadFlags);
if (scene && logger_) {
unsigned int rootMeshes = 0;
unsigned int rootChildren = 0;
if (scene->mRootNode) {
rootMeshes = scene->mRootNode->mNumMeshes;
rootChildren = scene->mRootNode->mNumChildren;
}
logger_->Trace("MeshService", "LoadFromArchive",
"BSP scene stats: meshes=" + std::to_string(scene->mNumMeshes) +
", materials=" + std::to_string(scene->mNumMaterials) +
", textures=" + std::to_string(scene->mNumTextures) +
", rootMeshes=" + std::to_string(rootMeshes) +
", rootChildren=" + std::to_string(rootChildren));
}
std::string assimpError;
if (!scene) {
assimpError = importer.GetErrorString() ? importer.GetErrorString()
: "Assimp failed to load BSP from archive";
} else if (buildPayload(scene, assimpError)) {
return true;
}
if (logger_) {
logger_->Trace("MeshService", "LoadFromArchive",
"Assimp BSP import did not yield meshes, falling back to BSP parser: " +
assimpError);
}
std::vector<uint8_t> buffer;
std::string readError;
if (!ReadArchiveEntry(archive.get(), entryPath, entryStat, buffer, readError)) {
outError = "Failed to read BSP entry for fallback parser: " + readError;
return false;
}
if (!BuildPayloadFromBspBuffer(buffer, outPayload, outError, logger_)) {
return false;
}
return true;
}
std::vector<uint8_t> buffer;
if (!ReadArchiveEntry(archive.get(), entryPath, entryStat, buffer, outError)) {
return false;
}
Assimp::Importer importer;
const aiScene* scene = importer.ReadFileFromMemory(
buffer.data(),
buffer.size(),
kAssimpLoadFlags,
extensionHint.c_str());
if (!scene) {
std::string memoryError = importer.GetErrorString() ? importer.GetErrorString()
: "Assimp failed to load archive entry";
if (logger_) {
logger_->Trace("MeshService", "LoadFromArchive",
"ReadFileFromMemory failed: " + memoryError +
", entryPath=" + entryPath +
", extensionHint=" + extensionHint);
}
std::filesystem::path tempPath;
std::string tempError;
if (!WriteArchiveEntryToTempFile(buffer, entryPath, extensionHint, tempPath, tempError)) {
outError = "Assimp failed to load archive entry from memory: " + memoryError +
"; " + tempError;
return false;
}
if (logger_) {
logger_->Trace("MeshService", "LoadFromArchive",
"Falling back to temp file: " + tempPath.string());
}
Assimp::Importer fallbackImporter;
const aiScene* fallbackScene = fallbackImporter.ReadFile(tempPath.string(), kAssimpLoadFlags);
std::error_code removeError;
std::filesystem::remove(tempPath, removeError);
if (removeError && logger_) {
logger_->Trace("MeshService", "LoadFromArchive",
"Failed to remove temp file: " + tempPath.string() +
", error=" + removeError.message());
}
if (!fallbackScene) {
std::string fallbackError = fallbackImporter.GetErrorString() ? fallbackImporter.GetErrorString()
: "Assimp failed to load temp file";
outError = "Assimp failed to load archive entry from memory: " + memoryError +
"; fallback to temp file failed: " + fallbackError;
return false;
}
if (!buildPayload(fallbackScene, outError)) {
return false;
}
return true;
}
if (!buildPayload(scene, outError)) {
return false;
}
return true;
}
bool MeshService::ResolvePath(const std::string& requestedPath,
std::filesystem::path& resolvedPath,
std::string& outError) const {
if (!configService_) {
outError = "Config service not available";
return false;
}
std::filesystem::path resolved(requestedPath);
if (!resolved.is_absolute()) {
resolved = configService_->GetScriptPath().parent_path() / resolved;
}
std::error_code ec;
resolved = std::filesystem::weakly_canonical(resolved, ec);
if (ec) {
outError = "Failed to resolve path: " + ec.message();
return false;
}
resolvedPath = std::move(resolved);
return true;
}
void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) {
if (logger_) {
logger_->Trace("MeshService", "PushMeshToLua",
"positions.size=" + std::to_string(payload.positions.size()) +
", normals.size=" + std::to_string(payload.normals.size()) +
", colors.size=" + std::to_string(payload.colors.size()) +
", texcoords.size=" + std::to_string(payload.texcoords.size()) +
", indices.size=" + std::to_string(payload.indices.size()) +
", luaStateIsNull=" + std::string(L ? "false" : "true"));
}
lua_newtable(L);
lua_newtable(L);
for (size_t vertexIndex = 0; vertexIndex < payload.positions.size(); ++vertexIndex) {
lua_newtable(L);
lua_newtable(L);
for (int component = 0; component < 3; ++component) {
lua_pushnumber(L, payload.positions[vertexIndex][component]);
lua_rawseti(L, -2, component + 1);
}
lua_setfield(L, -2, "position");
lua_newtable(L);
std::array<float, 3> normal = {0.0f, 0.0f, 1.0f};
if (vertexIndex < payload.normals.size()) {
normal = payload.normals[vertexIndex];
}
for (int component = 0; component < 3; ++component) {
lua_pushnumber(L, normal[component]);
lua_rawseti(L, -2, component + 1);
}
lua_setfield(L, -2, "normal");
lua_newtable(L);
for (int component = 0; component < 3; ++component) {
lua_pushnumber(L, payload.colors[vertexIndex][component]);
lua_rawseti(L, -2, component + 1);
}
lua_setfield(L, -2, "color");
lua_newtable(L);
std::array<float, 2> texcoord = {0.0f, 0.0f};
if (vertexIndex < payload.texcoords.size()) {
texcoord = payload.texcoords[vertexIndex];
}
for (int component = 0; component < 2; ++component) {
lua_pushnumber(L, texcoord[component]);
lua_rawseti(L, -2, component + 1);
}
lua_setfield(L, -2, "texcoord");
lua_rawseti(L, -2, static_cast<int>(vertexIndex + 1));
}
lua_setfield(L, -2, "vertices");
lua_newtable(L);
for (size_t index = 0; index < payload.indices.size(); ++index) {
lua_pushinteger(L, static_cast<lua_Integer>(payload.indices[index]) + 1);
lua_rawseti(L, -2, static_cast<int>(index + 1));
}
lua_setfield(L, -2, "indices");
}
} // namespace sdl3cpp::services::impl