#include "LocationEditor.h" #include "Location.h" #include "Character.h" #include "utils/Utils.h" #include "render/OpenGlExtensions.h" #include "TextModel.h" #include "external/nlohmann/json.hpp" #include #include #include #include #include #include namespace ZL { extern const char* CONST_ZIP_FILE; LocationEditor::LocationEditor(Location& location) : loc(location) { } void LocationEditor::buildNavMeshes() { navigationEditorNavMeshes.clear(); if (!loc.navigation) return; const float y = loc.navigation->getFloorY() + 0.02f; const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); for (const auto& obs : loc.navigation->getObstaclePolygons()) { if (obs.polygon.size() < 3) continue; VertexRenderStruct mesh; mesh.data = CreatePolygonFloor(obs.polygon, y, red); mesh.RefreshVBO(); navigationEditorNavMeshes.push_back(std::move(mesh)); } } void LocationEditor::drawNavigation() { loc.renderer.shaderManager.PushShader("defaultColor"); loc.renderer.SetMatrix(); for (const auto& mesh : navigationEditorNavMeshes) { loc.renderer.DrawVertexRenderStruct(mesh); } loc.renderer.shaderManager.PopShader(); loc.renderer.SetMatrix(); } void LocationEditor::rebuildPointsMesh() { VertexDataStruct data; const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f); const float y = loc.navigation ? loc.navigation->getFloorY() + 0.05f : 0.05f; const float s = 0.2f; for (const auto& pt : navigationEditorPoints) { Eigen::Vector3f v0(pt.x(), y, pt.z() - s * 1.15f); Eigen::Vector3f v1(pt.x() - s, y, pt.z() + s * 0.58f); Eigen::Vector3f v2(pt.x() + s, y, pt.z() + s * 0.58f); data.PositionData.push_back(v0); data.PositionData.push_back(v1); data.PositionData.push_back(v2); data.ColorData.push_back(yellow); data.ColorData.push_back(yellow); data.ColorData.push_back(yellow); } navigationEditorPointsMesh.data = std::move(data); navigationEditorPointsMesh.RefreshVBO(); } void LocationEditor::drawPoints() { if (navigationEditorPoints.empty()) return; loc.renderer.shaderManager.PushShader("defaultColor"); loc.renderer.SetMatrix(); loc.renderer.DrawVertexRenderStruct(navigationEditorPointsMesh); loc.renderer.shaderManager.PopShader(); loc.renderer.SetMatrix(); } void LocationEditor::handleLeftClick(const Eigen::Vector3f& hit, bool ctrlHeld) { if (ctrlHeld) { const float removeRadius = 1.0f; for (auto it = navigationEditorPoints.begin(); it != navigationEditorPoints.end(); ++it) { const float dx = it->x() - hit.x(); const float dz = it->z() - hit.z(); if (dx * dx + dz * dz <= removeRadius * removeRadius) { navigationEditorPoints.erase(it); rebuildPointsMesh(); std::cout << "[NAV_EDITOR] Removed point, " << navigationEditorPoints.size() << " remaining\n"; return; } } std::cout << "[NAV_EDITOR] No point found within " << removeRadius << " units of click\n"; } else { navigationEditorPoints.push_back(hit); rebuildPointsMesh(); std::cout << "[NAV_EDITOR] Added point (" << hit.x() << ", " << hit.z() << "), total: " << navigationEditorPoints.size() << "\n"; } } void LocationEditor::handleRightClick() { if (navigationEditorPoints.size() < 3) { std::cout << "[NAV_EDITOR] Need at least 3 points to form a polygon (have " << navigationEditorPoints.size() << "); clearing\n"; navigationEditorPoints.clear(); rebuildPointsMesh(); return; } PathFinder::ObstaclePolygon poly; poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter); for (const auto& pt : navigationEditorPoints) { poly.polygon.emplace_back(pt.x(), pt.z()); } if (loc.navigation) loc.navigation->addObstaclePolygon(poly); std::cout << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with " << poly.polygon.size() << " vertices\n"; { const float y = loc.navigation ? loc.navigation->getFloorY() + 0.02f : 0.02f; const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); VertexRenderStruct mesh; mesh.data = CreatePolygonFloor(poly.polygon, y, red); mesh.RefreshVBO(); navigationEditorNavMeshes.push_back(std::move(mesh)); } navigationEditorPoints.clear(); rebuildPointsMesh(); } void LocationEditor::save() { std::string baseName; for (int i = 1; ; ++i) { char buf[32]; snprintf(buf, sizeof(buf), "saved_mesh%03d", i); baseName = buf; if (!std::filesystem::exists(baseName + ".json")) break; } if (!loc.navigation) return; if (loc.navigation->saveConfig(baseName + ".json")) std::cout << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n"; else std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n"; if (loc.navigation->saveGrid(baseName + ".txt")) std::cout << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n"; else std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n"; } void LocationEditor::reload() { loc.setupNavigation(loc.navigationMapPaths); std::cout << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n"; } static VertexDataStruct CreateBoundsBoxMesh(const Eigen::Vector3f corners[8], const Eigen::Vector3f& color) { VertexDataStruct data; auto addTriangle = [&](const Eigen::Vector3f& a, const Eigen::Vector3f& b, const Eigen::Vector3f& c) { data.PositionData.push_back(a); data.PositionData.push_back(b); data.PositionData.push_back(c); data.ColorData.push_back(color); data.ColorData.push_back(color); data.ColorData.push_back(color); }; auto addFace = [&](int a, int b, int c, int d) { addTriangle(corners[a], corners[b], corners[c]); addTriangle(corners[a], corners[c], corners[d]); }; // corners: 0-3 = bottom ring (y=min), 4-7 = top ring (y=max) // 0=(x0,y0,z0) 1=(x1,y0,z0) 2=(x1,y0,z1) 3=(x0,y0,z1) // 4=(x0,y1,z0) 5=(x1,y1,z0) 6=(x1,y1,z1) 7=(x0,y1,z1) addFace(0, 1, 2, 3); // bottom addFace(7, 6, 5, 4); // top addFace(0, 4, 5, 1); // front (z=min) addFace(3, 2, 6, 7); // back (z=max) addFace(0, 3, 7, 4); // left (x=min) addFace(1, 5, 6, 2); // right (x=max) return data; } void LocationEditor::buildInteractiveObjectBoundsMeshes() { interactiveObjectBoundsMeshes.clear(); interactionPositionMeshes.clear(); const Eigen::Vector3f zero = Eigen::Vector3f::Zero(); const Eigen::Vector3f colorDefault(0.1f, 0.9f, 0.6f); // teal const Eigen::Vector3f colorSelected(1.0f, 1.0f, 0.0f); // yellow const Eigen::Vector3f colorPole(1.0f, 0.55f, 0.0f); // orange int idx = 0; for (const auto& obj : loc.interactiveObjects) { const bool isSelected = (idx == selectedInteractiveObjectIndex); // --- bounds box --- if (obj.boundsMin != zero || obj.boundsMax != zero) { const Eigen::Vector3f& color = isSelected ? colorSelected : colorDefault; const float x0 = obj.boundsMin.x(), x1 = obj.boundsMax.x(); const float y0 = obj.boundsMin.y(), y1 = obj.boundsMax.y(); const float z0 = obj.boundsMin.z(), z1 = obj.boundsMax.z(); Eigen::Vector3f corners[8] = { { x0, y0, z0 }, { x1, y0, z0 }, { x1, y0, z1 }, { x0, y0, z1 }, { x0, y1, z0 }, { x1, y1, z0 }, { x1, y1, z1 }, { x0, y1, z1 }, }; // Apply the same TRS the draw function applies: scale → rotateY → translate if (obj.scale != 1.f) { for (auto& c : corners) c *= obj.scale; } if (obj.rotationY != 0.f) { const float cosR = std::cos(obj.rotationY); const float sinR = std::sin(obj.rotationY); for (auto& c : corners) { const float nx = c.x() * cosR - c.z() * sinR; const float nz = c.x() * sinR + c.z() * cosR; c.x() = nx; c.z() = nz; } } for (auto& c : corners) c += obj.position; VertexRenderStruct mesh; mesh.data = CreateBoundsBoxMesh(corners, color); mesh.RefreshVBO(); interactiveObjectBoundsMeshes.push_back(std::move(mesh)); } // --- interaction position pole --- if (obj.hasInteractionPosition) { const Eigen::Vector3f& p = obj.interactionPosition; static constexpr float hw = 0.05f; // half width/depth static constexpr float hh = 1.5f; // half height (pole = 3.0 tall) Eigen::Vector3f corners[8] = { { p.x()-hw, p.y()-hh, p.z()-hw }, { p.x()+hw, p.y()-hh, p.z()-hw }, { p.x()+hw, p.y()-hh, p.z()+hw }, { p.x()-hw, p.y()-hh, p.z()+hw }, { p.x()-hw, p.y()+hh, p.z()-hw }, { p.x()+hw, p.y()+hh, p.z()-hw }, { p.x()+hw, p.y()+hh, p.z()+hw }, { p.x()-hw, p.y()+hh, p.z()+hw }, }; VertexRenderStruct mesh; mesh.data = CreateBoundsBoxMesh(corners, colorPole); mesh.RefreshVBO(); interactionPositionMeshes.push_back(std::move(mesh)); } ++idx; } } void LocationEditor::drawInteractiveObjectBounds() { if (interactiveObjectBoundsMeshes.empty() && interactionPositionMeshes.empty()) return; loc.renderer.shaderManager.PushShader("defaultColor"); loc.renderer.SetMatrix(); for (const auto& mesh : interactiveObjectBoundsMeshes) { loc.renderer.DrawVertexRenderStruct(mesh); } for (const auto& mesh : interactionPositionMeshes) { loc.renderer.DrawVertexRenderStruct(mesh); } loc.renderer.shaderManager.PopShader(); loc.renderer.SetMatrix(); } void LocationEditor::selectInteractiveObject(int index) { if (index < 0 || index >= static_cast(loc.interactiveObjects.size())) { std::cout << "[IO_EDITOR] Index " << index << " out of range (" << loc.interactiveObjects.size() << " objects)\n"; return; } selectedInteractiveObjectIndex = index; boundsClickCount = 0; buildInteractiveObjectBoundsMeshes(); std::cout << "[IO_EDITOR] Selected object " << index << " (" << loc.interactiveObjects[index].loadedObject.name << ")\n"; } void LocationEditor::handleInteractiveObjectClick(const Eigen::Vector3f& worldHit, bool ctrlHeld) { if (selectedInteractiveObjectIndex < 0 || selectedInteractiveObjectIndex >= static_cast(loc.interactiveObjects.size())) { std::cout << "[IO_EDITOR] No valid object selected\n"; return; } if (ctrlHeld) { // Set the interaction position for the selected object (world XZ, Y=0). auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex]; target.interactionPosition = Eigen::Vector3f(worldHit.x(), 0.f, worldHit.z()); target.hasInteractionPosition = true; boundsClickCount = 0; // cancel any in-progress bounds placement buildInteractiveObjectBoundsMeshes(); std::cout << "[IO_EDITOR] Interaction position set on object " << selectedInteractiveObjectIndex << " (" << target.loadedObject.name << ")" << " at (" << worldHit.x() << ", " << worldHit.z() << ")\n"; return; } const auto& obj = loc.interactiveObjects[selectedInteractiveObjectIndex]; // Convert world-space hit to object local space (inverse of scale → rotateY → translate) auto worldToLocal = [&](const Eigen::Vector3f& w) -> Eigen::Vector3f { Eigen::Vector3f local = w - obj.position; if (obj.rotationY != 0.f) { const float c = std::cos(-obj.rotationY); const float s = std::sin(-obj.rotationY); const float nx = local.x() * c - local.z() * s; const float nz = local.x() * s + local.z() * c; local.x() = nx; local.z() = nz; } if (obj.scale > 1e-6f && obj.scale != 1.f) local /= obj.scale; return local; }; if (boundsClickCount == 0) { boundsClickA = worldHit; boundsClickCount = 1; std::cout << "[IO_EDITOR] First corner placed at world (" << worldHit.x() << ", " << worldHit.z() << ") — click again for second corner\n"; } else { const Eigen::Vector3f localA = worldToLocal(boundsClickA); const Eigen::Vector3f localB = worldToLocal(worldHit); auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex]; target.boundsMin = Eigen::Vector3f( min(localA.x(), localB.x()), 0.f, min(localA.z(), localB.z())); target.boundsMax = Eigen::Vector3f( max(localA.x(), localB.x()), 1.f, max(localA.z(), localB.z())); boundsClickCount = 0; buildInteractiveObjectBoundsMeshes(); std::cout << "[IO_EDITOR] Bounds set on object " << selectedInteractiveObjectIndex << " (" << target.loadedObject.name << ")" << " min=(" << target.boundsMin.x() << ", " << target.boundsMin.z() << ")" << " max=(" << target.boundsMax.x() << ", " << target.boundsMax.z() << ")\n"; } } void LocationEditor::saveInteractiveObjects() { std::string baseName; for (int i = 1; ; ++i) { char buf[32]; snprintf(buf, sizeof(buf), "saved_interactive%03d", i); baseName = buf; if (!std::filesystem::exists(baseName + ".json")) break; } using json = nlohmann::json; json j; j["objects"] = json::array(); for (const auto& obj : loc.interactiveObjects) { json item; item["name"] = obj.loadedObject.name; item["texturePath"] = obj.loadedObject.texturePath; if (!obj.loadedObject.textureDarkandsPath.empty()) item["textureDarkandsPath"] = obj.loadedObject.textureDarkandsPath; item["meshPath"] = obj.loadedObject.meshPath; item["rotationX"] = obj.loadedObject.meshRotationX; item["rotationY"] = obj.loadedObject.meshRotationY; item["rotationZ"] = obj.loadedObject.meshRotationZ; item["positionX"] = obj.jsonPositionX; item["positionY"] = obj.jsonPositionY; item["positionZ"] = obj.jsonPositionZ; item["scale"] = obj.loadedObject.meshScale; item["interactionRadius"] = obj.interactionRadius; item["approachRadius"] = obj.approachRadius; if (!obj.activateFunctionName.empty()) item["activateFunction"] = obj.activateFunctionName; item["pivotX"] = obj.pivot.x(); item["pivotY"] = obj.pivot.y(); item["pivotZ"] = obj.pivot.z(); item["boundsMinX"] = obj.boundsMin.x(); item["boundsMinY"] = obj.boundsMin.y(); item["boundsMinZ"] = obj.boundsMin.z(); item["boundsMaxX"] = obj.boundsMax.x(); item["boundsMaxY"] = obj.boundsMax.y(); item["boundsMaxZ"] = obj.boundsMax.z(); if (obj.hasInteractionPosition) { item["interactionPositionX"] = obj.interactionPosition.x(); item["interactionPositionY"] = obj.interactionPosition.y(); item["interactionPositionZ"] = obj.interactionPosition.z(); } j["objects"].push_back(item); } const std::string filename = baseName + ".json"; std::ofstream out(filename); if (out.is_open()) { out << j.dump(4); std::cout << "[IO_EDITOR] Saved " << loc.interactiveObjects.size() << " interactive object(s) to " << filename << "\n"; } else { std::cerr << "[IO_EDITOR] Failed to open " << filename << " for writing\n"; } } void LocationEditor::saveAll() { save(); saveInteractiveObjects(); } void LocationEditor::placeTree() { if (!loc.player) return; static std::mt19937 rng(std::random_device{}()); std::uniform_real_distribution scaleDist(0.8f, 1.2f); std::uniform_real_distribution rotDist(0.0f, 360.0f); GameObjectData data; data.name = "editor_tree_" + std::to_string(++editorPlacedObjectCounter); data.texturePath = "resources/w/exterior/tree001.png"; data.meshPath = "resources/w/exterior/tree003.txt"; data.rotationX = 0.0f; data.rotationY = rotDist(rng); data.rotationZ = 0.0f; data.positionX = loc.player->position.x(); data.positionY = loc.player->position.y(); data.positionZ = loc.player->position.z(); data.scale = scaleDist(rng); LoadedGameObject obj = GameObjectLoader::buildLoadedObject(data, loc.renderer, CONST_ZIP_FILE); obj.mesh.data.Move({ data.positionX, data.positionY, data.positionZ }); obj.mesh.RefreshVBO(); loc.gameObjects[data.name] = std::move(obj); editorPlacedObjects.push_back(data); std::cout << "[GAME_EDITOR] Placed '" << data.name << "' at (" << data.positionX << ", " << data.positionZ << ") scale=" << data.scale << " rotY=" << data.rotationY << "\n"; } void LocationEditor::saveObjects() { if (editorPlacedObjects.empty()) { std::cout << "[GAME_EDITOR] No editor-placed objects to save\n"; return; } std::string baseName; for (int i = 1; ; ++i) { char buf[32]; snprintf(buf, sizeof(buf), "saved_objects%03d", i); baseName = buf; if (!std::filesystem::exists(baseName + ".json")) break; } using json = nlohmann::json; json j; j["objects"] = json::array(); for (const auto& d : editorPlacedObjects) { json obj; obj["name"] = d.name; obj["texturePath"] = d.texturePath; obj["meshPath"] = d.meshPath; obj["rotationX"] = d.rotationX; obj["rotationY"] = d.rotationY; obj["rotationZ"] = d.rotationZ; obj["positionX"] = d.positionX; obj["positionY"] = d.positionY; obj["positionZ"] = d.positionZ; obj["scale"] = d.scale; j["objects"].push_back(obj); } const std::string filename = baseName + ".json"; std::ofstream out(filename); if (out.is_open()) { out << j.dump(4); std::cout << "[GAME_EDITOR] Saved " << editorPlacedObjects.size() << " object(s) to " << filename << "\n"; } else { std::cerr << "[GAME_EDITOR] Failed to open " << filename << " for writing\n"; } } } // namespace ZL