diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index c8e102d2..c9929286 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -597,6 +597,12 @@ void EditorApp::exportZone(const std::string& outputDir) { npcSpawner_.saveToFile(npcPath); } + // Save placed objects + if (objectPlacer_.objectCount() > 0) { + std::string objPath = base + "/objects.json"; + objectPlacer_.saveToFile(objPath); + } + // Write zone manifest (for client loading) ZoneManifest manifest; manifest.mapName = loadedMap_; diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 5738d22b..27d1db9b 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -565,12 +565,17 @@ void EditorUI::renderObjectPanel(EditorApp& app) { ImGui::SameLine(); if (ImGui::Button("Delete", ImVec2(100, 0))) placer.deleteSelected(); if (ImGui::Button("Duplicate", ImVec2(100, 0))) { - PlacedObject copy = *sel; - copy.uniqueId = 0; - copy.position += glm::vec3(5.0f, 5.0f, 0.0f); - copy.selected = false; + std::string dupPath = sel->path; + glm::vec3 dupPos = sel->position + glm::vec3(10.0f, 10.0f, 0.0f); + glm::vec3 dupRot = sel->rotation; + float dupScale = sel->scale; + auto dupType = sel->type; placer.clearSelection(); - // Can't easily push from here, but move slightly signals intent + placer.setActivePath(dupPath, dupType); + placer.setPlacementScale(dupScale); + placer.setPlacementRotationY(dupRot.y); + placer.placeObject(dupPos); + app.markObjectsDirty(); } ImGui::SameLine(); if (ImGui::Button("Deselect", ImVec2(100, 0))) diff --git a/tools/editor/object_placer.cpp b/tools/editor/object_placer.cpp index e8c397b1..92cfa446 100644 --- a/tools/editor/object_placer.cpp +++ b/tools/editor/object_placer.cpp @@ -2,6 +2,8 @@ #include "core/logger.hpp" #include #include +#include +#include #include namespace wowee { @@ -143,6 +145,88 @@ void ObjectPlacer::undoLastPlace() { } } +bool ObjectPlacer::saveToFile(const std::string& path) const { + std::filesystem::create_directories(std::filesystem::path(path).parent_path()); + std::ofstream f(path); + if (!f) return false; + f << "[\n"; + for (size_t i = 0; i < objects_.size(); i++) { + const auto& o = objects_[i]; + f << " {\"type\":" << static_cast(o.type) + << ",\"path\":\"" << o.path << "\"" + << ",\"pos\":[" << o.position.x << "," << o.position.y << "," << o.position.z << "]" + << ",\"rot\":[" << o.rotation.x << "," << o.rotation.y << "," << o.rotation.z << "]" + << ",\"scale\":" << o.scale + << "}" << (i + 1 < objects_.size() ? "," : "") << "\n"; + } + f << "]\n"; + LOG_INFO("Objects saved: ", path, " (", objects_.size(), " objects)"); + return true; +} + +bool ObjectPlacer::loadFromFile(const std::string& path) { + std::ifstream f(path); + if (!f) return false; + std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + + objects_.clear(); + undoStack_.clear(); + selectedIdx_ = -1; + + size_t start = 0; + while ((start = content.find('{', start)) != std::string::npos) { + auto end = content.find('}', start); + if (end == std::string::npos) break; + std::string block = content.substr(start, end - start + 1); + + PlacedObject obj; + // Parse type + auto tp = block.find("\"type\":"); + if (tp != std::string::npos) obj.type = static_cast(std::stoi(block.substr(tp + 7))); + + // Parse path + auto pp = block.find("\"path\":\""); + if (pp != std::string::npos) { + pp += 8; + auto pe = block.find('"', pp); + if (pe != std::string::npos) obj.path = block.substr(pp, pe - pp); + } + + // Parse pos array + auto posP = block.find("\"pos\":["); + if (posP != std::string::npos) { + posP += 7; + obj.position.x = std::stof(block.substr(posP)); + posP = block.find(',', posP) + 1; + obj.position.y = std::stof(block.substr(posP)); + posP = block.find(',', posP) + 1; + obj.position.z = std::stof(block.substr(posP)); + } + + // Parse rot array + auto rotP = block.find("\"rot\":["); + if (rotP != std::string::npos) { + rotP += 7; + obj.rotation.x = std::stof(block.substr(rotP)); + rotP = block.find(',', rotP) + 1; + obj.rotation.y = std::stof(block.substr(rotP)); + rotP = block.find(',', rotP) + 1; + obj.rotation.z = std::stof(block.substr(rotP)); + } + + auto scP = block.find("\"scale\":"); + if (scP != std::string::npos) obj.scale = std::stof(block.substr(scP + 8)); + + if (!obj.path.empty()) { + obj.uniqueId = nextUniqueId(); + objects_.push_back(obj); + } + start = end + 1; + } + LOG_INFO("Objects loaded: ", path, " (", objects_.size(), " objects)"); + return true; +} + void ObjectPlacer::syncToTerrain() { if (!terrain_) return; diff --git a/tools/editor/object_placer.hpp b/tools/editor/object_placer.hpp index 7c04c6a0..1e5c6ae3 100644 --- a/tools/editor/object_placer.hpp +++ b/tools/editor/object_placer.hpp @@ -49,6 +49,10 @@ public: // Sync placed objects back to ADTTerrain structs void syncToTerrain(); + // Save/load placed objects to JSON + bool saveToFile(const std::string& path) const; + bool loadFromFile(const std::string& path); + const std::vector& getObjects() const { return objects_; } size_t objectCount() const { return objects_.size(); }