merge audita-nolint: 77→6 NOLINT (15 refactors + neteja obsolets)

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