From ff65a191c1ba2beb256caabff8b8f315a3293fbd Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:05:59 +0200 Subject: [PATCH 01/15] logLoadError sense [[noreturn]]: throw queda al call site --- source/core/resources/resource_cache.cpp | 49 +++++++++++++++--------- source/core/resources/resource_cache.hpp | 2 +- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp index 8539532..c245b93 100644 --- a/source/core/resources/resource_cache.cpp +++ b/source/core/resources/resource_cache.cpp @@ -244,14 +244,13 @@ namespace Resource { return rooms_; } - // Helper para lanzar errores de carga con formato consistente - [[noreturn]] void Cache::throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) { + // Helper para registrar errores de carga con formato consistente. + // El rethrow es responsabilitat del catch que crida la funció. + void Cache::logLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) { std::cerr << "\n[ ERROR ] Failed to load " << asset_type << ": " << getFileName(file_path) << '\n'; std::cerr << "[ ERROR ] Path: " << file_path << '\n'; std::cerr << "[ ERROR ] Reason: " << e.what() << '\n'; std::cerr << "[ ERROR ] Check config/assets.yaml configuration\n"; - // cppcheck-suppress rethrowNoCurrentException -- helper [[noreturn]] invocado desde dentro de un catch; cppcheck no puede ver que el rethrow es válido a través de la llamada. - throw; } // Carga los sonidos @@ -284,7 +283,8 @@ namespace Resource { printWithDots("Sound : ", name, "[ LOADED ]"); updateLoadingProgress(); } catch (const std::exception& e) { - throwLoadError("SOUND", l, e); + logLoadError("SOUND", l, e); + throw; } } } @@ -319,7 +319,8 @@ namespace Resource { printWithDots("Music : ", name, "[ LOADED ]"); updateLoadingProgress(1); } catch (const std::exception& e) { - throwLoadError("MUSIC", l, e); + logLoadError("MUSIC", l, e); + throw; } } } @@ -337,7 +338,8 @@ namespace Resource { surfaces_.back().surface->setTransparentColor(0); updateLoadingProgress(); } catch (const std::exception& e) { - throwLoadError("BITMAP", l, e); + logLoadError("BITMAP", l, e); + throw; } } @@ -360,7 +362,8 @@ namespace Resource { palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)}); updateLoadingProgress(); } catch (const std::exception& e) { - throwLoadError("PALETTE", l, e); + logLoadError("PALETTE", l, e); + throw; } } } @@ -377,7 +380,8 @@ namespace Resource { text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)}); updateLoadingProgress(); } catch (const std::exception& e) { - throwLoadError("FONT", l, e); + logLoadError("FONT", l, e); + throw; } } } @@ -403,7 +407,8 @@ namespace Resource { printWithDots("Animation : ", name, "[ LOADED ]"); updateLoadingProgress(); } catch (const std::exception& e) { - throwLoadError("ANIMATION", l, e); + logLoadError("ANIMATION", l, e); + throw; } } } @@ -421,7 +426,8 @@ namespace Resource { printWithDots("Room : ", name, "[ LOADED ]"); updateLoadingProgress(); } catch (const std::exception& e) { - throwLoadError("ROOM", l, e); + logLoadError("ROOM", l, e); + throw; } } } @@ -601,7 +607,8 @@ namespace Resource { it->sound = sound; std::cout << "[lazy] Sound loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("SOUND", path, e); + logLoadError("SOUND", path, e); + throw; } } @@ -618,7 +625,8 @@ namespace Resource { it->music = music; std::cout << "[lazy] Music loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("MUSIC", path, e); + logLoadError("MUSIC", path, e); + throw; } } @@ -638,7 +646,8 @@ namespace Resource { } std::cout << "[lazy] Surface loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("BITMAP", path, e); + logLoadError("BITMAP", path, e); + throw; } } @@ -651,7 +660,8 @@ namespace Resource { it->loaded = true; std::cout << "[lazy] Palette loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("PALETTE", path, e); + logLoadError("PALETTE", path, e); + throw; } } @@ -663,7 +673,8 @@ namespace Resource { it->text_file = Text::loadTextFile(path); std::cout << "[lazy] TextFile loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("FONT", path, e); + logLoadError("FONT", path, e); + throw; } } @@ -677,7 +688,8 @@ namespace Resource { it->yaml_data = bytes; std::cout << "[lazy] Animation loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("ANIMATION", path, e); + logLoadError("ANIMATION", path, e); + throw; } } @@ -689,7 +701,8 @@ namespace Resource { it->room = std::make_shared(Room::loadYAML(path)); std::cout << "[lazy] Room loaded: " << name << '\n'; } catch (const std::exception& e) { - throwLoadError("ROOM", path, e); + logLoadError("ROOM", path, e); + throw; } } diff --git a/source/core/resources/resource_cache.hpp b/source/core/resources/resource_cache.hpp index 1c21a43..1c63dde 100644 --- a/source/core/resources/resource_cache.hpp +++ b/source/core/resources/resource_cache.hpp @@ -88,7 +88,7 @@ namespace Resource { void updateLoadingProgress(int steps = 5); // Helper para mensajes de error de carga - [[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e); + static void logLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e); // Constructor y destructor explicit Cache(LoadingMode mode); From 38d927a7a1590c3be1dd1ab14866ca8aab3b1bf1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:07:00 +0200 Subject: [PATCH 02/15] neteja NOLINT obsolets (de 77 a 53) --- source/core/rendering/pixel_reveal.cpp | 2 +- source/core/rendering/screen.cpp | 2 +- source/core/rendering/surface.cpp | 22 +++++++++++----------- source/core/rendering/text.cpp | 4 ++-- source/core/resources/resource_loader.cpp | 6 +++--- source/core/system/debug.cpp | 2 +- source/game/entities/path_enemy.cpp | 2 +- source/game/entities/player.cpp | 2 +- source/game/gameplay/room.cpp | 2 +- source/game/gameplay/room_format.hpp | 2 +- source/game/gameplay/room_tracker.cpp | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/source/core/rendering/pixel_reveal.cpp b/source/core/rendering/pixel_reveal.cpp index 8beec6e..a5754f7 100644 --- a/source/core/rendering/pixel_reveal.cpp +++ b/source/core/rendering/pixel_reveal.cpp @@ -66,7 +66,7 @@ PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float s } // Actualiza el estado del revelado -void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const) +void PixelReveal::update(float time_active) { // En modo normal revela (pone transparente); en modo inverso cubre (pone negro) const auto PIXEL_COLOR = reverse_ ? 0 : 255; diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 83cef7c..0735d37 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -808,7 +808,7 @@ auto Screen::initSDLVideo() -> bool { // Registra los callbacks nativos de Emscripten que restauran el canvas cuando // SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op. -void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static) +void Screen::registerEmscriptenEventCallbacks() { #ifdef __EMSCRIPTEN__ // NO registramos resize callback. En móvil, el scroll hace que el navegador // oculte/muestre la barra de URL, disparando un resize del DOM por cada scroll, diff --git a/source/core/rendering/surface.cpp b/source/core/rendering/surface.cpp index f64f078..9ecbd5b 100644 --- a/source/core/rendering/surface.cpp +++ b/source/core/rendering/surface.cpp @@ -148,14 +148,14 @@ void Surface::setColor(int index, Uint32 color) { } // Rellena la superficie con un color -void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::clear(Uint8 color) { const size_t TOTAL_PIXELS = static_cast(surface_data_->width) * static_cast(surface_data_->height); Uint8* data_ptr = surface_data_->data.get(); std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color); } // Pone un pixel en la SurfaceData -void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::putPixel(int x, int y, Uint8 color) { if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) { return; // Coordenadas fuera de rango } @@ -168,7 +168,7 @@ void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-conve auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * surface_data_->width)]; } // Dibuja un rectangulo relleno -void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // Limitar los valores del rectángulo al tamaño de la superficie float x_start = std::max(0.0F, rect->x); float y_start = std::max(0.0F, rect->y); @@ -185,7 +185,7 @@ void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readabil } // Dibuja el borde de un rectangulo -void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // Limitar los valores del rectángulo al tamaño de la superficie float x_start = std::max(0.0F, rect->x); float y_start = std::max(0.0F, rect->y); @@ -212,7 +212,7 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re } // Dibuja una linea (Bresenham en enteros) -void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { int ix1 = static_cast(std::lround(x1)); int iy1 = static_cast(std::lround(y1)); const int IX2 = static_cast(std::lround(x2)); @@ -247,7 +247,7 @@ void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { / } } -void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const) +void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData(); // Aplicar render offset (usado por transiciones entre pantallas) @@ -533,7 +533,7 @@ void Surface::toARGBBuffer(Uint32* buffer) const { } // Vuelca la superficie a una textura -void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) { throw std::runtime_error("Renderer or texture is null."); } @@ -575,7 +575,7 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // } // Vuelca la superficie a una textura -void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { // NOLINT(readability-convert-member-functions-to-static) +void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) { throw std::runtime_error("Renderer or texture is null."); } @@ -624,7 +624,7 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR } // Realiza un efecto de fundido en la paleta principal -auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static) +auto Surface::fadePalette() -> bool { static constexpr int PALETTE_SIZE = 19; static_assert(std::tuple_size_v >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation."); @@ -641,7 +641,7 @@ auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-func } // Realiza un efecto de fundido en la paleta secundaria -auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static) +auto Surface::fadeSubPalette(Uint32 delay) -> bool { // Variable estática para almacenar el último tick static Uint32 last_tick_ = 0; @@ -672,4 +672,4 @@ auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-conv } // Restaura la sub paleta a su estado original -void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static) +void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 20540a6..bf641fd 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -167,7 +167,7 @@ void Text::write(int x, int y, const std::string& text, int kerning, int lenght) } // Escribe el texto en una surface -auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr { // NOLINT(readability-make-member-function-const) +auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr { auto width = length(text, kerning) * zoom; auto height = box_height_ * zoom; auto surface = std::make_shared(width, height); @@ -181,7 +181,7 @@ auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std } // Escribe el texto con extras en una surface -auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr { // NOLINT(readability-make-member-function-const) +auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr { auto width = Text::length(text, kerning) + shadow_distance; auto height = box_height_ + shadow_distance; auto surface = std::make_shared(width, height); diff --git a/source/core/resources/resource_loader.cpp b/source/core/resources/resource_loader.cpp index f56a4e6..3b30fcf 100644 --- a/source/core/resources/resource_loader.cpp +++ b/source/core/resources/resource_loader.cpp @@ -53,7 +53,7 @@ namespace Resource { } // Load a resource - auto Loader::loadResource(const std::string& filename) -> std::vector { // NOLINT(readability-make-member-function-const) + auto Loader::loadResource(const std::string& filename) -> std::vector { if (!initialized_) { std::cerr << "Loader: Not initialized\n"; return {}; @@ -81,7 +81,7 @@ namespace Resource { } // Check if a resource exists - auto Loader::resourceExists(const std::string& filename) -> bool { // NOLINT(readability-make-member-function-const) + auto Loader::resourceExists(const std::string& filename) -> bool { if (!initialized_) { return false; } @@ -158,7 +158,7 @@ namespace Resource { if (checksum == 0) { std::cerr << "Loader: Pack checksum is zero (invalid)\n"; - return false; // NOLINT(readability-simplify-boolean-expr) + return false; } std::cout << "Loader: Pack checksum: 0x" << std::hex << checksum << std::dec diff --git a/source/core/system/debug.cpp b/source/core/system/debug.cpp index 1b13369..820e076 100644 --- a/source/core/system/debug.cpp +++ b/source/core/system/debug.cpp @@ -32,7 +32,7 @@ auto Debug::get() -> Debug* { } // Dibuja en pantalla -void Debug::render() { // NOLINT(readability-make-member-function-const) +void Debug::render() { auto text = Resource::Cache::get()->getText("aseprite"); int y = y_; int w = 0; diff --git a/source/game/entities/path_enemy.cpp b/source/game/entities/path_enemy.cpp index c714559..7df3472 100644 --- a/source/game/entities/path_enemy.cpp +++ b/source/game/entities/path_enemy.cpp @@ -38,7 +38,7 @@ void PathEnemy::resetToInitialPosition(const Data& data) { #endif // Comprueba si ha llegado al limite del recorrido para darse media vuelta -void PathEnemy::checkPath() { // NOLINT(readability-make-member-function-const) +void PathEnemy::checkPath() { if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) { // Recoloca if (sprite_->getPosX() > x2_) { diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index f221b62..02f40bf 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -632,7 +632,7 @@ void Player::placeSprite() { sprite_->setPos(x_, y_); } -void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const) +void Player::animate(float delta_time) { if (state_ == State::ON_AIR) { turning_ = false; const bool NEAR_PEAK = vy_ > JUMP_VELOCITY * 0.5F && vy_ < -JUMP_VELOCITY * 0.5F; diff --git a/source/game/gameplay/room.cpp b/source/game/gameplay/room.cpp index c915a01..cdf0cc0 100644 --- a/source/game/gameplay/room.cpp +++ b/source/game/gameplay/room.cpp @@ -219,7 +219,7 @@ void Room::setBgColor(Uint8 bg_color) { #endif // Actualiza las variables y objetos de la habitación -void Room::update(float delta_time) { // NOLINT(readability-make-member-function-const) +void Room::update(float delta_time) { if (is_paused_) { // Si está en modo pausa no se actualiza nada return; diff --git a/source/game/gameplay/room_format.hpp b/source/game/gameplay/room_format.hpp index da7fcce..94f72e7 100644 --- a/source/game/gameplay/room_format.hpp +++ b/source/game/gameplay/room_format.hpp @@ -69,7 +69,7 @@ class RoomFormat { // --- Parsing helpers (siempre disponibles, los usa loadYAML) --- static auto convertRoomConnection(const std::string& value) -> std::string; static auto convertAutoSurface(const fkyaml::node& node) -> int; - static auto flattenTilemap(const std::vector>& tilemap_2d) -> std::vector; // NOLINT(readability-avoid-const-params-in-decls) + static auto flattenTilemap(const std::vector>& tilemap_2d) -> std::vector; static void parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name); static void parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room); static void parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose); diff --git a/source/game/gameplay/room_tracker.cpp b/source/game/gameplay/room_tracker.cpp index 199b150..ac7e98b 100644 --- a/source/game/gameplay/room_tracker.cpp +++ b/source/game/gameplay/room_tracker.cpp @@ -13,7 +13,7 @@ auto RoomTracker::addRoom(const std::string& name) -> bool { if (!hasBeenVisited(name)) { // En caso contrario añádela a la lista rooms_.push_back(name); - return true; // NOLINT(readability-simplify-boolean-expr) + return true; } return false; From c7b88cd05f5a4ab8d6b0b8fefd5e49436bce1d58 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:22:06 +0200 Subject: [PATCH 03/15] neteja NOLINT identifier-naming i lambdes a metodes privats Game --- source/core/rendering/gif.cpp | 4 +- source/core/resources/resource_pack.cpp | 6 +-- source/game/editor/map_editor.hpp | 2 +- source/game/gameplay/door_manager.cpp | 2 +- source/game/gameplay/enemy_manager.cpp | 2 +- source/game/gameplay/item_manager.cpp | 2 +- source/game/gameplay/key_manager.cpp | 2 +- source/game/scenes/game.cpp | 68 ++++++++++++------------- source/game/scenes/game.hpp | 63 ++++++++++++----------- source/utils/color.hpp | 2 +- 10 files changed, 77 insertions(+), 76 deletions(-) diff --git a/source/core/rendering/gif.cpp b/source/core/rendering/gif.cpp index 0b5c8c1..ef0ebd6 100644 --- a/source/core/rendering/gif.cpp +++ b/source/core/rendering/gif.cpp @@ -15,7 +15,7 @@ namespace GIF { } // Inicializa el diccionario LZW con los valores iniciales - inline void initializeDictionary(std::vector& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming) + inline void initializeDictionary(std::vector& dictionary, int code_length, int& dictionary_ind) { int size = 1 << code_length; dictionary.resize(1 << (code_length + 1)); for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) { @@ -55,7 +55,7 @@ namespace GIF { } // Agrega una nueva entrada al diccionario - inline void addDictionaryEntry(std::vector& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming) + inline void addDictionaryEntry(std::vector& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { uint8_t first_byte; if (code == dictionary_ind) { first_byte = findFirstByte(dictionary, prev); diff --git a/source/core/resources/resource_pack.cpp b/source/core/resources/resource_pack.cpp index 8fb42b9..9a832f9 100644 --- a/source/core/resources/resource_pack.cpp +++ b/source/core/resources/resource_pack.cpp @@ -19,7 +19,7 @@ namespace Resource { } // XOR encryption (symmetric - same function for encrypt/decrypt) - void Pack::encryptData(std::vector& data, const std::string& key) { // NOLINT(readability-identifier-naming) + void Pack::encryptData(std::vector& data, const std::string& key) { if (key.empty()) { return; } @@ -28,7 +28,7 @@ namespace Resource { } } - void Pack::decryptData(std::vector& data, const std::string& key) { // NOLINT(readability-identifier-naming) + void Pack::decryptData(std::vector& data, const std::string& key) { // XOR is symmetric encryptData(data, key); } @@ -79,7 +79,7 @@ namespace Resource { // Add all files from a directory recursively auto Pack::addDirectory(const std::string& dir_path, const std::string& base_path) -> bool { - namespace fs = std::filesystem; // NOLINT(readability-identifier-naming) + namespace fs = std::filesystem; if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) { std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n'; diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index deddc8d..1894e60 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -149,7 +149,7 @@ class MapEditor { [[nodiscard]] auto getSelectionType() const -> EntityType { return selection_.type; } private: - static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado + static MapEditor* instance_; MapEditor(); // Constructor ~MapEditor(); // Destructor diff --git a/source/game/gameplay/door_manager.cpp b/source/game/gameplay/door_manager.cpp index 1482e0f..21528af 100644 --- a/source/game/gameplay/door_manager.cpp +++ b/source/game/gameplay/door_manager.cpp @@ -17,7 +17,7 @@ DoorManager::DoorManager(std::string room_id, SolidActorManager* solid_actors) // Añade una puerta y la registra en el SolidActorManager. El bit // BLOCKS_PLAYER del propio Door determina si bloquea al Player (se setea en // el constructor si la puerta no arranca ya abierta desde el DoorTracker). -void DoorManager::addDoor(std::shared_ptr door) { // NOLINT(readability-identifier-naming) +void DoorManager::addDoor(std::shared_ptr door) { solid_actors_->registerActor(door.get()); doors_.push_back(std::move(door)); } diff --git a/source/game/gameplay/enemy_manager.cpp b/source/game/gameplay/enemy_manager.cpp index 70d63fd..6b28e0e 100644 --- a/source/game/gameplay/enemy_manager.cpp +++ b/source/game/gameplay/enemy_manager.cpp @@ -6,7 +6,7 @@ #include "utils/utils.hpp" // Para checkCollision // Añade un enemigo a la colección -void EnemyManager::addEnemy(std::shared_ptr enemy) { // NOLINT(readability-identifier-naming) +void EnemyManager::addEnemy(std::shared_ptr enemy) { enemies_.push_back(std::move(enemy)); } diff --git a/source/game/gameplay/item_manager.cpp b/source/game/gameplay/item_manager.cpp index 480b41c..5a8b59b 100644 --- a/source/game/gameplay/item_manager.cpp +++ b/source/game/gameplay/item_manager.cpp @@ -15,7 +15,7 @@ ItemManager::ItemManager(std::string room_id, std::shared_ptr } // Añade un item a la colección -void ItemManager::addItem(std::shared_ptr item) { // NOLINT(readability-identifier-naming) +void ItemManager::addItem(std::shared_ptr item) { items_.push_back(std::move(item)); } diff --git a/source/game/gameplay/key_manager.cpp b/source/game/gameplay/key_manager.cpp index e287d5c..8d21a36 100644 --- a/source/game/gameplay/key_manager.cpp +++ b/source/game/gameplay/key_manager.cpp @@ -15,7 +15,7 @@ KeyManager::KeyManager(std::string room_id) } // Añade una llave a la colección -void KeyManager::addKey(std::shared_ptr key) { // NOLINT(readability-identifier-naming) +void KeyManager::addKey(std::shared_ptr key) { keys_.push_back(std::move(key)); } diff --git a/source/game/scenes/game.cpp b/source/game/scenes/game.cpp index c491fe7..d14cf41 100644 --- a/source/game/scenes/game.cpp +++ b/source/game/scenes/game.cpp @@ -798,36 +798,41 @@ auto Game::getOrCreateRoom(const std::string& room_path) -> std::shared_ptr const std::vector* { + auto name = room_->getRoom(border); + if (name == "0") { return nullptr; } + return &getOrCreateRoom(name)->getCollisionTileMap(); +} + +// Obtiene el collision tilemap de una room diagonal (A→B o C→D) +auto Game::getDiagCollision(Room::Border first, Room::Border second) -> const std::vector* { + // Camino 1: room en dirección 'first', luego su adyacente en dirección 'second' + auto name1 = room_->getRoom(first); + if (name1 != "0") { + auto r = getOrCreateRoom(name1); + auto name2 = r->getRoom(second); + if (name2 != "0") { return &getOrCreateRoom(name2)->getCollisionTileMap(); } + } + // Camino 2: room en dirección 'second', luego su adyacente en dirección 'first' + auto name3 = room_->getRoom(second); + if (name3 != "0") { + auto r = getOrCreateRoom(name3); + auto name4 = r->getRoom(first); + if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); } + } + return nullptr; +} + +// SolidActorManager de la room adyacente (nullptr si no existe) +auto Game::getAdjacentSolidActors(Room::Border border) -> SolidActorManager* { + auto name = room_->getRoom(border); + if (name == "0") { return nullptr; } + return &getOrCreateRoom(name)->getSolidActors(); +} + // Construye el tilemap extendido de la room actual con los bordes de las adyacentes void Game::buildCollisionBorders() { - // NOLINTBEGIN(readability-identifier-naming) -- lambdas locales: se leen como llamadas a función, no son constantes. - // Helper: obtiene el collision tilemap de una room adyacente (nullptr si no existe) - auto getAdjacentCollision = [&](Room::Border b) -> const std::vector* { - auto name = room_->getRoom(b); - if (name == "0") { return nullptr; } - return &getOrCreateRoom(name)->getCollisionTileMap(); - }; - - // Helper: obtiene el collision tilemap de una room diagonal (A→B o C→D) - auto getDiagCollision = [&](Room::Border first, Room::Border second) -> const std::vector* { - // Camino 1: room en dirección 'first', luego su adyacente en dirección 'second' - auto name1 = room_->getRoom(first); - if (name1 != "0") { - auto r = getOrCreateRoom(name1); - auto name2 = r->getRoom(second); - if (name2 != "0") { return &getOrCreateRoom(name2)->getCollisionTileMap(); } - } - // Camino 2: room en dirección 'second', luego su adyacente en dirección 'first' - auto name3 = room_->getRoom(second); - if (name3 != "0") { - auto r = getOrCreateRoom(name3); - auto name4 = r->getRoom(first); - if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); } - } - return nullptr; - }; - // NOLINTEND(readability-identifier-naming) - CollisionMap::AdjacentData adj; adj.top = getAdjacentCollision(Room::Border::TOP); adj.bottom = getAdjacentCollision(Room::Border::BOTTOM); @@ -845,13 +850,6 @@ void Game::buildCollisionBorders() { // que los sweeps del Player vean AABBs dinámicos (puertas, plataformas) // de la room vecina cuando está cerca del borde, sin tener que esperar // a una transición completa de room. - // NOLINTNEXTLINE(readability-identifier-naming) -- lambda local: se lee como función, no es constante. - auto getAdjacentSolidActors = [&](Room::Border b) -> SolidActorManager* { - auto name = room_->getRoom(b); - if (name == "0") { return nullptr; } - return &getOrCreateRoom(name)->getSolidActors(); - }; - SolidActorManager::AdjacentActors sadj; sadj.upper = getAdjacentSolidActors(Room::Border::TOP); sadj.lower = getAdjacentSolidActors(Room::Border::BOTTOM); diff --git a/source/game/scenes/game.hpp b/source/game/scenes/game.hpp index 0b5d004..6cb3786 100644 --- a/source/game/scenes/game.hpp +++ b/source/game/scenes/game.hpp @@ -57,36 +57,39 @@ class Game { }; // --- Métodos --- - static void handleEvents(); // Comprueba los eventos de la cola - void transitionToState(State new_state); // Cambia al estado especificado y resetea los timers - void updatePlaying(float delta_time); // Actualiza el juego en estado PLAYING - void updateBlackScreen(float delta_time); // Actualiza el juego en estado BLACK_SCREEN - void updateGameOver(float delta_time); // Actualiza el juego en estado GAME_OVER - void updateFadeToEnding(float delta_time); // Actualiza el juego en estado FADE_TO_ENDING - void updatePostFadeEnding(float delta_time); // Actualiza el juego en estado POST_FADE_ENDING - void renderPlaying(); // Renderiza el juego en estado PLAYING (directo a pantalla) - static void renderBlackScreen(); // Renderiza el juego en estado BLACK_SCREEN (pantalla negra) - static void renderGameOver(); // Renderiza el juego en estado GAME_OVER (pantalla negra) - void renderFadeToEnding(); // Renderiza el juego en estado FADE_TO_ENDING (via backbuffer) - static void renderPostFadeEnding(); // Renderiza el juego en estado POST_FADE_ENDING (pantalla negra) - auto changeRoom(const std::string& room_path) -> bool; // Cambia de habitación - auto getOrCreateRoom(const std::string& room_path) -> std::shared_ptr; // Obtiene una habitación del caché o la crea - void buildCollisionBorders(); // Rellena el tilemap extendido de la room actual con bordes adyacentes - void updateAdjacentRooms(float delta_time); // Actualiza enemigos de las habitaciones adyacentes - void handleInput(); // Comprueba el teclado - void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua - auto checkPlayerAndEnemies() -> bool; // Comprueba las colisiones del jugador con los enemigos - void checkPlayerAndItems(); // Comprueba las colisiones del jugador con los objetos - void checkPlayerAndKeys(); // Comprueba las colisiones del jugador con las llaves - void checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo - void killPlayer(); // Mata al jugador - void togglePause(); // Pone el juego en pausa - void initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr room); // Inicializa al jugador - void endTransition(); // Finaliza la transición entre pantallas - void keepMusicPlaying(); // Hace sonar la música (safety net, delega en updateMusicForRoom) - void updateMusicForRoom(); // Sincroniza la música con la zona de la room actual - void demoInit(); // DEMO MODE: Inicializa las variables para el modo demo - void demoCheckRoomChange(float delta_time); // DEMO MODE: Comprueba si se ha de cambiar de habitación + static void handleEvents(); // Comprueba los eventos de la cola + void transitionToState(State new_state); // Cambia al estado especificado y resetea los timers + void updatePlaying(float delta_time); // Actualiza el juego en estado PLAYING + void updateBlackScreen(float delta_time); // Actualiza el juego en estado BLACK_SCREEN + void updateGameOver(float delta_time); // Actualiza el juego en estado GAME_OVER + void updateFadeToEnding(float delta_time); // Actualiza el juego en estado FADE_TO_ENDING + void updatePostFadeEnding(float delta_time); // Actualiza el juego en estado POST_FADE_ENDING + void renderPlaying(); // Renderiza el juego en estado PLAYING (directo a pantalla) + static void renderBlackScreen(); // Renderiza el juego en estado BLACK_SCREEN (pantalla negra) + static void renderGameOver(); // Renderiza el juego en estado GAME_OVER (pantalla negra) + void renderFadeToEnding(); // Renderiza el juego en estado FADE_TO_ENDING (via backbuffer) + static void renderPostFadeEnding(); // Renderiza el juego en estado POST_FADE_ENDING (pantalla negra) + auto changeRoom(const std::string& room_path) -> bool; // Cambia de habitación + auto getOrCreateRoom(const std::string& room_path) -> std::shared_ptr; // Obtiene una habitación del caché o la crea + void buildCollisionBorders(); // Rellena el tilemap extendido de la room actual con bordes adyacentes + auto getAdjacentCollision(Room::Border border) -> const std::vector*; // Collision tilemap de la room adyacente o nullptr + auto getDiagCollision(Room::Border first, Room::Border second) -> const std::vector*; // Collision tilemap de la room diagonal o nullptr + auto getAdjacentSolidActors(Room::Border border) -> SolidActorManager*; // SolidActorManager de la room adyacente o nullptr + void updateAdjacentRooms(float delta_time); // Actualiza enemigos de las habitaciones adyacentes + void handleInput(); // Comprueba el teclado + void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua + auto checkPlayerAndEnemies() -> bool; // Comprueba las colisiones del jugador con los enemigos + void checkPlayerAndItems(); // Comprueba las colisiones del jugador con los objetos + void checkPlayerAndKeys(); // Comprueba las colisiones del jugador con las llaves + void checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo + void killPlayer(); // Mata al jugador + void togglePause(); // Pone el juego en pausa + void initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr room); // Inicializa al jugador + void endTransition(); // Finaliza la transición entre pantallas + void keepMusicPlaying(); // Hace sonar la música (safety net, delega en updateMusicForRoom) + void updateMusicForRoom(); // Sincroniza la música con la zona de la room actual + void demoInit(); // DEMO MODE: Inicializa las variables para el modo demo + void demoCheckRoomChange(float delta_time); // DEMO MODE: Comprueba si se ha de cambiar de habitación #ifdef _DEBUG static void renderDebugInfo(); // Pone la información de debug en pantalla void handleDebugEvents(const SDL_Event& event); // Comprueba los eventos diff --git a/source/utils/color.hpp b/source/utils/color.hpp index 4aa9da0..0500882 100644 --- a/source/utils/color.hpp +++ b/source/utils/color.hpp @@ -78,7 +78,7 @@ class Color { * @param color Color del enum Cpc * @return Índice de paleta (Uint8) */ - static constexpr auto getIndex(Cpc color) -> Uint8 { // NOLINT(readability-identifier-naming) + static constexpr auto getIndex(Cpc color) -> Uint8 { return static_cast(color); } From 058f9d7630adc300f2550dae2d5ed9ea874109f5 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:26:36 +0200 Subject: [PATCH 04/15] justificacio NOLINTs i neteja obsolet a Title::~Title --- source/game/gameplay/room.hpp | 3 ++- source/game/scenes/title.cpp | 2 +- source/game/ui/console_commands.cpp | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/source/game/gameplay/room.hpp b/source/game/gameplay/room.hpp index b4820e3..eb9706f 100644 --- a/source/game/gameplay/room.hpp +++ b/source/game/gameplay/room.hpp @@ -61,7 +61,8 @@ class Room { // Constructor y destructor Room(const std::string& room_path, std::shared_ptr data); - ~Room(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) + // NOLINTNEXTLINE(modernize-use-equals-default,performance-trivially-destructible) -- destructor definit al .cpp perquè la classe té unique_ptr a tipus forward-declared; no es pot fer = default ni eliminar a l'header sense incloure tots els headers transitivament. + ~Room(); // --- Funciones --- [[nodiscard]] auto getNumber() const -> const std::string& { return number_; } diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index f9b6c6d..492b0ec 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -47,7 +47,7 @@ Title::Title() } // Destructor -Title::~Title() { // NOLINT(modernize-use-equals-default) +Title::~Title() { title_surface_->resetSubPalette(); } diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index 788bce0..fbd7ba8 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -69,8 +69,10 @@ static auto applyPreset(const std::vector& args) -> std::string { if (COUNT == 0) { return "No " + SHADER_LABEL + " presets available"; } const auto PRESET_NAME = [&]() -> std::string { - const auto& name = IS_CRTPI ? presets_crtpi[static_cast(current_idx)].name // NOLINT(clang-analyzer-core.CallAndMessage) - : presets_postfx[static_cast(current_idx)].name; // NOLINT(clang-analyzer-core.CallAndMessage) + // NOLINTBEGIN(clang-analyzer-core.CallAndMessage) -- fals positiu: la guard de la línia 69 garanteix COUNT > 0, així que current_idx és sempre dins de rang. + const auto& name = IS_CRTPI ? presets_crtpi[static_cast(current_idx)].name + : presets_postfx[static_cast(current_idx)].name; + // NOLINTEND(clang-analyzer-core.CallAndMessage) return prettyName(name); }; From 0d876b902db13c62d4741ab13ece1a45258d8d60 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:34:25 +0200 Subject: [PATCH 05/15] refactor sdl3gpu_shader: extreu createPostfxVertexShader i createPostfxLikePipeline --- .../core/rendering/sdl3gpu/sdl3gpu_shader.cpp | 115 ++++++++---------- .../core/rendering/sdl3gpu/sdl3gpu_shader.hpp | 6 +- 2 files changed, 52 insertions(+), 69 deletions(-) diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index a67de39..f247496 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -162,25 +162,30 @@ namespace Rendering { } // --------------------------------------------------------------------------- - // createPipeline + // createPostfxVertexShader — fullscreen-triangle vertex compartit per tots els pipelines // --------------------------------------------------------------------------- - auto SDL3GPUShader::createPipeline() -> bool { // NOLINT(readability-function-cognitive-complexity) - const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_); - - // ---- PostFX pipeline (scene/scaled → swapchain) ---- + auto SDL3GPUShader::createPostfxVertexShader() -> SDL_GPUShader* { #ifdef __APPLE__ - SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + return createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); #else - SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + return createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); #endif + } - if ((vert == nullptr) || (frag == nullptr)) { - SDL_Log("SDL3GPUShader: failed to compile PostFX shaders"); - if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); } - if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); } - return false; + // --------------------------------------------------------------------------- + // createPostfxLikePipeline — empaqueta vert(postfx) + frag dado + target en un pipeline. + // Pren ownership de `frag` (el libera abans de retornar). + // --------------------------------------------------------------------------- + auto SDL3GPUShader::createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline* { + if (frag == nullptr) { + SDL_Log("SDL3GPUShader: %s frag shader is null", debug_name); + return nullptr; + } + SDL_GPUShader* vert = createPostfxVertexShader(); + if (vert == nullptr) { + SDL_Log("SDL3GPUShader: %s vert shader creation failed", debug_name); + SDL_ReleaseGPUShader(device_, frag); + return nullptr; } SDL_GPUColorTargetBlendState no_blend = {}; @@ -188,30 +193,44 @@ namespace Rendering { no_blend.enable_color_write_mask = false; SDL_GPUColorTargetDescription color_target = {}; - color_target.format = SWAPCHAIN_FMT; + color_target.format = format; color_target.blend_state = no_blend; SDL_GPUVertexInputState no_input = {}; - SDL_GPUGraphicsPipelineCreateInfo pipe_info = {}; - pipe_info.vertex_shader = vert; - pipe_info.fragment_shader = frag; - pipe_info.vertex_input_state = no_input; - pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - pipe_info.target_info.num_color_targets = 1; - pipe_info.target_info.color_target_descriptions = &color_target; + SDL_GPUGraphicsPipelineCreateInfo info = {}; + info.vertex_shader = vert; + info.fragment_shader = frag; + info.vertex_input_state = no_input; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.target_info.num_color_targets = 1; + info.target_info.color_target_descriptions = &color_target; - pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info); + SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info); SDL_ReleaseGPUShader(device_, vert); SDL_ReleaseGPUShader(device_, frag); - if (pipeline_ == nullptr) { - SDL_Log("SDL3GPUShader: PostFX pipeline creation failed: %s", SDL_GetError()); - return false; + if (pipeline == nullptr) { + SDL_Log("SDL3GPUShader: %s pipeline creation failed: %s", debug_name, SDL_GetError()); } + return pipeline; + } - return true; + // --------------------------------------------------------------------------- + // createPipeline — pipeline únic PostFX → swapchain + // --------------------------------------------------------------------------- + auto SDL3GPUShader::createPipeline() -> bool { + const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_); + +#ifdef __APPLE__ + SDL_GPUShader* postfx_frag = createShaderMSL(device_, Rendering::Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); +#else + SDL_GPUShader* postfx_frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); +#endif + + pipeline_ = createPostfxLikePipeline(postfx_frag, SWAPCHAIN_FMT, "PostFX"); + return pipeline_ != nullptr; } // --------------------------------------------------------------------------- @@ -222,51 +241,13 @@ namespace Rendering { // --------------------------------------------------------------------------- auto SDL3GPUShader::createCrtPiPipeline() -> bool { const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_); - #ifdef __APPLE__ - SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #else - SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #endif - - if ((vert == nullptr) || (frag == nullptr)) { - SDL_Log("SDL3GPUShader: failed to compile CrtPi shaders"); - if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); } - if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); } - return false; - } - - SDL_GPUColorTargetBlendState no_blend = {}; - no_blend.enable_blend = false; - no_blend.enable_color_write_mask = false; - - SDL_GPUColorTargetDescription color_target = {}; - color_target.format = SWAPCHAIN_FMT; - color_target.blend_state = no_blend; - - SDL_GPUVertexInputState no_input = {}; - - SDL_GPUGraphicsPipelineCreateInfo pipe_info = {}; - pipe_info.vertex_shader = vert; - pipe_info.fragment_shader = frag; - pipe_info.vertex_input_state = no_input; - pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - pipe_info.target_info.num_color_targets = 1; - pipe_info.target_info.color_target_descriptions = &color_target; - - crtpi_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info); - - SDL_ReleaseGPUShader(device_, vert); - SDL_ReleaseGPUShader(device_, frag); - - if (crtpi_pipeline_ == nullptr) { - SDL_Log("SDL3GPUShader: CrtPi pipeline creation failed: %s", SDL_GetError()); - return false; - } - - return true; + crtpi_pipeline_ = createPostfxLikePipeline(frag, SWAPCHAIN_FMT, "CrtPi"); + return crtpi_pipeline_ != nullptr; } // --------------------------------------------------------------------------- diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index d1fe704..41b539e 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -124,8 +124,10 @@ namespace Rendering { Uint32 num_uniform_buffers) -> SDL_GPUShader*; auto createPipeline() -> bool; - auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi - auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_ + auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi + auto createPostfxVertexShader() -> SDL_GPUShader*; // Vertex shader fullscreen-triangle compartit (MSL/SPIRV) + auto createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline*; // Empaqueta vert + frag + target en un pipeline + auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_ // Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC [[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode; From 030180443e805764a26818c11b3de4786c3ff67e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:36:51 +0200 Subject: [PATCH 06/15] refactor global_inputs: extreu getShaderAction, getPaletteAction, getScreenshotAction i firstPressedFrom --- source/core/input/global_inputs.cpp | 103 ++++++++++++---------------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 357e889..72e2854 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -2,8 +2,10 @@ #include -#include // Para allocator, operator+, char_traits, string -#include // Para vector +#include // Para std::ranges::find_if +#include // Para std::initializer_list +#include // Para allocator, operator+, char_traits, string +#include // Para vector #include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT #include "core/locale/locale.hpp" // Para Locale @@ -158,65 +160,50 @@ namespace GlobalInputs { Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")}); } + // F4 amb modificadors: Ctrl=toggle supersampling, Shift=next preset, sense modificador=toggle shader + auto getShaderAction() -> InputAction { + if (!Screen::get()->isHardwareAccelerated()) { return InputAction::NONE; } + if (!Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; } + const SDL_Keymod MOD = SDL_GetModState(); + if ((MOD & SDL_KMOD_CTRL) != 0U) { return InputAction::TOGGLE_SUPERSAMPLING; } + if (Options::video.shader.enabled && ((MOD & SDL_KMOD_SHIFT) != 0U)) { return InputAction::NEXT_SHADER_PRESET; } + return InputAction::TOGGLE_SHADER; + } + + // F5 amb modificador Ctrl per a paleta anterior + auto getPaletteAction() -> InputAction { + if (!Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; } + return ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) ? InputAction::PREVIOUS_PALETTE : InputAction::NEXT_PALETTE; + } + + // Comprova una llista d'accions 1:1 (sense modificadors); retorna la primera que dispare + auto firstPressedFrom(std::initializer_list actions) -> InputAction { + const auto* const IT = std::ranges::find_if(actions, [](const InputAction act) { + return Input::get()->checkAction(act, Input::DO_NOT_ALLOW_REPEAT); + }); + return (IT != actions.end()) ? *IT : InputAction::NONE; + } + + // SCREENSHOT requereix Ctrl mantingut a més de la tecla + auto getScreenshotAction() -> InputAction { + if (!Input::get()->checkAction(InputAction::SCREENSHOT, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; } + return ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) ? InputAction::SCREENSHOT : InputAction::NONE; + } + // Detecta qué acción global ha sido presionada (si alguna) - auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity) - if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::EXIT; - } - if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::ACCEPT; - } - if (Input::get()->checkAction(InputAction::TOGGLE_BORDER, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::TOGGLE_BORDER; - } + auto getPressedAction() -> InputAction { + if (const InputAction ACT = firstPressedFrom({InputAction::EXIT, InputAction::ACCEPT, InputAction::TOGGLE_BORDER}); ACT != InputAction::NONE) { return ACT; } + if (!Options::kiosk.enabled) { - if (Input::get()->checkAction(InputAction::TOGGLE_FULLSCREEN, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::TOGGLE_FULLSCREEN; - } - if (Input::get()->checkAction(InputAction::WINDOW_DEC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::WINDOW_DEC_ZOOM; - } - if (Input::get()->checkAction(InputAction::WINDOW_INC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::WINDOW_INC_ZOOM; - } + if (const InputAction ACT = firstPressedFrom({InputAction::TOGGLE_FULLSCREEN, InputAction::WINDOW_DEC_ZOOM, InputAction::WINDOW_INC_ZOOM}); ACT != InputAction::NONE) { return ACT; } } - if (Screen::get()->isHardwareAccelerated()) { - if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) { - if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) { - return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4 - } - if (Options::video.shader.enabled && ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0U)) { - return InputAction::NEXT_SHADER_PRESET; // Shift+F4 - } - return InputAction::TOGGLE_SHADER; // F4 - } - } - if (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) { - if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) { - return InputAction::PREVIOUS_PALETTE; // Ctrl+F5 - } - return InputAction::NEXT_PALETTE; // F5 - } - if (Input::get()->checkAction(InputAction::NEXT_PALETTE_SORT, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::NEXT_PALETTE_SORT; // F6 - } - if (Input::get()->checkAction(InputAction::TOGGLE_INTEGER_SCALE, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::TOGGLE_INTEGER_SCALE; - } - if (Input::get()->checkAction(InputAction::TOGGLE_VSYNC, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::TOGGLE_VSYNC; - } - if (Input::get()->checkAction(InputAction::TOGGLE_INFO, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::TOGGLE_INFO; - } - if (Input::get()->checkAction(InputAction::TOGGLE_CONSOLE, Input::DO_NOT_ALLOW_REPEAT)) { - return InputAction::TOGGLE_CONSOLE; - } - if (Input::get()->checkAction(InputAction::SCREENSHOT, Input::DO_NOT_ALLOW_REPEAT) && - ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U)) { - return InputAction::SCREENSHOT; - } - return InputAction::NONE; + + if (const InputAction ACT = getShaderAction(); ACT != InputAction::NONE) { return ACT; } + if (const InputAction ACT = getPaletteAction(); ACT != InputAction::NONE) { return ACT; } + + if (const InputAction ACT = firstPressedFrom({InputAction::NEXT_PALETTE_SORT, InputAction::TOGGLE_INTEGER_SCALE, InputAction::TOGGLE_VSYNC, InputAction::TOGGLE_INFO, InputAction::TOGGLE_CONSOLE}); ACT != InputAction::NONE) { return ACT; } + + return getScreenshotAction(); } } // namespace From 4be51ea3189c11032d2c5c734288102902c79bf4 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:40:05 +0200 Subject: [PATCH 07/15] refactor console: extreu updateCursorBlink/Typewriter/Resize/OpenClose i handleTextInput/HistoryUp/Down/Tab --- source/game/ui/console.cpp | 315 +++++++++++++++++++------------------ source/game/ui/console.hpp | 8 + 2 files changed, 174 insertions(+), 149 deletions(-) diff --git a/source/game/ui/console.cpp b/source/game/ui/console.cpp index f47d8ba..aeeef25 100644 --- a/source/game/ui/console.cpp +++ b/source/game/ui/console.cpp @@ -193,84 +193,92 @@ void Console::redrawText() { Screen::get()->setRendererSurface(previous_renderer); } -// Actualiza la animación de la consola -void Console::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity) - if (status_ == Status::HIDDEN) { +// Parpadeig del cursor (només quan ACTIVE) +void Console::updateCursorBlink(float delta_time) { + cursor_timer_ += delta_time; + const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME; + if (cursor_timer_ >= THRESHOLD) { + cursor_timer_ = 0.0F; + cursor_visible_ = !cursor_visible_; + } +} + +// Revelat lletra a lletra de msg_lines_ (només quan ACTIVE) +void Console::updateTypewriter(float delta_time) { + const int TOTAL_CHARS = std::accumulate(msg_lines_.begin(), msg_lines_.end(), 0, [](int acc, const auto& line) { return acc + static_cast(line.size()); }); + if (typewriter_chars_ >= TOTAL_CHARS) { return; } + typewriter_timer_ += delta_time; + while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) { + typewriter_timer_ -= TYPEWRITER_CHAR_DELAY; + ++typewriter_chars_; + } +} + +// Animació d'altura quan msg_lines_ canvia (només quan ACTIVE i height_ != target_height_) +void Console::updateResizeAnimation(float delta_time) { + if (anim_progress_ == 0.0F) { + // Iniciar animació de resize + anim_start_ = height_; + anim_end_ = target_height_; + } + anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F); + const float PREV_HEIGHT = height_; + height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_)); + if (anim_progress_ >= 1.0F) { + height_ = target_height_; + anim_progress_ = 0.0F; + } + // Actualitzar el Notifier incrementalment amb el delta d'altura + if (Notifier::get() != nullptr) { + const int DELTA_PX = static_cast(height_) - static_cast(PREV_HEIGHT); + if (DELTA_PX > 0) { + Notifier::get()->addYOffset(DELTA_PX); + notifier_offset_applied_ += DELTA_PX; + } else if (DELTA_PX < 0) { + Notifier::get()->removeYOffset(-DELTA_PX); + notifier_offset_applied_ += DELTA_PX; + } + } + // Reconstruir la Surface al nou tamany (xicoteta: 256×~18-72px) + const float WIDTH = Options::game.width; + surface_ = std::make_shared(WIDTH, height_); + sprite_->setSurface(surface_); +} + +// Animació RISING/VANISHING (basada en temps amb easing) +void Console::updateOpenCloseAnimation(float delta_time) { + anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F); + y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_)); + + if (anim_progress_ < 1.0F) { return; } + y_ = anim_end_; + anim_progress_ = 0.0F; + if (status_ == Status::RISING) { + status_ = Status::ACTIVE; return; } + status_ = Status::HIDDEN; + // Reset del missatge una vegada completament oculta + msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)}; + target_height_ = calcTargetHeight(static_cast(msg_lines_.size())); +} + +// Actualiza la animación de la consola +void Console::update(float delta_time) { + if (status_ == Status::HIDDEN) { return; } - // Parpadeo del cursor (solo cuando activa) if (status_ == Status::ACTIVE) { - cursor_timer_ += delta_time; - const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME; - if (cursor_timer_ >= THRESHOLD) { - cursor_timer_ = 0.0F; - cursor_visible_ = !cursor_visible_; + updateCursorBlink(delta_time); + updateTypewriter(delta_time); + if (height_ != target_height_) { + updateResizeAnimation(delta_time); } } - // Efecto typewriter: revelar letras una a una (solo cuando ACTIVE) - if (status_ == Status::ACTIVE) { - const int TOTAL_CHARS = std::accumulate(msg_lines_.begin(), msg_lines_.end(), 0, [](int acc, const auto& line) { return acc + static_cast(line.size()); }); - if (typewriter_chars_ < TOTAL_CHARS) { - typewriter_timer_ += delta_time; - while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) { - typewriter_timer_ -= TYPEWRITER_CHAR_DELAY; - ++typewriter_chars_; - } - } - } - - // Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE - if (status_ == Status::ACTIVE && height_ != target_height_) { - if (anim_progress_ == 0.0F) { - // Iniciar animación de resize - anim_start_ = height_; - anim_end_ = target_height_; - } - anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F); - const float PREV_HEIGHT = height_; - height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_)); - if (anim_progress_ >= 1.0F) { - height_ = target_height_; - anim_progress_ = 0.0F; - } - // Actualizar el Notifier incrementalmente con el delta de altura - if (Notifier::get() != nullptr) { - const int DELTA_PX = static_cast(height_) - static_cast(PREV_HEIGHT); - if (DELTA_PX > 0) { - Notifier::get()->addYOffset(DELTA_PX); - notifier_offset_applied_ += DELTA_PX; - } else if (DELTA_PX < 0) { - Notifier::get()->removeYOffset(-DELTA_PX); - notifier_offset_applied_ += DELTA_PX; - } - } - // Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px) - const float WIDTH = Options::game.width; - surface_ = std::make_shared(WIDTH, height_); - sprite_->setSurface(surface_); - } - - // Redibujar texto cada frame redrawText(); - // Animación de apertura/cierre (basada en tiempo) if (status_ == Status::RISING || status_ == Status::VANISHING) { - anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F); - y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_)); - - if (anim_progress_ >= 1.0F) { - y_ = anim_end_; - anim_progress_ = 0.0F; - if (status_ == Status::RISING) { - status_ = Status::ACTIVE; - } else { - status_ = Status::HIDDEN; - msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)}; - target_height_ = calcTargetHeight(static_cast(msg_lines_.size())); - } - } + updateOpenCloseAnimation(delta_time); } SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_}; @@ -334,94 +342,103 @@ void Console::toggle() { } } +// Insereix caràcters imprimibles a input_line_ (filtra control i la toggle key activa) +void Console::handleTextInput(const SDL_Event& event) { + if (static_cast(event.text.text[0]) < 32) { return; } + // Ignorar text si la tecla toggle està pulsada (evita escriure el seu caràcter) + if (KeyConfig::get() != nullptr) { + SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console"); + SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr); + if (toggle_sc != SDL_SCANCODE_UNKNOWN) { + const bool* ks = SDL_GetKeyboardState(nullptr); + if (ks[toggle_sc]) { return; } + } + } + if (static_cast(input_line_.size()) < MAX_LINE_CHARS) { + input_line_ += event.text.text; + } + tab_matches_.clear(); +} + +// Navega enrere a l'historial (cap a comandes més antigues) +void Console::handleHistoryUp() { + tab_matches_.clear(); + if (history_index_ >= static_cast(history_.size()) - 1) { return; } + if (history_index_ == -1) { saved_input_ = input_line_; } + ++history_index_; + input_line_ = history_[static_cast(history_index_)]; +} + +// Navega cap al present a l'historial (cap a comandes més recents) +void Console::handleHistoryDown() { + tab_matches_.clear(); + if (history_index_ < 0) { return; } + --history_index_; + input_line_ = (history_index_ == -1) ? saved_input_ : history_[static_cast(history_index_)]; +} + +// Autocompletat per TAB: calcula candidats si cal i cicla +void Console::handleTab() { + if (tab_matches_.empty()) { + std::string upper; + for (const unsigned char C : input_line_) { upper += static_cast(std::toupper(C)); } + + const size_t SPACE_POS = upper.rfind(' '); + if (SPACE_POS == std::string::npos) { + // Mode comanda: cicla keywords visibles que comencen pel prefix + const auto KEYWORDS = registry_.getVisibleKeywords(); + std::ranges::copy_if(KEYWORDS, std::back_inserter(tab_matches_), [&upper](const auto& kw) { return upper.empty() || kw.starts_with(upper); }); + } else { + const std::string BASE_CMD = upper.substr(0, SPACE_POS); + const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1); + const auto OPTS = registry_.getCompletions(BASE_CMD); + for (const auto& arg : OPTS) { + if (!SUB_PREFIX.empty() && !std::string_view{arg}.starts_with(SUB_PREFIX)) { continue; } + std::string match = BASE_CMD; + match += ' '; + match += arg; + tab_matches_.emplace_back(std::move(match)); + } + } + tab_index_ = -1; + } + if (tab_matches_.empty()) { return; } + tab_index_ = (tab_index_ + 1) % static_cast(tab_matches_.size()); + std::string result = tab_matches_[static_cast(tab_index_)]; + std::ranges::transform(result, result.begin(), [](char c) { return static_cast(std::tolower(static_cast(c))); }); + input_line_ = result; +} + // Procesa el evento SDL: entrada de texto, Backspace, Enter -void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity) +void Console::handleEvent(const SDL_Event& event) { if (status_ != Status::ACTIVE) { return; } if (event.type == SDL_EVENT_TEXT_INPUT) { - // Filtrar caracteres de control (tab, newline, etc.) - if (static_cast(event.text.text[0]) < 32) { return; } - // Ignorar texto si la tecla toggle está pulsada (evita escribir su carácter) - if (KeyConfig::get() != nullptr) { - SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console"); - SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr); - if (toggle_sc != SDL_SCANCODE_UNKNOWN) { - const bool* ks = SDL_GetKeyboardState(nullptr); - if (ks[toggle_sc]) { return; } - } - } - if (static_cast(input_line_.size()) < MAX_LINE_CHARS) { - input_line_ += event.text.text; - } - tab_matches_.clear(); + handleTextInput(event); return; } + if (event.type != SDL_EVENT_KEY_DOWN) { return; } - if (event.type == SDL_EVENT_KEY_DOWN) { - switch (event.key.scancode) { - case SDL_SCANCODE_BACKSPACE: - tab_matches_.clear(); - if (!input_line_.empty()) { input_line_.pop_back(); } - break; - case SDL_SCANCODE_RETURN: - case SDL_SCANCODE_KP_ENTER: - processCommand(); - break; - case SDL_SCANCODE_UP: - // Navegar hacia atrás en el historial - tab_matches_.clear(); - if (history_index_ < static_cast(history_.size()) - 1) { - if (history_index_ == -1) { saved_input_ = input_line_; } - ++history_index_; - input_line_ = history_[static_cast(history_index_)]; - } - break; - case SDL_SCANCODE_DOWN: - // Navegar hacia el presente en el historial - tab_matches_.clear(); - if (history_index_ >= 0) { - --history_index_; - input_line_ = (history_index_ == -1) - ? saved_input_ - : history_[static_cast(history_index_)]; - } - break; - case SDL_SCANCODE_TAB: { - if (tab_matches_.empty()) { - // Calcular el input actual en mayúsculas - std::string upper; - for (unsigned char c : input_line_) { upper += static_cast(std::toupper(c)); } - - const size_t SPACE_POS = upper.rfind(' '); - if (SPACE_POS == std::string::npos) { - // Modo comando: ciclar keywords visibles que empiecen por el prefijo - const auto VISIBLE = registry_.getVisibleKeywords(); - std::ranges::copy_if(VISIBLE, std::back_inserter(tab_matches_), [&](const auto& kw) { return upper.empty() || kw.starts_with(upper); }); - } else { - const std::string BASE_CMD = upper.substr(0, SPACE_POS); - const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1); - const auto OPTS = registry_.getCompletions(BASE_CMD); - for (const auto& arg : OPTS) { - if (SUB_PREFIX.empty() || std::string_view{arg}.starts_with(SUB_PREFIX)) { - std::string match = BASE_CMD; - match += ' '; - match += arg; - tab_matches_.emplace_back(std::move(match)); - } - } - } - tab_index_ = -1; - } - if (tab_matches_.empty()) { break; } - tab_index_ = (tab_index_ + 1) % static_cast(tab_matches_.size()); - std::string result = tab_matches_[static_cast(tab_index_)]; - std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - input_line_ = result; - break; - } - default: - break; - } + switch (event.key.scancode) { + case SDL_SCANCODE_BACKSPACE: + tab_matches_.clear(); + if (!input_line_.empty()) { input_line_.pop_back(); } + break; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + processCommand(); + break; + case SDL_SCANCODE_UP: + handleHistoryUp(); + break; + case SDL_SCANCODE_DOWN: + handleHistoryDown(); + break; + case SDL_SCANCODE_TAB: + handleTab(); + break; + default: + break; } } diff --git a/source/game/ui/console.hpp b/source/game/ui/console.hpp index 9a779d7..5f49202 100644 --- a/source/game/ui/console.hpp +++ b/source/game/ui/console.hpp @@ -79,6 +79,14 @@ class Console { void redrawText(); // Redibuja el texto dinámico (msg + input + cursor) void processCommand(); // Procesa el comando introducido por el usuario [[nodiscard]] auto wrapText(const std::string& text) const -> std::vector; // Word-wrap por ancho en píxeles + void updateCursorBlink(float delta_time); // Parpadeig del cursor (només quan ACTIVE) + void updateTypewriter(float delta_time); // Revelat lletra a lletra de msg_lines_ + void updateResizeAnimation(float delta_time); // Animació d'altura quan msg_lines_ canvia + void updateOpenCloseAnimation(float delta_time); // Animació RISING/VANISHING + void handleTextInput(const SDL_Event& event); // Insereix caràcters imprimibles a input_line_ + void handleHistoryUp(); // Navega enrere a l'historial + void handleHistoryDown(); // Navega cap al present a l'historial + void handleTab(); // Autocompletat per TAB: calcula candidats si cal i cicla // Objetos de renderizado std::shared_ptr text_; From 2518e0f436c315b6d2a95cf6edcc1c63c69b8f06 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:43:36 +0200 Subject: [PATCH 08/15] refactor resource_list: extreu parseAssetItem/ModernType/LegacyAsset/ModernCategory al namespace anonim --- source/core/resources/resource_list.cpp | 167 ++++++++++-------------- source/core/resources/resource_list.hpp | 12 +- 2 files changed, 78 insertions(+), 101 deletions(-) diff --git a/source/core/resources/resource_list.cpp b/source/core/resources/resource_list.cpp index 084bfed..2fff728 100644 --- a/source/core/resources/resource_list.cpp +++ b/source/core/resources/resource_list.cpp @@ -16,6 +16,71 @@ namespace Resource { + namespace { + // Un item del format modern: pot ser string (path) o mapping ({path, required?, absolute?}) + void parseAssetItem(List& list, const fkyaml::node& item, List::Type type, const std::string& prefix, const std::string& system_folder, const std::string& category, const std::string& type_str) { + try { + if (item.is_string()) { + auto path = List::replaceVariables(item.get_value(), prefix, system_folder); + list.add(path, type, true, false); + return; + } + if (item.is_mapping() && item.contains("path")) { + auto path = List::replaceVariables(item["path"].get_value(), prefix, system_folder); + const bool REQUIRED = !item.contains("required") || item["required"].get_value(); + const bool ABSOLUTE = item.contains("absolute") && item["absolute"].get_value(); + list.add(path, type, REQUIRED, ABSOLUTE); + return; + } + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Invalid item in type '%s', category '%s', skipping", type_str.c_str(), category.c_str()); + } catch (const std::exception& e) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing asset in category '%s', type '%s': %s", category.c_str(), type_str.c_str(), e.what()); + } + } + + // (TIPO: [items...]) del format modern. Itera els items i delega a parseAssetItem. + void parseModernType(List& list, const fkyaml::node& items_node, List::Type type, const std::string& type_str, const std::string& category, const std::string& prefix, const std::string& system_folder) { + if (!items_node.is_sequence()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Type '%s' in category '%s' is not a sequence, skipping", type_str.c_str(), category.c_str()); + return; + } + for (const auto& item : items_node) { + parseAssetItem(list, item, type, prefix, system_folder, category, type_str); + } + } + + // {type, path, required?, absolute?} del format antic + void parseLegacyAsset(List& list, const fkyaml::node& asset, const std::string& category, const std::string& prefix, const std::string& system_folder) { + try { + if (!asset.contains("type") || !asset.contains("path")) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Asset in category '%s' missing 'type' or 'path', skipping", category.c_str()); + return; + } + auto type_str = asset["type"].get_value(); + auto path = asset["path"].get_value(); + const bool REQUIRED = !asset.contains("required") || asset["required"].get_value(); + const bool ABSOLUTE = asset.contains("absolute") && asset["absolute"].get_value(); + path = List::replaceVariables(path, prefix, system_folder); + list.add(path, List::parseAssetType(type_str), REQUIRED, ABSOLUTE); + } catch (const std::exception& e) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing asset in category '%s': %s", category.c_str(), e.what()); + } + } + + // Categoria amb format modern (TIPO → [items]). Itera els tipus. + void parseModernCategory(List& list, const fkyaml::node& category_assets, const std::string& category, const std::string& prefix, const std::string& system_folder) { + for (auto type_it = category_assets.begin(); type_it != category_assets.end(); ++type_it) { + try { + auto type_str = type_it.key().get_value(); + const List::Type TYPE = List::parseAssetType(type_str); + parseModernType(list, type_it.value(), TYPE, type_str, category, prefix, system_folder); + } catch (const std::exception& e) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing type in category '%s': %s", category.c_str(), e.what()); + } + } + } + } // namespace + // Singleton List* List::instance = nullptr; @@ -160,17 +225,13 @@ namespace Resource { } // Carga recursos desde un string de configuración (para release con pack) - void List::loadFromString(const std::string& config_content, const std::string& prefix, const std::string& system_folder) { // NOLINT(readability-convert-member-functions-to-static,readability-function-cognitive-complexity) + void List::loadFromString(const std::string& config_content, const std::string& prefix, const std::string& system_folder) { try { - // Parsear YAML auto yaml = fkyaml::node::deserialize(config_content); - - // Verificar estructura básica if (!yaml.contains("assets")) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid assets.yaml format - missing 'assets' key"); return; } - const auto& assets = yaml["assets"]; // Iterar sobre cada categoría (fonts, palettes, etc.) @@ -179,105 +240,19 @@ namespace Resource { const auto& category_assets = it.value(); if (category_assets.is_mapping()) { - // Nuevo formato: categoría → { TIPO: [paths...], TIPO2: [paths...] } - for (auto type_it = category_assets.begin(); type_it != category_assets.end(); ++type_it) { - try { - auto type_str = type_it.key().get_value(); - Type type = parseAssetType(type_str); - const auto& items = type_it.value(); - - if (!items.is_sequence()) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Warning: Type '%s' in category '%s' is not a sequence, skipping", - type_str.c_str(), - category.c_str()); - continue; - } - - for (const auto& item : items) { - try { - if (item.is_string()) { - // Formato simple: solo el path - auto path = replaceVariables(item.get_value(), prefix, system_folder); - addToMap(path, type, true, false); - } else if (item.is_mapping() && item.contains("path")) { - // Formato expandido: { path, required?, absolute? } - auto path = replaceVariables(item["path"].get_value(), prefix, system_folder); - bool required = !item.contains("required") || item["required"].get_value(); - bool absolute = item.contains("absolute") && item["absolute"].get_value(); - addToMap(path, type, required, absolute); - } else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Warning: Invalid item in type '%s', category '%s', skipping", - type_str.c_str(), - category.c_str()); - } - } catch (const std::exception& e) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Error parsing asset in category '%s', type '%s': %s", - category.c_str(), - type_str.c_str(), - e.what()); - } - } - } catch (const std::exception& e) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Error parsing type in category '%s': %s", - category.c_str(), - e.what()); - } - } + parseModernCategory(*this, category_assets, category, prefix, system_folder); } else if (category_assets.is_sequence()) { - // Formato antiguo (retrocompatibilidad): categoría → [{type, path}, ...] - for (const auto& asset : category_assets) { - try { - if (!asset.contains("type") || !asset.contains("path")) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Warning: Asset in category '%s' missing 'type' or 'path', skipping", - category.c_str()); - continue; - } - - auto type_str = asset["type"].get_value(); - auto path = asset["path"].get_value(); - bool required = true; - bool absolute = false; - - if (asset.contains("required")) { - required = asset["required"].get_value(); - } - if (asset.contains("absolute")) { - absolute = asset["absolute"].get_value(); - } - - path = replaceVariables(path, prefix, system_folder); - Type type = parseAssetType(type_str); - addToMap(path, type, required, absolute); - - } catch (const std::exception& e) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Error parsing asset in category '%s': %s", - category.c_str(), - e.what()); - } - } + for (const auto& asset : category_assets) { parseLegacyAsset(*this, asset, category, prefix, system_folder); } } else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "Warning: Category '%s' has invalid format, skipping", - category.c_str()); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Category '%s' has invalid format, skipping", category.c_str()); } } std::cout << "Loaded " << file_list_.size() << " assets from YAML config" << '\n'; - } catch (const fkyaml::exception& e) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "YAML parsing error: %s", - e.what()); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "YAML parsing error: %s", e.what()); } catch (const std::exception& e) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Error loading assets: %s", - e.what()); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error loading assets: %s", e.what()); } } diff --git a/source/core/resources/resource_list.hpp b/source/core/resources/resource_list.hpp index 731e0de..2e26073 100644 --- a/source/core/resources/resource_list.hpp +++ b/source/core/resources/resource_list.hpp @@ -42,6 +42,10 @@ namespace Resource { [[nodiscard]] auto getListByType(Type type) const -> std::vector; [[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe + // Helpers públics per al parseig YAML (usats des del namespace anònim al .cpp) + [[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; + [[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; + private: // --- Estructuras privadas --- struct Item { @@ -62,11 +66,9 @@ namespace Resource { std::string prefix_; // Prefijo para rutas (${PREFIX}) // --- Métodos internos --- - [[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo - [[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo - void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa - [[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta - static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones + [[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo + void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa + static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones // --- Constructores y destructor privados (singleton) --- explicit List(std::string executable_path) // Constructor privado From 87d3e70af3512548a1aba29c6d774ee20e64623f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:48:48 +0200 Subject: [PATCH 09/15] refactor options: extreu readYamlField/Volume/PaletteIndex i helpers CrtPi --- source/game/options.cpp | 350 +++++++++++++++------------------------- 1 file changed, 126 insertions(+), 224 deletions(-) diff --git a/source/game/options.cpp b/source/game/options.cpp index 3ce82ce..f4c1183 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -2,9 +2,11 @@ #include +#include // Para std::clamp, std::ranges::transform #include // Para create_directories #include // Para ifstream, ofstream #include // Para cout, cerr +#include // Para std::back_inserter #include // Para string #include // Para unordered_map @@ -517,90 +519,65 @@ namespace Options { } } + namespace { + // Llig parent[key] cap a dst si existeix; ignora errors de format (conserva el default). + template + void readYamlField(const fkyaml::node& parent, const char* key, T& dst) { + if (!parent.contains(key)) { return; } + try { + dst = parent[key].template get_value(); + } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ + } + } + + // Versió específica per a volums (clamp a [0,1]) + void readYamlVolume(const fkyaml::node& parent, const char* key, float& dst) { + if (!parent.contains(key)) { return; } + try { + dst = std::clamp(parent[key].get_value(), 0.0F, 1.0F); + } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ + } + } + + // Versió específica per a colors de paleta (clamp a [0,255]) + void readYamlPaletteIndex(const fkyaml::node& parent, const char* key, int& dst) { + if (!parent.contains(key)) { return; } + try { + dst = std::clamp(parent[key].get_value(), 0, 255); + } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ + } + } + } // namespace + // Carga configuración de audio desde YAML - void loadAudioConfigFromYaml(const fkyaml::node& yaml) { // NOLINT(readability-function-cognitive-complexity) + void loadAudioConfigFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("audio")) { return; } const auto& a = yaml["audio"]; - if (a.contains("enabled")) { - try { - audio.enabled = a["enabled"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (a.contains("volume")) { - try { - audio.volume = std::clamp(a["volume"].get_value(), 0.0F, 1.0F); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } + readYamlField(a, "enabled", audio.enabled); + readYamlVolume(a, "volume", audio.volume); if (a.contains("music")) { const auto& m = a["music"]; - if (m.contains("enabled")) { - try { - audio.music.enabled = m["enabled"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (m.contains("volume")) { - try { - audio.music.volume = std::clamp(m["volume"].get_value(), 0.0F, 1.0F); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } + readYamlField(m, "enabled", audio.music.enabled); + readYamlVolume(m, "volume", audio.music.volume); } if (a.contains("sound")) { const auto& s = a["sound"]; - if (s.contains("enabled")) { - try { - audio.sound.enabled = s["enabled"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (s.contains("volume")) { - try { - audio.sound.volume = std::clamp(s["volume"].get_value(), 0.0F, 1.0F); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } + readYamlField(s, "enabled", audio.sound.enabled); + readYamlVolume(s, "volume", audio.sound.volume); } } // Carga configuración de la consola desde YAML - void loadConsoleConfigFromYaml(const fkyaml::node& yaml) { // NOLINT(readability-function-cognitive-complexity) + void loadConsoleConfigFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("console")) { return; } const auto& c = yaml["console"]; - if (c.contains("transparent")) { - try { - console.transparent = c["transparent"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (c.contains("bg_color")) { - try { - console.bg_color = std::clamp(c["bg_color"].get_value(), 0, 255); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (c.contains("msg_color")) { - try { - console.msg_color = std::clamp(c["msg_color"].get_value(), 0, 255); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (c.contains("prompt_color")) { - try { - console.prompt_color = std::clamp(c["prompt_color"].get_value(), 0, 255); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (c.contains("command_color")) { - try { - console.command_color = std::clamp(c["command_color"].get_value(), 0, 255); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } + readYamlField(c, "transparent", console.transparent); + readYamlPaletteIndex(c, "bg_color", console.bg_color); + readYamlPaletteIndex(c, "msg_color", console.msg_color); + readYamlPaletteIndex(c, "prompt_color", console.prompt_color); + readYamlPaletteIndex(c, "command_color", console.command_color); } // Carga configuración de la pantalla de carga de recursos desde YAML @@ -1048,110 +1025,91 @@ namespace Options { crtpi_file_path = path; } + // Defaults dels 4 presets CrtPi (DEFAULT, CURVED, SHARP, MINIMAL) + static auto defaultCrtPiPresets() -> std::vector { + return { + {.name = "DEFAULT", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = false, .enable_sharper = false}, + {.name = "CURVED", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = true, .enable_sharper = false}, + {.name = "SHARP", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = true, .enable_curvature = false, .enable_sharper = true}, + {.name = "MINIMAL", .scanline_weight = 8.0F, .scanline_gap_brightness = 0.05F, .bloom_factor = 2.0F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 1.00F, .curvature_x = 0.0F, .curvature_y = 0.0F, .mask_type = 0, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = false, .enable_curvature = false, .enable_sharper = false}}; + } + + // Escriu el fitxer CrtPi amb capçalera + els presets default. Retorna false si no pot obrir. + static auto writeCrtPiDefaultFile(const std::string& path, const std::vector& presets) -> bool { + const std::filesystem::path P(path); + if (P.has_parent_path()) { + std::error_code ec; + std::filesystem::create_directories(P.parent_path(), ec); + } + std::ofstream out(path); + if (!out.is_open()) { return false; } + + out << "# Projecte 2026 - CrtPi Shader Presets\n"; + out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\n"; + out << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n"; + out << "# bloom_factor: factor de brillo para zonas iluminadas (default 3.5)\n"; + out << "# input_gamma: gamma de entrada - linealizacion (default 2.4)\n"; + out << "# output_gamma: gamma de salida - codificacion (default 2.2)\n"; + out << "# mask_brightness: brillo sub-pixeles de la mascara de fosforo (default 0.80)\n"; + out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\n"; + out << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n"; + out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n"; + out << "\npresets:\n"; + for (const auto& p : presets) { + out << " - name: \"" << p.name << "\"\n"; + out << " scanline_weight: " << p.scanline_weight << "\n"; + out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n"; + out << " bloom_factor: " << p.bloom_factor << "\n"; + out << " input_gamma: " << p.input_gamma << "\n"; + out << " output_gamma: " << p.output_gamma << "\n"; + out << " mask_brightness: " << p.mask_brightness << "\n"; + out << " curvature_x: " << p.curvature_x << "\n"; + out << " curvature_y: " << p.curvature_y << "\n"; + out << " mask_type: " << p.mask_type << "\n"; + out << " enable_scanlines: " << (p.enable_scanlines ? "true" : "false") << "\n"; + out << " enable_multisample: " << (p.enable_multisample ? "true" : "false") << "\n"; + out << " enable_gamma: " << (p.enable_gamma ? "true" : "false") << "\n"; + out << " enable_curvature: " << (p.enable_curvature ? "true" : "false") << "\n"; + out << " enable_sharper: " << (p.enable_sharper ? "true" : "false") << "\n"; + } + return true; + } + + // Parseja un node YAML a un CrtPiPreset usant els helpers genèrics + static auto parseCrtPiPreset(const fkyaml::node& p) -> CrtPiPreset { + CrtPiPreset preset; + readYamlField(p, "name", preset.name); + parseFloatField(p, "scanline_weight", preset.scanline_weight); + parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness); + parseFloatField(p, "bloom_factor", preset.bloom_factor); + parseFloatField(p, "input_gamma", preset.input_gamma); + parseFloatField(p, "output_gamma", preset.output_gamma); + parseFloatField(p, "mask_brightness", preset.mask_brightness); + parseFloatField(p, "curvature_x", preset.curvature_x); + parseFloatField(p, "curvature_y", preset.curvature_y); + readYamlField(p, "mask_type", preset.mask_type); + readYamlField(p, "enable_scanlines", preset.enable_scanlines); + readYamlField(p, "enable_multisample", preset.enable_multisample); + readYamlField(p, "enable_gamma", preset.enable_gamma); + readYamlField(p, "enable_curvature", preset.enable_curvature); + readYamlField(p, "enable_sharper", preset.enable_sharper); + return preset; + } + // Carga los presets del shader CrtPi desde el fichero. Crea defaults si no existe. - auto loadCrtPiFromFile() -> bool { // NOLINT(readability-function-cognitive-complexity) + auto loadCrtPiFromFile() -> bool { crtpi_presets.clear(); std::ifstream file(crtpi_file_path); if (!file.good()) { std::cout << "CrtPi file not found, creating default: " << crtpi_file_path << '\n'; - // Crear directorio padre si no existe - const std::filesystem::path P(crtpi_file_path); - if (P.has_parent_path()) { - std::error_code ec; - std::filesystem::create_directories(P.parent_path(), ec); - } - // Escribir defaults - std::ofstream out(crtpi_file_path); - if (!out.is_open()) { - std::cerr << "Error: Cannot create CrtPi file: " << crtpi_file_path << '\n'; - // Cargar defaults en memoria aunque no se pueda escribir - crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false}); - crtpi_presets.push_back({"CURVED", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false}); - crtpi_presets.push_back({"SHARP", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true}); - crtpi_presets.push_back({"MINIMAL", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false}); - video.shader.current_crtpi_preset = 0; - return true; - } - out << "# Projecte 2026 - CrtPi Shader Presets\n"; - out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\n"; - out << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n"; - out << "# bloom_factor: factor de brillo para zonas iluminadas (default 3.5)\n"; - out << "# input_gamma: gamma de entrada - linealizacion (default 2.4)\n"; - out << "# output_gamma: gamma de salida - codificacion (default 2.2)\n"; - out << "# mask_brightness: brillo sub-pixeles de la mascara de fosforo (default 0.80)\n"; - out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\n"; - out << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n"; - out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n"; - out << "\n"; - out << "presets:\n"; - out << " - name: \"DEFAULT\"\n"; - out << " scanline_weight: 6.0\n"; - out << " scanline_gap_brightness: 0.12\n"; - out << " bloom_factor: 3.5\n"; - out << " input_gamma: 2.4\n"; - out << " output_gamma: 2.2\n"; - out << " mask_brightness: 0.80\n"; - out << " curvature_x: 0.05\n"; - out << " curvature_y: 0.10\n"; - out << " mask_type: 2\n"; - out << " enable_scanlines: true\n"; - out << " enable_multisample: true\n"; - out << " enable_gamma: true\n"; - out << " enable_curvature: false\n"; - out << " enable_sharper: false\n"; - out << " - name: \"CURVED\"\n"; - out << " scanline_weight: 6.0\n"; - out << " scanline_gap_brightness: 0.12\n"; - out << " bloom_factor: 3.5\n"; - out << " input_gamma: 2.4\n"; - out << " output_gamma: 2.2\n"; - out << " mask_brightness: 0.80\n"; - out << " curvature_x: 0.05\n"; - out << " curvature_y: 0.10\n"; - out << " mask_type: 2\n"; - out << " enable_scanlines: true\n"; - out << " enable_multisample: true\n"; - out << " enable_gamma: true\n"; - out << " enable_curvature: true\n"; - out << " enable_sharper: false\n"; - out << " - name: \"SHARP\"\n"; - out << " scanline_weight: 6.0\n"; - out << " scanline_gap_brightness: 0.12\n"; - out << " bloom_factor: 3.5\n"; - out << " input_gamma: 2.4\n"; - out << " output_gamma: 2.2\n"; - out << " mask_brightness: 0.80\n"; - out << " curvature_x: 0.05\n"; - out << " curvature_y: 0.10\n"; - out << " mask_type: 2\n"; - out << " enable_scanlines: true\n"; - out << " enable_multisample: false\n"; - out << " enable_gamma: true\n"; - out << " enable_curvature: false\n"; - out << " enable_sharper: true\n"; - out << " - name: \"MINIMAL\"\n"; - out << " scanline_weight: 8.0\n"; - out << " scanline_gap_brightness: 0.05\n"; - out << " bloom_factor: 2.0\n"; - out << " input_gamma: 2.4\n"; - out << " output_gamma: 2.2\n"; - out << " mask_brightness: 1.00\n"; - out << " curvature_x: 0.0\n"; - out << " curvature_y: 0.0\n"; - out << " mask_type: 0\n"; - out << " enable_scanlines: true\n"; - out << " enable_multisample: false\n"; - out << " enable_gamma: false\n"; - out << " enable_curvature: false\n"; - out << " enable_sharper: false\n"; - out.close(); - std::cout << "CrtPi file created with defaults: " << crtpi_file_path << '\n'; - crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false}); - crtpi_presets.push_back({"CURVED", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false}); - crtpi_presets.push_back({"SHARP", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true}); - crtpi_presets.push_back({"MINIMAL", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false}); + crtpi_presets = defaultCrtPiPresets(); video.shader.current_crtpi_preset = 0; + if (!writeCrtPiDefaultFile(crtpi_file_path, crtpi_presets)) { + std::cerr << "Error: Cannot create CrtPi file: " << crtpi_file_path << '\n'; + return true; // defaults en memòria igualment + } + std::cout << "CrtPi file created with defaults: " << crtpi_file_path << '\n'; return true; } @@ -1160,77 +1118,21 @@ namespace Options { try { auto yaml = fkyaml::node::deserialize(content); - if (yaml.contains("presets")) { - const auto& presets = yaml["presets"]; - for (const auto& p : presets) { - CrtPiPreset preset; - if (p.contains("name")) { - preset.name = p["name"].get_value(); - } - parseFloatField(p, "scanline_weight", preset.scanline_weight); - parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness); - parseFloatField(p, "bloom_factor", preset.bloom_factor); - parseFloatField(p, "input_gamma", preset.input_gamma); - parseFloatField(p, "output_gamma", preset.output_gamma); - parseFloatField(p, "mask_brightness", preset.mask_brightness); - parseFloatField(p, "curvature_x", preset.curvature_x); - parseFloatField(p, "curvature_y", preset.curvature_y); - if (p.contains("mask_type")) { - try { - preset.mask_type = p["mask_type"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (p.contains("enable_scanlines")) { - try { - preset.enable_scanlines = p["enable_scanlines"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (p.contains("enable_multisample")) { - try { - preset.enable_multisample = p["enable_multisample"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (p.contains("enable_gamma")) { - try { - preset.enable_gamma = p["enable_gamma"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (p.contains("enable_curvature")) { - try { - preset.enable_curvature = p["enable_curvature"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - if (p.contains("enable_sharper")) { - try { - preset.enable_sharper = p["enable_sharper"].get_value(); - } catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */ - } - } - crtpi_presets.push_back(preset); - } + const auto& presets_node = yaml["presets"]; + std::ranges::transform(presets_node, std::back_inserter(crtpi_presets), parseCrtPiPreset); } - - // Resolver el nombre del preset a índice if (!crtpi_presets.empty()) { resolveCrtPiPresetName(); } else { video.shader.current_crtpi_preset = 0; } - std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n"; return true; - } catch (const fkyaml::exception& e) { std::cerr << "Error parsing CrtPi YAML: " << e.what() << '\n'; - // Cargar defaults en memoria en caso de error crtpi_presets.clear(); - crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false}); + crtpi_presets.push_back(defaultCrtPiPresets().front()); // només DEFAULT en cas d'error video.shader.current_crtpi_preset = 0; return false; } From 62bf99f174e67707130ef0016d334bc548a91078 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:56:47 +0200 Subject: [PATCH 10/15] refactor console_commands: applyCheatToggle, parsers YAML al namespace anonim, buildHelp i append_csv --- source/game/ui/console_commands.cpp | 295 ++++++++++++++-------------- source/game/ui/console_commands.hpp | 1 + 2 files changed, 145 insertions(+), 151 deletions(-) diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index fbd7ba8..a9ac85f 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -426,7 +426,7 @@ static auto cmdSound(const std::vector& args) -> std::string { #ifdef _DEBUG // DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE ]] -static auto cmdDebug(const std::vector& args) -> std::string { // NOLINT(readability-function-cognitive-complexity) +static auto cmdDebug(const std::vector& args) -> std::string { // --- START subcommands (START SCENE works from any scene) --- if (!args.empty() && args[0] == "START") { // START SCENE [] — works from any scene @@ -525,7 +525,7 @@ static auto changeRoomWithEditor(const std::string& room_file) -> std::string { return std::string("Room: ") + room_file; } -static auto cmdRoom(const std::vector& args) -> std::string { // NOLINT(readability-function-cognitive-complexity) +static auto cmdRoom(const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (args.empty()) { return "usage: room |next|prev|left|right|up|down"; } @@ -613,7 +613,7 @@ static auto cmdScene(const std::vector& args) -> std::string { } // EDIT [ON|OFF|REVERT] -static auto cmdEdit(const std::vector& args) -> std::string { // NOLINT(readability-function-cognitive-complexity) +static auto cmdEdit(const std::vector& args) -> std::string { if (args.empty()) { // Toggle: si está activo → off, si no → on if ((MapEditor::get() != nullptr) && MapEditor::get()->isActive()) { @@ -814,47 +814,38 @@ static auto cmdHide(const std::vector& args) -> std::string { } // CHEAT [subcomando] -static auto cmdCheat(const std::vector& args) -> std::string { // NOLINT(readability-function-cognitive-complexity) +// Apply ON/OFF/toggle a un cheat binari. mode="" → toggle; "ON"/"OFF" → estableix; altra cosa → cadena buida (usage error). +static auto applyCheatToggle(Options::Cheat::State& cheat, std::string_view mode, std::string_view label) -> std::string { + using State = Options::Cheat::State; + if (mode.empty()) { + cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; + } else if (mode == "ON") { + if (cheat == State::ENABLED) { return std::string(label) + " already ON"; } + cheat = State::ENABLED; + } else if (mode == "OFF") { + if (cheat == State::DISABLED) { return std::string(label) + " already OFF"; } + cheat = State::DISABLED; + } else { + return {}; // sentinel: mode invàlid + } + return std::string(label) + " " + (cheat == State::ENABLED ? "ON" : "OFF"); +} + +static auto cmdCheat(const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (args.empty()) { return "usage: cheat [infinite lives|invincibility]"; } - // CHEAT INFINITE LIVES [ON|OFF] if (args[0] == "INFINITE") { if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; } - auto& cheat = Options::cheats.infinite_lives; - using State = Options::Cheat::State; - const std::vector REST(args.begin() + 2, args.end()); - // cppcheck-suppress knownConditionTrueFalse -- cppcheck no infiere que REST puede estar vacío cuando args.size() == 2. - if (REST.empty()) { - cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; - } else if (REST[0] == "ON") { - if (cheat == State::ENABLED) { return "Infinite lives already ON"; } - cheat = State::ENABLED; - } else if (REST[0] == "OFF") { - if (cheat == State::DISABLED) { return "Infinite lives already OFF"; } - cheat = State::DISABLED; - } else { - return "usage: cheat infinite lives [on|off]"; - } - return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF"); + const std::string_view MODE = (args.size() > 2) ? std::string_view(args[2]) : std::string_view(); + const std::string RES = applyCheatToggle(Options::cheats.infinite_lives, MODE, "Infinite lives"); + return RES.empty() ? "usage: cheat infinite lives [on|off]" : RES; } - // CHEAT INVINCIBILITY [ON|OFF] if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") { - auto& cheat = Options::cheats.invincible; - using State = Options::Cheat::State; - if (args.size() == 1) { - cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; - } else if (args[1] == "ON") { - if (cheat == State::ENABLED) { return "Invincibility already ON"; } - cheat = State::ENABLED; - } else if (args[1] == "OFF") { - if (cheat == State::DISABLED) { return "Invincibility already OFF"; } - cheat = State::DISABLED; - } else { - return "usage: cheat invincibility [on|off]"; - } - return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF"); + const std::string_view MODE = (args.size() > 1) ? std::string_view(args[1]) : std::string_view(); + const std::string RES = applyCheatToggle(Options::cheats.invincible, MODE, "Invincibility"); + return RES.empty() ? "usage: cheat invincibility [on|off]" : RES; } return "usage: cheat [infinite lives|invincibility]"; @@ -910,7 +901,7 @@ static auto cmdSize(const std::vector& /*unused*/) -> std::string { } // CONSOLE [TRANSPARENT [ON|OFF]|BG|MSG|PROMPT|COMMAND <0-255>] -static auto cmdConsole(const std::vector& args) -> std::string { // NOLINT(readability-function-cognitive-complexity) +static auto cmdConsole(const std::vector& args) -> std::string { if (args.empty()) { return std::string("Console ") + (Options::console.transparent ? "transparent" : "solid") + " bg:" + std::to_string(Options::console.bg_color) + " msg:" + std::to_string(Options::console.msg_color) + " prompt:" + std::to_string(Options::console.prompt_color) + " cmd:" + std::to_string(Options::console.command_color); } @@ -955,7 +946,7 @@ static auto cmdConsole(const std::vector& args) -> std::string { / // ── CommandRegistry ────────────────────────────────────────────────────────── -void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cognitive-complexity) +void CommandRegistry::registerHandlers() { handlers_["cmd_shader"] = cmdShader; handlers_["cmd_border"] = cmdBorder; handlers_["cmd_fullscreen"] = cmdFullscreen; @@ -1074,7 +1065,108 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni #endif } -void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readability-function-cognitive-complexity) +namespace { + // Parseja un node "scope" (string o sequence) a un vector. Buit si node és buit. + auto parseScopeNode(const fkyaml::node& scope_node) -> std::vector { + std::vector result; + if (scope_node.is_sequence()) { + std::ranges::transform(scope_node, std::back_inserter(result), [](const auto& s) { return s.template get_value(); }); + } else { + result.push_back(scope_node.get_value()); + } + return result; + } + + // Parseja un mapping de path → [options] en l'unordered_map de destí + void parseCompletionsNode(const fkyaml::node& completions_node, std::unordered_map>& out) { + for (auto it = completions_node.begin(); it != completions_node.end(); ++it) { + auto path = it.key().get_value(); + std::vector opts; + std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value(); }); + out[path] = std::move(opts); + } + } + +#ifdef _DEBUG + // Aplica camps "debug_extras" sobre un CommandDef ja inicialitzat (només en _DEBUG) + void applyDebugExtras(const fkyaml::node& extras, CommandDef& def) { + if (extras.contains("description")) { def.description = extras["description"].get_value(); } + if (extras.contains("usage")) { def.usage = extras["usage"].get_value(); } + if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value(); } + if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value(); } + if (extras.contains("completions")) { + def.completions.clear(); + parseCompletionsNode(extras["completions"], def.completions); + } + } +#endif + + // Parseja un cmd_node a CommandDef. Hereta de la categoria si el comand no defineix scope/debug_only. + auto parseCommandDef(const fkyaml::node& cmd_node, const std::string& category, bool cat_debug_only, const std::vector& cat_scopes) -> CommandDef { + CommandDef def; + def.keyword = cmd_node["keyword"].get_value(); + def.handler_id = cmd_node["handler"].get_value(); + def.category = category; + def.description = cmd_node.contains("description") ? cmd_node["description"].get_value() : ""; + def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value() : def.keyword; + def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value(); + def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value(); + def.debug_only = cat_debug_only || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value()); + def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value(); + def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value(); + + if (cmd_node.contains("scope")) { + def.scopes = parseScopeNode(cmd_node["scope"]); + } else if (!cat_scopes.empty()) { + def.scopes = cat_scopes; + } else { + def.scopes.emplace_back("global"); + } + + if (cmd_node.contains("completions")) { + parseCompletionsNode(cmd_node["completions"], def.completions); + } + +#ifdef _DEBUG + if (cmd_node.contains("debug_extras")) { + applyDebugExtras(cmd_node["debug_extras"], def); + } +#endif + return def; + } +} // namespace + +// Implementació del handler "cmd_help" +auto CommandRegistry::buildHelp(const std::vector& args) const -> std::string { + if (!args.empty()) { + // HELP KEYS [scope]: referencia de atajos de teclado + if (args[0] == "KEYS") { + return generateKeysHelp(args.size() > 1 ? args[1] : ""); + } + // HELP : mostrar ayuda detallada de un comando + const auto* cmd = findCommand(args[0]); + if (cmd == nullptr) { return "Unknown command: " + args[0]; } + std::string kw_lower = cmd->keyword; + std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower); + std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage; + + // Listar subcomandos/opciones si hay completions + const auto OPTS = getCompletions(cmd->keyword); + if (!OPTS.empty()) { + result += "\noptions:"; + for (const auto& opt : OPTS) { + std::string opt_lower = opt; + std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower); + result += " " + opt_lower; + } + } + return result; + } + std::cout << generateTerminalHelp(); + return generateConsoleHelp(); +} + +void CommandRegistry::load(const std::string& yaml_path) { registerHandlers(); // Cargar y parsear el YAML @@ -1098,121 +1190,21 @@ void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readabilit for (const auto& cat_node : yaml["categories"]) { const auto CATEGORY = cat_node["name"].get_value(); const bool CAT_DEBUG_ONLY = cat_node.contains("debug_only") && cat_node["debug_only"].get_value(); - - // Scopes por defecto de la categoría - std::vector cat_scopes; - if (cat_node.contains("scope")) { - const auto& scope_node = cat_node["scope"]; - if (scope_node.is_sequence()) { - std::ranges::transform(scope_node, std::back_inserter(cat_scopes), [](const auto& s) { return s.template get_value(); }); - } else { - cat_scopes.push_back(scope_node.get_value()); - } - } + const std::vector CAT_SCOPES = cat_node.contains("scope") ? parseScopeNode(cat_node["scope"]) : std::vector{}; if (!cat_node.contains("commands")) { continue; } for (const auto& cmd_node : cat_node["commands"]) { - CommandDef def; - def.keyword = cmd_node["keyword"].get_value(); - def.handler_id = cmd_node["handler"].get_value(); - def.category = CATEGORY; - def.description = cmd_node.contains("description") ? cmd_node["description"].get_value() : ""; - def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value() : def.keyword; - def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value(); - def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value(); - def.debug_only = CAT_DEBUG_ONLY || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value()); - def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value(); - def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value(); - - // Scopes: del comando, o hereda de la categoría, o "global" por defecto - if (cmd_node.contains("scope")) { - const auto& scope_node = cmd_node["scope"]; - if (scope_node.is_sequence()) { - std::ranges::transform(scope_node, std::back_inserter(def.scopes), [](const auto& s) { return s.template get_value(); }); - } else { - def.scopes.push_back(scope_node.get_value()); - } - } else if (!cat_scopes.empty()) { - def.scopes = cat_scopes; - } else { - def.scopes.emplace_back("global"); - } - - // Completions estáticas - if (cmd_node.contains("completions")) { - auto completions_node = cmd_node["completions"]; - for (auto it = completions_node.begin(); it != completions_node.end(); ++it) { - auto path = it.key().get_value(); - std::vector opts; - const auto& options_node = *it; - std::ranges::transform(options_node, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value(); }); - def.completions[path] = std::move(opts); - } - } - - // Aplicar debug_extras en debug builds -#ifdef _DEBUG - if (cmd_node.contains("debug_extras")) { - const auto& extras = cmd_node["debug_extras"]; - if (extras.contains("description")) { def.description = extras["description"].get_value(); } - if (extras.contains("usage")) { def.usage = extras["usage"].get_value(); } - if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value(); } - if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value(); } - if (extras.contains("completions")) { - def.completions.clear(); - auto extras_completions = extras["completions"]; - for (auto it = extras_completions.begin(); it != extras_completions.end(); ++it) { - auto path = it.key().get_value(); - std::vector opts; - const auto& options_node = *it; - std::ranges::transform(options_node, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value(); }); - def.completions[path] = std::move(opts); - } - } - } -#endif - - // En Release: saltar comandos debug_only + CommandDef def = parseCommandDef(cmd_node, CATEGORY, CAT_DEBUG_ONLY, CAT_SCOPES); #ifndef _DEBUG if (def.debug_only) { continue; } #endif - commands_.push_back(std::move(def)); } } - // Registrar el handler de HELP (captura this) - handlers_["cmd_help"] = [this](const std::vector& args) -> std::string { - if (!args.empty()) { - // HELP KEYS [scope]: referencia de atajos de teclado - if (args[0] == "KEYS") { - return generateKeysHelp(args.size() > 1 ? args[1] : ""); - } - // HELP : mostrar ayuda detallada de un comando - const auto* cmd = findCommand(args[0]); - if (cmd != nullptr) { - std::string kw_lower = cmd->keyword; - std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower); - std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage; - - // Listar subcomandos/opciones si hay completions - auto opts = getCompletions(cmd->keyword); - if (!opts.empty()) { - result += "\noptions:"; - for (const auto& opt : opts) { - std::string opt_lower = opt; - std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower); - result += " " + opt_lower; - } - } - return result; - } - return "Unknown command: " + args[0]; - } - std::cout << generateTerminalHelp(); - return generateConsoleHelp(); - }; + // Registrar el handler de HELP (delega a buildHelp per mantenir baixa la complexitat de load) + handlers_["cmd_help"] = [this](const std::vector& args) -> std::string { return buildHelp(args); }; // Aplanar completions en el mapa global for (const auto& cmd : commands_) { @@ -1300,12 +1292,17 @@ auto CommandRegistry::generateTerminalHelp() const -> std::string { return out.str(); } -auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(readability-function-cognitive-complexity) +auto CommandRegistry::generateConsoleHelp() const -> std::string { // Agrupar comandos visibles por scope std::string global_cmds; std::string debug_cmds; std::string editor_cmds; + auto append_csv = [](std::string& dst, const std::string& token) { + if (!dst.empty()) { dst += ", "; } + dst += token; + }; + for (const auto& cmd : commands_) { if (cmd.help_hidden) { continue; } if (!isCommandVisible(cmd)) { continue; } @@ -1315,16 +1312,12 @@ auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(re // Clasificar por el PRIMER scope del comando const std::string& primary = cmd.scopes.empty() ? "global" : cmd.scopes[0]; - if (primary == "editor") { - if (!editor_cmds.empty()) { editor_cmds += ", "; } - editor_cmds += kw_lower; + append_csv(editor_cmds, kw_lower); } else if (primary == "debug") { - if (!debug_cmds.empty()) { debug_cmds += ", "; } - debug_cmds += kw_lower; + append_csv(debug_cmds, kw_lower); } else { - if (!global_cmds.empty()) { global_cmds += ", "; } - global_cmds += kw_lower; + append_csv(global_cmds, kw_lower); } } diff --git a/source/game/ui/console_commands.hpp b/source/game/ui/console_commands.hpp index b425439..ff33eda 100644 --- a/source/game/ui/console_commands.hpp +++ b/source/game/ui/console_commands.hpp @@ -59,4 +59,5 @@ class CommandRegistry { void registerHandlers(); [[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool; [[nodiscard]] static auto generateKeysHelp(const std::string& scope_filter) -> std::string; + [[nodiscard]] auto buildHelp(const std::vector& args) const -> std::string; // Implementació del handler "cmd_help" }; From 3f4ead40e18fa1df775026d19b54e7380777d34b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 21:59:57 +0200 Subject: [PATCH 11/15] refactor palette_manager: extreu buildCostMatrix, hungarianAssign i buildPaletteFromAssignment --- source/core/rendering/palette_manager.cpp | 196 ++++++++++++---------- 1 file changed, 108 insertions(+), 88 deletions(-) diff --git a/source/core/rendering/palette_manager.cpp b/source/core/rendering/palette_manager.cpp index f839424..0b9d261 100644 --- a/source/core/rendering/palette_manager.cpp +++ b/source/core/rendering/palette_manager.cpp @@ -58,103 +58,123 @@ namespace { return count; } + namespace { + // Construeix la matriu de cost NxM (ampliada a SZxSZ amb zeros) per a l'algoritme hongarès. + auto buildCostMatrix(int n_rows, int m_cols, int sz, const Palette& palette, const Palette& reference) -> std::vector { + std::vector cost(static_cast(sz) * static_cast(sz), 0); + for (int i = 0; i < n_rows; ++i) { + for (int j = 0; j < m_cols; ++j) { + cost[(i * sz) + j] = rgbDistanceSq(palette[i], reference[j]); + } + } + return cost; + } + + // Estat compartit entre les fases d'una iteració del Kuhn-Munkres + struct HungarianStep { + int j0; + int delta; + int j1; + }; + + // Cerca la columna j1 que minimitza el cost reduït, actualitzant minv[] i way[]. + auto relaxColumns(int sz, const std::vector& cost, int j0, int i0, const std::vector& used, const std::vector& u, const std::vector& v, std::vector& minv, std::vector& way) -> HungarianStep { + constexpr int INF = INT_MAX / 2; + HungarianStep step{.j0 = j0, .delta = INF, .j1 = 0}; + for (int j = 1; j <= sz; ++j) { + if (used[j]) { continue; } + const int CUR = cost[((i0 - 1) * sz) + (j - 1)] - u[i0] - v[j]; + if (CUR < minv[j]) { + minv[j] = CUR; + way[j] = j0; + } + if (minv[j] < step.delta) { + step.delta = minv[j]; + step.j1 = j; + } + } + return step; + } + + // Aplica el delta calculat a potencials (u, v) i a minv[]. + void applyDelta(int sz, int delta, const std::vector& used, const std::vector& p, std::vector& u, std::vector& v, std::vector& minv) { + for (int j = 0; j <= sz; ++j) { + if (used[j]) { + u[p[j]] += delta; + v[j] -= delta; + } else { + minv[j] -= delta; + } + } + } + + // Algoritme hongarès (Kuhn-Munkres) basat en potencials. Retorna p[], + // on p[j] = fila assignada a la columna j (índexs 1-based; p[0] no s'usa). + auto hungarianAssign(int sz, const std::vector& cost) -> std::vector { + constexpr int INF = INT_MAX / 2; + std::vector u(sz + 1, 0); // Potencials de files + std::vector v(sz + 1, 0); // Potencials de columnes + std::vector p(sz + 1, 0); // p[j] = fila assignada a columna j + std::vector way(sz + 1, 0); + + for (int i = 1; i <= sz; ++i) { + p[0] = i; + int j0 = 0; + std::vector minv(sz + 1, INF); + std::vector used(sz + 1, false); + + do { + used[j0] = true; + const auto STEP = relaxColumns(sz, cost, j0, p[j0], used, u, v, minv, way); + applyDelta(sz, STEP.delta, used, p, u, v, minv); + j0 = STEP.j1; + } while (p[j0] != 0); + + do { + const int J1 = way[j0]; + p[j0] = p[J1]; + j0 = J1; + } while (j0 != 0); + } + return p; + } + + // Construeix la paleta resultant a partir de l'assignació p[]. Afegeix els colors + // de palette sense parella en reference al final (cas N > M). + auto buildPaletteFromAssignment(int n_rows, int m_cols, const std::vector& p, const Palette& palette) -> Palette { + Palette out{}; + out.fill(0); + for (int j = 1; j <= m_cols && j <= n_rows; ++j) { + const int ROW = p[j] - 1; + if (ROW >= 0 && ROW < n_rows) { out[j - 1] = palette[ROW]; } + } + if (n_rows <= m_cols) { return out; } + + std::vector used_rows(n_rows, false); + for (int j = 1; j <= m_cols; ++j) { + const int ROW = p[j] - 1; + if (ROW >= 0 && ROW < n_rows) { used_rows[ROW] = true; } + } + int out_idx = m_cols; + for (int i = 0; i < n_rows; ++i) { + if (!used_rows[i]) { out[out_idx++] = palette[i]; } + } + return out; + } + } // namespace + // Asignación óptima de colores mediante el algoritmo húngaro (Kuhn-Munkres). // Minimiza la distancia RGB total entre la paleta y la referencia. // O(N³) con N = número de colores activos — para N ≤ 256 es instantáneo. - // NOLINTNEXTLINE(readability-function-cognitive-complexity) auto sortByOptimal(const Palette& palette, const Palette& reference) -> Palette { const auto N = static_cast(countActiveColors(palette)); const auto M = static_cast(countActiveColors(reference)); const int SZ = std::max(N, M); if (SZ == 0) { return palette; } - // Matriz de coste NxM (ampliada a SZxSZ con ceros para hacerla cuadrada) - std::vector cost(static_cast(SZ) * static_cast(SZ), 0); - for (int i = 0; i < N; ++i) { - for (int j = 0; j < M; ++j) { - cost[(i * SZ) + j] = rgbDistanceSq(palette[i], reference[j]); - } - } - - // Hungarian algorithm (Kuhn-Munkres) — versión basada en potenciales - constexpr int INF = INT_MAX / 2; - std::vector u(SZ + 1, 0); // Potenciales de filas - std::vector v(SZ + 1, 0); // Potenciales de columnas - std::vector p(SZ + 1, 0); // p[j] = fila asignada a columna j - std::vector way(SZ + 1, 0); - - for (int i = 1; i <= SZ; ++i) { - p[0] = i; - int j0 = 0; - std::vector minv(SZ + 1, INF); - std::vector used(SZ + 1, false); - - do { - used[j0] = true; - int i0 = p[j0]; - int delta = INF; - int j1 = 0; - - for (int j = 1; j <= SZ; ++j) { - if (!used[j]) { - int cur = cost[((i0 - 1) * SZ) + (j - 1)] - u[i0] - v[j]; - if (cur < minv[j]) { - minv[j] = cur; - way[j] = j0; - } - if (minv[j] < delta) { - delta = minv[j]; - j1 = j; - } - } - } - - for (int j = 0; j <= SZ; ++j) { - if (used[j]) { - u[p[j]] += delta; - v[j] -= delta; - } else { - minv[j] -= delta; - } - } - - j0 = j1; - } while (p[j0] != 0); - - do { - int j1 = way[j0]; - p[j0] = p[j1]; - j0 = j1; - } while (j0 != 0); - } - - // Construir la paleta resultante: assignment[j] = fila asignada a columna j - // Queremos result[j] = palette[fila asignada a j] - Palette out{}; - out.fill(0); - for (int j = 1; j <= M && j <= N; ++j) { - int row = p[j] - 1; // Índice 0-based en palette - if (row >= 0 && row < N) { - out[j - 1] = palette[row]; - } - } - - // Colores extra de palette que no tienen pareja en reference (N > M) - if (N > M) { - std::vector used_rows(N, false); - for (int j = 1; j <= M; ++j) { - int row = p[j] - 1; - if (row >= 0 && row < N) { used_rows[row] = true; } - } - int out_idx = M; - for (int i = 0; i < N; ++i) { - if (!used_rows[i]) { - out[out_idx++] = palette[i]; - } - } - } - - return out; + const auto COST = buildCostMatrix(N, M, SZ, palette, reference); + const auto P = hungarianAssign(SZ, COST); + return buildPaletteFromAssignment(N, M, P, palette); } // Asignación greedy de colores a la paleta de referencia. From cbd7f1797856aeea6798b3f1ba88c95d6edbd40d Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 22:02:49 +0200 Subject: [PATCH 12/15] refactor room_format: write*Section per cada part del YAML --- source/game/gameplay/room_format.cpp | 258 ++++++++++++--------------- source/game/gameplay/room_format.hpp | 12 +- 2 files changed, 123 insertions(+), 147 deletions(-) diff --git a/source/game/gameplay/room_format.cpp b/source/game/gameplay/room_format.cpp index b0da8f4..904798f 100644 --- a/source/game/gameplay/room_format.cpp +++ b/source/game/gameplay/room_format.cpp @@ -548,31 +548,17 @@ auto RoomFormat::roomConnectionToYAML(const std::string& connection) -> std::str return connection; } -auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity) - std::ostringstream out; - - // --- Sección room --- +void RoomFormat::writeRoomSection(std::ostringstream& out, const Room::Data& room_data) { out << "room:\n"; // zone es siempre obligatoria out << " zone: " << room_data.zone << "\n"; - // tileSetFile solo si es override explícito del valor heredado de la zona - if (room_data.tile_set_overridden) { - out << " tileSetFile: " << room_data.tile_set_file << "\n"; - } + // tileSetFile/music/bgColor solo si son override explícito del valor heredado de la zona + if (room_data.tile_set_overridden) { out << " tileSetFile: " << room_data.tile_set_file << "\n"; } + if (room_data.music_overridden) { out << " music: " << room_data.music << "\n"; } + if (room_data.bg_color_overridden) { out << " bgColor: " << static_cast(room_data.bg_color) << "\n"; } - // music solo si es override explícito del valor heredado de la zona - if (room_data.music_overridden) { - out << " music: " << room_data.music << "\n"; - } - - // bgColor solo si es override explícito del valor heredado de la zona - if (room_data.bg_color_overridden) { - out << " bgColor: " << static_cast(room_data.bg_color) << "\n"; - } - - // Conexiones out << "\n"; out << " # Conexiones de la habitación (null = sin conexión)\n"; out << " connections:\n"; @@ -580,155 +566,137 @@ auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { // out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n"; out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n"; out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n"; +} - // --- Tilemap (MAP_HEIGHT filas × MAP_WIDTH columnas, formato flow) --- +void RoomFormat::writeTilemapSection(std::ostringstream& out, const Room::Data& room_data) { out << "\n"; out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n"; out << "tilemap:\n"; - // Mapa de dibujo - out << " # Mapa de dibujo (indices de tiles, -1 = vacio)\n"; - out << " draw:\n"; - for (int row = 0; row < Map::HEIGHT; ++row) { - out << " - ["; - for (int col = 0; col < Map::WIDTH; ++col) { - int index = (row * Map::WIDTH) + col; - if (index < static_cast(room_data.tile_map.size())) { - out << room_data.tile_map[index]; - } else { - out << -1; + auto write_grid = [&out](const auto& tilemap, const char* header, int empty_value) { + out << header; + for (int row = 0; row < Map::HEIGHT; ++row) { + out << " - ["; + for (int col = 0; col < Map::WIDTH; ++col) { + const int INDEX = (row * Map::WIDTH) + col; + out << (INDEX < static_cast(tilemap.size()) ? tilemap[INDEX] : empty_value); + if (col < Map::WIDTH - 1) { out << ", "; } } - if (col < Map::WIDTH - 1) { out << ", "; } + out << "]\n"; } - out << "]\n"; - } + }; - // Mapa de colisiones - out << " # Mapa de colisiones (0 = vacio, 1 = solido)\n"; - out << " collision:\n"; - for (int row = 0; row < Map::HEIGHT; ++row) { - out << " - ["; - for (int col = 0; col < Map::WIDTH; ++col) { - int index = (row * Map::WIDTH) + col; - if (index < static_cast(room_data.collision_tile_map.size())) { - out << room_data.collision_tile_map[index]; - } else { - out << 0; - } - if (col < Map::WIDTH - 1) { out << ", "; } - } - out << "]\n"; - } + write_grid(room_data.tile_map, " # Mapa de dibujo (indices de tiles, -1 = vacio)\n draw:\n", -1); + write_grid(room_data.collision_tile_map, " # Mapa de colisiones (0 = vacio, 1 = solido)\n collision:\n", 0); +} - // --- Enemigos --- - if (!room_data.enemies.empty()) { +void RoomFormat::writeEnemiesSection(std::ostringstream& out, const Room::Data& room_data) { + if (room_data.enemies.empty()) { return; } + out << "\n"; + out << "# Enemigos en esta habitación\n"; + out << "enemies:\n"; + for (const auto& enemy : room_data.enemies) { + out << " - animation: " << enemy.animation_path << "\n"; + if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; } + + const int POS_X = static_cast(std::round(enemy.x / Tile::SIZE)); + const int POS_Y = static_cast(std::round(enemy.y / Tile::SIZE)); + out << " position: {x: " << POS_X << ", y: " << POS_Y << "}\n"; + out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n"; + + const int B1_X = enemy.x1 / Tile::SIZE; + const int B1_Y = enemy.y1 / Tile::SIZE; + const int B2_X = enemy.x2 / Tile::SIZE; + const int B2_Y = enemy.y2 / Tile::SIZE; + out << " boundaries:\n"; + out << " position1: {x: " << B1_X << ", y: " << B1_Y << "}\n"; + out << " position2: {x: " << B2_X << ", y: " << B2_Y << "}\n"; + + if (enemy.flip) { out << " flip: true\n"; } + if (enemy.mirror) { out << " mirror: true\n"; } + if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; } out << "\n"; - out << "# Enemigos en esta habitación\n"; - out << "enemies:\n"; - for (const auto& enemy : room_data.enemies) { - out << " - animation: " << enemy.animation_path << "\n"; - if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; } - - int pos_x = static_cast(std::round(enemy.x / Tile::SIZE)); - int pos_y = static_cast(std::round(enemy.y / Tile::SIZE)); - out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n"; - - out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n"; - - int b1_x = enemy.x1 / Tile::SIZE; - int b1_y = enemy.y1 / Tile::SIZE; - int b2_x = enemy.x2 / Tile::SIZE; - int b2_y = enemy.y2 / Tile::SIZE; - out << " boundaries:\n"; - out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n"; - out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n"; - - if (enemy.flip) { out << " flip: true\n"; } - if (enemy.mirror) { out << " mirror: true\n"; } - if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; } - - out << "\n"; - } } +} - // --- Items --- - if (!room_data.items.empty()) { - out << "# Objetos en esta habitación\n"; - out << "items:\n"; - for (const auto& item : room_data.items) { - out << " - tileSetFile: " << item.tile_set_file << "\n"; - out << " tile: " << item.tile << "\n"; +void RoomFormat::writeItemsSection(std::ostringstream& out, const Room::Data& room_data) { + if (room_data.items.empty()) { return; } + out << "# Objetos en esta habitación\n"; + out << "items:\n"; + for (const auto& item : room_data.items) { + out << " - tileSetFile: " << item.tile_set_file << "\n"; + out << " tile: " << item.tile << "\n"; - int item_x = static_cast(std::round(item.x / Tile::SIZE)); - int item_y = static_cast(std::round(item.y / Tile::SIZE)); - out << " position: {x: " << item_x << ", y: " << item_y << "}\n"; + const int ITEM_X = static_cast(std::round(item.x / Tile::SIZE)); + const int ITEM_Y = static_cast(std::round(item.y / Tile::SIZE)); + out << " position: {x: " << ITEM_X << ", y: " << ITEM_Y << "}\n"; - if (item.counter != 0) { - out << " counter: " << item.counter << "\n"; - } - - // color1/color2 solo si son override explícito del default - if (item.color1_overridden) { - out << " color1: " << static_cast(item.color1) << "\n"; - } - if (item.color2_overridden) { - out << " color2: " << static_cast(item.color2) << "\n"; - } - - out << "\n"; - } + if (item.counter != 0) { out << " counter: " << item.counter << "\n"; } + if (item.color1_overridden) { out << " color1: " << static_cast(item.color1) << "\n"; } + if (item.color2_overridden) { out << " color2: " << static_cast(item.color2) << "\n"; } + out << "\n"; } +} - // --- Plataformas --- - if (!room_data.platforms.empty()) { - out << "# Plataformas móviles en esta habitación\n"; - out << "platforms:\n"; - for (const auto& plat : room_data.platforms) { - out << " - animation: " << plat.animation_path << "\n"; - out << " speed: " << plat.speed << "\n"; - out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n"; - if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; } - if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } - out << " path:\n"; - for (const auto& wp : plat.path) { - int wx = static_cast(std::round(wp.x / Tile::SIZE)); - int wy = static_cast(std::round(wp.y / Tile::SIZE)); - out << " - {x: " << wx << ", y: " << wy; - if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; } - out << "}\n"; - } - out << "\n"; +void RoomFormat::writePlatformsSection(std::ostringstream& out, const Room::Data& room_data) { + if (room_data.platforms.empty()) { return; } + out << "# Plataformas móviles en esta habitación\n"; + out << "platforms:\n"; + for (const auto& plat : room_data.platforms) { + out << " - animation: " << plat.animation_path << "\n"; + out << " speed: " << plat.speed << "\n"; + out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n"; + if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; } + if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } + out << " path:\n"; + for (const auto& wp : plat.path) { + const int WX = static_cast(std::round(wp.x / Tile::SIZE)); + const int WY = static_cast(std::round(wp.y / Tile::SIZE)); + out << " - {x: " << WX << ", y: " << WY; + if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; } + out << "}\n"; } + out << "\n"; } +} - // --- Llaves --- - if (!room_data.keys.empty()) { - out << "# Llaves en esta habitación\n"; - out << "keys:\n"; - for (const auto& key : room_data.keys) { - out << " - animation: " << key.animation_path << "\n"; - out << " id: \"" << key.id << "\"\n"; - int kx = static_cast(std::round(key.x / Tile::SIZE)); - int ky = static_cast(std::round(key.y / Tile::SIZE)); - out << " position: {x: " << kx << ", y: " << ky << "}\n"; - out << "\n"; - } +void RoomFormat::writeKeysSection(std::ostringstream& out, const Room::Data& room_data) { + if (room_data.keys.empty()) { return; } + out << "# Llaves en esta habitación\n"; + out << "keys:\n"; + for (const auto& key : room_data.keys) { + out << " - animation: " << key.animation_path << "\n"; + out << " id: \"" << key.id << "\"\n"; + const int KX = static_cast(std::round(key.x / Tile::SIZE)); + const int KY = static_cast(std::round(key.y / Tile::SIZE)); + out << " position: {x: " << KX << ", y: " << KY << "}\n"; + out << "\n"; } +} - // --- Puertas --- - if (!room_data.doors.empty()) { - out << "# Puertas en esta habitación\n"; - out << "doors:\n"; - for (const auto& door : room_data.doors) { - out << " - animation: " << door.animation_path << "\n"; - out << " id: \"" << door.id << "\"\n"; - int dx = static_cast(std::round(door.x / Tile::SIZE)); - int dy = static_cast(std::round(door.y / Tile::SIZE)); - out << " position: {x: " << dx << ", y: " << dy << "}\n"; - out << "\n"; - } +void RoomFormat::writeDoorsSection(std::ostringstream& out, const Room::Data& room_data) { + if (room_data.doors.empty()) { return; } + out << "# Puertas en esta habitación\n"; + out << "doors:\n"; + for (const auto& door : room_data.doors) { + out << " - animation: " << door.animation_path << "\n"; + out << " id: \"" << door.id << "\"\n"; + const int DX = static_cast(std::round(door.x / Tile::SIZE)); + const int DY = static_cast(std::round(door.y / Tile::SIZE)); + out << " position: {x: " << DX << ", y: " << DY << "}\n"; + out << "\n"; } +} +auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { + std::ostringstream out; + writeRoomSection(out, room_data); + writeTilemapSection(out, room_data); + writeEnemiesSection(out, room_data); + writeItemsSection(out, room_data); + writePlatformsSection(out, room_data); + writeKeysSection(out, room_data); + writeDoorsSection(out, room_data); return out.str(); } diff --git a/source/game/gameplay/room_format.hpp b/source/game/gameplay/room_format.hpp index 94f72e7..2ce1479 100644 --- a/source/game/gameplay/room_format.hpp +++ b/source/game/gameplay/room_format.hpp @@ -1,7 +1,8 @@ #pragma once -#include // Para string -#include // Para vector +#include // Para ostringstream +#include // Para string +#include // Para vector #include "external/fkyaml_node.hpp" // Para fkyaml::node #include "game/entities/door.hpp" // Para Door::Data @@ -90,5 +91,12 @@ class RoomFormat { // --- Serialization helpers (solo en debug, los usa saveYAML) --- static auto buildContent(const Room::Data& room_data) -> std::string; static auto roomConnectionToYAML(const std::string& connection) -> std::string; + static void writeRoomSection(std::ostringstream& out, const Room::Data& room_data); + static void writeTilemapSection(std::ostringstream& out, const Room::Data& room_data); + static void writeEnemiesSection(std::ostringstream& out, const Room::Data& room_data); + static void writeItemsSection(std::ostringstream& out, const Room::Data& room_data); + static void writePlatformsSection(std::ostringstream& out, const Room::Data& room_data); + static void writeKeysSection(std::ostringstream& out, const Room::Data& room_data); + static void writeDoorsSection(std::ostringstream& out, const Room::Data& room_data); #endif }; From d99902b9be8aca06eafdb4cdc846d18ae48f258f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 22:06:13 +0200 Subject: [PATCH 13/15] refactor tile_collider: extreu floorYForTile i elimina NOLINT obsolet a checkSlopeBelow --- source/game/gameplay/tile_collider.cpp | 65 ++++++++++++-------------- source/game/gameplay/tile_collider.hpp | 3 ++ 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/source/game/gameplay/tile_collider.cpp b/source/game/gameplay/tile_collider.cpp index fcfbe2e..ba6b50b 100644 --- a/source/game/gameplay/tile_collider.cpp +++ b/source/game/gameplay/tile_collider.cpp @@ -98,46 +98,41 @@ auto TileCollider::checkCeiling(float x, float y, float w) const -> float { // WALL: bloquea si los pies estaban por encima (como PASSABLE). // PASSABLE: solo si los pies estaban por encima del borde superior del tile. // SLOPE: siempre bloquea (las slopes son sólidas, como muros en diagonal). -// NOLINTNEXTLINE(readability-function-cognitive-complexity) + +// Calcula la y del suelo per a un tile concret. Retorna Collision::NONE si el tile +// no actua com a suelo en aquest cas (segons el tipus i el rang del moviment). +auto TileCollider::floorYForTile(Tile tile, int col, int row, float x, float w, float foot_y_current, float foot_y_new) const -> float { + if (tile == Tile::WALL || tile == Tile::PASSABLE) { + const float TILE_TOP = toPixel(row); + // WALL/PASSABLE només compten si els peus estaven per damunt abans del moviment + return (foot_y_current <= TILE_TOP) ? TILE_TOP : Collision::NONE; + } + if (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R) { + const float CHECK_X = (tile == Tile::SLOPE_L) ? x : x + w - 1; + const float SLOPE_Y = getSlopeY(col, row, CHECK_X); + // Slopes són sòlides: aterrar sempre que els peus arriben a la superfície + return (foot_y_new >= SLOPE_Y) ? SLOPE_Y : Collision::NONE; + } + return Collision::NONE; +} + auto TileCollider::checkFloor(float x, float foot_y_current, float w, float foot_y_new) const -> FloorHit { - int start_row = toTile(static_cast(foot_y_current)); - int end_row = toTile(static_cast(foot_y_new)); - int left_col = toTile(static_cast(x)); - int right_col = toTile(static_cast(x + w - 1)); + const int START_ROW = toTile(static_cast(foot_y_current)); + const int END_ROW = toTile(static_cast(foot_y_new)); + const int LEFT_COL = toTile(static_cast(x)); + const int RIGHT_COL = toTile(static_cast(x + w - 1)); FloorHit best; - - for (int row = start_row; row <= end_row; ++row) { - for (int col = left_col; col <= right_col; ++col) { - auto tile = getTileAt(col, row); - float floor_y = Collision::NONE; - - if (tile == Tile::WALL) { - float tile_top = toPixel(row); - if (foot_y_current <= tile_top) { - floor_y = tile_top; - } - } else if (tile == Tile::PASSABLE) { - float tile_top = toPixel(row); - // Solo cuenta como suelo si los pies estaban por encima antes del movimiento - if (foot_y_current <= tile_top) { - floor_y = tile_top; - } - } else if (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R) { - float check_x = (tile == Tile::SLOPE_L) ? x : x + w - 1; - float slope_y = getSlopeY(col, row, check_x); - // Slopes son sólidas: aterrizar siempre que los pies lleguen a la superficie - if (foot_y_new >= slope_y) { - floor_y = slope_y; - } - } - - if (floor_y != Collision::NONE && (best.y == Collision::NONE || floor_y < best.y)) { - best = {.y = floor_y, .type = tile, .tile_x = col, .tile_y = row}; + for (int row = START_ROW; row <= END_ROW; ++row) { + for (int col = LEFT_COL; col <= RIGHT_COL; ++col) { + const auto TILE = getTileAt(col, row); + const float FLOOR_Y = floorYForTile(TILE, col, row, x, w, foot_y_current, foot_y_new); + if (FLOOR_Y == Collision::NONE) { continue; } + if (best.y == Collision::NONE || FLOOR_Y < best.y) { + best = {.y = FLOOR_Y, .type = TILE, .tile_x = col, .tile_y = row}; } } } - return best; } @@ -196,7 +191,7 @@ auto TileCollider::isInsideAnySlope(float x, float foot_y, float w) const -> boo // Busca una slope directamente debajo del jugador (para transición ground→slope). // Escanea la fila de los pies Y la fila superior: las slopes en escalera siempre // tienen el tile de entrada una fila arriba del suelo desde el que se accede. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) + auto TileCollider::checkSlopeBelow(float x, float foot_y, float w) const -> SlopeInfo { int foot_row = toTile(static_cast(foot_y)); int left_col = toTile(static_cast(x)); diff --git a/source/game/gameplay/tile_collider.hpp b/source/game/gameplay/tile_collider.hpp index 454956f..25ccced 100644 --- a/source/game/gameplay/tile_collider.hpp +++ b/source/game/gameplay/tile_collider.hpp @@ -70,4 +70,7 @@ class TileCollider { int border_px_; // Offset en píxeles (CollisionBorder::PX) const std::vector& tile_map_; + + // Calcula la y del suelo per a un tile concret (helper de checkFloor) + [[nodiscard]] auto floorYForTile(Tile tile, int col, int row, float x, float w, float foot_y_current, float foot_y_new) const -> float; }; From 464abd505a4069cbbb7faa950ba6667d9e1aeef6 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 22:06:42 +0200 Subject: [PATCH 14/15] elimina 10 NOLINT cognitive-complexity obsolets a map_editor --- source/game/editor/map_editor.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index ebcab76..ea805a1 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -433,7 +433,7 @@ void MapEditor::render() { } // Maneja eventos del editor -void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity) +void MapEditor::handleEvent(const SDL_Event& event) { // Si el tile picker está abierto, los eventos van a él. // Excepción: la T lo cierra como toggle (sin tocar el brush). const auto* kc = KeyConfig::get(); @@ -1374,7 +1374,7 @@ void MapEditor::updateMousePosition() { } // Actualiza la información de la barra de estado -void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitive-complexity) +void MapEditor::updateStatusBarInfo() { if (!statusbar_) { return; } statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_); @@ -1563,7 +1563,7 @@ auto MapEditor::getAnimationCompletions() const -> std::vector { } // Modifica una propiedad del enemigo seleccionado -auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity) +auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } @@ -1730,7 +1730,7 @@ auto MapEditor::duplicateEnemy() -> std::string { } // Modifica una propiedad de la habitación -auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity) +auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } std::string val = toLower(value); @@ -1921,7 +1921,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& } // Crea una nueva habitación -auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // NOLINT(readability-function-cognitive-complexity) +auto MapEditor::createNewRoom(const std::string& direction) -> std::string { if (!active_) { return "Editor not active"; } // Validar dirección si se proporcionó @@ -2056,7 +2056,7 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // } // Elimina la habitación actual -auto MapEditor::deleteRoom() -> std::string { // NOLINT(readability-function-cognitive-complexity) +auto MapEditor::deleteRoom() -> std::string { if (!active_) { return "Editor not active"; } std::string deleted_name = room_path_; From 20ed92965ba4ee95d295cdd4ba7a6c68df110071 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 17 May 2026 22:07:32 +0200 Subject: [PATCH 15/15] elimina 4 NOLINT cognitive-complexity obsolets restants a map_editor --- source/game/editor/map_editor.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index ea805a1..0ea96c1 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -708,7 +708,6 @@ void MapEditor::handleMouseUp() { } // Commit de un drag de entidad (initial, bound1, bound2) para cualquier EntityType. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con una rama por tipo; refactor a visitor requiere cambio de diseño. auto MapEditor::commitEntityDrag() -> bool { const int IDX = drag_.index; const int SNAP_X = static_cast(drag_.snap_x); @@ -816,7 +815,6 @@ auto MapEditor::commitEntityDrag() -> bool { } // Mueve visualmente la entidad arrastrada a la posición snapped. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con cases paralelos para cada tipo. void MapEditor::moveEntityVisual() { switch (drag_.target) { case DragTarget::ENTITY_INITIAL: @@ -958,7 +956,6 @@ void MapEditor::renderSelectionHighlight() { } // Estampa el patrón del brush en la posición indicada (anclaje top-left). -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- nested loops + casos TRANSPARENT/ERASE/tile normal y ramas collision vs normal. void MapEditor::stampBrushAt(int tile_x, int tile_y) { if (brush_.isEmpty()) { return; } for (int dy = 0; dy < brush_.height; ++dy) { @@ -1233,7 +1230,6 @@ auto MapEditor::entityLabel(EntityType type) -> const char* { } // Dibuja marcadores de boundaries y líneas de ruta para enemigos y plataformas. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con ramas de enemigo patrullante vs plataforma con waypoints. void MapEditor::renderEntityBoundaries() { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; }