#include "core/rendering/palette_manager.hpp" #include #include #include #include #include #include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" #include "game/defaults.hpp" #include "game/options.hpp" #include "utils/utils.hpp" // ── Conversión string ↔ PaletteSortMode ────────────────────────────────────── auto sortModeFromString(const std::string& str) -> PaletteSortMode { const std::string LOWER = toLower(str); if (LOWER == "optimal") { return PaletteSortMode::OPTIMAL; } if (LOWER == "reference") { return PaletteSortMode::REFERENCE; } return PaletteSortMode::ORIGINAL; } auto sortModeToString(PaletteSortMode mode) -> std::string { switch (mode) { case PaletteSortMode::OPTIMAL: return "optimal"; case PaletteSortMode::REFERENCE: return "reference"; default: return "original"; } } // ── Helpers de color y ordenación de paletas ───────────────────────────────── namespace { // Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB) constexpr auto redOf(Uint32 c) -> int { return static_cast((c >> 16) & 0xFF); } constexpr auto greenOf(Uint32 c) -> int { return static_cast((c >> 8) & 0xFF); } constexpr auto blueOf(Uint32 c) -> int { return static_cast(c & 0xFF); } // Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar) auto rgbDistanceSq(Uint32 a, Uint32 b) -> int { const int DR = redOf(a) - redOf(b); const int DG = greenOf(a) - greenOf(b); const int DB = blueOf(a) - blueOf(b); return (DR * DR) + (DG * DG) + (DB * DB); } // Cuenta los colores activos en la paleta (los que tienen alpha != 0) auto countActiveColors(const Palette& palette) -> size_t { size_t count = 0; for (const auto& c : palette) { if (c == 0) { break; } ++count; } return count; } // Asignación óptima de colores mediante el algoritmo húngaro (Kuhn-Munkres). // Minimiza la distancia RGB total entre la paleta y la referencia. // O(N³) con N = número de colores activos — para N ≤ 256 es instantáneo. // NOLINTNEXTLINE(readability-function-cognitive-complexity) auto sortByOptimal(const Palette& palette, const Palette& reference) -> Palette { const auto N = static_cast(countActiveColors(palette)); const auto M = static_cast(countActiveColors(reference)); const int SZ = std::max(N, M); if (SZ == 0) { return palette; } // Matriz de coste NxM (ampliada a SZxSZ con ceros para hacerla cuadrada) std::vector cost(static_cast(SZ) * static_cast(SZ), 0); for (int i = 0; i < N; ++i) { for (int j = 0; j < M; ++j) { cost[(i * SZ) + j] = rgbDistanceSq(palette[i], reference[j]); } } // Hungarian algorithm (Kuhn-Munkres) — versión basada en potenciales constexpr int INF = INT_MAX / 2; std::vector u(SZ + 1, 0); // Potenciales de filas std::vector v(SZ + 1, 0); // Potenciales de columnas std::vector p(SZ + 1, 0); // p[j] = fila asignada a columna j std::vector way(SZ + 1, 0); for (int i = 1; i <= SZ; ++i) { p[0] = i; int j0 = 0; std::vector minv(SZ + 1, INF); std::vector used(SZ + 1, false); do { used[j0] = true; int i0 = p[j0]; int delta = INF; int j1 = 0; for (int j = 1; j <= SZ; ++j) { if (!used[j]) { int cur = cost[((i0 - 1) * SZ) + (j - 1)] - u[i0] - v[j]; if (cur < minv[j]) { minv[j] = cur; way[j] = j0; } if (minv[j] < delta) { delta = minv[j]; j1 = j; } } } for (int j = 0; j <= SZ; ++j) { if (used[j]) { u[p[j]] += delta; v[j] -= delta; } else { minv[j] -= delta; } } j0 = j1; } while (p[j0] != 0); do { int j1 = way[j0]; p[j0] = p[j1]; j0 = j1; } while (j0 != 0); } // Construir la paleta resultante: assignment[j] = fila asignada a columna j // Queremos result[j] = palette[fila asignada a j] Palette out{}; out.fill(0); for (int j = 1; j <= M && j <= N; ++j) { int row = p[j] - 1; // Índice 0-based en palette if (row >= 0 && row < N) { out[j - 1] = palette[row]; } } // Colores extra de palette que no tienen pareja en reference (N > M) if (N > M) { std::vector used_rows(N, false); for (int j = 1; j <= M; ++j) { int row = p[j] - 1; if (row >= 0 && row < N) { used_rows[row] = true; } } int out_idx = M; for (int i = 0; i < N; ++i) { if (!used_rows[i]) { out[out_idx++] = palette[i]; } } } return out; } // Asignación greedy de colores a la paleta de referencia. // Más rápida pero puede asignar colores subóptimamente. auto sortByReference(const Palette& palette, const Palette& reference) -> Palette { const size_t REF_COUNT = countActiveColors(reference); const size_t N = countActiveColors(palette); std::vector available(palette.begin(), palette.begin() + static_cast(N)); std::vector result; result.reserve(N); // Para cada color de referencia, buscar el más cercano disponible const size_t REFS = std::min(N, REF_COUNT); for (size_t i = 0; i < REFS && !available.empty(); ++i) { const Uint32 REF = reference[i]; auto best = std::ranges::min_element(available, [REF](Uint32 a, Uint32 b) { return rgbDistanceSq(a, REF) < rgbDistanceSq(b, REF); }); result.push_back(*best); available.erase(best); } // Si quedan colores sin asignar, añadirlos al final for (const auto& c : available) { result.push_back(c); } Palette out{}; out.fill(0); std::ranges::copy(result, out.begin()); return out; } } // namespace // ── PaletteManager ─────────────────────────────────────────────────────────── PaletteManager::PaletteManager( std::vector raw_paths, const std::string& initial_name, PaletteSortMode initial_sort_mode, std::shared_ptr game_surface, std::shared_ptr border_surface, OnChangeCallback on_change) : palettes_(std::move(raw_paths)), sort_mode_(initial_sort_mode), game_surface_(std::move(game_surface)), border_surface_(std::move(border_surface)), on_change_(std::move(on_change)) { current_ = findIndex(initial_name); // Leer la paleta de referencia directamente desde el archivo // (Resource::Cache aún no está disponible en este punto del ciclo de vida) const std::string REF_NAME = std::string(Defaults::Video::PALETTE_NAME) + ".pal"; for (const auto& p : palettes_) { if (getFileName(p) == REF_NAME) { reference_palette_ = readPalFile(p); break; } } // Leer y aplicar paleta inicial const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_, reference_palette_); game_surface_->setPalette(INITIAL_PALETTE); border_surface_->setPalette(INITIAL_PALETTE); // Procesar la lista: conservar solo los nombres de archivo (sin ruta) processPathList(); } void PaletteManager::next() { if (++current_ == palettes_.size()) { current_ = 0; } apply(); } void PaletteManager::previous() { current_ = (current_ > 0) ? current_ - 1 : palettes_.size() - 1; apply(); } auto PaletteManager::setByName(const std::string& name) -> bool { const std::string LOWER_NAME = toLower(name + ".pal"); for (size_t i = 0; i < palettes_.size(); ++i) { if (toLower(palettes_[i]) == LOWER_NAME) { current_ = i; apply(); return true; } } return false; } auto PaletteManager::getNames() const -> std::vector { std::vector names; names.reserve(palettes_.size()); for (const auto& p : palettes_) { std::string name = p; const size_t POS = name.find(".pal"); if (POS != std::string::npos) { name.erase(POS, 4); } std::ranges::transform(name, name.begin(), ::tolower); names.push_back(std::move(name)); } return names; } auto PaletteManager::getCurrentName() const -> std::string { std::string name = palettes_.at(current_); const size_t POS = name.find(".pal"); if (POS != std::string::npos) { name.erase(POS, 4); } std::ranges::transform(name, name.begin(), ::tolower); return name; } auto PaletteManager::getPrettyName() const -> std::string { std::string name = getCurrentName(); std::ranges::replace(name, '-', ' '); return name; } void PaletteManager::nextSortMode() { sort_mode_ = static_cast((static_cast(sort_mode_) + 1) % static_cast(PaletteSortMode::COUNT)); Options::video.palette_sort = sortModeToString(sort_mode_); apply(); } void PaletteManager::setSortMode(PaletteSortMode mode) { sort_mode_ = mode; Options::video.palette_sort = sortModeToString(sort_mode_); apply(); } auto PaletteManager::getSortMode() const -> PaletteSortMode { return sort_mode_; } auto PaletteManager::getSortModeName() const -> std::string { return sortModeToString(sort_mode_); } void PaletteManager::apply() { Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_)); Palette sorted = sortPalette(raw, sort_mode_, reference_palette_); game_surface_->loadPalette(sorted); border_surface_->loadPalette(sorted); Options::video.palette = getCurrentName(); if (on_change_) { on_change_(); } } auto PaletteManager::findIndex(const std::string& name) const -> size_t { const std::string LOWER_NAME = toLower(name + ".pal"); for (size_t i = 0; i < palettes_.size(); ++i) { if (toLower(getFileName(palettes_[i])) == LOWER_NAME) { return i; } } // Fallback: buscar la paleta por defecto const std::string DEFAULT_NAME = toLower(std::string(Defaults::Video::PALETTE_NAME) + ".pal"); for (size_t i = 0; i < palettes_.size(); ++i) { if (toLower(getFileName(palettes_[i])) == DEFAULT_NAME) { return i; } } return 0; } void PaletteManager::processPathList() { for (auto& palette : palettes_) { palette = getFileName(palette); } } auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode, const Palette& reference) -> Palette { switch (mode) { case PaletteSortMode::OPTIMAL: return sortByOptimal(palette, reference); case PaletteSortMode::REFERENCE: return sortByReference(palette, reference); default: return palette; } }