segon commit
This commit is contained in:
195
source/core/audio/audio.cpp
Normal file
195
source/core/audio/audio.cpp
Normal file
@@ -0,0 +1,195 @@
|
||||
#include "audio.hpp"
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
|
||||
|
||||
#include <algorithm> // Para clamp
|
||||
#include <iostream> // Para std::cout
|
||||
|
||||
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp)
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
// clang-format on
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
|
||||
|
||||
// Singleton
|
||||
Audio* Audio::instance = nullptr;
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Audio::init() { Audio::instance = new Audio(); }
|
||||
|
||||
// Libera la instancia
|
||||
void Audio::destroy() { delete Audio::instance; }
|
||||
|
||||
// Obtiene la instancia
|
||||
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||
|
||||
// Constructor
|
||||
Audio::Audio() { initSDLAudio(); }
|
||||
|
||||
// Destructor
|
||||
Audio::~Audio() {
|
||||
JA_Quit();
|
||||
}
|
||||
|
||||
// Método principal
|
||||
void Audio::update() {
|
||||
JA_Update();
|
||||
}
|
||||
|
||||
// Reproduce la música
|
||||
void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
bool new_loop = (loop != 0);
|
||||
|
||||
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
|
||||
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentar obtener recurso; si falla, no tocar estado
|
||||
auto* resource = Resource::Cache::get()->getMusic(name);
|
||||
if (resource == nullptr) {
|
||||
// manejo de error opcional
|
||||
return;
|
||||
}
|
||||
|
||||
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
|
||||
}
|
||||
|
||||
// Llamada al motor para reproducir la nueva pista
|
||||
JA_PlayMusic(resource, loop);
|
||||
|
||||
// Actualizar estado y metadatos después de iniciar con éxito
|
||||
music_.name = name;
|
||||
music_.loop = new_loop;
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Pausa la música
|
||||
void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||
JA_PauseMusic();
|
||||
music_.state = MusicState::PAUSED;
|
||||
}
|
||||
}
|
||||
|
||||
// Continua la música pausada
|
||||
void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||
JA_ResumeMusic();
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene la música
|
||||
void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
|
||||
if (music_enabled_) {
|
||||
JA_StopMusic();
|
||||
music_.state = MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por nombre
|
||||
void Audio::playSound(const std::string& name, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(Resource::Cache::get()->getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por puntero directo
|
||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene todos los sonidos
|
||||
void Audio::stopAllSounds() const {
|
||||
if (sound_enabled_) {
|
||||
JA_StopChannel(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un fundido de salida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) const {
|
||||
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
||||
JA_FadeOutMusic(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Consulta directamente el estado real de la música en jailaudio
|
||||
auto Audio::getRealMusicState() -> MusicState {
|
||||
JA_Music_state ja_state = JA_GetMusicState();
|
||||
switch (ja_state) {
|
||||
case JA_MUSIC_PLAYING:
|
||||
return MusicState::PLAYING;
|
||||
case JA_MUSIC_PAUSED:
|
||||
return MusicState::PAUSED;
|
||||
case JA_MUSIC_STOPPED:
|
||||
case JA_MUSIC_INVALID:
|
||||
case JA_MUSIC_DISABLED:
|
||||
default:
|
||||
return MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
|
||||
// Establece el volumen de los sonidos
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
|
||||
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Establece el volumen de la música
|
||||
void Audio::setMusicVolume(float music_volume) const {
|
||||
if (music_enabled_) {
|
||||
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
|
||||
JA_SetMusicVolume(CONVERTED_VOLUME);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica la configuración
|
||||
void Audio::applySettings() {
|
||||
enable(Options::audio.enabled);
|
||||
}
|
||||
|
||||
// Establecer estado general
|
||||
void Audio::enable(bool value) {
|
||||
enabled_ = value;
|
||||
|
||||
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
|
||||
}
|
||||
|
||||
// Inicializa SDL Audio
|
||||
void Audio::initSDLAudio() {
|
||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
|
||||
} else {
|
||||
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||
enable(Options::audio.enabled);
|
||||
|
||||
// Aplicar estado de música y sonido guardado en las opciones.
|
||||
// enable() ya aplica los volúmenes, pero no toca music_enabled_/sound_enabled_.
|
||||
// Si alguno está desactivado, hay que forzar el volumen a 0 en el backend.
|
||||
if (!Options::audio.music.enabled) {
|
||||
setMusicVolume(0.0F); // music_enabled_=true aún → llega a JA
|
||||
enableMusic(false);
|
||||
}
|
||||
if (!Options::audio.sound.enabled) {
|
||||
setSoundVolume(0.0F); // sound_enabled_=true aún → llega a JA
|
||||
enableSound(false);
|
||||
}
|
||||
|
||||
std::cout << "\n** AUDIO SYSTEM **\n";
|
||||
std::cout << "Audio system initialized successfully\n";
|
||||
}
|
||||
}
|
||||
97
source/core/audio/audio.hpp
Normal file
97
source/core/audio/audio.hpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
// --- Clase Audio: gestor de audio (singleton) ---
|
||||
class Audio {
|
||||
public:
|
||||
// --- Enums ---
|
||||
enum class Group : int {
|
||||
ALL = -1, // Todos los grupos
|
||||
GAME = 0, // Sonidos del juego
|
||||
INTERFACE = 1 // Sonidos de la interfaz
|
||||
};
|
||||
|
||||
enum class MusicState {
|
||||
PLAYING, // Reproduciendo música
|
||||
PAUSED, // Música pausada
|
||||
STOPPED, // Música detenida
|
||||
};
|
||||
|
||||
// --- Constantes ---
|
||||
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo
|
||||
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
|
||||
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
|
||||
|
||||
// --- Singleton ---
|
||||
static void init(); // Inicializa el objeto Audio
|
||||
static void destroy(); // Libera el objeto Audio
|
||||
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
|
||||
Audio(const Audio&) = delete; // Evitar copia
|
||||
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
|
||||
|
||||
static void update(); // Actualización del sistema de audio
|
||||
|
||||
// --- Control de música ---
|
||||
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
|
||||
// --- Control de sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
|
||||
// --- Control de volumen ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
void setMusicVolume(float volume) const; // Ajustar volumen de música
|
||||
|
||||
// --- Configuración general ---
|
||||
void enable(bool value); // Establecer estado general
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
|
||||
void applySettings(); // Aplica la configuración
|
||||
|
||||
// --- Configuración de sonidos ---
|
||||
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
|
||||
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
|
||||
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
|
||||
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
|
||||
|
||||
// --- Configuración de música ---
|
||||
void enableMusic() { music_enabled_ = true; } // Habilitar música
|
||||
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
|
||||
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
|
||||
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
|
||||
|
||||
// --- Consultas de estado ---
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
|
||||
[[nodiscard]] static auto getRealMusicState() -> MusicState;
|
||||
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||
|
||||
private:
|
||||
// --- Tipos anidados ---
|
||||
struct Music {
|
||||
MusicState state{MusicState::STOPPED}; // Estado actual de la música
|
||||
std::string name; // Última pista de música reproducida
|
||||
bool loop{false}; // Indica si se reproduce en bucle
|
||||
};
|
||||
|
||||
// --- Métodos ---
|
||||
Audio(); // Constructor privado
|
||||
~Audio(); // Destructor privado
|
||||
void initSDLAudio(); // Inicializa SDL Audio
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Audio* instance; // Instancia única de Audio
|
||||
|
||||
Music music_; // Estado de la música
|
||||
bool enabled_{true}; // Estado general del audio
|
||||
bool sound_enabled_{true}; // Estado de los efectos de sonido
|
||||
bool music_enabled_{true}; // Estado de la música
|
||||
};
|
||||
482
source/core/audio/jail_audio.hpp
Normal file
482
source/core/audio/jail_audio.hpp
Normal file
@@ -0,0 +1,482 @@
|
||||
#pragma once
|
||||
|
||||
// --- Includes ---
|
||||
#include <SDL3/SDL.h>
|
||||
#include <stdint.h> // Para uint32_t, uint8_t
|
||||
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
|
||||
#include <stdlib.h> // Para free, malloc
|
||||
#include <string.h> // Para strcpy, strlen
|
||||
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory
|
||||
|
||||
// --- Public Enums ---
|
||||
enum JA_Channel_state { JA_CHANNEL_INVALID,
|
||||
JA_CHANNEL_FREE,
|
||||
JA_CHANNEL_PLAYING,
|
||||
JA_CHANNEL_PAUSED,
|
||||
JA_SOUND_DISABLED };
|
||||
enum JA_Music_state { JA_MUSIC_INVALID,
|
||||
JA_MUSIC_PLAYING,
|
||||
JA_MUSIC_PAUSED,
|
||||
JA_MUSIC_STOPPED,
|
||||
JA_MUSIC_DISABLED };
|
||||
|
||||
// --- Struct Definitions ---
|
||||
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
|
||||
#define JA_MAX_GROUPS 2
|
||||
|
||||
struct JA_Sound_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{NULL};
|
||||
};
|
||||
|
||||
struct JA_Channel_t {
|
||||
JA_Sound_t* sound{nullptr};
|
||||
int pos{0};
|
||||
int times{0};
|
||||
int group{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Channel_state state{JA_CHANNEL_FREE};
|
||||
};
|
||||
|
||||
struct JA_Music_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{nullptr};
|
||||
char* filename{nullptr};
|
||||
|
||||
int pos{0};
|
||||
int times{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Music_state state{JA_MUSIC_INVALID};
|
||||
};
|
||||
|
||||
// --- Internal Global State ---
|
||||
// Marcado 'inline' (C++17) para asegurar una única instancia.
|
||||
|
||||
inline JA_Music_t* current_music{nullptr};
|
||||
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
|
||||
|
||||
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
|
||||
inline float JA_musicVolume{1.0f};
|
||||
inline float JA_soundVolume[JA_MAX_GROUPS];
|
||||
inline bool JA_musicEnabled{true};
|
||||
inline bool JA_soundEnabled{true};
|
||||
inline SDL_AudioDeviceID sdlAudioDevice{0};
|
||||
|
||||
inline bool fading{false};
|
||||
inline int fade_start_time{0};
|
||||
inline int fade_duration{0};
|
||||
inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float'
|
||||
|
||||
// --- Forward Declarations ---
|
||||
inline void JA_StopMusic();
|
||||
inline void JA_StopChannel(const int channel);
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
|
||||
|
||||
// --- Core Functions ---
|
||||
|
||||
inline void JA_Update() {
|
||||
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
|
||||
if (fading) {
|
||||
int time = SDL_GetTicks();
|
||||
if (time > (fade_start_time + fade_duration)) {
|
||||
fading = false;
|
||||
JA_StopMusic();
|
||||
return;
|
||||
} else {
|
||||
const int time_passed = time - fade_start_time;
|
||||
const float percent = (float)time_passed / (float)fade_duration;
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
|
||||
}
|
||||
}
|
||||
|
||||
if (current_music->times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
|
||||
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
|
||||
}
|
||||
if (current_music->times > 0) current_music->times--;
|
||||
} else {
|
||||
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
if (JA_soundEnabled) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
if (channels[i].times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
|
||||
if (channels[i].times > 0) channels[i].times--;
|
||||
}
|
||||
} else {
|
||||
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||
#ifdef _DEBUG
|
||||
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
|
||||
#endif
|
||||
|
||||
JA_audioSpec = {format, num_channels, freq};
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
|
||||
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
|
||||
}
|
||||
|
||||
inline void JA_Quit() {
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
|
||||
sdlAudioDevice = 0;
|
||||
}
|
||||
|
||||
// --- Music Functions ---
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
JA_Music_t* music = new JA_Music_t();
|
||||
|
||||
int chan, samplerate;
|
||||
short* output;
|
||||
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
|
||||
|
||||
music->spec.channels = chan;
|
||||
music->spec.freq = samplerate;
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
|
||||
SDL_memcpy(music->buffer, output, music->length);
|
||||
free(output);
|
||||
music->pos = 0;
|
||||
music->state = JA_MUSIC_STOPPED;
|
||||
|
||||
return music;
|
||||
}
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
|
||||
FILE* f = fopen(filename, "rb");
|
||||
if (!f) return NULL; // Añadida comprobación de apertura
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
|
||||
if (!buffer) { // Añadida comprobación de malloc
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
if (fread(buffer, fsize, 1, f) != 1) {
|
||||
fclose(f);
|
||||
free(buffer);
|
||||
return NULL;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
JA_Music_t* music = JA_LoadMusic(buffer, fsize);
|
||||
if (music) { // Comprobar que JA_LoadMusic tuvo éxito
|
||||
music->filename = static_cast<char*>(malloc(strlen(filename) + 1));
|
||||
if (music->filename) {
|
||||
strcpy(music->filename, filename);
|
||||
}
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
|
||||
return music;
|
||||
}
|
||||
|
||||
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music
|
||||
|
||||
JA_StopMusic();
|
||||
|
||||
current_music = music;
|
||||
current_music->pos = 0;
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
current_music->times = loop;
|
||||
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||
if (!current_music->stream) { // Comprobar creación de stream
|
||||
SDL_Log("Failed to create audio stream!");
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
return;
|
||||
}
|
||||
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
||||
}
|
||||
|
||||
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
|
||||
if (!music) music = current_music;
|
||||
if (!music) return nullptr; // Añadida comprobación
|
||||
return music->filename;
|
||||
}
|
||||
|
||||
inline void JA_PauseMusic() {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
|
||||
|
||||
current_music->state = JA_MUSIC_PAUSED;
|
||||
SDL_UnbindAudioStream(current_music->stream);
|
||||
}
|
||||
|
||||
inline void JA_ResumeMusic() {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
|
||||
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||
}
|
||||
|
||||
inline void JA_StopMusic() {
|
||||
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
||||
|
||||
current_music->pos = 0;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->stream) {
|
||||
SDL_DestroyAudioStream(current_music->stream);
|
||||
current_music->stream = nullptr;
|
||||
}
|
||||
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
|
||||
}
|
||||
|
||||
inline void JA_FadeOutMusic(const int milliseconds) {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
|
||||
|
||||
fading = true;
|
||||
fade_start_time = SDL_GetTicks();
|
||||
fade_duration = milliseconds;
|
||||
fade_initial_volume = JA_musicVolume;
|
||||
}
|
||||
|
||||
inline JA_Music_state JA_GetMusicState() {
|
||||
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
||||
if (!current_music) return JA_MUSIC_INVALID;
|
||||
|
||||
return current_music->state;
|
||||
}
|
||||
|
||||
inline void JA_DeleteMusic(JA_Music_t* music) {
|
||||
if (!music) return;
|
||||
if (current_music == music) {
|
||||
JA_StopMusic();
|
||||
current_music = nullptr;
|
||||
}
|
||||
SDL_free(music->buffer);
|
||||
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
||||
free(music->filename); // filename se libera aquí
|
||||
delete music;
|
||||
}
|
||||
|
||||
inline float JA_SetMusicVolume(float volume) {
|
||||
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
||||
if (current_music && current_music->stream) {
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||
}
|
||||
return JA_musicVolume;
|
||||
}
|
||||
|
||||
inline void JA_SetMusicPosition(float value) {
|
||||
if (!current_music) return;
|
||||
current_music->pos = value * current_music->spec.freq;
|
||||
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
|
||||
// el streaming. El streaming siempre parece empezar desde el principio.
|
||||
}
|
||||
|
||||
inline float JA_GetMusicPosition() {
|
||||
if (!current_music) return 0;
|
||||
return float(current_music->pos) / float(current_music->spec.freq);
|
||||
// Nota: Ver `JA_SetMusicPosition`
|
||||
}
|
||||
|
||||
inline void JA_EnableMusic(const bool value) {
|
||||
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
||||
|
||||
JA_musicEnabled = value;
|
||||
}
|
||||
|
||||
// --- Sound Functions ---
|
||||
|
||||
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
sound->buffer = buffer;
|
||||
sound->length = length;
|
||||
// Nota: spec se queda con los valores por defecto.
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
|
||||
delete sound;
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
|
||||
delete sound;
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
|
||||
if (!JA_soundEnabled || !sound) return -1;
|
||||
|
||||
int channel = 0;
|
||||
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
||||
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
// No hay canal libre, reemplazamos el primero
|
||||
channel = 0;
|
||||
}
|
||||
|
||||
return JA_PlaySoundOnChannel(sound, channel, loop, group);
|
||||
}
|
||||
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
|
||||
if (!JA_soundEnabled || !sound) return -1;
|
||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
|
||||
|
||||
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
|
||||
|
||||
channels[channel].sound = sound;
|
||||
channels[channel].times = loop;
|
||||
channels[channel].pos = 0;
|
||||
channels[channel].group = group; // Asignar grupo
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||
|
||||
if (!channels[channel].stream) {
|
||||
SDL_Log("Failed to create audio stream for sound!");
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
||||
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||
if (!sound) return;
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||
}
|
||||
SDL_free(sound->buffer);
|
||||
delete sound;
|
||||
}
|
||||
|
||||
inline void JA_PauseChannel(const int channel) {
|
||||
if (!JA_soundEnabled) return;
|
||||
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
channels[i].state = JA_CHANNEL_PAUSED;
|
||||
SDL_UnbindAudioStream(channels[i].stream);
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
||||
channels[channel].state = JA_CHANNEL_PAUSED;
|
||||
SDL_UnbindAudioStream(channels[channel].stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_ResumeChannel(const int channel) {
|
||||
if (!JA_soundEnabled) return;
|
||||
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||
if (channels[i].state == JA_CHANNEL_PAUSED) {
|
||||
channels[i].state = JA_CHANNEL_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_StopChannel(const int channel) {
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].state != JA_CHANNEL_FREE) {
|
||||
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
|
||||
channels[i].stream = nullptr;
|
||||
channels[i].state = JA_CHANNEL_FREE;
|
||||
channels[i].pos = 0;
|
||||
channels[i].sound = NULL;
|
||||
}
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state != JA_CHANNEL_FREE) {
|
||||
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
|
||||
channels[channel].stream = nullptr;
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
channels[channel].pos = 0;
|
||||
channels[channel].sound = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline JA_Channel_state JA_GetChannelState(const int channel) {
|
||||
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
|
||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
|
||||
|
||||
return channels[channel].state;
|
||||
}
|
||||
|
||||
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
|
||||
{
|
||||
const float v = SDL_clamp(volume, 0.0f, 1.0f);
|
||||
|
||||
if (group == -1) {
|
||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
|
||||
JA_soundVolume[i] = v;
|
||||
}
|
||||
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
||||
JA_soundVolume[group] = v;
|
||||
} else {
|
||||
return v; // Grupo inválido
|
||||
}
|
||||
|
||||
// Aplicar volumen a canales activos
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
||||
if (group == -1 || channels[i].group == group) {
|
||||
if (channels[i].stream) {
|
||||
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
inline void JA_EnableSound(const bool value) {
|
||||
if (!value) {
|
||||
JA_StopChannel(-1); // Detener todos los canales
|
||||
}
|
||||
JA_soundEnabled = value;
|
||||
}
|
||||
|
||||
inline float JA_SetVolume(float volume) {
|
||||
float v = JA_SetMusicVolume(volume);
|
||||
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
|
||||
return v;
|
||||
}
|
||||
310
source/core/input/global_inputs.cpp
Normal file
310
source/core/input/global_inputs.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
#include "core/input/global_inputs.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para allocator, operator+, char_traits, string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT
|
||||
#include "core/locale/locale.hpp" // Para Locale
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo, Section
|
||||
#include "game/scene_manager.hpp" // Para SceneManager
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier, NotificationText
|
||||
#include "utils/utils.hpp" // Para stringInVector
|
||||
|
||||
namespace GlobalInputs {
|
||||
|
||||
// Funciones internas
|
||||
namespace {
|
||||
void handleQuit() {
|
||||
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko)
|
||||
if (SceneManager::current == SceneManager::Scene::GAME) {
|
||||
const std::string CODE = "PRESS AGAIN TO RETURN TO MENU";
|
||||
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
|
||||
SceneManager::current = SceneManager::Scene::TITLE;
|
||||
} else {
|
||||
Notifier::get()->show({Locale::get()->get("ui.press_again_menu")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// En modo kiosko, fuera de GAME: mostrar el texto del kiosko y no salir nunca
|
||||
if (Options::kiosk.enabled) {
|
||||
const std::string KIOSK_CODE = "KIOSK_EXIT";
|
||||
if (!stringInVector(Notifier::get()->getCodes(), KIOSK_CODE)) {
|
||||
Notifier::get()->show({Options::kiosk.text}, Notifier::Style::DEFAULT, -1, true, KIOSK_CODE);
|
||||
}
|
||||
// Segunda pulsación: notificación ya activa → no hacer nada
|
||||
return;
|
||||
}
|
||||
|
||||
// Comportamiento normal fuera del modo kiosko
|
||||
const std::string CODE = "PRESS AGAIN TO EXIT";
|
||||
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
|
||||
SceneManager::current = SceneManager::Scene::QUIT;
|
||||
} else {
|
||||
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
|
||||
void handleSkipSection() {
|
||||
switch (SceneManager::current) {
|
||||
case SceneManager::Scene::LOGO:
|
||||
case SceneManager::Scene::LOADING_SCREEN:
|
||||
case SceneManager::Scene::CREDITS:
|
||||
case SceneManager::Scene::DEMO:
|
||||
case SceneManager::Scene::GAME_OVER:
|
||||
case SceneManager::Scene::ENDING:
|
||||
case SceneManager::Scene::ENDING2:
|
||||
SceneManager::current = SceneManager::Scene::TITLE;
|
||||
SceneManager::options = SceneManager::Options::NONE;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handleToggleBorder() {
|
||||
Screen::get()->toggleBorder();
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.border.enabled ? "ui.border_enabled" : "ui.border_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleToggleVideoMode() {
|
||||
Screen::get()->toggleVideoMode();
|
||||
Notifier::get()->show({Locale::get()->get(static_cast<int>(Options::video.fullscreen) == 0 ? "ui.fullscreen_disabled" : "ui.fullscreen_enabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleDecWindowZoom() {
|
||||
if (Screen::get()->decWindowZoom()) {
|
||||
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
|
||||
void handleIncWindowZoom() {
|
||||
if (Screen::get()->incWindowZoom()) {
|
||||
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
|
||||
void handleToggleShaders() {
|
||||
Screen::get()->toggleShaders();
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.shader.enabled ? "ui.shaders_enabled" : "ui.shaders_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleNextShaderPreset() {
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
if (!Options::crtpi_presets.empty()) {
|
||||
Options::video.shader.current_crtpi_preset = (Options::video.shader.current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
||||
Screen::get()->reloadCrtPi();
|
||||
Notifier::get()->show({Locale::get()->get("ui.crtpi") + " " + prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
} else {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
Options::video.shader.current_postfx_preset = (Options::video.shader.current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||
Screen::get()->reloadPostFX();
|
||||
Notifier::get()->show({Locale::get()->get("ui.postfx") + " " + prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleNextShader() {
|
||||
Screen::get()->nextShader();
|
||||
Notifier::get()->show({Locale::get()->get("ui.shader") + " " + // NOLINT(readability-static-accessed-through-instance)
|
||||
(Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX")});
|
||||
}
|
||||
|
||||
void handleNextPalette() {
|
||||
Screen::get()->nextPalette();
|
||||
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handlePreviousPalette() {
|
||||
Screen::get()->previousPalette();
|
||||
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleNextPaletteSortMode() {
|
||||
Screen::get()->nextPaletteSortMode();
|
||||
Notifier::get()->show({Locale::get()->get("ui.palette_sort") + " " + toUpper(Screen::get()->getPaletteSortModeName())}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleToggleIntegerScale() {
|
||||
Screen::get()->toggleIntegerScale();
|
||||
Screen::get()->setVideoMode(Options::video.fullscreen);
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.integer_scale ? "ui.integer_scale_enabled" : "ui.integer_scale_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleToggleVSync() {
|
||||
Screen::get()->toggleVSync();
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
// Detecta qué acción global ha sido presionada (si alguna)
|
||||
auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity)
|
||||
if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::EXIT;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::ACCEPT;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_BORDER, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_BORDER;
|
||||
}
|
||||
if (!Options::kiosk.enabled) {
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_FULLSCREEN, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_FULLSCREEN;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::WINDOW_DEC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::WINDOW_DEC_ZOOM;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::WINDOW_INC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::WINDOW_INC_ZOOM;
|
||||
}
|
||||
}
|
||||
if (Screen::get()->isHardwareAccelerated()) {
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
|
||||
return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4
|
||||
}
|
||||
if (Options::video.shader.enabled && ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0U)) {
|
||||
return InputAction::NEXT_SHADER_PRESET; // Shift+F4
|
||||
}
|
||||
return InputAction::TOGGLE_SHADER; // F4
|
||||
}
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
|
||||
return InputAction::PREVIOUS_PALETTE; // Ctrl+F5
|
||||
}
|
||||
return InputAction::NEXT_PALETTE; // F5
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::NEXT_PALETTE_SORT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::NEXT_PALETTE_SORT; // F6
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_INTEGER_SCALE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_INTEGER_SCALE;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_VSYNC, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_VSYNC;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_INFO, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_INFO;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_CONSOLE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_CONSOLE;
|
||||
}
|
||||
return InputAction::NONE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Funciones públicas
|
||||
|
||||
// Comprueba los inputs que se pueden introducir en cualquier sección del juego
|
||||
void handle() {
|
||||
const bool CONSOLE_ACTIVE = Console::get() != nullptr && Console::get()->isActive();
|
||||
|
||||
if (CONSOLE_ACTIVE) {
|
||||
// TAB/ESC cierran la consola en lugar de ejecutar sus acciones normales
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_CONSOLE, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
Console::get()->toggle();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Salida de administrador en modo kiosko (Ctrl+Shift+Alt+Q)
|
||||
if (Options::kiosk.enabled) {
|
||||
SDL_Keymod mod = SDL_GetModState();
|
||||
const bool* ks = SDL_GetKeyboardState(nullptr);
|
||||
if (((mod & SDL_KMOD_CTRL) != 0U) && ((mod & SDL_KMOD_SHIFT) != 0U) && ((mod & SDL_KMOD_ALT) != 0U) && ks[SDL_SCANCODE_Q]) {
|
||||
SceneManager::current = SceneManager::Scene::QUIT;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detectar qué acción global está siendo presionada
|
||||
InputAction action = getPressedAction();
|
||||
|
||||
// Con consola activa, ACCEPT (saltar sección) y EXIT están bloqueados
|
||||
if (CONSOLE_ACTIVE && (action == InputAction::ACCEPT || action == InputAction::EXIT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ejecutar el handler correspondiente usando switch statement
|
||||
switch (action) {
|
||||
case InputAction::EXIT:
|
||||
handleQuit();
|
||||
break;
|
||||
|
||||
case InputAction::ACCEPT:
|
||||
handleSkipSection();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_BORDER:
|
||||
handleToggleBorder();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_FULLSCREEN:
|
||||
handleToggleVideoMode();
|
||||
break;
|
||||
|
||||
case InputAction::WINDOW_DEC_ZOOM:
|
||||
handleDecWindowZoom();
|
||||
break;
|
||||
|
||||
case InputAction::WINDOW_INC_ZOOM:
|
||||
handleIncWindowZoom();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_SHADER:
|
||||
handleToggleShaders();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_SHADER_PRESET:
|
||||
handleNextShaderPreset();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_SUPERSAMPLING:
|
||||
handleNextShader();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_PALETTE:
|
||||
handleNextPalette();
|
||||
break;
|
||||
|
||||
case InputAction::PREVIOUS_PALETTE:
|
||||
handlePreviousPalette();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_PALETTE_SORT:
|
||||
handleNextPaletteSortMode();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_INTEGER_SCALE:
|
||||
handleToggleIntegerScale();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_VSYNC:
|
||||
handleToggleVSync();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_CONSOLE:
|
||||
if (Console::get() != nullptr) { Console::get()->toggle(); }
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_INFO:
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->toggle(); }
|
||||
break;
|
||||
|
||||
case InputAction::NONE:
|
||||
default:
|
||||
// No se presionó ninguna acción global
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace GlobalInputs
|
||||
6
source/core/input/global_inputs.hpp
Normal file
6
source/core/input/global_inputs.hpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace GlobalInputs {
|
||||
// Comprueba los inputs que se pueden introducir en cualquier sección del juego
|
||||
void handle();
|
||||
} // namespace GlobalInputs
|
||||
478
source/core/input/input.cpp
Normal file
478
source/core/input/input.cpp
Normal file
@@ -0,0 +1,478 @@
|
||||
#include "core/input/input.hpp"
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
||||
|
||||
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
||||
#include <ranges> // Para __find_if_fn, find_if
|
||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||
#include <utility> // Para pair, move
|
||||
|
||||
#include "game/options.hpp" // Para Options::controls
|
||||
|
||||
// Singleton
|
||||
Input* Input::instance = nullptr;
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Input::init(const std::string& game_controller_db_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Input::instance = new Input(game_controller_db_path);
|
||||
}
|
||||
|
||||
// Libera la instancia
|
||||
void Input::destroy() { delete Input::instance; }
|
||||
|
||||
// Obtiene la instancia
|
||||
auto Input::get() -> Input* { return Input::instance; }
|
||||
|
||||
// Constructor
|
||||
Input::Input(std::string game_controller_db_path)
|
||||
: gamepad_mappings_file_(std::move(game_controller_db_path)) {
|
||||
// Inicializar bindings del teclado
|
||||
keyboard_.bindings = {
|
||||
// Movimiento del jugador
|
||||
{Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
|
||||
{Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
|
||||
{Action::JUMP, KeyState{.scancode = SDL_SCANCODE_UP}},
|
||||
|
||||
// Inputs de control
|
||||
{Action::ACCEPT, KeyState{.scancode = SDL_SCANCODE_RETURN}},
|
||||
{Action::CANCEL, KeyState{.scancode = SDL_SCANCODE_ESCAPE}},
|
||||
{Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}},
|
||||
|
||||
// Inputs de sistema
|
||||
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
|
||||
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
||||
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
||||
{Action::TOGGLE_SHADER, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||
{Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}},
|
||||
{Action::NEXT_PALETTE_SORT, KeyState{.scancode = SDL_SCANCODE_F6}},
|
||||
{Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}},
|
||||
{Action::TOGGLE_IN_GAME_MUSIC, KeyState{.scancode = SDL_SCANCODE_F8}},
|
||||
{Action::TOGGLE_BORDER, KeyState{.scancode = SDL_SCANCODE_F9}},
|
||||
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F10}},
|
||||
{Action::PAUSE, KeyState{.scancode = SDL_SCANCODE_F11}},
|
||||
{Action::TOGGLE_INFO, KeyState{.scancode = SDL_SCANCODE_F12}},
|
||||
{Action::TOGGLE_CONSOLE, KeyState{.scancode = SDL_SCANCODE_GRAVE}}};
|
||||
|
||||
initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD
|
||||
}
|
||||
|
||||
// Asigna inputs a teclas
|
||||
void Input::bindKey(Action action, SDL_Scancode code) {
|
||||
keyboard_.bindings[action].scancode = code;
|
||||
}
|
||||
|
||||
// Aplica las teclas configuradas desde Options
|
||||
void Input::applyKeyboardBindingsFromOptions() {
|
||||
bindKey(Action::LEFT, Options::keyboard_controls.key_left);
|
||||
bindKey(Action::RIGHT, Options::keyboard_controls.key_right);
|
||||
bindKey(Action::JUMP, Options::keyboard_controls.key_jump);
|
||||
}
|
||||
|
||||
// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
|
||||
void Input::applyGamepadBindingsFromOptions() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Si no hay gamepads conectados, no hay nada que hacer
|
||||
if (gamepads_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener el primer gamepad conectado
|
||||
const auto& gamepad = gamepads_[0];
|
||||
|
||||
// Aplicar bindings desde Options
|
||||
// Los valores pueden ser:
|
||||
// - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders)
|
||||
// - 100: L2 trigger
|
||||
// - 101: R2 trigger
|
||||
// - 200+: Ejes del stick analógico
|
||||
gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left;
|
||||
gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right;
|
||||
gamepad->bindings[Action::JUMP].button = Options::gamepad_controls.button_jump;
|
||||
}
|
||||
|
||||
// Asigna inputs a botones del mando
|
||||
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (gamepad != nullptr) {
|
||||
gamepad->bindings[action].button = button;
|
||||
}
|
||||
}
|
||||
|
||||
// Asigna inputs a botones del mando
|
||||
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (gamepad != nullptr) {
|
||||
gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si alguna acción está activa
|
||||
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
bool success_keyboard = false;
|
||||
bool success_controller = false;
|
||||
|
||||
if (check_keyboard) {
|
||||
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
|
||||
success_keyboard = keyboard_.bindings[action].is_held;
|
||||
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
|
||||
success_keyboard = keyboard_.bindings[action].just_pressed;
|
||||
}
|
||||
}
|
||||
|
||||
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
|
||||
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||
active_gamepad = gamepads_[0];
|
||||
}
|
||||
|
||||
if (active_gamepad != nullptr) {
|
||||
success_controller = checkAxisInput(action, active_gamepad, repeat);
|
||||
|
||||
if (!success_controller) {
|
||||
success_controller = checkTriggerInput(action, active_gamepad, repeat);
|
||||
}
|
||||
|
||||
if (!success_controller) {
|
||||
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
|
||||
success_controller = active_gamepad->bindings[action].is_held;
|
||||
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
|
||||
success_controller = active_gamepad->bindings[action].just_pressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (success_keyboard || success_controller);
|
||||
}
|
||||
|
||||
// Comprueba si hay almenos una acción activa
|
||||
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Obtenemos el número total de acciones posibles para iterar sobre ellas.
|
||||
|
||||
// --- Comprobación del Teclado ---
|
||||
if (check_keyboard) {
|
||||
for (const auto& pair : keyboard_.bindings) {
|
||||
// Simplemente leemos el estado pre-calculado por Input::update().
|
||||
// Ya no se llama a SDL_GetKeyboardState ni se modifica el estado '.active'.
|
||||
if (pair.second.just_pressed) {
|
||||
return true; // Se encontró una acción recién pulsada.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
|
||||
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||
active_gamepad = gamepads_[0];
|
||||
}
|
||||
|
||||
// --- Comprobación del Mando ---
|
||||
// Comprobamos si hay mandos y si el índice solicitado es válido.
|
||||
if (active_gamepad != nullptr) {
|
||||
// Iteramos sobre todas las acciones, no sobre el número de mandos.
|
||||
for (const auto& pair : active_gamepad->bindings) {
|
||||
// Leemos el estado pre-calculado para el mando y la acción específicos.
|
||||
if (pair.second.just_pressed) {
|
||||
return true; // Se encontró una acción recién pulsada en el mando.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si llegamos hasta aquí, no se detectó ninguna nueva pulsación.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comprueba si hay algún botón pulsado
|
||||
auto Input::checkAnyButton(bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Solo comprueba los botones definidos previamente
|
||||
for (auto bi : BUTTON_INPUTS) {
|
||||
// Comprueba el teclado
|
||||
if (checkAction(bi, repeat, CHECK_KEYBOARD)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Comprueba los mandos
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comprueba si hay algun mando conectado
|
||||
auto Input::gameControllerFound() const -> bool { return !gamepads_.empty(); }
|
||||
|
||||
// Obten el nombre de un mando de juego
|
||||
auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string {
|
||||
return gamepad == nullptr ? std::string() : gamepad->name;
|
||||
}
|
||||
|
||||
// Obtiene la lista de nombres de mandos
|
||||
auto Input::getControllerNames() const -> std::vector<std::string> {
|
||||
std::vector<std::string> names;
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
names.push_back(gamepad->name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// Obten el número de mandos conectados
|
||||
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }
|
||||
|
||||
// Obtiene el gamepad a partir de un event.id
|
||||
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
if (gamepad->instance_id == id) {
|
||||
return gamepad;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
if (gamepad && gamepad->name == name) {
|
||||
return gamepad;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Obtiene el SDL_GamepadButton asignado a un action
|
||||
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton { // NOLINT(readability-convert-member-functions-to-static)
|
||||
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
|
||||
}
|
||||
|
||||
// Comprueba el eje del mando
|
||||
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Obtener el binding configurado para esta acción
|
||||
auto& binding = gamepad->bindings[action];
|
||||
|
||||
// Solo revisar ejes si el binding está configurado como eje (valores 200+)
|
||||
// 200 = Left stick izquierda, 201 = Left stick derecha
|
||||
if (binding.button < 200) {
|
||||
// El binding no es un eje, no revisar axis
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determinar qué eje y dirección revisar según el binding
|
||||
bool axis_active_now = false;
|
||||
|
||||
if (binding.button == 200) {
|
||||
// Left stick izquierda
|
||||
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
|
||||
} else if (binding.button == 201) {
|
||||
// Left stick derecha
|
||||
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
|
||||
} else {
|
||||
// Binding de eje no soportado
|
||||
return false;
|
||||
}
|
||||
|
||||
if (repeat) {
|
||||
// Si se permite repetir, simplemente devolvemos el estado actual
|
||||
return axis_active_now;
|
||||
} // Si no se permite repetir, aplicamos la lógica de transición
|
||||
if (axis_active_now && !binding.axis_active) {
|
||||
// Transición de inactivo a activo
|
||||
binding.axis_active = true;
|
||||
return true;
|
||||
}
|
||||
if (!axis_active_now && binding.axis_active) {
|
||||
// Transición de activo a inactivo
|
||||
binding.axis_active = false;
|
||||
}
|
||||
// Mantener el estado actual
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comprueba los triggers del mando como botones digitales
|
||||
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Solo manejamos botones específicos que pueden ser triggers
|
||||
if (gamepad->bindings[action].button != static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) {
|
||||
// Solo procesamos L2 y R2 como triggers
|
||||
int button = gamepad->bindings[action].button;
|
||||
|
||||
// Verificar si el botón mapeado corresponde a un trigger virtual
|
||||
// (Para esto necesitamos valores especiales que representen L2/R2 como botones)
|
||||
bool trigger_active_now = false;
|
||||
|
||||
// Usamos constantes especiales para L2 y R2 como botones
|
||||
if (button == TRIGGER_L2_AS_BUTTON) { // L2 como botón
|
||||
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER);
|
||||
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
|
||||
} else if (button == TRIGGER_R2_AS_BUTTON) { // R2 como botón
|
||||
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
|
||||
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
|
||||
} else {
|
||||
return false; // No es un trigger
|
||||
}
|
||||
|
||||
// Referencia al binding correspondiente
|
||||
auto& binding = gamepad->bindings[action];
|
||||
|
||||
if (repeat) {
|
||||
// Si se permite repetir, simplemente devolvemos el estado actual
|
||||
return trigger_active_now;
|
||||
}
|
||||
|
||||
// Si no se permite repetir, aplicamos la lógica de transición
|
||||
if (trigger_active_now && !binding.trigger_active) {
|
||||
// Transición de inactivo a activo
|
||||
binding.trigger_active = true;
|
||||
return true;
|
||||
}
|
||||
if (!trigger_active_now && binding.trigger_active) {
|
||||
// Transición de activo a inactivo
|
||||
binding.trigger_active = false;
|
||||
}
|
||||
|
||||
// Mantener el estado actual
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Input::addGamepadMappingsFromFile() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
|
||||
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void Input::discoverGamepads() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
handleEvent(event); // Comprueba mandos conectados
|
||||
}
|
||||
}
|
||||
|
||||
void Input::initSDLGamePad() {
|
||||
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
|
||||
} else {
|
||||
addGamepadMappingsFromFile();
|
||||
discoverGamepads();
|
||||
std::cout << "\n** INPUT SYSTEM **\n";
|
||||
std::cout << "Input System initialized successfully\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Input::resetInputStates() {
|
||||
// Resetear todos los KeyBindings.active a false
|
||||
for (auto& key : keyboard_.bindings) {
|
||||
key.second.is_held = false;
|
||||
key.second.just_pressed = false;
|
||||
}
|
||||
// Resetear todos los ControllerBindings.active a false
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
for (auto& binding : gamepad->bindings) {
|
||||
binding.second.is_held = false;
|
||||
binding.second.just_pressed = false;
|
||||
binding.second.trigger_active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Input::update() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// --- TECLADO ---
|
||||
const bool* key_states = SDL_GetKeyboardState(nullptr);
|
||||
|
||||
for (auto& binding : keyboard_.bindings) {
|
||||
bool key_is_down_now = key_states[binding.second.scancode];
|
||||
|
||||
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
|
||||
binding.second.is_held = key_is_down_now;
|
||||
}
|
||||
|
||||
// --- MANDOS ---
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
for (auto& binding : gamepad->bindings) {
|
||||
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||
|
||||
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||
binding.second.is_held = button_is_down_now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Input::handleEvent(const SDL_Event& event) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
return addGamepad(event.gdevice.which);
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
return removeGamepad(event.gdevice.which);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
|
||||
if (pad == nullptr) {
|
||||
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
auto gamepad = std::make_shared<Gamepad>(pad);
|
||||
auto name = gamepad->name;
|
||||
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
||||
gamepads_.push_back(std::move(gamepad));
|
||||
return name + " CONNECTED";
|
||||
}
|
||||
|
||||
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) -> bool {
|
||||
return gamepad->instance_id == id;
|
||||
});
|
||||
|
||||
if (it != gamepads_.end()) {
|
||||
std::string name = (*it)->name;
|
||||
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
||||
gamepads_.erase(it);
|
||||
return name + " DISCONNECTED";
|
||||
}
|
||||
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
void Input::printConnectedGamepads() const { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (gamepads_.empty()) {
|
||||
std::cout << "No hay gamepads conectados." << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Gamepads conectados:\n";
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
std::string name = gamepad->name.empty() ? "Desconocido" : gamepad->name;
|
||||
std::cout << " - ID: " << gamepad->instance_id
|
||||
<< ", Nombre: " << name << ")" << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Si no hay gamepads disponibles, devolver gamepad por defecto
|
||||
if (gamepads_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Buscar por nombre
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
if (gamepad && gamepad->name == gamepad_name) {
|
||||
return gamepad;
|
||||
}
|
||||
}
|
||||
|
||||
// Si no se encuentra por nombre, devolver el primer gamepad válido
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
if (gamepad) {
|
||||
return gamepad;
|
||||
}
|
||||
}
|
||||
|
||||
// Si llegamos aquí, no hay gamepads válidos
|
||||
return nullptr;
|
||||
}
|
||||
140
source/core/input/input.hpp
Normal file
140
source/core/input/input.hpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_Scancode, SDL_GamepadButton, SDL_JoystickID, SDL_CloseGamepad, SDL_Gamepad, SDL_GetGamepadJoystick, SDL_GetGamepadName, SDL_GetGamepadPath, SDL_GetJoystickID, Sint16, Uint8, SDL_Event
|
||||
|
||||
#include <array> // Para array
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string, basic_string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
#include <utility> // Para pair
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/input/input_types.hpp" // for InputAction
|
||||
|
||||
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
|
||||
class Input {
|
||||
public:
|
||||
// --- Constantes ---
|
||||
static constexpr bool ALLOW_REPEAT = true; // Permite repetición
|
||||
static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición
|
||||
static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado
|
||||
static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado
|
||||
static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón
|
||||
static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón
|
||||
|
||||
// --- Tipos ---
|
||||
using Action = InputAction; // Alias para mantener compatibilidad
|
||||
|
||||
// --- Estructuras ---
|
||||
struct KeyState {
|
||||
Uint8 scancode{0}; // Scancode asociado
|
||||
bool is_held{false}; // Está pulsada ahora mismo
|
||||
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||
};
|
||||
|
||||
struct ButtonState {
|
||||
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado
|
||||
bool is_held{false}; // Está pulsada ahora mismo
|
||||
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||
bool axis_active{false}; // Estado del eje
|
||||
bool trigger_active{false}; // Estado del trigger como botón digital
|
||||
};
|
||||
|
||||
struct Keyboard {
|
||||
std::unordered_map<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
|
||||
};
|
||||
|
||||
struct Gamepad {
|
||||
SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL
|
||||
SDL_JoystickID instance_id{0}; // ID de instancia del joystick
|
||||
std::string name; // Nombre del gamepad
|
||||
std::string path; // Ruta del dispositivo
|
||||
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
|
||||
|
||||
explicit Gamepad(SDL_Gamepad* gamepad)
|
||||
: pad(gamepad),
|
||||
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
|
||||
name(std::string(SDL_GetGamepadName(gamepad))),
|
||||
path(std::string(SDL_GetGamepadPath(pad))),
|
||||
bindings{
|
||||
// Movimiento del jugador
|
||||
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}} {}
|
||||
|
||||
~Gamepad() {
|
||||
if (pad != nullptr) {
|
||||
SDL_CloseGamepad(pad);
|
||||
}
|
||||
}
|
||||
|
||||
// Reasigna un botón a una acción
|
||||
void rebindAction(Action action, SDL_GamepadButton new_button) {
|
||||
bindings[action].button = static_cast<int>(new_button);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tipos ---
|
||||
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
|
||||
|
||||
// --- Singleton ---
|
||||
static void init(const std::string& game_controller_db_path);
|
||||
static void destroy();
|
||||
static auto get() -> Input*;
|
||||
|
||||
// --- Actualización del sistema ---
|
||||
void update(); // Actualiza estados de entrada
|
||||
|
||||
// --- Configuración de controles ---
|
||||
void bindKey(Action action, SDL_Scancode code);
|
||||
void applyKeyboardBindingsFromOptions();
|
||||
void applyGamepadBindingsFromOptions();
|
||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
|
||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
|
||||
|
||||
// --- Consulta de entrada ---
|
||||
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||
void resetInputStates();
|
||||
|
||||
// --- Gestión de gamepads ---
|
||||
[[nodiscard]] auto gameControllerFound() const -> bool;
|
||||
[[nodiscard]] auto getNumGamepads() const -> int;
|
||||
[[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
|
||||
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
||||
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
||||
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
|
||||
void printConnectedGamepads() const;
|
||||
|
||||
// --- Eventos ---
|
||||
auto handleEvent(const SDL_Event& event) -> std::string;
|
||||
|
||||
private:
|
||||
// --- Constantes ---
|
||||
static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos
|
||||
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango)
|
||||
static constexpr std::array<Action, 1> BUTTON_INPUTS = {Action::JUMP}; // Inputs que usan botones
|
||||
|
||||
// --- Métodos ---
|
||||
explicit Input(std::string game_controller_db_path);
|
||||
~Input() = default;
|
||||
|
||||
void initSDLGamePad();
|
||||
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||
auto addGamepad(int device_index) -> std::string;
|
||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||
void addGamepadMappingsFromFile();
|
||||
void discoverGamepads();
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Input* instance; // Instancia única del singleton
|
||||
|
||||
Gamepads gamepads_; // Lista de gamepads conectados
|
||||
Keyboard keyboard_{}; // Estado del teclado
|
||||
std::string gamepad_mappings_file_; // Ruta al archivo de mappings
|
||||
};
|
||||
82
source/core/input/input_types.cpp
Normal file
82
source/core/input/input_types.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "input_types.hpp"
|
||||
|
||||
#include <utility> // Para pair
|
||||
|
||||
// Definición de los mapas
|
||||
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::LEFT, "LEFT"},
|
||||
{InputAction::RIGHT, "RIGHT"},
|
||||
{InputAction::JUMP, "JUMP"},
|
||||
{InputAction::PAUSE, "PAUSE"},
|
||||
{InputAction::EXIT, "EXIT"},
|
||||
{InputAction::ACCEPT, "ACCEPT"},
|
||||
{InputAction::CANCEL, "CANCEL"},
|
||||
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||
{InputAction::TOGGLE_VSYNC, "TOGGLE_VSYNC"},
|
||||
{InputAction::TOGGLE_INTEGER_SCALE, "TOGGLE_INTEGER_SCALE"},
|
||||
{InputAction::TOGGLE_BORDER, "TOGGLE_BORDER"},
|
||||
{InputAction::TOGGLE_IN_GAME_MUSIC, "TOGGLE_MUSIC"},
|
||||
{InputAction::NEXT_PALETTE, "NEXT_PALETTE"},
|
||||
{InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"},
|
||||
{InputAction::NEXT_PALETTE_SORT, "NEXT_PALETTE_SORT"},
|
||||
{InputAction::TOGGLE_SHADER, "TOGGLE_POSTFX"},
|
||||
{InputAction::NEXT_SHADER_PRESET, "NEXT_POSTFX_PRESET"},
|
||||
{InputAction::TOGGLE_INFO, "TOGGLE_DEBUG"},
|
||||
{InputAction::NONE, "NONE"}};
|
||||
|
||||
const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"LEFT", InputAction::LEFT},
|
||||
{"RIGHT", InputAction::RIGHT},
|
||||
{"JUMP", InputAction::JUMP},
|
||||
{"PAUSE", InputAction::PAUSE},
|
||||
{"EXIT", InputAction::EXIT},
|
||||
{"ACCEPT", InputAction::ACCEPT},
|
||||
{"CANCEL", InputAction::CANCEL},
|
||||
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||
{"TOGGLE_VSYNC", InputAction::TOGGLE_VSYNC},
|
||||
{"TOGGLE_INTEGER_SCALE", InputAction::TOGGLE_INTEGER_SCALE},
|
||||
{"TOGGLE_BORDER", InputAction::TOGGLE_BORDER},
|
||||
{"TOGGLE_MUSIC", InputAction::TOGGLE_IN_GAME_MUSIC},
|
||||
{"NEXT_PALETTE", InputAction::NEXT_PALETTE},
|
||||
{"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE},
|
||||
{"NEXT_PALETTE_SORT", InputAction::NEXT_PALETTE_SORT},
|
||||
{"TOGGLE_POSTFX", InputAction::TOGGLE_SHADER},
|
||||
{"NEXT_POSTFX_PRESET", InputAction::NEXT_SHADER_PRESET},
|
||||
{"TOGGLE_DEBUG", InputAction::TOGGLE_INFO},
|
||||
{"NONE", InputAction::NONE}};
|
||||
|
||||
const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING = {
|
||||
{SDL_GAMEPAD_BUTTON_WEST, "WEST"},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, "NORTH"},
|
||||
{SDL_GAMEPAD_BUTTON_EAST, "EAST"},
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"},
|
||||
{SDL_GAMEPAD_BUTTON_START, "START"},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, "BACK"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"},
|
||||
{static_cast<SDL_GamepadButton>(100), "L2_AS_BUTTON"},
|
||||
{static_cast<SDL_GamepadButton>(101), "R2_AS_BUTTON"}};
|
||||
|
||||
const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON = {
|
||||
{"WEST", SDL_GAMEPAD_BUTTON_WEST},
|
||||
{"NORTH", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"EAST", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"START", SDL_GAMEPAD_BUTTON_START},
|
||||
{"BACK", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
{"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
{"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"L2_AS_BUTTON", static_cast<SDL_GamepadButton>(100)},
|
||||
{"R2_AS_BUTTON", static_cast<SDL_GamepadButton>(101)}};
|
||||
47
source/core/input/input_types.hpp
Normal file
47
source/core/input/input_types.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
// --- Enums ---
|
||||
enum class InputAction : int { // Acciones de entrada posibles en el juego
|
||||
// Inputs de movimiento
|
||||
LEFT,
|
||||
RIGHT,
|
||||
JUMP,
|
||||
|
||||
// Inputs de control
|
||||
PAUSE,
|
||||
EXIT,
|
||||
ACCEPT,
|
||||
CANCEL,
|
||||
|
||||
// Inputs de sistema
|
||||
WINDOW_INC_ZOOM,
|
||||
WINDOW_DEC_ZOOM,
|
||||
TOGGLE_FULLSCREEN,
|
||||
TOGGLE_VSYNC,
|
||||
TOGGLE_INTEGER_SCALE,
|
||||
TOGGLE_SHADER,
|
||||
NEXT_SHADER_PRESET,
|
||||
TOGGLE_SUPERSAMPLING,
|
||||
TOGGLE_BORDER,
|
||||
TOGGLE_IN_GAME_MUSIC,
|
||||
NEXT_PALETTE,
|
||||
PREVIOUS_PALETTE,
|
||||
NEXT_PALETTE_SORT,
|
||||
TOGGLE_INFO,
|
||||
TOGGLE_CONSOLE,
|
||||
|
||||
// Input obligatorio
|
||||
NONE,
|
||||
SIZE,
|
||||
};
|
||||
|
||||
// --- Variables ---
|
||||
extern const std::unordered_map<InputAction, std::string> ACTION_TO_STRING; // Mapeo de acción a string
|
||||
extern const std::unordered_map<std::string, InputAction> STRING_TO_ACTION; // Mapeo de string a acción
|
||||
extern const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING; // Mapeo de botón a string
|
||||
extern const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON; // Mapeo de string a botón
|
||||
25
source/core/input/mouse.cpp
Normal file
25
source/core/input/mouse.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "core/input/mouse.hpp"
|
||||
|
||||
namespace Mouse {
|
||||
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor
|
||||
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
|
||||
bool cursor_visible = true; // Estado del cursor
|
||||
|
||||
void handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
last_mouse_move_time = SDL_GetTicks();
|
||||
if (!cursor_visible) {
|
||||
SDL_ShowCursor();
|
||||
cursor_visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateCursorVisibility() {
|
||||
Uint32 current_time = SDL_GetTicks();
|
||||
if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) {
|
||||
SDL_HideCursor();
|
||||
cursor_visible = false;
|
||||
}
|
||||
}
|
||||
} // namespace Mouse
|
||||
12
source/core/input/mouse.hpp
Normal file
12
source/core/input/mouse.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Mouse {
|
||||
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor
|
||||
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
|
||||
extern bool cursor_visible; // Estado del cursor
|
||||
|
||||
void handleEvent(const SDL_Event& event);
|
||||
void updateCursorVisibility();
|
||||
} // namespace Mouse
|
||||
104
source/core/locale/locale.cpp
Normal file
104
source/core/locale/locale.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "core/locale/locale.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
|
||||
// [SINGLETON]
|
||||
Locale* Locale::instance = nullptr;
|
||||
|
||||
// [SINGLETON] Crea el objeto con esta función estática
|
||||
void Locale::init(const std::string& file_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Locale::instance = new Locale();
|
||||
Locale::instance->loadFromFile(file_path);
|
||||
}
|
||||
|
||||
// [SINGLETON] Crea el objeto desde contenido en memoria (para release con pack)
|
||||
void Locale::initFromContent(const std::string& content) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Locale::instance = new Locale();
|
||||
Locale::instance->loadFromContent(content);
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruye el objeto con esta función estática
|
||||
void Locale::destroy() {
|
||||
delete Locale::instance;
|
||||
Locale::instance = nullptr;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Locale::get() -> Locale* {
|
||||
return Locale::instance;
|
||||
}
|
||||
|
||||
// Devuelve la traducción de la clave o la clave como fallback
|
||||
auto Locale::get(const std::string& key) const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::cerr << "Locale: clave no encontrada: " << key << '\n';
|
||||
return key;
|
||||
}
|
||||
|
||||
// Aplana un nodo YAML de forma recursiva: {a: {b: "val"}} -> {"a.b" -> "val"}
|
||||
void Locale::flatten(const void* node_ptr, const std::string& prefix) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto& node = *static_cast<const fkyaml::node*>(node_ptr);
|
||||
|
||||
for (auto itr = node.begin(); itr != node.end(); ++itr) {
|
||||
const std::string KEY = prefix.empty()
|
||||
? itr.key().get_value<std::string>()
|
||||
: prefix + "." + itr.key().get_value<std::string>();
|
||||
|
||||
const auto& value = itr.value();
|
||||
if (value.is_mapping()) {
|
||||
flatten(&value, KEY);
|
||||
} else if (value.is_string()) {
|
||||
strings_[KEY] = value.get_value<std::string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las traducciones desde contenido YAML en memoria
|
||||
void Locale::loadFromContent(const std::string& content) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (content.empty()) {
|
||||
std::cerr << "Locale: contenido vacío, sin traducciones cargadas\n";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
std::istringstream stream(content);
|
||||
auto yaml = fkyaml::node::deserialize(stream);
|
||||
flatten(&yaml, "");
|
||||
|
||||
std::cout << "Locale: " << strings_.size() << " traducciones cargadas desde pack\n";
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Locale: error al parsear YAML: " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las traducciones desde el fichero YAML indicado
|
||||
void Locale::loadFromFile(const std::string& file_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (file_path.empty()) {
|
||||
std::cerr << "Locale: ruta de fichero vacía, sin traducciones cargadas\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Locale: no se puede abrir " << file_path << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(file);
|
||||
flatten(&yaml, "");
|
||||
|
||||
std::cout << "Locale: " << strings_.size() << " traducciones cargadas desde " << file_path << '\n';
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Locale: error al parsear YAML: " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
28
source/core/locale/locale.hpp
Normal file
28
source/core/locale/locale.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
// Clase Locale: gestiona las traducciones del juego (singleton)
|
||||
// Las traducciones se cargan desde un fichero YAML en el inicio.
|
||||
// No se permite cambio de idioma en caliente.
|
||||
class Locale {
|
||||
public:
|
||||
static void init(const std::string& file_path); // Crea e inicializa el singleton
|
||||
static void initFromContent(const std::string& content); // Crea e inicializa desde contenido en memoria (pack)
|
||||
static void destroy(); // Destruye el singleton
|
||||
static auto get() -> Locale*; // Devuelve el singleton
|
||||
|
||||
// Devuelve la traducción de la clave dada.
|
||||
// Si la clave no existe, devuelve la propia clave como fallback.
|
||||
[[nodiscard]] auto get(const std::string& key) const -> std::string;
|
||||
|
||||
private:
|
||||
Locale() = default;
|
||||
void loadFromFile(const std::string& file_path);
|
||||
void loadFromContent(const std::string& content);
|
||||
void flatten(const void* node_ptr, const std::string& prefix); // Aplana nodos YAML anidados
|
||||
|
||||
static Locale* instance;
|
||||
std::unordered_map<std::string, std::string> strings_;
|
||||
};
|
||||
295
source/core/rendering/gif.cpp
Normal file
295
source/core/rendering/gif.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#include "core/rendering/gif.hpp"
|
||||
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <iostream> // Para std::cout
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <string> // Para allocator, char_traits, operator==, basic_string
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Función inline para reemplazar el macro READ.
|
||||
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'.
|
||||
inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
|
||||
std::memcpy(dst, buffer, size);
|
||||
buffer += size;
|
||||
}
|
||||
|
||||
// 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)
|
||||
int size = 1 << code_length;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
|
||||
dictionary[dictionary_ind].prev = -1;
|
||||
dictionary[dictionary_ind].len = 1;
|
||||
}
|
||||
dictionary_ind += 2; // Reservamos espacio para clear y stop codes
|
||||
}
|
||||
|
||||
// Lee los próximos bits del stream de entrada para formar un código
|
||||
inline auto readNextCode(const uint8_t*& input, int& input_length, unsigned int& mask, int code_length) -> int {
|
||||
int code = 0;
|
||||
for (int i = 0; i < (code_length + 1); i++) {
|
||||
if (input_length <= 0) {
|
||||
throw std::runtime_error("Unexpected end of input in decompress");
|
||||
}
|
||||
int bit = ((*input & mask) != 0) ? 1 : 0;
|
||||
mask <<= 1;
|
||||
if (mask == 0x100) {
|
||||
mask = 0x01;
|
||||
input++;
|
||||
input_length--;
|
||||
}
|
||||
code |= (bit << i);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// Encuentra el primer byte de una cadena del diccionario
|
||||
inline auto findFirstByte(const std::vector<DictionaryEntry>& dictionary, int code) -> uint8_t {
|
||||
int ptr = code;
|
||||
while (dictionary[ptr].prev != -1) {
|
||||
ptr = dictionary[ptr].prev;
|
||||
}
|
||||
return dictionary[ptr].byte;
|
||||
}
|
||||
|
||||
// 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)
|
||||
uint8_t first_byte;
|
||||
if (code == dictionary_ind) {
|
||||
first_byte = findFirstByte(dictionary, prev);
|
||||
} else {
|
||||
first_byte = findFirstByte(dictionary, code);
|
||||
}
|
||||
|
||||
dictionary[dictionary_ind].byte = first_byte;
|
||||
dictionary[dictionary_ind].prev = prev;
|
||||
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
|
||||
dictionary_ind++;
|
||||
|
||||
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
|
||||
code_length++;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe la cadena decodificada al buffer de salida
|
||||
inline auto writeDecodedString(const std::vector<DictionaryEntry>& dictionary, int code, uint8_t*& out) -> int {
|
||||
int cur_code = code;
|
||||
int match_len = dictionary[cur_code].len;
|
||||
while (cur_code != -1) {
|
||||
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||
if (dictionary[cur_code].prev == cur_code) {
|
||||
std::cerr << "Internal error; self-reference detected." << '\n';
|
||||
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||
}
|
||||
cur_code = dictionary[cur_code].prev;
|
||||
}
|
||||
out += match_len;
|
||||
return match_len;
|
||||
}
|
||||
|
||||
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verifica que el code_length tenga un rango razonable.
|
||||
if (code_length < 2 || code_length > 12) {
|
||||
throw std::runtime_error("Invalid LZW code length");
|
||||
}
|
||||
|
||||
int prev = -1;
|
||||
std::vector<DictionaryEntry> dictionary;
|
||||
int dictionary_ind;
|
||||
unsigned int mask = 0x01;
|
||||
int reset_code_length = code_length;
|
||||
int clear_code = 1 << code_length;
|
||||
int stop_code = clear_code + 1;
|
||||
|
||||
// Inicializamos el diccionario con el tamaño correspondiente.
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
|
||||
// Bucle principal: procesar el stream comprimido.
|
||||
while (input_length > 0) {
|
||||
int code = readNextCode(input, input_length, mask, code_length);
|
||||
|
||||
if (code == clear_code) {
|
||||
// Reinicia el diccionario.
|
||||
code_length = reset_code_length;
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
prev = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == stop_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev > -1 && code_length < 12) {
|
||||
if (code > dictionary_ind) {
|
||||
std::cerr << "code = " << std::hex << code
|
||||
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
||||
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
|
||||
}
|
||||
|
||||
addDictionaryEntry(dictionary, dictionary_ind, code_length, prev, code);
|
||||
}
|
||||
|
||||
prev = code;
|
||||
|
||||
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||
std::cerr << "Invalid LZW code " << code
|
||||
<< ", dictionary size " << dictionary.size() << '\n';
|
||||
throw std::runtime_error("LZW error: invalid code encountered");
|
||||
}
|
||||
|
||||
writeDecodedString(dictionary, code, out);
|
||||
}
|
||||
}
|
||||
|
||||
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<uint8_t> data;
|
||||
uint8_t block_size = *buffer;
|
||||
buffer++;
|
||||
while (block_size != 0) {
|
||||
data.insert(data.end(), buffer, buffer + block_size);
|
||||
buffer += block_size;
|
||||
block_size = *buffer;
|
||||
buffer++;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
ImageDescriptor image_descriptor;
|
||||
// Lee 9 bytes para el image descriptor.
|
||||
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
|
||||
|
||||
uint8_t lzw_code_size;
|
||||
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
|
||||
|
||||
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
||||
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
|
||||
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
|
||||
|
||||
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
|
||||
return uncompressed_data;
|
||||
}
|
||||
|
||||
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
ScreenDescriptor screen_descriptor;
|
||||
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
|
||||
buffer += sizeof(ScreenDescriptor);
|
||||
|
||||
std::vector<uint32_t> global_color_table;
|
||||
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||
global_color_table.resize(global_color_table_size);
|
||||
for (int i = 0; i < global_color_table_size; ++i) {
|
||||
uint8_t r = buffer[0];
|
||||
uint8_t g = buffer[1];
|
||||
uint8_t b = buffer[2];
|
||||
global_color_table[i] = (r << 16) | (g << 8) | b;
|
||||
buffer += 3;
|
||||
}
|
||||
}
|
||||
return global_color_table;
|
||||
}
|
||||
|
||||
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
// Opcional: Validar header
|
||||
std::string header_str(reinterpret_cast<char*>(header), 6);
|
||||
if (header_str != "GIF87a" && header_str != "GIF89a") {
|
||||
throw std::runtime_error("Formato de archivo GIF inválido.");
|
||||
}
|
||||
|
||||
// Leer el Screen Descriptor (7 bytes, empaquetado sin padding)
|
||||
ScreenDescriptor screen_descriptor;
|
||||
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
|
||||
|
||||
// Asigna ancho y alto
|
||||
w = screen_descriptor.width;
|
||||
h = screen_descriptor.height;
|
||||
|
||||
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
|
||||
std::vector<RGB> global_color_table;
|
||||
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||
global_color_table.resize(global_color_table_size);
|
||||
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
|
||||
buffer += 3 * global_color_table_size;
|
||||
}
|
||||
|
||||
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
|
||||
uint8_t block_type = *buffer++;
|
||||
while (block_type != TRAILER) {
|
||||
if (block_type == EXTENSION_INTRODUCER) // 0x21
|
||||
{
|
||||
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión.
|
||||
uint8_t extension_label = *buffer++;
|
||||
switch (extension_label) {
|
||||
case GRAPHIC_CONTROL: // 0xF9
|
||||
{
|
||||
// Procesar Graphic Control Extension:
|
||||
uint8_t block_size = *buffer++; // Normalmente, blockSize == 4
|
||||
buffer += block_size; // Saltamos los 4 bytes del bloque fijo
|
||||
// Saltar los sub-bloques
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APPLICATION_EXTENSION: // 0xFF
|
||||
case COMMENT_EXTENSION: // 0xFE
|
||||
case PLAINTEXT_EXTENSION: // 0x01
|
||||
{
|
||||
// Para estas extensiones, saltamos el bloque fijo y los sub-bloques.
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Si la etiqueta de extensión es desconocida, saltarla también:
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||
// Procesar el Image Descriptor y retornar los datos de imagen
|
||||
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
|
||||
} else {
|
||||
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
block_type = *buffer++;
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
|
||||
auto Gif::loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
|
||||
return processGifStream(buffer, w, h);
|
||||
}
|
||||
|
||||
} // namespace GIF
|
||||
92
source/core/rendering/gif.hpp
Normal file
92
source/core/rendering/gif.hpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
|
||||
#include <vector> // Para vector
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Constantes definidas con constexpr, en lugar de macros
|
||||
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
|
||||
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
|
||||
constexpr uint8_t TRAILER = 0x3B;
|
||||
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
|
||||
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
|
||||
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
|
||||
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct ScreenDescriptor {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t fields;
|
||||
uint8_t background_color_index;
|
||||
uint8_t pixel_aspect_ratio;
|
||||
};
|
||||
|
||||
struct RGB {
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
struct ImageDescriptor {
|
||||
uint16_t image_left_position;
|
||||
uint16_t image_top_position;
|
||||
uint16_t image_width;
|
||||
uint16_t image_height;
|
||||
uint8_t fields;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct DictionaryEntry {
|
||||
uint8_t byte;
|
||||
int prev;
|
||||
int len;
|
||||
};
|
||||
|
||||
struct Extension {
|
||||
uint8_t extension_code;
|
||||
uint8_t block_size;
|
||||
};
|
||||
|
||||
struct GraphicControlExtension {
|
||||
uint8_t fields;
|
||||
uint16_t delay_time;
|
||||
uint8_t transparent_color_index;
|
||||
};
|
||||
|
||||
struct ApplicationExtension {
|
||||
uint8_t application_id[8];
|
||||
uint8_t version[3];
|
||||
};
|
||||
|
||||
struct PlaintextExtension {
|
||||
uint16_t left, top, width, height;
|
||||
uint8_t cell_width, cell_height;
|
||||
uint8_t foreground_color, background_color;
|
||||
};
|
||||
|
||||
class Gif {
|
||||
public:
|
||||
// Descompone (uncompress) el bloque comprimido usando LZW.
|
||||
// Este método puede lanzar std::runtime_error en caso de error.
|
||||
static void decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out);
|
||||
|
||||
// Carga la paleta (global color table) a partir de un buffer,
|
||||
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
|
||||
static auto loadPalette(const uint8_t* buffer) -> std::vector<uint32_t>;
|
||||
|
||||
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
|
||||
// asigna el ancho y alto mediante referencias.
|
||||
static auto loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
|
||||
private:
|
||||
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
|
||||
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
|
||||
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
|
||||
static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
};
|
||||
|
||||
} // namespace GIF
|
||||
276
source/core/rendering/palette_manager.cpp
Normal file
276
source/core/rendering/palette_manager.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
#include "core/rendering/palette_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#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 == "luminance") { return PaletteSortMode::LUMINANCE; }
|
||||
if (LOWER == "spectrum") { return PaletteSortMode::SPECTRUM; }
|
||||
return PaletteSortMode::ORIGINAL;
|
||||
}
|
||||
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return "luminance";
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return "spectrum";
|
||||
default:
|
||||
return "original";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paleta de referencia ZX Spectrum (16 colores ARGB) ───────────────────────
|
||||
|
||||
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); }
|
||||
|
||||
constexpr auto makeARGB(int r, int g, int b) -> Uint32 {
|
||||
return (0xFFU << 24) | (static_cast<Uint32>(r) << 16) | (static_cast<Uint32>(g) << 8) | static_cast<Uint32>(b);
|
||||
}
|
||||
|
||||
// Paleta ZX Spectrum de referencia (misma que en tools/sort_palette/sort_palette.py)
|
||||
constexpr std::array<Uint32, 16> SPECTRUM_REFERENCE = {
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 216),
|
||||
makeARGB(0, 0, 255),
|
||||
makeARGB(216, 0, 0),
|
||||
makeARGB(255, 0, 0),
|
||||
makeARGB(216, 0, 216),
|
||||
makeARGB(255, 0, 255),
|
||||
makeARGB(0, 216, 0),
|
||||
makeARGB(0, 255, 0),
|
||||
makeARGB(0, 216, 216),
|
||||
makeARGB(0, 255, 255),
|
||||
makeARGB(216, 216, 0),
|
||||
makeARGB(255, 255, 0),
|
||||
makeARGB(216, 216, 216),
|
||||
makeARGB(255, 255, 255),
|
||||
};
|
||||
|
||||
// Luminancia percibida (ITU-R BT.709)
|
||||
auto luminance(Uint32 color) -> double {
|
||||
return (0.2126 * redOf(color)) + (0.7152 * greenOf(color)) + (0.0722 * blueOf(color));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Ordenar por luminancia
|
||||
auto sortByLuminance(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> colors(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::ranges::sort(colors, [](Uint32 a, Uint32 b) {
|
||||
return luminance(a) < luminance(b);
|
||||
});
|
||||
|
||||
Palette result{};
|
||||
result.fill(0);
|
||||
std::ranges::copy(colors, result.begin());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ordenar por similitud con la paleta ZX Spectrum (greedy matching)
|
||||
auto sortBySpectrum(const Palette& palette) -> Palette {
|
||||
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 del Spectrum, buscar el más cercano disponible
|
||||
const size_t REFS = std::min(N, SPECTRUM_REFERENCE.size());
|
||||
for (size_t i = 0; i < REFS && !available.empty(); ++i) {
|
||||
const Uint32 REF = SPECTRUM_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 y aplicar paleta inicial directamente desde el archivo
|
||||
// (Resource::Cache aún no está disponible en este punto del ciclo de vida)
|
||||
const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_);
|
||||
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_);
|
||||
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) -> Palette {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return sortByLuminance(palette);
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return sortBySpectrum(palette);
|
||||
default:
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
64
source/core/rendering/palette_manager.hpp
Normal file
64
source/core/rendering/palette_manager.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Alias de paleta (igual que en surface.hpp; evita incluir todo el header)
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
|
||||
class Surface;
|
||||
|
||||
// Modo de ordenación de paletas
|
||||
enum class PaletteSortMode : int {
|
||||
ORIGINAL = 0, // Paleta tal cual viene del fichero
|
||||
LUMINANCE = 1, // Ordenada por luminancia percibida
|
||||
SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum
|
||||
COUNT = 3
|
||||
};
|
||||
|
||||
// Conversión string ↔ PaletteSortMode
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode;
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string;
|
||||
|
||||
class PaletteManager {
|
||||
public:
|
||||
using OnChangeCallback = std::function<void()>;
|
||||
|
||||
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 = nullptr);
|
||||
|
||||
void next(); // Avanza a la siguiente paleta
|
||||
void previous(); // Retrocede a la paleta anterior
|
||||
auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
|
||||
void nextSortMode(); // Cicla al siguiente modo de ordenación
|
||||
void setSortMode(PaletteSortMode mode); // Establece un modo de ordenación concreto
|
||||
[[nodiscard]] auto getSortMode() const -> PaletteSortMode; // Devuelve el modo de ordenación actual
|
||||
[[nodiscard]] auto getSortModeName() const -> std::string; // Nombre del modo actual ("ORIGINAL", etc.)
|
||||
|
||||
private:
|
||||
void apply(); // Aplica la paleta actual a ambas surfaces
|
||||
[[nodiscard]] auto findIndex(const std::string& name) const -> size_t; // Localiza paleta por nombre en el vector
|
||||
void processPathList(); // Extrae nombres de archivo de las rutas completas
|
||||
static auto sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette; // Reordena una paleta según el modo
|
||||
|
||||
std::vector<std::string> palettes_;
|
||||
size_t current_{0};
|
||||
PaletteSortMode sort_mode_{PaletteSortMode::ORIGINAL};
|
||||
std::shared_ptr<Surface> game_surface_;
|
||||
std::shared_ptr<Surface> border_surface_;
|
||||
OnChangeCallback on_change_;
|
||||
};
|
||||
107
source/core/rendering/pixel_reveal.cpp
Normal file
107
source/core/rendering/pixel_reveal.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "core/rendering/pixel_reveal.hpp"
|
||||
|
||||
#include <algorithm> // Para min, ranges::all_of
|
||||
#include <numeric> // Para iota
|
||||
#include <queue> // Para queue (BFS en modo ORDERED)
|
||||
#include <random> // Para mt19937, shuffle
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Constructor
|
||||
PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps, bool reverse, RevealMode mode)
|
||||
: cover_surface_(std::make_shared<Surface>(width, height)),
|
||||
reveal_order_(height),
|
||||
row_step_(height, 0),
|
||||
width_(width),
|
||||
height_(height),
|
||||
pixels_per_second_(pixels_per_second),
|
||||
step_duration_(step_duration),
|
||||
num_steps_(num_steps),
|
||||
reverse_(reverse),
|
||||
mode_(mode) {
|
||||
// En modo normal: empieza negro sólido (se irá revelando a transparente)
|
||||
// En modo inverso: empieza transparente (se irá cubriendo de negro)
|
||||
const auto INITIAL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::TRANSPARENT) : static_cast<Uint8>(PaletteColor::BLACK);
|
||||
cover_surface_->clear(INITIAL_COLOR);
|
||||
|
||||
if (mode_ == RevealMode::ORDERED) {
|
||||
// Calcula offsets por bisección BFS: 0, N/2, N/4, 3N/4, ...
|
||||
std::vector<int> offsets;
|
||||
offsets.push_back(0);
|
||||
std::queue<std::pair<int, int>> bq;
|
||||
bq.emplace(0, num_steps_);
|
||||
while (static_cast<int>(offsets.size()) < num_steps_) {
|
||||
auto [lo, hi] = bq.front();
|
||||
bq.pop();
|
||||
if (hi - lo <= 1) {
|
||||
continue;
|
||||
}
|
||||
const int MID = (lo + hi) / 2;
|
||||
offsets.push_back(MID);
|
||||
bq.emplace(lo, MID);
|
||||
bq.emplace(MID, hi);
|
||||
}
|
||||
// Genera el orden: para cada offset, todas las columnas col = offset, offset+N, offset+2N, ...
|
||||
std::vector<int> ordered_cols;
|
||||
ordered_cols.reserve(width_);
|
||||
for (const int OFF : offsets) {
|
||||
for (int col = OFF; col < width_; col += num_steps_) {
|
||||
ordered_cols.push_back(col);
|
||||
}
|
||||
}
|
||||
// Todas las filas usan el mismo orden (sin aleatoriedad)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r] = ordered_cols;
|
||||
}
|
||||
} else {
|
||||
// Modo RANDOM: orden aleatorio por fila usando la fila como semilla (reproducible)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r].resize(width_);
|
||||
std::iota(reveal_order_[r].begin(), reveal_order_[r].end(), 0);
|
||||
std::mt19937 rng(static_cast<unsigned int>(r));
|
||||
std::shuffle(reveal_order_[r].begin(), reveal_order_[r].end(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el estado del revelado
|
||||
void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const)
|
||||
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
|
||||
const auto PIXEL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::BLACK) : static_cast<Uint8>(PaletteColor::TRANSPARENT);
|
||||
|
||||
for (int r = 0; r < height_; r++) {
|
||||
const float T_START = static_cast<float>(r) / pixels_per_second_;
|
||||
const float TIME_IN_ROW = time_active - T_START;
|
||||
|
||||
if (TIME_IN_ROW < 0.0F) {
|
||||
continue; // Esta fila aún no ha empezado
|
||||
}
|
||||
|
||||
const int STEPS = std::min(num_steps_, static_cast<int>(TIME_IN_ROW / step_duration_));
|
||||
|
||||
if (STEPS > row_step_[r]) {
|
||||
// Procesa los píxeles de los pasos pendientes
|
||||
for (int step = row_step_[r]; step < STEPS; step++) {
|
||||
const int START_IDX = step * width_ / num_steps_;
|
||||
const int END_IDX = (step == num_steps_ - 1) ? width_ : (step + 1) * width_ / num_steps_;
|
||||
|
||||
for (int idx = START_IDX; idx < END_IDX; idx++) {
|
||||
const int COL = reveal_order_[r][idx];
|
||||
cover_surface_->putPixel(COL, r, PIXEL_COLOR);
|
||||
}
|
||||
}
|
||||
row_step_[r] = STEPS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja la máscara en la posición indicada
|
||||
void PixelReveal::render(int dst_x, int dst_y) const {
|
||||
cover_surface_->render(dst_x, dst_y);
|
||||
}
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
auto PixelReveal::isComplete() const -> bool {
|
||||
return std::ranges::all_of(row_step_, [this](int s) -> bool { return s >= num_steps_; });
|
||||
}
|
||||
41
source/core/rendering/pixel_reveal.hpp
Normal file
41
source/core/rendering/pixel_reveal.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <vector> // Para vector
|
||||
|
||||
class Surface;
|
||||
|
||||
// Efecto de revelado pixel a pixel por filas, de arriba a abajo.
|
||||
// Cada fila se revela en num_steps pasos, con píxeles en orden aleatorio u ordenado (bisección).
|
||||
class PixelReveal {
|
||||
public:
|
||||
// Modo de revelado: aleatorio por fila o en orden de bisección (dithering ordenado 1D)
|
||||
enum class RevealMode { RANDOM,
|
||||
ORDERED };
|
||||
|
||||
// Constructor
|
||||
PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps = 4, bool reverse = false, RevealMode mode = RevealMode::RANDOM);
|
||||
|
||||
~PixelReveal() = default;
|
||||
|
||||
// Actualiza el estado del revelado según el tiempo transcurrido
|
||||
void update(float time_active);
|
||||
|
||||
// Dibuja la máscara de revelado en la posición indicada
|
||||
void render(int dst_x, int dst_y) const;
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
[[nodiscard]] auto isComplete() const -> bool;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Surface> cover_surface_; // Máscara negra que se va haciendo transparente
|
||||
std::vector<std::vector<int>> reveal_order_; // Orden de columnas por fila (aleatorio u ordenado por bisección)
|
||||
std::vector<int> row_step_; // Paso actual de revelado por fila (0..num_steps_)
|
||||
int width_;
|
||||
int height_;
|
||||
float pixels_per_second_; // Filas reveladas por segundo
|
||||
float step_duration_; // Segundos por paso dentro de una fila
|
||||
int num_steps_; // Número de pasos de revelado por fila
|
||||
bool reverse_; // Si true: transparente → negro (ocultar); si false: negro → transparente (revelar)
|
||||
RevealMode mode_; // Modo de revelado: aleatorio u ordenado por bisección
|
||||
};
|
||||
159
source/core/rendering/render_info.cpp
Normal file
159
source/core/rendering/render_info.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "core/rendering/render_info.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para transform
|
||||
#include <cmath> // Para round, floor
|
||||
#include <iomanip> // Para setprecision
|
||||
#include <sstream> // Para ostringstream
|
||||
#include <string> // Para string
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "game/options.hpp" // Para Options
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/utils.hpp" // Para prettyName
|
||||
|
||||
// [SINGLETON]
|
||||
RenderInfo* RenderInfo::render_info = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void RenderInfo::init() {
|
||||
RenderInfo::render_info = new RenderInfo();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void RenderInfo::destroy() {
|
||||
delete RenderInfo::render_info;
|
||||
RenderInfo::render_info = nullptr;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto RenderInfo::get() -> RenderInfo* {
|
||||
return RenderInfo::render_info;
|
||||
}
|
||||
|
||||
// Constructor: en DEBUG se activa inmediatamente (notifica a Notifier del offset)
|
||||
RenderInfo::RenderInfo() {
|
||||
#ifdef _DEBUG
|
||||
toggle();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Actualiza la animación de entrada/salida del overlay
|
||||
void RenderInfo::update(float delta_time) {
|
||||
switch (status_) {
|
||||
case Status::RISING:
|
||||
y_ += SLIDE_SPEED * delta_time;
|
||||
if (y_ >= 0.0F) {
|
||||
y_ = 0.0F;
|
||||
status_ = Status::ACTIVE;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
y_ -= SLIDE_SPEED * delta_time;
|
||||
if (y_ <= static_cast<float>(-HEIGHT)) {
|
||||
y_ = static_cast<float>(-HEIGHT);
|
||||
status_ = Status::HIDDEN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza el overlay de información por pantalla
|
||||
void RenderInfo::render() const {
|
||||
if (status_ == Status::HIDDEN) { return; }
|
||||
|
||||
// FPS
|
||||
std::string line = std::to_string(Screen::get()->getLastFPS()) + " fps";
|
||||
|
||||
// Driver GPU
|
||||
const auto& driver = Screen::get()->getGPUDriver();
|
||||
line += " | " + (driver.empty() ? std::string("sdl") : driver);
|
||||
|
||||
// Zoom calculado (alto físico / alto lógico), con coma decimal y sin ceros innecesarios
|
||||
const float ROUNDED = std::round(Screen::get()->getZoomFactor() * 100.0F) / 100.0F;
|
||||
std::string zoom_str;
|
||||
if (ROUNDED == std::floor(ROUNDED)) {
|
||||
zoom_str = std::to_string(static_cast<int>(ROUNDED));
|
||||
} else {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(2) << ROUNDED;
|
||||
zoom_str = oss.str();
|
||||
if (zoom_str.back() == '0') { zoom_str.pop_back(); }
|
||||
std::ranges::replace(zoom_str, '.', ',');
|
||||
}
|
||||
line += " | " + zoom_str + "x";
|
||||
|
||||
// PostFX: muestra shader + preset y supersampling, o nada si está desactivado
|
||||
if (Options::video.shader.enabled) {
|
||||
const bool IS_CRTPI = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI);
|
||||
const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx";
|
||||
std::string preset_name = "-";
|
||||
if (IS_CRTPI) {
|
||||
if (!Options::crtpi_presets.empty()) {
|
||||
preset_name = prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name);
|
||||
}
|
||||
} else {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
preset_name = prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name);
|
||||
}
|
||||
}
|
||||
const bool SHOW_SS = Options::video.supersampling.enabled && !IS_CRTPI;
|
||||
line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : "");
|
||||
}
|
||||
|
||||
// Todo en lowercase
|
||||
std::ranges::transform(line, line.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Constantes visuales (igual que Console)
|
||||
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
|
||||
static constexpr Uint8 MSG_COLOR = 9; // PaletteColor::BRIGHT_GREEN
|
||||
static constexpr int TEXT_SIZE = 6;
|
||||
static constexpr int PADDING_V = (TEXT_SIZE / 2) - 1;
|
||||
|
||||
// Fuente: preferir la de la consola si está disponible
|
||||
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
|
||||
|
||||
// Posición Y: debajo de la consola + offset animado propio
|
||||
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
|
||||
const int Y = CONSOLE_Y + static_cast<int>(y_);
|
||||
|
||||
// Rectángulo de fondo: ancho completo, alto ajustado al texto
|
||||
const SDL_FRect RECT = {
|
||||
.x = 0.0F,
|
||||
.y = static_cast<float>(Y),
|
||||
.w = Options::game.width,
|
||||
.h = static_cast<float>(TEXT_SIZE + (PADDING_V * 2))};
|
||||
|
||||
auto game_surface = Screen::get()->getGameSurface();
|
||||
game_surface->fillRect(&RECT, BG_COLOR);
|
||||
// game_surface->drawRectBorder(&RECT, BORDER_COLOR);
|
||||
text_obj->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
static_cast<int>(Options::game.width / 2),
|
||||
Y + PADDING_V,
|
||||
line,
|
||||
1,
|
||||
MSG_COLOR);
|
||||
}
|
||||
|
||||
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset
|
||||
void RenderInfo::toggle() {
|
||||
switch (status_) {
|
||||
case Status::HIDDEN:
|
||||
status_ = Status::RISING;
|
||||
Screen::get()->updateZoomFactor();
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
|
||||
break;
|
||||
case Status::ACTIVE:
|
||||
status_ = Status::VANISHING;
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
37
source/core/rendering/render_info.hpp
Normal file
37
source/core/rendering/render_info.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
class RenderInfo {
|
||||
public:
|
||||
// Singleton
|
||||
static void init();
|
||||
static void destroy();
|
||||
static auto get() -> RenderInfo*;
|
||||
|
||||
// Métodos principales
|
||||
void update(float delta_time);
|
||||
void render() const;
|
||||
void toggle();
|
||||
|
||||
// Consultas
|
||||
[[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; }
|
||||
|
||||
// Altura fija del overlay (TEXT_SIZE(6) + PADDING_V(2) * 2)
|
||||
static constexpr int HEIGHT = 10;
|
||||
static constexpr float SLIDE_SPEED = 120.0F;
|
||||
|
||||
private:
|
||||
enum class Status { HIDDEN,
|
||||
RISING,
|
||||
ACTIVE,
|
||||
VANISHING };
|
||||
|
||||
// Singleton
|
||||
static RenderInfo* render_info;
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
RenderInfo();
|
||||
~RenderInfo() = default;
|
||||
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{static_cast<float>(-HEIGHT)};
|
||||
};
|
||||
750
source/core/rendering/screen.cpp
Normal file
750
source/core/rendering/screen.cpp
Normal file
@@ -0,0 +1,750 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para max, min, transform
|
||||
#include <cctype> // Para toupper
|
||||
#include <cmath> // Para round, floor
|
||||
#include <cstring> // Para memcpy
|
||||
#include <fstream> // Para basic_ostream, operator<<, endl, basic_...
|
||||
#include <iostream> // Para cerr
|
||||
#include <iterator> // Para istreambuf_iterator, operator==
|
||||
#include <string> // Para char_traits, string, operator+, operator==
|
||||
|
||||
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
|
||||
// [SINGLETON]
|
||||
Screen* Screen::screen = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Screen::init() {
|
||||
Screen::screen = new Screen();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Screen::destroy() {
|
||||
delete Screen::screen;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Screen::get() -> Screen* {
|
||||
return Screen::screen;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Screen::Screen() {
|
||||
// Arranca SDL VIDEO, crea la ventana y el renderizador
|
||||
initSDLVideo();
|
||||
if (Options::video.fullscreen) { SDL_HideCursor(); }
|
||||
|
||||
// Calcular tamaños y hacer .resize() de los buffers de píxeles
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
|
||||
// Ajusta los tamaños
|
||||
game_surface_dstrect_ = {.x = Options::video.border.width, .y = Options::video.border.height, .w = Options::game.width, .h = Options::game.height};
|
||||
|
||||
// Define el color del borde para el modo de pantalla completa
|
||||
border_color_ = static_cast<Uint8>(PaletteColor::BLACK);
|
||||
|
||||
// Crea la textura donde se dibujan los graficos del juego
|
||||
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width, Options::game.height);
|
||||
if (game_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: game_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(game_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea la textura donde se dibuja el borde que rodea el area de juego
|
||||
border_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
if (border_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: border_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(border_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea las surfaces (PaletteManager aplicará la paleta inicial en su constructor)
|
||||
game_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
|
||||
game_surface_->clear(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
|
||||
border_surface_ = std::make_shared<Surface>(Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Crea el gestor de paletas; aplica la paleta inicial a ambas surfaces
|
||||
palette_manager_ = std::make_unique<PaletteManager>(
|
||||
Resource::List::get()->getListByType(Resource::List::Type::PALETTE),
|
||||
Options::video.palette,
|
||||
sortModeFromString(Options::video.palette_sort),
|
||||
game_surface_,
|
||||
border_surface_,
|
||||
[this]() {
|
||||
// Actualizar caché ARGB del borde cuando cambia la paleta
|
||||
if (border_is_solid_) {
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Cachear el color ARGB inicial del borde (borde sólido por defecto)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
|
||||
// Establece la surface que actuará como renderer para recibir las llamadas a render()
|
||||
renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
|
||||
// Crea el objeto de texto para la pantalla de carga
|
||||
createText();
|
||||
|
||||
// Renderizar una vez la textura vacía para que tenga contenido válido
|
||||
// antes de inicializar los shaders (evita pantalla negra)
|
||||
SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr);
|
||||
SDL_RenderTexture(renderer_, border_texture_, nullptr, nullptr);
|
||||
|
||||
// Ahora sí inicializar los shaders
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Screen::~Screen() {
|
||||
SDL_DestroyTexture(game_texture_);
|
||||
SDL_DestroyTexture(border_texture_);
|
||||
}
|
||||
|
||||
// Limpia el renderer
|
||||
void Screen::clearRenderer(Rgb color) {
|
||||
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
|
||||
SDL_RenderClear(renderer_);
|
||||
}
|
||||
|
||||
// Prepara para empezar a dibujar en la textura de juego
|
||||
void Screen::start() { setRendererSurface(nullptr); }
|
||||
|
||||
// Vuelca el contenido del renderizador en pantalla
|
||||
void Screen::render() {
|
||||
fps_.increment();
|
||||
|
||||
// Renderiza todos los overlays (escribe en game_surface_ CPU-side)
|
||||
renderOverlays();
|
||||
|
||||
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
|
||||
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
|
||||
if (!(shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||
surfaceToTexture();
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
textureToRenderer();
|
||||
}
|
||||
|
||||
// Establece el modo de video
|
||||
void Screen::setVideoMode(bool mode) {
|
||||
// Actualiza las opciones
|
||||
Options::video.fullscreen = mode;
|
||||
|
||||
// Configura el modo de pantalla y ajusta la ventana
|
||||
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||
SDL_SyncWindow(window_);
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Camibia entre pantalla completa y ventana
|
||||
void Screen::toggleVideoMode() {
|
||||
Options::video.fullscreen = !Options::video.fullscreen;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
}
|
||||
|
||||
// Reduce el tamaño de la ventana
|
||||
auto Screen::decWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||
--Options::window.zoom;
|
||||
Options::window.zoom = std::max(Options::window.zoom, 1);
|
||||
|
||||
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aumenta el tamaño de la ventana
|
||||
auto Screen::incWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||
++Options::window.zoom;
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Establece el zoom directamente; false si fuera del rango [1, max_zoom] o en pantalla completa
|
||||
auto Screen::setWindowZoom(int zoom) -> bool {
|
||||
if (Options::video.fullscreen) { return false; }
|
||||
if (zoom < 1 || zoom > Options::window.max_zoom) { return false; }
|
||||
if (zoom == Options::window.zoom) { return false; }
|
||||
Options::window.zoom = zoom;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Devuelve el zoom máximo permitido según la pantalla actual
|
||||
auto Screen::getMaxZoom() -> int {
|
||||
return Options::window.max_zoom;
|
||||
}
|
||||
|
||||
// Cambia el color del borde
|
||||
void Screen::setBorderColor(Uint8 color) {
|
||||
border_color_ = color;
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Actualizar caché ARGB del borde sólido (ocurre una vez por habitación, no cada frame)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
border_is_solid_ = true;
|
||||
}
|
||||
|
||||
// Cambia entre borde visible y no visible
|
||||
void Screen::toggleBorder() {
|
||||
Options::video.border.enabled = !Options::video.border.enabled;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Dibuja las notificaciones
|
||||
void Screen::renderNotifications() const {
|
||||
if (notifications_enabled_) {
|
||||
Notifier::get()->render();
|
||||
}
|
||||
}
|
||||
|
||||
// Activa/desactiva todos los shaders respetando el shader actualmente seleccionado
|
||||
void Screen::toggleShaders() {
|
||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
if (Options::video.shader.enabled) {
|
||||
// Activar: usar el shader actualmente seleccionado
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
} else {
|
||||
// Desactivar: pass-through con POSTFX (pipeline sin efecto)
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
} else {
|
||||
// Backend no inicializado aún — inicializarlo ahora
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader del preset actual sin toggle
|
||||
void Screen::reloadPostFX() {
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
// El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline
|
||||
applyCurrentPostFXPreset();
|
||||
} else if (Options::video.shader.enabled) {
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader CrtPi del preset actual sin toggle
|
||||
void Screen::reloadCrtPi() {
|
||||
if (!shader_backend_) { return; }
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
|
||||
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
|
||||
void Screen::update(float delta_time) {
|
||||
fps_.calculate(SDL_GetTicks());
|
||||
Notifier::get()->update(delta_time);
|
||||
if (Console::get() != nullptr) {
|
||||
Console::get()->update(delta_time);
|
||||
}
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->update(delta_time); }
|
||||
Mouse::updateCursorVisibility();
|
||||
}
|
||||
|
||||
// Calcula el tamaño de la ventana
|
||||
void Screen::adjustWindowSize() {
|
||||
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
|
||||
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
|
||||
|
||||
// Reservamos memoria una sola vez.
|
||||
// Si el buffer es más pequeño que la superficie, crash asegurado.
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
|
||||
|
||||
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
|
||||
if (Options::video.border.enabled) {
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
}
|
||||
|
||||
// Lógica de centrado y redimensionado de ventana SDL
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
int old_w;
|
||||
int old_h;
|
||||
SDL_GetWindowSize(window_, &old_w, &old_h);
|
||||
int old_x;
|
||||
int old_y;
|
||||
SDL_GetWindowPosition(window_, &old_x, &old_y);
|
||||
|
||||
const int NEW_W = window_width_ * Options::window.zoom;
|
||||
const int NEW_H = window_height_ * Options::window.zoom;
|
||||
const int NEW_X = old_x + ((old_w - NEW_W) / 2);
|
||||
const int NEW_Y = old_y + ((old_h - NEW_H) / 2);
|
||||
|
||||
SDL_SetWindowSize(window_, NEW_W, NEW_H);
|
||||
|
||||
// En Wayland, SDL_SetWindowPosition es ignorado por el compositor (limitación de
|
||||
// protocolo: el compositor controla la posición de ventanas toplevel). Solo se
|
||||
// aplica en X11/Windows/macOS donde el posicionado funciona correctamente.
|
||||
// SDL_SyncWindow garantiza que el resize esté completado antes de reposicionar
|
||||
// (evita el race condition en X11).
|
||||
SDL_SyncWindow(window_);
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
const bool IS_WAYLAND = (driver != nullptr && SDL_strcmp(driver, "wayland") == 0);
|
||||
if (!IS_WAYLAND) {
|
||||
SDL_SetWindowPosition(window_, std::max(NEW_X, WINDOWS_DECORATIONS), std::max(NEW_Y, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ajusta el tamaño lógico del renderizador
|
||||
void Screen::adjustRenderLogicalSize() {
|
||||
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
}
|
||||
|
||||
// Recalcula y almacena el factor de zoom. Llamar solo cuando SDL ya ha estabilizado el estado de la ventana.
|
||||
// En ventana: Options::window.zoom (siempre entero).
|
||||
// En fullscreen: mínimo de las escalas en ambos ejes; floor si integer scale está activo.
|
||||
void Screen::updateZoomFactor() {
|
||||
if (!Options::video.fullscreen) {
|
||||
zoom_factor_ = static_cast<float>(Options::window.zoom);
|
||||
return;
|
||||
}
|
||||
if (window_width_ == 0 || window_height_ == 0) {
|
||||
zoom_factor_ = 1.0F;
|
||||
return;
|
||||
}
|
||||
int pw{0};
|
||||
int ph{0};
|
||||
SDL_GetRenderOutputSize(renderer_, &pw, &ph);
|
||||
const float SCALE = std::min(static_cast<float>(pw) / static_cast<float>(window_width_),
|
||||
static_cast<float>(ph) / static_cast<float>(window_height_));
|
||||
zoom_factor_ = Options::video.integer_scale ? std::floor(SCALE) : SCALE;
|
||||
}
|
||||
|
||||
// Establece el renderizador para las surfaces
|
||||
void Screen::setRendererSurface(const std::shared_ptr<Surface>& surface) {
|
||||
(surface) ? renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(surface) : renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
}
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::nextPalette() { palette_manager_->next(); }
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::previousPalette() { palette_manager_->previous(); }
|
||||
|
||||
// Copia la surface a la textura
|
||||
void Screen::surfaceToTexture() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (Options::video.border.enabled) {
|
||||
border_surface_->copyToTexture(renderer_, border_texture_);
|
||||
game_surface_->copyToTexture(renderer_, border_texture_, nullptr, &game_surface_dstrect_);
|
||||
} else {
|
||||
game_surface_->copyToTexture(renderer_, game_texture_);
|
||||
}
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
void Screen::textureToRenderer() {
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
const int GAME_W = Options::game.width;
|
||||
const int GAME_H = Options::game.height;
|
||||
|
||||
if (Options::video.border.enabled) {
|
||||
const int BORDER_W = window_width_;
|
||||
const int BORDER_H = window_height_;
|
||||
const int OFF_X = static_cast<int>(game_surface_dstrect_.x);
|
||||
const int OFF_Y = static_cast<int>(game_surface_dstrect_.y);
|
||||
|
||||
if (border_is_solid_) {
|
||||
// Path A: borde sólido (gameplay normal)
|
||||
// Rellena solo el marco con el color cacheado — sin lookups de paleta.
|
||||
// El área central (juego) se deja sin tocar; el overlay la sobreescribe igualmente.
|
||||
|
||||
// Franjas superior e inferior (ancho completo)
|
||||
std::fill_n(border_pixel_buffer_.data(), OFF_Y * BORDER_W, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(OFF_Y + GAME_H) * BORDER_W],
|
||||
(BORDER_H - OFF_Y - GAME_H) * BORDER_W,
|
||||
border_argb_color_);
|
||||
// Columnas laterales en las filas del área de juego
|
||||
for (int y = OFF_Y; y < OFF_Y + GAME_H; ++y) {
|
||||
std::fill_n(&border_pixel_buffer_[y * BORDER_W], OFF_X, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(y * BORDER_W) + OFF_X + GAME_W],
|
||||
BORDER_W - OFF_X - GAME_W,
|
||||
border_argb_color_);
|
||||
}
|
||||
} else {
|
||||
// Path B: borde dinámico (escena de carga — bandas de colores animadas)
|
||||
// Conversión completa: la escena modifica border_surface_ cada frame
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
}
|
||||
|
||||
// Overlay del juego sobre el centro del buffer (ambos paths)
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
for (int y = 0; y < GAME_H; ++y) {
|
||||
const Uint32* src = &game_pixel_buffer_[y * GAME_W];
|
||||
Uint32* dst = &border_pixel_buffer_[((OFF_Y + y) * BORDER_W) + OFF_X];
|
||||
std::memcpy(dst, src, GAME_W * sizeof(Uint32));
|
||||
}
|
||||
|
||||
shader_backend_->uploadPixels(border_pixel_buffer_.data(), BORDER_W, BORDER_H);
|
||||
} else {
|
||||
// Caso sin borde: subida directa simplificada
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
shader_backend_->uploadPixels(game_pixel_buffer_.data(), GAME_W, GAME_H);
|
||||
}
|
||||
|
||||
shader_backend_->render();
|
||||
} else {
|
||||
// Fallback SDL_Renderer (mantiene tu lógica de texturas SDL)
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
SDL_SetRenderTarget(renderer_, nullptr);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, tex, nullptr, nullptr);
|
||||
SDL_RenderPresent(renderer_);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza todos los overlays (orden: último dibujado queda encima)
|
||||
void Screen::renderOverlays() {
|
||||
renderNotifications(); // Notifier (abajo)
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->render(); } // RenderInfo (medio)
|
||||
if (Console::get() != nullptr) { Console::get()->render(); } // Console (encima)
|
||||
}
|
||||
|
||||
// Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe
|
||||
auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); }
|
||||
|
||||
// Devuelve los nombres de paletas disponibles (minúsculas, sin extensión .pal)
|
||||
auto Screen::getPaletteNames() const -> std::vector<std::string> { return palette_manager_->getNames(); }
|
||||
auto Screen::getPalettePrettyName() const -> std::string { return palette_manager_->getPrettyName(); }
|
||||
void Screen::nextPaletteSortMode() { palette_manager_->nextSortMode(); }
|
||||
void Screen::setPaletteSortMode(PaletteSortMode mode) { palette_manager_->setSortMode(mode); }
|
||||
auto Screen::getPaletteSortModeName() const -> std::string { return palette_manager_->getSortModeName(); }
|
||||
|
||||
// Limpia la game_surface_
|
||||
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderWidth(int width) { Options::video.border.width = width; }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderHeight(int height) { Options::video.border.height = height; }
|
||||
|
||||
// Establece si se ha de ver el borde en el modo ventana
|
||||
void Screen::setBorderEnabled(bool value) { Options::video.border.enabled = value; }
|
||||
|
||||
// Muestra la ventana
|
||||
void Screen::show() { SDL_ShowWindow(window_); }
|
||||
|
||||
// Oculta la ventana
|
||||
void Screen::hide() { SDL_HideWindow(window_); }
|
||||
|
||||
// Establece la visibilidad de las notificaciones
|
||||
void Screen::setNotificationsEnabled(bool value) { notifications_enabled_ = value; }
|
||||
|
||||
// Alterna entre activar y desactivar el escalado entero
|
||||
void Screen::toggleIntegerScale() {
|
||||
Options::video.integer_scale = !Options::video.integer_scale;
|
||||
SDL_SetRenderLogicalPresentation(renderer_, Options::game.width, Options::game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
}
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Alterna entre activar y desactivar el V-Sync
|
||||
void Screen::toggleVSync() {
|
||||
Options::video.vertical_sync = !Options::video.vertical_sync;
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
auto Screen::getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||
auto Screen::getRendererSurface() -> std::shared_ptr<Surface> { return (*renderer_surface_); }
|
||||
auto Screen::getGameSurface() -> std::shared_ptr<Surface> { return game_surface_; }
|
||||
auto Screen::getBorderSurface() -> std::shared_ptr<Surface> {
|
||||
border_is_solid_ = false; // Modificación externa → modo borde dinámico
|
||||
return border_surface_;
|
||||
}
|
||||
|
||||
auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
// Load using ResourceHelper (supports both filesystem and pack)
|
||||
return Resource::Helper::loadFile(filepath);
|
||||
}
|
||||
|
||||
void Screen::setLinearUpscale(bool linear) {
|
||||
Options::video.supersampling.linear_upscale = linear;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setLinearUpscale(linear);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::setDownscaleAlgo(int algo) {
|
||||
Options::video.supersampling.downscale_algo = algo;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setDownscaleAlgo(algo);
|
||||
}
|
||||
}
|
||||
|
||||
auto Screen::getSsTextureSize() const -> std::pair<int, int> {
|
||||
if (!shader_backend_) { return {0, 0}; }
|
||||
return shader_backend_->getSsTextureSize();
|
||||
}
|
||||
|
||||
// Activa/desactiva el supersampling global (Ctrl+F4)
|
||||
void Screen::toggleSupersampling() {
|
||||
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset actual al backend de shaders
|
||||
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::postfx_presets.empty()) {
|
||||
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)];
|
||||
// Supersampling es un toggle global (Options::video.supersampling.enabled), no por preset.
|
||||
// setOversample primero: puede recrear texturas antes de que setPostFXParams
|
||||
// decida si hornear scanlines en CPU o aplicarlas en GPU.
|
||||
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
|
||||
shader_backend_->setPostFXParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset CrtPi actual al backend de shaders
|
||||
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::crtpi_presets.empty()) {
|
||||
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
|
||||
Rendering::CrtPiParams params{
|
||||
.scanline_weight = p.scanline_weight,
|
||||
.scanline_gap_brightness = p.scanline_gap_brightness,
|
||||
.bloom_factor = p.bloom_factor,
|
||||
.input_gamma = p.input_gamma,
|
||||
.output_gamma = p.output_gamma,
|
||||
.mask_brightness = p.mask_brightness,
|
||||
.curvature_x = p.curvature_x,
|
||||
.curvature_y = p.curvature_y,
|
||||
.mask_type = p.mask_type,
|
||||
.enable_scanlines = p.enable_scanlines,
|
||||
.enable_multisample = p.enable_multisample,
|
||||
.enable_gamma = p.enable_gamma,
|
||||
.enable_curvature = p.enable_curvature,
|
||||
.enable_sharper = p.enable_sharper,
|
||||
};
|
||||
shader_backend_->setCrtPiParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Cambia el shader de post-procesado activo y aplica el preset correspondiente
|
||||
void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||
Options::video.shader.current_shader = type;
|
||||
if (!shader_backend_) { return; }
|
||||
if (!Options::video.shader.enabled) {
|
||||
// Shaders desactivados: guardar preferencia pero mantener pass-through
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
return;
|
||||
}
|
||||
shader_backend_->setActiveShader(type);
|
||||
if (type == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Cicla al siguiente shader disponible (preparado para futura UI)
|
||||
void Screen::nextShader() {
|
||||
const Rendering::ShaderType NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
|
||||
? Rendering::ShaderType::CRTPI
|
||||
: Rendering::ShaderType::POSTFX;
|
||||
setActiveShader(NEXT);
|
||||
}
|
||||
|
||||
// Inicializa los shaders
|
||||
// El device GPU se crea siempre (independientemente de postfx) para evitar
|
||||
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
|
||||
void Screen::initShaders() {
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
|
||||
if (!shader_backend_) {
|
||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
const std::string FALLBACK_DRIVER = "none";
|
||||
shader_backend_->setPreferredDriver(Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
|
||||
}
|
||||
shader_backend_->init(window_, tex, "", "");
|
||||
gpu_driver_ = shader_backend_->getDriverName();
|
||||
|
||||
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
|
||||
|
||||
if (Options::video.shader.enabled) {
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
// Pass-through: todos los efectos a 0, el shader solo copia la textura
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
|
||||
// Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica)
|
||||
shader_backend_->setActiveShader(Options::video.shader.current_shader);
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene información sobre la pantalla
|
||||
void Screen::getDisplayInfo() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n** VIDEO SYSTEM **\n";
|
||||
|
||||
int num_displays = 0;
|
||||
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
||||
if (displays != nullptr) {
|
||||
for (int i = 0; i < num_displays; ++i) {
|
||||
SDL_DisplayID instance_id = displays[i];
|
||||
const char* name = SDL_GetDisplayName(instance_id);
|
||||
|
||||
std::cout << "Display " << instance_id << ": " << ((name != nullptr) ? name : "Unknown") << '\n';
|
||||
}
|
||||
|
||||
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
||||
|
||||
// Guarda información del monitor en display_monitor_
|
||||
const char* first_display_name = SDL_GetDisplayName(displays[0]);
|
||||
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
|
||||
display_monitor_.width = static_cast<int>(dm->w);
|
||||
display_monitor_.height = static_cast<int>(dm->h);
|
||||
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
|
||||
|
||||
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||
Options::window.max_zoom = std::min(dm->w / Options::game.width, dm->h / Options::game.height);
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
|
||||
std::cout << "Current display mode: " << static_cast<int>(dm->w) << "x" << static_cast<int>(dm->h) << " @ " << static_cast<int>(dm->refresh_rate) << "Hz\n";
|
||||
|
||||
std::cout << "Window resolution: " << static_cast<int>(Options::game.width) << "x" << static_cast<int>(Options::game.height) << " x" << Options::window.zoom << '\n';
|
||||
|
||||
Options::video.info = std::to_string(static_cast<int>(dm->w)) + "x" +
|
||||
std::to_string(static_cast<int>(dm->h)) + " @ " +
|
||||
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
|
||||
|
||||
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||
const int MAX_ZOOM = std::min(dm->w / Options::game.width, (dm->h - WINDOWS_DECORATIONS) / Options::game.height);
|
||||
|
||||
// Normaliza los valores de zoom
|
||||
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
|
||||
|
||||
SDL_free(displays);
|
||||
}
|
||||
}
|
||||
|
||||
// Arranca SDL VIDEO y crea la ventana
|
||||
auto Screen::initSDLVideo() -> bool {
|
||||
// Inicializar SDL
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::cerr << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Obtener información de la pantalla
|
||||
getDisplayInfo();
|
||||
|
||||
// Configurar hint para renderizado
|
||||
#ifdef __APPLE__
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
|
||||
std::cout << "WARNING: Failed to set Metal hint!\n";
|
||||
}
|
||||
#endif
|
||||
|
||||
// Crear ventana
|
||||
const auto WINDOW_WIDTH = Options::video.border.enabled ? Options::game.width + (Options::video.border.width * 2) : Options::game.width;
|
||||
const auto WINDOW_HEIGHT = Options::video.border.enabled ? Options::game.height + (Options::video.border.height * 2) : Options::game.height;
|
||||
SDL_WindowFlags window_flags = 0;
|
||||
if (Options::video.fullscreen) {
|
||||
window_flags |= SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
window_ = SDL_CreateWindow(Options::window.caption.c_str(), WINDOW_WIDTH * Options::window.zoom, WINDOW_HEIGHT * Options::window.zoom, window_flags);
|
||||
|
||||
if (window_ == nullptr) {
|
||||
std::cerr << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Crear renderer
|
||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||
if (renderer_ == nullptr) {
|
||||
std::cerr << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configurar renderer
|
||||
const int EXTRA_WIDTH = Options::video.border.enabled ? Options::video.border.width * 2 : 0;
|
||||
const int EXTRA_HEIGHT = Options::video.border.enabled ? Options::video.border.height * 2 : 0;
|
||||
SDL_SetRenderLogicalPresentation(
|
||||
renderer_,
|
||||
Options::game.width + EXTRA_WIDTH,
|
||||
Options::game.height + EXTRA_HEIGHT,
|
||||
Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
|
||||
std::cout << "Video system initialized successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Crea el objeto de texto
|
||||
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Carga la surface de la fuente directamente del archivo
|
||||
auto surface = std::make_shared<Surface>(Resource::List::get()->get("aseprite.gif"));
|
||||
|
||||
// Crea el objeto de texto (el constructor de Text carga el archivo text_file internamente)
|
||||
text_ = std::make_shared<Text>(surface, Resource::List::get()->get("aseprite.fnt"));
|
||||
}
|
||||
184
source/core/rendering/screen.hpp
Normal file
184
source/core/rendering/screen.hpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_pixels.h> // Para Uint32
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para std::pair
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/palette_manager.hpp" // Para PaletteManager
|
||||
#include "core/rendering/shader_backend.hpp" // Para Rendering::ShaderType, ShaderBackend
|
||||
#include "utils/utils.hpp" // Para Color
|
||||
class Surface;
|
||||
class Text;
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
// Tipos de filtro
|
||||
enum class Filter : Uint32 {
|
||||
NEAREST = 0,
|
||||
LINEAR = 1,
|
||||
};
|
||||
|
||||
// Singleton
|
||||
static void init(); // Crea el singleton
|
||||
static void destroy(); // Destruye el singleton
|
||||
static auto get() -> Screen*; // Obtiene el singleton
|
||||
|
||||
// Renderizado
|
||||
void clearRenderer(Rgb color = {0x00, 0x00, 0x00}); // Limpia el renderer
|
||||
void clearSurface(Uint8 index); // Limpia la game_surface_
|
||||
void start(); // Prepara para empezar a dibujar en la textura de juego
|
||||
void render(); // Vuelca el contenido del renderizador en pantalla
|
||||
void update(float delta_time); // Actualiza la lógica de la clase
|
||||
|
||||
// Video y ventana
|
||||
void setVideoMode(bool mode); // Establece el modo de video
|
||||
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
|
||||
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
|
||||
auto incWindowZoom() -> bool; // Aumenta el tamaño de la ventana
|
||||
auto setWindowZoom(int zoom) -> bool; // Establece zoom directo; false si fuera de [1, max_zoom]
|
||||
void show(); // Muestra la ventana
|
||||
void hide(); // Oculta la ventana
|
||||
|
||||
// Borde
|
||||
void setBorderColor(Uint8 color); // Cambia el color del borde
|
||||
static void setBorderWidth(int width); // Establece el ancho del borde
|
||||
static void setBorderHeight(int height); // Establece el alto del borde
|
||||
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
|
||||
void toggleBorder(); // Cambia entre borde visible y no visible
|
||||
|
||||
// Paletas y PostFX
|
||||
void nextPalette(); // Cambia a la siguiente paleta
|
||||
void previousPalette(); // Cambia a la paleta anterior
|
||||
auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPalettePrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
void nextPaletteSortMode(); // Cicla al siguiente modo de ordenación de paleta
|
||||
void setPaletteSortMode(PaletteSortMode mode); // Establece modo de ordenación concreto
|
||||
[[nodiscard]] auto getPaletteSortModeName() const -> std::string; // Nombre del modo de ordenación actual
|
||||
void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
|
||||
void toggleSupersampling(); // Activa/desactiva el supersampling global
|
||||
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
|
||||
void reloadCrtPi(); // Recarga el shader CrtPi del preset actual sin toggle
|
||||
void setLinearUpscale(bool linear); // Upscale NEAREST (false) o LINEAR (true) en el paso SS
|
||||
void setDownscaleAlgo(int algo); // 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setActiveShader(Rendering::ShaderType type); // Cambia el shader de post-procesado activo
|
||||
void nextShader(); // Cicla al siguiente shader disponible (para futura UI)
|
||||
|
||||
// Surfaces y notificaciones
|
||||
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
|
||||
void setNotificationsEnabled(bool value); // Establece la visibilidad de las notificaciones
|
||||
void updateZoomFactor(); // Recalcula y almacena el factor de zoom real
|
||||
|
||||
// Getters
|
||||
auto getRenderer() -> SDL_Renderer*;
|
||||
auto getRendererSurface() -> std::shared_ptr<Surface>;
|
||||
auto getBorderSurface() -> std::shared_ptr<Surface>;
|
||||
auto getGameSurface() -> std::shared_ptr<Surface>;
|
||||
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
|
||||
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
|
||||
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); }
|
||||
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
||||
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
|
||||
[[nodiscard]] static auto getMaxZoom() -> int;
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int>;
|
||||
|
||||
private:
|
||||
// Estructuras
|
||||
struct DisplayMonitor {
|
||||
std::string name;
|
||||
int width{0};
|
||||
int height{0};
|
||||
int refresh_rate{0};
|
||||
};
|
||||
|
||||
struct FPS {
|
||||
Uint32 ticks{0}; // Tiempo en milisegundos desde que se comenzó a contar
|
||||
int frame_count{0}; // Número acumulado de frames en el intervalo
|
||||
int last_value{0}; // Número de frames calculado en el último segundo
|
||||
|
||||
void increment() {
|
||||
frame_count++;
|
||||
}
|
||||
|
||||
auto calculate(Uint32 current_ticks) -> int {
|
||||
if (current_ticks - ticks >= 1000) {
|
||||
last_value = frame_count;
|
||||
frame_count = 0;
|
||||
ticks = current_ticks;
|
||||
}
|
||||
return last_value;
|
||||
}
|
||||
};
|
||||
|
||||
// Constantes
|
||||
static constexpr int WINDOWS_DECORATIONS = 35; // Decoraciones de la ventana
|
||||
|
||||
// Singleton
|
||||
static Screen* screen;
|
||||
|
||||
// Métodos privados
|
||||
void renderNotifications() const; // Dibuja las notificaciones
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
|
||||
void surfaceToTexture(); // Copia la surface a la textura
|
||||
void textureToRenderer(); // Copia la textura al renderizador
|
||||
void renderOverlays(); // Renderiza todos los overlays
|
||||
void initShaders(); // Inicializa los shaders
|
||||
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void createText(); // Crea el objeto de texto
|
||||
|
||||
// Constructor y destructor
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
// Objetos SDL
|
||||
SDL_Window* window_{nullptr}; // Ventana de la aplicación
|
||||
SDL_Renderer* renderer_{nullptr}; // Renderizador de la ventana
|
||||
SDL_Texture* game_texture_{nullptr}; // Textura donde se dibuja el juego
|
||||
SDL_Texture* border_texture_{nullptr}; // Textura donde se dibuja el borde del juego
|
||||
|
||||
// Surfaces y renderizado
|
||||
std::shared_ptr<Surface> game_surface_; // Surface principal del juego
|
||||
std::shared_ptr<Surface> border_surface_; // Surface para el borde de la pantalla
|
||||
std::shared_ptr<std::shared_ptr<Surface>> renderer_surface_; // Puntero a la Surface activa
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (OpenGL/Metal/Vulkan)
|
||||
std::shared_ptr<Text> text_; // Objeto para escribir texto
|
||||
|
||||
// Buffers persistentes para evitar .resize() cada frame
|
||||
std::vector<Uint32> game_pixel_buffer_; // Textura de juego
|
||||
std::vector<Uint32> border_pixel_buffer_; // Textura de borde (composición final borde+juego)
|
||||
|
||||
// Caché del borde sólido (gameplay normal)
|
||||
bool border_is_solid_{true}; // true = borde de color sólido; false = borde dinámico (carga)
|
||||
Uint32 border_argb_color_{0}; // Color ARGB pre-convertido del borde sólido
|
||||
|
||||
// Configuración de ventana y pantalla
|
||||
int window_width_{0}; // Ancho de la pantalla o ventana
|
||||
int window_height_{0}; // Alto de la pantalla o ventana
|
||||
float zoom_factor_{1.0F}; // Factor de zoom calculado (alto físico / alto lógico)
|
||||
SDL_FRect game_surface_dstrect_; // Coordenadas donde se dibuja la textura del juego
|
||||
|
||||
// Paletas y colores
|
||||
Uint8 border_color_{0}; // Color del borde
|
||||
std::unique_ptr<PaletteManager> palette_manager_; // Gestor de paletas de color
|
||||
|
||||
// Estado y configuración
|
||||
bool notifications_enabled_{false}; // Indica si se muestran las notificaciones
|
||||
FPS fps_; // Gestor de frames por segundo
|
||||
DisplayMonitor display_monitor_; // Información de la pantalla
|
||||
|
||||
// Shaders
|
||||
std::string info_resolution_; // Texto con la información de la pantalla
|
||||
std::string gpu_driver_; // Nombre del driver GPU (SDL3GPU), capturado en initShaders()
|
||||
};
|
||||
10362
source/core/rendering/sdl3gpu/crtpi_frag_spv.h
Normal file
10362
source/core/rendering/sdl3gpu/crtpi_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
4253
source/core/rendering/sdl3gpu/downscale_frag_spv.h
Normal file
4253
source/core/rendering/sdl3gpu/downscale_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
11717
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
11717
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1329
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
1329
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
178
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
178
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||
float scanline_strength; // 0 = off, 1 = full
|
||||
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
|
||||
struct CrtPiUniforms {
|
||||
// vec4 #0
|
||||
float scanline_weight; // Ajuste gaussiano (default 6.0)
|
||||
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
|
||||
float input_gamma; // Gamma de entrada (default 2.4)
|
||||
// vec4 #1
|
||||
float output_gamma; // Gamma de salida (default 2.2)
|
||||
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
|
||||
float curvature_x; // Distorsión barrel X (default 0.05)
|
||||
float curvature_y; // Distorsión barrel Y (default 0.10)
|
||||
// vec4 #2
|
||||
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
int enable_scanlines; // 0 = off, 1 = on
|
||||
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
|
||||
int enable_gamma; // 0 = off, 1 = on
|
||||
// vec4 #3
|
||||
int enable_curvature; // 0 = off, 1 = on
|
||||
int enable_sharper; // 0 = off, 1 = on
|
||||
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
|
||||
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||
};
|
||||
|
||||
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
};
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/**
|
||||
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
|
||||
*
|
||||
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
|
||||
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||
* → PostFX render pass → swapchain → present
|
||||
*/
|
||||
class SDL3GPUShader : public ShaderBackend {
|
||||
public:
|
||||
SDL3GPUShader() = default;
|
||||
~SDL3GPUShader() override;
|
||||
|
||||
auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool override;
|
||||
|
||||
void render() override;
|
||||
void setTextureSize(float width, float height) override {}
|
||||
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
||||
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
|
||||
|
||||
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
|
||||
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||
|
||||
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
|
||||
void uploadPixels(const Uint32* pixels, int width, int height) override;
|
||||
|
||||
// Actualiza los parámetros de intensidad de los efectos PostFX
|
||||
void setPostFXParams(const PostFXParams& p) override;
|
||||
|
||||
// Activa/desactiva VSync en el swapchain
|
||||
void setVSync(bool vsync) override;
|
||||
|
||||
// Activa/desactiva escalado entero (integer scale)
|
||||
void setScaleMode(bool integer_scale) override;
|
||||
|
||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||
void setOversample(int factor) override;
|
||||
|
||||
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
|
||||
void setLinearUpscale(bool linear) override;
|
||||
|
||||
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
|
||||
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||
void setActiveShader(ShaderType type) override;
|
||||
|
||||
// Actualiza los parámetros del shader CRT-Pi
|
||||
void setCrtPiParams(const CrtPiParams& p) override;
|
||||
|
||||
// Devuelve el shader activo
|
||||
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
|
||||
|
||||
private:
|
||||
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
static auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||
const uint8_t* spv_code,
|
||||
size_t spv_size,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
auto createPipeline() -> bool;
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
||||
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
|
||||
|
||||
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
|
||||
CrtPiUniforms crtpi_uniforms_{.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 = 1, .enable_multisample = 1, .enable_gamma = 1};
|
||||
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
|
||||
|
||||
int game_width_ = 0; // Dimensiones originales del canvas
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
bool vsync_ = true;
|
||||
bool integer_scale_ = false;
|
||||
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
633
source/core/rendering/sdl3gpu/upscale_frag_spv.h
Normal file
633
source/core/rendering/sdl3gpu/upscale_frag_spv.h
Normal file
@@ -0,0 +1,633 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
static const uint8_t kupscale_frag_spv[] = {
|
||||
0x03,
|
||||
0x02,
|
||||
0x23,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x14,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x53,
|
||||
0x4c,
|
||||
0x2e,
|
||||
0x73,
|
||||
0x74,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x34,
|
||||
0x35,
|
||||
0x30,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xc2,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x70,
|
||||
0x70,
|
||||
0x5f,
|
||||
0x73,
|
||||
0x74,
|
||||
0x79,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x6c,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x63,
|
||||
0x6c,
|
||||
0x75,
|
||||
0x64,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6f,
|
||||
0x75,
|
||||
0x74,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6c,
|
||||
0x6f,
|
||||
0x72,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x73,
|
||||
0x63,
|
||||
0x65,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x76,
|
||||
0x5f,
|
||||
0x75,
|
||||
0x76,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x22,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1b,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x36,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xf8,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x57,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xfd,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x38,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00};
|
||||
static const size_t kupscale_frag_spv_size = 628;
|
||||
175
source/core/rendering/shader_backend.hpp
Normal file
175
source/core/rendering/shader_backend.hpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/** @brief Identificador del shader de post-procesado activo */
|
||||
enum class ShaderType { POSTFX,
|
||||
CRTPI };
|
||||
|
||||
/**
|
||||
* @brief Parámetros de intensidad de los efectos PostFX
|
||||
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
|
||||
*/
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F; // Intensidad de la viñeta
|
||||
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||
float chroma = 0.0F; // Aberración cromática
|
||||
float mask = 0.0F; // Máscara de fósforo RGB
|
||||
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||
float curvature = 0.0F; // Curvatura barrel CRT
|
||||
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
|
||||
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
|
||||
*/
|
||||
struct CrtPiParams {
|
||||
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
|
||||
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
|
||||
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
|
||||
float input_gamma{2.4F}; // Gamma de entrada (linealización)
|
||||
float output_gamma{2.2F}; // Gamma de salida (codificación)
|
||||
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
|
||||
float curvature_x{0.05F}; // Distorsión barrel eje X
|
||||
float curvature_y{0.10F}; // Distorsión barrel eje Y
|
||||
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
bool enable_scanlines{true}; // Activar efecto de scanlines
|
||||
bool enable_multisample{true}; // Antialiasing analítico de scanlines
|
||||
bool enable_gamma{true}; // Corrección gamma
|
||||
bool enable_curvature{false}; // Distorsión barrel CRT
|
||||
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Interfaz abstracta para backends de renderizado con shaders
|
||||
*
|
||||
* Esta interfaz define el contrato que todos los backends de shaders
|
||||
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
|
||||
*/
|
||||
class ShaderBackend {
|
||||
public:
|
||||
virtual ~ShaderBackend() = default;
|
||||
|
||||
/**
|
||||
* @brief Inicializa el backend de shaders
|
||||
* @param window Ventana SDL
|
||||
* @param texture Textura de backbuffer a la que aplicar shaders
|
||||
* @param vertex_source Código fuente del vertex shader
|
||||
* @param fragment_source Código fuente del fragment shader
|
||||
* @return true si la inicialización fue exitosa
|
||||
*/
|
||||
virtual auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Renderiza la textura con los shaders aplicados
|
||||
*/
|
||||
virtual void render() = 0;
|
||||
|
||||
/**
|
||||
* @brief Establece el tamaño de la textura como parámetro del shader
|
||||
* @param width Ancho de la textura
|
||||
* @param height Alto de la textura
|
||||
*/
|
||||
virtual void setTextureSize(float width, float height) = 0;
|
||||
|
||||
/**
|
||||
* @brief Limpia y libera recursos del backend
|
||||
*/
|
||||
virtual void cleanup() = 0;
|
||||
|
||||
/**
|
||||
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
|
||||
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
|
||||
*/
|
||||
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros de intensidad de los efectos PostFX
|
||||
* @param p Struct con todos los parámetros PostFX
|
||||
*/
|
||||
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva VSync en el swapchain del GPU device
|
||||
*/
|
||||
virtual void setVSync(bool /*vsync*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva el escalado entero (integer scale)
|
||||
*/
|
||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||
*/
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
|
||||
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
|
||||
*/
|
||||
virtual void setLinearUpscale(bool /*linear*/) {}
|
||||
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
|
||||
|
||||
/**
|
||||
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
|
||||
*/
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
/**
|
||||
* @brief Devuelve las dimensiones de la textura de supersampling.
|
||||
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
/**
|
||||
* @brief Verifica si el backend está usando aceleración por hardware
|
||||
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||
*/
|
||||
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
|
||||
* @return Cadena vacía si no disponible
|
||||
*/
|
||||
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
|
||||
|
||||
/**
|
||||
* @brief Establece el driver GPU preferido antes de init().
|
||||
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
|
||||
*/
|
||||
virtual void setPreferredDriver(const std::string& /*driver*/) {}
|
||||
|
||||
/**
|
||||
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
|
||||
* Debe llamarse antes de render(). No recrea pipelines.
|
||||
*/
|
||||
virtual void setActiveShader(ShaderType /*type*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros del shader CRT-Pi.
|
||||
*/
|
||||
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Devuelve el shader de post-procesado activo.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
344
source/core/rendering/sprite/animated_sprite.cpp
Normal file
344
source/core/rendering/sprite/animated_sprite.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <fstream> // Para basic_ostream, basic_istream, operator<<, basic...
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <sstream> // Para basic_stringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "utils/utils.hpp" // Para printWithDots
|
||||
|
||||
// Helper: Convierte un nodo YAML de frames (array) a vector de SDL_FRect
|
||||
auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
||||
std::vector<SDL_FRect> frames;
|
||||
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
||||
|
||||
for (const auto& frame_index_node : frames_node) {
|
||||
const int NUM_TILE = frame_index_node.get_value<int>();
|
||||
if (NUM_TILE <= max_tiles) {
|
||||
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||
frames.emplace_back(rect);
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
// Carga las animaciones desde un fichero YAML
|
||||
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<AnimationData> animations;
|
||||
|
||||
// Extract filename for logging
|
||||
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||
|
||||
try {
|
||||
// Load YAML file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Unable to load animation file " << FILE_NAME << '\n';
|
||||
throw std::runtime_error("Animation file not found: " + file_path);
|
||||
}
|
||||
|
||||
printWithDots("Animation : ", FILE_NAME, "[ LOADED ]");
|
||||
|
||||
// Parse YAML from string
|
||||
std::string yaml_content(file_data.begin(), file_data.end());
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
surface = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface) {
|
||||
frames_per_row = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
// Constructor con bytes YAML del cache (parsing lazy)
|
||||
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
// Parsear YAML desde los bytes cargados en cache
|
||||
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// Variables para almacenar configuración global
|
||||
float frame_width = 0.0F;
|
||||
float frame_height = 0.0F;
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
// Ahora SÍ podemos acceder al cache (ya está completamente cargado)
|
||||
surface_ = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface_) {
|
||||
frames_per_row = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface_->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations_.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
// Set dimensions
|
||||
setWidth(frame_width);
|
||||
setHeight(frame_height);
|
||||
|
||||
// Inicializar con la primera animación si existe
|
||||
if (!animations_.empty() && !animations_[0].frames.empty()) {
|
||||
setClip(animations_[0].frames[0]);
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor per a subclasses amb surface directa (sense YAML)
|
||||
AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: MovingSprite(std::move(surface), pos) {
|
||||
// animations_ queda buit (protegit per el guard de animate())
|
||||
if (surface_) {
|
||||
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene el indice de la animación a partir del nombre
|
||||
auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto index = -1;
|
||||
|
||||
for (const auto& a : animations_) {
|
||||
index++;
|
||||
if (a.name == name) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calcula el frame correspondiente a la animación (time-based)
|
||||
void AnimatedSprite::animate(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (animations_.empty()) { return; }
|
||||
if (animations_[current_animation_].speed <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acumula el tiempo transcurrido
|
||||
animations_[current_animation_].accumulated_time += delta_time;
|
||||
|
||||
// Calcula el frame actual a partir del tiempo acumulado
|
||||
const int TARGET_FRAME = static_cast<int>(
|
||||
animations_[current_animation_].accumulated_time /
|
||||
animations_[current_animation_].speed);
|
||||
|
||||
// Si alcanza el final de la animación, maneja el loop
|
||||
if (TARGET_FRAME >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
if (animations_[current_animation_].loop == -1) {
|
||||
// Si no hay loop, congela en el último frame
|
||||
animations_[current_animation_].current_frame =
|
||||
static_cast<int>(animations_[current_animation_].frames.size()) - 1;
|
||||
animations_[current_animation_].completed = true;
|
||||
|
||||
// Establece el clip del último frame
|
||||
if (animations_[current_animation_].current_frame >= 0) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Si hay loop, vuelve al frame indicado
|
||||
animations_[current_animation_].accumulated_time =
|
||||
static_cast<float>(animations_[current_animation_].loop) *
|
||||
animations_[current_animation_].speed;
|
||||
animations_[current_animation_].current_frame = animations_[current_animation_].loop;
|
||||
|
||||
// Establece el clip del frame de loop
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Actualiza el frame actual
|
||||
animations_[current_animation_].current_frame = TARGET_FRAME;
|
||||
|
||||
// Establece el clip del frame actual
|
||||
if (animations_[current_animation_].current_frame >= 0 &&
|
||||
animations_[current_animation_].current_frame <
|
||||
static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si ha terminado la animación
|
||||
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||
return animations_[current_animation_].completed;
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||
const auto NEW_ANIMATION = getIndex(name);
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||
const auto NEW_ANIMATION = index;
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto (time-based)
|
||||
void AnimatedSprite::update(float delta_time) {
|
||||
animate(delta_time);
|
||||
MovingSprite::update(delta_time);
|
||||
}
|
||||
|
||||
// Reinicia la animación
|
||||
void AnimatedSprite::resetAnimation() {
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
}
|
||||
|
||||
// Establece el frame actual de la animación
|
||||
void AnimatedSprite::setCurrentAnimationFrame(int num) {
|
||||
// Descarta valores fuera de rango
|
||||
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
num = 0;
|
||||
}
|
||||
|
||||
// Cambia el valor de la variable
|
||||
animations_[current_animation_].current_frame = num;
|
||||
|
||||
// Escoge el frame correspondiente de la animación
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
60
source/core/rendering/sprite/animated_sprite.hpp
Normal file
60
source/core/rendering/sprite/animated_sprite.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/sprite/moving_sprite.hpp" // Para SMovingSprite
|
||||
#include "core/resources/resource_types.hpp" // Para AnimationResource
|
||||
|
||||
class Surface;
|
||||
|
||||
class AnimatedSprite : public MovingSprite {
|
||||
public:
|
||||
using Animations = std::vector<std::string>; // Tipo para lista de animaciones
|
||||
|
||||
// Estructura pública de datos de animación
|
||||
struct AnimationData {
|
||||
std::string name; // Nombre de la animacion
|
||||
std::vector<SDL_FRect> frames; // Cada uno de los frames que componen la animación
|
||||
float speed{0.083F}; // Velocidad de la animación (segundos por frame)
|
||||
int loop{0}; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
||||
bool completed{false}; // Indica si ha finalizado la animación
|
||||
int current_frame{0}; // Frame actual
|
||||
float accumulated_time{0.0F}; // Tiempo acumulado para las animaciones (time-based)
|
||||
};
|
||||
|
||||
// Métodos estáticos
|
||||
static auto loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData>; // Carga las animaciones desde fichero YAML
|
||||
|
||||
// Constructores
|
||||
explicit AnimatedSprite(const AnimationResource& cached_data); // Constructor con datos pre-cargados del cache
|
||||
|
||||
~AnimatedSprite() override = default; // Destructor
|
||||
|
||||
void update(float delta_time) override; // Actualiza las variables del objeto (time-based)
|
||||
|
||||
// Consultas de estado
|
||||
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
|
||||
auto getIndex(const std::string& name) -> int; // Obtiene el índice de la animación por nombre
|
||||
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); } // Número de frames de la animación actual
|
||||
|
||||
// Modificadores de animación
|
||||
void setCurrentAnimation(const std::string& name = "default"); // Establece la animación actual por nombre
|
||||
void setCurrentAnimation(int index = 0); // Establece la animación actual por índice
|
||||
void resetAnimation(); // Reinicia la animación
|
||||
void setCurrentAnimationFrame(int num); // Establece el frame actual de la animación
|
||||
void animate(float delta_time); // Calcula el frame correspondiente a la animación actual (time-based)
|
||||
|
||||
protected:
|
||||
// Constructor per a ús de subclasses que gestionen la surface directament (sense YAML)
|
||||
AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
|
||||
private:
|
||||
// Variables miembro
|
||||
std::vector<AnimationData> animations_; // Vector con las diferentes animaciones
|
||||
int current_animation_{0}; // Animación activa
|
||||
};
|
||||
188
source/core/rendering/sprite/dissolve_sprite.cpp
Normal file
188
source/core/rendering/sprite/dissolve_sprite.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "core/rendering/sprite/dissolve_sprite.hpp"
|
||||
|
||||
#include <algorithm> // Para min
|
||||
#include <cstdint> // Para uint32_t
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Hash 2D estable per a dithering (rank aleatori per posició de píxel)
|
||||
static auto pixelRank(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Rang per a un píxel tenint en compte direcció (70% direccional + 30% aleatori)
|
||||
auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float {
|
||||
const float RANDOM = pixelRank(col, row);
|
||||
if (dir == DissolveDirection::NONE || frame_h <= 0) {
|
||||
return RANDOM;
|
||||
}
|
||||
|
||||
float y_factor = 0.0F;
|
||||
if (dir == DissolveDirection::DOWN) {
|
||||
y_factor = static_cast<float>(row) / static_cast<float>(frame_h);
|
||||
} else {
|
||||
y_factor = static_cast<float>(frame_h - 1 - row) / static_cast<float>(frame_h);
|
||||
}
|
||||
|
||||
return (y_factor * 0.7F) + (RANDOM * 0.3F);
|
||||
}
|
||||
|
||||
// Constructor per a surface directa (sense AnimationResource)
|
||||
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: AnimatedSprite(std::move(surface), pos) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor
|
||||
DissolveSprite::DissolveSprite(const AnimationResource& data)
|
||||
: AnimatedSprite(data) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
// Inicialitza tots els píxels com a transparents
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstrueix la surface_display_ filtrant píxels per progress_
|
||||
void DissolveSprite::rebuildDisplaySurface() {
|
||||
if (!surface_ || !surface_display_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SDL_FRect CLIP = clip_;
|
||||
const int SX = static_cast<int>(CLIP.x);
|
||||
const int SY = static_cast<int>(CLIP.y);
|
||||
const int SW = static_cast<int>(CLIP.w);
|
||||
const int SH = static_cast<int>(CLIP.h);
|
||||
|
||||
if (SW <= 0 || SH <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto src_data = surface_->getSurfaceData();
|
||||
auto dst_data = surface_display_->getSurfaceData();
|
||||
|
||||
const int SRC_W = static_cast<int>(src_data->width);
|
||||
const int DST_W = static_cast<int>(dst_data->width);
|
||||
const Uint8 TRANSPARENT = surface_->getTransparentColor();
|
||||
|
||||
// Esborra frame anterior si ha canviat
|
||||
if (prev_clip_.w > 0 && prev_clip_.h > 0 &&
|
||||
(prev_clip_.x != CLIP.x || prev_clip_.y != CLIP.y ||
|
||||
prev_clip_.w != CLIP.w || prev_clip_.h != CLIP.h)) {
|
||||
surface_display_->fillRect(&prev_clip_, TRANSPARENT);
|
||||
}
|
||||
|
||||
// Esborra la zona del frame actual (reconstrucció neta)
|
||||
surface_display_->fillRect(&CLIP, TRANSPARENT);
|
||||
|
||||
// Copia píxels filtrats per progress_
|
||||
for (int row = 0; row < SH; ++row) {
|
||||
for (int col = 0; col < SW; ++col) {
|
||||
const Uint8 COLOR = src_data->data[((SY + row) * SRC_W) + (SX + col)];
|
||||
if (COLOR == TRANSPARENT) {
|
||||
continue;
|
||||
}
|
||||
const float RANK = computePixelRank(col, row, SH, direction_);
|
||||
if (RANK >= progress_) {
|
||||
const Uint8 OUT = (COLOR == source_color_) ? target_color_ : COLOR;
|
||||
dst_data->data[((SY + row) * DST_W) + (SX + col)] = OUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev_clip_ = CLIP;
|
||||
needs_rebuild_ = false;
|
||||
}
|
||||
|
||||
// Actualitza animació, moviment i transició temporal
|
||||
void DissolveSprite::update(float delta_time) {
|
||||
const SDL_FRect OLD_CLIP = clip_;
|
||||
AnimatedSprite::update(delta_time);
|
||||
|
||||
// Detecta canvi de frame d'animació
|
||||
if (clip_.x != OLD_CLIP.x || clip_.y != OLD_CLIP.y ||
|
||||
clip_.w != OLD_CLIP.w || clip_.h != OLD_CLIP.h) {
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Actualitza transició temporal si activa
|
||||
if (transition_mode_ != TransitionMode::NONE) {
|
||||
transition_elapsed_ += delta_time * 1000.0F;
|
||||
const float T = std::min(transition_elapsed_ / transition_duration_, 1.0F);
|
||||
progress_ = (transition_mode_ == TransitionMode::DISSOLVING) ? T : (1.0F - T);
|
||||
needs_rebuild_ = true;
|
||||
if (T >= 1.0F) {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_rebuild_) {
|
||||
rebuildDisplaySurface();
|
||||
}
|
||||
}
|
||||
|
||||
// Renderitza: usa surface_display_ (amb color replace) si disponible
|
||||
void DissolveSprite::render() {
|
||||
if (!surface_display_) {
|
||||
AnimatedSprite::render();
|
||||
return;
|
||||
}
|
||||
surface_display_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_, flip_);
|
||||
}
|
||||
|
||||
// Estableix el progrés manualment
|
||||
void DissolveSprite::setProgress(float progress) {
|
||||
progress_ = std::min(std::max(progress, 0.0F), 1.0F);
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia dissolució temporal (visible → invisible)
|
||||
void DissolveSprite::startDissolve(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::DISSOLVING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 0.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia generació temporal (invisible → visible)
|
||||
void DissolveSprite::startGenerate(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::GENERATING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 1.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Atura la transició temporal
|
||||
void DissolveSprite::stopTransition() {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Retorna si la transició ha acabat
|
||||
auto DissolveSprite::isTransitionDone() const -> bool {
|
||||
return transition_mode_ == TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Configura substitució de color per a la reconstrucció
|
||||
void DissolveSprite::setColorReplace(Uint8 source, Uint8 target) {
|
||||
source_color_ = source;
|
||||
target_color_ = target;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
62
source/core/rendering/sprite/dissolve_sprite.hpp
Normal file
62
source/core/rendering/sprite/dissolve_sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SurfaceAnimatedSprite
|
||||
|
||||
class Surface;
|
||||
|
||||
// Direcció de la dissolució
|
||||
enum class DissolveDirection { NONE,
|
||||
DOWN,
|
||||
UP };
|
||||
|
||||
// Sprite que pot dissoldre's o generar-se de forma aleatòria en X mil·lisegons.
|
||||
// progress_ va de 0.0 (totalment visible) a 1.0 (totalment invisible).
|
||||
class DissolveSprite : public AnimatedSprite {
|
||||
public:
|
||||
explicit DissolveSprite(const AnimationResource& data);
|
||||
DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
~DissolveSprite() override = default;
|
||||
|
||||
void update(float delta_time) override;
|
||||
void render() override;
|
||||
|
||||
// Progrés manual [0.0 = totalment visible, 1.0 = totalment invisible]
|
||||
void setProgress(float progress);
|
||||
[[nodiscard]] auto getProgress() const -> float { return progress_; }
|
||||
|
||||
// Inicia una dissolució temporal (visible → invisible en duration_ms)
|
||||
void startDissolve(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
// Inicia una generació temporal (invisible → visible en duration_ms)
|
||||
void startGenerate(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
void stopTransition();
|
||||
[[nodiscard]] auto isTransitionDone() const -> bool;
|
||||
|
||||
// Substitució de color: en reconstruir, substitueix source per target
|
||||
void setColorReplace(Uint8 source, Uint8 target);
|
||||
|
||||
private:
|
||||
enum class TransitionMode { NONE,
|
||||
DISSOLVING,
|
||||
GENERATING };
|
||||
|
||||
std::shared_ptr<Surface> surface_display_; // Superfície amb els píxels filtrats
|
||||
|
||||
float progress_{0.0F}; // [0=visible, 1=invisible]
|
||||
DissolveDirection direction_{DissolveDirection::NONE};
|
||||
TransitionMode transition_mode_{TransitionMode::NONE};
|
||||
float transition_duration_{0.0F};
|
||||
float transition_elapsed_{0.0F};
|
||||
SDL_FRect prev_clip_{.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
bool needs_rebuild_{false};
|
||||
Uint8 source_color_{255}; // 255 = transparent = sense replace per defecte
|
||||
Uint8 target_color_{0};
|
||||
|
||||
void rebuildDisplaySurface();
|
||||
[[nodiscard]] static auto computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float;
|
||||
};
|
||||
103
source/core/rendering/sprite/moving_sprite.cpp
Normal file
103
source/core/rendering/sprite/moving_sprite.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "core/rendering/sprite/moving_sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y),
|
||||
flip_(flip) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite() { Sprite::clear(); }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
|
||||
: Sprite(std::move(surface)) { Sprite::clear(); }
|
||||
|
||||
// Reinicia todas las variables
|
||||
void MovingSprite::clear() {
|
||||
// Resetea posición
|
||||
x_ = 0.0F;
|
||||
y_ = 0.0F;
|
||||
|
||||
// Resetea velocidad
|
||||
vx_ = 0.0F;
|
||||
vy_ = 0.0F;
|
||||
|
||||
// Resetea aceleración
|
||||
ax_ = 0.0F;
|
||||
ay_ = 0.0F;
|
||||
|
||||
// Resetea flip
|
||||
flip_ = SDL_FLIP_NONE;
|
||||
|
||||
Sprite::clear();
|
||||
}
|
||||
|
||||
// Mueve el sprite (time-based)
|
||||
// Nota: vx_, vy_ ahora se interpretan como pixels/segundo
|
||||
// Nota: ax_, ay_ ahora se interpretan como pixels/segundo²
|
||||
void MovingSprite::move(float delta_time) {
|
||||
// Aplica aceleración a velocidad (time-based)
|
||||
vx_ += ax_ * delta_time;
|
||||
vy_ += ay_ * delta_time;
|
||||
|
||||
// Aplica velocidad a posición (time-based)
|
||||
x_ += vx_ * delta_time;
|
||||
y_ += vy_ * delta_time;
|
||||
|
||||
// Actualiza posición entera para renderizado
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Actualiza las variables internas del objeto (time-based)
|
||||
void MovingSprite::update(float delta_time) {
|
||||
move(delta_time);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Establece la posición y_ el tamaño del objeto
|
||||
void MovingSprite::setPos(SDL_FRect rect) {
|
||||
x_ = rect.x;
|
||||
y_ = rect.y;
|
||||
|
||||
pos_ = rect;
|
||||
}
|
||||
|
||||
// Establece el valor de las variables
|
||||
void MovingSprite::setPos(float x, float y) {
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosX(float value) {
|
||||
x_ = value;
|
||||
pos_.x = static_cast<int>(x_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosY(float value) {
|
||||
y_ = value;
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
77
source/core/rendering/sprite/moving_sprite.hpp
Normal file
77
source/core/rendering/sprite/moving_sprite.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // lines 8-8
|
||||
|
||||
// Clase SMovingSprite. Añade movimiento y flip al sprite
|
||||
class MovingSprite : public Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip);
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
explicit MovingSprite();
|
||||
explicit MovingSprite(std::shared_ptr<Surface> surface);
|
||||
~MovingSprite() override = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
void update(float delta_time) override; // Actualiza variables internas (time-based)
|
||||
void render() override; // Muestra el sprite por pantalla
|
||||
void render(Uint8 source_color, Uint8 target_color) override; // Renderiza con reemplazo de color
|
||||
|
||||
// Gestión de estado
|
||||
void clear() override; // Reinicia todas las variables a cero
|
||||
|
||||
// Getters de posición
|
||||
[[nodiscard]] auto getPosX() const -> float { return x_; }
|
||||
[[nodiscard]] auto getPosY() const -> float { return y_; }
|
||||
|
||||
// Getters de velocidad
|
||||
[[nodiscard]] auto getVelX() const -> float { return vx_; }
|
||||
[[nodiscard]] auto getVelY() const -> float { return vy_; }
|
||||
|
||||
// Getters de aceleración
|
||||
[[nodiscard]] auto getAccelX() const -> float { return ax_; }
|
||||
[[nodiscard]] auto getAccelY() const -> float { return ay_; }
|
||||
|
||||
// Setters de posición
|
||||
void setPos(SDL_FRect rect); // Establece posición y tamaño del objeto
|
||||
void setPos(float x, float y); // Establece posición x, y
|
||||
void setPosX(float value); // Establece posición X
|
||||
void setPosY(float value); // Establece posición Y
|
||||
|
||||
// Setters de velocidad
|
||||
void setVelX(float value) { vx_ = value; }
|
||||
void setVelY(float value) { vy_ = value; }
|
||||
|
||||
// Setters de aceleración
|
||||
void setAccelX(float value) { ax_ = value; }
|
||||
void setAccelY(float value) { ay_ = value; }
|
||||
|
||||
// Gestión de flip (volteo horizontal)
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; } // Establece modo de flip
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; } // Obtiene modo de flip
|
||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } // Alterna flip horizontal
|
||||
|
||||
protected:
|
||||
// Métodos protegidos
|
||||
void move(float delta_time); // Mueve el sprite (time-based)
|
||||
|
||||
// Variables miembro - Posición
|
||||
float x_{0.0F}; // Posición en el eje X
|
||||
float y_{0.0F}; // Posición en el eje Y
|
||||
|
||||
// Variables miembro - Velocidad (pixels/segundo)
|
||||
float vx_{0.0F}; // Velocidad en el eje X
|
||||
float vy_{0.0F}; // Velocidad en el eje Y
|
||||
|
||||
// Variables miembro - Aceleración (pixels/segundo²)
|
||||
float ax_{0.0F}; // Aceleración en el eje X
|
||||
float ay_{0.0F}; // Aceleración en el eje Y
|
||||
|
||||
// Variables miembro - Renderizado
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE}; // Modo de volteo del sprite
|
||||
};
|
||||
76
source/core/rendering/sprite/sprite.cpp
Normal file
76
source/core/rendering/sprite/sprite.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{.x = x, .y = y, .w = w, .h = h},
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
|
||||
: surface_(std::move(surface)),
|
||||
pos_(rect),
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite() = default;
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
|
||||
clip_(pos_) {}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void Sprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
source_color,
|
||||
target_color,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(float x, float y) {
|
||||
pos_.x = x;
|
||||
pos_.y = y;
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(SDL_FPoint p) {
|
||||
pos_.x = p.x;
|
||||
pos_.y = p.y;
|
||||
}
|
||||
|
||||
// Reinicia las variables a cero
|
||||
void Sprite::clear() {
|
||||
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
}
|
||||
|
||||
// Actualiza el estado del sprite (time-based)
|
||||
void Sprite::update(float delta_time) {
|
||||
// Base implementation does nothing (static sprites)
|
||||
(void)delta_time; // Evita warning de parámetro no usado
|
||||
}
|
||||
62
source/core/rendering/sprite/sprite.hpp
Normal file
62
source/core/rendering/sprite/sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <utility>
|
||||
class Surface; // lines 5-5
|
||||
|
||||
// Clase SurfaceSprite
|
||||
class Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
Sprite(std::shared_ptr<Surface>, float x, float y, float w, float h);
|
||||
Sprite(std::shared_ptr<Surface>, SDL_FRect rect);
|
||||
Sprite();
|
||||
explicit Sprite(std::shared_ptr<Surface>);
|
||||
|
||||
// Destructor
|
||||
virtual ~Sprite() = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
virtual void update(float delta_time); // Actualiza el estado del sprite (time-based)
|
||||
virtual void render(); // Muestra el sprite por pantalla
|
||||
virtual void render(Uint8 source_color, Uint8 target_color); // Renderiza con reemplazo de color
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height); // Renderiza amb dissolució vertical (hash 2D, sense parpelleig)
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color); // Idem amb reemplaç de color
|
||||
|
||||
// Gestión de estado
|
||||
virtual void clear(); // Reinicia las variables a cero
|
||||
|
||||
// Obtención de propiedades
|
||||
[[nodiscard]] auto getX() const -> float { return pos_.x; }
|
||||
[[nodiscard]] auto getY() const -> float { return pos_.y; }
|
||||
[[nodiscard]] auto getWidth() const -> float { return pos_.w; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return pos_.h; }
|
||||
[[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; }
|
||||
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
|
||||
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
|
||||
auto getRect() -> SDL_FRect& { return pos_; }
|
||||
|
||||
// Modificación de posición y tamaño
|
||||
void setX(float x) { pos_.x = x; }
|
||||
void setY(float y) { pos_.y = y; }
|
||||
void setWidth(float w) { pos_.w = w; }
|
||||
void setHeight(float h) { pos_.h = h; }
|
||||
void setPosition(float x, float y);
|
||||
void setPosition(SDL_FPoint p);
|
||||
void setPosition(SDL_FRect r) { pos_ = r; }
|
||||
void incX(float value) { pos_.x += value; }
|
||||
void incY(float value) { pos_.y += value; }
|
||||
|
||||
// Modificación de clip y surface
|
||||
void setClip(SDL_FRect rect) { clip_ = rect; }
|
||||
void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
|
||||
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
|
||||
|
||||
protected:
|
||||
// Variables miembro
|
||||
std::shared_ptr<Surface> surface_{nullptr}; // Surface donde estan todos los dibujos del sprite
|
||||
SDL_FRect pos_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Posición y tamaño donde dibujar el sprite
|
||||
SDL_FRect clip_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Rectangulo de origen de la surface que se dibujará en pantalla
|
||||
};
|
||||
695
source/core/rendering/surface.cpp
Normal file
695
source/core/rendering/surface.cpp
Normal file
@@ -0,0 +1,695 @@
|
||||
// IWYU pragma: no_include <bits/std_abs.h>
|
||||
#include "core/rendering/surface.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para min, max, copy_n, fill
|
||||
#include <cmath> // Para abs
|
||||
#include <cstdint> // Para uint32_t
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ist...
|
||||
#include <iostream> // Para cerr
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access, default...
|
||||
#include <sstream> // Para basic_istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/gif.hpp" // Para Gif
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette {
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
throw std::runtime_error("Error opening file: " + file_path);
|
||||
}
|
||||
|
||||
// Cargar la paleta usando los datos del buffer
|
||||
std::vector<uint32_t> pal = GIF::Gif::loadPalette(buffer.data());
|
||||
if (pal.empty()) {
|
||||
throw std::runtime_error("No palette found in GIF file: " + file_path);
|
||||
}
|
||||
|
||||
// Crear la paleta y copiar los datos desde 'pal'
|
||||
Palette palette = {}; // Inicializa la paleta con ceros
|
||||
std::copy_n(pal.begin(), std::min(pal.size(), palette.size()), palette.begin());
|
||||
|
||||
// Mensaje de depuración
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette {
|
||||
Palette palette{};
|
||||
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)
|
||||
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
throw std::runtime_error("No se pudo abrir el archivo .pal: " + file_path);
|
||||
}
|
||||
|
||||
// Convert bytes to string for parsing
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
int line_number = 0;
|
||||
int color_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
++line_number;
|
||||
|
||||
// Ignorar las tres primeras líneas del archivo
|
||||
if (line_number <= 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Procesar las líneas restantes con valores RGB
|
||||
std::istringstream ss(line);
|
||||
int r;
|
||||
int g;
|
||||
int b;
|
||||
if (ss >> r >> g >> b) {
|
||||
// Construir el color ARGB (A = 255 por defecto)
|
||||
Uint32 color = (255 << 24) | (r << 16) | (g << 8) | b;
|
||||
palette[color_index++] = color;
|
||||
|
||||
// Limitar a un máximo de 256 colores (opcional)
|
||||
if (color_index >= 256) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Surface::Surface(int w, int h)
|
||||
: surface_data_(std::make_shared<SurfaceData>(w, h)),
|
||||
transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) { initializeSubPalette(sub_palette_); }
|
||||
|
||||
Surface::Surface(const std::string& file_path)
|
||||
: transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) {
|
||||
SurfaceData loaded_data = loadSurface(file_path);
|
||||
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded_data));
|
||||
|
||||
initializeSubPalette(sub_palette_);
|
||||
}
|
||||
|
||||
// Carga una superficie desde un archivo
|
||||
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Error opening file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error opening file");
|
||||
}
|
||||
|
||||
// Crear un objeto Gif y llamar a la función loadGif
|
||||
Uint16 w = 0;
|
||||
Uint16 h = 0;
|
||||
std::vector<Uint8> raw_pixels = GIF::Gif::loadGif(buffer.data(), w, h);
|
||||
if (raw_pixels.empty()) {
|
||||
std::cerr << "Error loading GIF from file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error loading GIF");
|
||||
}
|
||||
|
||||
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>,
|
||||
// reservamos un bloque dinámico y copiamos los datos del vector.
|
||||
size_t pixel_count = raw_pixels.size();
|
||||
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>());
|
||||
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
|
||||
|
||||
// Crear y devolver directamente el objeto SurfaceData
|
||||
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return {static_cast<float>(w), static_cast<float>(h), pixels};
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void Surface::loadPalette(const std::string& file_path) {
|
||||
palette_ = ::loadPalette(file_path);
|
||||
}
|
||||
|
||||
// Carga una paleta desde otra paleta
|
||||
void Surface::loadPalette(const Palette& palette) {
|
||||
palette_ = palette;
|
||||
}
|
||||
|
||||
// Establece un color en la paleta
|
||||
void Surface::setColor(int index, Uint32 color) {
|
||||
palette_.at(index) = color;
|
||||
}
|
||||
|
||||
// Rellena la superficie con un color
|
||||
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
||||
}
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||
return; // Coordenadas fuera de rango
|
||||
}
|
||||
|
||||
const int INDEX = x + (y * surface_data_->width);
|
||||
surface_data_->data.get()[INDEX] = color;
|
||||
}
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Rellenar fila a fila con memset (memoria contigua por fila)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
|
||||
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
|
||||
// Dibujar bordes verticales
|
||||
for (int y = y_start; y < y_end; ++y) {
|
||||
// Borde izquierdo
|
||||
const int LEFT_INDEX = x_start + (y * surface_data_->width);
|
||||
surface_data_->data.get()[LEFT_INDEX] = color;
|
||||
|
||||
// Borde derecho
|
||||
const int RIGHT_INDEX = (x_end - 1) + (y * surface_data_->width);
|
||||
surface_data_->data.get()[RIGHT_INDEX] = color;
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja una linea
|
||||
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Calcula las diferencias
|
||||
float dx = std::abs(x2 - x1);
|
||||
float dy = std::abs(y2 - y1);
|
||||
|
||||
// Determina la dirección del incremento
|
||||
float sx = (x1 < x2) ? 1 : -1;
|
||||
float sy = (y1 < y2) ? 1 : -1;
|
||||
|
||||
float err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
// Asegúrate de no dibujar fuera de los límites de la superficie
|
||||
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
|
||||
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
|
||||
}
|
||||
|
||||
// Si alcanzamos el punto final, salimos
|
||||
if (x1 == x2 && y1 == y2) {
|
||||
break;
|
||||
}
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x1 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y1 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data->width - dx);
|
||||
h = std::min(h, surface_data->height - dy);
|
||||
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
|
||||
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
|
||||
int src_x = sx + ix;
|
||||
int src_y = sy + iy;
|
||||
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data_dest->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data_dest->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper para calcular coordenadas con flip
|
||||
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
|
||||
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
}
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void Surface::copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const {
|
||||
if (dest_x < 0 || dest_y < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dest_data[dest_x + (dest_y * dest_width)] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la superficie de origen a la de destino
|
||||
void Surface::render(SDL_FRect* src_rect, SDL_FRect* dst_rect, SDL_FlipMode flip) {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Si srcRect es nullptr, tomar toda la superficie fuente
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float sw = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float sh = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Si dstRect es nullptr, asignar las mismas dimensiones que srcRect
|
||||
float dx = (dst_rect != nullptr) ? dst_rect->x : 0;
|
||||
float dy = (dst_rect != nullptr) ? dst_rect->y : 0;
|
||||
float dw = (dst_rect != nullptr) ? dst_rect->w : sw;
|
||||
float dh = (dst_rect != nullptr) ? dst_rect->h : sh;
|
||||
|
||||
// Asegurarse de que srcRect y dstRect tienen las mismas dimensiones
|
||||
if (sw != dw || sh != dh) {
|
||||
dw = sw; // Respetar las dimensiones de srcRect
|
||||
dh = sh;
|
||||
}
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en src y dst
|
||||
sw = std::min(sw, surface_data_->width - sx);
|
||||
sh = std::min(sh, surface_data_->height - sy);
|
||||
dw = std::min(dw, surface_data->width - dx);
|
||||
dh = std::min(dh, surface_data->height - dy);
|
||||
|
||||
int final_width = std::min(sw, dw);
|
||||
int final_height = std::min(sh, dh);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < final_height; ++iy) {
|
||||
for (int ix = 0; ix < final_width; ++ix) {
|
||||
int src_x = 0;
|
||||
int src_y = 0;
|
||||
calculateFlippedCoords(ix, iy, sx, sy, final_width, final_height, flip, src_x, src_y);
|
||||
|
||||
int dest_x = dx + ix;
|
||||
int dest_y = dy + iy;
|
||||
|
||||
// Verificar límites de destino antes de copiar
|
||||
if (dest_x >= 0 && dest_x < surface_data->width && dest_y >= 0 && dest_y < surface_data->height) {
|
||||
copyPixelIfNotTransparent(surface_data->data.get(), dest_x, dest_y, surface_data->width, src_x, src_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect, SDL_FlipMode flip) const {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verifica que las coordenadas de destino estén dentro de los límites
|
||||
if (dest_x < 0 || dest_y < 0 || dest_x >= surface_data->width || dest_y >= surface_data->height) {
|
||||
continue; // Saltar píxeles fuera del rango del destino
|
||||
}
|
||||
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
surface_data->data[dest_x + (dest_y * surface_data->width)] =
|
||||
(color == source_color) ? target_color : color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash 2D estable per a dithering sense flickering
|
||||
static auto pixelThreshold(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Calcula la densidad de fade para un pixel en posición screen_y
|
||||
static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> float {
|
||||
if (screen_y < fade_h) {
|
||||
return static_cast<float>(fade_h - screen_y) / static_cast<float>(fade_h);
|
||||
}
|
||||
if (screen_y >= canvas_height - fade_h) {
|
||||
return static_cast<float>(screen_y - (canvas_height - fade_h)) / static_cast<float>(fade_h);
|
||||
}
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Idem però reemplaçant un color índex
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture ni SDL_Renderer)
|
||||
void Surface::toARGBBuffer(Uint32* buffer) const {
|
||||
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
|
||||
|
||||
const int WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int HEIGHT = static_cast<int>(surface_data_->height);
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
|
||||
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
|
||||
const size_t PAL_SIZE = palette_.size();
|
||||
|
||||
for (int i = 0; i < WIDTH * HEIGHT; ++i) {
|
||||
Uint8 color_index = src[i];
|
||||
|
||||
// Verificación de seguridad: ¿El índice existe en la paleta?
|
||||
if (color_index < PAL_SIZE) {
|
||||
buffer[i] = palette_[color_index];
|
||||
} else {
|
||||
buffer[i] = 0xFF000000; // Negro opaco si el índice es erróneo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
// Bloquea la textura para modificar los píxeles directamente
|
||||
if (!SDL_LockTexture(texture, nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
// Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware)
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture); // Desbloquea la textura
|
||||
|
||||
// Renderiza la textura en la pantalla completa
|
||||
if (!SDL_RenderTexture(renderer, texture, nullptr, nullptr)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
SDL_Rect lock_rect;
|
||||
if (dest_rect != nullptr) {
|
||||
lock_rect.x = static_cast<int>(dest_rect->x);
|
||||
lock_rect.y = static_cast<int>(dest_rect->y);
|
||||
lock_rect.w = static_cast<int>(dest_rect->w);
|
||||
lock_rect.h = static_cast<int>(dest_rect->h);
|
||||
}
|
||||
|
||||
// Usa lockRect solo si destRect no es nulo
|
||||
if (!SDL_LockTexture(texture, (dest_rect != nullptr) ? &lock_rect : nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture);
|
||||
|
||||
// Renderiza la textura con los rectángulos especificados
|
||||
if (!SDL_RenderTexture(renderer, texture, src_rect, dest_rect)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta principal
|
||||
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verificar que el tamaño mínimo de palette_ sea adecuado
|
||||
static constexpr int PALETTE_SIZE = 19;
|
||||
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
palette_[i] = palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
palette_[1] = palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return palette_[15] == palette_[0];
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta secundaria
|
||||
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Variable estática para almacenar el último tick
|
||||
static Uint32 last_tick_ = 0;
|
||||
|
||||
// Obtener el tiempo actual
|
||||
Uint32 current_tick = SDL_GetTicks();
|
||||
|
||||
// Verificar si ha pasado el tiempo de retardo
|
||||
if (current_tick - last_tick_ < delay) {
|
||||
return false; // No se realiza el fade
|
||||
}
|
||||
|
||||
// Actualizar el último tick
|
||||
last_tick_ = current_tick;
|
||||
|
||||
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
|
||||
static constexpr int SUB_PALETTE_SIZE = 19;
|
||||
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
sub_palette_[i] = sub_palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
sub_palette_[1] = sub_palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return sub_palette_[15] == sub_palette_[0];
|
||||
}
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
|
||||
153
source/core/rendering/surface.hpp
Normal file
153
source/core/rendering/surface.hpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array> // Para array
|
||||
#include <memory> // Para default_delete, shared_ptr, __shared_pt...
|
||||
#include <numeric> // Para iota
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Alias
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
using SubPalette = std::array<Uint8, 256>;
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette;
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette;
|
||||
|
||||
struct SurfaceData {
|
||||
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
|
||||
float width; // Ancho de la imagen
|
||||
float height; // Alto de la imagen
|
||||
|
||||
// Constructor por defecto
|
||||
SurfaceData()
|
||||
: data(nullptr),
|
||||
width(0),
|
||||
height(0) {}
|
||||
|
||||
// Constructor que inicializa dimensiones y asigna memoria
|
||||
SurfaceData(float w, float h)
|
||||
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor para inicializar directamente con datos
|
||||
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
|
||||
: data(std::move(pixels)),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor de movimiento
|
||||
SurfaceData(SurfaceData&& other) noexcept = default;
|
||||
|
||||
// Operador de movimiento
|
||||
auto operator=(SurfaceData&& other) noexcept -> SurfaceData& = default;
|
||||
|
||||
// Evita copias accidentales
|
||||
SurfaceData(const SurfaceData&) = delete;
|
||||
auto operator=(const SurfaceData&) -> SurfaceData& = delete;
|
||||
};
|
||||
|
||||
class Surface {
|
||||
private:
|
||||
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
|
||||
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
|
||||
SubPalette sub_palette_; // Paleta para reindexar colores
|
||||
int transparent_color_; // Indice de la paleta que se omite en la copia de datos
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
Surface(int w, int h);
|
||||
explicit Surface(const std::string& file_path);
|
||||
|
||||
// Destructor
|
||||
~Surface() = default;
|
||||
|
||||
// Carga una SurfaceData desde un archivo
|
||||
static auto loadSurface(const std::string& file_path) -> SurfaceData;
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void loadPalette(const std::string& file_path);
|
||||
void loadPalette(const Palette& palette);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
|
||||
void render(float dx, float dy, float sx, float sy, float w, float h);
|
||||
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void renderWithColorReplace(int x, int y, Uint8 source_color = 0, Uint8 target_color = 0, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Idem però reemplaçant un color índex (per a sprites sobre fons del mateix color)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Establece un color en la paleta
|
||||
void setColor(int index, Uint32 color);
|
||||
|
||||
// Rellena la SurfaceData con un color
|
||||
void clear(Uint8 color);
|
||||
|
||||
// Vuelca la SurfaceData a una textura
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture);
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect);
|
||||
|
||||
// Realiza un efecto de fundido en las paletas
|
||||
auto fadePalette() -> bool;
|
||||
auto fadeSubPalette(Uint32 delay = 0) -> bool;
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void resetSubPalette();
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture)
|
||||
void toARGBBuffer(Uint32* buffer) const;
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void putPixel(int x, int y, Uint8 color);
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto getPixel(int x, int y) -> Uint8;
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void fillRect(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void drawRectBorder(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja una linea
|
||||
void drawLine(float x1, float y1, float x2, float y2, Uint8 color);
|
||||
|
||||
// Metodos para gestionar surface_data_
|
||||
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
|
||||
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
|
||||
|
||||
// Obtien ancho y alto
|
||||
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
|
||||
|
||||
// Color transparente
|
||||
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
|
||||
void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; }
|
||||
|
||||
// Paleta
|
||||
void setPalette(const std::array<Uint32, 256>& palette) { palette_ = palette; }
|
||||
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
|
||||
|
||||
// Inicializa la sub paleta
|
||||
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
|
||||
|
||||
private:
|
||||
// Helper para calcular coordenadas con flip
|
||||
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;
|
||||
};
|
||||
311
source/core/rendering/text.cpp
Normal file
311
source/core/rendering/text.cpp
Normal file
@@ -0,0 +1,311 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <iostream> // Para cerr
|
||||
#include <sstream> // Para istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "utils/utils.hpp" // Para getFileName, stringToColor, printWithDots
|
||||
|
||||
// Extrae el siguiente codepoint UTF-8 de la cadena, avanzando 'pos' al byte siguiente
|
||||
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto c = static_cast<unsigned char>(s[pos]);
|
||||
uint32_t cp = 0;
|
||||
size_t extra = 0;
|
||||
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
extra = 0;
|
||||
} else if (c < 0xC0) {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
} // byte de continuación suelto
|
||||
else if (c < 0xE0) {
|
||||
cp = c & 0x1F;
|
||||
extra = 1;
|
||||
} else if (c < 0xF0) {
|
||||
cp = c & 0x0F;
|
||||
extra = 2;
|
||||
} else if (c < 0xF8) {
|
||||
cp = c & 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
pos++;
|
||||
for (size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
||||
auto cb = static_cast<unsigned char>(s[pos]);
|
||||
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
||||
cp = (cp << 6) | (cb & 0x3F);
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
// Convierte un codepoint Unicode a una cadena UTF-8
|
||||
auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::string result;
|
||||
if (cp < 0x80) {
|
||||
result += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
result += static_cast<char>(0xC0 | (cp >> 6));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
result += static_cast<char>(0xE0 | (cp >> 12));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
result += static_cast<char>(0xF0 | (cp >> 18));
|
||||
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Carga un fichero de definición de fuente .fnt
|
||||
// Formato: líneas "clave valor", comentarios con #, gliphos como "codepoint ancho"
|
||||
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto tf = std::make_shared<File>();
|
||||
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
|
||||
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
|
||||
}
|
||||
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
if (line.empty() || line[0] == '#') { continue; }
|
||||
|
||||
std::istringstream ls(line);
|
||||
std::string key;
|
||||
ls >> key;
|
||||
|
||||
if (key == "box_width") {
|
||||
ls >> tf->box_width;
|
||||
} else if (key == "box_height") {
|
||||
ls >> tf->box_height;
|
||||
} else if (key == "columns") {
|
||||
ls >> tf->columns;
|
||||
} else if (key == "cell_spacing") {
|
||||
ls >> tf->cell_spacing;
|
||||
} else if (key == "row_spacing") {
|
||||
ls >> tf->row_spacing;
|
||||
} else {
|
||||
// Línea de glifo: codepoint_decimal ancho_visual
|
||||
uint32_t codepoint = 0;
|
||||
int width = 0;
|
||||
try {
|
||||
codepoint = static_cast<uint32_t>(std::stoul(key));
|
||||
ls >> width;
|
||||
} catch (...) {
|
||||
continue; // línea mal formateada, ignorar
|
||||
}
|
||||
Offset off{};
|
||||
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
||||
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
||||
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
||||
off.w = width;
|
||||
tf->offset[codepoint] = off;
|
||||
++glyph_index;
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||
return tf;
|
||||
}
|
||||
|
||||
// Constructor desde fichero
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
||||
auto tf = loadTextFile(text_file);
|
||||
|
||||
box_height_ = tf->box_height;
|
||||
box_width_ = tf->box_width;
|
||||
offset_ = tf->offset;
|
||||
|
||||
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
}
|
||||
|
||||
// Constructor desde estructura precargada
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
||||
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
||||
box_width_(text_file->box_width),
|
||||
box_height_(text_file->box_height),
|
||||
offset_(text_file->offset) {
|
||||
}
|
||||
|
||||
// Escribe texto en pantalla
|
||||
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, 15);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 width = length(text, kerning) * zoom;
|
||||
auto height = box_height_ * zoom;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
write(0, 0, text, kerning);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return 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 width = Text::length(text, kerning) + shadow_distance;
|
||||
auto height = box_height_ + shadow_distance;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
writeDX(flags, 0, 0, text, kerning, text_color, shadow_distance, shadow_color, lenght);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con colores
|
||||
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, color);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto con sombra
|
||||
void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int lenght) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, lenght);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe el texto centrado en un punto x
|
||||
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int lenght) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe texto con extras
|
||||
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
||||
const auto SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||
const auto COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||
const auto STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||
|
||||
if (CENTERED) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
}
|
||||
|
||||
if (SHADOWED) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
|
||||
if (STROKED) {
|
||||
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||
for (int dy = -dist; dy <= dist; ++dy) {
|
||||
for (int dx = -dist; dx <= dist; ++dx) {
|
||||
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (COLORED) {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
} else {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene la longitud en pixels de una cadena UTF-8
|
||||
auto Text::length(const std::string& text, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
}
|
||||
|
||||
return shift > 0 ? shift - kerning : 0;
|
||||
}
|
||||
|
||||
// Devuelve el ancho en pixels de un glifo dado su codepoint Unicode
|
||||
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) { return it->second.w + kerning; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Devuelve el clip rect (región en el bitmap) de un glifo dado su codepoint
|
||||
auto Text::getGlyphClip(uint32_t codepoint) const -> SDL_FRect {
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; }
|
||||
return {.x = static_cast<float>(it->second.x),
|
||||
.y = static_cast<float>(it->second.y),
|
||||
.w = static_cast<float>(box_width_),
|
||||
.h = static_cast<float>(box_height_)};
|
||||
}
|
||||
|
||||
// Devuelve el tamaño de la caja de cada caracter
|
||||
auto Text::getCharacterSize() const -> int {
|
||||
return box_width_;
|
||||
}
|
||||
|
||||
// Establece si se usa un tamaño fijo de letra
|
||||
void Text::setFixedWidth(bool value) {
|
||||
fixed_width_ = value;
|
||||
}
|
||||
72
source/core/rendering/text.hpp
Normal file
72
source/core/rendering/text.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr, unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // Forward declaration
|
||||
|
||||
// Clase texto. Pinta texto en pantalla a partir de un bitmap con soporte UTF-8
|
||||
class Text {
|
||||
public:
|
||||
// Tipos anidados públicos
|
||||
struct Offset {
|
||||
int x{0}, y{0}, w{0};
|
||||
};
|
||||
|
||||
struct File {
|
||||
int box_width{0}; // Anchura de la caja de cada caracter en el png
|
||||
int box_height{0}; // Altura de la caja de cada caracter en el png
|
||||
int columns{16}; // Número de columnas en el bitmap
|
||||
int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior)
|
||||
int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing)
|
||||
std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
|
||||
// Constructor
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
|
||||
|
||||
// Destructor
|
||||
~Text() = default;
|
||||
|
||||
// Constantes de flags para writeDX
|
||||
static constexpr int COLOR_FLAG = 1;
|
||||
static constexpr int SHADOW_FLAG = 2;
|
||||
static constexpr int CENTER_FLAG = 4;
|
||||
static constexpr int STROKE_FLAG = 8;
|
||||
|
||||
void write(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
|
||||
void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
|
||||
void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
|
||||
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
|
||||
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1); // Escribe texto con extras
|
||||
|
||||
auto writeToSurface(const std::string& text, int zoom = 1, int kerning = 1) -> std::shared_ptr<Surface>; // Escribe el texto en una textura
|
||||
auto writeDXToSurface(Uint8 flags, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1) -> std::shared_ptr<Surface>; // Escribe el texto con extras en una textura
|
||||
|
||||
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; // Obtiene la longitud en pixels de una cadena
|
||||
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el tamaño del caracter
|
||||
[[nodiscard]] auto glyphWidth(uint32_t codepoint, int kerning = 0) const -> int; // Devuelve el ancho en pixels de un glifo
|
||||
[[nodiscard]] auto getGlyphClip(uint32_t codepoint) const -> SDL_FRect; // Devuelve el clip rect del glifo
|
||||
[[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } // Acceso al sprite interno
|
||||
|
||||
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
|
||||
|
||||
static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>; // Carga un fichero de definición de fuente .fnt
|
||||
static auto codepointToUtf8(uint32_t cp) -> std::string; // Convierte un codepoint Unicode a string UTF-8
|
||||
static auto nextCodepoint(const std::string& s, size_t& pos) -> uint32_t; // Extrae el siguiente codepoint UTF-8
|
||||
|
||||
private:
|
||||
// Objetos y punteros
|
||||
std::unique_ptr<Sprite> sprite_ = nullptr; // Objeto con los graficos para el texto
|
||||
|
||||
// Variables
|
||||
int box_width_ = 0; // Anchura de la caja de cada caracter en el png
|
||||
int box_height_ = 0; // Altura de la caja de cada caracter en el png
|
||||
bool fixed_width_ = false; // Indica si el texto se ha de escribir con longitud fija
|
||||
std::unordered_map<uint32_t, Offset> offset_; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
533
source/core/resources/resource_cache.cpp
Normal file
533
source/core/resources/resource_cache.cpp
Normal file
@@ -0,0 +1,533 @@
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para find_if
|
||||
#include <cstdlib> // Para exit, size_t
|
||||
#include <fstream> // Para ifstream, istreambuf_iterator
|
||||
#include <iostream> // Para basic_ostream, operator<<, endl, cout
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_DeleteMusic, JA_DeleteSound, JA_Loa...
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/text.hpp" // Para Text, loadTextFile
|
||||
#include "core/resources/resource_helper.hpp" // Para Helper
|
||||
#include "core/resources/resource_list.hpp" // Para List, List::Type
|
||||
#include "game/defaults.hpp" // Para Defaults namespace
|
||||
#include "game/gameplay/room.hpp" // Para RoomData, loadRoomFile, loadRoomTileFile
|
||||
#include "game/gameplay/room_loader.hpp" // Para RoomLoader::loadFromString
|
||||
#include "game/options.hpp" // Para Options, OptionsGame, options
|
||||
#include "utils/defines.hpp" // Para WINDOW_CAPTION
|
||||
#include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor
|
||||
#include "version.h" // Para Version::GIT_HASH
|
||||
struct JA_Music_t; // lines 17-17
|
||||
struct JA_Sound_t; // lines 18-18
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// [SINGLETON] Hay que definir las variables estáticas, desde el .h sólo la hemos declarado
|
||||
Cache* Cache::cache = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto cache con esta función estática
|
||||
void Cache::init() { Cache::cache = new Cache(); }
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto cache con esta función estática
|
||||
void Cache::destroy() { delete Cache::cache; }
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él
|
||||
auto Cache::get() -> Cache* { return Cache::cache; }
|
||||
|
||||
// Constructor
|
||||
Cache::Cache()
|
||||
: loading_text_(Screen::get()->getText()) {
|
||||
load();
|
||||
}
|
||||
|
||||
// Vacia todos los vectores de recursos
|
||||
void Cache::clear() {
|
||||
clearSounds();
|
||||
clearMusics();
|
||||
surfaces_.clear();
|
||||
palettes_.clear();
|
||||
text_files_.clear();
|
||||
texts_.clear();
|
||||
animations_.clear();
|
||||
}
|
||||
|
||||
// Carga todos los recursos
|
||||
void Cache::load() {
|
||||
// Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga,
|
||||
// por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init().
|
||||
calculateTotal();
|
||||
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
std::cout << "\n** LOADING RESOURCES" << '\n';
|
||||
loadSounds();
|
||||
loadMusics();
|
||||
loadSurfaces();
|
||||
loadPalettes();
|
||||
loadTextFiles();
|
||||
loadAnimations();
|
||||
loadRooms();
|
||||
createText();
|
||||
std::cout << "\n** RESOURCES LOADED" << '\n';
|
||||
}
|
||||
|
||||
// Recarga todos los recursos
|
||||
void Cache::reload() {
|
||||
clear();
|
||||
load();
|
||||
}
|
||||
|
||||
// Obtiene el sonido a partir de un nombre
|
||||
auto Cache::getSound(const std::string& name) -> JA_Sound_t* { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> bool { return s.name == name; });
|
||||
|
||||
if (it != sounds_.end()) {
|
||||
return it->sound;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Sonido no encontrado " << name << '\n';
|
||||
throw std::runtime_error("Sonido no encontrado: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la música a partir de un nombre
|
||||
auto Cache::getMusic(const std::string& name) -> JA_Music_t* { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> bool { return m.name == name; });
|
||||
|
||||
if (it != musics_.end()) {
|
||||
return it->music;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Música no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Música no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la surface a partir de un nombre
|
||||
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(surfaces_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != surfaces_.end()) {
|
||||
return it->surface;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Imagen no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Imagen no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la paleta a partir de un nombre
|
||||
auto Cache::getPalette(const std::string& name) -> Palette { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(palettes_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != palettes_.end()) {
|
||||
return it->palette;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Paleta no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Paleta no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene el fichero de texto a partir de un nombre
|
||||
auto Cache::getTextFile(const std::string& name) -> std::shared_ptr<Text::File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != text_files_.end()) {
|
||||
return it->text_file;
|
||||
}
|
||||
|
||||
std::cerr << "Error: TextFile no encontrado " << name << '\n';
|
||||
throw std::runtime_error("TextFile no encontrado: " + name);
|
||||
}
|
||||
|
||||
// Obtiene el objeto de texto a partir de un nombre
|
||||
auto Cache::getText(const std::string& name) -> std::shared_ptr<Text> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(texts_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != texts_.end()) {
|
||||
return it->text;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Text no encontrado " << name << '\n';
|
||||
throw std::runtime_error("Texto no encontrado: " + name);
|
||||
}
|
||||
|
||||
// Obtiene los datos de animación parseados a partir de un nombre
|
||||
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> bool { return a.name == name; });
|
||||
|
||||
if (it != animations_.end()) {
|
||||
return *it;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Animación no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Animación no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la habitación a partir de un nombre
|
||||
auto Cache::getRoom(const std::string& name) -> std::shared_ptr<Room::Data> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; });
|
||||
|
||||
if (it != rooms_.end()) {
|
||||
return it->room;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Habitación no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Habitación no encontrada: " + name);
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Recarga una habitación desde disco (para el editor de mapas)
|
||||
// Lee directamente del filesystem (no del resource pack) para obtener los cambios del editor
|
||||
void Cache::reloadRoom(const std::string& name) {
|
||||
auto file_path = List::get()->get(name);
|
||||
if (file_path.empty()) {
|
||||
std::cerr << "reloadRoom: Cannot resolve path for " << name << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Leer directamente del filesystem (evita el resource pack que tiene datos antiguos)
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "reloadRoom: Cannot open " << file_path << '\n';
|
||||
return;
|
||||
}
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
// Parsear y actualizar el cache
|
||||
auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; });
|
||||
if (it != rooms_.end()) {
|
||||
*(it->room) = RoomLoader::loadFromString(content, name);
|
||||
std::cout << "reloadRoom: " << name << " reloaded from filesystem\n";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Obtiene todas las habitaciones
|
||||
auto Cache::getRooms() -> std::vector<RoomResource>& {
|
||||
return rooms_;
|
||||
}
|
||||
|
||||
// Helper para lanzar errores de carga con formato consistente
|
||||
[[noreturn]] void Cache::throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cerr << "\n[ ERROR ] Failed to load " << asset_type << ": " << getFileName(file_path) << '\n';
|
||||
std::cerr << "[ ERROR ] Path: " << file_path << '\n';
|
||||
std::cerr << "[ ERROR ] Reason: " << e.what() << '\n';
|
||||
std::cerr << "[ ERROR ] Check config/assets.yaml configuration\n";
|
||||
throw;
|
||||
}
|
||||
|
||||
// Carga los sonidos
|
||||
void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> SOUND FILES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::SOUND);
|
||||
sounds_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
JA_Sound_t* sound = nullptr;
|
||||
|
||||
// Try loading from resource pack first
|
||||
auto audio_data = Helper::loadFile(l);
|
||||
if (!audio_data.empty()) {
|
||||
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
|
||||
}
|
||||
|
||||
// Fallback to file path if memory loading failed
|
||||
if (sound == nullptr) {
|
||||
sound = JA_LoadSound(l.c_str());
|
||||
}
|
||||
|
||||
if (sound == nullptr) {
|
||||
throw std::runtime_error("Failed to decode audio file");
|
||||
}
|
||||
|
||||
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
|
||||
printWithDots("Sound : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("SOUND", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las musicas
|
||||
void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> MUSIC FILES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::MUSIC);
|
||||
musics_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
JA_Music_t* music = nullptr;
|
||||
|
||||
// Try loading from resource pack first
|
||||
auto audio_data = Helper::loadFile(l);
|
||||
if (!audio_data.empty()) {
|
||||
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
|
||||
}
|
||||
|
||||
// Fallback to file path if memory loading failed
|
||||
if (music == nullptr) {
|
||||
music = JA_LoadMusic(l.c_str());
|
||||
}
|
||||
|
||||
if (music == nullptr) {
|
||||
throw std::runtime_error("Failed to decode music file");
|
||||
}
|
||||
|
||||
musics_.emplace_back(MusicResource{.name = name, .music = music});
|
||||
printWithDots("Music : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress(1);
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("MUSIC", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las texturas
|
||||
void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> SURFACES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::BITMAP);
|
||||
surfaces_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
|
||||
surfaces_.back().surface->setTransparentColor(0);
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("BITMAP", l, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconfigura el color transparente de algunas surfaces
|
||||
getSurface("loading_screen_color.gif")->setTransparentColor();
|
||||
getSurface("ending1.gif")->setTransparentColor();
|
||||
getSurface("ending2.gif")->setTransparentColor();
|
||||
getSurface("ending3.gif")->setTransparentColor();
|
||||
getSurface("ending4.gif")->setTransparentColor();
|
||||
getSurface("ending5.gif")->setTransparentColor();
|
||||
getSurface("standard.gif")->setTransparentColor(16);
|
||||
}
|
||||
|
||||
// Carga las paletas
|
||||
void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> PALETTES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::PALETTE);
|
||||
palettes_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("PALETTE", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga los ficheros de texto
|
||||
void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> TEXT FILES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::FONT);
|
||||
text_files_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("FONT", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las animaciones
|
||||
void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> ANIMATIONS" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::ANIMATION);
|
||||
animations_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
|
||||
// Cargar bytes del archivo YAML sin parsear (carga lazy)
|
||||
auto yaml_bytes = Helper::loadFile(l);
|
||||
|
||||
if (yaml_bytes.empty()) {
|
||||
throw std::runtime_error("File is empty or could not be loaded");
|
||||
}
|
||||
|
||||
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
|
||||
printWithDots("Animation : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ANIMATION", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las habitaciones desde archivos YAML
|
||||
void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> ROOMS" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::ROOM);
|
||||
rooms_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
|
||||
printWithDots("Room : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ROOM", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
struct ResourceInfo {
|
||||
std::string key; // Identificador del recurso
|
||||
std::string texture_file; // Nombre del archivo de textura
|
||||
std::string text_file; // Nombre del archivo de texto
|
||||
};
|
||||
|
||||
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
|
||||
|
||||
std::vector<ResourceInfo> resources = {
|
||||
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
|
||||
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
|
||||
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
|
||||
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
|
||||
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
|
||||
|
||||
for (const auto& res_info : resources) {
|
||||
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
|
||||
printWithDots("Text : ", res_info.key, "[ DONE ]");
|
||||
}
|
||||
}
|
||||
|
||||
// Vacía el vector de sonidos
|
||||
void Cache::clearSounds() {
|
||||
// Itera sobre el vector y libera los recursos asociados a cada JA_Sound_t
|
||||
for (auto& sound : sounds_) {
|
||||
if (sound.sound != nullptr) {
|
||||
JA_DeleteSound(sound.sound);
|
||||
sound.sound = nullptr;
|
||||
}
|
||||
}
|
||||
sounds_.clear(); // Limpia el vector después de liberar todos los recursos
|
||||
}
|
||||
|
||||
// Vacía el vector de musicas
|
||||
void Cache::clearMusics() {
|
||||
// Itera sobre el vector y libera los recursos asociados a cada JA_Music_t
|
||||
for (auto& music : musics_) {
|
||||
if (music.music != nullptr) {
|
||||
JA_DeleteMusic(music.music);
|
||||
music.music = nullptr;
|
||||
}
|
||||
}
|
||||
musics_.clear(); // Limpia el vector después de liberar todos los recursos
|
||||
}
|
||||
|
||||
// Calcula el numero de recursos para cargar
|
||||
void Cache::calculateTotal() {
|
||||
std::vector<List::Type> asset_types = {
|
||||
List::Type::SOUND,
|
||||
List::Type::MUSIC,
|
||||
List::Type::BITMAP,
|
||||
List::Type::PALETTE,
|
||||
List::Type::FONT,
|
||||
List::Type::ANIMATION,
|
||||
List::Type::ROOM};
|
||||
|
||||
int total = 0;
|
||||
for (const auto& asset_type : asset_types) {
|
||||
auto list = List::get()->getListByType(asset_type);
|
||||
total += list.size();
|
||||
}
|
||||
|
||||
count_ = ResourceCount{.total = total, .loaded = 0};
|
||||
}
|
||||
|
||||
// Muestra el progreso de carga
|
||||
void Cache::renderProgress() {
|
||||
constexpr float X_PADDING = 60.0F;
|
||||
constexpr float Y_PADDING = 10.0F;
|
||||
constexpr float BAR_HEIGHT = 5.0F;
|
||||
|
||||
const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING;
|
||||
Screen::get()->start();
|
||||
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
|
||||
auto surface = Screen::get()->getRendererSurface();
|
||||
const auto LOADING_TEXT_COLOR = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE);
|
||||
const auto BAR_COLOR = static_cast<Uint8>(PaletteColor::WHITE);
|
||||
const int TEXT_HEIGHT = loading_text_->getCharacterSize();
|
||||
const int CENTER_X = Options::game.width / 2;
|
||||
const int CENTER_Y = Options::game.height / 2;
|
||||
|
||||
// Draw APP_NAME centered above center
|
||||
const std::string APP_NAME = spaceBetweenLetters(Version::APP_NAME);
|
||||
loading_text_->writeColored(
|
||||
CENTER_X - (loading_text_->length(APP_NAME) / 2),
|
||||
CENTER_Y - TEXT_HEIGHT,
|
||||
APP_NAME,
|
||||
LOADING_TEXT_COLOR);
|
||||
|
||||
// Draw VERSION centered below center
|
||||
const std::string VERSION_TEXT = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")";
|
||||
loading_text_->writeColored(
|
||||
CENTER_X - (loading_text_->length(VERSION_TEXT) / 2),
|
||||
CENTER_Y + TEXT_HEIGHT,
|
||||
VERSION_TEXT,
|
||||
LOADING_TEXT_COLOR);
|
||||
|
||||
// Draw progress bar border
|
||||
const float WIRED_BAR_WIDTH = Options::game.width - (X_PADDING * 2);
|
||||
SDL_FRect rect_wired = {.x = X_PADDING, .y = BAR_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
surface->drawRectBorder(&rect_wired, BAR_COLOR);
|
||||
|
||||
// Draw progress bar fill
|
||||
const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * count_.getPercentage();
|
||||
SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
surface->fillRect(&rect_full, BAR_COLOR);
|
||||
|
||||
Screen::get()->render();
|
||||
}
|
||||
|
||||
// Comprueba los eventos de la pantalla de carga
|
||||
void Cache::checkEvents() {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_QUIT:
|
||||
exit(0);
|
||||
break;
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
if (event.key.key == SDLK_ESCAPE) {
|
||||
exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el progreso de carga
|
||||
void Cache::updateLoadingProgress(int steps) {
|
||||
count_.add(1);
|
||||
if (count_.loaded % steps == 0 || count_.loaded == count_.total) {
|
||||
renderProgress();
|
||||
}
|
||||
checkEvents();
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
96
source/core/resources/resource_cache.hpp
Normal file
96
source/core/resources/resource_cache.hpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/resources/resource_types.hpp" // Para structs de recursos
|
||||
|
||||
namespace Resource {
|
||||
|
||||
class Cache {
|
||||
public:
|
||||
static void init(); // Inicialización singleton
|
||||
static void destroy(); // Destrucción singleton
|
||||
static auto get() -> Cache*; // Acceso al singleton
|
||||
|
||||
auto getSound(const std::string& name) -> JA_Sound_t*; // Getters de recursos
|
||||
auto getMusic(const std::string& name) -> JA_Music_t*;
|
||||
auto getSurface(const std::string& name) -> std::shared_ptr<Surface>;
|
||||
auto getPalette(const std::string& name) -> Palette;
|
||||
auto getTextFile(const std::string& name) -> std::shared_ptr<Text::File>;
|
||||
auto getText(const std::string& name) -> std::shared_ptr<Text>;
|
||||
auto getAnimationData(const std::string& name) -> const AnimationResource&;
|
||||
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
|
||||
auto getRooms() -> std::vector<RoomResource>&;
|
||||
|
||||
void reload(); // Recarga todos los recursos
|
||||
#ifdef _DEBUG
|
||||
void reloadRoom(const std::string& name); // Recarga una habitación desde disco
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Estructura para llevar la cuenta de los recursos cargados
|
||||
struct ResourceCount {
|
||||
int total{0}; // Número total de recursos
|
||||
int loaded{0}; // Número de recursos cargados
|
||||
|
||||
// Añade una cantidad a los recursos cargados
|
||||
void add(int amount) {
|
||||
loaded += amount;
|
||||
}
|
||||
|
||||
// Obtiene el porcentaje de recursos cargados
|
||||
[[nodiscard]] auto getPercentage() const -> float {
|
||||
return static_cast<float>(loaded) / static_cast<float>(total);
|
||||
}
|
||||
};
|
||||
|
||||
// Métodos de carga de recursos
|
||||
void loadSounds();
|
||||
void loadMusics();
|
||||
void loadSurfaces();
|
||||
void loadPalettes();
|
||||
void loadTextFiles();
|
||||
void loadAnimations();
|
||||
void loadRooms();
|
||||
void createText();
|
||||
|
||||
// Métodos de limpieza
|
||||
void clear();
|
||||
void clearSounds();
|
||||
void clearMusics();
|
||||
|
||||
// Métodos de gestión de carga
|
||||
void load();
|
||||
void calculateTotal();
|
||||
void renderProgress();
|
||||
static void checkEvents();
|
||||
void updateLoadingProgress(int steps = 5);
|
||||
|
||||
// Helper para mensajes de error de carga
|
||||
[[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
|
||||
|
||||
// Constructor y destructor
|
||||
Cache();
|
||||
~Cache() = default;
|
||||
|
||||
// Singleton instance
|
||||
static Cache* cache;
|
||||
|
||||
// Variables miembro
|
||||
std::vector<SoundResource> sounds_; // Vector con los sonidos
|
||||
std::vector<MusicResource> musics_; // Vector con las musicas
|
||||
std::vector<SurfaceResource> surfaces_; // Vector con las surfaces
|
||||
std::vector<ResourcePalette> palettes_; // Vector con las paletas
|
||||
std::vector<TextFileResource> text_files_; // Vector con los ficheros de texto
|
||||
std::vector<TextResource> texts_; // Vector con los objetos de texto
|
||||
std::vector<AnimationResource> animations_; // Vector con las animaciones
|
||||
std::vector<RoomResource> rooms_; // Vector con las habitaciones
|
||||
|
||||
ResourceCount count_{}; // Contador de recursos
|
||||
std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
182
source/core/resources/resource_helper.cpp
Normal file
182
source/core/resources/resource_helper.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
// resource_helper.cpp
|
||||
// Resource helper implementation
|
||||
|
||||
#include "resource_helper.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
static bool resource_system_initialized = false;
|
||||
|
||||
// Initialize the resource system
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback)
|
||||
-> bool {
|
||||
if (resource_system_initialized) {
|
||||
std::cout << "ResourceHelper: Already initialized\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cout << "ResourceHelper: Initializing with pack: " << pack_file << '\n';
|
||||
std::cout << "ResourceHelper: Fallback enabled: " << (enable_fallback ? "Yes" : "No")
|
||||
<< '\n';
|
||||
|
||||
bool success = Loader::get().initialize(pack_file, enable_fallback);
|
||||
if (success) {
|
||||
resource_system_initialized = true;
|
||||
std::cout << "ResourceHelper: Initialization successful\n";
|
||||
} else {
|
||||
std::cerr << "ResourceHelper: Initialization failed\n";
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Shutdown the resource system
|
||||
void shutdownResourceSystem() {
|
||||
if (resource_system_initialized) {
|
||||
Loader::get().shutdown();
|
||||
resource_system_initialized = false;
|
||||
std::cout << "ResourceHelper: Shutdown complete\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Load a file
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
if (!resource_system_initialized) {
|
||||
std::cerr << "ResourceHelper: System not initialized, loading from filesystem\n";
|
||||
// Fallback to direct filesystem access
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
std::streamsize file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> data(file_size);
|
||||
file.read(reinterpret_cast<char*>(data.data()), file_size);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Determine if we should use the pack
|
||||
if (shouldUseResourcePack(filepath)) {
|
||||
// Convert to pack path
|
||||
std::string pack_path = getPackPath(filepath);
|
||||
|
||||
// Try to load from pack
|
||||
auto data = Loader::get().loadResource(pack_path);
|
||||
if (!data.empty()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// If pack loading failed, try filesystem as fallback
|
||||
std::cerr << "ResourceHelper: Pack failed for " << pack_path
|
||||
<< ", trying filesystem\n";
|
||||
}
|
||||
|
||||
// Load from filesystem
|
||||
return Loader::get().loadResource(filepath);
|
||||
}
|
||||
|
||||
// Check if a file exists
|
||||
auto fileExists(const std::string& filepath) -> bool {
|
||||
if (!resource_system_initialized) {
|
||||
return std::filesystem::exists(filepath);
|
||||
}
|
||||
|
||||
// Check pack if appropriate
|
||||
if (shouldUseResourcePack(filepath)) {
|
||||
std::string pack_path = getPackPath(filepath);
|
||||
if (Loader::get().resourceExists(pack_path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check filesystem
|
||||
return std::filesystem::exists(filepath);
|
||||
}
|
||||
|
||||
// Convert asset path to pack path
|
||||
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||
std::string path = asset_path;
|
||||
|
||||
// Convert backslashes to forward slashes
|
||||
std::ranges::replace(path, '\\', '/');
|
||||
|
||||
// If it's an absolute path containing "/data/", extract everything after "/data/"
|
||||
// This handles paths like: /Users/sergio/.../data/palette/file.pal -> palette/file.pal
|
||||
size_t data_pos = path.find("/data/");
|
||||
if (data_pos != std::string::npos) {
|
||||
return path.substr(data_pos + 6); // +6 to skip "/data/"
|
||||
}
|
||||
|
||||
// Remove leading slashes
|
||||
while (!path.empty() && path[0] == '/') {
|
||||
path = path.substr(1);
|
||||
}
|
||||
|
||||
// Remove "./" prefix if present
|
||||
if (path.starts_with("./")) {
|
||||
path = path.substr(2);
|
||||
}
|
||||
|
||||
// Remove "../" prefixes (for macOS bundle paths in development)
|
||||
while (path.starts_with("../")) {
|
||||
path = path.substr(3);
|
||||
}
|
||||
|
||||
// Remove "Resources/" prefix if present (for macOS bundle)
|
||||
const std::string RESOURCES_PREFIX = "Resources/";
|
||||
if (path.starts_with(RESOURCES_PREFIX)) {
|
||||
path = path.substr(RESOURCES_PREFIX.length());
|
||||
}
|
||||
|
||||
// Remove "data/" prefix if present
|
||||
const std::string DATA_PREFIX = "data/";
|
||||
if (path.starts_with(DATA_PREFIX)) {
|
||||
path = path.substr(DATA_PREFIX.length());
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// Check if file should use resource pack
|
||||
auto shouldUseResourcePack(const std::string& filepath) -> bool {
|
||||
std::string path = filepath;
|
||||
std::ranges::replace(path, '\\', '/');
|
||||
|
||||
// Don't use pack for most config files (except config/assets.yaml which is loaded
|
||||
// directly via Loader::loadAssetsConfig() in release builds)
|
||||
if (path.find("config/") != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use pack for data files
|
||||
if (path.find("data/") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it looks like a data file (has common extensions)
|
||||
if (path.find(".ogg") != std::string::npos || path.find(".wav") != std::string::npos ||
|
||||
path.find(".gif") != std::string::npos || path.find(".png") != std::string::npos ||
|
||||
path.find(".pal") != std::string::npos || path.find(".yaml") != std::string::npos ||
|
||||
path.find(".txt") != std::string::npos || path.find(".glsl") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if pack is loaded
|
||||
auto isPackLoaded() -> bool {
|
||||
if (!resource_system_initialized) {
|
||||
return false;
|
||||
}
|
||||
return Loader::get().isPackLoaded();
|
||||
}
|
||||
|
||||
} // namespace Resource::Helper
|
||||
38
source/core/resources/resource_helper.hpp
Normal file
38
source/core/resources/resource_helper.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// resource_helper.hpp
|
||||
// Helper functions for resource loading (bridge to pack system)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
// Initialize the resource system
|
||||
// pack_file: Path to resources.pack
|
||||
// enable_fallback: Allow loading from filesystem if pack not available
|
||||
auto initializeResourceSystem(const std::string& pack_file = "resources.pack",
|
||||
bool enable_fallback = true) -> bool;
|
||||
|
||||
// Shutdown the resource system
|
||||
void shutdownResourceSystem();
|
||||
|
||||
// Load a file (tries pack first, then filesystem if fallback enabled)
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
||||
|
||||
// Check if a file exists
|
||||
auto fileExists(const std::string& filepath) -> bool;
|
||||
|
||||
// Convert an asset path to a pack path
|
||||
// Example: "data/music/title.ogg" -> "music/title.ogg"
|
||||
auto getPackPath(const std::string& asset_path) -> std::string;
|
||||
|
||||
// Check if a file should use the resource pack
|
||||
// Returns false for config/ files (always from filesystem)
|
||||
auto shouldUseResourcePack(const std::string& filepath) -> bool;
|
||||
|
||||
// Check if pack is loaded
|
||||
auto isPackLoaded() -> bool;
|
||||
|
||||
} // namespace Resource::Helper
|
||||
446
source/core/resources/resource_list.cpp
Normal file
446
source/core/resources/resource_list.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "core/resources/resource_list.hpp"
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_LogWarn, SDL_LogCategory, SDL_LogError
|
||||
|
||||
#include <algorithm> // Para sort
|
||||
#include <cstddef> // Para size_t
|
||||
#include <exception> // Para exception
|
||||
#include <filesystem> // Para exists, path
|
||||
#include <fstream> // Para ifstream, istringstream
|
||||
#include <iostream> // Para cout
|
||||
#include <sstream> // Para istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para parsear YAML
|
||||
#include "utils/utils.hpp" // Para getFileName, printWithDots
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Singleton
|
||||
List* List::instance = nullptr;
|
||||
|
||||
void List::init(const std::string& executable_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
List::instance = new List(executable_path);
|
||||
}
|
||||
|
||||
void List::destroy() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
delete List::instance;
|
||||
}
|
||||
|
||||
auto List::get() -> List* {
|
||||
return List::instance;
|
||||
}
|
||||
|
||||
// Añade un elemento al mapa (función auxiliar)
|
||||
void List::addToMap(const std::string& file_path, Type type, bool required, bool absolute) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::string full_path = absolute ? file_path : executable_path_ + file_path;
|
||||
std::string filename = getFileName(full_path);
|
||||
|
||||
// Verificar si ya existe el archivo
|
||||
if (file_list_.contains(filename)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Asset '%s' already exists, overwriting",
|
||||
filename.c_str());
|
||||
}
|
||||
|
||||
file_list_.emplace(filename, Item{std::move(full_path), type, required});
|
||||
}
|
||||
|
||||
// Añade un elemento a la lista
|
||||
void List::add(const std::string& file_path, Type type, bool required, bool absolute) {
|
||||
addToMap(file_path, type, required, absolute);
|
||||
}
|
||||
|
||||
// Añade un asset al mapa y lo persiste en assets.yaml
|
||||
void List::addAsset(const std::string& path, Type type) {
|
||||
// Añadir al mapa en memoria
|
||||
addToMap(path, type, true, true);
|
||||
|
||||
// Persistir en assets.yaml
|
||||
if (config_file_path_.empty()) { return; }
|
||||
|
||||
std::ifstream in(config_file_path_);
|
||||
if (!in.is_open()) { return; }
|
||||
std::string content((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
in.close();
|
||||
|
||||
// Construir la ruta con variable ${PREFIX} (invertir la sustitución)
|
||||
std::string var_path = path;
|
||||
if (!prefix_.empty() && !executable_path_.empty()) {
|
||||
std::string full_prefix = executable_path_ + prefix_;
|
||||
auto pos = var_path.find(full_prefix);
|
||||
if (pos != std::string::npos) {
|
||||
var_path.replace(pos, full_prefix.length(), "${PREFIX}");
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar la última entrada con el mismo prefijo de ruta e insertar después
|
||||
std::string entry = " - " + var_path + "\n";
|
||||
auto last_pos = content.rfind(var_path.substr(0, var_path.rfind('/')));
|
||||
if (last_pos != std::string::npos) {
|
||||
auto end_of_line = content.find('\n', last_pos);
|
||||
if (end_of_line != std::string::npos) {
|
||||
content.insert(end_of_line + 1, entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream out(config_file_path_);
|
||||
if (out.is_open()) {
|
||||
out << content;
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Quita un asset del mapa y lo elimina de assets.yaml
|
||||
void List::removeAsset(const std::string& filename) {
|
||||
// Obtener la ruta antes de borrar del mapa
|
||||
auto it = file_list_.find(filename);
|
||||
std::string file_path;
|
||||
if (it != file_list_.end()) {
|
||||
file_path = it->second.file;
|
||||
file_list_.erase(it);
|
||||
}
|
||||
|
||||
// Persistir en assets.yaml
|
||||
if (config_file_path_.empty() || file_path.empty()) { return; }
|
||||
|
||||
std::ifstream in(config_file_path_);
|
||||
if (!in.is_open()) { return; }
|
||||
std::string content((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
in.close();
|
||||
|
||||
// Construir la ruta con variable ${PREFIX}
|
||||
std::string var_path = file_path;
|
||||
if (!prefix_.empty() && !executable_path_.empty()) {
|
||||
std::string full_prefix = executable_path_ + prefix_;
|
||||
auto pos = var_path.find(full_prefix);
|
||||
if (pos != std::string::npos) {
|
||||
var_path.replace(pos, full_prefix.length(), "${PREFIX}");
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar la línea con el path y eliminarla
|
||||
auto pos = content.find(var_path);
|
||||
if (pos != std::string::npos) {
|
||||
auto line_start = content.rfind('\n', pos);
|
||||
line_start = (line_start == std::string::npos) ? 0 : line_start;
|
||||
auto line_end = content.find('\n', pos);
|
||||
if (line_end != std::string::npos) {
|
||||
content.erase(line_start, line_end - line_start);
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream out(config_file_path_);
|
||||
if (out.is_open()) {
|
||||
out << content;
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Carga recursos desde un archivo de configuración con soporte para variables
|
||||
void List::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
config_file_path_ = config_file_path;
|
||||
prefix_ = prefix;
|
||||
|
||||
std::ifstream file(config_file_path);
|
||||
if (!file.is_open()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error: Cannot open config file: %s",
|
||||
config_file_path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Read entire file into string
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
file.close();
|
||||
|
||||
// Parse using loadFromString
|
||||
loadFromString(buffer.str(), prefix, system_folder);
|
||||
}
|
||||
|
||||
// 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)
|
||||
try {
|
||||
// Parsear YAML
|
||||
auto yaml = fkyaml::node::deserialize(config_content);
|
||||
|
||||
// Verificar estructura básica
|
||||
if (!yaml.contains("assets")) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid assets.yaml format - missing 'assets' key");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& assets = yaml["assets"];
|
||||
|
||||
// Iterar sobre cada categoría (fonts, palettes, etc.)
|
||||
for (auto it = assets.begin(); it != assets.end(); ++it) {
|
||||
const std::string& category = it.key().get_value<std::string>();
|
||||
const auto& category_assets = it.value();
|
||||
|
||||
if (category_assets.is_mapping()) {
|
||||
// Nuevo formato: categoría → { TIPO: [paths...], TIPO2: [paths...] }
|
||||
for (auto type_it = category_assets.begin(); type_it != category_assets.end(); ++type_it) {
|
||||
try {
|
||||
auto type_str = type_it.key().get_value<std::string>();
|
||||
Type type = parseAssetType(type_str);
|
||||
const auto& items = type_it.value();
|
||||
|
||||
if (!items.is_sequence()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Type '%s' in category '%s' is not a sequence, skipping",
|
||||
type_str.c_str(),
|
||||
category.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& item : items) {
|
||||
try {
|
||||
if (item.is_string()) {
|
||||
// Formato simple: solo el path
|
||||
auto path = replaceVariables(item.get_value<std::string>(), prefix, system_folder);
|
||||
addToMap(path, type, true, false);
|
||||
} else if (item.is_mapping() && item.contains("path")) {
|
||||
// Formato expandido: { path, required?, absolute? }
|
||||
auto path = replaceVariables(item["path"].get_value<std::string>(), prefix, system_folder);
|
||||
bool required = !item.contains("required") || item["required"].get_value<bool>();
|
||||
bool absolute = item.contains("absolute") && item["absolute"].get_value<bool>();
|
||||
addToMap(path, type, required, absolute);
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Invalid item in type '%s', category '%s', skipping",
|
||||
type_str.c_str(),
|
||||
category.c_str());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error parsing asset in category '%s', type '%s': %s",
|
||||
category.c_str(),
|
||||
type_str.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error parsing type in category '%s': %s",
|
||||
category.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
} else if (category_assets.is_sequence()) {
|
||||
// Formato antiguo (retrocompatibilidad): categoría → [{type, path}, ...]
|
||||
for (const auto& asset : category_assets) {
|
||||
try {
|
||||
if (!asset.contains("type") || !asset.contains("path")) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Asset in category '%s' missing 'type' or 'path', skipping",
|
||||
category.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto type_str = asset["type"].get_value<std::string>();
|
||||
auto path = asset["path"].get_value<std::string>();
|
||||
bool required = true;
|
||||
bool absolute = false;
|
||||
|
||||
if (asset.contains("required")) {
|
||||
required = asset["required"].get_value<bool>();
|
||||
}
|
||||
if (asset.contains("absolute")) {
|
||||
absolute = asset["absolute"].get_value<bool>();
|
||||
}
|
||||
|
||||
path = replaceVariables(path, prefix, system_folder);
|
||||
Type type = parseAssetType(type_str);
|
||||
addToMap(path, type, required, absolute);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error parsing asset in category '%s': %s",
|
||||
category.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Category '%s' has invalid format, skipping",
|
||||
category.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Loaded " << file_list_.size() << " assets from YAML config" << '\n';
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"YAML parsing error: %s",
|
||||
e.what());
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading assets: %s",
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve la ruta completa a un fichero (búsqueda O(1))
|
||||
auto List::get(const std::string& filename) const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = file_list_.find(filename);
|
||||
if (it != file_list_.end()) {
|
||||
return it->second.file;
|
||||
}
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: file %s not found", filename.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
// Carga datos del archivo
|
||||
auto List::loadData(const std::string& filename) const -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = file_list_.find(filename);
|
||||
if (it != file_list_.end()) {
|
||||
std::ifstream file(it->second.file, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Could not open file %s for data loading",
|
||||
filename.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Obtener tamaño del archivo
|
||||
file.seekg(0, std::ios::end);
|
||||
size_t size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Leer datos
|
||||
std::vector<uint8_t> data(size);
|
||||
file.read(reinterpret_cast<char*>(data.data()), size);
|
||||
file.close();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: file %s not found for data loading", filename.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Verifica si un recurso existe
|
||||
auto List::exists(const std::string& filename) const -> bool {
|
||||
return file_list_.contains(filename);
|
||||
}
|
||||
|
||||
// Parsea string a Type
|
||||
auto List::parseAssetType(const std::string& type_str) -> Type { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (type_str == "DATA") {
|
||||
return Type::DATA;
|
||||
}
|
||||
if (type_str == "BITMAP") {
|
||||
return Type::BITMAP;
|
||||
}
|
||||
if (type_str == "ANIMATION") {
|
||||
return Type::ANIMATION;
|
||||
}
|
||||
if (type_str == "MUSIC") {
|
||||
return Type::MUSIC;
|
||||
}
|
||||
if (type_str == "SOUND") {
|
||||
return Type::SOUND;
|
||||
}
|
||||
if (type_str == "FONT") {
|
||||
return Type::FONT;
|
||||
}
|
||||
if (type_str == "ROOM") {
|
||||
return Type::ROOM;
|
||||
}
|
||||
if (type_str == "TILEMAP") {
|
||||
// TILEMAP está obsoleto, ahora todo es ROOM (.yaml unificado)
|
||||
return Type::ROOM;
|
||||
}
|
||||
if (type_str == "PALETTE") {
|
||||
return Type::PALETTE;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown asset type: " + type_str);
|
||||
}
|
||||
|
||||
// Devuelve el nombre del tipo de recurso
|
||||
auto List::getTypeName(Type type) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
switch (type) {
|
||||
case Type::DATA:
|
||||
return "DATA";
|
||||
case Type::BITMAP:
|
||||
return "BITMAP";
|
||||
case Type::ANIMATION:
|
||||
return "ANIMATION";
|
||||
case Type::MUSIC:
|
||||
return "MUSIC";
|
||||
case Type::SOUND:
|
||||
return "SOUND";
|
||||
case Type::FONT:
|
||||
return "FONT";
|
||||
case Type::ROOM:
|
||||
return "ROOM";
|
||||
case Type::PALETTE:
|
||||
return "PALETTE";
|
||||
default:
|
||||
return "ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve la lista de recursos de un tipo
|
||||
auto List::getListByType(Type type) const -> std::vector<std::string> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<std::string> list;
|
||||
|
||||
for (const auto& [filename, item] : file_list_) {
|
||||
if (item.type == type) {
|
||||
list.push_back(item.file);
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar alfabéticamente para garantizar orden consistente
|
||||
std::ranges::sort(list);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// Reemplaza variables en las rutas
|
||||
auto List::replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::string result = path;
|
||||
|
||||
// Reemplazar ${PREFIX}
|
||||
size_t pos = 0;
|
||||
while ((pos = result.find("${PREFIX}", pos)) != std::string::npos) {
|
||||
result.replace(pos, 9, prefix); // 9 = longitud de "${PREFIX}"
|
||||
pos += prefix.length();
|
||||
}
|
||||
|
||||
// Reemplazar ${SYSTEM_FOLDER}
|
||||
pos = 0;
|
||||
while ((pos = result.find("${SYSTEM_FOLDER}", pos)) != std::string::npos) {
|
||||
result.replace(pos, 16, system_folder); // 16 = longitud de "${SYSTEM_FOLDER}"
|
||||
pos += system_folder.length();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parsea las opciones de una línea de configuración
|
||||
auto List::parseOptions(const std::string& options, bool& required, bool& absolute) -> void { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (options.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::istringstream iss(options);
|
||||
std::string option;
|
||||
|
||||
while (std::getline(iss, option, ',')) {
|
||||
// Eliminar espacios
|
||||
option.erase(0, option.find_first_not_of(" \t"));
|
||||
option.erase(option.find_last_not_of(" \t") + 1);
|
||||
|
||||
if (option == "optional") {
|
||||
required = false;
|
||||
} else if (option == "absolute") {
|
||||
absolute = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
80
source/core/resources/resource_list.hpp
Normal file
80
source/core/resources/resource_list.hpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
#include <utility> // Para move
|
||||
#include <vector> // Para vector
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// --- Clase List: gestor optimizado de recursos (singleton) ---
|
||||
class List {
|
||||
public:
|
||||
// --- Enums ---
|
||||
enum class Type : int {
|
||||
DATA, // Datos
|
||||
BITMAP, // Imágenes
|
||||
ANIMATION, // Animaciones
|
||||
MUSIC, // Música
|
||||
SOUND, // Sonidos
|
||||
FONT, // Fuentes
|
||||
ROOM, // Datos de habitación (.yaml - formato unificado con tilemap)
|
||||
PALETTE, // Paletas
|
||||
SIZE, // Tamaño (para iteración)
|
||||
};
|
||||
|
||||
// --- Métodos de singleton ---
|
||||
static void init(const std::string& executable_path);
|
||||
static void destroy();
|
||||
static auto get() -> List*;
|
||||
List(const List&) = delete;
|
||||
auto operator=(const List&) -> List& = delete;
|
||||
|
||||
// --- Métodos para la gestión de recursos ---
|
||||
void add(const std::string& file_path, Type type, bool required = true, bool absolute = false);
|
||||
void addAsset(const std::string& path, Type type); // Añade al mapa y persiste en assets.yaml
|
||||
void removeAsset(const std::string& filename); // Quita del mapa y persiste en assets.yaml
|
||||
void loadFromFile(const std::string& config_file_path, const std::string& prefix = "", const std::string& system_folder = ""); // Con soporte para variables
|
||||
void loadFromString(const std::string& config_content, const std::string& prefix = "", const std::string& system_folder = ""); // Para cargar desde pack (release)
|
||||
[[nodiscard]] auto get(const std::string& filename) const -> std::string; // Obtiene la ruta completa
|
||||
[[nodiscard]] auto loadData(const std::string& filename) const -> std::vector<uint8_t>; // Carga datos del archivo
|
||||
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
|
||||
[[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe
|
||||
|
||||
private:
|
||||
// --- Estructuras privadas ---
|
||||
struct Item {
|
||||
std::string file; // Ruta completa del archivo
|
||||
Type type; // Tipo de recurso
|
||||
bool required; // Indica si el archivo es obligatorio
|
||||
|
||||
Item(std::string path, Type asset_type, bool is_required)
|
||||
: file(std::move(path)),
|
||||
type(asset_type),
|
||||
required(is_required) {}
|
||||
};
|
||||
|
||||
// --- Variables internas ---
|
||||
std::unordered_map<std::string, Item> file_list_; // Mapa para búsqueda O(1)
|
||||
std::string executable_path_; // Ruta del ejecutable
|
||||
std::string config_file_path_; // Ruta del fichero assets.yaml
|
||||
std::string prefix_; // Prefijo para rutas (${PREFIX})
|
||||
|
||||
// --- Métodos internos ---
|
||||
[[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo
|
||||
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo
|
||||
void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa
|
||||
[[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta
|
||||
static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones
|
||||
|
||||
// --- Constructores y destructor privados (singleton) ---
|
||||
explicit List(std::string executable_path) // Constructor privado
|
||||
: executable_path_(std::move(executable_path)) {}
|
||||
~List() = default; // Destructor privado
|
||||
|
||||
// --- Instancia singleton ---
|
||||
static List* instance; // Instancia única de List
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
199
source/core/resources/resource_loader.cpp
Normal file
199
source/core/resources/resource_loader.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
// resource_loader.cpp
|
||||
// Resource loader implementation
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Get singleton instance
|
||||
auto Loader::get() -> Loader& {
|
||||
static Loader instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Initialize with a pack file
|
||||
auto Loader::initialize(const std::string& pack_file, bool enable_fallback)
|
||||
-> bool {
|
||||
if (initialized_) {
|
||||
std::cout << "Loader: Already initialized\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
fallback_to_files_ = enable_fallback;
|
||||
|
||||
// Try to load the pack file
|
||||
if (!pack_file.empty() && fileExistsOnFilesystem(pack_file)) {
|
||||
std::cout << "Loader: Loading pack file: " << pack_file << '\n';
|
||||
resource_pack_ = std::make_unique<Pack>();
|
||||
if (resource_pack_->loadPack(pack_file)) {
|
||||
std::cout << "Loader: Pack loaded successfully\n";
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
std::cerr << "Loader: Failed to load pack file\n";
|
||||
resource_pack_.reset();
|
||||
} else {
|
||||
std::cout << "Loader: Pack file not found: " << pack_file << '\n';
|
||||
}
|
||||
|
||||
// If pack loading failed and fallback is disabled, fail
|
||||
if (!fallback_to_files_) {
|
||||
std::cerr << "Loader: Pack required but not found (fallback disabled)\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, fallback to filesystem
|
||||
std::cout << "Loader: Using filesystem fallback\n";
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load a resource
|
||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-make-member-function-const)
|
||||
if (!initialized_) {
|
||||
std::cerr << "Loader: Not initialized\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Try pack first if available
|
||||
if (resource_pack_ && resource_pack_->isLoaded()) {
|
||||
if (resource_pack_->hasResource(filename)) {
|
||||
auto data = resource_pack_->getResource(filename);
|
||||
if (!data.empty()) {
|
||||
return data;
|
||||
}
|
||||
std::cerr << "Loader: Failed to extract from pack: " << filename
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to filesystem if enabled
|
||||
if (fallback_to_files_) {
|
||||
return loadFromFilesystem(filename);
|
||||
}
|
||||
|
||||
std::cerr << "Loader: Resource not found: " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
// Check if a resource exists
|
||||
auto Loader::resourceExists(const std::string& filename) -> bool { // NOLINT(readability-make-member-function-const)
|
||||
if (!initialized_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check pack first
|
||||
if (resource_pack_ && resource_pack_->isLoaded()) {
|
||||
if (resource_pack_->hasResource(filename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check filesystem if fallback enabled
|
||||
if (fallback_to_files_) {
|
||||
return fileExistsOnFilesystem(filename);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if pack is loaded
|
||||
auto Loader::isPackLoaded() const -> bool {
|
||||
return resource_pack_ && resource_pack_->isLoaded();
|
||||
}
|
||||
|
||||
// Get pack statistics
|
||||
auto Loader::getPackResourceCount() const -> size_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (resource_pack_ && resource_pack_->isLoaded()) {
|
||||
return resource_pack_->getResourceCount();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
void Loader::shutdown() {
|
||||
resource_pack_.reset();
|
||||
initialized_ = false;
|
||||
std::cout << "Loader: Shutdown complete\n";
|
||||
}
|
||||
|
||||
// Load from filesystem
|
||||
auto Loader::loadFromFilesystem(const std::string& filepath) // NOLINT(readability-convert-member-functions-to-static)
|
||||
-> std::vector<uint8_t> {
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::streamsize file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> data(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||
std::cerr << "Loader: Failed to read file: " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Check if file exists on filesystem
|
||||
auto Loader::fileExistsOnFilesystem(const std::string& filepath) -> bool {
|
||||
return std::filesystem::exists(filepath);
|
||||
}
|
||||
|
||||
// Validate pack integrity
|
||||
auto Loader::validatePack() const -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!initialized_ || !resource_pack_ || !resource_pack_->isLoaded()) {
|
||||
std::cerr << "Loader: Cannot validate - pack not loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate pack checksum
|
||||
uint32_t checksum = resource_pack_->calculatePackChecksum();
|
||||
|
||||
if (checksum == 0) {
|
||||
std::cerr << "Loader: Pack checksum is zero (invalid)\n";
|
||||
return false; // NOLINT(readability-simplify-boolean-expr)
|
||||
}
|
||||
|
||||
std::cout << "Loader: Pack checksum: 0x" << std::hex << checksum << std::dec
|
||||
<< '\n';
|
||||
std::cout << "Loader: Pack validation successful\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load assets.yaml from pack
|
||||
auto Loader::loadAssetsConfig() const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!initialized_ || !resource_pack_ || !resource_pack_->isLoaded()) {
|
||||
std::cerr << "Loader: Cannot load assets config - pack not loaded\n";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try to load config/assets.yaml from pack
|
||||
std::string config_path = "config/assets.yaml";
|
||||
|
||||
if (!resource_pack_->hasResource(config_path)) {
|
||||
std::cerr << "Loader: assets.yaml not found in pack: " << config_path << '\n';
|
||||
return "";
|
||||
}
|
||||
|
||||
auto data = resource_pack_->getResource(config_path);
|
||||
if (data.empty()) {
|
||||
std::cerr << "Loader: Failed to load assets.yaml from pack\n";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Convert bytes to string
|
||||
std::string config_content(data.begin(), data.end());
|
||||
std::cout << "Loader: Loaded assets.yaml from pack (" << data.size()
|
||||
<< " bytes)\n";
|
||||
|
||||
return config_content;
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
48
source/core/resources/resource_loader.hpp
Normal file
48
source/core/resources/resource_loader.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// resource_loader.hpp
|
||||
// Singleton resource loader for managing pack and filesystem access
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "resource_pack.hpp"
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Singleton class for loading resources from pack or filesystem
|
||||
class Loader {
|
||||
public:
|
||||
static auto get() -> Loader&; // Singleton instance access
|
||||
|
||||
auto initialize(const std::string& pack_file, bool enable_fallback = true) -> bool; // Initialize loader with pack file
|
||||
|
||||
auto loadResource(const std::string& filename) -> std::vector<uint8_t>; // Load resource data
|
||||
auto resourceExists(const std::string& filename) -> bool; // Check resource availability
|
||||
|
||||
[[nodiscard]] auto isPackLoaded() const -> bool; // Pack status queries
|
||||
[[nodiscard]] auto getPackResourceCount() const -> size_t;
|
||||
[[nodiscard]] auto validatePack() const -> bool; // Validate pack integrity
|
||||
[[nodiscard]] auto loadAssetsConfig() const -> std::string; // Load assets.yaml from pack
|
||||
|
||||
void shutdown(); // Cleanup
|
||||
|
||||
Loader(const Loader&) = delete; // Deleted copy/move constructors
|
||||
auto operator=(const Loader&) -> Loader& = delete;
|
||||
Loader(Loader&&) = delete;
|
||||
auto operator=(Loader&&) -> Loader& = delete;
|
||||
|
||||
private:
|
||||
Loader() = default;
|
||||
~Loader() = default;
|
||||
|
||||
static auto loadFromFilesystem(const std::string& filepath) -> std::vector<uint8_t>; // Filesystem helpers
|
||||
static auto fileExistsOnFilesystem(const std::string& filepath) -> bool;
|
||||
|
||||
std::unique_ptr<Pack> resource_pack_; // Member variables
|
||||
bool fallback_to_files_{true};
|
||||
bool initialized_{false};
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
303
source/core/resources/resource_pack.cpp
Normal file
303
source/core/resources/resource_pack.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
// resource_pack.cpp
|
||||
// Resource pack implementation for Projecte 2026
|
||||
|
||||
#include "resource_pack.hpp"
|
||||
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Calculate CRC32 checksum for data verification
|
||||
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
uint32_t checksum = 0x12345678;
|
||||
for (unsigned char byte : data) {
|
||||
checksum = ((checksum << 5) + checksum) + byte;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
// XOR encryption (symmetric - same function for encrypt/decrypt)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= key[i % key.length()];
|
||||
}
|
||||
}
|
||||
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
// XOR is symmetric
|
||||
encryptData(data, key);
|
||||
}
|
||||
|
||||
// Read entire file into memory
|
||||
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
std::cerr << "ResourcePack: Failed to open file: " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
std::streamsize file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> data(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||
std::cerr << "ResourcePack: Failed to read file: " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Add a single file to the pack
|
||||
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) // NOLINT(readability-convert-member-functions-to-static)
|
||||
-> bool {
|
||||
auto file_data = readFile(filepath);
|
||||
if (file_data.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResourceEntry entry{
|
||||
.filename = pack_name,
|
||||
.offset = data_.size(),
|
||||
.size = file_data.size(),
|
||||
.checksum = calculateChecksum(file_data)};
|
||||
|
||||
// Append file data to the data block
|
||||
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||
|
||||
resources_[pack_name] = entry;
|
||||
|
||||
std::cout << "Added: " << pack_name << " (" << file_data.size() << " bytes)\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add all files from a directory recursively
|
||||
auto Pack::addDirectory(const std::string& dir_path, // NOLINT(readability-convert-member-functions-to-static)
|
||||
const std::string& base_path) -> bool {
|
||||
namespace fs = std::filesystem; // NOLINT(readability-identifier-naming)
|
||||
|
||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||
std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string current_base = base_path.empty() ? "" : base_path + "/";
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string full_path = entry.path().string();
|
||||
std::string relative_path = entry.path().lexically_relative(dir_path).string();
|
||||
|
||||
// Convert backslashes to forward slashes (Windows compatibility)
|
||||
std::ranges::replace(relative_path, '\\', '/');
|
||||
|
||||
// Skip development files
|
||||
if (relative_path.find(".world") != std::string::npos ||
|
||||
relative_path.find(".tsx") != std::string::npos) {
|
||||
std::cout << "Skipping development file: " << relative_path << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string pack_name = current_base + relative_path;
|
||||
addFile(full_path, pack_name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save the pack to a file
|
||||
auto Pack::savePack(const std::string& pack_file) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::ofstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "ResourcePack: Failed to create pack file: " << pack_file << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write header
|
||||
file.write(MAGIC_HEADER.data(), MAGIC_HEADER.size());
|
||||
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
|
||||
|
||||
// Write resource count
|
||||
auto resource_count = static_cast<uint32_t>(resources_.size());
|
||||
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||
|
||||
// Write resource entries
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Write filename length and name
|
||||
auto name_len = static_cast<uint32_t>(entry.filename.length());
|
||||
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
|
||||
file.write(entry.filename.c_str(), name_len);
|
||||
|
||||
// Write offset, size, checksum
|
||||
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
||||
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
||||
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
||||
}
|
||||
|
||||
// Encrypt data
|
||||
std::vector<uint8_t> encrypted_data = data_;
|
||||
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
// Write encrypted data size and data
|
||||
uint64_t data_size = encrypted_data.size();
|
||||
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
||||
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
|
||||
|
||||
std::cout << "\nPack saved successfully: " << pack_file << '\n';
|
||||
std::cout << "Resources: " << resource_count << '\n';
|
||||
std::cout << "Total size: " << data_size << " bytes\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load a pack from a file
|
||||
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
||||
std::ifstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "ResourcePack: Failed to open pack file: " << pack_file << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and verify header
|
||||
std::array<char, 4> header{};
|
||||
file.read(header.data(), header.size());
|
||||
if (header != MAGIC_HEADER) {
|
||||
std::cerr << "ResourcePack: Invalid pack header\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and verify version
|
||||
uint32_t version = 0;
|
||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||
if (version != VERSION) {
|
||||
std::cerr << "ResourcePack: Unsupported pack version: " << version << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read resource count
|
||||
uint32_t resource_count = 0;
|
||||
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||
|
||||
// Read resource entries
|
||||
resources_.clear();
|
||||
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||
// Read filename
|
||||
uint32_t name_len = 0;
|
||||
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
||||
|
||||
std::string filename(name_len, '\0');
|
||||
file.read(filename.data(), name_len);
|
||||
|
||||
// Read entry data
|
||||
ResourceEntry entry{};
|
||||
entry.filename = filename;
|
||||
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
||||
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
||||
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
||||
|
||||
resources_[filename] = entry;
|
||||
}
|
||||
|
||||
// Read encrypted data
|
||||
uint64_t data_size = 0;
|
||||
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||
|
||||
data_.resize(data_size);
|
||||
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
||||
|
||||
// Decrypt data
|
||||
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
loaded_ = true;
|
||||
|
||||
std::cout << "ResourcePack loaded: " << pack_file << '\n';
|
||||
std::cout << "Resources: " << resource_count << '\n';
|
||||
std::cout << "Data size: " << data_size << " bytes\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get a resource by name
|
||||
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = resources_.find(filename);
|
||||
if (it == resources_.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ResourceEntry& entry = it->second;
|
||||
|
||||
// Extract data slice
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
std::cerr << "ResourcePack: Invalid offset/size for: " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> result(data_.begin() + entry.offset,
|
||||
data_.begin() + entry.offset + entry.size);
|
||||
|
||||
// Verify checksum
|
||||
uint32_t checksum = calculateChecksum(result);
|
||||
if (checksum != entry.checksum) {
|
||||
std::cerr << "ResourcePack: Checksum mismatch for: " << filename << '\n';
|
||||
std::cerr << " Expected: 0x" << std::hex << entry.checksum << '\n';
|
||||
std::cerr << " Got: 0x" << std::hex << checksum << std::dec << '\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if a resource exists
|
||||
auto Pack::hasResource(const std::string& filename) const -> bool {
|
||||
return resources_.contains(filename);
|
||||
}
|
||||
|
||||
// Get list of all resources
|
||||
auto Pack::getResourceList() const -> std::vector<std::string> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<std::string> list;
|
||||
list.reserve(resources_.size());
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
list.push_back(name);
|
||||
}
|
||||
std::ranges::sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
// Calculate overall pack checksum for validation
|
||||
auto Pack::calculatePackChecksum() const -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!loaded_ || data_.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Combine checksums of all resources for a global checksum
|
||||
uint32_t global_checksum = 0x87654321;
|
||||
|
||||
// Sort resources by name for deterministic checksum
|
||||
std::vector<std::string> sorted_names;
|
||||
sorted_names.reserve(resources_.size());
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
sorted_names.push_back(name);
|
||||
}
|
||||
std::ranges::sort(sorted_names);
|
||||
|
||||
// Combine individual checksums
|
||||
for (const auto& name : sorted_names) {
|
||||
const auto& entry = resources_.at(name);
|
||||
global_checksum = ((global_checksum << 5) + global_checksum) + entry.checksum;
|
||||
global_checksum = ((global_checksum << 5) + global_checksum) + entry.size;
|
||||
}
|
||||
|
||||
return global_checksum;
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
68
source/core/resources/resource_pack.hpp
Normal file
68
source/core/resources/resource_pack.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// resource_pack.hpp
|
||||
// Resource pack file format and management for Projecte 2026
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Entry metadata for each resource in the pack
|
||||
struct ResourceEntry {
|
||||
std::string filename; // Relative path within pack
|
||||
uint64_t offset{0}; // Byte offset in data block
|
||||
uint64_t size{0}; // Size in bytes
|
||||
uint32_t checksum{0}; // CRC32 checksum for verification
|
||||
};
|
||||
|
||||
// Resource pack file format
|
||||
// Header: "JDDI" (4 bytes) + Version (4 bytes)
|
||||
// Metadata: Count + array of ResourceEntry
|
||||
// Data: Encrypted data block
|
||||
class Pack {
|
||||
public:
|
||||
Pack() = default;
|
||||
~Pack() = default;
|
||||
|
||||
Pack(const Pack&) = delete; // Deleted copy/move constructors
|
||||
auto operator=(const Pack&) -> Pack& = delete;
|
||||
Pack(Pack&&) = delete;
|
||||
auto operator=(Pack&&) -> Pack& = delete;
|
||||
|
||||
auto addFile(const std::string& filepath, const std::string& pack_name) -> bool; // Building packs
|
||||
auto addDirectory(const std::string& dir_path, const std::string& base_path = "") -> bool;
|
||||
|
||||
auto savePack(const std::string& pack_file) -> bool; // Pack I/O
|
||||
auto loadPack(const std::string& pack_file) -> bool;
|
||||
|
||||
auto getResource(const std::string& filename) -> std::vector<uint8_t>; // Resource access
|
||||
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
|
||||
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
|
||||
|
||||
[[nodiscard]] auto isLoaded() const -> bool { return loaded_; } // Status queries
|
||||
[[nodiscard]] auto getResourceCount() const -> size_t { return resources_.size(); }
|
||||
[[nodiscard]] auto getDataSize() const -> size_t { return data_.size(); }
|
||||
[[nodiscard]] auto calculatePackChecksum() const -> uint32_t; // Validation
|
||||
|
||||
private:
|
||||
static constexpr std::array<char, 4> MAGIC_HEADER = {'J', 'D', 'D', 'I'}; // Pack format constants
|
||||
static constexpr uint32_t VERSION = 1;
|
||||
static constexpr const char* DEFAULT_ENCRYPT_KEY = "JDDI_RESOURCES_2024";
|
||||
|
||||
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t; // Utility methods
|
||||
|
||||
static void encryptData(std::vector<uint8_t>& data, const std::string& key); // Encryption/decryption
|
||||
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
|
||||
|
||||
static auto readFile(const std::string& filepath) -> std::vector<uint8_t>; // File I/O
|
||||
|
||||
std::unordered_map<std::string, ResourceEntry> resources_; // Member variables
|
||||
std::vector<uint8_t> data_; // Encrypted data block
|
||||
bool loaded_{false};
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
62
source/core/resources/resource_types.hpp
Normal file
62
source/core/resources/resource_types.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Palette y Surface
|
||||
#include "core/rendering/text.hpp" // Para Text y Text::File
|
||||
#include "game/gameplay/room.hpp" // Para Room::Data
|
||||
|
||||
// Forward declarations
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
// Estructura para almacenar ficheros de sonido y su nombre
|
||||
struct SoundResource {
|
||||
std::string name; // Nombre del sonido
|
||||
JA_Sound_t* sound{nullptr}; // Objeto con el sonido
|
||||
};
|
||||
|
||||
// Estructura para almacenar ficheros musicales y su nombre
|
||||
struct MusicResource {
|
||||
std::string name; // Nombre de la musica
|
||||
JA_Music_t* music{nullptr}; // Objeto con la música
|
||||
};
|
||||
|
||||
// Estructura para almacenar objetos Surface y su nombre
|
||||
struct SurfaceResource {
|
||||
std::string name; // Nombre de la surface
|
||||
std::shared_ptr<Surface> surface; // Objeto con la surface
|
||||
};
|
||||
|
||||
// Estructura para almacenar objetos Palette y su nombre
|
||||
struct ResourcePalette {
|
||||
std::string name; // Nombre de la surface
|
||||
Palette palette{}; // Paleta
|
||||
};
|
||||
|
||||
// Estructura para almacenar ficheros TextFile y su nombre
|
||||
struct TextFileResource {
|
||||
std::string name; // Nombre del fichero
|
||||
std::shared_ptr<Text::File> text_file; // Objeto con los descriptores de la fuente de texto
|
||||
};
|
||||
|
||||
// Estructura para almacenar objetos Text y su nombre
|
||||
struct TextResource {
|
||||
std::string name; // Nombre del objeto
|
||||
std::shared_ptr<Text> text; // Objeto
|
||||
};
|
||||
|
||||
// Estructura para almacenar ficheros animaciones y su nombre
|
||||
struct AnimationResource {
|
||||
std::string name; // Nombre del fichero
|
||||
std::vector<uint8_t> yaml_data; // Bytes del archivo YAML sin parsear
|
||||
};
|
||||
|
||||
// Estructura para almacenar habitaciones y su nombre
|
||||
struct RoomResource {
|
||||
std::string name; // Nombre de la habitación
|
||||
std::shared_ptr<Room::Data> room; // Habitación
|
||||
};
|
||||
189
source/core/system/debug.cpp
Normal file
189
source/core/system/debug.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "core/system/debug.hpp"
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <algorithm> // Para max
|
||||
#include <fstream> // Para ifstream, ofstream
|
||||
#include <memory> // Para __shared_ptr_access, shared_ptr
|
||||
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/defaults.hpp" // Para Defaults::Game::*
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE
|
||||
#include "utils/utils.hpp" // Para Color, Flip::
|
||||
|
||||
// [SINGLETON]
|
||||
Debug* Debug::debug = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Debug::init() {
|
||||
Debug::debug = new Debug();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Debug::destroy() {
|
||||
delete Debug::debug;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Debug::get() -> Debug* {
|
||||
return Debug::debug;
|
||||
}
|
||||
|
||||
// Dibuja en pantalla
|
||||
void Debug::render() { // NOLINT(readability-make-member-function-const)
|
||||
auto text = Resource::Cache::get()->getText("aseprite");
|
||||
int y = y_;
|
||||
int w = 0;
|
||||
constexpr int DESP_Y = 7;
|
||||
const int CHAR_SIZE = text->getCharacterSize();
|
||||
|
||||
// Watch window: valores persistentes (key: value)
|
||||
for (const auto& [key, value] : watches_) {
|
||||
const std::string LINE = key + ": " + value;
|
||||
text->write(x_, y, LINE);
|
||||
w = std::max(w, text->length(LINE));
|
||||
y += DESP_Y;
|
||||
if (y > 192 - CHAR_SIZE) {
|
||||
y = y_;
|
||||
x_ += w + 2;
|
||||
w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Slot one-shot: mensajes de un solo frame
|
||||
for (const auto& s : slot_) {
|
||||
text->write(x_, y, s);
|
||||
w = std::max(w, text->length(s));
|
||||
y += DESP_Y;
|
||||
if (y > 192 - CHAR_SIZE) {
|
||||
y = y_;
|
||||
x_ += w + 2;
|
||||
w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
y = 0;
|
||||
for (const auto& l : log_) {
|
||||
text->writeColored(x_ + 10, y, l, static_cast<Uint8>(PaletteColor::WHITE));
|
||||
y += CHAR_SIZE + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Establece/actualiza un valor persistente en el watch window
|
||||
void Debug::set(const std::string& key, const std::string& value) {
|
||||
watches_[key] = value;
|
||||
}
|
||||
|
||||
// Elimina un valor del watch window
|
||||
void Debug::unset(const std::string& key) {
|
||||
watches_.erase(key);
|
||||
}
|
||||
|
||||
// Establece la posición donde se colocará la información de debug
|
||||
void Debug::setPos(SDL_FPoint p) {
|
||||
x_ = p.x;
|
||||
y_ = p.y;
|
||||
}
|
||||
|
||||
// Establece la ruta del archivo debug.yaml
|
||||
void Debug::setDebugFile(const std::string& path) {
|
||||
debug_file_path_ = path;
|
||||
}
|
||||
|
||||
// Convierte string a SceneManager::Scene (para debug.yaml)
|
||||
static auto sceneFromString(const std::string& s) -> SceneManager::Scene {
|
||||
if (s == "LOGO") { return SceneManager::Scene::LOGO; }
|
||||
if (s == "LOADING") { return SceneManager::Scene::LOADING_SCREEN; }
|
||||
if (s == "TITLE") { return SceneManager::Scene::TITLE; }
|
||||
if (s == "CREDITS") { return SceneManager::Scene::CREDITS; }
|
||||
if (s == "DEMO") { return SceneManager::Scene::DEMO; }
|
||||
if (s == "ENDING") { return SceneManager::Scene::ENDING; }
|
||||
if (s == "ENDING2") { return SceneManager::Scene::ENDING2; }
|
||||
return SceneManager::Scene::GAME; // Fallback seguro
|
||||
}
|
||||
|
||||
// Convierte SceneManager::Scene a string (para debug.yaml)
|
||||
static auto sceneToString(SceneManager::Scene scene) -> std::string {
|
||||
switch (scene) {
|
||||
case SceneManager::Scene::LOGO:
|
||||
return "LOGO";
|
||||
case SceneManager::Scene::LOADING_SCREEN:
|
||||
return "LOADING";
|
||||
case SceneManager::Scene::TITLE:
|
||||
return "TITLE";
|
||||
case SceneManager::Scene::CREDITS:
|
||||
return "CREDITS";
|
||||
case SceneManager::Scene::DEMO:
|
||||
return "DEMO";
|
||||
case SceneManager::Scene::ENDING:
|
||||
return "ENDING";
|
||||
case SceneManager::Scene::ENDING2:
|
||||
return "ENDING2";
|
||||
default:
|
||||
return "GAME";
|
||||
}
|
||||
}
|
||||
|
||||
// Carga la configuración de debug desde debug.yaml
|
||||
void Debug::loadFromFile() {
|
||||
// Inicializar con valores de release por defecto
|
||||
spawn_settings_.room = Defaults::Game::Room::INITIAL;
|
||||
spawn_settings_.spawn_x = Defaults::Game::Player::SPAWN_X;
|
||||
spawn_settings_.spawn_y = Defaults::Game::Player::SPAWN_Y;
|
||||
spawn_settings_.flip = Defaults::Game::Player::SPAWN_FLIP;
|
||||
initial_scene_ = SceneManager::Scene::GAME;
|
||||
|
||||
std::ifstream file(debug_file_path_);
|
||||
if (!file.good()) {
|
||||
saveToFile(); // No existe: crear con valores por defecto
|
||||
return;
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (yaml.contains("room")) {
|
||||
spawn_settings_.room = yaml["room"].get_value<std::string>();
|
||||
}
|
||||
if (yaml.contains("spawn_x")) {
|
||||
spawn_settings_.spawn_x = yaml["spawn_x"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (yaml.contains("spawn_y")) {
|
||||
spawn_settings_.spawn_y = yaml["spawn_y"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (yaml.contains("spawn_flip")) {
|
||||
auto s = yaml["spawn_flip"].get_value<std::string>();
|
||||
spawn_settings_.flip = (s == "right") ? Flip::RIGHT : Flip::LEFT;
|
||||
}
|
||||
if (yaml.contains("initial_scene")) {
|
||||
initial_scene_ = sceneFromString(yaml["initial_scene"].get_value<std::string>());
|
||||
}
|
||||
} catch (...) {
|
||||
// YAML inválido: resetear a defaults y sobreescribir
|
||||
spawn_settings_.room = Defaults::Game::Room::INITIAL;
|
||||
spawn_settings_.spawn_x = Defaults::Game::Player::SPAWN_X;
|
||||
spawn_settings_.spawn_y = Defaults::Game::Player::SPAWN_Y;
|
||||
spawn_settings_.flip = Defaults::Game::Player::SPAWN_FLIP;
|
||||
initial_scene_ = SceneManager::Scene::GAME;
|
||||
saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Guarda la configuración de debug en debug.yaml
|
||||
void Debug::saveToFile() const {
|
||||
std::ofstream file(debug_file_path_);
|
||||
if (!file.is_open()) { return; }
|
||||
file << "# Projecte 2026 - Debug Configuration\n";
|
||||
file << "# Edita para cambiar la habitacion y spawn del jugador en builds debug.\n\n";
|
||||
file << "room: \"" << spawn_settings_.room << "\"\n";
|
||||
file << "spawn_x: " << (spawn_settings_.spawn_x / Tile::SIZE) << " # en tiles\n";
|
||||
file << "spawn_y: " << (spawn_settings_.spawn_y / Tile::SIZE) << " # en tiles\n";
|
||||
file << "spawn_flip: " << ((spawn_settings_.flip == Flip::RIGHT) ? "right" : "left") << "\n";
|
||||
file << "initial_scene: " << sceneToString(initial_scene_) << "\n";
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
69
source/core/system/debug.hpp
Normal file
69
source/core/system/debug.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <map> // Para map
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "game/scene_manager.hpp" // Para SceneManager::Scene
|
||||
|
||||
// Clase Debug
|
||||
class Debug {
|
||||
public:
|
||||
struct SpawnSettings {
|
||||
std::string room;
|
||||
int spawn_x = 0;
|
||||
int spawn_y = 0;
|
||||
SDL_FlipMode flip = SDL_FLIP_NONE;
|
||||
};
|
||||
|
||||
static void init(); // [SINGLETON] Crearemos el objeto con esta función estática
|
||||
static void destroy(); // [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
static auto get() -> Debug*; // [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
|
||||
void render(); // Dibuja en pantalla
|
||||
|
||||
void setPos(SDL_FPoint p); // Establece la posición donde se colocará la información de debug
|
||||
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; } // Obtiene si el debug está activo
|
||||
|
||||
void add(const std::string& text) { slot_.push_back(text); } // Añade texto one-shot al slot (se limpia cada frame)
|
||||
void clear() { slot_.clear(); } // Limpia el slot one-shot (no afecta a watches)
|
||||
void addToLog(const std::string& text) { log_.push_back(text); } // Añade texto al log
|
||||
void clearLog() { log_.clear(); } // Limpia el log
|
||||
void set(const std::string& key, const std::string& value); // Establece/actualiza un valor persistente en el watch window
|
||||
void unset(const std::string& key); // Elimina un valor del watch window
|
||||
void clearWatches() { watches_.clear(); } // Limpia todos los watches
|
||||
void setEnabled(bool value) { enabled_ = value; } // Establece si el debug está activo
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alterna el estado del debug
|
||||
|
||||
void setDebugFile(const std::string& path); // Establece la ruta del archivo debug.yaml
|
||||
void loadFromFile(); // Carga la configuración de debug desde debug.yaml
|
||||
void saveToFile() const; // Guarda la configuración de debug en debug.yaml
|
||||
[[nodiscard]] auto getSpawnSettings() const -> const SpawnSettings& { return spawn_settings_; } // Obtiene los valores de spawn
|
||||
void setSpawnSettings(const SpawnSettings& s) { spawn_settings_ = s; } // Establece los valores de spawn
|
||||
[[nodiscard]] auto getInitialScene() const -> SceneManager::Scene { return initial_scene_; } // Obtiene la escena inicial de debug
|
||||
void setInitialScene(SceneManager::Scene s) { initial_scene_ = s; } // Establece la escena inicial de debug
|
||||
|
||||
private:
|
||||
static Debug* debug; // [SINGLETON] Objeto privado
|
||||
|
||||
Debug() = default; // Constructor
|
||||
~Debug() = default; // Destructor
|
||||
|
||||
// Variables
|
||||
std::map<std::string, std::string> watches_; // Watch window: valores persistentes (key→value)
|
||||
std::vector<std::string> slot_; // One-shot: textos que se limpian cada frame
|
||||
std::vector<std::string> log_; // Log persistente
|
||||
int x_ = 0; // Posicion donde escribir el texto de debug
|
||||
int y_ = 0; // Posición donde escribir el texto de debug
|
||||
bool enabled_ = false; // Indica si esta activo el modo debug
|
||||
std::string debug_file_path_; // Ruta del archivo debug.yaml
|
||||
SpawnSettings spawn_settings_; // Configuración de spawn para debug
|
||||
SceneManager::Scene initial_scene_ = SceneManager::Scene::GAME; // Escena inicial en debug
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
428
source/core/system/director.cpp
Normal file
428
source/core/system/director.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
#include "core/system/director.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sys/stat.h> // Para mkdir, stat, S_IRWXU
|
||||
#include <unistd.h> // Para getuid
|
||||
|
||||
#include <cerrno> // Para errno, EEXIST, EACCES, ENAMETOO...
|
||||
#include <cstdio> // Para printf, perror
|
||||
#include <cstdlib> // Para exit, EXIT_FAILURE, srand
|
||||
#include <iostream> // Para basic_ostream, operator<<, cout
|
||||
#include <memory> // Para make_unique, unique_ptr
|
||||
#include <string> // Para operator+, allocator, char_traits
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/input/input.hpp" // Para Input, InputAction
|
||||
#include "core/locale/locale.hpp" // Para Locale
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
|
||||
#include "core/resources/resource_loader.hpp" // Para ResourceLoader
|
||||
#include "game/gameplay/cheevos.hpp" // Para Cheevos
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo
|
||||
#include "game/scene_manager.hpp" // Para SceneManager
|
||||
#include "game/scenes/credits.hpp" // Para Credits
|
||||
#include "game/scenes/ending.hpp" // Para Ending
|
||||
#include "game/scenes/ending2.hpp" // Para Ending2
|
||||
#include "game/scenes/game.hpp" // Para Game, GameMode
|
||||
#include "game/scenes/game_over.hpp" // Para GameOver
|
||||
#include "game/scenes/loading_screen.hpp" // Para LoadingScreen
|
||||
#include "game/scenes/logo.hpp" // Para Logo
|
||||
#include "game/scenes/title.hpp" // Para Title
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/defines.hpp" // Para WINDOW_CAPTION
|
||||
|
||||
#ifdef _DEBUG
|
||||
#include "core/system/debug.hpp" // Para Debug
|
||||
#include "game/editor/map_editor.hpp" // Para MapEditor
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
Director::Director() {
|
||||
std::cout << "Game start" << '\n';
|
||||
|
||||
// Obtiene la ruta del ejecutable
|
||||
std::string base = SDL_GetBasePath();
|
||||
if (!base.empty() && base.back() == '/') {
|
||||
base.pop_back();
|
||||
}
|
||||
executable_path_ = base;
|
||||
|
||||
// Crea la carpeta del sistema donde guardar datos
|
||||
createSystemFolder("jailgames");
|
||||
createSystemFolder("jailgames/projecte_2026");
|
||||
|
||||
// Crea el subdirectorio shaders/ dentro de system_folder_ sin modificar system_folder_
|
||||
{
|
||||
std::string shaders_dir = system_folder_ + "/shaders";
|
||||
struct stat st = {.st_dev = 0};
|
||||
if (stat(shaders_dir.c_str(), &st) == -1) {
|
||||
errno = 0;
|
||||
#ifdef _WIN32
|
||||
mkdir(shaders_dir.c_str());
|
||||
#else
|
||||
mkdir(shaders_dir.c_str(), S_IRWXU);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar el prefijo de ruta según la plataforma
|
||||
#ifdef MACOS_BUNDLE
|
||||
const std::string PREFIX = "/../Resources";
|
||||
#else
|
||||
const std::string PREFIX;
|
||||
#endif
|
||||
|
||||
// Preparar ruta al pack (en macOS bundle está en Contents/Resources/)
|
||||
std::string pack_path = executable_path_ + PREFIX + "/resources.pack";
|
||||
|
||||
#ifdef RELEASE_BUILD
|
||||
// ============================================================
|
||||
// RELEASE BUILD: Pack-first architecture
|
||||
// ============================================================
|
||||
std::cout << "\n** RELEASE MODE: Pack-first initialization\n";
|
||||
|
||||
// 1. Initialize resource pack system (required, no fallback)
|
||||
std::cout << "Initializing resource pack: " << pack_path << '\n';
|
||||
if (!Resource::Helper::initializeResourceSystem(pack_path, false)) {
|
||||
std::cerr << "ERROR: Failed to load resources.pack (required in release builds)\n";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// 2. Validate pack integrity
|
||||
std::cout << "Validating pack integrity..." << '\n';
|
||||
if (!Resource::Loader::get().validatePack()) {
|
||||
std::cerr << "ERROR: Pack validation failed\n";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// 3. Load assets.yaml from pack
|
||||
std::cout << "Loading assets configuration from pack..." << '\n';
|
||||
std::string assets_config = Resource::Loader::get().loadAssetsConfig();
|
||||
if (assets_config.empty()) {
|
||||
std::cerr << "ERROR: Failed to load assets.yaml from pack\n";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// 4. Initialize Asset system with config from pack
|
||||
// NOTE: In release, don't use executable_path or PREFIX - paths in pack are relative
|
||||
// Pass empty string to avoid issues when running from different directories
|
||||
Resource::List::init(""); // Empty executable_path in release
|
||||
Resource::List::get()->loadFromString(assets_config, "", system_folder_); // Empty PREFIX for pack
|
||||
std::cout << "Asset system initialized from pack\n";
|
||||
|
||||
#else
|
||||
// ============================================================
|
||||
// DEVELOPMENT BUILD: Filesystem-first architecture
|
||||
// ============================================================
|
||||
std::cout << "\n** DEVELOPMENT MODE: Filesystem-first initialization\n";
|
||||
|
||||
// 1. Initialize Asset system from filesystem
|
||||
Resource::List::init(executable_path_);
|
||||
|
||||
// 2. Load asset configuration from disk
|
||||
// Note: Asset verification happens during Resource::Cache::load()
|
||||
setFileList();
|
||||
|
||||
// 3. Initialize resource pack system (optional, with fallback)
|
||||
std::cout << "Initializing resource pack (development mode): " << pack_path << '\n';
|
||||
Resource::Helper::initializeResourceSystem(pack_path, true);
|
||||
|
||||
#endif
|
||||
|
||||
// Configura la ruta y carga las opciones desde un fichero
|
||||
Options::setConfigFile(Resource::List::get()->get("config.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
Options::loadFromFile();
|
||||
|
||||
// Configura la ruta y carga los presets de PostFX
|
||||
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
Options::loadPostFXFromFile();
|
||||
|
||||
// Configura la ruta y carga los presets del shader CrtPi
|
||||
Options::setCrtPiFile(Resource::List::get()->get("crtpi.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
Options::loadCrtPiFromFile();
|
||||
|
||||
// En mode quiosc, forçar pantalla completa independentment de la configuració
|
||||
if (Options::kiosk.enabled) {
|
||||
Options::video.fullscreen = true;
|
||||
}
|
||||
|
||||
// Inicializa JailAudio
|
||||
Audio::init();
|
||||
|
||||
// Crea los objetos
|
||||
Screen::init();
|
||||
|
||||
// Initialize resources (works for both release and development)
|
||||
Resource::Cache::init();
|
||||
Notifier::init("", "8bithud");
|
||||
RenderInfo::init();
|
||||
Console::init("8bithud");
|
||||
Screen::get()->setNotificationsEnabled(true);
|
||||
|
||||
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path
|
||||
#ifdef RELEASE_BUILD
|
||||
// In release, construct the path manually (not from Asset which has empty executable_path)
|
||||
std::string gamecontroller_db = executable_path_ + PREFIX + "/gamecontrollerdb.txt";
|
||||
Input::init(gamecontroller_db);
|
||||
#else
|
||||
// In development, use Asset as normal
|
||||
Input::init(Resource::List::get()->get("gamecontrollerdb.txt")); // NOLINT(readability-static-accessed-through-instance) Carga configuración de controles
|
||||
#endif
|
||||
|
||||
// Aplica las teclas y botones del gamepad configurados desde Options
|
||||
Input::get()->applyKeyboardBindingsFromOptions();
|
||||
Input::get()->applyGamepadBindingsFromOptions();
|
||||
|
||||
#ifdef _DEBUG
|
||||
Debug::init();
|
||||
Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml"));
|
||||
Debug::get()->loadFromFile();
|
||||
SceneManager::current = Debug::get()->getInitialScene();
|
||||
MapEditor::init();
|
||||
#endif
|
||||
|
||||
std::cout << "\n"; // Fin de inicialización de sistemas
|
||||
|
||||
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
|
||||
#ifdef RELEASE_BUILD
|
||||
{
|
||||
// En release el locale está en el pack, no en el filesystem
|
||||
std::string locale_key = Resource::List::get()->get(Options::language + ".yaml"); // NOLINT(readability-static-accessed-through-instance)
|
||||
auto locale_bytes = Resource::Helper::loadFile(locale_key);
|
||||
std::string locale_content(locale_bytes.begin(), locale_bytes.end());
|
||||
Locale::initFromContent(locale_content);
|
||||
}
|
||||
#else
|
||||
Locale::init(Resource::List::get()->get(Options::language + ".yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
#endif
|
||||
|
||||
// Special handling for cheevos.bin - also needs filesystem path
|
||||
#ifdef RELEASE_BUILD
|
||||
std::string cheevos_path = system_folder_ + "/cheevos.bin";
|
||||
Cheevos::init(cheevos_path);
|
||||
#else
|
||||
Cheevos::init(Resource::List::get()->get("cheevos.bin"));
|
||||
#endif
|
||||
}
|
||||
|
||||
Director::~Director() {
|
||||
// Guarda las opciones a un fichero
|
||||
Options::saveToFile();
|
||||
|
||||
// Destruye los singletones
|
||||
Cheevos::destroy();
|
||||
Locale::destroy();
|
||||
#ifdef _DEBUG
|
||||
MapEditor::destroy();
|
||||
Debug::destroy();
|
||||
#endif
|
||||
Input::destroy();
|
||||
Console::destroy();
|
||||
RenderInfo::destroy();
|
||||
Notifier::destroy();
|
||||
Resource::Cache::destroy();
|
||||
Resource::Helper::shutdownResourceSystem(); // Shutdown resource pack system
|
||||
Audio::destroy();
|
||||
Screen::destroy();
|
||||
Resource::List::destroy();
|
||||
|
||||
SDL_Quit();
|
||||
|
||||
std::cout << "\nBye!" << '\n';
|
||||
}
|
||||
|
||||
// Crea la carpeta del sistema donde guardar datos
|
||||
void Director::createSystemFolder(const std::string& folder) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
#ifdef _WIN32
|
||||
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
|
||||
#elif __APPLE__
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
const char* homedir = pw->pw_dir;
|
||||
system_folder_ = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
||||
#elif __linux__
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
const char* homedir = pw->pw_dir;
|
||||
system_folder_ = std::string(homedir) + "/.config/" + folder;
|
||||
|
||||
{
|
||||
// Intenta crear ".config", per si no existeix
|
||||
std::string config_base_folder = std::string(homedir) + "/.config";
|
||||
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
|
||||
if (ret == -1 && errno != EEXIST) {
|
||||
printf("ERROR CREATING CONFIG BASE FOLDER.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct stat st = {.st_dev = 0};
|
||||
if (stat(system_folder_.c_str(), &st) == -1) {
|
||||
errno = 0;
|
||||
#ifdef _WIN32
|
||||
int ret = mkdir(system_folder_.c_str());
|
||||
#else
|
||||
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
|
||||
#endif
|
||||
|
||||
if (ret == -1) {
|
||||
switch (errno) {
|
||||
case EACCES:
|
||||
printf("the parent directory does not allow write");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
case EEXIST:
|
||||
printf("pathname already exists");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
case ENAMETOOLONG:
|
||||
printf("pathname is too long");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
default:
|
||||
perror("mkdir");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga la configuración de assets desde assets.yaml
|
||||
void Director::setFileList() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Determinar el prefijo de ruta según la plataforma
|
||||
#ifdef MACOS_BUNDLE
|
||||
const std::string PREFIX = "/../Resources";
|
||||
#else
|
||||
const std::string PREFIX;
|
||||
#endif
|
||||
|
||||
// Construir ruta al archivo de configuración de assets
|
||||
std::string config_path = executable_path_ + PREFIX + "/config/assets.yaml";
|
||||
|
||||
// Cargar todos los assets desde el archivo de configuración
|
||||
// La verificación de existencia de archivos se realiza durante Resource::Cache::load()
|
||||
Resource::List::get()->loadFromFile(config_path, PREFIX, system_folder_);
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego con el logo
|
||||
void Director::runLogo() {
|
||||
auto logo = std::make_unique<Logo>();
|
||||
logo->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego de la pantalla de carga
|
||||
void Director::runLoadingScreen() {
|
||||
auto loading_screen = std::make_unique<LoadingScreen>();
|
||||
loading_screen->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego con el titulo y los menus
|
||||
void Director::runTitle() {
|
||||
auto title = std::make_unique<Title>();
|
||||
title->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de los creditos del juego
|
||||
void Director::runCredits() {
|
||||
auto credits = std::make_unique<Credits>();
|
||||
credits->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de la demo, donde se ven pantallas del juego
|
||||
void Director::runDemo() {
|
||||
auto game = std::make_unique<Game>(Game::Mode::DEMO);
|
||||
game->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion del final del juego
|
||||
void Director::runEnding() {
|
||||
auto ending = std::make_unique<Ending>();
|
||||
ending->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion del final del juego
|
||||
void Director::runEnding2() {
|
||||
auto ending2 = std::make_unique<Ending2>();
|
||||
ending2->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion del final de la partida
|
||||
void Director::runGameOver() {
|
||||
auto game_over = std::make_unique<GameOver>();
|
||||
game_over->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego donde se juega
|
||||
void Director::runGame() {
|
||||
Audio::get()->stopMusic();
|
||||
auto game = std::make_unique<Game>(Game::Mode::GAME);
|
||||
game->run();
|
||||
}
|
||||
|
||||
auto Director::run() -> int {
|
||||
// Bucle principal
|
||||
while (SceneManager::current != SceneManager::Scene::QUIT) {
|
||||
const SceneManager::Scene ACTIVE = SceneManager::current;
|
||||
|
||||
switch (SceneManager::current) {
|
||||
case SceneManager::Scene::LOGO:
|
||||
runLogo();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::LOADING_SCREEN:
|
||||
runLoadingScreen();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::TITLE:
|
||||
runTitle();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::CREDITS:
|
||||
runCredits();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::DEMO:
|
||||
runDemo();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::GAME:
|
||||
runGame();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::GAME_OVER:
|
||||
runGameOver();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::ENDING:
|
||||
runEnding();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::ENDING2:
|
||||
runEnding2();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::RESTART_CURRENT:
|
||||
// La escena salió por RESTART_CURRENT → relanzar la escena guardada
|
||||
SceneManager::current = SceneManager::scene_before_restart;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Si la escena que acaba de correr dejó RESTART_CURRENT pendiente,
|
||||
// restaurar la escena que estaba activa para relanzarla en la próxima iteración
|
||||
if (SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
|
||||
SceneManager::current = ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
30
source/core/system/director.hpp
Normal file
30
source/core/system/director.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para string
|
||||
|
||||
class Director {
|
||||
public:
|
||||
Director(); // Constructor
|
||||
~Director(); // Destructor
|
||||
static auto run() -> int; // Bucle principal
|
||||
|
||||
private:
|
||||
// --- Variables ---
|
||||
std::string executable_path_; // Path del ejecutable
|
||||
std::string system_folder_; // Carpeta del sistema donde guardar datos
|
||||
|
||||
// --- Funciones ---
|
||||
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos
|
||||
void setFileList(); // Carga la configuración de assets desde assets.yaml
|
||||
static void runLogo(); // Ejecuta la seccion de juego con el logo
|
||||
static void runLoadingScreen(); // Ejecuta la seccion de juego de la pantalla de carga
|
||||
static void runTitle(); // Ejecuta la seccion de juego con el titulo y los menus
|
||||
static void runCredits(); // Ejecuta la seccion de los creditos del juego
|
||||
static void runDemo(); // Ejecuta la seccion de la demo, donde se ven pantallas del juego
|
||||
static void runEnding(); // Ejecuta la seccion del final del juego
|
||||
static void runEnding2(); // Ejecuta la seccion del final del juego
|
||||
static void runGameOver(); // Ejecuta la seccion del final de la partida
|
||||
static void runGame(); // Ejecuta la seccion de juego donde se juega
|
||||
};
|
||||
31
source/core/system/global_events.cpp
Normal file
31
source/core/system/global_events.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "core/system/global_events.hpp"
|
||||
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
|
||||
#include "game/scene_manager.hpp" // Para SceneManager
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
|
||||
namespace GlobalEvents {
|
||||
// Comprueba los eventos que se pueden producir en cualquier sección del juego
|
||||
void handle(const SDL_Event& event) {
|
||||
// Evento de salida de la aplicación
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
SceneManager::current = SceneManager::Scene::QUIT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_RENDER_DEVICE_RESET || event.type == SDL_EVENT_RENDER_TARGETS_RESET) {
|
||||
// reLoadTextures();
|
||||
}
|
||||
|
||||
// Enrutar eventos de texto a la consola cuando está activa
|
||||
if (Console::get() != nullptr && Console::get()->isActive()) {
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {
|
||||
Console::get()->handleEvent(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Mouse::handleEvent(event);
|
||||
}
|
||||
} // namespace GlobalEvents
|
||||
8
source/core/system/global_events.hpp
Normal file
8
source/core/system/global_events.hpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace GlobalEvents {
|
||||
// Comprueba los eventos que se pueden producir en cualquier sección del juego
|
||||
void handle(const SDL_Event& event);
|
||||
} // namespace GlobalEvents
|
||||
Reference in New Issue
Block a user