merge audita-nolint: 287→9 NOLINT (10 refactors + neteja obsolets)
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para allocator, operator+, char_traits, string
|
||||
#include <vector> // Para vector
|
||||
#include <algorithm> // Para ranges::find_if
|
||||
#include <initializer_list> // Para initializer_list
|
||||
#include <string> // Para allocator, operator+, char_traits, string
|
||||
#include <vector> // 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<InputAction> 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
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace GIF {
|
||||
}
|
||||
|
||||
// Inicializa el diccionario LZW con los valores iniciales
|
||||
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
|
||||
inline void initializeDictionary(std::vector<DictionaryEntry>& 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<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
|
||||
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) {
|
||||
uint8_t first_byte;
|
||||
if (code == dictionary_ind) {
|
||||
first_byte = findFirstByte(dictionary, prev);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
// MSL shaders (Metal Shading Language) — macOS
|
||||
// ============================================================================
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
static const char* POSTFX_VERT_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
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<size_t>(width) * static_cast<size_t>(height) * 4);
|
||||
std::memcpy(mapped, pixels, static_cast<size_t>(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<float>(win_h) / static_cast<float>(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<float>(win_h) / static_cast<float>(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<Uint32>(game_width_);
|
||||
src.rows_per_layer = static_cast<Uint32>(game_height_);
|
||||
if (copy == nullptr) { return; }
|
||||
|
||||
SDL_GPUTextureRegion dst = {};
|
||||
dst.texture = scene_texture_;
|
||||
dst.w = static_cast<Uint32>(game_width_);
|
||||
dst.h = static_cast<Uint32>(game_height_);
|
||||
dst.d = 1;
|
||||
SDL_GPUTextureTransferInfo src = {};
|
||||
src.transfer_buffer = upload_buffer_;
|
||||
src.offset = 0;
|
||||
src.pixels_per_row = static_cast<Uint32>(game_width_);
|
||||
src.rows_per_layer = static_cast<Uint32>(game_height_);
|
||||
|
||||
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
SDL_GPUTextureRegion dst = {};
|
||||
dst.texture = scene_texture_;
|
||||
dst.w = static_cast<Uint32>(game_width_);
|
||||
dst.h = static_cast<Uint32>(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<float>(game_width_) * SCALE;
|
||||
vh = static_cast<float>(game_height_) * SCALE;
|
||||
}
|
||||
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
|
||||
const float VX = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||
const float VY = std::floor((static_cast<float>(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<float>(ss_factor_);
|
||||
} else {
|
||||
uniforms_.pixel_scale = (game_height_ > 0) ? (vh / static_cast<float>(game_height_)) : 1.0F;
|
||||
uniforms_.pixel_scale = (game_height_ > 0) ? (viewport_h / static_cast<float>(game_height_)) : 1.0F;
|
||||
}
|
||||
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
uniforms_.oversample = (oversample_ > 1 && ss_factor_ > 0)
|
||||
? static_cast<float>(ss_factor_)
|
||||
: 1.0F;
|
||||
uniforms_.oversample = (oversample_ > 1 && ss_factor_ > 0) ? static_cast<float>(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<float>(game_width_);
|
||||
crtpi_uniforms_.texture_height = static_cast<float>(game_height_);
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms));
|
||||
crtpi_uniforms_.texture_width = static_cast<float>(game_width_);
|
||||
crtpi_uniforms_.texture_height = static_cast<float>(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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<std::string>(), 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<std::string>(), prefix, system_folder);
|
||||
const bool REQUIRED = !item.contains("required") || item["required"].get_value<bool>();
|
||||
const bool ABSOLUTE = item.contains("absolute") && item["absolute"].get_value<bool>();
|
||||
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<std::string>();
|
||||
auto path = asset["path"].get_value<std::string>();
|
||||
const bool REQUIRED = !asset.contains("required") || asset["required"].get_value<bool>();
|
||||
const bool ABSOLUTE = asset.contains("absolute") && asset["absolute"].get_value<bool>();
|
||||
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<std::string>();
|
||||
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<std::string>();
|
||||
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<std::string>(), 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<std::string>(), prefix, system_folder);
|
||||
bool required = !item.contains("required") || item["required"].get_value<bool>();
|
||||
bool absolute = item.contains("absolute") && item["absolute"].get_value<bool>();
|
||||
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<std::string>();
|
||||
auto path = asset["path"].get_value<std::string>();
|
||||
bool required = true;
|
||||
bool absolute = false;
|
||||
|
||||
if (asset.contains("required")) {
|
||||
required = asset["required"].get_value<bool>();
|
||||
}
|
||||
if (asset.contains("absolute")) {
|
||||
absolute = asset["absolute"].get_value<bool>();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ namespace Resource {
|
||||
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
|
||||
[[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
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Resource {
|
||||
}
|
||||
|
||||
// XOR encryption (symmetric - same function for encrypt/decrypt)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ namespace Resource {
|
||||
}
|
||||
}
|
||||
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
void Pack::decryptData(std::vector<uint8_t>& 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';
|
||||
|
||||
@@ -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<std::string> {
|
||||
}
|
||||
|
||||
// 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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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<SDL_FlipMode>(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<SDL_FlipMode>(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<SDL_FlipMode>(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<SDL_FlipMode>(FLIP | MIRROR));
|
||||
|
||||
collider_ = getRect();
|
||||
}
|
||||
|
||||
@@ -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> enemy) { // NOLINT(readability-identifier-naming)
|
||||
void EnemyManager::addEnemy(std::shared_ptr<Enemy> enemy) {
|
||||
enemies_.push_back(std::move(enemy));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ ItemManager::ItemManager(std::string room_name, std::shared_ptr<Scoreboard::Data
|
||||
}
|
||||
|
||||
// Añade un item a la colección
|
||||
void ItemManager::addItem(std::shared_ptr<Item> item) { // NOLINT(readability-identifier-naming)
|
||||
void ItemManager::addItem(std::shared_ptr<Item> item) {
|
||||
items_.push_back(std::move(item));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::vector<int>>& tilemap_2d) -> std::vector<int> { // NOLINT(readability-named-parameter)
|
||||
auto RoomLoader::flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int> {
|
||||
std::vector<int> tilemap_flat;
|
||||
tilemap_flat.reserve(512); // 16 rows × 32 cols
|
||||
|
||||
|
||||
+111
-193
@@ -2,9 +2,11 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para ranges::transform
|
||||
#include <filesystem> // Para create_directories
|
||||
#include <fstream> // Para ifstream, ofstream
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <iterator> // Para back_inserter
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // 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 <typename T>
|
||||
void readYamlField(const fkyaml::node& parent, const char* key, T& dst) {
|
||||
if (!parent.contains(key)) { return; }
|
||||
try {
|
||||
dst = parent[key].template get_value<T>();
|
||||
} 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<float>(), 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<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (a.contains("volume")) {
|
||||
try {
|
||||
audio.volume = std::clamp(a["volume"].get_value<float>(), 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<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (m.contains("volume")) {
|
||||
try {
|
||||
audio.music.volume = std::clamp(m["volume"].get_value<float>(), 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<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (s.contains("volume")) {
|
||||
try {
|
||||
audio.sound.volume = std::clamp(s["volume"].get_value<float>(), 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<CrtPiPreset> {
|
||||
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<CrtPiPreset>& 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<std::string>();
|
||||
}
|
||||
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<int>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_scanlines")) {
|
||||
try {
|
||||
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_multisample")) {
|
||||
try {
|
||||
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_gamma")) {
|
||||
try {
|
||||
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_curvature")) {
|
||||
try {
|
||||
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_sharper")) {
|
||||
try {
|
||||
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ Title::Title()
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Title::~Title() { // NOLINT(modernize-use-equals-default)
|
||||
Title::~Title() {
|
||||
loading_screen_surface_->resetSubPalette();
|
||||
title_surface_->resetSubPalette();
|
||||
}
|
||||
|
||||
+145
-128
@@ -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<int>(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<Surface>(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<int>(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<int>(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<Surface>(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<int>(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<unsigned char>(event.text.text[0]) < 32) { return; }
|
||||
if (static_cast<int>(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<int>(history_.size()) - 1) { return; }
|
||||
if (history_index_ == -1) { saved_input_ = input_line_; }
|
||||
++history_index_;
|
||||
input_line_ = history_[static_cast<size_t>(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<size_t>(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<char>(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<int>(tab_matches_.size());
|
||||
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
|
||||
std::ranges::transform(result, result.begin(), [](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(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<unsigned char>(event.text.text[0]) < 32) { return; }
|
||||
if (static_cast<int>(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<int>(history_.size()) - 1) {
|
||||
if (history_index_ == -1) { saved_input_ = input_line_; }
|
||||
++history_index_;
|
||||
input_line_ = history_[static_cast<size_t>(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<size_t>(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<char>(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<int>(tab_matches_.size());
|
||||
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
|
||||
std::ranges::transform(result, result.begin(), [](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::string>; // 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> text_;
|
||||
std::shared_ptr<Surface> surface_;
|
||||
|
||||
+223
-207
@@ -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<std::string>& 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<std::string>& 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<std::string>& args) -> std::string {
|
||||
static const std::array<std::string_view, 3> 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<size_t>(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<size_t>(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<size_t>(algo)]);
|
||||
}
|
||||
Screen::get()->setDownscaleAlgo(algo);
|
||||
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(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<size_t>(algo)]);
|
||||
}
|
||||
Screen::get()->setDownscaleAlgo(algo);
|
||||
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(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<std::string>& 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<std::string>& 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<size_t>(current_idx)].name // NOLINT(clang-analyzer-core.CallAndMessage)
|
||||
: presets_postfx[static_cast<size_t>(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<size_t>(current_idx)].name
|
||||
: presets_postfx[static_cast<size_t>(current_idx)].name;
|
||||
// NOLINTEND(clang-analyzer-core.CallAndMessage)
|
||||
return prettyName(name);
|
||||
};
|
||||
|
||||
@@ -481,7 +499,7 @@ static auto cmdSound(const std::vector<std::string>& args) -> std::string {
|
||||
|
||||
#ifdef _DEBUG
|
||||
// DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE <name>]]
|
||||
static auto cmdDebug(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdDebug(const std::vector<std::string>& args) -> std::string {
|
||||
// --- START subcommands (START SCENE works from any scene) ---
|
||||
if (!args.empty() && args[0] == "START") {
|
||||
// START SCENE [<name>] — 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<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdRoom(const std::vector<std::string>& 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<std::string>& args) -> std::string {
|
||||
}
|
||||
|
||||
// EDIT [ON|OFF|REVERT]
|
||||
static auto cmdEdit(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdEdit(const std::vector<std::string>& 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<std::string>& args) -> std::string {
|
||||
}
|
||||
|
||||
// CHEAT [subcomando]
|
||||
static auto cmdCheat(const std::vector<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& /*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::string> {
|
||||
std::vector<std::string> result;
|
||||
if (scope_node.is_sequence()) {
|
||||
std::ranges::transform(scope_node, std::back_inserter(result), [](const auto& s) { return s.template get_value<std::string>(); });
|
||||
} else {
|
||||
result.push_back(scope_node.get_value<std::string>());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parseja un mapping de path → [options] en l'unordered_map de destí
|
||||
void parseCompletionsNode(const fkyaml::node& completions_node, std::unordered_map<std::string, std::vector<std::string>>& out) {
|
||||
for (auto it = completions_node.begin(); it != completions_node.end(); ++it) {
|
||||
auto path = it.key().get_value<std::string>();
|
||||
std::vector<std::string> opts;
|
||||
std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
|
||||
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<std::string>(); }
|
||||
if (extras.contains("usage")) { def.usage = extras["usage"].get_value<std::string>(); }
|
||||
if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value<bool>(); }
|
||||
if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value<bool>(); }
|
||||
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<std::string>& cat_scopes) -> CommandDef {
|
||||
CommandDef def;
|
||||
def.keyword = cmd_node["keyword"].get_value<std::string>();
|
||||
def.handler_id = cmd_node["handler"].get_value<std::string>();
|
||||
def.category = category;
|
||||
def.description = cmd_node.contains("description") ? cmd_node["description"].get_value<std::string>() : "";
|
||||
def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value<std::string>() : def.keyword;
|
||||
def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value<bool>();
|
||||
def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value<bool>();
|
||||
def.debug_only = cat_debug_only || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value<bool>());
|
||||
def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value<bool>();
|
||||
def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value<bool>();
|
||||
|
||||
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<std::string>();
|
||||
const bool CAT_DEBUG_ONLY = cat_node.contains("debug_only") && cat_node["debug_only"].get_value<bool>();
|
||||
|
||||
// Scopes por defecto de la categoría
|
||||
std::vector<std::string> 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<std::string>(); });
|
||||
} else {
|
||||
cat_scopes.push_back(scope_node.get_value<std::string>());
|
||||
}
|
||||
}
|
||||
const std::vector<std::string> CAT_SCOPES = cat_node.contains("scope") ? parseScopeNode(cat_node["scope"]) : std::vector<std::string>{};
|
||||
|
||||
if (!cat_node.contains("commands")) { continue; }
|
||||
|
||||
for (const auto& cmd_node : cat_node["commands"]) {
|
||||
CommandDef def;
|
||||
def.keyword = cmd_node["keyword"].get_value<std::string>();
|
||||
def.handler_id = cmd_node["handler"].get_value<std::string>();
|
||||
def.category = CATEGORY;
|
||||
def.description = cmd_node.contains("description") ? cmd_node["description"].get_value<std::string>() : "";
|
||||
def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value<std::string>() : def.keyword;
|
||||
def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value<bool>();
|
||||
def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value<bool>();
|
||||
def.debug_only = CAT_DEBUG_ONLY || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value<bool>());
|
||||
def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value<bool>();
|
||||
def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value<bool>();
|
||||
|
||||
// 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<std::string>()); }
|
||||
} else {
|
||||
def.scopes.push_back(scope_node.get_value<std::string>());
|
||||
}
|
||||
} 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::string>();
|
||||
std::vector<std::string> opts;
|
||||
std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
|
||||
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<std::string>(); }
|
||||
if (extras.contains("usage")) { def.usage = extras["usage"].get_value<std::string>(); }
|
||||
if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value<bool>(); }
|
||||
if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value<bool>(); }
|
||||
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::string>();
|
||||
std::vector<std::string> opts;
|
||||
std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
|
||||
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<std::string>& args) -> std::string {
|
||||
if (!args.empty()) {
|
||||
// HELP <command>: 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<std::string>& 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<std::string>& args) const -> std::string {
|
||||
if (args.empty()) {
|
||||
std::cout << generateTerminalHelp();
|
||||
return generateConsoleHelp();
|
||||
}
|
||||
// HELP <command>: 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,4 +58,5 @@ class CommandRegistry {
|
||||
|
||||
void registerHandlers();
|
||||
[[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool;
|
||||
[[nodiscard]] auto buildHelp(const std::vector<std::string>& args) const -> std::string; // Cos del handler HELP (extret per reduir complexitat de load())
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user