From b19627d82c1d10b7552e43c2ad7453a121e96bff Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 16:06:34 -0700 Subject: [PATCH] feat(editor): SQL spawn export for AzerothCore/TrinityCore Private server integration: export creature spawns, patrol waypoints, and quest definitions as ready-to-import SQL for AzerothCore/TrinityCore. - creature_template: name, level, health, mana, damage, armor, faction, npcflag (questgiver/vendor/flightmaster/innkeeper), displayId, scale - creature: spawn position, orientation, respawn time, wander distance, movement type (stationary/wander/patrol) - creature_addon + waypoint_data: patrol path waypoints with delays - quest_template: title, description, completion text, level, XP, money - All use ON DUPLICATE KEY UPDATE for safe re-imports - Auto-exported as spawns.sql alongside other assets on zone save - File > Export Server SQL menu item for standalone export - Map ID from zone metadata panel used in spawn table --- CMakeLists.txt | 1 + tools/editor/editor_app.cpp | 10 +- tools/editor/editor_ui.cpp | 10 ++ tools/editor/sql_exporter.cpp | 186 ++++++++++++++++++++++++++++++++++ tools/editor/sql_exporter.hpp | 33 ++++++ 5 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 tools/editor/sql_exporter.cpp create mode 100644 tools/editor/sql_exporter.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 72ee8156..acaa5ff6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1297,6 +1297,7 @@ add_executable(wowee_editor tools/editor/object_placer.cpp tools/editor/npc_spawner.cpp tools/editor/npc_presets.cpp + tools/editor/sql_exporter.cpp tools/editor/quest_editor.cpp tools/editor/transform_gizmo.cpp tools/editor/zone_manifest.cpp diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 56dc8c8c..27c8dad0 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -9,6 +9,7 @@ #include "pipeline/wowee_building.hpp" #include "pipeline/wowee_collision.hpp" #include "pipeline/wmo_loader.hpp" +#include "sql_exporter.hpp" #include "core/coordinates.hpp" #include #include "rendering/vk_context.hpp" @@ -875,8 +876,13 @@ void EditorApp::exportZone(const std::string& outputDir) { } } - // Update WDT with additional tiles from adjacent exports - // (future: scan output dir for existing ADTs and include all in WDT) + // Export SQL for private server integration (AzerothCore/TrinityCore) + if (npcSpawner_.spawnCount() > 0 || questEditor_.questCount() > 0) { + std::string sqlPath = base + "/spawns.sql"; + SQLExporter::exportAll(npcSpawner_.getSpawns(), + questEditor_.getQuests(), + sqlPath, zoneManifest_.mapId); + } // Save placed objects if (objectPlacer_.objectCount() > 0) { diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index f68098e7..d8c612b2 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -8,6 +8,7 @@ #include "quest_editor.hpp" #include "pipeline/custom_zone_discovery.hpp" #include "content_pack.hpp" +#include "sql_exporter.hpp" #include "wowee_terrain.hpp" #include "pipeline/wowee_terrain_loader.hpp" #include @@ -363,6 +364,15 @@ void EditorUI::renderMenuBar(EditorApp& app) { showSaveDialog_ = true; if (ImGui::MenuItem("Export Open Format (.wot/.whm)", nullptr, false, app.hasTerrainLoaded())) app.exportOpenFormat("output"); + if (ImGui::MenuItem("Export Server SQL", nullptr, false, + app.getNpcSpawner().spawnCount() > 0 || app.getQuestEditor().questCount() > 0)) { + std::string sqlPath = "output/" + app.getLoadedMap() + "/spawns.sql"; + editor::SQLExporter::exportAll( + app.getNpcSpawner().getSpawns(), + app.getQuestEditor().getQuests(), + sqlPath, app.getZoneManifest().mapId); + app.showToast("SQL exported: " + sqlPath); + } if (ImGui::MenuItem("Export Content Pack (.wcp)", "Ctrl+Shift+E", false, app.hasTerrainLoaded())) { std::string wcpPath = "output/" + app.getLoadedMap() + ".wcp"; app.exportContentPack(wcpPath); diff --git a/tools/editor/sql_exporter.cpp b/tools/editor/sql_exporter.cpp new file mode 100644 index 00000000..59069ecc --- /dev/null +++ b/tools/editor/sql_exporter.cpp @@ -0,0 +1,186 @@ +#include "sql_exporter.hpp" +#include "core/logger.hpp" +#include +#include +#include +#include + +namespace wowee { +namespace editor { + +static std::string escapeSql(const std::string& s) { + std::string out; + for (char c : s) { + if (c == '\'') out += "''"; + else if (c == '\\') out += "\\\\"; + else out += c; + } + return out; +} + +bool SQLExporter::exportCreatures(const std::vector& spawns, + const std::string& path, + uint32_t mapId, + uint32_t startEntry) { + namespace fs = std::filesystem; + fs::create_directories(fs::path(path).parent_path()); + + std::ofstream f(path); + if (!f) return false; + + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + char timeBuf[32]; + std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", std::localtime(&time)); + + f << "-- Wowee World Editor — Creature Spawn Export\n"; + f << "-- Generated: " << timeBuf << "\n"; + f << "-- Target: AzerothCore / TrinityCore 3.3.5a\n"; + f << "-- Map ID: " << mapId << "\n"; + f << "-- Creatures: " << spawns.size() << "\n\n"; + + // creature_template entries + f << "-- =============================================\n"; + f << "-- creature_template (NPC definitions)\n"; + f << "-- =============================================\n\n"; + + for (size_t i = 0; i < spawns.size(); i++) { + const auto& s = spawns[i]; + uint32_t entry = startEntry + static_cast(i); + + uint32_t npcFlags = 0; + if (s.questgiver) npcFlags |= 0x02; + if (s.vendor) npcFlags |= 0x80; + if (s.flightmaster) npcFlags |= 0x02000000; + if (s.innkeeper) npcFlags |= 0x10000; + + uint32_t unitFlags = 0; + if (!s.hostile) unitFlags |= 0x02; // NON_ATTACKABLE + + f << "INSERT INTO `creature_template` " + << "(`entry`, `name`, `minlevel`, `maxlevel`, `minhealth`, `maxhealth`, " + << "`mana`, `armor`, `mindmg`, `maxdmg`, `faction`, `npcflag`, " + << "`unit_flags`, `modelid1`, `scale`) VALUES (" + << entry << ", " + << "'" << escapeSql(s.name) << "', " + << s.level << ", " << s.level << ", " + << s.health << ", " << s.health << ", " + << s.mana << ", " << s.armor << ", " + << s.minDamage << ", " << s.maxDamage << ", " + << s.faction << ", " << npcFlags << ", " + << unitFlags << ", " + << s.displayId << ", " + << s.scale + << ") ON DUPLICATE KEY UPDATE `name`='" << escapeSql(s.name) << "';\n"; + } + + // creature spawn entries + f << "\n-- =============================================\n"; + f << "-- creature (spawn positions)\n"; + f << "-- =============================================\n\n"; + + for (size_t i = 0; i < spawns.size(); i++) { + const auto& s = spawns[i]; + uint32_t entry = startEntry + static_cast(i); + uint32_t guid = startEntry + static_cast(i); + + uint8_t movementType = 0; + if (s.behavior == CreatureBehavior::Wander) movementType = 1; + if (s.behavior == CreatureBehavior::Patrol) movementType = 2; + + f << "INSERT INTO `creature` " + << "(`guid`, `id`, `map`, `position_x`, `position_y`, `position_z`, " + << "`orientation`, `spawntimesecs`, `wander_distance`, `MovementType`) VALUES (" + << guid << ", " << entry << ", " << mapId << ", " + << s.position.x << ", " << s.position.y << ", " << s.position.z << ", " + << s.orientation << ", " + << (s.respawnTimeMs / 1000) << ", " + << s.wanderRadius << ", " + << static_cast(movementType) + << ") ON DUPLICATE KEY UPDATE `position_x`=" << s.position.x << ";\n"; + } + + // Patrol waypoints + bool hasPatrols = false; + for (const auto& s : spawns) { + if (!s.patrolPath.empty()) { hasPatrols = true; break; } + } + if (hasPatrols) { + f << "\n-- =============================================\n"; + f << "-- creature_addon + waypoint_data (patrol paths)\n"; + f << "-- =============================================\n\n"; + + for (size_t i = 0; i < spawns.size(); i++) { + const auto& s = spawns[i]; + if (s.patrolPath.empty()) continue; + uint32_t guid = startEntry + static_cast(i); + uint32_t pathId = guid; + + f << "INSERT INTO `creature_addon` (`guid`, `path_id`) VALUES (" + << guid << ", " << pathId + << ") ON DUPLICATE KEY UPDATE `path_id`=" << pathId << ";\n"; + + for (size_t pi = 0; pi < s.patrolPath.size(); pi++) { + const auto& wp = s.patrolPath[pi]; + f << "INSERT INTO `waypoint_data` " + << "(`id`, `point`, `position_x`, `position_y`, `position_z`, `delay`) VALUES (" + << pathId << ", " << (pi + 1) << ", " + << wp.position.x << ", " << wp.position.y << ", " << wp.position.z << ", " + << wp.waitTimeMs + << ") ON DUPLICATE KEY UPDATE `position_x`=" << wp.position.x << ";\n"; + } + } + } + + LOG_INFO("SQL exported: ", path, " (", spawns.size(), " creatures)"); + return true; +} + +bool SQLExporter::exportQuests(const std::vector& quests, + const std::string& path, + uint32_t startEntry) { + namespace fs = std::filesystem; + fs::create_directories(fs::path(path).parent_path()); + + std::ofstream f(path, std::ios::app); + if (!f) return false; + + if (quests.empty()) return true; + + f << "\n-- =============================================\n"; + f << "-- quest_template (quest definitions)\n"; + f << "-- =============================================\n\n"; + + for (const auto& q : quests) { + uint32_t entry = startEntry + q.id; + uint32_t rewardMoney = q.reward.gold * 10000 + q.reward.silver * 100 + q.reward.copper; + + f << "INSERT INTO `quest_template` " + << "(`ID`, `LogTitle`, `LogDescription`, `QuestCompletionLog`, " + << "`MinLevel`, `RewardXP`, `RewardMoney`) VALUES (" + << entry << ", " + << "'" << escapeSql(q.title) << "', " + << "'" << escapeSql(q.description) << "', " + << "'" << escapeSql(q.completionText) << "', " + << q.requiredLevel << ", " + << q.reward.xp << ", " + << rewardMoney + << ") ON DUPLICATE KEY UPDATE `LogTitle`='" << escapeSql(q.title) << "';\n"; + } + + LOG_INFO("SQL quests exported: ", quests.size(), " quests"); + return true; +} + +bool SQLExporter::exportAll(const std::vector& spawns, + const std::vector& quests, + const std::string& path, + uint32_t mapId, + uint32_t startEntry) { + if (!exportCreatures(spawns, path, mapId, startEntry)) return false; + if (!quests.empty()) exportQuests(quests, path, startEntry); + return true; +} + +} // namespace editor +} // namespace wowee diff --git a/tools/editor/sql_exporter.hpp b/tools/editor/sql_exporter.hpp new file mode 100644 index 00000000..2de00bd6 --- /dev/null +++ b/tools/editor/sql_exporter.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "npc_spawner.hpp" +#include "quest_editor.hpp" +#include +#include + +namespace wowee { +namespace editor { + +class SQLExporter { +public: + // Export creature spawns as AzerothCore/TrinityCore SQL + static bool exportCreatures(const std::vector& spawns, + const std::string& path, + uint32_t mapId = 9000, + uint32_t startEntry = 100000); + + // Export quest definitions as AzerothCore quest_template SQL + static bool exportQuests(const std::vector& quests, + const std::string& path, + uint32_t startEntry = 100000); + + // Export everything to a single SQL file + static bool exportAll(const std::vector& spawns, + const std::vector& quests, + const std::string& path, + uint32_t mapId = 9000, + uint32_t startEntry = 100000); +}; + +} // namespace editor +} // namespace wowee