merge audita-nolint: 77→6 NOLINT (15 refactors + neteja obsolets)
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // Para std::ranges::find_if
|
||||||
|
#include <initializer_list> // Para std::initializer_list
|
||||||
#include <string> // Para allocator, operator+, char_traits, string
|
#include <string> // Para allocator, operator+, char_traits, string
|
||||||
#include <vector> // Para vector
|
#include <vector> // Para vector
|
||||||
|
|
||||||
@@ -158,65 +160,50 @@ namespace GlobalInputs {
|
|||||||
Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")});
|
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)
|
// Detecta qué acción global ha sido presionada (si alguna)
|
||||||
auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity)
|
auto getPressedAction() -> InputAction {
|
||||||
if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
|
if (const InputAction ACT = firstPressedFrom({InputAction::EXIT, InputAction::ACCEPT, InputAction::TOGGLE_BORDER}); ACT != InputAction::NONE) { return ACT; }
|
||||||
return InputAction::EXIT;
|
|
||||||
}
|
|
||||||
if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
||||||
return InputAction::ACCEPT;
|
|
||||||
}
|
|
||||||
if (Input::get()->checkAction(InputAction::TOGGLE_BORDER, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
||||||
return InputAction::TOGGLE_BORDER;
|
|
||||||
}
|
|
||||||
if (!Options::kiosk.enabled) {
|
if (!Options::kiosk.enabled) {
|
||||||
if (Input::get()->checkAction(InputAction::TOGGLE_FULLSCREEN, Input::DO_NOT_ALLOW_REPEAT)) {
|
if (const InputAction ACT = firstPressedFrom({InputAction::TOGGLE_FULLSCREEN, InputAction::WINDOW_DEC_ZOOM, InputAction::WINDOW_INC_ZOOM}); ACT != InputAction::NONE) { return ACT; }
|
||||||
return InputAction::TOGGLE_FULLSCREEN;
|
|
||||||
}
|
}
|
||||||
if (Input::get()->checkAction(InputAction::WINDOW_DEC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
||||||
return InputAction::WINDOW_DEC_ZOOM;
|
if (const InputAction ACT = getShaderAction(); ACT != InputAction::NONE) { return ACT; }
|
||||||
}
|
if (const InputAction ACT = getPaletteAction(); ACT != InputAction::NONE) { return ACT; }
|
||||||
if (Input::get()->checkAction(InputAction::WINDOW_INC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
||||||
return InputAction::WINDOW_INC_ZOOM;
|
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();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace GIF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inicializa el diccionario LZW con los valores iniciales
|
// 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;
|
int size = 1 << code_length;
|
||||||
dictionary.resize(1 << (code_length + 1));
|
dictionary.resize(1 << (code_length + 1));
|
||||||
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||||
@@ -55,7 +55,7 @@ namespace GIF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agrega una nueva entrada al diccionario
|
// 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;
|
uint8_t first_byte;
|
||||||
if (code == dictionary_ind) {
|
if (code == dictionary_ind) {
|
||||||
first_byte = findFirstByte(dictionary, prev);
|
first_byte = findFirstByte(dictionary, prev);
|
||||||
|
|||||||
@@ -58,58 +58,47 @@ namespace {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asignación óptima de colores mediante el algoritmo húngaro (Kuhn-Munkres).
|
namespace {
|
||||||
// Minimiza la distancia RGB total entre la paleta y la referencia.
|
// Construeix la matriu de cost NxM (ampliada a SZxSZ amb zeros) per a l'algoritme hongarès.
|
||||||
// O(N³) con N = número de colores activos — para N ≤ 256 es instantáneo.
|
auto buildCostMatrix(int n_rows, int m_cols, int sz, const Palette& palette, const Palette& reference) -> std::vector<int> {
|
||||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
std::vector<int> cost(static_cast<size_t>(sz) * static_cast<size_t>(sz), 0);
|
||||||
auto sortByOptimal(const Palette& palette, const Palette& reference) -> Palette {
|
for (int i = 0; i < n_rows; ++i) {
|
||||||
const auto N = static_cast<int>(countActiveColors(palette));
|
for (int j = 0; j < m_cols; ++j) {
|
||||||
const auto M = static_cast<int>(countActiveColors(reference));
|
cost[(i * sz) + j] = rgbDistanceSq(palette[i], reference[j]);
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
|
||||||
// Hungarian algorithm (Kuhn-Munkres) — versión basada en potenciales
|
// 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;
|
constexpr int INF = INT_MAX / 2;
|
||||||
std::vector<int> u(SZ + 1, 0); // Potenciales de filas
|
HungarianStep step{.j0 = j0, .delta = INF, .j1 = 0};
|
||||||
std::vector<int> v(SZ + 1, 0); // Potenciales de columnas
|
for (int j = 1; j <= sz; ++j) {
|
||||||
std::vector<int> p(SZ + 1, 0); // p[j] = fila asignada a columna j
|
if (used[j]) { continue; }
|
||||||
std::vector<int> way(SZ + 1, 0);
|
const int CUR = cost[((i0 - 1) * sz) + (j - 1)] - u[i0] - v[j];
|
||||||
|
if (CUR < minv[j]) {
|
||||||
for (int i = 1; i <= SZ; ++i) {
|
minv[j] = CUR;
|
||||||
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;
|
way[j] = j0;
|
||||||
}
|
}
|
||||||
if (minv[j] < delta) {
|
if (minv[j] < step.delta) {
|
||||||
delta = minv[j];
|
step.delta = minv[j];
|
||||||
j1 = j;
|
step.j1 = j;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return step;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int j = 0; j <= SZ; ++j) {
|
// 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]) {
|
if (used[j]) {
|
||||||
u[p[j]] += delta;
|
u[p[j]] += delta;
|
||||||
v[j] -= delta;
|
v[j] -= delta;
|
||||||
@@ -117,45 +106,76 @@ namespace {
|
|||||||
minv[j] -= delta;
|
minv[j] -= delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
j0 = j1;
|
// 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);
|
} while (p[j0] != 0);
|
||||||
|
|
||||||
do {
|
do {
|
||||||
int j1 = way[j0];
|
const int J1 = way[j0];
|
||||||
p[j0] = p[j1];
|
p[j0] = p[J1];
|
||||||
j0 = j1;
|
j0 = J1;
|
||||||
} while (j0 != 0);
|
} while (j0 != 0);
|
||||||
}
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
// Construir la paleta resultante: assignment[j] = fila asignada a columna j
|
// Construeix la paleta resultant a partir de l'assignació p[]. Afegeix els colors
|
||||||
// Queremos result[j] = palette[fila asignada a j]
|
// 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{};
|
Palette out{};
|
||||||
out.fill(0);
|
out.fill(0);
|
||||||
for (int j = 1; j <= M && j <= N; ++j) {
|
for (int j = 1; j <= m_cols && j <= n_rows; ++j) {
|
||||||
int row = p[j] - 1; // Índice 0-based en palette
|
const int ROW = p[j] - 1;
|
||||||
if (row >= 0 && row < N) {
|
if (ROW >= 0 && ROW < n_rows) { out[j - 1] = palette[ROW]; }
|
||||||
out[j - 1] = palette[row];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (n_rows <= m_cols) { return out; }
|
||||||
|
|
||||||
// Colores extra de palette que no tienen pareja en reference (N > M)
|
std::vector<bool> used_rows(n_rows, false);
|
||||||
if (N > M) {
|
for (int j = 1; j <= m_cols; ++j) {
|
||||||
std::vector<bool> used_rows(N, false);
|
const int ROW = p[j] - 1;
|
||||||
for (int j = 1; j <= M; ++j) {
|
if (ROW >= 0 && ROW < n_rows) { used_rows[ROW] = true; }
|
||||||
int row = p[j] - 1;
|
|
||||||
if (row >= 0 && row < N) { used_rows[row] = true; }
|
|
||||||
}
|
}
|
||||||
int out_idx = M;
|
int out_idx = m_cols;
|
||||||
for (int i = 0; i < N; ++i) {
|
for (int i = 0; i < n_rows; ++i) {
|
||||||
if (!used_rows[i]) {
|
if (!used_rows[i]) { out[out_idx++] = palette[i]; }
|
||||||
out[out_idx++] = palette[i];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
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.
|
||||||
|
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; }
|
||||||
|
|
||||||
|
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.
|
// Asignación greedy de colores a la paleta de referencia.
|
||||||
// Más rápida pero puede asignar colores subóptimamente.
|
// Más rápida pero puede asignar colores subóptimamente.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actualiza el estado del revelado
|
// 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)
|
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
|
||||||
const auto PIXEL_COLOR = reverse_ ? 0 : 255;
|
const auto PIXEL_COLOR = reverse_ ? 0 : 255;
|
||||||
|
|
||||||
|
|||||||
@@ -808,7 +808,7 @@ auto Screen::initSDLVideo() -> bool {
|
|||||||
|
|
||||||
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
|
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
|
||||||
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
|
// 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__
|
#ifdef __EMSCRIPTEN__
|
||||||
// NO registramos resize callback. En móvil, el scroll hace que el navegador
|
// 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,
|
// 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)
|
auto SDL3GPUShader::createPostfxVertexShader() -> SDL_GPUShader* {
|
||||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
|
||||||
|
|
||||||
// ---- PostFX pipeline (scene/scaled → swapchain) ----
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
return 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);
|
|
||||||
#else
|
#else
|
||||||
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
return 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);
|
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
if ((vert == nullptr) || (frag == nullptr)) {
|
// ---------------------------------------------------------------------------
|
||||||
SDL_Log("SDL3GPUShader: failed to compile PostFX shaders");
|
// createPostfxLikePipeline — empaqueta vert(postfx) + frag dado + target en un pipeline.
|
||||||
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
|
// Pren ownership de `frag` (el libera abans de retornar).
|
||||||
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
// ---------------------------------------------------------------------------
|
||||||
return false;
|
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 = {};
|
SDL_GPUColorTargetBlendState no_blend = {};
|
||||||
@@ -188,30 +193,44 @@ namespace Rendering {
|
|||||||
no_blend.enable_color_write_mask = false;
|
no_blend.enable_color_write_mask = false;
|
||||||
|
|
||||||
SDL_GPUColorTargetDescription color_target = {};
|
SDL_GPUColorTargetDescription color_target = {};
|
||||||
color_target.format = SWAPCHAIN_FMT;
|
color_target.format = format;
|
||||||
color_target.blend_state = no_blend;
|
color_target.blend_state = no_blend;
|
||||||
|
|
||||||
SDL_GPUVertexInputState no_input = {};
|
SDL_GPUVertexInputState no_input = {};
|
||||||
|
|
||||||
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
|
SDL_GPUGraphicsPipelineCreateInfo info = {};
|
||||||
pipe_info.vertex_shader = vert;
|
info.vertex_shader = vert;
|
||||||
pipe_info.fragment_shader = frag;
|
info.fragment_shader = frag;
|
||||||
pipe_info.vertex_input_state = no_input;
|
info.vertex_input_state = no_input;
|
||||||
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
pipe_info.target_info.num_color_targets = 1;
|
info.target_info.num_color_targets = 1;
|
||||||
pipe_info.target_info.color_target_descriptions = &color_target;
|
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_, vert);
|
||||||
SDL_ReleaseGPUShader(device_, frag);
|
SDL_ReleaseGPUShader(device_, frag);
|
||||||
|
|
||||||
if (pipeline_ == nullptr) {
|
if (pipeline == nullptr) {
|
||||||
SDL_Log("SDL3GPUShader: PostFX pipeline creation failed: %s", SDL_GetError());
|
SDL_Log("SDL3GPUShader: %s pipeline creation failed: %s", debug_name, SDL_GetError());
|
||||||
return false;
|
}
|
||||||
|
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 {
|
auto SDL3GPUShader::createCrtPiPipeline() -> bool {
|
||||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#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);
|
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
#else
|
#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);
|
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
#endif
|
#endif
|
||||||
|
crtpi_pipeline_ = createPostfxLikePipeline(frag, SWAPCHAIN_FMT, "CrtPi");
|
||||||
if ((vert == nullptr) || (frag == nullptr)) {
|
return crtpi_pipeline_ != 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ namespace Rendering {
|
|||||||
|
|
||||||
auto createPipeline() -> bool;
|
auto createPipeline() -> bool;
|
||||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||||
|
auto createPostfxVertexShader() -> SDL_GPUShader*; // Vertex shader fullscreen-triangle 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_
|
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||||
|
|||||||
@@ -148,14 +148,14 @@ void Surface::setColor(int index, Uint32 color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rellena la superficie con un 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);
|
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();
|
Uint8* data_ptr = surface_data_->data.get();
|
||||||
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pone un pixel en la SurfaceData
|
// 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) {
|
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||||
return; // Coordenadas fuera de rango
|
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)]; }
|
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * surface_data_->width)]; }
|
||||||
|
|
||||||
// Dibuja un rectangulo relleno
|
// 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
|
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||||
float x_start = std::max(0.0F, rect->x);
|
float x_start = std::max(0.0F, rect->x);
|
||||||
float y_start = std::max(0.0F, rect->y);
|
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
|
// 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
|
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||||
float x_start = std::max(0.0F, rect->x);
|
float x_start = std::max(0.0F, rect->x);
|
||||||
float y_start = std::max(0.0F, rect->y);
|
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)
|
// 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 ix1 = static_cast<int>(std::lround(x1));
|
||||||
int iy1 = static_cast<int>(std::lround(y1));
|
int iy1 = static_cast<int>(std::lround(y1));
|
||||||
const int IX2 = static_cast<int>(std::lround(x2));
|
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();
|
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
// Aplicar render offset (usado por transiciones entre pantallas)
|
// Aplicar render offset (usado por transiciones entre pantallas)
|
||||||
@@ -533,7 +533,7 @@ void Surface::toARGBBuffer(Uint32* buffer) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vuelca la superficie a una textura
|
// 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_) {
|
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||||
throw std::runtime_error("Renderer or texture is null.");
|
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
|
// 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_) {
|
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||||
throw std::runtime_error("Renderer or texture is null.");
|
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
|
// 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 constexpr int PALETTE_SIZE = 19;
|
||||||
static_assert(std::tuple_size_v<Palette> >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation.");
|
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
|
// 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
|
// Variable estática para almacenar el último tick
|
||||||
static Uint32 last_tick_ = 0;
|
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
|
// 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_); }
|
||||||
|
|||||||
@@ -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
|
// 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 width = length(text, kerning) * zoom;
|
||||||
auto height = box_height_ * zoom;
|
auto height = box_height_ * zoom;
|
||||||
auto surface = std::make_shared<Surface>(width, height);
|
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
|
// 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 width = Text::length(text, kerning) + shadow_distance;
|
||||||
auto height = box_height_ + shadow_distance;
|
auto height = box_height_ + shadow_distance;
|
||||||
auto surface = std::make_shared<Surface>(width, height);
|
auto surface = std::make_shared<Surface>(width, height);
|
||||||
|
|||||||
@@ -244,14 +244,13 @@ namespace Resource {
|
|||||||
return rooms_;
|
return rooms_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper para lanzar errores de carga con formato consistente
|
// Helper para registrar errores de carga con formato consistente.
|
||||||
[[noreturn]] void Cache::throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) {
|
// 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 << "\n[ ERROR ] Failed to load " << asset_type << ": " << getFileName(file_path) << '\n';
|
||||||
std::cerr << "[ ERROR ] Path: " << file_path << '\n';
|
std::cerr << "[ ERROR ] Path: " << file_path << '\n';
|
||||||
std::cerr << "[ ERROR ] Reason: " << e.what() << '\n';
|
std::cerr << "[ ERROR ] Reason: " << e.what() << '\n';
|
||||||
std::cerr << "[ ERROR ] Check config/assets.yaml configuration\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
|
// Carga los sonidos
|
||||||
@@ -284,7 +283,8 @@ namespace Resource {
|
|||||||
printWithDots("Sound : ", name, "[ LOADED ]");
|
printWithDots("Sound : ", name, "[ LOADED ]");
|
||||||
updateLoadingProgress();
|
updateLoadingProgress();
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("SOUND", l, e);
|
logLoadError("SOUND", l, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,7 +319,8 @@ namespace Resource {
|
|||||||
printWithDots("Music : ", name, "[ LOADED ]");
|
printWithDots("Music : ", name, "[ LOADED ]");
|
||||||
updateLoadingProgress(1);
|
updateLoadingProgress(1);
|
||||||
} catch (const std::exception& e) {
|
} 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);
|
surfaces_.back().surface->setTransparentColor(0);
|
||||||
updateLoadingProgress();
|
updateLoadingProgress();
|
||||||
} catch (const std::exception& e) {
|
} 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)});
|
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
|
||||||
updateLoadingProgress();
|
updateLoadingProgress();
|
||||||
} catch (const std::exception& e) {
|
} 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)});
|
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
|
||||||
updateLoadingProgress();
|
updateLoadingProgress();
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("FONT", l, e);
|
logLoadError("FONT", l, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +407,8 @@ namespace Resource {
|
|||||||
printWithDots("Animation : ", name, "[ LOADED ]");
|
printWithDots("Animation : ", name, "[ LOADED ]");
|
||||||
updateLoadingProgress();
|
updateLoadingProgress();
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("ANIMATION", l, e);
|
logLoadError("ANIMATION", l, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,7 +426,8 @@ namespace Resource {
|
|||||||
printWithDots("Room : ", name, "[ LOADED ]");
|
printWithDots("Room : ", name, "[ LOADED ]");
|
||||||
updateLoadingProgress();
|
updateLoadingProgress();
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("ROOM", l, e);
|
logLoadError("ROOM", l, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,7 +607,8 @@ namespace Resource {
|
|||||||
it->sound = sound;
|
it->sound = sound;
|
||||||
std::cout << "[lazy] Sound loaded: " << name << '\n';
|
std::cout << "[lazy] Sound loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("SOUND", path, e);
|
logLoadError("SOUND", path, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +625,8 @@ namespace Resource {
|
|||||||
it->music = music;
|
it->music = music;
|
||||||
std::cout << "[lazy] Music loaded: " << name << '\n';
|
std::cout << "[lazy] Music loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} 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';
|
std::cout << "[lazy] Surface loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("BITMAP", path, e);
|
logLoadError("BITMAP", path, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +660,8 @@ namespace Resource {
|
|||||||
it->loaded = true;
|
it->loaded = true;
|
||||||
std::cout << "[lazy] Palette loaded: " << name << '\n';
|
std::cout << "[lazy] Palette loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} 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);
|
it->text_file = Text::loadTextFile(path);
|
||||||
std::cout << "[lazy] TextFile loaded: " << name << '\n';
|
std::cout << "[lazy] TextFile loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("FONT", path, e);
|
logLoadError("FONT", path, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +688,8 @@ namespace Resource {
|
|||||||
it->yaml_data = bytes;
|
it->yaml_data = bytes;
|
||||||
std::cout << "[lazy] Animation loaded: " << name << '\n';
|
std::cout << "[lazy] Animation loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} 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));
|
it->room = std::make_shared<Room::Data>(Room::loadYAML(path));
|
||||||
std::cout << "[lazy] Room loaded: " << name << '\n';
|
std::cout << "[lazy] Room loaded: " << name << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
throwLoadError("ROOM", path, e);
|
logLoadError("ROOM", path, e);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ namespace Resource {
|
|||||||
void updateLoadingProgress(int steps = 5);
|
void updateLoadingProgress(int steps = 5);
|
||||||
|
|
||||||
// Helper para mensajes de error de carga
|
// 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
|
// Constructor y destructor
|
||||||
explicit Cache(LoadingMode mode);
|
explicit Cache(LoadingMode mode);
|
||||||
|
|||||||
@@ -16,6 +16,71 @@
|
|||||||
|
|
||||||
namespace Resource {
|
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
|
// Singleton
|
||||||
List* List::instance = nullptr;
|
List* List::instance = nullptr;
|
||||||
|
|
||||||
@@ -160,17 +225,13 @@ namespace Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Carga recursos desde un string de configuración (para release con pack)
|
// 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 {
|
try {
|
||||||
// Parsear YAML
|
|
||||||
auto yaml = fkyaml::node::deserialize(config_content);
|
auto yaml = fkyaml::node::deserialize(config_content);
|
||||||
|
|
||||||
// Verificar estructura básica
|
|
||||||
if (!yaml.contains("assets")) {
|
if (!yaml.contains("assets")) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid assets.yaml format - missing 'assets' key");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid assets.yaml format - missing 'assets' key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& assets = yaml["assets"];
|
const auto& assets = yaml["assets"];
|
||||||
|
|
||||||
// Iterar sobre cada categoría (fonts, palettes, etc.)
|
// Iterar sobre cada categoría (fonts, palettes, etc.)
|
||||||
@@ -179,105 +240,19 @@ namespace Resource {
|
|||||||
const auto& category_assets = it.value();
|
const auto& category_assets = it.value();
|
||||||
|
|
||||||
if (category_assets.is_mapping()) {
|
if (category_assets.is_mapping()) {
|
||||||
// Nuevo formato: categoría → { TIPO: [paths...], TIPO2: [paths...] }
|
parseModernCategory(*this, category_assets, category, prefix, 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>();
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (category_assets.is_sequence()) {
|
} else if (category_assets.is_sequence()) {
|
||||||
// Formato antiguo (retrocompatibilidad): categoría → [{type, path}, ...]
|
for (const auto& asset : category_assets) { parseLegacyAsset(*this, asset, category, prefix, system_folder); }
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Category '%s' has invalid format, skipping", category.c_str());
|
||||||
"Warning: Category '%s' has invalid format, skipping",
|
|
||||||
category.c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Loaded " << file_list_.size() << " assets from YAML config" << '\n';
|
std::cout << "Loaded " << file_list_.size() << " assets from YAML config" << '\n';
|
||||||
|
|
||||||
} catch (const fkyaml::exception& e) {
|
} catch (const fkyaml::exception& e) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "YAML parsing error: %s", e.what());
|
||||||
"YAML parsing error: %s",
|
|
||||||
e.what());
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error loading assets: %s", e.what());
|
||||||
"Error loading assets: %s",
|
|
||||||
e.what());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ namespace Resource {
|
|||||||
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
|
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
|
||||||
[[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe
|
[[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:
|
private:
|
||||||
// --- Estructuras privadas ---
|
// --- Estructuras privadas ---
|
||||||
struct Item {
|
struct Item {
|
||||||
@@ -63,9 +67,7 @@ namespace Resource {
|
|||||||
|
|
||||||
// --- Métodos internos ---
|
// --- Métodos internos ---
|
||||||
[[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo
|
[[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
|
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
|
static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones
|
||||||
|
|
||||||
// --- Constructores y destructor privados (singleton) ---
|
// --- Constructores y destructor privados (singleton) ---
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ namespace Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load a 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_) {
|
if (!initialized_) {
|
||||||
std::cerr << "Loader: Not initialized\n";
|
std::cerr << "Loader: Not initialized\n";
|
||||||
return {};
|
return {};
|
||||||
@@ -81,7 +81,7 @@ namespace Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a resource exists
|
// 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_) {
|
if (!initialized_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ namespace Resource {
|
|||||||
|
|
||||||
if (checksum == 0) {
|
if (checksum == 0) {
|
||||||
std::cerr << "Loader: Pack checksum is zero (invalid)\n";
|
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
|
std::cout << "Loader: Pack checksum: 0x" << std::hex << checksum << std::dec
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// XOR encryption (symmetric - same function for encrypt/decrypt)
|
// 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()) {
|
if (key.empty()) {
|
||||||
return;
|
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
|
// XOR is symmetric
|
||||||
encryptData(data, key);
|
encryptData(data, key);
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ namespace Resource {
|
|||||||
// Add all files from a directory recursively
|
// Add all files from a directory recursively
|
||||||
auto Pack::addDirectory(const std::string& dir_path,
|
auto Pack::addDirectory(const std::string& dir_path,
|
||||||
const std::string& base_path) -> bool {
|
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)) {
|
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||||
std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n';
|
std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n';
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ auto Debug::get() -> Debug* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dibuja en pantalla
|
// Dibuja en pantalla
|
||||||
void Debug::render() { // NOLINT(readability-make-member-function-const)
|
void Debug::render() {
|
||||||
auto text = Resource::Cache::get()->getText("aseprite");
|
auto text = Resource::Cache::get()->getText("aseprite");
|
||||||
int y = y_;
|
int y = y_;
|
||||||
int w = 0;
|
int w = 0;
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ void MapEditor::render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Maneja eventos del editor
|
// 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.
|
// Si el tile picker está abierto, los eventos van a él.
|
||||||
// Excepción: la T lo cierra como toggle (sin tocar el brush).
|
// Excepción: la T lo cierra como toggle (sin tocar el brush).
|
||||||
const auto* kc = KeyConfig::get();
|
const auto* kc = KeyConfig::get();
|
||||||
@@ -708,7 +708,6 @@ void MapEditor::handleMouseUp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Commit de un drag de entidad (initial, bound1, bound2) para cualquier EntityType.
|
// 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 {
|
auto MapEditor::commitEntityDrag() -> bool {
|
||||||
const int IDX = drag_.index;
|
const int IDX = drag_.index;
|
||||||
const int SNAP_X = static_cast<int>(drag_.snap_x);
|
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.
|
// 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() {
|
void MapEditor::moveEntityVisual() {
|
||||||
switch (drag_.target) {
|
switch (drag_.target) {
|
||||||
case DragTarget::ENTITY_INITIAL:
|
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).
|
// 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) {
|
void MapEditor::stampBrushAt(int tile_x, int tile_y) {
|
||||||
if (brush_.isEmpty()) { return; }
|
if (brush_.isEmpty()) { return; }
|
||||||
for (int dy = 0; dy < brush_.height; ++dy) {
|
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.
|
// 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() {
|
void MapEditor::renderEntityBoundaries() {
|
||||||
auto game_surface = Screen::get()->getRendererSurface();
|
auto game_surface = Screen::get()->getRendererSurface();
|
||||||
if (!game_surface) { return; }
|
if (!game_surface) { return; }
|
||||||
@@ -1374,7 +1370,7 @@ void MapEditor::updateMousePosition() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actualiza la información de la barra de estado
|
// Actualiza la información de la barra de estado
|
||||||
void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitive-complexity)
|
void MapEditor::updateStatusBarInfo() {
|
||||||
if (!statusbar_) { return; }
|
if (!statusbar_) { return; }
|
||||||
|
|
||||||
statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_);
|
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
|
// 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 (!active_) { return "Editor not active"; }
|
||||||
if (!hasSelectedEnemy()) { return "No enemy selected"; }
|
if (!hasSelectedEnemy()) { return "No enemy selected"; }
|
||||||
|
|
||||||
@@ -1730,7 +1726,7 @@ auto MapEditor::duplicateEnemy() -> std::string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Modifica una propiedad de la habitación
|
// 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"; }
|
if (!active_) { return "Editor not active"; }
|
||||||
|
|
||||||
std::string val = toLower(value);
|
std::string val = toLower(value);
|
||||||
@@ -1921,7 +1917,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string&
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Crea una nueva habitación
|
// 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"; }
|
if (!active_) { return "Editor not active"; }
|
||||||
|
|
||||||
// Validar dirección si se proporcionó
|
// Validar dirección si se proporcionó
|
||||||
@@ -2056,7 +2052,7 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { //
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Elimina la habitación actual
|
// 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"; }
|
if (!active_) { return "Editor not active"; }
|
||||||
|
|
||||||
std::string deleted_name = room_path_;
|
std::string deleted_name = room_path_;
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class MapEditor {
|
|||||||
[[nodiscard]] auto getSelectionType() const -> EntityType { return selection_.type; }
|
[[nodiscard]] auto getSelectionType() const -> EntityType { return selection_.type; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado
|
static MapEditor* instance_;
|
||||||
|
|
||||||
MapEditor(); // Constructor
|
MapEditor(); // Constructor
|
||||||
~MapEditor(); // Destructor
|
~MapEditor(); // Destructor
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ void PathEnemy::resetToInitialPosition(const Data& data) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Comprueba si ha llegado al limite del recorrido para darse media vuelta
|
// 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_) {
|
if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) {
|
||||||
// Recoloca
|
// Recoloca
|
||||||
if (sprite_->getPosX() > x2_) {
|
if (sprite_->getPosX() > x2_) {
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ void Player::placeSprite() {
|
|||||||
sprite_->setPos(x_, y_);
|
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) {
|
if (state_ == State::ON_AIR) {
|
||||||
turning_ = false;
|
turning_ = false;
|
||||||
const bool NEAR_PEAK = vy_ > JUMP_VELOCITY * 0.5F && vy_ < -JUMP_VELOCITY * 0.5F;
|
const bool NEAR_PEAK = vy_ > JUMP_VELOCITY * 0.5F && vy_ < -JUMP_VELOCITY * 0.5F;
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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).
|
// 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());
|
solid_actors_->registerActor(door.get());
|
||||||
doors_.push_back(std::move(door));
|
doors_.push_back(std::move(door));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include "utils/utils.hpp" // Para checkCollision
|
#include "utils/utils.hpp" // Para checkCollision
|
||||||
|
|
||||||
// Añade un enemigo a la colección
|
// 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));
|
enemies_.push_back(std::move(enemy));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ ItemManager::ItemManager(std::string room_id, std::shared_ptr<Scoreboard::Data>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Añade un item a la colección
|
// 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));
|
items_.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ KeyManager::KeyManager(std::string room_id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Añade una llave a la colección
|
// 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));
|
keys_.push_back(std::move(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ void Room::setBgColor(Uint8 bg_color) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Actualiza las variables y objetos de la habitación
|
// 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_) {
|
if (is_paused_) {
|
||||||
// Si está en modo pausa no se actualiza nada
|
// Si está en modo pausa no se actualiza nada
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ class Room {
|
|||||||
|
|
||||||
// Constructor y destructor
|
// Constructor y destructor
|
||||||
Room(const std::string& room_path, std::shared_ptr<Scoreboard::Data> data);
|
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 ---
|
// --- Funciones ---
|
||||||
[[nodiscard]] auto getNumber() const -> const std::string& { return number_; }
|
[[nodiscard]] auto getNumber() const -> const std::string& { return number_; }
|
||||||
|
|||||||
@@ -548,31 +548,17 @@ auto RoomFormat::roomConnectionToYAML(const std::string& connection) -> std::str
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
void RoomFormat::writeRoomSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||||
std::ostringstream out;
|
|
||||||
|
|
||||||
// --- Sección room ---
|
|
||||||
out << "room:\n";
|
out << "room:\n";
|
||||||
|
|
||||||
// zone es siempre obligatoria
|
// zone es siempre obligatoria
|
||||||
out << " zone: " << room_data.zone << "\n";
|
out << " zone: " << room_data.zone << "\n";
|
||||||
|
|
||||||
// tileSetFile solo si es override explícito del valor heredado de la zona
|
// tileSetFile/music/bgColor solo si son override explícito del valor heredado de la zona
|
||||||
if (room_data.tile_set_overridden) {
|
if (room_data.tile_set_overridden) { out << " tileSetFile: " << room_data.tile_set_file << "\n"; }
|
||||||
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 << "\n";
|
||||||
out << " # Conexiones de la habitación (null = sin conexión)\n";
|
out << " # Conexiones de la habitación (null = sin conexión)\n";
|
||||||
out << " connections:\n";
|
out << " connections:\n";
|
||||||
@@ -580,48 +566,32 @@ auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { //
|
|||||||
out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n";
|
out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n";
|
||||||
out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n";
|
out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n";
|
||||||
out << " right: " << roomConnectionToYAML(room_data.right_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 << "\n";
|
||||||
out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n";
|
out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n";
|
||||||
out << "tilemap:\n";
|
out << "tilemap:\n";
|
||||||
|
|
||||||
// Mapa de dibujo
|
auto write_grid = [&out](const auto& tilemap, const char* header, int empty_value) {
|
||||||
out << " # Mapa de dibujo (indices de tiles, -1 = vacio)\n";
|
out << header;
|
||||||
out << " draw:\n";
|
|
||||||
for (int row = 0; row < Map::HEIGHT; ++row) {
|
for (int row = 0; row < Map::HEIGHT; ++row) {
|
||||||
out << " - [";
|
out << " - [";
|
||||||
for (int col = 0; col < Map::WIDTH; ++col) {
|
for (int col = 0; col < Map::WIDTH; ++col) {
|
||||||
int index = (row * Map::WIDTH) + col;
|
const int INDEX = (row * Map::WIDTH) + col;
|
||||||
if (index < static_cast<int>(room_data.tile_map.size())) {
|
out << (INDEX < static_cast<int>(tilemap.size()) ? tilemap[INDEX] : empty_value);
|
||||||
out << room_data.tile_map[index];
|
|
||||||
} else {
|
|
||||||
out << -1;
|
|
||||||
}
|
|
||||||
if (col < Map::WIDTH - 1) { out << ", "; }
|
if (col < Map::WIDTH - 1) { out << ", "; }
|
||||||
}
|
}
|
||||||
out << "]\n";
|
out << "]\n";
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Mapa de colisiones
|
write_grid(room_data.tile_map, " # Mapa de dibujo (indices de tiles, -1 = vacio)\n draw:\n", -1);
|
||||||
out << " # Mapa de colisiones (0 = vacio, 1 = solido)\n";
|
write_grid(room_data.collision_tile_map, " # Mapa de colisiones (0 = vacio, 1 = solido)\n collision:\n", 0);
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Enemigos ---
|
void RoomFormat::writeEnemiesSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||||
if (!room_data.enemies.empty()) {
|
if (room_data.enemies.empty()) { return; }
|
||||||
out << "\n";
|
out << "\n";
|
||||||
out << "# Enemigos en esta habitación\n";
|
out << "# Enemigos en esta habitación\n";
|
||||||
out << "enemies:\n";
|
out << "enemies:\n";
|
||||||
@@ -629,58 +599,47 @@ auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { //
|
|||||||
out << " - animation: " << enemy.animation_path << "\n";
|
out << " - animation: " << enemy.animation_path << "\n";
|
||||||
if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; }
|
if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; }
|
||||||
|
|
||||||
int pos_x = static_cast<int>(std::round(enemy.x / Tile::SIZE));
|
const int POS_X = static_cast<int>(std::round(enemy.x / Tile::SIZE));
|
||||||
int pos_y = static_cast<int>(std::round(enemy.y / 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 << " position: {x: " << POS_X << ", y: " << POS_Y << "}\n";
|
||||||
|
|
||||||
out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n";
|
out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n";
|
||||||
|
|
||||||
int b1_x = enemy.x1 / Tile::SIZE;
|
const int B1_X = enemy.x1 / Tile::SIZE;
|
||||||
int b1_y = enemy.y1 / Tile::SIZE;
|
const int B1_Y = enemy.y1 / Tile::SIZE;
|
||||||
int b2_x = enemy.x2 / Tile::SIZE;
|
const int B2_X = enemy.x2 / Tile::SIZE;
|
||||||
int b2_y = enemy.y2 / Tile::SIZE;
|
const int B2_Y = enemy.y2 / Tile::SIZE;
|
||||||
out << " boundaries:\n";
|
out << " boundaries:\n";
|
||||||
out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n";
|
out << " position1: {x: " << B1_X << ", y: " << B1_Y << "}\n";
|
||||||
out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n";
|
out << " position2: {x: " << B2_X << ", y: " << B2_Y << "}\n";
|
||||||
|
|
||||||
if (enemy.flip) { out << " flip: true\n"; }
|
if (enemy.flip) { out << " flip: true\n"; }
|
||||||
if (enemy.mirror) { out << " mirror: true\n"; }
|
if (enemy.mirror) { out << " mirror: true\n"; }
|
||||||
if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; }
|
if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; }
|
||||||
|
|
||||||
out << "\n";
|
out << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Items ---
|
void RoomFormat::writeItemsSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||||
if (!room_data.items.empty()) {
|
if (room_data.items.empty()) { return; }
|
||||||
out << "# Objetos en esta habitación\n";
|
out << "# Objetos en esta habitación\n";
|
||||||
out << "items:\n";
|
out << "items:\n";
|
||||||
for (const auto& item : room_data.items) {
|
for (const auto& item : room_data.items) {
|
||||||
out << " - tileSetFile: " << item.tile_set_file << "\n";
|
out << " - tileSetFile: " << item.tile_set_file << "\n";
|
||||||
out << " tile: " << item.tile << "\n";
|
out << " tile: " << item.tile << "\n";
|
||||||
|
|
||||||
int item_x = static_cast<int>(std::round(item.x / Tile::SIZE));
|
const int ITEM_X = static_cast<int>(std::round(item.x / Tile::SIZE));
|
||||||
int item_y = static_cast<int>(std::round(item.y / Tile::SIZE));
|
const int ITEM_Y = static_cast<int>(std::round(item.y / Tile::SIZE));
|
||||||
out << " position: {x: " << item_x << ", y: " << item_y << "}\n";
|
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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";
|
out << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Plataformas ---
|
void RoomFormat::writePlatformsSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||||
if (!room_data.platforms.empty()) {
|
if (room_data.platforms.empty()) { return; }
|
||||||
out << "# Plataformas móviles en esta habitación\n";
|
out << "# Plataformas móviles en esta habitación\n";
|
||||||
out << "platforms:\n";
|
out << "platforms:\n";
|
||||||
for (const auto& plat : room_data.platforms) {
|
for (const auto& plat : room_data.platforms) {
|
||||||
@@ -691,44 +650,53 @@ auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { //
|
|||||||
if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; }
|
if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; }
|
||||||
out << " path:\n";
|
out << " path:\n";
|
||||||
for (const auto& wp : plat.path) {
|
for (const auto& wp : plat.path) {
|
||||||
int wx = static_cast<int>(std::round(wp.x / Tile::SIZE));
|
const int WX = static_cast<int>(std::round(wp.x / Tile::SIZE));
|
||||||
int wy = static_cast<int>(std::round(wp.y / Tile::SIZE));
|
const int WY = static_cast<int>(std::round(wp.y / Tile::SIZE));
|
||||||
out << " - {x: " << wx << ", y: " << wy;
|
out << " - {x: " << WX << ", y: " << WY;
|
||||||
if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; }
|
if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; }
|
||||||
out << "}\n";
|
out << "}\n";
|
||||||
}
|
}
|
||||||
out << "\n";
|
out << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Llaves ---
|
void RoomFormat::writeKeysSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||||
if (!room_data.keys.empty()) {
|
if (room_data.keys.empty()) { return; }
|
||||||
out << "# Llaves en esta habitación\n";
|
out << "# Llaves en esta habitación\n";
|
||||||
out << "keys:\n";
|
out << "keys:\n";
|
||||||
for (const auto& key : room_data.keys) {
|
for (const auto& key : room_data.keys) {
|
||||||
out << " - animation: " << key.animation_path << "\n";
|
out << " - animation: " << key.animation_path << "\n";
|
||||||
out << " id: \"" << key.id << "\"\n";
|
out << " id: \"" << key.id << "\"\n";
|
||||||
int kx = static_cast<int>(std::round(key.x / Tile::SIZE));
|
const int KX = static_cast<int>(std::round(key.x / Tile::SIZE));
|
||||||
int ky = static_cast<int>(std::round(key.y / Tile::SIZE));
|
const int KY = static_cast<int>(std::round(key.y / Tile::SIZE));
|
||||||
out << " position: {x: " << kx << ", y: " << ky << "}\n";
|
out << " position: {x: " << KX << ", y: " << KY << "}\n";
|
||||||
out << "\n";
|
out << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Puertas ---
|
void RoomFormat::writeDoorsSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||||
if (!room_data.doors.empty()) {
|
if (room_data.doors.empty()) { return; }
|
||||||
out << "# Puertas en esta habitación\n";
|
out << "# Puertas en esta habitación\n";
|
||||||
out << "doors:\n";
|
out << "doors:\n";
|
||||||
for (const auto& door : room_data.doors) {
|
for (const auto& door : room_data.doors) {
|
||||||
out << " - animation: " << door.animation_path << "\n";
|
out << " - animation: " << door.animation_path << "\n";
|
||||||
out << " id: \"" << door.id << "\"\n";
|
out << " id: \"" << door.id << "\"\n";
|
||||||
int dx = static_cast<int>(std::round(door.x / Tile::SIZE));
|
const int DX = static_cast<int>(std::round(door.x / Tile::SIZE));
|
||||||
int dy = static_cast<int>(std::round(door.y / Tile::SIZE));
|
const int DY = static_cast<int>(std::round(door.y / Tile::SIZE));
|
||||||
out << " position: {x: " << dx << ", y: " << dy << "}\n";
|
out << " position: {x: " << DX << ", y: " << DY << "}\n";
|
||||||
out << "\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();
|
return out.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <sstream> // Para ostringstream
|
||||||
#include <string> // Para string
|
#include <string> // Para string
|
||||||
#include <vector> // Para vector
|
#include <vector> // Para vector
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ class RoomFormat {
|
|||||||
// --- Parsing helpers (siempre disponibles, los usa loadYAML) ---
|
// --- Parsing helpers (siempre disponibles, los usa loadYAML) ---
|
||||||
static auto convertRoomConnection(const std::string& value) -> std::string;
|
static auto convertRoomConnection(const std::string& value) -> std::string;
|
||||||
static auto convertAutoSurface(const fkyaml::node& node) -> int;
|
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 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 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);
|
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) ---
|
// --- Serialization helpers (solo en debug, los usa saveYAML) ---
|
||||||
static auto buildContent(const Room::Data& room_data) -> std::string;
|
static auto buildContent(const Room::Data& room_data) -> std::string;
|
||||||
static auto roomConnectionToYAML(const std::string& connection) -> 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
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ auto RoomTracker::addRoom(const std::string& name) -> bool {
|
|||||||
if (!hasBeenVisited(name)) {
|
if (!hasBeenVisited(name)) {
|
||||||
// En caso contrario añádela a la lista
|
// En caso contrario añádela a la lista
|
||||||
rooms_.push_back(name);
|
rooms_.push_back(name);
|
||||||
return true; // NOLINT(readability-simplify-boolean-expr)
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -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).
|
// WALL: bloquea si los pies estaban por encima (como PASSABLE).
|
||||||
// PASSABLE: solo si los pies estaban por encima del borde superior del tile.
|
// 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).
|
// 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 {
|
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));
|
const int START_ROW = toTile(static_cast<int>(foot_y_current));
|
||||||
int end_row = toTile(static_cast<int>(foot_y_new));
|
const int END_ROW = toTile(static_cast<int>(foot_y_new));
|
||||||
int left_col = toTile(static_cast<int>(x));
|
const int LEFT_COL = toTile(static_cast<int>(x));
|
||||||
int right_col = toTile(static_cast<int>(x + w - 1));
|
const int RIGHT_COL = toTile(static_cast<int>(x + w - 1));
|
||||||
|
|
||||||
FloorHit best;
|
FloorHit best;
|
||||||
|
for (int row = START_ROW; row <= END_ROW; ++row) {
|
||||||
for (int row = start_row; row <= end_row; ++row) {
|
for (int col = LEFT_COL; col <= RIGHT_COL; ++col) {
|
||||||
for (int col = left_col; col <= right_col; ++col) {
|
const auto TILE = getTileAt(col, row);
|
||||||
auto tile = getTileAt(col, row);
|
const float FLOOR_Y = floorYForTile(TILE, col, row, x, w, foot_y_current, foot_y_new);
|
||||||
float floor_y = Collision::NONE;
|
if (FLOOR_Y == Collision::NONE) { continue; }
|
||||||
|
if (best.y == Collision::NONE || FLOOR_Y < best.y) {
|
||||||
if (tile == Tile::WALL) {
|
best = {.y = FLOOR_Y, .type = TILE, .tile_x = col, .tile_y = row};
|
||||||
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};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return best;
|
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).
|
// 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
|
// 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.
|
// 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 {
|
auto TileCollider::checkSlopeBelow(float x, float foot_y, float w) const -> SlopeInfo {
|
||||||
int foot_row = toTile(static_cast<int>(foot_y));
|
int foot_row = toTile(static_cast<int>(foot_y));
|
||||||
int left_col = toTile(static_cast<int>(x));
|
int left_col = toTile(static_cast<int>(x));
|
||||||
|
|||||||
@@ -70,4 +70,7 @@ class TileCollider {
|
|||||||
int border_px_; // Offset en píxeles (CollisionBorder::PX)
|
int border_px_; // Offset en píxeles (CollisionBorder::PX)
|
||||||
|
|
||||||
const std::vector<int>& tile_map_;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
+114
-212
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // Para std::clamp, std::ranges::transform
|
||||||
#include <filesystem> // Para create_directories
|
#include <filesystem> // Para create_directories
|
||||||
#include <fstream> // Para ifstream, ofstream
|
#include <fstream> // Para ifstream, ofstream
|
||||||
#include <iostream> // Para cout, cerr
|
#include <iostream> // Para cout, cerr
|
||||||
|
#include <iterator> // Para std::back_inserter
|
||||||
#include <string> // Para string
|
#include <string> // Para string
|
||||||
#include <unordered_map> // Para unordered_map
|
#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
|
// 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; }
|
if (!yaml.contains("audio")) { return; }
|
||||||
const auto& a = yaml["audio"];
|
const auto& a = yaml["audio"];
|
||||||
|
|
||||||
if (a.contains("enabled")) {
|
readYamlField(a, "enabled", audio.enabled);
|
||||||
try {
|
readYamlVolume(a, "volume", audio.volume);
|
||||||
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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (a.contains("music")) {
|
if (a.contains("music")) {
|
||||||
const auto& m = a["music"];
|
const auto& m = a["music"];
|
||||||
if (m.contains("enabled")) {
|
readYamlField(m, "enabled", audio.music.enabled);
|
||||||
try {
|
readYamlVolume(m, "volume", audio.music.volume);
|
||||||
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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (a.contains("sound")) {
|
if (a.contains("sound")) {
|
||||||
const auto& s = a["sound"];
|
const auto& s = a["sound"];
|
||||||
if (s.contains("enabled")) {
|
readYamlField(s, "enabled", audio.sound.enabled);
|
||||||
try {
|
readYamlVolume(s, "volume", audio.sound.volume);
|
||||||
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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carga configuración de la consola desde YAML
|
// 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; }
|
if (!yaml.contains("console")) { return; }
|
||||||
const auto& c = yaml["console"];
|
const auto& c = yaml["console"];
|
||||||
|
|
||||||
if (c.contains("transparent")) {
|
readYamlField(c, "transparent", console.transparent);
|
||||||
try {
|
readYamlPaletteIndex(c, "bg_color", console.bg_color);
|
||||||
console.transparent = c["transparent"].get_value<bool>();
|
readYamlPaletteIndex(c, "msg_color", console.msg_color);
|
||||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
readYamlPaletteIndex(c, "prompt_color", console.prompt_color);
|
||||||
}
|
readYamlPaletteIndex(c, "command_color", console.command_color);
|
||||||
}
|
|
||||||
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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carga configuración de la pantalla de carga de recursos desde YAML
|
// Carga configuración de la pantalla de carga de recursos desde YAML
|
||||||
@@ -1048,31 +1025,25 @@ namespace Options {
|
|||||||
crtpi_file_path = path;
|
crtpi_file_path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carga los presets del shader CrtPi desde el fichero. Crea defaults si no existe.
|
// Defaults dels 4 presets CrtPi (DEFAULT, CURVED, SHARP, MINIMAL)
|
||||||
auto loadCrtPiFromFile() -> bool { // NOLINT(readability-function-cognitive-complexity)
|
static auto defaultCrtPiPresets() -> std::vector<CrtPiPreset> {
|
||||||
crtpi_presets.clear();
|
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}};
|
||||||
|
}
|
||||||
|
|
||||||
std::ifstream file(crtpi_file_path);
|
// Escriu el fitxer CrtPi amb capçalera + els presets default. Retorna false si no pot obrir.
|
||||||
if (!file.good()) {
|
static auto writeCrtPiDefaultFile(const std::string& path, const std::vector<CrtPiPreset>& presets) -> bool {
|
||||||
std::cout << "CrtPi file not found, creating default: " << crtpi_file_path << '\n';
|
const std::filesystem::path P(path);
|
||||||
// Crear directorio padre si no existe
|
|
||||||
const std::filesystem::path P(crtpi_file_path);
|
|
||||||
if (P.has_parent_path()) {
|
if (P.has_parent_path()) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::create_directories(P.parent_path(), ec);
|
std::filesystem::create_directories(P.parent_path(), ec);
|
||||||
}
|
}
|
||||||
// Escribir defaults
|
std::ofstream out(path);
|
||||||
std::ofstream out(crtpi_file_path);
|
if (!out.is_open()) { return false; }
|
||||||
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 << "# Projecte 2026 - CrtPi Shader Presets\n";
|
||||||
out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\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 << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n";
|
||||||
@@ -1083,91 +1054,31 @@ namespace Options {
|
|||||||
out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\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 << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n";
|
||||||
out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
|
out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
|
||||||
out << "\n";
|
out << "\npresets:\n";
|
||||||
out << "presets:\n";
|
for (const auto& p : presets) {
|
||||||
out << " - name: \"DEFAULT\"\n";
|
out << " - name: \"" << p.name << "\"\n";
|
||||||
out << " scanline_weight: 6.0\n";
|
out << " scanline_weight: " << p.scanline_weight << "\n";
|
||||||
out << " scanline_gap_brightness: 0.12\n";
|
out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n";
|
||||||
out << " bloom_factor: 3.5\n";
|
out << " bloom_factor: " << p.bloom_factor << "\n";
|
||||||
out << " input_gamma: 2.4\n";
|
out << " input_gamma: " << p.input_gamma << "\n";
|
||||||
out << " output_gamma: 2.2\n";
|
out << " output_gamma: " << p.output_gamma << "\n";
|
||||||
out << " mask_brightness: 0.80\n";
|
out << " mask_brightness: " << p.mask_brightness << "\n";
|
||||||
out << " curvature_x: 0.05\n";
|
out << " curvature_x: " << p.curvature_x << "\n";
|
||||||
out << " curvature_y: 0.10\n";
|
out << " curvature_y: " << p.curvature_y << "\n";
|
||||||
out << " mask_type: 2\n";
|
out << " mask_type: " << p.mask_type << "\n";
|
||||||
out << " enable_scanlines: true\n";
|
out << " enable_scanlines: " << (p.enable_scanlines ? "true" : "false") << "\n";
|
||||||
out << " enable_multisample: true\n";
|
out << " enable_multisample: " << (p.enable_multisample ? "true" : "false") << "\n";
|
||||||
out << " enable_gamma: true\n";
|
out << " enable_gamma: " << (p.enable_gamma ? "true" : "false") << "\n";
|
||||||
out << " enable_curvature: false\n";
|
out << " enable_curvature: " << (p.enable_curvature ? "true" : "false") << "\n";
|
||||||
out << " enable_sharper: false\n";
|
out << " enable_sharper: " << (p.enable_sharper ? "true" : "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});
|
|
||||||
video.shader.current_crtpi_preset = 0;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
// Parseja un node YAML a un CrtPiPreset usant els helpers genèrics
|
||||||
file.close();
|
static auto parseCrtPiPreset(const fkyaml::node& p) -> CrtPiPreset {
|
||||||
|
|
||||||
try {
|
|
||||||
auto yaml = fkyaml::node::deserialize(content);
|
|
||||||
|
|
||||||
if (yaml.contains("presets")) {
|
|
||||||
const auto& presets = yaml["presets"];
|
|
||||||
for (const auto& p : presets) {
|
|
||||||
CrtPiPreset preset;
|
CrtPiPreset preset;
|
||||||
if (p.contains("name")) {
|
readYamlField(p, "name", preset.name);
|
||||||
preset.name = p["name"].get_value<std::string>();
|
|
||||||
}
|
|
||||||
parseFloatField(p, "scanline_weight", preset.scanline_weight);
|
parseFloatField(p, "scanline_weight", preset.scanline_weight);
|
||||||
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
|
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
|
||||||
parseFloatField(p, "bloom_factor", preset.bloom_factor);
|
parseFloatField(p, "bloom_factor", preset.bloom_factor);
|
||||||
@@ -1176,61 +1087,52 @@ namespace Options {
|
|||||||
parseFloatField(p, "mask_brightness", preset.mask_brightness);
|
parseFloatField(p, "mask_brightness", preset.mask_brightness);
|
||||||
parseFloatField(p, "curvature_x", preset.curvature_x);
|
parseFloatField(p, "curvature_x", preset.curvature_x);
|
||||||
parseFloatField(p, "curvature_y", preset.curvature_y);
|
parseFloatField(p, "curvature_y", preset.curvature_y);
|
||||||
if (p.contains("mask_type")) {
|
readYamlField(p, "mask_type", preset.mask_type);
|
||||||
try {
|
readYamlField(p, "enable_scanlines", preset.enable_scanlines);
|
||||||
preset.mask_type = p["mask_type"].get_value<int>();
|
readYamlField(p, "enable_multisample", preset.enable_multisample);
|
||||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
readYamlField(p, "enable_gamma", preset.enable_gamma);
|
||||||
}
|
readYamlField(p, "enable_curvature", preset.enable_curvature);
|
||||||
}
|
readYamlField(p, "enable_sharper", preset.enable_sharper);
|
||||||
if (p.contains("enable_scanlines")) {
|
return preset;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolver el nombre del preset a índice
|
// Carga los presets del shader CrtPi desde el fichero. Crea defaults si no existe.
|
||||||
|
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';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto yaml = fkyaml::node::deserialize(content);
|
||||||
|
if (yaml.contains("presets")) {
|
||||||
|
const auto& presets_node = yaml["presets"];
|
||||||
|
std::ranges::transform(presets_node, std::back_inserter(crtpi_presets), parseCrtPiPreset);
|
||||||
|
}
|
||||||
if (!crtpi_presets.empty()) {
|
if (!crtpi_presets.empty()) {
|
||||||
resolveCrtPiPresetName();
|
resolveCrtPiPresetName();
|
||||||
} else {
|
} else {
|
||||||
video.shader.current_crtpi_preset = 0;
|
video.shader.current_crtpi_preset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n";
|
std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n";
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (const fkyaml::exception& e) {
|
} catch (const fkyaml::exception& e) {
|
||||||
std::cerr << "Error parsing CrtPi YAML: " << e.what() << '\n';
|
std::cerr << "Error parsing CrtPi YAML: " << e.what() << '\n';
|
||||||
// Cargar defaults en memoria en caso de error
|
|
||||||
crtpi_presets.clear();
|
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;
|
video.shader.current_crtpi_preset = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-18
@@ -798,18 +798,15 @@ auto Game::getOrCreateRoom(const std::string& room_path) -> std::shared_ptr<Room
|
|||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construye el tilemap extendido de la room actual con los bordes de las adyacentes
|
// Obtiene el collision tilemap de una room adyacente (nullptr si no existe)
|
||||||
void Game::buildCollisionBorders() {
|
auto Game::getAdjacentCollision(Room::Border border) -> const std::vector<int>* {
|
||||||
// NOLINTBEGIN(readability-identifier-naming) -- lambdas locales: se leen como llamadas a función, no son constantes.
|
auto name = room_->getRoom(border);
|
||||||
// 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; }
|
if (name == "0") { return nullptr; }
|
||||||
return &getOrCreateRoom(name)->getCollisionTileMap();
|
return &getOrCreateRoom(name)->getCollisionTileMap();
|
||||||
};
|
}
|
||||||
|
|
||||||
// Helper: obtiene el collision tilemap de una room diagonal (A→B o C→D)
|
// 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>* {
|
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'
|
// Camino 1: room en dirección 'first', luego su adyacente en dirección 'second'
|
||||||
auto name1 = room_->getRoom(first);
|
auto name1 = room_->getRoom(first);
|
||||||
if (name1 != "0") {
|
if (name1 != "0") {
|
||||||
@@ -825,9 +822,17 @@ void Game::buildCollisionBorders() {
|
|||||||
if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); }
|
if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); }
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
};
|
}
|
||||||
// NOLINTEND(readability-identifier-naming)
|
|
||||||
|
|
||||||
|
// 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() {
|
||||||
CollisionMap::AdjacentData adj;
|
CollisionMap::AdjacentData adj;
|
||||||
adj.top = getAdjacentCollision(Room::Border::TOP);
|
adj.top = getAdjacentCollision(Room::Border::TOP);
|
||||||
adj.bottom = getAdjacentCollision(Room::Border::BOTTOM);
|
adj.bottom = getAdjacentCollision(Room::Border::BOTTOM);
|
||||||
@@ -845,13 +850,6 @@ void Game::buildCollisionBorders() {
|
|||||||
// que los sweeps del Player vean AABBs dinámicos (puertas, plataformas)
|
// que los sweeps del Player vean AABBs dinámicos (puertas, plataformas)
|
||||||
// de la room vecina cuando está cerca del borde, sin tener que esperar
|
// de la room vecina cuando está cerca del borde, sin tener que esperar
|
||||||
// a una transición completa de room.
|
// 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;
|
SolidActorManager::AdjacentActors sadj;
|
||||||
sadj.upper = getAdjacentSolidActors(Room::Border::TOP);
|
sadj.upper = getAdjacentSolidActors(Room::Border::TOP);
|
||||||
sadj.lower = getAdjacentSolidActors(Room::Border::BOTTOM);
|
sadj.lower = getAdjacentSolidActors(Room::Border::BOTTOM);
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ class Game {
|
|||||||
auto changeRoom(const std::string& room_path) -> bool; // Cambia de habitación
|
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
|
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 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 updateAdjacentRooms(float delta_time); // Actualiza enemigos de las habitaciones adyacentes
|
||||||
void handleInput(); // Comprueba el teclado
|
void handleInput(); // Comprueba el teclado
|
||||||
void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua
|
void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Title::Title()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Destructor
|
// Destructor
|
||||||
Title::~Title() { // NOLINT(modernize-use-equals-default)
|
Title::~Title() {
|
||||||
title_surface_->resetSubPalette();
|
title_surface_->resetSubPalette();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+100
-83
@@ -193,38 +193,31 @@ void Console::redrawText() {
|
|||||||
Screen::get()->setRendererSurface(previous_renderer);
|
Screen::get()->setRendererSurface(previous_renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualiza la animación de la consola
|
// Parpadeig del cursor (només quan ACTIVE)
|
||||||
void Console::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
|
void Console::updateCursorBlink(float delta_time) {
|
||||||
if (status_ == Status::HIDDEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parpadeo del cursor (solo cuando activa)
|
|
||||||
if (status_ == Status::ACTIVE) {
|
|
||||||
cursor_timer_ += delta_time;
|
cursor_timer_ += delta_time;
|
||||||
const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
|
const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
|
||||||
if (cursor_timer_ >= THRESHOLD) {
|
if (cursor_timer_ >= THRESHOLD) {
|
||||||
cursor_timer_ = 0.0F;
|
cursor_timer_ = 0.0F;
|
||||||
cursor_visible_ = !cursor_visible_;
|
cursor_visible_ = !cursor_visible_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Efecto typewriter: revelar letras una a una (solo cuando ACTIVE)
|
// Revelat lletra a lletra de msg_lines_ (només quan ACTIVE)
|
||||||
if (status_ == Status::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()); });
|
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) {
|
if (typewriter_chars_ >= TOTAL_CHARS) { return; }
|
||||||
typewriter_timer_ += delta_time;
|
typewriter_timer_ += delta_time;
|
||||||
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) {
|
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) {
|
||||||
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
|
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
|
||||||
++typewriter_chars_;
|
++typewriter_chars_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
|
// Animació d'altura quan msg_lines_ canvia (només quan ACTIVE i height_ != target_height_)
|
||||||
if (status_ == Status::ACTIVE && height_ != target_height_) {
|
void Console::updateResizeAnimation(float delta_time) {
|
||||||
if (anim_progress_ == 0.0F) {
|
if (anim_progress_ == 0.0F) {
|
||||||
// Iniciar animación de resize
|
// Iniciar animació de resize
|
||||||
anim_start_ = height_;
|
anim_start_ = height_;
|
||||||
anim_end_ = target_height_;
|
anim_end_ = target_height_;
|
||||||
}
|
}
|
||||||
@@ -235,7 +228,7 @@ void Console::update(float delta_time) { // NOLINT(readability-function-cogniti
|
|||||||
height_ = target_height_;
|
height_ = target_height_;
|
||||||
anim_progress_ = 0.0F;
|
anim_progress_ = 0.0F;
|
||||||
}
|
}
|
||||||
// Actualizar el Notifier incrementalmente con el delta de altura
|
// Actualitzar el Notifier incrementalment amb el delta d'altura
|
||||||
if (Notifier::get() != nullptr) {
|
if (Notifier::get() != nullptr) {
|
||||||
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
|
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
|
||||||
if (DELTA_PX > 0) {
|
if (DELTA_PX > 0) {
|
||||||
@@ -246,31 +239,46 @@ void Console::update(float delta_time) { // NOLINT(readability-function-cogniti
|
|||||||
notifier_offset_applied_ += DELTA_PX;
|
notifier_offset_applied_ += DELTA_PX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px)
|
// Reconstruir la Surface al nou tamany (xicoteta: 256×~18-72px)
|
||||||
const float WIDTH = Options::game.width;
|
const float WIDTH = Options::game.width;
|
||||||
surface_ = std::make_shared<Surface>(WIDTH, height_);
|
surface_ = std::make_shared<Surface>(WIDTH, height_);
|
||||||
sprite_->setSurface(surface_);
|
sprite_->setSurface(surface_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redibujar texto cada frame
|
// Animació RISING/VANISHING (basada en temps amb easing)
|
||||||
redrawText();
|
void Console::updateOpenCloseAnimation(float delta_time) {
|
||||||
|
|
||||||
// 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);
|
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
|
||||||
y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
|
y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
|
||||||
|
|
||||||
if (anim_progress_ >= 1.0F) {
|
if (anim_progress_ < 1.0F) { return; }
|
||||||
y_ = anim_end_;
|
y_ = anim_end_;
|
||||||
anim_progress_ = 0.0F;
|
anim_progress_ = 0.0F;
|
||||||
if (status_ == Status::RISING) {
|
if (status_ == Status::RISING) {
|
||||||
status_ = Status::ACTIVE;
|
status_ = Status::ACTIVE;
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
status_ = Status::HIDDEN;
|
status_ = Status::HIDDEN;
|
||||||
|
// Reset del missatge una vegada completament oculta
|
||||||
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
|
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
|
||||||
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
|
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; }
|
||||||
|
|
||||||
|
if (status_ == Status::ACTIVE) {
|
||||||
|
updateCursorBlink(delta_time);
|
||||||
|
updateTypewriter(delta_time);
|
||||||
|
if (height_ != target_height_) {
|
||||||
|
updateResizeAnimation(delta_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redrawText();
|
||||||
|
|
||||||
|
if (status_ == Status::RISING || status_ == Status::VANISHING) {
|
||||||
|
updateOpenCloseAnimation(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
|
SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
|
||||||
@@ -334,14 +342,10 @@ void Console::toggle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Procesa el evento SDL: entrada de texto, Backspace, Enter
|
// Insereix caràcters imprimibles a input_line_ (filtra control i la toggle key activa)
|
||||||
void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity)
|
void Console::handleTextInput(const SDL_Event& event) {
|
||||||
if (status_ != Status::ACTIVE) { return; }
|
|
||||||
|
|
||||||
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
|
||||||
// Filtrar caracteres de control (tab, newline, etc.)
|
|
||||||
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
|
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
|
||||||
// Ignorar texto si la tecla toggle está pulsada (evita escribir su carácter)
|
// Ignorar text si la tecla toggle està pulsada (evita escriure el seu caràcter)
|
||||||
if (KeyConfig::get() != nullptr) {
|
if (KeyConfig::get() != nullptr) {
|
||||||
SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console");
|
SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console");
|
||||||
SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr);
|
SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr);
|
||||||
@@ -354,10 +358,67 @@ void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-funct
|
|||||||
input_line_ += event.text.text;
|
input_line_ += event.text.text;
|
||||||
}
|
}
|
||||||
tab_matches_.clear();
|
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) {
|
||||||
|
if (status_ != Status::ACTIVE) { return; }
|
||||||
|
|
||||||
|
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||||
|
handleTextInput(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.type != SDL_EVENT_KEY_DOWN) { return; }
|
||||||
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
|
||||||
switch (event.key.scancode) {
|
switch (event.key.scancode) {
|
||||||
case SDL_SCANCODE_BACKSPACE:
|
case SDL_SCANCODE_BACKSPACE:
|
||||||
tab_matches_.clear();
|
tab_matches_.clear();
|
||||||
@@ -368,61 +429,17 @@ void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-funct
|
|||||||
processCommand();
|
processCommand();
|
||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_UP:
|
case SDL_SCANCODE_UP:
|
||||||
// Navegar hacia atrás en el historial
|
handleHistoryUp();
|
||||||
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;
|
break;
|
||||||
case SDL_SCANCODE_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
// Navegar hacia el presente en el historial
|
handleHistoryDown();
|
||||||
tab_matches_.clear();
|
|
||||||
if (history_index_ >= 0) {
|
|
||||||
--history_index_;
|
|
||||||
input_line_ = (history_index_ == -1)
|
|
||||||
? saved_input_
|
|
||||||
: history_[static_cast<size_t>(history_index_)];
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_TAB: {
|
case SDL_SCANCODE_TAB:
|
||||||
if (tab_matches_.empty()) {
|
handleTab();
|
||||||
// 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;
|
break;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejecuta el comando introducido y reinicia la línea de input
|
// Ejecuta el comando introducido y reinicia la línea de input
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ class Console {
|
|||||||
void redrawText(); // Redibuja el texto dinámico (msg + input + cursor)
|
void redrawText(); // Redibuja el texto dinámico (msg + input + cursor)
|
||||||
void processCommand(); // Procesa el comando introducido por el usuario
|
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
|
[[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
|
// Objetos de renderizado
|
||||||
std::shared_ptr<Text> text_;
|
std::shared_ptr<Text> text_;
|
||||||
|
|||||||
+148
-153
@@ -69,8 +69,10 @@ static auto applyPreset(const std::vector<std::string>& args) -> std::string {
|
|||||||
if (COUNT == 0) { return "No " + SHADER_LABEL + " presets available"; }
|
if (COUNT == 0) { return "No " + SHADER_LABEL + " presets available"; }
|
||||||
|
|
||||||
const auto PRESET_NAME = [&]() -> std::string {
|
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)
|
// 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.
|
||||||
: presets_postfx[static_cast<size_t>(current_idx)].name; // NOLINT(clang-analyzer-core.CallAndMessage)
|
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);
|
return prettyName(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -424,7 +426,7 @@ static auto cmdSound(const std::vector<std::string>& args) -> std::string {
|
|||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
// DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE <name>]]
|
// 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) ---
|
// --- START subcommands (START SCENE works from any scene) ---
|
||||||
if (!args.empty() && args[0] == "START") {
|
if (!args.empty() && args[0] == "START") {
|
||||||
// START SCENE [<name>] — works from any scene
|
// 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;
|
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 (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
|
||||||
if (args.empty()) { return "usage: room <num>|next|prev|left|right|up|down"; }
|
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]
|
// 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()) {
|
if (args.empty()) {
|
||||||
// Toggle: si está activo → off, si no → on
|
// Toggle: si está activo → off, si no → on
|
||||||
if ((MapEditor::get() != nullptr) && MapEditor::get()->isActive()) {
|
if ((MapEditor::get() != nullptr) && MapEditor::get()->isActive()) {
|
||||||
@@ -812,47 +814,38 @@ static auto cmdHide(const std::vector<std::string>& args) -> std::string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CHEAT [subcomando]
|
// 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 (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
|
||||||
if (args.empty()) { return "usage: cheat [infinite lives|invincibility]"; }
|
if (args.empty()) { return "usage: cheat [infinite lives|invincibility]"; }
|
||||||
|
|
||||||
// CHEAT INFINITE LIVES [ON|OFF]
|
|
||||||
if (args[0] == "INFINITE") {
|
if (args[0] == "INFINITE") {
|
||||||
if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; }
|
if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; }
|
||||||
auto& cheat = Options::cheats.infinite_lives;
|
const std::string_view MODE = (args.size() > 2) ? std::string_view(args[2]) : std::string_view();
|
||||||
using State = Options::Cheat::State;
|
const std::string RES = applyCheatToggle(Options::cheats.infinite_lives, MODE, "Infinite lives");
|
||||||
const std::vector<std::string> REST(args.begin() + 2, args.end());
|
return RES.empty() ? "usage: cheat infinite lives [on|off]" : RES;
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHEAT INVINCIBILITY [ON|OFF]
|
|
||||||
if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") {
|
if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") {
|
||||||
auto& cheat = Options::cheats.invincible;
|
const std::string_view MODE = (args.size() > 1) ? std::string_view(args[1]) : std::string_view();
|
||||||
using State = Options::Cheat::State;
|
const std::string RES = applyCheatToggle(Options::cheats.invincible, MODE, "Invincibility");
|
||||||
if (args.size() == 1) {
|
return RES.empty() ? "usage: cheat invincibility [on|off]" : RES;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "usage: cheat [infinite lives|invincibility]";
|
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>]
|
// 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()) {
|
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);
|
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 ──────────────────────────────────────────────────────────
|
// ── CommandRegistry ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cognitive-complexity)
|
void CommandRegistry::registerHandlers() {
|
||||||
handlers_["cmd_shader"] = cmdShader;
|
handlers_["cmd_shader"] = cmdShader;
|
||||||
handlers_["cmd_border"] = cmdBorder;
|
handlers_["cmd_border"] = cmdBorder;
|
||||||
handlers_["cmd_fullscreen"] = cmdFullscreen;
|
handlers_["cmd_fullscreen"] = cmdFullscreen;
|
||||||
@@ -1072,7 +1065,108 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni
|
|||||||
#endif
|
#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();
|
registerHandlers();
|
||||||
|
|
||||||
// Cargar y parsear el YAML
|
// 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"]) {
|
for (const auto& cat_node : yaml["categories"]) {
|
||||||
const auto CATEGORY = cat_node["name"].get_value<std::string>();
|
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>();
|
const bool CAT_DEBUG_ONLY = cat_node.contains("debug_only") && cat_node["debug_only"].get_value<bool>();
|
||||||
|
const std::vector<std::string> CAT_SCOPES = cat_node.contains("scope") ? parseScopeNode(cat_node["scope"]) : std::vector<std::string>{};
|
||||||
// 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>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cat_node.contains("commands")) { continue; }
|
if (!cat_node.contains("commands")) { continue; }
|
||||||
|
|
||||||
for (const auto& cmd_node : cat_node["commands"]) {
|
for (const auto& cmd_node : cat_node["commands"]) {
|
||||||
CommandDef def;
|
CommandDef def = parseCommandDef(cmd_node, CATEGORY, CAT_DEBUG_ONLY, CAT_SCOPES);
|
||||||
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
|
|
||||||
#ifndef _DEBUG
|
#ifndef _DEBUG
|
||||||
if (def.debug_only) { continue; }
|
if (def.debug_only) { continue; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
commands_.push_back(std::move(def));
|
commands_.push_back(std::move(def));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registrar el handler de HELP (captura this)
|
// 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 {
|
handlers_["cmd_help"] = [this](const std::vector<std::string>& args) -> std::string { return buildHelp(args); };
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Aplanar completions en el mapa global
|
// Aplanar completions en el mapa global
|
||||||
for (const auto& cmd : commands_) {
|
for (const auto& cmd : commands_) {
|
||||||
@@ -1298,12 +1292,17 @@ auto CommandRegistry::generateTerminalHelp() const -> std::string {
|
|||||||
return out.str();
|
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
|
// Agrupar comandos visibles por scope
|
||||||
std::string global_cmds;
|
std::string global_cmds;
|
||||||
std::string debug_cmds;
|
std::string debug_cmds;
|
||||||
std::string editor_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_) {
|
for (const auto& cmd : commands_) {
|
||||||
if (cmd.help_hidden) { continue; }
|
if (cmd.help_hidden) { continue; }
|
||||||
if (!isCommandVisible(cmd)) { continue; }
|
if (!isCommandVisible(cmd)) { continue; }
|
||||||
@@ -1313,16 +1312,12 @@ auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(re
|
|||||||
|
|
||||||
// Clasificar por el PRIMER scope del comando
|
// Clasificar por el PRIMER scope del comando
|
||||||
const std::string& primary = cmd.scopes.empty() ? "global" : cmd.scopes[0];
|
const std::string& primary = cmd.scopes.empty() ? "global" : cmd.scopes[0];
|
||||||
|
|
||||||
if (primary == "editor") {
|
if (primary == "editor") {
|
||||||
if (!editor_cmds.empty()) { editor_cmds += ", "; }
|
append_csv(editor_cmds, kw_lower);
|
||||||
editor_cmds += kw_lower;
|
|
||||||
} else if (primary == "debug") {
|
} else if (primary == "debug") {
|
||||||
if (!debug_cmds.empty()) { debug_cmds += ", "; }
|
append_csv(debug_cmds, kw_lower);
|
||||||
debug_cmds += kw_lower;
|
|
||||||
} else {
|
} else {
|
||||||
if (!global_cmds.empty()) { global_cmds += ", "; }
|
append_csv(global_cmds, kw_lower);
|
||||||
global_cmds += kw_lower;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,4 +59,5 @@ class CommandRegistry {
|
|||||||
void registerHandlers();
|
void registerHandlers();
|
||||||
[[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool;
|
[[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool;
|
||||||
[[nodiscard]] static auto generateKeysHelp(const std::string& scope_filter) -> std::string;
|
[[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"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class Color {
|
|||||||
* @param color Color del enum Cpc
|
* @param color Color del enum Cpc
|
||||||
* @return Índice de paleta (Uint8)
|
* @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);
|
return static_cast<Uint8>(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user