diff --git a/tools/editor/dbc_exporter.cpp b/tools/editor/dbc_exporter.cpp index 795e311f..feae64db 100644 --- a/tools/editor/dbc_exporter.cpp +++ b/tools/editor/dbc_exporter.cpp @@ -1,6 +1,7 @@ #include "dbc_exporter.hpp" #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" +#include #include #include @@ -16,49 +17,37 @@ bool DBCExporter::exportAsJson(pipeline::AssetManager* am, namespace fs = std::filesystem; fs::create_directories(fs::path(outputPath).parent_path()); - std::ofstream f(outputPath); - if (!f) return false; - - f << "{\n"; - f << " \"format\": \"wowee-dbc-json-1.0\",\n"; - f << " \"source\": \"" << dbcName << "\",\n"; - f << " \"recordCount\": " << dbc->getRecordCount() << ",\n"; - f << " \"fieldCount\": " << dbc->getFieldCount() << ",\n"; - f << " \"records\": [\n"; + nlohmann::json j; + j["format"] = "wowee-dbc-json-1.0"; + j["source"] = dbcName; + j["recordCount"] = dbc->getRecordCount(); + j["fieldCount"] = dbc->getFieldCount(); + nlohmann::json records = nlohmann::json::array(); for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { - f << " ["; - for (uint32_t j = 0; j < dbc->getFieldCount(); j++) { - // Try to detect string fields vs numeric - uint32_t val = dbc->getUInt32(i, j); - // Check if it looks like a string offset (points into string block) - std::string strVal = dbc->getString(i, j); + nlohmann::json row = nlohmann::json::array(); + for (uint32_t field = 0; field < dbc->getFieldCount(); field++) { + uint32_t val = dbc->getUInt32(i, field); + std::string strVal = dbc->getString(i, field); if (!strVal.empty() && strVal[0] != '\0' && strVal.size() < 200) { - // Escape quotes in string - std::string escaped; - for (char c : strVal) { - if (c == '"') escaped += "\\\""; - else if (c == '\\') escaped += "\\\\"; - else if (c == '\n') escaped += "\\n"; - else escaped += c; - } - f << "\"" << escaped << "\""; + row.push_back(strVal); } else { - // Check if it's a float - float fval = dbc->getFloat(i, j); + float fval = dbc->getFloat(i, field); if (val != 0 && fval != 0.0f && fval > -1e10f && fval < 1e10f && static_cast(fval) != val) { - f << fval; + row.push_back(fval); } else { - f << val; + row.push_back(val); } } - if (j + 1 < dbc->getFieldCount()) f << ", "; } - f << "]" << (i + 1 < dbc->getRecordCount() ? "," : "") << "\n"; + records.push_back(row); } + j["records"] = records; - f << " ]\n}\n"; + std::ofstream f(outputPath); + if (!f) return false; + f << j.dump(2) << "\n"; LOG_INFO("DBC exported as JSON: ", dbcName, " → ", outputPath, " (", dbc->getRecordCount(), " records)"); @@ -67,7 +56,6 @@ bool DBCExporter::exportAsJson(pipeline::AssetManager* am, int DBCExporter::exportZoneDBCs(pipeline::AssetManager* am, const std::string& outputDir) { - // Zone-relevant DBCs for custom content const char* zoneDBCs[] = { "AreaTable.dbc", "Map.dbc", diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index a22cb2e7..fd693bcd 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -241,6 +241,9 @@ void EditorApp::processEvents() { if (sc == SDL_SCANCODE_6) setMode(EditorMode::Quest); } // F1 handled by UI (showHelp_ toggle) + // F1 = toggle help + if (sc == SDL_SCANCODE_F1 && !io.WantCaptureKeyboard) + ui_.toggleHelp(); // Transform shortcuts (Blender-style) if (objectPlacer_.getSelected()) { if (sc == SDL_SCANCODE_G) startGizmoMode(TransformMode::Move); @@ -248,6 +251,8 @@ void EditorApp::processEvents() { if (sc == SDL_SCANCODE_T) startGizmoMode(TransformMode::Scale); if (sc == SDL_SCANCODE_X) setGizmoAxis(TransformAxis::X); if (sc == SDL_SCANCODE_Y) setGizmoAxis(TransformAxis::Y); + if (sc == SDL_SCANCODE_Z && !(event.key.keysym.mod & KMOD_CTRL)) + setGizmoAxis(TransformAxis::Z); if (sc == SDL_SCANCODE_ESCAPE) { viewport_.getGizmo().endDrag(); viewport_.getGizmo().setMode(TransformMode::None); @@ -269,10 +274,16 @@ void EditorApp::processEvents() { ui_.openNewTerrainDialog(); if (sc == SDL_SCANCODE_O && (event.key.keysym.mod & KMOD_CTRL)) ui_.openLoadDialog(); + // Ctrl+Y = Redo (alternate binding) + if (sc == SDL_SCANCODE_Y && (event.key.keysym.mod & KMOD_CTRL)) { + if (terrainEditor_.history().canRedo()) { + terrainEditor_.redo(); + showToast("Redo"); + } + } if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) { bool isRedo = (event.key.keysym.mod & KMOD_SHIFT) != 0; if (isRedo) { - // Ctrl+Shift+Z = Redo (sculpt only for now) if (terrainEditor_.history().canRedo()) { terrainEditor_.redo(); showToast("Redo"); diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index d9f2a7b2..ccdfeea1 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -418,14 +418,14 @@ void EditorUI::renderMenuBar(EditorApp& app) { ImGui::BulletText("Ctrl+click — select object/NPC"); ImGui::BulletText("Ctrl+S — quick save"); ImGui::BulletText("Ctrl+Z — undo"); - ImGui::BulletText("Ctrl+Shift+Z — redo"); + ImGui::BulletText("Ctrl+Shift+Z / Ctrl+Y — redo"); ImGui::BulletText("Delete — remove selected"); ImGui::Separator(); ImGui::Text("Object Transform:"); ImGui::BulletText("G — move mode (then drag)"); ImGui::BulletText("R — rotate mode (then drag)"); ImGui::BulletText("T — scale mode (then drag)"); - ImGui::BulletText("X/Y — constrain to axis"); + ImGui::BulletText("X/Y/Z — constrain to axis"); ImGui::BulletText("Escape — deselect / cancel"); ImGui::BulletText("Right-click — context menu"); ImGui::Separator(); diff --git a/tools/editor/editor_ui.hpp b/tools/editor/editor_ui.hpp index 06cf4dad..ff067af4 100644 --- a/tools/editor/editor_ui.hpp +++ b/tools/editor/editor_ui.hpp @@ -20,6 +20,7 @@ public: void processActions(EditorApp& app); void openNewTerrainDialog() { showNewDialog_ = true; } void openLoadDialog() { showLoadDialog_ = true; } + void toggleHelp() { showHelp_ = !showHelp_; } PaintMode getPaintMode() const { return paintMode_; } diff --git a/tools/editor/wowee_terrain.cpp b/tools/editor/wowee_terrain.cpp index 2b51e3f9..e27aa536 100644 --- a/tools/editor/wowee_terrain.cpp +++ b/tools/editor/wowee_terrain.cpp @@ -217,6 +217,7 @@ int WoweeTerrain::exportAlphaMaps(const pipeline::ADTTerrain& terrain, } bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain) { + // Load binary heightmap (.whm) std::string hmPath = basePath + ".whm"; std::ifstream f(hmPath, std::ios::binary); if (!f) return false; @@ -233,10 +234,91 @@ bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& for (int ci = 0; ci < 256; ci++) { auto& chunk = terrain.chunks[ci]; chunk.heightMap.loaded = true; + chunk.indexX = ci % 16; + chunk.indexY = ci / 16; float base; f.read(reinterpret_cast(&base), 4); chunk.position[2] = base; f.read(reinterpret_cast(chunk.heightMap.heights.data()), 145 * 4); + + uint32_t alphaSize = 0; + if (f.read(reinterpret_cast(&alphaSize), 4) && alphaSize > 0 && alphaSize <= 65536) { + chunk.alphaMap.resize(alphaSize); + f.read(reinterpret_cast(chunk.alphaMap.data()), alphaSize); + } + + for (int i = 0; i < 145; i++) { + chunk.normals[i * 3 + 0] = 0; + chunk.normals[i * 3 + 1] = 0; + chunk.normals[i * 3 + 2] = 127; + } + } + + // Load JSON metadata (.wot) + std::string wotPath = basePath + ".wot"; + std::ifstream wf(wotPath); + if (wf) { + try { + auto j = nlohmann::json::parse(wf); + + terrain.coord.x = j.value("tileX", 0); + terrain.coord.y = j.value("tileY", 0); + + float tileSize = 533.33333f; + float chunkSize = tileSize / 16.0f; + for (int cy = 0; cy < 16; cy++) { + for (int cx = 0; cx < 16; cx++) { + auto& chunk = terrain.chunks[cy * 16 + cx]; + chunk.position[0] = (32.0f - terrain.coord.x) * tileSize - cx * chunkSize; + chunk.position[1] = (32.0f - terrain.coord.y) * tileSize - cy * chunkSize; + } + } + + if (j.contains("textures") && j["textures"].is_array()) { + for (const auto& tex : j["textures"]) { + if (tex.is_string() && !tex.get().empty()) + terrain.textures.push_back(tex.get()); + } + } + + if (j.contains("chunkLayers") && j["chunkLayers"].is_array()) { + const auto& layers = j["chunkLayers"]; + for (int ci = 0; ci < std::min(256, static_cast(layers.size())); ci++) { + const auto& cl = layers[ci]; + if (cl.contains("layers") && cl["layers"].is_array()) { + for (const auto& texId : cl["layers"]) { + pipeline::TextureLayer layer{}; + layer.textureId = texId.get(); + layer.flags = terrain.chunks[ci].layers.empty() ? 0 : 0x100; + terrain.chunks[ci].layers.push_back(layer); + } + } + if (cl.contains("holes")) + terrain.chunks[ci].holes = cl["holes"].get(); + } + } + + if (j.contains("water") && j["water"].is_array()) { + for (const auto& w : j["water"]) { + if (w.is_null()) continue; + int wci = w.value("chunk", -1); + if (wci < 0 || wci >= 256) continue; + pipeline::ADTTerrain::WaterLayer wl; + wl.liquidType = w.value("type", 0u); + wl.maxHeight = w.value("height", 0.0f); + wl.minHeight = wl.maxHeight; + wl.x = 0; wl.y = 0; wl.width = 9; wl.height = 9; + wl.heights.assign(81, wl.maxHeight); + wl.mask.assign(8, 0xFF); + terrain.waterData[wci].layers.push_back(wl); + } + } + + LOG_INFO("WOT metadata loaded: tile [", terrain.coord.x, ",", terrain.coord.y, + "], ", terrain.textures.size(), " textures"); + } catch (const std::exception& e) { + LOG_WARNING("Could not parse WOT metadata: ", e.what()); + } } LOG_INFO("Open terrain imported: ", basePath);