diff --git a/CMakeLists.txt b/CMakeLists.txt index 84f3ebb..ca7cb1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,9 @@ set(APP_SOURCES # arrossega cap símbol del joc final. source/game/editor/editor.cpp source/game/editor/editor_config.cpp + # Writer del bloc `entities:` del YAML del nivell, paral·lel al + # TilemapWriter pero per a la persistència de l'editor d'entitats. + source/game/entities/entity_writer.cpp ${ENTITY_SOURCES} # Fitxer només del binari principal: `loadFromLevel` depèn de # Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 4a2625f..224c2f4 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -21,6 +21,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/entity_loader.hpp" #include "game/entities/entity_manager.hpp" +#include "game/entities/entity_writer.hpp" #include "game/levels/level_manager.hpp" #include "game/map/map.hpp" #include "game/map/tilemap.hpp" @@ -226,10 +227,42 @@ void Editor::ensureEditableEntities() { } } +namespace { + + // Reconstrueix els spawns del nivell des de l'estat viu del manager. + // Per cada entitat: extreu type i la posició adequada (`position` + // generic, `sarc_position` per a sarcs, cap per a `door`). + auto gatherSpawns(const EntityManager& entities) -> std::vector { + std::vector out; + const auto LIST = entities.getEntities(); + out.reserve(LIST.size()); + for (const auto* e : LIST) { + EntityWriter::Spawn s; + s.type = e->getName(); + if (s.type == "sarcophagus") { + if (Map::pitch_x > 0 && Map::pitch_y > 0) { + const auto LOGICAL = e->getCollisionRect(); + s.sarc_col = (static_cast(LOGICAL.x) - Map::origin_x) / Map::pitch_x; + s.sarc_row = (static_cast(LOGICAL.y) - Map::origin_y) / Map::pitch_y; + } + } else if (s.type != "door") { + if (Map::tile_size > 0) { + const auto R = e->getPixelRect(); + s.col = (static_cast(R.x) - Map::tilemap_origin_x) / Map::tile_size; + s.row = (static_cast(R.y) - Map::tilemap_origin_y) / Map::tile_size; + } + } + out.push_back(std::move(s)); + } + return out; + } + +} // namespace + // cppcheck-suppress constParameterReference // Tilemap& mutable per simetria amb `toggle()` i per futures // invalidacions de caches d'edició, encara que `save()` sigui const. -void Editor::exit(Tilemap& tilemap) { +void Editor::exit(Tilemap& tilemap, const EntityManager& entities) { if (!active_) { return; } active_ = false; picker_ = Picker::NONE; @@ -240,13 +273,15 @@ void Editor::exit(Tilemap& tilemap) { // no el CWD; sense això, el writer obriria el path relatiu al CWD i // fallaria si el binari no es llança des del repo root. const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath()); - const bool SAVED = tilemap.save(ABS_PATH); + const bool TILES_OK = tilemap.save(ABS_PATH); + const bool ENTITIES_OK = EntityWriter::saveEntitiesBlock(ABS_PATH, gatherSpawns(entities)); + const bool SAVED = TILES_OK && ENTITIES_OK; Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error")); } -void Editor::toggle(Tilemap& tilemap) { +void Editor::toggle(Tilemap& tilemap, const EntityManager& entities) { if (active_) { - exit(tilemap); + exit(tilemap, entities); } else { enter(); } diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index 39927d4..90e7637 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -40,13 +40,14 @@ class Editor { // d'entitats / jugador la gestiona GameScene consultant `active()`. void enter(); - // Surt del mode editor: persisteix el tilemap al YAML (writer surgical) - // i notifica. Si la persistència falla, emet la notificació d'error. - void exit(Tilemap& tilemap); + // Surt del mode editor: persisteix el tilemap i les entitats al YAML + // (writers surgicals) i notifica. Si la persistència falla, emet la + // notificació d'error. + void exit(Tilemap& tilemap, const EntityManager& entities); // Helper: enter() si no actiu, exit() si actiu. Útil des de GameScene // quan detecta la tecla de toggle. - void toggle(Tilemap& tilemap); + void toggle(Tilemap& tilemap, const EntityManager& entities); [[nodiscard]] auto active() const -> bool { return active_; } diff --git a/source/game/entities/entity_writer.cpp b/source/game/entities/entity_writer.cpp new file mode 100644 index 0000000..7195ab5 --- /dev/null +++ b/source/game/entities/entity_writer.cpp @@ -0,0 +1,115 @@ +#include "game/entities/entity_writer.hpp" + +#ifdef _DEBUG + +#include +#include +#include +#include +#include +#include + +namespace EntityWriter { + + namespace { + + // Cerca la línia que comença per `:` a columna 0. Mateix + // patró que TilemapWriter::findDataLine però parametritzat. Si + // un dia paga la pena, podem extreure'l a un helper compartit. + auto findKeyLine(const std::string& content, const std::string& key) -> std::size_t { + const std::string PREFIX = key + ":"; + std::size_t pos = 0; + while (pos < content.size()) { + const bool AT_LINE_START = (pos == 0 || content[pos - 1] == '\n'); + if (AT_LINE_START && content.compare(pos, PREFIX.size(), PREFIX) == 0) { + const std::size_t AFTER = pos + PREFIX.size(); + if (AFTER >= content.size() || content[AFTER] == '\n' || content[AFTER] == '\r' || content[AFTER] == ' ' || content[AFTER] == '\t' || content[AFTER] == '#') { + return pos; + } + } + const auto NL = content.find('\n', pos); + if (NL == std::string::npos) { break; } + pos = NL + 1; + } + return std::string::npos; + } + + // Avança mentre trobe línies indentades (espai/tab) o línies buides. + // A diferència del TilemapWriter, ací incloem línies buides dins + // del bloc: l'editor de YAML deixa sovint blanc entre grups + // d'entitats, i volem absorbir-lo per no deixar restes orfes + // entre el bloc nou i el següent top-level. + auto advancePastBlock(const std::string& content, std::size_t pos) -> std::size_t { + while (pos < content.size()) { + const char C = content[pos]; + const bool IS_INDENT = (C == ' ' || C == '\t'); + const bool IS_BLANK = (C == '\n' || C == '\r'); + if (!IS_INDENT && !IS_BLANK) { break; } + const auto NL = content.find('\n', pos); + pos = (NL == std::string::npos) ? content.size() : NL + 1; + } + return pos; + } + + auto buildBlock(const std::vector& spawns) -> std::string { + std::string out; + for (const auto& s : spawns) { + out += " - type: " + s.type + "\n"; + if (s.sarc_col.has_value() && s.sarc_row.has_value()) { + out += " sarc_position: [" + std::to_string(*s.sarc_col) + ", " + std::to_string(*s.sarc_row) + "]\n"; + } else if (s.col.has_value() && s.row.has_value()) { + out += " position: [" + std::to_string(*s.col) + ", " + std::to_string(*s.row) + "]\n"; + } + } + return out; + } + + } // namespace + + auto saveEntitiesBlock(const std::string& path, const std::vector& spawns) -> bool { + std::ifstream in(path, std::ios::binary); + if (!in) { return false; } + std::ostringstream ss; + ss << in.rdbuf(); + const std::string CONTENT = ss.str(); + in.close(); + + const auto KEY_POS = findKeyLine(CONTENT, "entities"); + if (KEY_POS == std::string::npos) { return false; } + + const auto LINE_END = CONTENT.find('\n', KEY_POS); + const auto AFTER_KEY = (LINE_END == std::string::npos) ? CONTENT.size() : LINE_END + 1; + const auto BLOCK_END = advancePastBlock(CONTENT, AFTER_KEY); + + // Mantenim una línia en blanc darrere del bloc si abans n'hi + // havia: ajuda a separar visualment el `entities:` de la pròxima + // secció. Si no n'hi havia, no afegim res. + const std::string TAIL = CONTENT.substr(BLOCK_END); + const std::string NEW_CONTENT = + CONTENT.substr(0, AFTER_KEY) + buildBlock(spawns) + TAIL; + + const auto TMP_PATH = path + ".tmp"; + { + std::ofstream out(TMP_PATH, std::ios::binary | std::ios::trunc); + if (!out) { return false; } + out.write(NEW_CONTENT.data(), static_cast(NEW_CONTENT.size())); + out.close(); + if (!out) { + std::error_code ec; + std::filesystem::remove(TMP_PATH, ec); + return false; + } + } + + std::error_code ec; + std::filesystem::rename(TMP_PATH, path, ec); + if (ec) { + std::filesystem::remove(TMP_PATH, ec); + return false; + } + return true; + } + +} // namespace EntityWriter + +#endif // _DEBUG diff --git a/source/game/entities/entity_writer.hpp b/source/game/entities/entity_writer.hpp new file mode 100644 index 0000000..bf003f6 --- /dev/null +++ b/source/game/entities/entity_writer.hpp @@ -0,0 +1,50 @@ +#pragma once + +#ifdef _DEBUG + +#include +#include +#include + +// Reescriu NOMÉS el bloc `entities:` d'un YAML de nivell, preservant byte +// a byte la resta del fitxer: comentaris, ordre de claus, format del +// `geometry:` i `data:`, encoding i line endings entre línies no tocades. +// Pensat exclusivament per a l'editor (Debug). +// +// Format de sortida (estable, llegible a l'editor de text): +// entities: +// - type: torch +// position: [4, 2] +// - type: sarcophagus +// sarc_position: [0, 0] +// - type: door +// +// Semàntica del bloc origen: +// - Línia de capçalera: primera aparició de "entities:" a columna 0. +// Si manca, el writer FALLA i no toca el fitxer. +// - Contingut: línies indentades (espai o tab). Una línia buida o una +// línia a columna 0 marquen la fi del bloc. +// +// Si el fitxer original no té el bloc `entities:`, el writer no inventa +// ubicació: retorna false per que el caller decidisca (mostrar error, +// avortar, etc.) — no és funció del writer escollir l'estil del YAML. +// +// Escriptura atòmica: via `.tmp` + `std::filesystem::rename`. Si +// l'escriptura falla a meitat, el YAML original queda intacte. +namespace EntityWriter { + + // Cada spawn duplica l'estructura mínima del que el lector espera: + // type sempre, i un dels (position, sarc_position) o cap (cas door). + struct Spawn { + std::string type; + std::optional col; + std::optional row; + std::optional sarc_col; + std::optional sarc_row; + }; + + auto saveEntitiesBlock(const std::string& path, const std::vector& spawns) -> bool; + +} // namespace EntityWriter + +#endif // _DEBUG diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index ca0fd7a..6d56735 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1147,7 +1147,7 @@ void GameScene::update(int delta_ms) { // sessió d'edició parteix del fitxer, no de l'estat runtime previ). if (pause_.state == PauseState::IDLE && detectEditorToggle()) { const bool WAS_ACTIVE = editor_.active(); - editor_.toggle(tilemap_); + editor_.toggle(tilemap_, entity_manager_); if (WAS_ACTIVE) { entity_manager_.reload(Levels::LevelManager::get().currentPath()); const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);