feat(editor): unified Export Zone, quick-save, cursor world coords

- Ctrl+S quick-saves entire zone (ADT + WDT + creatures.json) to output/
- File > Export Zone dialog saves all data to chosen directory
- exportZone() bundles ADT, WDT, and NPC spawns in one operation
- Info panel shows cursor world coordinates for placement debugging
- Info panel shows NPC count alongside object count
- Save state indicator: "Saved" (green) vs "* Unsaved (Ctrl+S)" (yellow)
- File menu reorganized: Quick Save + Export Zone replaces separate ADT/WDT
This commit is contained in:
Kelsi 2026-05-05 04:06:19 -07:00
parent 88abbfb564
commit d28c5ec842
3 changed files with 69 additions and 23 deletions

View file

@ -211,6 +211,9 @@ void EditorApp::processEvents() {
objectsDirty_ = true; objectsDirty_ = true;
} }
} }
if (sc == SDL_SCANCODE_S && (event.key.keysym.mod & KMOD_CTRL)) {
quickSave();
}
if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) { if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) {
if (mode_ == EditorMode::Sculpt) { if (mode_ == EditorMode::Sculpt) {
if (event.key.keysym.mod & KMOD_SHIFT) if (event.key.keysym.mod & KMOD_SHIFT)
@ -544,6 +547,37 @@ void EditorApp::saveWDT(const std::string& path) {
ADTWriter::writeWDT(loadedMap_, loadedTileX_, loadedTileY_, path); ADTWriter::writeWDT(loadedMap_, loadedTileX_, loadedTileY_, path);
} }
void EditorApp::exportZone(const std::string& outputDir) {
if (!terrain_.isLoaded() || loadedMap_.empty()) return;
std::string base = outputDir + "/" + loadedMap_;
// Save ADT
std::string adtPath = base + "/" + loadedMap_ + "_" +
std::to_string(loadedTileX_) + "_" +
std::to_string(loadedTileY_) + ".adt";
saveADT(adtPath);
// Save WDT
std::string wdtPath = base + "/" + loadedMap_ + ".wdt";
saveWDT(wdtPath);
// Save creature spawns
if (npcSpawner_.spawnCount() > 0) {
std::string npcPath = base + "/creatures.json";
npcSpawner_.saveToFile(npcPath);
}
lastSavePath_ = outputDir;
LOG_INFO("Zone exported to: ", base);
}
void EditorApp::quickSave() {
if (!terrain_.isLoaded()) return;
std::string dir = lastSavePath_.empty() ? "output" : lastSavePath_;
exportZone(dir);
}
void EditorApp::requestQuit() { void EditorApp::requestQuit() {
window_->setShouldClose(true); window_->setShouldClose(true);
} }

View file

@ -32,6 +32,8 @@ public:
void createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome); void createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome);
void saveADT(const std::string& path); void saveADT(const std::string& path);
void saveWDT(const std::string& path); void saveWDT(const std::string& path);
void exportZone(const std::string& outputDir);
void quickSave();
void requestQuit(); void requestQuit();
void resetCamera(); void resetCamera();
@ -96,6 +98,7 @@ private:
bool painting_ = false; bool painting_ = false;
bool objectsDirty_ = false; bool objectsDirty_ = false;
bool openContextMenu_ = false; bool openContextMenu_ = false;
std::string lastSavePath_;
size_t lastObjectCount_ = 0; size_t lastObjectCount_ = 0;
EditorMode mode_ = EditorMode::Sculpt; EditorMode mode_ = EditorMode::Sculpt;
float waterHeight_ = 100.0f; float waterHeight_ = 100.0f;

View file

@ -59,14 +59,8 @@ void EditorUI::processActions(EditorApp& app) {
loadRequested_ = false; loadRequested_ = false;
app.loadADT(loadMapNameBuf_, loadTileX_, loadTileY_); app.loadADT(loadMapNameBuf_, loadTileX_, loadTileY_);
} }
if (saveAdtRequested_) { (void)saveAdtRequested_;
saveAdtRequested_ = false; (void)saveWdtRequested_;
app.saveADT(savePathBuf_);
}
if (saveWdtRequested_) {
saveWdtRequested_ = false;
app.saveWDT(std::string(savePathBuf_));
}
} }
void EditorUI::renderMenuBar(EditorApp& app) { void EditorUI::renderMenuBar(EditorApp& app) {
@ -75,7 +69,9 @@ void EditorUI::renderMenuBar(EditorApp& app) {
if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true; if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true;
if (ImGui::MenuItem("Load ADT...", "Ctrl+O")) showLoadDialog_ = true; if (ImGui::MenuItem("Load ADT...", "Ctrl+O")) showLoadDialog_ = true;
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Save ADT...", "Ctrl+S", false, app.hasTerrainLoaded())) if (ImGui::MenuItem("Quick Save", "Ctrl+S", false, app.hasTerrainLoaded()))
app.quickSave();
if (ImGui::MenuItem("Export Zone...", nullptr, false, app.hasTerrainLoaded()))
showSaveDialog_ = true; showSaveDialog_ = true;
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Quit", "Alt+F4")) app.requestQuit(); if (ImGui::MenuItem("Quit", "Alt+F4")) app.requestQuit();
@ -168,18 +164,18 @@ void EditorUI::renderLoadDialog(EditorApp& /*app*/) {
} }
void EditorUI::renderSaveDialog(EditorApp& app) { void EditorUI::renderSaveDialog(EditorApp& app) {
ImGui::SetNextWindowSize(ImVec2(500, 200), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(500, 220), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Save", &showSaveDialog_)) { if (ImGui::Begin("Export Zone", &showSaveDialog_)) {
if (savePathBuf_[0] == '\0' && app.hasTerrainLoaded()) if (savePathBuf_[0] == '\0' && app.hasTerrainLoaded())
std::snprintf(savePathBuf_, sizeof(savePathBuf_), "output/%s/%s_%d_%d.adt", std::snprintf(savePathBuf_, sizeof(savePathBuf_), "output");
app.getLoadedMap().c_str(), app.getLoadedMap().c_str(), ImGui::InputText("Output Directory", savePathBuf_, sizeof(savePathBuf_));
app.getLoadedTileX(), app.getLoadedTileY()); ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
ImGui::InputText("Path", savePathBuf_, sizeof(savePathBuf_)); "Exports: ADT, WDT, and creature spawns to %s/%s/",
savePathBuf_, app.getLoadedMap().c_str());
ImGui::Spacing(); ImGui::Spacing();
if (ImGui::Button("Save ADT", ImVec2(140, 0))) { saveAdtRequested_ = true; showSaveDialog_ = false; } if (ImGui::Button("Export All", ImVec2(140, 0))) {
ImGui::SameLine(); app.exportZone(savePathBuf_);
if (ImGui::Button("Save ADT + WDT", ImVec2(140, 0))) { showSaveDialog_ = false;
saveAdtRequested_ = true; saveWdtRequested_ = true; showSaveDialog_ = false;
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) showSaveDialog_ = false; if (ImGui::Button("Cancel", ImVec2(80, 0))) showSaveDialog_ = false;
@ -639,14 +635,16 @@ void EditorUI::renderContextMenu(EditorApp& app) {
void EditorUI::renderPropertiesPanel(EditorApp& app) { void EditorUI::renderPropertiesPanel(EditorApp& app) {
ImGuiViewport* vp = ImGui::GetMainViewport(); ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(vp->Size.x - 280, 90), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(vp->Size.x - 280, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(270, 180), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(270, 220), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Properties")) { if (ImGui::Begin("Info")) {
auto* tr = app.getTerrainRenderer(); auto* tr = app.getTerrainRenderer();
if (tr && tr->getChunkCount() > 0) { if (tr && tr->getChunkCount() > 0) {
ImGui::Text("Map: %s [%d, %d]", app.getLoadedMap().c_str(), ImGui::Text("Map: %s [%d, %d]", app.getLoadedMap().c_str(),
app.getLoadedTileX(), app.getLoadedTileY()); app.getLoadedTileX(), app.getLoadedTileY());
ImGui::Text("Chunks: %d Tris: %d", tr->getChunkCount(), tr->getTriangleCount()); ImGui::Text("Chunks: %d Tris: %d", tr->getChunkCount(), tr->getTriangleCount());
ImGui::Text("Objects: %zu", app.getObjectPlacer().objectCount()); ImGui::Text("Objects: %zu NPCs: %zu",
app.getObjectPlacer().objectCount(),
app.getNpcSpawner().spawnCount());
} else { } else {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No terrain loaded"); ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No terrain loaded");
} }
@ -654,8 +652,19 @@ void EditorUI::renderPropertiesPanel(EditorApp& app) {
auto pos = app.getEditorCamera().getCamera().getPosition(); auto pos = app.getEditorCamera().getCamera().getPosition();
ImGui::Text("Camera: %.0f, %.0f, %.0f", pos.x, pos.y, pos.z); ImGui::Text("Camera: %.0f, %.0f, %.0f", pos.x, pos.y, pos.z);
ImGui::Text("Speed: %.0f (scroll)", app.getEditorCamera().getSpeed()); ImGui::Text("Speed: %.0f (scroll)", app.getEditorCamera().getSpeed());
// Cursor world position
auto& brush = app.getTerrainEditor().brush();
if (brush.isActive()) {
auto bp = brush.getPosition();
ImGui::Text("Cursor: %.1f, %.1f, %.1f", bp.x, bp.y, bp.z);
}
ImGui::Separator();
if (app.getTerrainEditor().hasUnsavedChanges()) if (app.getTerrainEditor().hasUnsavedChanges())
ImGui::TextColored(ImVec4(1, 0.8f, 0.3f, 1), "* Unsaved changes"); ImGui::TextColored(ImVec4(1, 0.8f, 0.3f, 1), "* Unsaved (Ctrl+S to save)");
else
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1), "Saved");
} }
ImGui::End(); ImGui::End();
} }