344 lines
12 KiB
C++
344 lines
12 KiB
C++
#include "core/rendering/palette_manager.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <climits>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<int>((c >> 16) & 0xFF); }
|
|
constexpr auto greenOf(Uint32 c) -> int { return static_cast<int>((c >> 8) & 0xFF); }
|
|
constexpr auto blueOf(Uint32 c) -> int { return static_cast<int>(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<int>(countActiveColors(palette));
|
|
const auto M = static_cast<int>(countActiveColors(reference));
|
|
const int SZ = std::max(N, M);
|
|
if (SZ == 0) { return palette; }
|
|
|
|
// Matriz de coste NxM (ampliada a SZxSZ con ceros para hacerla cuadrada)
|
|
std::vector<int> cost(SZ * SZ, 0);
|
|
for (int i = 0; i < N; ++i) {
|
|
for (int j = 0; j < M; ++j) {
|
|
cost[(i * SZ) + j] = rgbDistanceSq(palette[i], reference[j]);
|
|
}
|
|
}
|
|
|
|
// Hungarian algorithm (Kuhn-Munkres) — versión basada en potenciales
|
|
constexpr int INF = INT_MAX / 2;
|
|
std::vector<int> u(SZ + 1, 0); // Potenciales de filas
|
|
std::vector<int> v(SZ + 1, 0); // Potenciales de columnas
|
|
std::vector<int> p(SZ + 1, 0); // p[j] = fila asignada a columna j
|
|
std::vector<int> way(SZ + 1, 0);
|
|
|
|
for (int i = 1; i <= SZ; ++i) {
|
|
p[0] = i;
|
|
int j0 = 0;
|
|
std::vector<int> minv(SZ + 1, INF);
|
|
std::vector<bool> used(SZ + 1, false);
|
|
|
|
do {
|
|
used[j0] = true;
|
|
int i0 = p[j0];
|
|
int delta = INF;
|
|
int j1 = 0;
|
|
|
|
for (int j = 1; j <= SZ; ++j) {
|
|
if (!used[j]) {
|
|
int cur = cost[((i0 - 1) * SZ) + (j - 1)] - u[i0] - v[j];
|
|
if (cur < minv[j]) {
|
|
minv[j] = cur;
|
|
way[j] = j0;
|
|
}
|
|
if (minv[j] < delta) {
|
|
delta = minv[j];
|
|
j1 = j;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j <= SZ; ++j) {
|
|
if (used[j]) {
|
|
u[p[j]] += delta;
|
|
v[j] -= delta;
|
|
} else {
|
|
minv[j] -= delta;
|
|
}
|
|
}
|
|
|
|
j0 = j1;
|
|
} while (p[j0] != 0);
|
|
|
|
do {
|
|
int j1 = way[j0];
|
|
p[j0] = p[j1];
|
|
j0 = j1;
|
|
} while (j0 != 0);
|
|
}
|
|
|
|
// Construir la paleta resultante: assignment[j] = fila asignada a columna j
|
|
// Queremos result[j] = palette[fila asignada a j]
|
|
Palette out{};
|
|
out.fill(0);
|
|
for (int j = 1; j <= M && j <= N; ++j) {
|
|
int row = p[j] - 1; // Índice 0-based en palette
|
|
if (row >= 0 && row < N) {
|
|
out[j - 1] = palette[row];
|
|
}
|
|
}
|
|
|
|
// Colores extra de palette que no tienen pareja en reference (N > M)
|
|
if (N > M) {
|
|
std::vector<bool> used_rows(N, false);
|
|
for (int j = 1; j <= M; ++j) {
|
|
int row = p[j] - 1;
|
|
if (row >= 0 && row < N) { used_rows[row] = true; }
|
|
}
|
|
int out_idx = M;
|
|
for (int i = 0; i < N; ++i) {
|
|
if (!used_rows[i]) {
|
|
out[out_idx++] = palette[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
// 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<Uint32> available(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
|
std::vector<Uint32> 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<std::string> raw_paths,
|
|
const std::string& initial_name,
|
|
PaletteSortMode initial_sort_mode,
|
|
std::shared_ptr<Surface> game_surface,
|
|
std::shared_ptr<Surface> 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::string> {
|
|
std::vector<std::string> 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<PaletteSortMode>((static_cast<int>(sort_mode_) + 1) % static_cast<int>(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;
|
|
}
|
|
}
|