diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index d99e650..3e73cea 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 ranges::find_if +#include // Para 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 @@ -151,68 +153,48 @@ namespace GlobalInputs { Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")}); } + // F4 amb modificadors: Ctrl=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; + } + // Detecta qué acción global ha sido presionada (si alguna) - auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity) + auto getPressedAction() -> InputAction { // Qualsevol botó del comandament actua com a ACCEPT (saltar escenes // d'attract mode: logo, loading, credits, demo, ending...). El botó // BACK queda filtrat prèviament a GlobalEvents per no colidir amb EXIT // (excepte en emscripten, on BACK no pot sortir i sí pot saltar). - if (GlobalEvents::consumeGamepadButtonPressed()) { - return InputAction::ACCEPT; - } - 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; - } + if (GlobalEvents::consumeGamepadButtonPressed()) { return InputAction::ACCEPT; } + + 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; - } - return InputAction::NONE; + + if (const InputAction ACT = getShaderAction(); ACT != InputAction::NONE) { return ACT; } + if (const InputAction ACT = getPaletteAction(); ACT != InputAction::NONE) { return ACT; } + + return firstPressedFrom({InputAction::NEXT_PALETTE_SORT, InputAction::TOGGLE_INTEGER_SCALE, InputAction::TOGGLE_VSYNC, InputAction::TOGGLE_INFO, InputAction::TOGGLE_CONSOLE}); } } // namespace diff --git a/source/core/rendering/gif.cpp b/source/core/rendering/gif.cpp index 77846cc..6fac8f2 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/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 4ad3452..b89b96f 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -20,7 +20,6 @@ // MSL shaders (Metal Shading Language) — macOS // ============================================================================ -// NOLINTBEGIN(readability-identifier-naming) static const char* POSTFX_VERT_MSL = R"( #include using namespace metal; @@ -360,7 +359,6 @@ fragment float4 crtpi_fs(PostVOut in [[stage_in]], return float4(colour, 1.0f); } )"; -// NOLINTEND(readability-identifier-naming) #endif // __APPLE__ @@ -525,25 +523,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_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* frag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + return createShaderMSL(device_, POSTFX_VERT_MSL, "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 = {}; @@ -551,145 +554,55 @@ 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; + } + + // --------------------------------------------------------------------------- + // createPipeline — crea els 4 pipelines del flux PostFX + // --------------------------------------------------------------------------- + auto SDL3GPUShader::createPipeline() -> bool { + const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_); + const SDL_GPUTextureFormat OFFSCREEN_FMT = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; - // ---- Upscale pipeline (scene → scaled_texture_, nearest) ---- #ifdef __APPLE__ - SDL_GPUShader* uvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* ufrag = createShaderMSL(device_, UPSCALE_FRAG_MSL, "upscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0); + SDL_GPUShader* postfx_frag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + SDL_GPUShader* upscale_frag = createShaderMSL(device_, UPSCALE_FRAG_MSL, "upscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0); + SDL_GPUShader* offscreen_frag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + SDL_GPUShader* downscale_frag = createShaderMSL(device_, DOWNSCALE_FRAG_MSL, "downscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #else - SDL_GPUShader* uvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* ufrag = createShaderSPIRV(device_, kupscale_frag_spv, kupscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0); + SDL_GPUShader* postfx_frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + SDL_GPUShader* upscale_frag = createShaderSPIRV(device_, kupscale_frag_spv, kupscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0); + SDL_GPUShader* offscreen_frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + SDL_GPUShader* downscale_frag = createShaderSPIRV(device_, kdownscale_frag_spv, kdownscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #endif - if ((uvert == nullptr) || (ufrag == nullptr)) { - SDL_Log("SDL3GPUShader: failed to compile upscale shaders"); - if (uvert != nullptr) { SDL_ReleaseGPUShader(device_, uvert); } - if (ufrag != nullptr) { SDL_ReleaseGPUShader(device_, ufrag); } - return false; - } + pipeline_ = createPostfxLikePipeline(postfx_frag, SWAPCHAIN_FMT, "PostFX"); + upscale_pipeline_ = createPostfxLikePipeline(upscale_frag, OFFSCREEN_FMT, "upscale"); + postfx_offscreen_pipeline_ = createPostfxLikePipeline(offscreen_frag, OFFSCREEN_FMT, "PostFX offscreen"); + downscale_pipeline_ = createPostfxLikePipeline(downscale_frag, SWAPCHAIN_FMT, "downscale"); - SDL_GPUColorTargetDescription upscale_color_target = {}; - upscale_color_target.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; - upscale_color_target.blend_state = no_blend; - - SDL_GPUGraphicsPipelineCreateInfo upscale_pipe_info = {}; - upscale_pipe_info.vertex_shader = uvert; - upscale_pipe_info.fragment_shader = ufrag; - upscale_pipe_info.vertex_input_state = no_input; - upscale_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - upscale_pipe_info.target_info.num_color_targets = 1; - upscale_pipe_info.target_info.color_target_descriptions = &upscale_color_target; - - upscale_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &upscale_pipe_info); - - SDL_ReleaseGPUShader(device_, uvert); - SDL_ReleaseGPUShader(device_, ufrag); - - if (upscale_pipeline_ == nullptr) { - SDL_Log("SDL3GPUShader: upscale pipeline creation failed: %s", SDL_GetError()); - return false; - } - - // ---- PostFX offscreen pipeline (scaled_texture_ → postfx_texture_, B8G8R8A8) ---- - // Mismos shaders que pipeline_ pero con formato de salida B8G8R8A8_UNORM para textura intermedia. -#ifdef __APPLE__ - SDL_GPUShader* ofvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* offrag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); -#else - SDL_GPUShader* ofvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* offrag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); -#endif - - if ((ofvert == nullptr) || (offrag == nullptr)) { - SDL_Log("SDL3GPUShader: failed to compile PostFX offscreen shaders"); - if (ofvert != nullptr) { SDL_ReleaseGPUShader(device_, ofvert); } - if (offrag != nullptr) { SDL_ReleaseGPUShader(device_, offrag); } - return false; - } - - SDL_GPUColorTargetDescription offscreen_color_target = {}; - offscreen_color_target.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; - offscreen_color_target.blend_state = no_blend; - - SDL_GPUGraphicsPipelineCreateInfo offscreen_pipe_info = {}; - offscreen_pipe_info.vertex_shader = ofvert; - offscreen_pipe_info.fragment_shader = offrag; - offscreen_pipe_info.vertex_input_state = no_input; - offscreen_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - offscreen_pipe_info.target_info.num_color_targets = 1; - offscreen_pipe_info.target_info.color_target_descriptions = &offscreen_color_target; - - postfx_offscreen_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &offscreen_pipe_info); - - SDL_ReleaseGPUShader(device_, ofvert); - SDL_ReleaseGPUShader(device_, offrag); - - if (postfx_offscreen_pipeline_ == nullptr) { - SDL_Log("SDL3GPUShader: PostFX offscreen pipeline creation failed: %s", SDL_GetError()); - return false; - } - - // ---- Downscale pipeline (postfx_texture_ → swapchain, Lanczos) ---- -#ifdef __APPLE__ - SDL_GPUShader* dvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* dfrag = createShaderMSL(device_, DOWNSCALE_FRAG_MSL, "downscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); -#else - SDL_GPUShader* dvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* dfrag = createShaderSPIRV(device_, kdownscale_frag_spv, kdownscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); -#endif - - if ((dvert == nullptr) || (dfrag == nullptr)) { - SDL_Log("SDL3GPUShader: failed to compile downscale shaders"); - if (dvert != nullptr) { SDL_ReleaseGPUShader(device_, dvert); } - if (dfrag != nullptr) { SDL_ReleaseGPUShader(device_, dfrag); } - return false; - } - - SDL_GPUColorTargetDescription downscale_color_target = {}; - downscale_color_target.format = SWAPCHAIN_FMT; - downscale_color_target.blend_state = no_blend; - - SDL_GPUGraphicsPipelineCreateInfo downscale_pipe_info = {}; - downscale_pipe_info.vertex_shader = dvert; - downscale_pipe_info.fragment_shader = dfrag; - downscale_pipe_info.vertex_input_state = no_input; - downscale_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - downscale_pipe_info.target_info.num_color_targets = 1; - downscale_pipe_info.target_info.color_target_descriptions = &downscale_color_target; - - downscale_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &downscale_pipe_info); - - SDL_ReleaseGPUShader(device_, dvert); - SDL_ReleaseGPUShader(device_, dfrag); - - if (downscale_pipeline_ == nullptr) { - SDL_Log("SDL3GPUShader: downscale pipeline creation failed: %s", SDL_GetError()); - return false; - } - - return true; + return (pipeline_ != nullptr) && (upscale_pipeline_ != nullptr) && (postfx_offscreen_pipeline_ != nullptr) && (downscale_pipeline_ != nullptr); } // --------------------------------------------------------------------------- @@ -700,51 +613,13 @@ namespace Rendering { // --------------------------------------------------------------------------- auto SDL3GPUShader::createCrtPiPipeline() -> bool { const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_); - #ifdef __APPLE__ - SDL_GPUShader* vert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* frag = createShaderMSL(device_, CRTPI_FRAG_MSL, "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; } // --------------------------------------------------------------------------- @@ -762,93 +637,76 @@ namespace Rendering { } // Copia directa — el upscale lo hace la GPU en el primer render pass - std::memcpy(mapped, pixels, static_cast(width) * static_cast(height) * 4); + std::memcpy(mapped, pixels, static_cast(width) * height * 4); SDL_UnmapGPUTransferBuffer(device_, upload_buffer_); } // --------------------------------------------------------------------------- - // render — upload scene texture + PostFX pass → swapchain + // maybeRescaleSsTexture — recalcula factor SS i recrea scaled_texture_ si cal // --------------------------------------------------------------------------- - void SDL3GPUShader::render() { // NOLINT(readability-function-cognitive-complexity) - if (!is_initialized_) { return; } - - // Paso 0: si SS activo, calcular el factor necesario según el zoom actual y recrear si cambió. - // Factor = primer múltiplo de 3 >= zoom (mín 3). Se recrea solo en saltos de factor. - if (oversample_ > 1 && game_height_ > 0) { - int win_w = 0; - int win_h = 0; - SDL_GetWindowSizeInPixels(window_, &win_w, &win_h); - const float ZOOM = static_cast(win_h) / static_cast(game_height_); - const int NEED_FACTOR = calcSsFactor(ZOOM); - if (NEED_FACTOR != ss_factor_) { - SDL_WaitForGPUIdle(device_); - recreateScaledTexture(NEED_FACTOR); - } + void SDL3GPUShader::maybeRescaleSsTexture() { + if (oversample_ <= 1 || game_height_ <= 0) { return; } + int win_w = 0; + int win_h = 0; + SDL_GetWindowSizeInPixels(window_, &win_w, &win_h); + const float ZOOM = static_cast(win_h) / static_cast(game_height_); + const int NEED_FACTOR = calcSsFactor(ZOOM); + if (NEED_FACTOR != ss_factor_) { + SDL_WaitForGPUIdle(device_); + recreateScaledTexture(NEED_FACTOR); } + } - SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_); - if (cmd == nullptr) { - SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError()); - return; - } - - // ---- Copy pass: transfer buffer → scene texture (siempre a resolución del juego) ---- + // --------------------------------------------------------------------------- + // uploadSceneTexture — copy pass: transfer buffer → scene texture + // --------------------------------------------------------------------------- + void SDL3GPUShader::uploadSceneTexture(SDL_GPUCommandBuffer* cmd) { SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd); - if (copy != nullptr) { - SDL_GPUTextureTransferInfo src = {}; - src.transfer_buffer = upload_buffer_; - src.offset = 0; - src.pixels_per_row = static_cast(game_width_); - src.rows_per_layer = static_cast(game_height_); + if (copy == nullptr) { return; } - SDL_GPUTextureRegion dst = {}; - dst.texture = scene_texture_; - dst.w = static_cast(game_width_); - dst.h = static_cast(game_height_); - dst.d = 1; + SDL_GPUTextureTransferInfo src = {}; + src.transfer_buffer = upload_buffer_; + src.offset = 0; + src.pixels_per_row = static_cast(game_width_); + src.rows_per_layer = static_cast(game_height_); - SDL_UploadToGPUTexture(copy, &src, &dst, false); - SDL_EndGPUCopyPass(copy); - } + SDL_GPUTextureRegion dst = {}; + dst.texture = scene_texture_; + dst.w = static_cast(game_width_); + dst.h = static_cast(game_height_); + dst.d = 1; - // ---- Upscale pass: scene_texture_ → scaled_texture_ (NEAREST o LINEAR según linear_upscale_) ---- - if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) { - SDL_GPUColorTargetInfo upscale_target = {}; - upscale_target.texture = scaled_texture_; - upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE; - upscale_target.store_op = SDL_GPU_STOREOP_STORE; + SDL_UploadToGPUTexture(copy, &src, &dst, false); + SDL_EndGPUCopyPass(copy); + } - SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr); - if (upass != nullptr) { - SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_); - SDL_GPUTextureSamplerBinding ubinding = {}; - ubinding.texture = scene_texture_; - ubinding.sampler = (linear_upscale_ && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_; - SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1); - SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0); - SDL_EndGPURenderPass(upass); - } - } + // --------------------------------------------------------------------------- + // runUpscalePass — scene_texture_ → scaled_texture_ (NEAREST o LINEAR segons linear_upscale_) + // --------------------------------------------------------------------------- + void SDL3GPUShader::runUpscalePass(SDL_GPUCommandBuffer* cmd) { + if (oversample_ <= 1 || scaled_texture_ == nullptr || upscale_pipeline_ == nullptr) { return; } - // ---- Acquire swapchain texture ---- - SDL_GPUTexture* swapchain = nullptr; - Uint32 sw = 0; - Uint32 sh = 0; - if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) { - SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError()); - SDL_SubmitGPUCommandBuffer(cmd); - return; - } - if (swapchain == nullptr) { - // Window minimized — skip frame - SDL_SubmitGPUCommandBuffer(cmd); - return; - } + SDL_GPUColorTargetInfo target = {}; + target.texture = scaled_texture_; + target.load_op = SDL_GPU_LOADOP_DONT_CARE; + target.store_op = SDL_GPU_STOREOP_STORE; - // ---- Calcular viewport (dimensiones lógicas del canvas, no de textura GPU) ---- - float vx = 0.0F; - float vy = 0.0F; + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr); + if (pass == nullptr) { return; } + SDL_BindGPUGraphicsPipeline(pass, upscale_pipeline_); + SDL_GPUTextureSamplerBinding binding = {}; + binding.texture = scene_texture_; + binding.sampler = (linear_upscale_ && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_; + SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); + SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); + SDL_EndGPURenderPass(pass); + } + + // --------------------------------------------------------------------------- + // computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox) + // --------------------------------------------------------------------------- + auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport { float vw = 0.0F; float vh = 0.0F; if (integer_scale_) { @@ -862,131 +720,172 @@ namespace Rendering { vw = static_cast(game_width_) * SCALE; vh = static_cast(game_height_) * SCALE; } - vx = std::floor((static_cast(sw) - vw) * 0.5F); - vy = std::floor((static_cast(sh) - vh) * 0.5F); + const float VX = std::floor((static_cast(sw) - vw) * 0.5F); + const float VY = std::floor((static_cast(sh) - vh) * 0.5F); + return {.x = VX, .y = VY, .w = vw, .h = vh}; + } - // pixel_scale: subpíxeles por pixel lógico. - // Sin SS: vh/game_height (zoom de ventana). - // Con SS: ss_factor_ exacto (3, 6, 9...). + // --------------------------------------------------------------------------- + // updateDynamicUniforms — actualitza pixel_scale, time, oversample per a aquest frame + // --------------------------------------------------------------------------- + void SDL3GPUShader::updateDynamicUniforms(float viewport_h) { + // pixel_scale: subpíxels per pixel lògic. Amb SS: ss_factor_ exacte; sense SS: zoom de finestra. if (oversample_ > 1 && ss_factor_ > 0) { uniforms_.pixel_scale = static_cast(ss_factor_); } else { - uniforms_.pixel_scale = (game_height_ > 0) ? (vh / static_cast(game_height_)) : 1.0F; + uniforms_.pixel_scale = (game_height_ > 0) ? (viewport_h / static_cast(game_height_)) : 1.0F; } uniforms_.time = static_cast(SDL_GetTicks()) / 1000.0F; - uniforms_.oversample = (oversample_ > 1 && ss_factor_ > 0) - ? static_cast(ss_factor_) - : 1.0F; + uniforms_.oversample = (oversample_ > 1 && ss_factor_ > 0) ? static_cast(ss_factor_) : 1.0F; + } - // ---- Path CrtPi: directo scene_texture_ → swapchain, sin SS ni Lanczos ---- - if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) { - SDL_GPUColorTargetInfo color_target = {}; - color_target.texture = swapchain; - color_target.load_op = SDL_GPU_LOADOP_CLEAR; - color_target.store_op = SDL_GPU_STOREOP_STORE; - color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + // --------------------------------------------------------------------------- + // runCrtPiPass — scene_texture_ → swapchain via pipeline CrtPi (sense SS ni Lanczos) + // --------------------------------------------------------------------------- + void SDL3GPUShader::runCrtPiPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) { + SDL_GPUColorTargetInfo color_target = {}; + color_target.texture = swapchain; + color_target.load_op = SDL_GPU_LOADOP_CLEAR; + color_target.store_op = SDL_GPU_STOREOP_STORE; + color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; - SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); - if (pass != nullptr) { - SDL_BindGPUGraphicsPipeline(pass, crtpi_pipeline_); - SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F}; - SDL_SetGPUViewport(pass, &vp); + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); + if (pass == nullptr) { return; } + SDL_BindGPUGraphicsPipeline(pass, crtpi_pipeline_); + SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F}; + SDL_SetGPUViewport(pass, &sdlvp); - SDL_GPUTextureSamplerBinding binding = {}; - binding.texture = scene_texture_; - binding.sampler = sampler_; // NEAREST: el shader CrtPi hace su propio filtrado analítico - SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); + SDL_GPUTextureSamplerBinding binding = {}; + binding.texture = scene_texture_; + binding.sampler = sampler_; // NEAREST: el shader CrtPi fa el seu filtrat analític + SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); - // Inyectar texture_width/height antes del push - crtpi_uniforms_.texture_width = static_cast(game_width_); - crtpi_uniforms_.texture_height = static_cast(game_height_); - SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms)); + crtpi_uniforms_.texture_width = static_cast(game_width_); + crtpi_uniforms_.texture_height = static_cast(game_height_); + SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms)); - SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); - SDL_EndGPURenderPass(pass); - } + SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); + SDL_EndGPURenderPass(pass); + } + // --------------------------------------------------------------------------- + // runLanczosPasses — scaled_texture_ → postfx_texture_ (PostFX) → swapchain (Lanczos) + // --------------------------------------------------------------------------- + void SDL3GPUShader::runLanczosPasses(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) { + // Pass A: PostFX → postfx_texture_ (full scaled size, sense viewport) + SDL_GPUColorTargetInfo postfx_target = {}; + postfx_target.texture = postfx_texture_; + postfx_target.load_op = SDL_GPU_LOADOP_CLEAR; + postfx_target.store_op = SDL_GPU_STOREOP_STORE; + postfx_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + + SDL_GPURenderPass* ppass = SDL_BeginGPURenderPass(cmd, &postfx_target, 1, nullptr); + if (ppass != nullptr) { + SDL_BindGPUGraphicsPipeline(ppass, postfx_offscreen_pipeline_); + SDL_GPUTextureSamplerBinding pbinding = {}; + pbinding.texture = scaled_texture_; + pbinding.sampler = sampler_; // NEAREST: 1:1 pass, efectes calculats analíticament + SDL_BindGPUFragmentSamplers(ppass, 0, &pbinding, 1); + SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); + SDL_DrawGPUPrimitives(ppass, 3, 1, 0, 0); + SDL_EndGPURenderPass(ppass); + } + + // Pass B: Downscale Lanczos → swapchain (amb viewport/letterbox) + SDL_GPUColorTargetInfo ds_target = {}; + ds_target.texture = swapchain; + ds_target.load_op = SDL_GPU_LOADOP_CLEAR; + ds_target.store_op = SDL_GPU_STOREOP_STORE; + ds_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + + SDL_GPURenderPass* dpass = SDL_BeginGPURenderPass(cmd, &ds_target, 1, nullptr); + if (dpass == nullptr) { return; } + SDL_BindGPUGraphicsPipeline(dpass, downscale_pipeline_); + SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F}; + SDL_SetGPUViewport(dpass, &sdlvp); + SDL_GPUTextureSamplerBinding dbinding = {}; + dbinding.texture = postfx_texture_; + dbinding.sampler = sampler_; // NEAREST: el shader Lanczos fa la seua pròpia interpolació + SDL_BindGPUFragmentSamplers(dpass, 0, &dbinding, 1); + // algorithm: 0=Lanczos2, 1=Lanczos3 (downscale_algo_ és 1-based) + DownscaleUniforms downscale_u = {.algorithm = downscale_algo_ - 1, .pad0 = 0.0F, .pad1 = 0.0F, .pad2 = 0.0F}; + SDL_PushGPUFragmentUniformData(cmd, 0, &downscale_u, sizeof(DownscaleUniforms)); + SDL_DrawGPUPrimitives(dpass, 3, 1, 0, 0); + SDL_EndGPURenderPass(dpass); + } + + // --------------------------------------------------------------------------- + // runDirectPostfxPass — PostFX → swapchain directament (sense Lanczos) + // --------------------------------------------------------------------------- + void SDL3GPUShader::runDirectPostfxPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) { + SDL_GPUColorTargetInfo color_target = {}; + color_target.texture = swapchain; + color_target.load_op = SDL_GPU_LOADOP_CLEAR; + color_target.store_op = SDL_GPU_STOREOP_STORE; + color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); + if (pass == nullptr) { return; } + SDL_BindGPUGraphicsPipeline(pass, pipeline_); + SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F}; + SDL_SetGPUViewport(pass, &sdlvp); + + // Amb SS: llegir de scaled_texture_ amb LINEAR; sense SS: scene_texture_ amb NEAREST. + SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr) ? scaled_texture_ : scene_texture_; + SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_; + + SDL_GPUTextureSamplerBinding binding = {}; + binding.texture = input_texture; + binding.sampler = active_sampler; + SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); + + SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); + SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); + SDL_EndGPURenderPass(pass); + } + + // --------------------------------------------------------------------------- + // render — orquestra upload + upscale + path PostFX (CrtPi / Lanczos / direct) + // --------------------------------------------------------------------------- + void SDL3GPUShader::render() { + if (!is_initialized_) { return; } + + maybeRescaleSsTexture(); + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_); + if (cmd == nullptr) { + SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError()); + return; + } + + uploadSceneTexture(cmd); + runUpscalePass(cmd); + + SDL_GPUTexture* swapchain = nullptr; + Uint32 sw = 0; + Uint32 sh = 0; + if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) { + SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError()); + SDL_SubmitGPUCommandBuffer(cmd); + return; + } + if (swapchain == nullptr) { + // Finestra minimitzada — saltem el frame SDL_SubmitGPUCommandBuffer(cmd); return; } - // ---- Determinar si usar el path Lanczos (SS activo + algo seleccionado) ---- + const Viewport VP = computeViewport(sw, sh); + updateDynamicUniforms(VP.h); + const bool USE_LANCZOS = (oversample_ > 1 && downscale_algo_ > 0 && scaled_texture_ != nullptr && postfx_texture_ != nullptr && postfx_offscreen_pipeline_ != nullptr && downscale_pipeline_ != nullptr); - if (USE_LANCZOS) { - // ---- Pass A: PostFX → postfx_texture_ (full scaled size, sin viewport) ---- - SDL_GPUColorTargetInfo postfx_target = {}; - postfx_target.texture = postfx_texture_; - postfx_target.load_op = SDL_GPU_LOADOP_CLEAR; - postfx_target.store_op = SDL_GPU_STOREOP_STORE; - postfx_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; - - SDL_GPURenderPass* ppass = SDL_BeginGPURenderPass(cmd, &postfx_target, 1, nullptr); - if (ppass != nullptr) { - SDL_BindGPUGraphicsPipeline(ppass, postfx_offscreen_pipeline_); - SDL_GPUTextureSamplerBinding pbinding = {}; - pbinding.texture = scaled_texture_; - pbinding.sampler = sampler_; // NEAREST: 1:1 pass, efectos calculados analíticamente - SDL_BindGPUFragmentSamplers(ppass, 0, &pbinding, 1); - SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); - SDL_DrawGPUPrimitives(ppass, 3, 1, 0, 0); - SDL_EndGPURenderPass(ppass); - } - - // ---- Pass B: Downscale Lanczos → swapchain (con viewport/letterbox) ---- - SDL_GPUColorTargetInfo ds_target = {}; - ds_target.texture = swapchain; - ds_target.load_op = SDL_GPU_LOADOP_CLEAR; - ds_target.store_op = SDL_GPU_STOREOP_STORE; - ds_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; - - SDL_GPURenderPass* dpass = SDL_BeginGPURenderPass(cmd, &ds_target, 1, nullptr); - if (dpass != nullptr) { - SDL_BindGPUGraphicsPipeline(dpass, downscale_pipeline_); - SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F}; - SDL_SetGPUViewport(dpass, &vp); - SDL_GPUTextureSamplerBinding dbinding = {}; - dbinding.texture = postfx_texture_; - dbinding.sampler = sampler_; // NEAREST: el shader Lanczos hace su propia interpolación - SDL_BindGPUFragmentSamplers(dpass, 0, &dbinding, 1); - // algorithm: 0=Lanczos2, 1=Lanczos3 (downscale_algo_ es 1-based) - DownscaleUniforms downscale_u = {.algorithm = downscale_algo_ - 1, .pad0 = 0.0F, .pad1 = 0.0F, .pad2 = 0.0F}; - SDL_PushGPUFragmentUniformData(cmd, 0, &downscale_u, sizeof(DownscaleUniforms)); - SDL_DrawGPUPrimitives(dpass, 3, 1, 0, 0); - SDL_EndGPURenderPass(dpass); - } + if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) { + runCrtPiPass(cmd, swapchain, VP); + } else if (USE_LANCZOS) { + runLanczosPasses(cmd, swapchain, VP); } else { - // ---- Render pass: PostFX → swapchain directamente (bilinear, comportamiento original) ---- - SDL_GPUColorTargetInfo color_target = {}; - color_target.texture = swapchain; - color_target.load_op = SDL_GPU_LOADOP_CLEAR; - color_target.store_op = SDL_GPU_STOREOP_STORE; - color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; - - SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); - if (pass != nullptr) { - SDL_BindGPUGraphicsPipeline(pass, pipeline_); - SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F}; - SDL_SetGPUViewport(pass, &vp); - - // Con SS: leer de scaled_texture_ con LINEAR; sin SS: scene_texture_ con NEAREST. - SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr) - ? scaled_texture_ - : scene_texture_; - SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr) - ? linear_sampler_ - : sampler_; - - SDL_GPUTextureSamplerBinding binding = {}; - binding.texture = input_texture; - binding.sampler = active_sampler; - SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); - - SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); - - SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); - SDL_EndGPURenderPass(pass); - } + runDirectPostfxPass(cmd, swapchain, VP); } SDL_SubmitGPUCommandBuffer(cmd); diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index 6298898..57393c6 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -137,7 +137,24 @@ namespace Rendering { Uint32 num_uniform_buffers) -> SDL_GPUShader*; auto createPipeline() -> bool; - auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi + auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi + auto createPostfxVertexShader() -> SDL_GPUShader*; // Vertex shader fullscreen-triangle compartido (MSL/SPIRV) + // Empaqueta el patrón vert(postfx) + frag dado + target format en un pipeline gráfico. + // Toma ownership de `frag`: lo libera tras crear el pipeline (o si vert falla). + auto createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline*; + + // Sub-passos de render() (extrets per reduir complexitat ciclomàtica) + struct Viewport { + float x, y, w, h; + }; + void maybeRescaleSsTexture(); + void uploadSceneTexture(SDL_GPUCommandBuffer* cmd); + void runUpscalePass(SDL_GPUCommandBuffer* cmd); + [[nodiscard]] auto computeViewport(Uint32 sw, Uint32 sh) const -> Viewport; + void updateDynamicUniforms(float viewport_h); + void runCrtPiPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp); + void runLanczosPasses(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp); + void runDirectPostfxPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp); auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_ auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3) diff --git a/source/core/resources/resource_list.cpp b/source/core/resources/resource_list.cpp index 64d1b75..ef25380 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-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..c791f79 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 static (públics perquè els fan servir parsers externs del .cpp) --- + [[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo + [[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta + 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 diff --git a/source/core/resources/resource_pack.cpp b/source/core/resources/resource_pack.cpp index 95a8175..5c70feb 100644 --- a/source/core/resources/resource_pack.cpp +++ b/source/core/resources/resource_pack.cpp @@ -21,7 +21,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; } @@ -30,7 +30,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); } @@ -81,7 +81,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.cpp b/source/game/editor/map_editor.cpp index 401430f..813b273 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -383,7 +383,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 if (tile_picker_.isOpen()) { tile_picker_.handleEvent(event); @@ -549,7 +549,7 @@ void MapEditor::handleMouseDown(float game_x, float game_y) { } // Procesa soltar el ratón: commit del drag -void MapEditor::handleMouseUp() { // NOLINT(readability-function-cognitive-complexity) +void MapEditor::handleMouseUp() { if (drag_.target == DragTarget::NONE) { return; } const int IDX = drag_.index; @@ -832,7 +832,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_); @@ -945,7 +945,7 @@ auto MapEditor::getSetCompletions() 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"; } @@ -1125,7 +1125,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); @@ -1268,7 +1268,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ó @@ -1406,7 +1406,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_; diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index 31befa6..81e721b 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -65,7 +65,7 @@ class MapEditor { void openTilePicker(const std::string& tileset_name, int current_tile); private: - static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado + static MapEditor* instance_; MapEditor(); // Constructor ~MapEditor(); // Destructor diff --git a/source/game/editor/room_saver.cpp b/source/game/editor/room_saver.cpp index fc96305..9cad879 100644 --- a/source/game/editor/room_saver.cpp +++ b/source/game/editor/room_saver.cpp @@ -36,7 +36,7 @@ auto RoomSaver::conveyorBeltToString(int direction) -> std::string { } // Genera el YAML completo como texto con formato compacto -auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity) +auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { std::ostringstream out; // --- Cabecera: nombre como comentario --- diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 39234bf..1abb574 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -26,7 +26,8 @@ Enemy::Enemy(const Data& enemy) const int FLIP = (should_flip_ && enemy.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE; - sprite_->setFlip(static_cast(FLIP | MIRROR)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) SDL flags are designed for bitwise OR + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange): SDL_FlipMode és un enum dissenyat com a bitmask (FLIP_NONE=0, FLIP_HORIZONTAL=1, FLIP_VERTICAL=2). El cast del OR és el patró d'ús previst per la API de SDL. + sprite_->setFlip(static_cast(FLIP | MIRROR)); collider_ = getRect(); @@ -63,7 +64,8 @@ void Enemy::resetToInitialPosition(const Data& data) { const int FLIP = (should_flip_ && data.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE; - sprite_->setFlip(static_cast(FLIP | MIRROR)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange): SDL_FlipMode és un enum bitmask (vegeu nota al constructor); el cast del OR és el patró d'ús de SDL + sprite_->setFlip(static_cast(FLIP | MIRROR)); collider_ = getRect(); } 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 10ea9cd..ebbe075 100644 --- a/source/game/gameplay/item_manager.cpp +++ b/source/game/gameplay/item_manager.cpp @@ -14,7 +14,7 @@ ItemManager::ItemManager(std::string room_name, 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/room_loader.cpp b/source/game/gameplay/room_loader.cpp index 7dc5890..7e757f2 100644 --- a/source/game/gameplay/room_loader.cpp +++ b/source/game/gameplay/room_loader.cpp @@ -39,7 +39,7 @@ auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int { } // Convierte un tilemap 2D a vector 1D flat -auto RoomLoader::flattenTilemap(const std::vector>& tilemap_2d) -> std::vector { // NOLINT(readability-named-parameter) +auto RoomLoader::flattenTilemap(const std::vector>& tilemap_2d) -> std::vector { std::vector tilemap_flat; tilemap_flat.reserve(512); // 16 rows × 32 cols diff --git a/source/game/options.cpp b/source/game/options.cpp index bcbdaaf..6d4fe52 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -2,9 +2,11 @@ #include +#include // Para ranges::transform #include // Para create_directories #include // Para ifstream, ofstream #include // Para cout, cerr +#include // Para back_inserter #include // Para string #include // Para unordered_map @@ -568,51 +570,42 @@ namespace Options { } // Carga configuración de audio desde YAML - void loadAudioConfigFromYaml(const fkyaml::node& yaml) { // NOLINT(readability-function-cognitive-complexity) + 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 */ + } + } + } // namespace + + 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: camp YAML malformat → conservem default */ - } - } - if (a.contains("volume")) { - try { - audio.volume = std::clamp(a["volume"].get_value(), 0.0F, 1.0F); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem 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: camp YAML malformat → conservem default */ - } - } - if (m.contains("volume")) { - try { - audio.music.volume = std::clamp(m["volume"].get_value(), 0.0F, 1.0F); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem 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: camp YAML malformat → conservem default */ - } - } - if (s.contains("volume")) { - try { - audio.sound.volume = std::clamp(s["volume"].get_value(), 0.0F, 1.0F); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ - } - } + readYamlField(s, "enabled", audio.sound.enabled); + readYamlVolume(s, "volume", audio.sound.volume); } } @@ -1059,110 +1052,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 4 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 << "# JailDoctor's Dilemma - 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 << "# JailDoctor's Dilemma - 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; } @@ -1171,77 +1145,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: camp YAML malformat → conservem default */ - } - } - if (p.contains("enable_scanlines")) { - try { - preset.enable_scanlines = p["enable_scanlines"].get_value(); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ - } - } - if (p.contains("enable_multisample")) { - try { - preset.enable_multisample = p["enable_multisample"].get_value(); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ - } - } - if (p.contains("enable_gamma")) { - try { - preset.enable_gamma = p["enable_gamma"].get_value(); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ - } - } - if (p.contains("enable_curvature")) { - try { - preset.enable_curvature = p["enable_curvature"].get_value(); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */ - } - } - if (p.contains("enable_sharper")) { - try { - preset.enable_sharper = p["enable_sharper"].get_value(); - } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem 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; } diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index 969ae3a..d59344e 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -51,7 +51,7 @@ Title::Title() } // Destructor -Title::~Title() { // NOLINT(modernize-use-equals-default) +Title::~Title() { loading_screen_surface_->resetSubPalette(); title_surface_->resetSubPalette(); } diff --git a/source/game/ui/console.cpp b/source/game/ui/console.cpp index 63144db..0a6d461 100644 --- a/source/game/ui/console.cpp +++ b/source/game/ui/console.cpp @@ -167,72 +167,79 @@ void Console::redrawText() { } // 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); + height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_)); + if (anim_progress_ >= 1.0F) { + height_ = target_height_; + anim_progress_ = 0.0F; + } + // 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; + // Resetear el missatge una vegada completament oculta + msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)}; + target_height_ = calcTargetHeight(static_cast(msg_lines_.size())); +} + +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); - height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_)); - if (anim_progress_ >= 1.0F) { - height_ = target_height_; - anim_progress_ = 0.0F; - } - // 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 con easing) 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; - // Resetear el mensaje una vez completamente oculta - 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_}; @@ -288,84 +295,94 @@ void Console::toggle() { } // Procesa el evento SDL: entrada de texto, Backspace, Enter -void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity) +// Insereix caràcters imprimibles a input_line_ +void Console::handleTextInput(const SDL_Event& event) { + // Filtrar caràcters de control (tab, newline, etc.) + if (static_cast(event.text.text[0]) < 32) { 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_.push_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; +} + +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; } - 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 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)) { - std::string match = BASE_CMD; - match += ' '; - match += arg; - tab_matches_.push_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(), [](char c) { return static_cast(std::tolower(static_cast(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 92f4948..06289cb 100644 --- a/source/game/ui/console.hpp +++ b/source/game/ui/console.hpp @@ -79,6 +79,18 @@ class Console { 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 + // Sub-pasos de update() (extrets per reduir complexitat cognitiva) + void updateCursorBlink(float delta_time); // Parpadeig del cursor + 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 + + // Sub-pasos de handleEvent() (extrets per reduir complexitat cognitiva) + void handleTextInput(const SDL_Event& event); // Insereix caràcters imprimibles a input_line_ + void handleHistoryUp(); // Navegar enrere a l'historial + void handleHistoryDown(); // Navegar cap al present a l'historial + void handleTab(); // Autocompletat per TAB (comandes o sub-args) + // Objetos de renderizado std::shared_ptr text_; std::shared_ptr surface_; diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index 970e754..ade3c39 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -55,61 +55,77 @@ static auto boolToggle( // ── Command handlers ───────────────────────────────────────────────────────── // SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]] -static auto cmdSs(const std::vector& args) -> std::string { // NOLINT(readability-function-cognitive-complexity) - if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; } +// SS SIZE — dimensions de la textura supersampling activa +static auto cmdSsSize() -> std::string { + if (!Options::video.supersampling.enabled) { return "Supersampling is OFF: no texture"; } + const auto [w, h] = Screen::get()->getSsTextureSize(); + if (w == 0) { return "SS texture: not active"; } + return "SS texture: " + std::to_string(w) + "x" + std::to_string(h); +} + +// SS UPSCALE [NEAREST|LINEAR] — toggle o estableix mode upscale +static auto cmdSsUpscale(const std::vector& args) -> std::string { + if (args.size() == 1) { + Screen::get()->setLinearUpscale(!Options::video.supersampling.linear_upscale); + return std::string("Upscale: ") + (Options::video.supersampling.linear_upscale ? "Linear" : "Nearest"); + } + if (args[1] == "NEAREST") { + if (!Options::video.supersampling.linear_upscale) { return "Upscale already Nearest"; } + Screen::get()->setLinearUpscale(false); + return "Upscale: Nearest"; + } + if (args[1] == "LINEAR") { + if (Options::video.supersampling.linear_upscale) { return "Upscale already Linear"; } + Screen::get()->setLinearUpscale(true); + return "Upscale: Linear"; + } + return "usage: ss upscale [nearest|linear]"; +} + +// SS DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3] — consulta o estableix algorisme +static auto cmdSsDownscale(const std::vector& args) -> std::string { static const std::array DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"}; - if (!args.empty() && args[0] == "SIZE") { - if (!Options::video.supersampling.enabled) { return "Supersampling is OFF: no texture"; } - const auto [w, h] = Screen::get()->getSsTextureSize(); - if (w == 0) { return "SS texture: not active"; } - return "SS texture: " + std::to_string(w) + "x" + std::to_string(h); + if (args.size() == 1) { + return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(Options::video.supersampling.downscale_algo)]); } - if (!args.empty() && args[0] == "UPSCALE") { - if (args.size() == 1) { - Screen::get()->setLinearUpscale(!Options::video.supersampling.linear_upscale); - return std::string("Upscale: ") + (Options::video.supersampling.linear_upscale ? "Linear" : "Nearest"); - } - if (args[1] == "NEAREST") { - if (!Options::video.supersampling.linear_upscale) { return "Upscale already Nearest"; } - Screen::get()->setLinearUpscale(false); - return "Upscale: Nearest"; - } - if (args[1] == "LINEAR") { - if (Options::video.supersampling.linear_upscale) { return "Upscale already Linear"; } - Screen::get()->setLinearUpscale(true); - return "Upscale: Linear"; - } - return "usage: ss upscale [nearest|linear]"; - } - if (!args.empty() && args[0] == "DOWNSCALE") { - if (args.size() == 1) { - return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(Options::video.supersampling.downscale_algo)]); - } - int algo = -1; - if (args[1] == "BILINEAR") { algo = 0; } - if (args[1] == "LANCZOS2") { algo = 1; } - if (args[1] == "LANCZOS3") { algo = 2; } - if (algo == -1) { return "usage: ss downscale [bilinear|lanczos2|lanczos3]"; } - if (Options::video.supersampling.downscale_algo == algo) { - return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); - } - Screen::get()->setDownscaleAlgo(algo); - return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); + int algo = -1; + if (args[1] == "BILINEAR") { algo = 0; } + if (args[1] == "LANCZOS2") { algo = 1; } + if (args[1] == "LANCZOS3") { algo = 2; } + if (algo == -1) { return "usage: ss downscale [bilinear|lanczos2|lanczos3]"; } + if (Options::video.supersampling.downscale_algo == algo) { + return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); } + Screen::get()->setDownscaleAlgo(algo); + return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); +} + +// SS ON — activa supersampling si encara no ho està +static auto cmdSsOn() -> std::string { + if (Options::video.supersampling.enabled) { return "Supersampling already ON"; } + Screen::get()->toggleSupersampling(); + return "PostFX Supersampling ON"; +} + +// SS OFF — desactiva supersampling si encara està actiu +static auto cmdSsOff() -> std::string { + if (!Options::video.supersampling.enabled) { return "Supersampling already OFF"; } + Screen::get()->toggleSupersampling(); + return "PostFX Supersampling OFF"; +} + +// SS — toggle (sense args) o dispatch a subcomandes +static auto cmdSs(const std::vector& args) -> std::string { + if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; } if (args.empty()) { Screen::get()->toggleSupersampling(); return std::string("PostFX Supersampling ") + (Options::video.supersampling.enabled ? "ON" : "OFF"); } - if (args[0] == "ON") { - if (Options::video.supersampling.enabled) { return "Supersampling already ON"; } - Screen::get()->toggleSupersampling(); - return "PostFX Supersampling ON"; - } - if (args[0] == "OFF") { - if (!Options::video.supersampling.enabled) { return "Supersampling already OFF"; } - Screen::get()->toggleSupersampling(); - return "PostFX Supersampling OFF"; - } + if (args[0] == "SIZE") { return cmdSsSize(); } + if (args[0] == "UPSCALE") { return cmdSsUpscale(args); } + if (args[0] == "DOWNSCALE") { return cmdSsDownscale(args); } + if (args[0] == "ON") { return cmdSsOn(); } + if (args[0] == "OFF") { return cmdSsOff(); } return "usage: ss [on|off|size|upscale [nearest|linear]|downscale [bilinear|lanczos2|lanczos3]]"; } @@ -127,8 +143,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 — l'analitzador no veu que el guard `if (COUNT == 0) return ...` (línia 143) garanteix que el vector no està buit, així que current_idx és un índex vàlid + 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); }; @@ -481,7 +499,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 @@ -595,7 +613,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 <1-60>|next|prev|left|right|up|down"; } @@ -688,7 +706,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()) { @@ -828,62 +846,58 @@ 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"); +} + +// CHEAT OPEN/CLOSE THE JAIL — comprova "the jail" i estableix l'estat +static auto cmdCheatJail(const std::vector& args, bool open) -> std::string { + const std::string_view ACTION = open ? "open" : "close"; + if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { + return std::string("usage: cheat ") + std::string(ACTION) + " the jail"; + } + using State = Options::Cheat::State; + auto& jail = Options::cheats.jail_is_open; + const State TARGET = open ? State::ENABLED : State::DISABLED; + if (jail == TARGET) { + return open ? "Jail already open" : "Jail already closed"; + } + jail = TARGET; + return open ? "Jail opened" : "Jail closed"; +} + +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|open the jail|close the jail]"; } - // 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; - if (args.size() == 2) { - cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; - } else if (args[2] == "ON") { - if (cheat == State::ENABLED) { return "Infinite lives already ON"; } - cheat = State::ENABLED; - } else if (args[2] == "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; } - // CHEAT OPEN THE JAIL - if (args[0] == "OPEN") { - if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat open the jail"; } - if (Options::cheats.jail_is_open == Options::Cheat::State::ENABLED) { return "Jail already open"; } - Options::cheats.jail_is_open = Options::Cheat::State::ENABLED; - return "Jail opened"; - } - - // CHEAT CLOSE THE JAIL - if (args[0] == "CLOSE") { - if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat close the jail"; } - if (Options::cheats.jail_is_open == Options::Cheat::State::DISABLED) { return "Jail already closed"; } - Options::cheats.jail_is_open = Options::Cheat::State::DISABLED; - return "Jail closed"; - } + if (args[0] == "OPEN") { return cmdCheatJail(args, true); } + if (args[0] == "CLOSE") { return cmdCheatJail(args, false); } return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]"; } @@ -975,7 +989,7 @@ static auto cmdSize(const std::vector& /*unused*/) -> std::string { // ── CommandRegistry ────────────────────────────────────────────────────────── -void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cognitive-complexity) +void CommandRegistry::registerHandlers() { handlers_["cmd_ss"] = cmdSs; handlers_["cmd_shader"] = cmdShader; handlers_["cmd_border"] = cmdBorder; @@ -1091,7 +1105,78 @@ 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 + +void CommandRegistry::load(const std::string& yaml_path) { registerHandlers(); // Cargar y parsear el YAML @@ -1115,115 +1200,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()) { - for (const auto& s : scope_node) { def.scopes.push_back(s.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; - std::ranges::transform(*it, 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; - std::ranges::transform(*it, 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 : 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_) { @@ -1233,6 +1224,30 @@ void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readabilit } } +auto CommandRegistry::buildHelp(const std::vector& args) const -> std::string { + if (args.empty()) { + std::cout << generateTerminalHelp(); + return generateConsoleHelp(); + } + // HELP : ajuda detallada + 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; + + const auto OPTS = getCompletions(cmd->keyword); + if (OPTS.empty()) { return result; } + 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; +} + auto CommandRegistry::findCommand(const std::string& keyword) const -> const CommandDef* { auto it = std::ranges::find_if(commands_, [&keyword](const auto& cmd) { return cmd.keyword == keyword; }); @@ -1275,12 +1290,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; } @@ -1290,16 +1310,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 b775402..1cf1b64 100644 --- a/source/game/ui/console_commands.hpp +++ b/source/game/ui/console_commands.hpp @@ -58,4 +58,5 @@ class CommandRegistry { void registerHandlers(); [[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool; + [[nodiscard]] auto buildHelp(const std::vector& args) const -> std::string; // Cos del handler HELP (extret per reduir complexitat de load()) };