diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index 633a767..ad58c6a 100644 --- a/source/core/audio/audio.cpp +++ b/source/core/audio/audio.cpp @@ -8,6 +8,7 @@ // Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp). // clang-format off #undef STB_VORBIS_HEADER_ONLY +// NOLINTNEXTLINE(bugprone-suspicious-include) — stb_vorbis és single-file: el TU principal inclou el .c per portar la implementació. #include "external/stb_vorbis.c" // stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem // perquè xocarien amb noms de paràmetres de plantilla en altres headers. @@ -43,15 +44,15 @@ Audio::Audio() { initSDLAudio(); } // Destructor Audio::~Audio() { - JA_Quit(); + Ja::quit(); } // Método principal void Audio::update() { - JA_Update(); + Ja::update(); // Sincronizar estado: detectar cuando la música se para (ej. fade-out completado) - if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) { + if (instance != nullptr && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) { instance->music_.state = MusicState::STOPPED; } } @@ -65,18 +66,18 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa return; } - if (!music_enabled_) return; + if (!music_enabled_) { return; } auto* resource = AudioResource::getMusic(name); - if (resource == nullptr) return; + if (resource == nullptr) { return; } if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) { - JA_CrossfadeMusic(resource, crossfade_ms, loop); + Ja::crossfadeMusic(resource, crossfade_ms, loop); } else { if (music_.state == MusicState::PLAYING) { - JA_StopMusic(); + Ja::stopMusic(); } - JA_PlayMusic(resource, loop); + Ja::playMusic(resource, loop); } music_.name = name; @@ -85,16 +86,16 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa } // Reproduce la música por puntero (con crossfade opcional) -void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) { - if (!music_enabled_ || music == nullptr) return; +void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) { + if (!music_enabled_ || music == nullptr) { return; } if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) { - JA_CrossfadeMusic(music, crossfade_ms, loop); + Ja::crossfadeMusic(music, crossfade_ms, loop); } else { if (music_.state == MusicState::PLAYING) { - JA_StopMusic(); + Ja::stopMusic(); } - JA_PlayMusic(music, loop); + Ja::playMusic(music, loop); } music_.name.clear(); // nom desconegut quan es passa per punter @@ -105,7 +106,7 @@ void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) // Pausa la música void Audio::pauseMusic() { if (music_enabled_ && music_.state == MusicState::PLAYING) { - JA_PauseMusic(); + Ja::pauseMusic(); music_.state = MusicState::PAUSED; } } @@ -113,7 +114,7 @@ void Audio::pauseMusic() { // Continua la música pausada void Audio::resumeMusic() { if (music_enabled_ && music_.state == MusicState::PAUSED) { - JA_ResumeMusic(); + Ja::resumeMusic(); music_.state = MusicState::PLAYING; } } @@ -121,7 +122,7 @@ void Audio::resumeMusic() { // Detiene la música void Audio::stopMusic() { if (music_enabled_) { - JA_StopMusic(); + Ja::stopMusic(); music_.state = MusicState::STOPPED; } } @@ -129,42 +130,42 @@ void Audio::stopMusic() { // Reproduce un sonido por nombre void Audio::playSound(const std::string& name, Group group) const { if (sound_enabled_) { - JA_PlaySound(AudioResource::getSound(name), 0, static_cast(group)); + Ja::playSound(AudioResource::getSound(name), 0, static_cast(group)); } } // Reproduce un sonido por puntero directo -void Audio::playSound(JA_Sound_t* sound, Group group) const { +void Audio::playSound(Ja::Sound* sound, Group group) const { if (sound_enabled_ && sound != nullptr) { - JA_PlaySound(sound, 0, static_cast(group)); + Ja::playSound(sound, 0, static_cast(group)); } } // Detiene todos los sonidos void Audio::stopAllSounds() const { if (sound_enabled_) { - JA_StopChannel(-1); + 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); + 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(); + Ja::MusicState ja_state = Ja::getMusicState(); switch (ja_state) { - case JA_MUSIC_PLAYING: + case Ja::MusicState::PLAYING: return MusicState::PLAYING; - case JA_MUSIC_PAUSED: + case Ja::MusicState::PAUSED: return MusicState::PAUSED; - case JA_MUSIC_STOPPED: - case JA_MUSIC_INVALID: - case JA_MUSIC_DISABLED: + case Ja::MusicState::STOPPED: + case Ja::MusicState::INVALID: + case Ja::MusicState::DISABLED: default: return MusicState::STOPPED; } @@ -175,7 +176,7 @@ 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(group)); + Ja::setSoundVolume(CONVERTED_VOLUME, static_cast(group)); } } @@ -184,7 +185,7 @@ 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); + Ja::setMusicVolume(CONVERTED_VOLUME); } } @@ -206,7 +207,7 @@ void Audio::initSDLAudio() { if (!SDL_Init(SDL_INIT_AUDIO)) { std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n'; } else { - JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2); + Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2); enable(Options::audio.enabled); } } diff --git a/source/core/audio/audio.hpp b/source/core/audio/audio.hpp index 9c5ea6f..7cb7656 100644 --- a/source/core/audio/audio.hpp +++ b/source/core/audio/audio.hpp @@ -1,8 +1,13 @@ #pragma once +#include // Para std::lround #include // Para int8_t, uint8_t #include // Para string -#include // Para move + +namespace Ja { + struct Music; + struct Sound; +} // namespace Ja // --- Clase Audio: gestor de audio (singleton) --- // Implementació canònica, byte-idèntica entre projectes. @@ -41,17 +46,17 @@ class Audio { static void update(); // Actualización del sistema de audio // --- Control de música --- - void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional) - void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional) - 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 + void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional) + void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional) + 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 + void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre + void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero + void stopAllSounds() const; // Detener todos los sonidos // --- Control de volumen (API interna: float 0.0..1.0) --- void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos @@ -59,8 +64,8 @@ class Audio { // --- Helpers de conversió per a la capa de presentació --- // UI (menús, notificacions) manega enters 0..100; internament viu float 0..1. - static constexpr auto toPercent(float volume) -> int { - return static_cast(volume * 100.0F + 0.5F); + static auto toPercent(float volume) -> int { + return static_cast(std::lround(volume * 100.0F)); } static constexpr auto fromPercent(int percent) -> float { return static_cast(percent) / 100.0F; diff --git a/source/core/audio/audio_adapter.cpp b/source/core/audio/audio_adapter.cpp index c46a008..a4026e0 100644 --- a/source/core/audio/audio_adapter.cpp +++ b/source/core/audio/audio_adapter.cpp @@ -3,11 +3,11 @@ #include "core/resources/resource.hpp" namespace AudioResource { - JA_Music_t* getMusic(const std::string& name) { + auto getMusic(const std::string& name) -> Ja::Music* { return Resource::get()->getMusic(name); } - JA_Sound_t* getSound(const std::string& name) { + auto getSound(const std::string& name) -> Ja::Sound* { return Resource::get()->getSound(name); } } // namespace AudioResource diff --git a/source/core/audio/audio_adapter.hpp b/source/core/audio/audio_adapter.hpp index a5eb16e..7feda5a 100644 --- a/source/core/audio/audio_adapter.hpp +++ b/source/core/audio/audio_adapter.hpp @@ -1,17 +1,19 @@ #pragma once // --- Audio Resource Adapter --- -// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* / -// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp +// Aquest fitxer exposa una interfície comuna a Audio per obtenir Ja::Music* / +// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp // delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(), // etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes. #include // Para string -struct JA_Music_t; -struct JA_Sound_t; +namespace Ja { + struct Music; + struct Sound; +} // namespace Ja namespace AudioResource { - JA_Music_t* getMusic(const std::string& name); - JA_Sound_t* getSound(const std::string& name); + auto getMusic(const std::string& name) -> Ja::Music*; + auto getSound(const std::string& name) -> Ja::Sound*; } // namespace AudioResource diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp index f6dc224..276841f 100644 --- a/source/core/audio/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -2,678 +2,698 @@ // --- Includes --- #include -#include // Para uint32_t, uint8_t -#include // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET -#include // Para free, malloc -#include // Para std::cout -#include // Para std::unique_ptr -#include // Para std::string -#include // Para std::vector +#include +#include +#include +#include +#include +#include +#include +#include #define STB_VORBIS_HEADER_ONLY +// NOLINTNEXTLINE(bugprone-suspicious-include) — stb_vorbis és single-file: la macro de dalt limita a només-declaracions. #include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming // Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`. -// Compatible amb `std::unique_ptr` — zero size +// Compatible amb `std::unique_ptr` — zero size // overhead gràcies a EBO, igual que un unique_ptr amb default_delete. -struct SDLFreeDeleter { +struct SdlFreeDeleter { void operator()(Uint8* p) const noexcept { - if (p) SDL_free(p); + if (p != nullptr) { SDL_free(p); } } }; -// --- 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, -}; +namespace Ja { -// --- Struct Definitions --- -#define JA_MAX_SIMULTANEOUS_CHANNELS 20 -#define JA_MAX_GROUPS 2 + // --- Public Enums --- + enum class ChannelState : std::uint8_t { + INVALID, + FREE, + PLAYING, + PAUSED, + DISABLED, + }; -struct JA_Sound_t { - SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; - Uint32 length{0}; - // Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV - // via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free. - std::unique_ptr buffer; -}; + enum class MusicState : std::uint8_t { + INVALID, + PLAYING, + PAUSED, + STOPPED, + DISABLED, + }; -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}; -}; + // --- Constants --- + inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20; + inline constexpr int MAX_GROUPS = 2; + inline constexpr SDL_AudioSpec DEFAULT_SPEC{.format = SDL_AUDIO_S16, .channels = 2, .freq = 48000}; -struct JA_Music_t { - SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; + // --- Struct Definitions --- + struct Sound { + SDL_AudioSpec spec{DEFAULT_SPEC}; + Uint32 length{0}; + // Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV + // via SDL_malloc; el deleter `SdlFreeDeleter` allibera amb SDL_free. + std::unique_ptr buffer; + }; - // OGG comprimit en memòria. Propietat nostra; es copia des del buffer - // d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks - // per streaming. Com que stb_vorbis guarda un punter persistent al - // `.data()` d'aquest vector, no el podem resize'jar un cop establert - // (una reallocation invalidaria el punter que el decoder conserva). - std::vector ogg_data; - stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t + // L'ordre (punters primer, ints després, enum de 8 bits al final) minimitza + // el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding). + struct Channel { + Sound* sound{nullptr}; + SDL_AudioStream* stream{nullptr}; + int pos{0}; + int times{0}; + int group{0}; + ChannelState state{ChannelState::FREE}; + }; - std::string filename; + struct Music { + SDL_AudioSpec spec{DEFAULT_SPEC}; - int times{0}; // loops restants (-1 = infinit, 0 = un sol play) - SDL_AudioStream* stream{nullptr}; - JA_Music_state state{JA_MUSIC_INVALID}; -}; + // OGG comprimit en memòria. Propietat nostra; es copia des del buffer + // d'entrada una sola vegada en loadMusic i es descomprimix en chunks + // per streaming. Com que stb_vorbis guarda un punter persistent al + // `.data()` d'aquest vector, no el podem resize'jar un cop establert + // (una reallocation invalidaria el punter que el decoder conserva). + std::vector ogg_data; + stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music -// --- Internal Global State (inline, C++17) --- + std::string filename; -inline JA_Music_t* current_music{nullptr}; -inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; + int times{0}; // loops restants (-1 = infinit, 0 = un sol play) + SDL_AudioStream* stream{nullptr}; + MusicState state{MusicState::INVALID}; + }; -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}; + struct FadeState { + bool active{false}; + Uint64 start_time{0}; + int duration_ms{0}; + float initial_volume{0.0F}; + }; -// --- Crossfade / Fade State --- -struct JA_FadeState { - bool active{false}; - Uint64 start_time{0}; - int duration_ms{0}; - float initial_volume{0.0f}; -}; + struct OutgoingMusic { + SDL_AudioStream* stream{nullptr}; + FadeState fade; + }; -struct JA_OutgoingMusic { - SDL_AudioStream* stream{nullptr}; - JA_FadeState fade; -}; + // --- Internal Global State (inline, C++17) --- + inline Music* current_music{nullptr}; + inline Channel channels[MAX_SIMULTANEOUS_CHANNELS]; -inline JA_OutgoingMusic outgoing_music; -inline JA_FadeState incoming_fade; + inline SDL_AudioSpec audio_spec{DEFAULT_SPEC}; + inline float music_volume{1.0F}; + inline float sound_volume[MAX_GROUPS]; + inline bool music_enabled{true}; + inline bool sound_enabled{true}; + inline SDL_AudioDeviceID sdl_audio_device{0}; -// --- 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); -inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1); + inline OutgoingMusic outgoing_music; + inline FadeState incoming_fade; -// --- Music streaming internals --- -// Bytes-per-sample per canal (sempre s16) -static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2; -// Quants shorts decodifiquem per crida a get_samples_short_interleaved. -// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz. -static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192; -// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a -// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. -static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f; + // --- Forward Declarations --- + inline void stopMusic(); + inline void stopChannel(int channel); + inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; + inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); -// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples -// decodificats per canal (0 = EOF de l'stream vorbis). -inline int JA_FeedMusicChunk(JA_Music_t* music) { - if (!music || !music->vorbis || !music->stream) return 0; + // --- Music streaming internals --- + // Bytes-per-sample per canal (sempre s16) + inline constexpr int MUSIC_BYTES_PER_SAMPLE = 2; + // Quants shorts decodifiquem per crida a get_samples_short_interleaved. + // 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz. + inline constexpr int MUSIC_CHUNK_SHORTS = 8192; + // Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a + // l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. + inline constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F; - short chunk[JA_MUSIC_CHUNK_SHORTS]; - const int num_channels = music->spec.channels; - const int samples_per_channel = stb_vorbis_get_samples_short_interleaved( - music->vorbis, - num_channels, - chunk, - JA_MUSIC_CHUNK_SHORTS); - if (samples_per_channel <= 0) return 0; + // Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples + // decodificats per canal (0 = EOF de l'stream vorbis). + inline auto feedMusicChunk(Music* music) -> int { + if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return 0; } - const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE; - SDL_PutAudioStreamData(music->stream, chunk, bytes); - return samples_per_channel; -} + short chunk[MUSIC_CHUNK_SHORTS]; + const int NUM_CHANNELS = music->spec.channels; + const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved( + music->vorbis, + NUM_CHANNELS, + chunk, + MUSIC_CHUNK_SHORTS); + if (SAMPLES_PER_CHANNEL <= 0) { return 0; } -// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats. -// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. -inline void JA_PumpMusic(JA_Music_t* music) { - if (!music || !music->vorbis || !music->stream) return; + const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE; + SDL_PutAudioStreamData(music->stream, chunk, BYTES); + return SAMPLES_PER_CHANNEL; + } - const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE; - const int low_water_bytes = static_cast(JA_MUSIC_LOW_WATER_SECONDS * static_cast(bytes_per_second)); + // Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats. + // En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. + inline void pumpMusic(Music* music) { + if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; } - while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) { - const int decoded = JA_FeedMusicChunk(music); - if (decoded > 0) continue; + const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; + const int LOW_WATER_BYTES = static_cast(MUSIC_LOW_WATER_SECONDS * static_cast(BYTES_PER_SECOND)); - // EOF: si queden loops, rebobinar; si no, tallar i deixar drenar. - if (music->times != 0) { - stb_vorbis_seek_start(music->vorbis); - if (music->times > 0) music->times--; - } else { - break; + while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) { + const int DECODED = feedMusicChunk(music); + if (DECODED > 0) { continue; } + + // EOF: si queden loops, rebobinar; si no, tallar i deixar drenar. + if (music->times != 0) { + stb_vorbis_seek_start(music->vorbis); + if (music->times > 0) { music->times--; } + } else { + break; + } } } -} -// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream -// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb -// streaming: l'stream robat no es pot re-alimentar perquè perd la referència -// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem. -inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) { - if (!music || !music->vorbis || !music->stream) return; + // Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream + // siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb + // streaming: l'stream robat no es pot re-alimentar perquè perd la referència + // al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem. + inline void preFillOutgoing(Music* music, int duration_ms) { + if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; } - const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE; - const int needed_bytes = static_cast((static_cast(duration_ms) * bytes_per_second) / 1000); + const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; + const int NEEDED_BYTES = static_cast((static_cast(duration_ms) * BYTES_PER_SECOND) / 1000); - while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) { - const int decoded = JA_FeedMusicChunk(music); - if (decoded <= 0) break; // EOF: deixem drenar el que hi haja + while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) { + const int DECODED = feedMusicChunk(music); + if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja + } } -} -// --- Core Functions --- + // --- update() helpers --- + inline void updateOutgoingFade() { + if ((outgoing_music.stream == nullptr) || !outgoing_music.fade.active) { return; } -inline void JA_Update() { - // --- Outgoing music fade-out (crossfade o fade-out a silencio) --- - if (outgoing_music.stream && outgoing_music.fade.active) { - Uint64 now = SDL_GetTicks(); - Uint64 elapsed = now - outgoing_music.fade.start_time; - if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) { + const Uint64 NOW = SDL_GetTicks(); + const Uint64 ELAPSED = NOW - outgoing_music.fade.start_time; + if (ELAPSED >= static_cast(outgoing_music.fade.duration_ms)) { SDL_DestroyAudioStream(outgoing_music.stream); outgoing_music.stream = nullptr; outgoing_music.fade.active = false; } else { - float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms; - SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent)); + const float PERCENT = static_cast(ELAPSED) / static_cast(outgoing_music.fade.duration_ms); + SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PERCENT)); } } - // --- Current music --- - if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { - // Fade-in (parte de un crossfade) - if (incoming_fade.active) { - Uint64 now = SDL_GetTicks(); - Uint64 elapsed = now - incoming_fade.start_time; - if (elapsed >= (Uint64)incoming_fade.duration_ms) { - incoming_fade.active = false; - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); - } else { - float percent = (float)elapsed / (float)incoming_fade.duration_ms; - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent); - } + inline void updateIncomingFade() { + if (!incoming_fade.active) { return; } + + const Uint64 NOW = SDL_GetTicks(); + const Uint64 ELAPSED = NOW - incoming_fade.start_time; + if (ELAPSED >= static_cast(incoming_fade.duration_ms)) { + incoming_fade.active = false; + SDL_SetAudioStreamGain(current_music->stream, music_volume); + } else { + const float PERCENT = static_cast(ELAPSED) / static_cast(incoming_fade.duration_ms); + SDL_SetAudioStreamGain(current_music->stream, music_volume * PERCENT); } + } + + inline void updateCurrentMusic() { + if (!music_enabled || (current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; } + + updateIncomingFade(); // Streaming: rellenem l'stream fins al low-water-mark i parem si el // vorbis s'ha esgotat i no queden loops. - JA_PumpMusic(current_music); + pumpMusic(current_music); if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { - JA_StopMusic(); + stopMusic(); } } - // --- Sound channels --- - 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.get(), 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 updateSoundChannels() { + if (!sound_enabled) { return; } + + for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { + auto& ch = channels[i]; + if (ch.state != ChannelState::PLAYING) { continue; } + + if (ch.times != 0) { + if (static_cast(SDL_GetAudioStreamAvailable(ch.stream)) < (ch.sound->length / 2)) { + SDL_PutAudioStreamData(ch.stream, ch.sound->buffer.get(), ch.sound->length); + if (ch.times > 0) { ch.times--; } } + } else { + if (SDL_GetAudioStreamAvailable(ch.stream) == 0) { stopChannel(i); } } - } -} - -inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { - JA_audioSpec = {format, num_channels, freq}; - if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); - sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); - if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n'; - 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 (outgoing_music.stream) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - } - if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); - sdlAudioDevice = 0; -} - -// --- Music Functions --- - -inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { - if (!buffer || length == 0) return nullptr; - - // Allocem el JA_Music_t primer per aprofitar el seu `std::vector` - // com a propietari del OGG comprimit. stb_vorbis guarda un punter - // persistent al buffer; com que ací no el resize'jem, el .data() és - // estable durant tot el cicle de vida del music. - auto* music = new JA_Music_t(); - music->ogg_data.assign(buffer, buffer + length); - - int err_code = 0; - music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), - static_cast(length), - &err_code, - nullptr); - if (!music->vorbis) { - std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << err_code << ")" << '\n'; - delete music; - return nullptr; + } } - const stb_vorbis_info info = stb_vorbis_get_info(music->vorbis); - music->spec.channels = info.channels; - music->spec.freq = static_cast(info.sample_rate); - music->spec.format = SDL_AUDIO_S16; - music->state = JA_MUSIC_STOPPED; - - return music; -} - -// Overload amb filename — els callers l'usen per poder comparar la música -// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa. -inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) { - JA_Music_t* music = JA_LoadMusic(static_cast(buffer), length); - if (music && filename) music->filename = filename; - return music; -} - -inline JA_Music_t* JA_LoadMusic(const char* filename) { - // Carreguem primer el arxiu en memòria i després el descomprimim. - FILE* f = fopen(filename, "rb"); - if (!f) return nullptr; - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - auto* buffer = static_cast(malloc(fsize + 1)); - if (!buffer) { - fclose(f); - return nullptr; - } - if (fread(buffer, fsize, 1, f) != 1) { - fclose(f); - free(buffer); - return nullptr; - } - fclose(f); - - JA_Music_t* music = JA_LoadMusic(static_cast(buffer), static_cast(fsize)); - if (music) { - music->filename = filename; + inline void update() { + updateOutgoingFade(); + updateCurrentMusic(); + updateSoundChannels(); } - free(buffer); - - return music; -} - -inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { - if (!JA_musicEnabled || !music || !music->vorbis) return; - - JA_StopMusic(); - - current_music = music; - current_music->state = JA_MUSIC_PLAYING; - current_music->times = loop; - - // Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera- - // vegada com replays/canvis de track que tornen a la mateixa pista. - stb_vorbis_seek_start(current_music->vorbis); - - current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); - if (!current_music->stream) { - std::cout << "Failed to create audio stream!" << '\n'; - current_music->state = JA_MUSIC_STOPPED; - return; + inline void init(int freq, SDL_AudioFormat format, int num_channels) { + audio_spec = {.format = format, .channels = num_channels, .freq = freq}; + if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } + sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec); + if (sdl_audio_device == 0) { std::cout << "Failed to initialize SDL audio!" << '\n'; } + for (auto& ch : channels) { ch.state = ChannelState::FREE; } + std::fill(std::begin(sound_volume), std::end(sound_volume), 0.5F); } - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); - // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. - JA_PumpMusic(current_music); - - if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) { - std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n'; + inline void quit() { + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + } + if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } + sdl_audio_device = 0; } -} -inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) { - if (!music) music = current_music; - if (!music || music->filename.empty()) return nullptr; - return music->filename.c_str(); -} + // --- Music Functions --- + inline auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* { + if ((buffer == nullptr) || length == 0) { return nullptr; } -inline void JA_PauseMusic() { - if (!JA_musicEnabled) return; - if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; + // Allocem el Music primer per aprofitar el seu `std::vector` + // com a propietari del OGG comprimit. stb_vorbis guarda un punter + // persistent al buffer; com que ací no el resize'jem, el .data() és + // estable durant tot el cicle de vida del music. + auto* music = new Music(); + music->ogg_data.assign(buffer, buffer + length); - current_music->state = JA_MUSIC_PAUSED; - SDL_UnbindAudioStream(current_music->stream); -} + int vorbis_error = 0; + music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), + static_cast(length), + &vorbis_error, + nullptr); + if (music->vorbis == nullptr) { + std::cout << "loadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n'; + delete music; + return nullptr; + } -inline void JA_ResumeMusic() { - if (!JA_musicEnabled) return; - if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; + const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis); + music->spec.channels = INFO.channels; + music->spec.freq = static_cast(INFO.sample_rate); + music->spec.format = SDL_AUDIO_S16; + music->state = MusicState::STOPPED; - current_music->state = JA_MUSIC_PLAYING; - SDL_BindAudioStream(sdlAudioDevice, current_music->stream); -} - -inline void JA_StopMusic() { - // Limpiar outgoing crossfade si existe - if (outgoing_music.stream) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; + return music; } - incoming_fade.active = false; - if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return; - - current_music->state = JA_MUSIC_STOPPED; - if (current_music->stream) { - SDL_DestroyAudioStream(current_music->stream); - current_music->stream = nullptr; + // Overload amb filename — els callers l'usen per poder comparar la música + // en curs amb getMusicFilename() i no rearrancar-la si ja és la mateixa. + inline auto loadMusic(Uint8* buffer, Uint32 length, const char* filename) -> Music* { + Music* music = loadMusic(static_cast(buffer), length); + if ((music != nullptr) && (filename != nullptr)) { music->filename = filename; } + return music; } - // Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic. - // Rebobinem perquè un futur JA_PlayMusic comence des del principi. - if (current_music->vorbis) { + + inline auto loadMusic(const char* filename) -> Music* { + // Carreguem primer el arxiu en memòria i després el descomprimim. + FILE* f = std::fopen(filename, "rb"); + if (f == nullptr) { return nullptr; } + std::fseek(f, 0, SEEK_END); + const long FSIZE = std::ftell(f); + std::fseek(f, 0, SEEK_SET); + if (FSIZE <= 0) { + std::fclose(f); + return nullptr; + } + auto* buffer = static_cast(std::malloc(static_cast(FSIZE) + 1)); + if (buffer == nullptr) { + std::fclose(f); + return nullptr; + } + if (std::fread(buffer, FSIZE, 1, f) != 1) { + std::fclose(f); + std::free(buffer); + return nullptr; + } + std::fclose(f); + + Music* music = loadMusic(static_cast(buffer), static_cast(FSIZE)); + if (music != nullptr) { music->filename = filename; } + + std::free(buffer); + + return music; + } + + inline void playMusic(Music* music, int loop = -1) { + if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; } + + stopMusic(); + + current_music = music; + current_music->state = MusicState::PLAYING; + current_music->times = loop; + + // Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera- + // vegada com replays/canvis de track que tornen a la mateixa pista. stb_vorbis_seek_start(current_music->vorbis); - } -} -inline void JA_FadeOutMusic(const int milliseconds) { - if (!JA_musicEnabled) return; - if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec); + if (current_music->stream == nullptr) { + std::cout << "Failed to create audio stream!" << '\n'; + current_music->state = MusicState::STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music->stream, music_volume); - // Destruir outgoing anterior si existe - if (outgoing_music.stream) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; + // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. + pumpMusic(current_music); + + if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) { + std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n'; + } } - // Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no - // tindrà accés al vorbis decoder i només podrà drenar el que tinga. - JA_PreFillOutgoing(current_music, milliseconds); - - // Robar el stream del current_music al outgoing - outgoing_music.stream = current_music->stream; - outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume}; - - // Dejar current_music sin stream (ya lo tiene outgoing) - current_music->stream = nullptr; - current_music->state = JA_MUSIC_STOPPED; - if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis); - incoming_fade.active = false; -} - -inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) { - if (!JA_musicEnabled || !music || !music->vorbis) return; - - // Destruir outgoing anterior si existe (crossfade durante crossfade) - if (outgoing_music.stream) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; + inline auto getMusicFilename(const Music* music = nullptr) -> const char* { + if (music == nullptr) { music = current_music; } + if ((music == nullptr) || music->filename.empty()) { return nullptr; } + return music->filename.c_str(); } - // Robar el stream de la musica actual al outgoing para el fade-out. - // Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci - // abans d'acabar el fade (l'stream robat ja no pot alimentar-se). - if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) { - JA_PreFillOutgoing(current_music, crossfade_ms); + inline void pauseMusic() { + if (!music_enabled) { return; } + if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; } + + current_music->state = MusicState::PAUSED; + SDL_UnbindAudioStream(current_music->stream); + } + + inline void resumeMusic() { + if (!music_enabled) { return; } + if ((current_music == nullptr) || current_music->state != MusicState::PAUSED) { return; } + + current_music->state = MusicState::PLAYING; + SDL_BindAudioStream(sdl_audio_device, current_music->stream); + } + + inline void stopMusic() { + // Limpiar outgoing crossfade si existe + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } + incoming_fade.active = false; + + if ((current_music == nullptr) || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; } + + current_music->state = MusicState::STOPPED; + if (current_music->stream != nullptr) { + SDL_DestroyAudioStream(current_music->stream); + current_music->stream = nullptr; + } + // Deixem el handle de vorbis viu — es tanca en deleteMusic. + // Rebobinem perquè un futur playMusic comence des del principi. + if (current_music->vorbis != nullptr) { + stb_vorbis_seek_start(current_music->vorbis); + } + } + + inline void fadeOutMusic(int milliseconds) { + if (!music_enabled) { return; } + if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; } + + // Destruir outgoing anterior si existe + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + } + + // Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no + // tindrà accés al vorbis decoder i només podrà drenar el que tinga. + preFillOutgoing(current_music, milliseconds); + + // Robar el stream del current_music al outgoing outgoing_music.stream = current_music->stream; - outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume}; + outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = milliseconds, .initial_volume = music_volume}; + + // Dejar current_music sin stream (ya lo tiene outgoing) current_music->stream = nullptr; - current_music->state = JA_MUSIC_STOPPED; - if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis); + current_music->state = MusicState::STOPPED; + if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); } + incoming_fade.active = false; } - // Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente) - current_music = music; - current_music->state = JA_MUSIC_PLAYING; - current_music->times = loop; + inline void crossfadeMusic(Music* music, int crossfade_ms, int loop) { + if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; } - stb_vorbis_seek_start(current_music->vorbis); - current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); - if (!current_music->stream) { - std::cout << "Failed to create audio stream for crossfade!" << '\n'; - current_music->state = JA_MUSIC_STOPPED; - return; - } - SDL_SetAudioStreamGain(current_music->stream, 0.0f); - JA_PumpMusic(current_music); // pre-carrega abans de bindejar - SDL_BindAudioStream(sdlAudioDevice, current_music->stream); - - // Configurar fade-in - incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f}; -} - -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; - } - if (music->stream) SDL_DestroyAudioStream(music->stream); - if (music->vorbis) stb_vorbis_close(music->vorbis); - // ogg_data (std::vector) i filename (std::string) s'alliberen sols - // al destructor de JA_Music_t. - 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*/) { - // No implementat amb el backend de streaming. -} - -inline float JA_GetMusicPosition() { - return 0.0f; -} - -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_LoadSound(uint8_t* buffer, uint32_t size) { - auto sound = std::make_unique(); - Uint8* raw = nullptr; - if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) { - std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n'; - return nullptr; - } - sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer - return sound.release(); -} - -inline JA_Sound_t* JA_LoadSound(const char* filename) { - auto sound = std::make_unique(); - Uint8* raw = nullptr; - if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) { - std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n'; - return nullptr; - } - sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer - return sound.release(); -} - -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); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].group = group; - channels[channel].state = JA_CHANNEL_PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); - - if (!channels[channel].stream) { - std::cout << "Failed to create audio stream for sound!" << '\n'; - channels[channel].state = JA_CHANNEL_FREE; - return -1; - } - - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), 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); - } - // buffer es destrueix automàticament via RAII (SDLFreeDeleter). - 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); + // Destruir outgoing anterior si existe (crossfade durante crossfade) + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; } - } -} -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); + // Robar el stream de la musica actual al outgoing para el fade-out. + // Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci + // abans d'acabar el fade (l'stream robat ja no pot alimentar-se). + if ((current_music != nullptr) && current_music->state == MusicState::PLAYING && (current_music->stream != nullptr)) { + preFillOutgoing(current_music, crossfade_ms); + outgoing_music.stream = current_music->stream; + outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = music_volume}; + current_music->stream = nullptr; + current_music->state = MusicState::STOPPED; + if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); } } - } -} -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 = nullptr; + // Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente) + current_music = music; + current_music->state = MusicState::PLAYING; + current_music->times = loop; + + stb_vorbis_seek_start(current_music->vorbis); + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec); + if (current_music->stream == nullptr) { + std::cout << "Failed to create audio stream for crossfade!" << '\n'; + current_music->state = MusicState::STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music->stream, 0.0F); + pumpMusic(current_music); // pre-carrega abans de bindejar + SDL_BindAudioStream(sdl_audio_device, current_music->stream); + + // Configurar fade-in + incoming_fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = 0.0F}; + } + + inline auto getMusicState() -> MusicState { + if (!music_enabled) { return MusicState::DISABLED; } + if (current_music == nullptr) { return MusicState::INVALID; } + + return current_music->state; + } + + inline void deleteMusic(Music* music) { + if (music == nullptr) { return; } + if (current_music == music) { + stopMusic(); + current_music = nullptr; + } + if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); } + if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); } + // ogg_data (std::vector) i filename (std::string) s'alliberen sols + // al destructor de Music. + delete music; + } + + inline auto setMusicVolume(float volume) -> float { + music_volume = SDL_clamp(volume, 0.0F, 1.0F); + if ((current_music != nullptr) && (current_music->stream != nullptr)) { + SDL_SetAudioStreamGain(current_music->stream, music_volume); + } + return music_volume; + } + + inline void setMusicPosition(float /*value*/) { + // No implementat amb el backend de streaming. + } + + inline auto getMusicPosition() -> float { + return 0.0F; + } + + inline void enableMusic(bool value) { + if (!value && (current_music != nullptr) && (current_music->state == MusicState::PLAYING)) { stopMusic(); } + music_enabled = value; + } + + // --- Sound Functions --- + inline auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* { + auto sound = std::make_unique(); + Uint8* raw = nullptr; + if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) { + std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n'; + return nullptr; + } + sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer + return sound.release(); + } + + inline auto loadSound(const char* filename) -> Sound* { + auto sound = std::make_unique(); + Uint8* raw = nullptr; + if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) { + std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n'; + return nullptr; + } + sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer + return sound.release(); + } + + inline auto playSound(Sound* sound, int loop = 0, int group = 0) -> int { + if (!sound_enabled || (sound == nullptr)) { return -1; } + + int channel = 0; + while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; } + if (channel == MAX_SIMULTANEOUS_CHANNELS) { + // No hi ha canal lliure, reemplacem el primer + channel = 0; + } + + return playSoundOnChannel(sound, channel, loop, group); + } + + inline auto playSoundOnChannel(Sound* sound, int channel, int loop, int group) -> int { + if (!sound_enabled || (sound == nullptr)) { return -1; } + if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; } + + stopChannel(channel); + + channels[channel].sound = sound; + channels[channel].times = loop; + channels[channel].pos = 0; + channels[channel].group = group; + channels[channel].state = ChannelState::PLAYING; + channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec); + + if (channels[channel].stream == nullptr) { + std::cout << "Failed to create audio stream for sound!" << '\n'; + channels[channel].state = ChannelState::FREE; + return -1; + } + + SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length); + SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]); + SDL_BindAudioStream(sdl_audio_device, channels[channel].stream); + + return channel; + } + + inline void deleteSound(Sound* sound) { + if (sound == nullptr) { return; } + for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) { + if (channels[i].sound == sound) { stopChannel(i); } + } + // buffer es destrueix automàticament via RAII (SdlFreeDeleter). + delete sound; + } + + inline void pauseChannel(int channel) { + if (!sound_enabled) { return; } + + if (channel == -1) { + for (auto& ch : channels) { + if (ch.state == ChannelState::PLAYING) { + ch.state = ChannelState::PAUSED; + SDL_UnbindAudioStream(ch.stream); + } + } + } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state == ChannelState::PLAYING) { + channels[channel].state = ChannelState::PAUSED; + SDL_UnbindAudioStream(channels[channel].stream); } } - } 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 = nullptr; - } - } -} - -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) { - 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; } - // Aplicar volum als canals actius. - 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]); + inline void resumeChannel(int channel) { + if (!sound_enabled) { return; } + + if (channel == -1) { + for (auto& ch : channels) { + if (ch.state == ChannelState::PAUSED) { + ch.state = ChannelState::PLAYING; + SDL_BindAudioStream(sdl_audio_device, ch.stream); + } + } + } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state == ChannelState::PAUSED) { + channels[channel].state = ChannelState::PLAYING; + SDL_BindAudioStream(sdl_audio_device, channels[channel].stream); + } + } + } + + inline void stopChannel(int channel) { + if (channel == -1) { + for (auto& ch : channels) { + if (ch.state != ChannelState::FREE) { + if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); } + ch.stream = nullptr; + ch.state = ChannelState::FREE; + ch.pos = 0; + ch.sound = nullptr; + } + } + } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state != ChannelState::FREE) { + if (channels[channel].stream != nullptr) { SDL_DestroyAudioStream(channels[channel].stream); } + channels[channel].stream = nullptr; + channels[channel].state = ChannelState::FREE; + channels[channel].pos = 0; + channels[channel].sound = nullptr; + } + } + } + + inline auto getChannelState(int channel) -> ChannelState { + if (!sound_enabled) { return ChannelState::DISABLED; } + if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return ChannelState::INVALID; } + + return channels[channel].state; + } + + inline auto setSoundVolume(float volume, int group = -1) -> float { + const float V = SDL_clamp(volume, 0.0F, 1.0F); + + if (group == -1) { + std::fill(std::begin(sound_volume), std::end(sound_volume), V); + } else if (group >= 0 && group < MAX_GROUPS) { + sound_volume[group] = V; + } else { + return V; + } + + // Aplicar volum als canals actius. + for (auto& ch : channels) { + if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) { + if (group == -1 || ch.group == group) { + if (ch.stream != nullptr) { + SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]); + } } } } + return V; } - return v; -} -inline void JA_EnableSound(const bool value) { - if (!value) { - JA_StopChannel(-1); + inline void enableSound(bool value) { + if (!value) { + stopChannel(-1); + } + sound_enabled = value; } - JA_soundEnabled = value; -} -inline float JA_SetVolume(float volume) { - float v = JA_SetMusicVolume(volume); - JA_SetSoundVolume(v, -1); - return v; -} + inline auto setVolume(float volume) -> float { + const float V = setMusicVolume(volume); + setSoundVolume(V, -1); + return V; + } + +} // namespace Ja diff --git a/source/core/input/define_buttons.cpp b/source/core/input/define_buttons.cpp index 750b346..ca028fd 100644 --- a/source/core/input/define_buttons.cpp +++ b/source/core/input/define_buttons.cpp @@ -5,7 +5,6 @@ #include // Para unique_ptr, allocator, shared_ptr, operator==, make_unique #include "core/input/input.hpp" // Para Input -#include "core/input/input_types.hpp" // Para InputAction #include "core/locale/lang.hpp" // Para getText #include "core/resources/resource.hpp" // Para Resource #include "game/options.hpp" // Para Gamepad @@ -17,9 +16,9 @@ DefineButtons::DefineButtons() : input_(Input::get()) { clearButtons(); - const auto gamepads = input_->getGamepads(); - controller_names_.reserve(gamepads.size()); - std::ranges::transform(gamepads, std::back_inserter(controller_names_), Input::getControllerName); + const auto GAMEPADS = input_->getGamepads(); + controller_names_.reserve(GAMEPADS.size()); + std::ranges::transform(GAMEPADS, std::back_inserter(controller_names_), Input::getControllerName); // Crear la ventana de mensaje WindowMessage::Config config(param.service_menu.window_message); diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 090e395..ba4cd70 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -2,14 +2,12 @@ #include // Para __any_of_fn, any_of #include // Para function -#include // Para pair #include // Para basic_string, operator+, allocator, char_traits, string, to_string #include // Para pair #include // Para vector #include "core/audio/audio.hpp" // Para Audio #include "core/input/input.hpp" // Para Input -#include "core/input/input_types.hpp" // Para InputAction #include "core/locale/lang.hpp" // Para getText, getLangFile, getLangName, getNextLangCode, loadFromFile #include "core/rendering/screen.hpp" // Para Screen #include "core/system/section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode @@ -84,12 +82,12 @@ namespace GlobalInputs { void nextPreset() { if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) { Screen::nextCrtPiPreset(); - const std::string name = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast(Options::video.shader.current_crtpi_preset)).name; - Notifier::get()->show({"CrtPi: " + name}); + const std::string NAME = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast(Options::video.shader.current_crtpi_preset)).name; + Notifier::get()->show({"CrtPi: " + NAME}); } else { Screen::nextPostFXPreset(); - const std::string name = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast(Options::video.shader.current_postfx_preset)).name; - Notifier::get()->show({"PostFX: " + name}); + const std::string NAME = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast(Options::video.shader.current_postfx_preset)).name; + Notifier::get()->show({"PostFX: " + NAME}); } } diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 0ef77fe..9fd3f07 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -157,15 +157,15 @@ 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 { - const auto it = std::ranges::find_if(gamepads_, + const auto IT = std::ranges::find_if(gamepads_, [id](const auto& gamepad) { return gamepad->instance_id == id; }); - return it != gamepads_.end() ? *it : nullptr; + return IT != gamepads_.end() ? *IT : nullptr; } auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr { - const auto it = std::ranges::find_if(gamepads_, + const auto IT = std::ranges::find_if(gamepads_, [&name](const auto& gamepad) { return gamepad && gamepad->name == name; }); - return it != gamepads_.end() ? *it : nullptr; + return IT != gamepads_.end() ? *IT : nullptr; } // Obtiene el SDL_GamepadButton asignado a un action @@ -360,8 +360,9 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string { return addGamepad(event.gdevice.which); case SDL_EVENT_GAMEPAD_REMOVED: return removeGamepad(event.gdevice.which); + default: + return {}; } - return {}; } auto Input::addGamepad(int device_index) -> std::string { diff --git a/source/core/input/input.hpp b/source/core/input/input.hpp index 52bd6d0..faeb6f0 100644 --- a/source/core/input/input.hpp +++ b/source/core/input/input.hpp @@ -6,7 +6,6 @@ #include // Para shared_ptr #include // Para string, basic_string #include // Para unordered_map -#include // Para pair #include // Para vector #include "core/input/gamepad_config_manager.hpp" // for GamepadConfig (ptr only), GamepadConfigs @@ -109,8 +108,8 @@ class Input { // Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones. static auto trimName(const char* raw) -> std::string { std::string s(raw != nullptr ? raw : ""); - const auto pos = s.find_first_of("(["); - if (pos != std::string::npos) { s.erase(pos); } + const auto POS = s.find_first_of("(["); + if (POS != std::string::npos) { s.erase(POS); } while (!s.empty() && s.back() == ' ') { s.pop_back(); } return s; } diff --git a/source/core/input/input_types.hpp b/source/core/input/input_types.hpp index c46ced7..acfa601 100644 --- a/source/core/input/input_types.hpp +++ b/source/core/input/input_types.hpp @@ -2,11 +2,12 @@ #include +#include #include #include // --- Enums --- -enum class InputAction : int { // Acciones de entrada posibles en el juego +enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el juego // Inputs de movimiento UP, DOWN, diff --git a/source/core/locale/lang.cpp b/source/core/locale/lang.cpp index 97eaa53..199b150 100644 --- a/source/core/locale/lang.cpp +++ b/source/core/locale/lang.cpp @@ -14,7 +14,7 @@ #include "game/gameplay/difficulty.hpp" // Para Difficulty #include "game/options.hpp" // Para SettingsOpt... -using json = nlohmann::json; +using Json = nlohmann::json; namespace Lang { std::unordered_map texts; @@ -33,12 +33,12 @@ namespace Lang { auto resource_data = ResourceHelper::loadFile(file_path); try { - json j; + Json j; if (!resource_data.empty()) { // Cargar desde datos del pack std::string content(resource_data.begin(), resource_data.end()); - j = json::parse(content); + j = Json::parse(content); } else { // Fallback a filesystem directo std::ifstream rfile(file_path); @@ -81,23 +81,23 @@ namespace Lang { // Obtiene un idioma del vector de idiomas a partir de un código auto getLanguage(Code code) -> Language { - const auto it = std::ranges::find_if(languages, + const auto IT = std::ranges::find_if(languages, [code](const auto& lang) { return lang.code == code; }); - return it != languages.end() ? *it : languages[0]; + return IT != languages.end() ? *IT : languages[0]; } // Devuelve el código de un idioma a partir de un nombre auto getCodeFromName(const std::string& name) -> Code { - const auto it = std::ranges::find_if(languages, + const auto IT = std::ranges::find_if(languages, [&name](const auto& lang) { return lang.name == name; }); - return it != languages.end() ? it->code : languages[0].code; + return IT != languages.end() ? IT->code : languages[0].code; } // Devuelve el nombre de un idioma a partir de un código auto getNameFromCode(Code code) -> std::string { - const auto it = std::ranges::find_if(languages, + const auto IT = std::ranges::find_if(languages, [code](const auto& lang) { return lang.code == code; }); - return it != languages.end() ? it->name : languages[0].name; + return IT != languages.end() ? IT->name : languages[0].name; } // Actualiza los nombres de los idiomas @@ -144,9 +144,9 @@ namespace Lang { // Obtiene una fichero a partir de un lang::Code auto getLanguageFileName(Lang::Code code) -> std::string { - const auto it = std::ranges::find_if(languages, + const auto IT = std::ranges::find_if(languages, [code](const auto& lang) { return lang.code == code; }); - const auto& file = (it != languages.end()) ? it->file_name : languages[0].file_name; + const auto& file = (IT != languages.end()) ? IT->file_name : languages[0].file_name; return Asset::get()->getPath(file); } diff --git a/source/core/locale/lang.hpp b/source/core/locale/lang.hpp index 35e6033..3afaf83 100644 --- a/source/core/locale/lang.hpp +++ b/source/core/locale/lang.hpp @@ -1,12 +1,13 @@ #pragma once +#include // Para std::uint8_t #include // Para string, basic_string #include // Para move // --- Namespace Lang: gestión de idiomas y textos --- namespace Lang { // --- Enums --- - enum class Code : int { + enum class Code : std::uint8_t { SPANISH = 0, // Español VALENCIAN = 1, // Valenciano ENGLISH = 2 // Inglés diff --git a/source/core/rendering/background.cpp b/source/core/rendering/background.cpp index 86cebcb..61c66bb 100644 --- a/source/core/rendering/background.cpp +++ b/source/core/rendering/background.cpp @@ -29,10 +29,10 @@ Background::Background(float total_progress_to_complete) moon_texture_(Resource::get()->getTexture("game_moon.png")), grass_sprite_(std::make_unique(Resource::get()->getTexture("game_grass.png"), Resource::get()->getAnimation("game_grass.ani"))), - total_progress_to_complete_(total_progress_to_complete), - progress_per_stage_(total_progress_to_complete_ / STAGES), - sun_completion_progress_(total_progress_to_complete_ * SUN_COMPLETION_FACTOR), - minimum_completed_progress_(total_progress_to_complete_ * MINIMUM_COMPLETED_PROGRESS_PERCENTAGE), + TOTAL_PROGRESS_TO_COMPLETE(total_progress_to_complete), + PROGRESS_PER_STAGE(TOTAL_PROGRESS_TO_COMPLETE / STAGES), + SUM_COMPLETION_PROGRESS(TOTAL_PROGRESS_TO_COMPLETE * SUN_COMPLETION_FACTOR), + MINIMUM_COMPLETED_PROGRESS(TOTAL_PROGRESS_TO_COMPLETE * MINIMUM_COMPLETED_PROGRESS_PERCENTAGE), rect_(SDL_FRect{.x = 0, .y = 0, .w = static_cast(gradients_texture_->getWidth() / 2), .h = static_cast(gradients_texture_->getHeight() / 2)}), src_rect_({.x = 0, .y = 0, .w = 320, .h = 240}), @@ -167,7 +167,7 @@ void Background::incrementProgress(float amount) { if (state_ == State::NORMAL) { float old_progress = progress_; progress_ += amount; - progress_ = std::min(progress_, total_progress_to_complete_); + progress_ = std::min(progress_, TOTAL_PROGRESS_TO_COMPLETE); // Notifica el cambio si hay callback y el progreso cambió if (progress_callback_ && progress_ != old_progress) { @@ -179,7 +179,7 @@ void Background::incrementProgress(float amount) { // Establece la progresión absoluta void Background::setProgress(float absolute_progress) { float old_progress = progress_; - progress_ = std::clamp(absolute_progress, 0.0F, total_progress_to_complete_); + progress_ = std::clamp(absolute_progress, 0.0F, TOTAL_PROGRESS_TO_COMPLETE); // Notifica el cambio si hay callback y el progreso cambió if (progress_callback_ && progress_ != old_progress) { @@ -282,27 +282,27 @@ void Background::updateProgression(float delta_time) { float eased_t = easeOutCubic(static_cast(t)); // Interpolación desde progreso inicial hasta mínimo - float progress_range = completion_initial_progress_ - minimum_completed_progress_; + float progress_range = completion_initial_progress_ - MINIMUM_COMPLETED_PROGRESS; progress_ = completion_initial_progress_ - (progress_range * eased_t); } else { // Transición completada, fijar al valor mínimo - progress_ = minimum_completed_progress_; + progress_ = MINIMUM_COMPLETED_PROGRESS; } } // Calcula la transición de los diferentes fondos - const float GRADIENT_NUMBER_FLOAT = std::min(progress_ / progress_per_stage_, 3.0F); + const float GRADIENT_NUMBER_FLOAT = std::min(progress_ / PROGRESS_PER_STAGE, 3.0F); const float PERCENT = GRADIENT_NUMBER_FLOAT - static_cast(GRADIENT_NUMBER_FLOAT); gradient_number_ = static_cast(GRADIENT_NUMBER_FLOAT); transition_ = PERCENT; // Calcula la posición del sol - const float SUN_PROGRESSION = std::min(progress_ / sun_completion_progress_, 1.0F); + const float SUN_PROGRESSION = std::min(progress_ / SUM_COMPLETION_PROGRESS, 1.0F); sun_index_ = static_cast(SUN_PROGRESSION * (sun_path_.size() - 1)); // Calcula la posición de la luna - const float MOON_PROGRESSION = std::min(progress_ / total_progress_to_complete_, 1.0F); + const float MOON_PROGRESSION = std::min(progress_ / TOTAL_PROGRESS_TO_COMPLETE, 1.0F); moon_index_ = static_cast(MOON_PROGRESSION * (moon_path_.size() - 1)); // Actualiza la velocidad de las nubes @@ -318,12 +318,12 @@ void Background::updateCloudsSpeed() { // Velocidad base según progreso (de -3.0 a -120.0 píxeles/segundo, igual que la versión original) float base_clouds_speed = (-CLOUDS_INITIAL_SPEED_PX_PER_S) + - (-CLOUDS_FINAL_SPEED_RANGE_PX_PER_S * (progress_ / total_progress_to_complete_)); + (-CLOUDS_FINAL_SPEED_RANGE_PX_PER_S * (progress_ / TOTAL_PROGRESS_TO_COMPLETE)); // En estado completado, las nubes se ralentizan gradualmente if (state_ == State::COMPLETED) { - float completion_factor = (progress_ - minimum_completed_progress_) / - (total_progress_to_complete_ - minimum_completed_progress_); + float completion_factor = (progress_ - MINIMUM_COMPLETED_PROGRESS) / + (TOTAL_PROGRESS_TO_COMPLETE - MINIMUM_COMPLETED_PROGRESS); completion_factor = std::max(0.1F, completion_factor); base_clouds_speed *= completion_factor; } diff --git a/source/core/rendering/background.hpp b/source/core/rendering/background.hpp index bd5b05c..678d5d1 100644 --- a/source/core/rendering/background.hpp +++ b/source/core/rendering/background.hpp @@ -4,6 +4,7 @@ #include // Para array #include // Para size_t +#include // Para std::uint8_t #include // Para function #include // Para unique_ptr, shared_ptr #include // Para vector @@ -19,7 +20,7 @@ class AnimatedSprite; class Background { public: // --- Enums --- - enum class State { + enum class State : std::uint8_t { NORMAL, // Progresión normal del día COMPLETED // Reducción gradual de la actividad }; @@ -87,10 +88,10 @@ class Background { std::unique_ptr grass_sprite_; // Sprite con la hierba // --- Variables de configuración --- - const float total_progress_to_complete_; // Progreso total para completar - const float progress_per_stage_; // Progreso por etapa - const float sun_completion_progress_; // Progreso de completado del sol - const float minimum_completed_progress_; // Progreso mínimo calculado dinámicamente + const float TOTAL_PROGRESS_TO_COMPLETE; // Progreso total para completar + const float PROGRESS_PER_STAGE; // Progreso por etapa + const float SUM_COMPLETION_PROGRESS; // Progreso de completado del sol + const float MINIMUM_COMPLETED_PROGRESS; // Progreso mínimo calculado dinámicamente ProgressCallback progress_callback_; // Callback para notificar cambios de progreso // --- Variables de estado --- diff --git a/source/core/rendering/fade.cpp b/source/core/rendering/fade.cpp index bff32f1..6d2faa8 100644 --- a/source/core/rendering/fade.cpp +++ b/source/core/rendering/fade.cpp @@ -481,7 +481,7 @@ void Fade::activate() { case Type::DIAGONAL: { rect1_ = {.x = 0, .y = 0, .w = static_cast(param.game.width / num_squares_width_), .h = static_cast(param.game.height / num_squares_height_)}; square_.clear(); - square_age_.assign(num_squares_width_ * num_squares_height_, -1); + square_age_.assign(static_cast(num_squares_width_) * num_squares_height_, -1); for (int i = 0; i < num_squares_width_ * num_squares_height_; ++i) { rect1_.x = (i % num_squares_width_) * rect1_.w; rect1_.y = (i / num_squares_width_) * rect1_.h; diff --git a/source/core/rendering/gif.cpp b/source/core/rendering/gif.cpp index 5a9a37c..c15872d 100644 --- a/source/core/rendering/gif.cpp +++ b/source/core/rendering/gif.cpp @@ -8,145 +8,233 @@ #include // Para char_traits, operator==, basic_string, string namespace GIF { - inline void readBytes(const uint8_t *&buffer, void *dst, size_t size) { - std::memcpy(dst, buffer, size); - buffer += size; - } - - void Gif::decompress(int code_length, const uint8_t *input, int input_length, uint8_t *out) { - if (code_length < 2 || code_length > 12) { - std::cout << "Invalid LZW code length: " << code_length << '\n'; - throw std::runtime_error("Invalid LZW code length"); + namespace { + inline void readBytes(const uint8_t *&buffer, void *dst, size_t size) { + std::memcpy(dst, buffer, size); + buffer += size; } - int i, bit; - int prev = -1; - std::vector 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; - int match_len = 0; - - dictionary.resize(1 << (code_length + 1)); - for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) { - dictionary[dictionary_ind].byte = static_cast(dictionary_ind); - dictionary[dictionary_ind].prev = -1; - dictionary[dictionary_ind].len = 1; + // Llavor del diccionari LZW: 0..N-1 com a entrades base, i salta 2 (clear_code + stop_code). + void resetDictionary(std::vector &dict, int code_length, int &dictionary_ind) { + dict.resize(1 << (code_length + 1)); + for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) { + dict[dictionary_ind].byte = static_cast(dictionary_ind); + dict[dictionary_ind].prev = -1; + dict[dictionary_ind].len = 1; + } + dictionary_ind += 2; } - dictionary_ind += 2; - while (input_length > 0) { + // Llig `code_length + 1` bits LSB-first del flux d'entrada. Llança si s'acaba el buffer. + auto readNextCode(const uint8_t *&input, int &input_length, int code_length, unsigned int &mask) -> int { int code = 0; - for (i = 0; i < (code_length + 1); i++) { + for (int i = 0; i < code_length + 1; i++) { if (input_length <= 0) { std::cout << "Unexpected end of input in decompress" << '\n'; throw std::runtime_error("Unexpected end of input in decompress"); } - bit = ((*input & mask) != 0) ? 1 : 0; + const int BIT = ((*input & mask) != 0) ? 1 : 0; mask <<= 1; if (mask == 0x100) { mask = 0x01; input++; input_length--; } - code |= (bit << i); + code |= (BIT << i); } + return code; + } - if (code == clear_code) { - code_length = reset_code_length; - dictionary.resize(1 << (code_length + 1)); - for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) { - dictionary[dictionary_ind].byte = static_cast(dictionary_ind); - dictionary[dictionary_ind].prev = -1; - dictionary[dictionary_ind].len = 1; - } - dictionary_ind += 2; - prev = -1; - continue; - } else if (code == stop_code) { - break; + // Afig una nova entrada al diccionari. Resol el cas especial KwKwK (code == dictionary_ind) + // començant la cadena des de `prev` en lloc de des de `code`. + void addDictionaryEntry(std::vector &dict, int dictionary_ind, int code, int prev) { + int ptr = (code == dictionary_ind) ? prev : code; + while (dict[ptr].prev != -1) { + ptr = dict[ptr].prev; } + dict[dictionary_ind].byte = dict[ptr].byte; + dict[dictionary_ind].prev = prev; + dict[dictionary_ind].len = dict[prev].len + 1; + } - if (prev > -1 && code_length < 12) { - if (code > dictionary_ind) { - std::cout << "LZW error: code (" << code << ") exceeds dictionary_ind (" << dictionary_ind << ")" << '\n'; - throw std::runtime_error("LZW error: code exceeds dictionary_ind."); - } - - int ptr; - if (code == dictionary_ind) { - ptr = prev; - while (dictionary[ptr].prev != -1) - ptr = dictionary[ptr].prev; - dictionary[dictionary_ind].byte = dictionary[ptr].byte; - } else { - ptr = code; - while (dictionary[ptr].prev != -1) - ptr = dictionary[ptr].prev; - dictionary[dictionary_ind].byte = dictionary[ptr].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)); - } - } - - prev = code; - - if (code < 0 || static_cast(code) >= dictionary.size()) { - std::cout << "Invalid LZW code " << code << ", dictionary size " << static_cast(dictionary.size()) << '\n'; - throw std::runtime_error("LZW error: invalid code encountered"); - } - - int curCode = code; - match_len = dictionary[curCode].len; - while (curCode != -1) { - out[dictionary[curCode].len - 1] = dictionary[curCode].byte; - if (dictionary[curCode].prev == curCode) { + // Escriu la cadena de bytes associada a `code` en `out` (en ordre invers seguint .prev). + // Retorna la longitud del match per avançar el cursor de l'eixida. + auto emitMatch(const std::vector &dict, int code, uint8_t *out) -> int { + const int MATCH_LEN = dict[code].len; + int cur_code = code; + while (cur_code != -1) { + out[dict[cur_code].len - 1] = dict[cur_code].byte; + if (dict[cur_code].prev == cur_code) { std::cout << "Internal error; self-reference detected." << '\n'; throw std::runtime_error("Internal error in decompress: self-reference"); } - curCode = dictionary[curCode].prev; + cur_code = dict[cur_code].prev; } - out += match_len; + return MATCH_LEN; } - } - std::vector Gif::readSubBlocks(const uint8_t *&buffer) { - std::vector 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; + // Descompone (uncompress) el bloque comprimido usando LZW. + void decompress(int code_length, const uint8_t *input, int input_length, uint8_t *out) { + if (code_length < 2 || code_length > 12) { + std::cout << "Invalid LZW code length: " << code_length << '\n'; + throw std::runtime_error("Invalid LZW code length"); + } + + int prev = -1; + std::vector dictionary; + int dictionary_ind = 0; + unsigned int mask = 0x01; + const int RESET_CODE_LENGTH = code_length; + const int CLEAR_CODE = 1 << code_length; + const int STOP_CODE = CLEAR_CODE + 1; + + resetDictionary(dictionary, code_length, dictionary_ind); + + while (input_length > 0) { + const int CODE = readNextCode(input, input_length, code_length, mask); + + if (CODE == CLEAR_CODE) { + code_length = RESET_CODE_LENGTH; + resetDictionary(dictionary, code_length, dictionary_ind); + prev = -1; + continue; + } + if (CODE == STOP_CODE) { break; } + + if (prev > -1 && code_length < 12) { + if (CODE > dictionary_ind) { + std::cout << "LZW error: code (" << CODE << ") exceeds dictionary_ind (" << dictionary_ind << ")" << '\n'; + throw std::runtime_error("LZW error: code exceeds dictionary_ind."); + } + addDictionaryEntry(dictionary, dictionary_ind, CODE, prev); + dictionary_ind++; + + if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) { + code_length++; + dictionary.resize(1 << (code_length + 1)); + } + } + + prev = CODE; + + if (CODE < 0 || static_cast(CODE) >= dictionary.size()) { + std::cout << "Invalid LZW code " << CODE << ", dictionary size " << static_cast(dictionary.size()) << '\n'; + throw std::runtime_error("LZW error: invalid code encountered"); + } + + out += emitMatch(dictionary, CODE, out); + } + } + + // Lee los sub-bloques de datos y los acumula en un std::vector. + auto readSubBlocks(const uint8_t *&buffer) -> std::vector { + std::vector 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; } - return data; - } - std::vector Gif::processImageDescriptor(const uint8_t *&buffer, const std::vector &gct, int resolution_bits) { - ImageDescriptor image_descriptor; - readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor)); + // Procesa el Image Descriptor y retorna el vector de datos sin comprimir. + auto processImageDescriptor(const uint8_t *&buffer, const std::vector &gct, int resolution_bits) -> std::vector { + ImageDescriptor image_descriptor; + readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor)); - uint8_t lzw_code_size; - readBytes(buffer, &lzw_code_size, sizeof(uint8_t)); + uint8_t lzw_code_size; + readBytes(buffer, &lzw_code_size, sizeof(uint8_t)); - std::vector compressed_data = readSubBlocks(buffer); - int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height; - std::vector uncompressed_data(uncompressed_data_length); + std::vector compressed_data = readSubBlocks(buffer); + int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height; + std::vector uncompressed_data(uncompressed_data_length); - decompress(lzw_code_size, compressed_data.data(), static_cast(compressed_data.size()), uncompressed_data.data()); - return uncompressed_data; - } + decompress(lzw_code_size, compressed_data.data(), static_cast(compressed_data.size()), uncompressed_data.data()); + return uncompressed_data; + } - std::vector Gif::loadPalette(const uint8_t *buffer) { + // Procesa el stream completo del GIF y devuelve los datos sin comprimir. + auto processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h) -> std::vector { + uint8_t header[6]; + std::memcpy(header, buffer, 6); + buffer += 6; + + std::string header_str(reinterpret_cast(header), 6); + if (header_str != "GIF87a" && header_str != "GIF89a") { + std::cout << "Formato de archivo GIF inválido: " << header_str << '\n'; + throw std::runtime_error("Formato de archivo GIF inválido."); + } + + ScreenDescriptor screen_descriptor; + readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor)); + + w = screen_descriptor.width; + h = screen_descriptor.height; + + int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1; + std::vector global_color_table; + if ((screen_descriptor.fields & 0x80) != 0) { + const size_t GLOBAL_COLOR_TABLE_SIZE = 1U << (((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; + } + + uint8_t block_type = *buffer++; + while (block_type != TRAILER) { + if (block_type == EXTENSION_INTRODUCER) { + uint8_t extension_label = *buffer++; + switch (extension_label) { + case GRAPHIC_CONTROL: { + 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; + } + case APPLICATION_EXTENSION: + case COMMENT_EXTENSION: + case PLAINTEXT_EXTENSION: { + 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: { + 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) { + return processImageDescriptor(buffer, global_color_table, color_resolution_bits); + } else { + std::cout << "Unrecognized block type: 0x" << std::hex << static_cast(block_type) << std::dec << '\n'; + return std::vector{}; + } + block_type = *buffer++; + } + + return std::vector{}; + } + } // namespace + + auto loadPalette(const uint8_t *buffer) -> std::vector { uint8_t header[6]; std::memcpy(header, buffer, 6); buffer += 6; @@ -156,7 +244,7 @@ namespace GIF { buffer += sizeof(ScreenDescriptor); std::vector global_color_table; - if (screen_descriptor.fields & 0x80) { + 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) { @@ -170,83 +258,7 @@ namespace GIF { return global_color_table; } - std::vector Gif::processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h) { - uint8_t header[6]; - std::memcpy(header, buffer, 6); - buffer += 6; - - std::string headerStr(reinterpret_cast(header), 6); - if (headerStr != "GIF87a" && headerStr != "GIF89a") { - std::cout << "Formato de archivo GIF inválido: " << headerStr << '\n'; - throw std::runtime_error("Formato de archivo GIF inválido."); - } - - ScreenDescriptor screen_descriptor; - readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor)); - - w = screen_descriptor.width; - h = screen_descriptor.height; - - int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1; - std::vector global_color_table; - if (screen_descriptor.fields & 0x80) { - 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; - } - - uint8_t block_type = *buffer++; - while (block_type != TRAILER) { - if (block_type == EXTENSION_INTRODUCER) { - uint8_t extension_label = *buffer++; - switch (extension_label) { - case GRAPHIC_CONTROL: { - uint8_t blockSize = *buffer++; - buffer += blockSize; - uint8_t subBlockSize = *buffer++; - while (subBlockSize != 0) { - buffer += subBlockSize; - subBlockSize = *buffer++; - } - break; - } - case APPLICATION_EXTENSION: - case COMMENT_EXTENSION: - case PLAINTEXT_EXTENSION: { - uint8_t blockSize = *buffer++; - buffer += blockSize; - uint8_t subBlockSize = *buffer++; - while (subBlockSize != 0) { - buffer += subBlockSize; - subBlockSize = *buffer++; - } - break; - } - default: { - uint8_t blockSize = *buffer++; - buffer += blockSize; - uint8_t subBlockSize = *buffer++; - while (subBlockSize != 0) { - buffer += subBlockSize; - subBlockSize = *buffer++; - } - break; - } - } - } else if (block_type == IMAGE_DESCRIPTOR) { - return processImageDescriptor(buffer, global_color_table, color_resolution_bits); - } else { - std::cout << "Unrecognized block type: 0x" << std::hex << static_cast(block_type) << std::dec << '\n'; - return std::vector{}; - } - block_type = *buffer++; - } - - return std::vector{}; - } - - std::vector Gif::loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h) { + auto loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h) -> std::vector { return processGifStream(buffer, w, h); } diff --git a/source/core/rendering/gif.hpp b/source/core/rendering/gif.hpp index 8bb26c1..da618d8 100644 --- a/source/core/rendering/gif.hpp +++ b/source/core/rendering/gif.hpp @@ -64,29 +64,12 @@ namespace GIF { 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. - 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). + auto loadPalette(const uint8_t *buffer) -> std::vector; - // 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). - std::vector loadPalette(const uint8_t *buffer); - - // Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y - // asigna el ancho y alto mediante referencias. - std::vector loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h); - - private: - // Lee los sub-bloques de datos y los acumula en un std::vector. - std::vector readSubBlocks(const uint8_t *&buffer); - - // Procesa el Image Descriptor y retorna el vector de datos sin comprimir. - std::vector processImageDescriptor(const uint8_t *&buffer, const std::vector &gct, int resolution_bits); - - // Procesa el stream completo del GIF y devuelve los datos sin comprimir. - std::vector processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h); - }; + // Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y + // asigna el ancho y alto mediante referencias. + auto loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h) -> std::vector; } // namespace GIF diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 0120b38..ee27a5f 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -320,49 +320,50 @@ void Screen::renderShake() { } } #ifdef _DEBUG +// Compone la línia d'informació de debug: "fps - driver - shader preset" +auto Screen::buildDebugInfoText() const -> std::string { + std::string info_text = std::to_string(fps_.last_value) + " fps"; + + // Driver GPU + if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { + const std::string DRIVER = shader_backend_->getDriverName(); + info_text += DRIVER.empty() ? "" : " - " + toLower(DRIVER); + } else { + info_text += " - sdl"; + } + + // Shader + preset (només si està activat) + if (!Options::video.shader.enabled) { return info_text; } + + if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) { + const std::string PRESET_NAME = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast(Options::video.shader.current_crtpi_preset)).name; + info_text += " - crtpi " + toLower(PRESET_NAME); + } else { + const std::string PRESET_NAME = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast(Options::video.shader.current_postfx_preset)).name; + info_text += " - postfx " + toLower(PRESET_NAME); + if (Options::video.supersampling.enabled) { info_text += " (ss)"; } + } + return info_text; +} + // Muestra información por pantalla void Screen::renderInfo() const { - if (debug_info_.show) { - const Color GOLD(0xFF, 0xD7, 0x00); - const Color GOLD_SHADOW = GOLD.DARKEN(150); + if (!debug_info_.show) { return; } - // Construir texto: fps - driver - preset - std::string info_text = std::to_string(fps_.last_value) + " fps"; + const Color GOLD(0xFF, 0xD7, 0x00); + const Color GOLD_SHADOW = GOLD.DARKEN(150); - // Driver GPU - if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { - const std::string DRIVER = shader_backend_->getDriverName(); - if (!DRIVER.empty()) { - info_text += " - " + toLower(DRIVER); - } - } else { - info_text += " - sdl"; - } - - // Shader + preset - if (Options::video.shader.enabled) { - if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) { - const std::string PRESET_NAME = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast(Options::video.shader.current_crtpi_preset)).name; - info_text += " - crtpi " + toLower(PRESET_NAME); - } else { - const std::string PRESET_NAME = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast(Options::video.shader.current_postfx_preset)).name; - info_text += " - postfx " + toLower(PRESET_NAME); - if (Options::video.supersampling.enabled) { info_text += " (ss)"; } - } - } - - // Centrado arriba - const int TEXT_WIDTH = debug_info_.text->length(info_text); - const int X_POS = (static_cast(param.game.width) - TEXT_WIDTH) / 2; - debug_info_.text->writeDX(Text::COLOR | Text::STROKE, X_POS, 1, info_text, 1, GOLD, 1, GOLD_SHADOW); + const std::string INFO_TEXT = buildDebugInfoText(); + const int TEXT_WIDTH = debug_info_.text->length(INFO_TEXT); + const int X_POS = (static_cast(param.game.width) - TEXT_WIDTH) / 2; + debug_info_.text->writeDX(Text::COLOR | Text::STROKE, X_POS, 1, INFO_TEXT, 1, GOLD, 1, GOLD_SHADOW); #ifdef RECORDING - const std::string REC_TEXT = "recording"; - const int REC_WIDTH = debug_info_.text->length(REC_TEXT); - const int REC_X = (static_cast(param.game.width) - REC_WIDTH) / 2; - debug_info_.text->writeDX(Text::COLOR | Text::STROKE, REC_X, 1 + debug_info_.text->getCharacterSize(), REC_TEXT, 1, GOLD, 1, GOLD_SHADOW); + const std::string REC_TEXT = "recording"; + const int REC_WIDTH = debug_info_.text->length(REC_TEXT); + const int REC_X = (static_cast(param.game.width) - REC_WIDTH) / 2; + debug_info_.text->writeDX(Text::COLOR | Text::STROKE, REC_X, 1 + debug_info_.text->getCharacterSize(), REC_TEXT, 1, GOLD, 1, GOLD_SHADOW); #endif - } } #endif // Inicializa shaders (SDL3GPU) @@ -380,8 +381,8 @@ void Screen::initShaders() { Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER); } if (!self->shader_backend_->isHardwareAccelerated()) { - const bool ok = self->shader_backend_->init(self->window_, self->game_canvas_, "", ""); - std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n'; + const bool OK = self->shader_backend_->init(self->window_, self->game_canvas_, "", ""); + std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (OK ? "OK" : "FAILED") << '\n'; } if (self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) { self->shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale); diff --git a/source/core/rendering/screen.hpp b/source/core/rendering/screen.hpp index c34ee7f..c3ea96a 100644 --- a/source/core/rendering/screen.hpp +++ b/source/core/rendering/screen.hpp @@ -241,6 +241,7 @@ class Screen { void renderFlash(); // Dibuja el efecto de flash en la pantalla void renderShake(); // Aplica el efecto de agitar la pantalla void renderInfo() const; // Muestra información por pantalla + [[nodiscard]] auto buildDebugInfoText() const -> std::string; // Compone fps + driver + shader/preset para renderInfo void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend void applyCurrentCrtPiPreset(); // Aplica el preset CrtPi activo al backend diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index e8a734f..b1df6a4 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -702,7 +702,7 @@ namespace Rendering { return; } - std::memcpy(mapped, pixels, static_cast(width * height * 4)); + std::memcpy(mapped, pixels, static_cast(width) * height * 4); SDL_UnmapGPUTransferBuffer(device_, upload_buffer_); } diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index e3dcd97..ed44de8 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -2,13 +2,14 @@ #include +#include #include #include namespace Rendering { /** @brief Identificador del shader de post-procesado activo */ - enum class ShaderType { POSTFX, + enum class ShaderType : std::uint8_t { POSTFX, CRTPI }; /** diff --git a/source/core/rendering/sprite/card_sprite.cpp b/source/core/rendering/sprite/card_sprite.cpp index 41d8e99..b68042b 100644 --- a/source/core/rendering/sprite/card_sprite.cpp +++ b/source/core/rendering/sprite/card_sprite.cpp @@ -31,7 +31,7 @@ auto CardSprite::enable() -> bool { // Ángulo inicial rotate_.angle = start_angle_; - rotate_.center = {pos_.w / 2.0F, pos_.h / 2.0F}; + rotate_.center = {.x = pos_.w / 2.0F, .y = pos_.h / 2.0F}; shadow_visible_ = true; return true; @@ -55,7 +55,7 @@ void CardSprite::startExit() { // Rotación continua rotate_.enabled = true; rotate_.amount = exit_rotate_amount_; - rotate_.center = {pos_.w / 2.0F, pos_.h / 2.0F}; + rotate_.center = {.x = pos_.w / 2.0F, .y = pos_.h / 2.0F}; } // Actualiza según el estado @@ -80,7 +80,7 @@ void CardSprite::updateEntering(float delta_time) { double eased = entry_easing_(static_cast(progress)); // Zoom: de start_zoom_ a 1.0 con rebote - auto current_zoom = static_cast(start_zoom_ + (1.0 - start_zoom_) * eased); + auto current_zoom = static_cast(start_zoom_ + ((1.0 - start_zoom_) * eased)); horizontal_zoom_ = current_zoom; vertical_zoom_ = current_zoom; @@ -90,8 +90,8 @@ void CardSprite::updateEntering(float delta_time) { // Posición: de entry_start a landing con easing suave (sin rebote) // Usamos easeOutCubic para que el desplazamiento sea fluido double pos_eased = easeOutCubic(static_cast(progress)); - auto current_x = static_cast(entry_start_x_ + (landing_x_ - entry_start_x_) * pos_eased); - auto current_y = static_cast(entry_start_y_ + (landing_y_ - entry_start_y_) * pos_eased); + auto current_x = static_cast(entry_start_x_ + ((landing_x_ - entry_start_x_) * pos_eased)); + auto current_y = static_cast(entry_start_y_ + ((landing_y_ - entry_start_y_) * pos_eased)); setPos(current_x, current_y); // Detecta el primer toque (cuando el easing alcanza ~1.0 por primera vez) @@ -117,12 +117,9 @@ void CardSprite::updateExiting(float delta_time) { // Ganar altura gradualmente (zoom hacia el objetivo) if (exit_zoom_speed_ > 0.0F && horizontal_zoom_ < exit_target_zoom_) { - float new_zoom = horizontal_zoom_ + exit_zoom_speed_ * delta_time; - if (new_zoom > exit_target_zoom_) { - new_zoom = exit_target_zoom_; - } - horizontal_zoom_ = new_zoom; - vertical_zoom_ = new_zoom; + const float NEW_ZOOM = std::min(horizontal_zoom_ + (exit_zoom_speed_ * delta_time), exit_target_zoom_); + horizontal_zoom_ = NEW_ZOOM; + vertical_zoom_ = NEW_ZOOM; } if (isOffScreen()) { @@ -164,8 +161,8 @@ void CardSprite::renderShadow() { // Offset respecto a la tarjeta: base + extra proporcional a la altura // La sombra se aleja en diagonal abajo-derecha (opuesta a la luz en 0,0) - float offset_x = shadow_offset_x_ + height * SHADOW_HEIGHT_MULTIPLIER; - float offset_y = shadow_offset_y_ + height * SHADOW_HEIGHT_MULTIPLIER; + float offset_x = shadow_offset_x_ + (height * SHADOW_HEIGHT_MULTIPLIER); + float offset_y = shadow_offset_y_ + (height * SHADOW_HEIGHT_MULTIPLIER); shadow_texture_->render( pos_.x + offset_x, diff --git a/source/core/rendering/sprite/card_sprite.hpp b/source/core/rendering/sprite/card_sprite.hpp index 5c74eb6..6aa38b9 100644 --- a/source/core/rendering/sprite/card_sprite.hpp +++ b/source/core/rendering/sprite/card_sprite.hpp @@ -2,6 +2,7 @@ #include // Para SDL_FPoint +#include // Para std::uint8_t #include // Para function #include // Para shared_ptr @@ -10,7 +11,7 @@ class Texture; // --- Estados de la tarjeta --- -enum class CardState { +enum class CardState : std::uint8_t { IDLE, // No activada todavía ENTERING, // Animación de entrada (zoom + rotación + desplazamiento con rebote) LANDED, // En reposo sobre la mesa diff --git a/source/core/rendering/sprite/path_sprite.hpp b/source/core/rendering/sprite/path_sprite.hpp index a8689db..07457bd 100644 --- a/source/core/rendering/sprite/path_sprite.hpp +++ b/source/core/rendering/sprite/path_sprite.hpp @@ -2,6 +2,7 @@ #include // Para SDL_FPoint +#include // Para std::uint8_t #include // Para std::function #include // Para shared_ptr #include @@ -12,12 +13,12 @@ class Texture; // --- Enums --- -enum class PathType { // Tipos de recorrido +enum class PathType : std::uint8_t { // Tipos de recorrido VERTICAL, HORIZONTAL, }; -enum class PathCentered { // Centrado del recorrido +enum class PathCentered : std::uint8_t { // Centrado del recorrido ON_X, ON_Y, NONE, diff --git a/source/core/rendering/sprite/smart_sprite.cpp b/source/core/rendering/sprite/smart_sprite.cpp index 0e1dd84..a9bfdf6 100644 --- a/source/core/rendering/sprite/smart_sprite.cpp +++ b/source/core/rendering/sprite/smart_sprite.cpp @@ -1,7 +1,5 @@ #include "core/rendering/sprite/smart_sprite.hpp" -#include "core/rendering/sprite/moving_sprite.hpp" // Para MovingSprite - // Actualiza la posición y comprueba si ha llegado a su destino (time-based) void SmartSprite::update(float delta_time) { if (enabled_) { diff --git a/source/core/rendering/sprite/smart_sprite.hpp b/source/core/rendering/sprite/smart_sprite.hpp index 3b60e4b..2c2c943 100644 --- a/source/core/rendering/sprite/smart_sprite.hpp +++ b/source/core/rendering/sprite/smart_sprite.hpp @@ -3,16 +3,16 @@ #include // Para shared_ptr #include -#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite +#include "core/rendering/sprite/moving_sprite.hpp" // Para MovingSprite class Texture; -// --- Clase SmartSprite: sprite animado que se mueve hacia un destino y puede deshabilitarse automáticamente --- -class SmartSprite : public AnimatedSprite { +// --- Clase SmartSprite: sprite que se mueve hacia un destino y puede deshabilitarse automáticamente --- +class SmartSprite : public MovingSprite { public: // --- Constructor y destructor --- explicit SmartSprite(std::shared_ptr texture) - : AnimatedSprite(std::move(texture)) {} + : MovingSprite(std::move(texture)) {} ~SmartSprite() override = default; // --- Métodos principales --- diff --git a/source/core/rendering/texture.cpp b/source/core/rendering/texture.cpp index 118a2c6..e084bbd 100644 --- a/source/core/rendering/texture.cpp +++ b/source/core/rendering/texture.cpp @@ -251,11 +251,9 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr raw_pixels = gif.loadGif(buffer.data(), w, h); + std::vector raw_pixels = GIF::loadGif(buffer.data(), w, h); if (raw_pixels.empty()) { std::cout << "Error: No se pudo cargar el GIF " << file_path << '\n'; return nullptr; @@ -329,8 +327,7 @@ auto Texture::loadPaletteFromFile(const std::string& file_path) -> Palette { } // Usar la nueva función loadPalette, que devuelve un vector - GIF::Gif gif; - std::vector pal = gif.loadPalette(buffer.data()); + std::vector pal = GIF::loadPalette(buffer.data()); if (pal.empty()) { std::cout << "Advertencia: No se encontró paleta en el archivo " << file_path << '\n'; return palette; // Devuelve un vector vacío si no hay paleta diff --git a/source/core/rendering/tiled_bg.hpp b/source/core/rendering/tiled_bg.hpp index bf79bdb..f4c3c37 100644 --- a/source/core/rendering/tiled_bg.hpp +++ b/source/core/rendering/tiled_bg.hpp @@ -2,10 +2,12 @@ #include // Para SDL_FRect, SDL_SetTextureColorMod, SDL_Renderer, SDL_Texture +#include // Para std::uint8_t + #include "utils/color.hpp" // Para Color // --- Enums --- -enum class TiledBGMode : int { // Modos de funcionamiento para el tileado de fondo +enum class TiledBGMode : std::uint8_t { // Modos de funcionamiento para el tileado de fondo CIRCLE = 0, DIAGONAL = 1, RANDOM = 2, diff --git a/source/core/rendering/writer.cpp b/source/core/rendering/writer.cpp index 25eccfe..1bc824e 100644 --- a/source/core/rendering/writer.cpp +++ b/source/core/rendering/writer.cpp @@ -1,6 +1,7 @@ #include "core/rendering/writer.hpp" -#include "core/rendering/text.hpp" // Para Text +// Text es completat ací per `text_->write/length` via shared_ptr; include-cleaner no detecta l'ús indirecte. +#include "core/rendering/text.hpp" // IWYU pragma: keep // Actualiza el objeto (delta_time en ms) void Writer::update(float delta_time) { diff --git a/source/core/resources/asset.hpp b/source/core/resources/asset.hpp index 7b62633..31fbef1 100644 --- a/source/core/resources/asset.hpp +++ b/source/core/resources/asset.hpp @@ -10,7 +10,7 @@ class Asset { public: // --- Enums --- - enum class Type : int { + enum class Type : std::uint8_t { BITMAP, // Imágenes MUSIC, // Música SOUND, // Sonidos diff --git a/source/core/resources/asset_integrated.cpp b/source/core/resources/asset_integrated.cpp index 6c7cf88..36389ac 100644 --- a/source/core/resources/asset_integrated.cpp +++ b/source/core/resources/asset_integrated.cpp @@ -4,6 +4,8 @@ #include #include +#include "core/resources/resource_loader.hpp" + bool AssetIntegrated::resource_pack_enabled = false; void AssetIntegrated::initWithResourcePack(const std::string& executable_path, diff --git a/source/core/resources/asset_integrated.hpp b/source/core/resources/asset_integrated.hpp index 35da80c..b6ed522 100644 --- a/source/core/resources/asset_integrated.hpp +++ b/source/core/resources/asset_integrated.hpp @@ -1,9 +1,6 @@ #pragma once -#include - #include "core/resources/asset.hpp" -#include "core/resources/resource_loader.hpp" // Extensión de Asset que integra ResourceLoader class AssetIntegrated : public Asset { diff --git a/source/core/resources/resource.cpp b/source/core/resources/resource.cpp index 26b19e0..ffba136 100644 --- a/source/core/resources/resource.cpp +++ b/source/core/resources/resource.cpp @@ -5,16 +5,12 @@ #include // Para ranges::transform, ranges::find_if #include // Para array #include // Para exit -#include // Para exception -#include // Para exists, path, remove -#include // Para basic_ofstream, basic_ios, basic_ostream::write, ios, ofstream #include // Para std::cout #include // Para back_inserter -#include // Para __find_if_fn, find_if, __find_fn, find #include // Para runtime_error #include // Para move -#include "core/audio/jail_audio.hpp" // Para JA_LoadMusic, JA_LoadSound, JA_DeleteMusic, JA_DeleteSound +#include "core/audio/jail_audio.hpp" // Para Ja::loadMusic, Ja::loadSound, Ja::deleteMusic, Ja::deleteSound #include "core/locale/lang.hpp" // Para getText #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/text.hpp" // Para Text @@ -27,9 +23,6 @@ #include "utils/utils.hpp" // Para getFileName #include "version.h" // Para APP_NAME, GIT_HASH -struct JA_Music_t; // lines 11-11 -struct JA_Sound_t; // lines 12-12 - // Helper para cargar archivos de audio desde pack o filesystem en memoria namespace { struct AudioData { @@ -140,37 +133,37 @@ void Resource::loadEssentialTextures() { // Inicializa las listas de recursos sin cargar el contenido (modo lazy) void Resource::initResourceLists() { - const auto file_to_name = [](const auto& file) { return getFileName(file); }; + const auto FILE_TO_NAME = [](const auto& file) { return getFileName(file); }; // Inicializa lista de sonidos - const auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND); + const auto SOUND_LIST = Asset::get()->getListByType(Asset::Type::SOUND); sounds_.clear(); - sounds_.reserve(sound_list.size()); - std::ranges::transform(sound_list, std::back_inserter(sounds_), [&](const auto& file) { return ResourceSound(file_to_name(file)); }); + sounds_.reserve(SOUND_LIST.size()); + std::ranges::transform(SOUND_LIST, std::back_inserter(sounds_), [&](const auto& file) { return ResourceSound(FILE_TO_NAME(file)); }); // Inicializa lista de músicas - const auto music_list = Asset::get()->getListByType(Asset::Type::MUSIC); + const auto MUSIC_LIST = Asset::get()->getListByType(Asset::Type::MUSIC); musics_.clear(); - musics_.reserve(music_list.size()); - std::ranges::transform(music_list, std::back_inserter(musics_), [&](const auto& file) { return ResourceMusic(file_to_name(file)); }); + musics_.reserve(MUSIC_LIST.size()); + std::ranges::transform(MUSIC_LIST, std::back_inserter(musics_), [&](const auto& file) { return ResourceMusic(FILE_TO_NAME(file)); }); // Inicializa lista de texturas - const auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP); + const auto TEXTURE_LIST = Asset::get()->getListByType(Asset::Type::BITMAP); textures_.clear(); - textures_.reserve(texture_list.size()); - std::ranges::transform(texture_list, std::back_inserter(textures_), [&](const auto& file) { return ResourceTexture(file_to_name(file)); }); + textures_.reserve(TEXTURE_LIST.size()); + std::ranges::transform(TEXTURE_LIST, std::back_inserter(textures_), [&](const auto& file) { return ResourceTexture(FILE_TO_NAME(file)); }); // Inicializa lista de ficheros de texto - const auto text_file_list = Asset::get()->getListByType(Asset::Type::FONT); + const auto TEXT_FILE_LIST = Asset::get()->getListByType(Asset::Type::FONT); text_files_.clear(); - text_files_.reserve(text_file_list.size()); - std::ranges::transform(text_file_list, std::back_inserter(text_files_), [&](const auto& file) { return ResourceTextFile(file_to_name(file)); }); + text_files_.reserve(TEXT_FILE_LIST.size()); + std::ranges::transform(TEXT_FILE_LIST, std::back_inserter(text_files_), [&](const auto& file) { return ResourceTextFile(FILE_TO_NAME(file)); }); // Inicializa lista de animaciones - const auto animation_list = Asset::get()->getListByType(Asset::Type::ANIMATION); + const auto ANIMATION_LIST = Asset::get()->getListByType(Asset::Type::ANIMATION); animations_.clear(); - animations_.reserve(animation_list.size()); - std::ranges::transform(animation_list, std::back_inserter(animations_), [&](const auto& file) { return ResourceAnimation(file_to_name(file)); }); + animations_.reserve(ANIMATION_LIST.size()); + std::ranges::transform(ANIMATION_LIST, std::back_inserter(animations_), [&](const auto& file) { return ResourceAnimation(FILE_TO_NAME(file)); }); // Los demos se cargan directamente sin mostrar progreso (son pocos y pequeños) loadDemoDataQuiet(); @@ -196,7 +189,7 @@ void Resource::initResourceLists() { } // Obtiene el sonido a partir de un nombre (con carga perezosa) -auto Resource::getSound(const std::string& name) -> JA_Sound_t* { +auto Resource::getSound(const std::string& name) -> Ja::Sound* { auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> auto { return s.name == name; }); if (it != sounds_.end()) { @@ -212,7 +205,7 @@ auto Resource::getSound(const std::string& name) -> JA_Sound_t* { } // Obtiene la música a partir de un nombre (con carga perezosa) -auto Resource::getMusic(const std::string& name) -> JA_Music_t* { +auto Resource::getMusic(const std::string& name) -> Ja::Music* { auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> auto { return m.name == name; }); if (it != musics_.end()) { @@ -303,48 +296,48 @@ auto Resource::getDemoData(int index) -> DemoData& { // --- Métodos de carga perezosa --- -auto Resource::loadSoundLazy(const std::string& name) -> JA_Sound_t* { +auto Resource::loadSoundLazy(const std::string& name) -> Ja::Sound* { auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND); for (const auto& file : sound_list) { if (getFileName(file) == name) { auto audio_data = loadAudioData(file); if (!audio_data.data.empty()) { - return JA_LoadSound(audio_data.data.data(), audio_data.data.size()); + return Ja::loadSound(audio_data.data.data(), audio_data.data.size()); } // Fallback a cargar desde disco si no está en pack - return JA_LoadSound(file.c_str()); + return Ja::loadSound(file.c_str()); } } return nullptr; } -auto Resource::loadMusicLazy(const std::string& name) -> JA_Music_t* { +auto Resource::loadMusicLazy(const std::string& name) -> Ja::Music* { auto music_list = Asset::get()->getListByType(Asset::Type::MUSIC); for (const auto& file : music_list) { if (getFileName(file) == name) { auto audio_data = loadAudioData(file); if (!audio_data.data.empty()) { - return JA_LoadMusic(audio_data.data.data(), audio_data.data.size()); + return Ja::loadMusic(audio_data.data.data(), audio_data.data.size()); } // Fallback a cargar desde disco si no está en pack - return JA_LoadMusic(file.c_str()); + return Ja::loadMusic(file.c_str()); } } return nullptr; } auto Resource::loadTextureLazy(const std::string& name) -> std::shared_ptr { - const auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP); - const auto it = std::ranges::find_if(texture_list, + const auto TEXTURE_LIST = Asset::get()->getListByType(Asset::Type::BITMAP); + const auto IT = std::ranges::find_if(TEXTURE_LIST, [&name](const auto& file) { return getFileName(file) == name; }); - return it != texture_list.end() ? std::make_shared(Screen::get()->getRenderer(), *it) : nullptr; + return IT != TEXTURE_LIST.end() ? std::make_shared(Screen::get()->getRenderer(), *IT) : nullptr; } auto Resource::loadTextFileLazy(const std::string& name) -> std::shared_ptr { - const auto text_file_list = Asset::get()->getListByType(Asset::Type::FONT); - const auto it = std::ranges::find_if(text_file_list, + const auto TEXT_FILE_LIST = Asset::get()->getListByType(Asset::Type::FONT); + const auto IT = std::ranges::find_if(TEXT_FILE_LIST, [&name](const auto& file) { return getFileName(file) == name; }); - return it != text_file_list.end() ? Text::loadFile(*it) : nullptr; + return IT != TEXT_FILE_LIST.end() ? Text::loadFile(*IT) : nullptr; } auto Resource::loadTextLazy(const std::string& name) -> std::shared_ptr { @@ -369,22 +362,22 @@ auto Resource::loadTextLazy(const std::string& name) -> std::shared_ptr { {.key = "smb2", .texture_file = "smb2.png", .text_file = "smb2.txt"}, {.key = "smb2_grad", .texture_file = "smb2_grad.png", .text_file = "smb2.txt"}}; - const auto it = std::ranges::find_if(TEXT_MAPPINGS, + const auto IT = std::ranges::find_if(TEXT_MAPPINGS, [&name](const auto& mapping) { return mapping.key == name; }); - if (it == TEXT_MAPPINGS.end()) { + if (IT == TEXT_MAPPINGS.end()) { return nullptr; } - auto texture = getTexture(it->texture_file); // Esto cargará la textura si no está cargada - auto text_file = getTextFile(it->text_file); // Esto cargará el archivo de texto si no está cargado + auto texture = getTexture(IT->texture_file); // Esto cargará la textura si no está cargada + auto text_file = getTextFile(IT->text_file); // Esto cargará el archivo de texto si no está cargado return (texture && text_file) ? std::make_shared(texture, text_file) : nullptr; } auto Resource::loadAnimationLazy(const std::string& name) -> AnimationsFileBuffer { - const auto animation_list = Asset::get()->getListByType(Asset::Type::ANIMATION); - const auto it = std::ranges::find_if(animation_list, + const auto ANIMATION_LIST = Asset::get()->getListByType(Asset::Type::ANIMATION); + const auto IT = std::ranges::find_if(ANIMATION_LIST, [&name](const auto& file) { return getFileName(file) == name; }); - if (it != animation_list.end()) { - return loadAnimationsFromFile(*it); + if (IT != ANIMATION_LIST.end()) { + return loadAnimationsFromFile(*IT); } // Si no se encuentra, retorna vector vacío return AnimationsFileBuffer{}; @@ -427,80 +420,54 @@ auto Resource::isLoadDone() const -> bool { return stage_ == LoadStage::DONE; } +// Avança una etapa que descarrega una llista d'assets. +void Resource::advanceListLoadStage(const std::vector& list, void (Resource::*load_one)(size_t), LoadStage next_stage) { + if (stage_index_ >= list.size()) { + stage_ = next_stage; + stage_index_ = 0; + return; + } + (this->*load_one)(stage_index_++); +} + // Bombea la máquina de etapas hasta agotar el presupuesto de tiempo o completar la carga. // Devuelve true cuando ya no queda nada por cargar. auto Resource::loadStep(int budget_ms) -> bool { if (stage_ == LoadStage::DONE) { return true; } - const Uint64 start_ns = SDL_GetTicksNS(); - const Uint64 budget_ns = static_cast(budget_ms) * 1'000'000ULL; + const Uint64 START_NS = SDL_GetTicksNS(); + const Uint64 BUDGET_NS = static_cast(budget_ms) * 1'000'000ULL; while (stage_ != LoadStage::DONE) { switch (stage_) { case LoadStage::SOUNDS: { - auto list = Asset::get()->getListByType(Asset::Type::SOUND); if (stage_index_ == 0) { sounds_.clear(); } - if (stage_index_ >= list.size()) { - stage_ = LoadStage::MUSICS; - stage_index_ = 0; - break; - } - loadOneSound(stage_index_++); + advanceListLoadStage(Asset::get()->getListByType(Asset::Type::SOUND), &Resource::loadOneSound, LoadStage::MUSICS); break; } case LoadStage::MUSICS: { - auto list = Asset::get()->getListByType(Asset::Type::MUSIC); if (stage_index_ == 0) { musics_.clear(); } - if (stage_index_ >= list.size()) { - stage_ = LoadStage::TEXTURES; - stage_index_ = 0; - break; - } - loadOneMusic(stage_index_++); + advanceListLoadStage(Asset::get()->getListByType(Asset::Type::MUSIC), &Resource::loadOneMusic, LoadStage::TEXTURES); break; } case LoadStage::TEXTURES: { - auto list = Asset::get()->getListByType(Asset::Type::BITMAP); if (stage_index_ == 0) { textures_.clear(); } - if (stage_index_ >= list.size()) { - stage_ = LoadStage::TEXT_FILES; - stage_index_ = 0; - break; - } - loadOneTexture(stage_index_++); + advanceListLoadStage(Asset::get()->getListByType(Asset::Type::BITMAP), &Resource::loadOneTexture, LoadStage::TEXT_FILES); break; } case LoadStage::TEXT_FILES: { - auto list = Asset::get()->getListByType(Asset::Type::FONT); if (stage_index_ == 0) { text_files_.clear(); } - if (stage_index_ >= list.size()) { - stage_ = LoadStage::ANIMATIONS; - stage_index_ = 0; - break; - } - loadOneTextFile(stage_index_++); + advanceListLoadStage(Asset::get()->getListByType(Asset::Type::FONT), &Resource::loadOneTextFile, LoadStage::ANIMATIONS); break; } case LoadStage::ANIMATIONS: { - auto list = Asset::get()->getListByType(Asset::Type::ANIMATION); if (stage_index_ == 0) { animations_.clear(); } - if (stage_index_ >= list.size()) { - stage_ = LoadStage::DEMO_DATA; - stage_index_ = 0; - break; - } - loadOneAnimation(stage_index_++); + advanceListLoadStage(Asset::get()->getListByType(Asset::Type::ANIMATION), &Resource::loadOneAnimation, LoadStage::DEMO_DATA); break; } case LoadStage::DEMO_DATA: { - auto list = Asset::get()->getListByType(Asset::Type::DEMODATA); if (stage_index_ == 0) { demos_.clear(); } - if (stage_index_ >= list.size()) { - stage_ = LoadStage::CREATE_TEXT; - stage_index_ = 0; - break; - } - loadOneDemoData(stage_index_++); + advanceListLoadStage(Asset::get()->getListByType(Asset::Type::DEMODATA), &Resource::loadOneDemoData, LoadStage::CREATE_TEXT); break; } case LoadStage::CREATE_TEXT: @@ -519,7 +486,7 @@ auto Resource::loadStep(int budget_ms) -> bool { break; } - if ((SDL_GetTicksNS() - start_ns) >= budget_ns) { break; } + if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) { break; } } return stage_ == LoadStage::DONE; @@ -542,11 +509,11 @@ void Resource::loadOneSound(size_t idx) { auto name = getFileName(path); updateLoadingProgress(name); auto audio_data = loadAudioData(path); - JA_Sound_t* sound = nullptr; + Ja::Sound* sound = nullptr; if (!audio_data.data.empty()) { - sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size()); + sound = Ja::loadSound(audio_data.data.data(), audio_data.data.size()); } else { - sound = JA_LoadSound(path.c_str()); + sound = Ja::loadSound(path.c_str()); } if (sound == nullptr) { std::cout << "Sound load failed: " << name << '\n'; @@ -561,11 +528,11 @@ void Resource::loadOneMusic(size_t idx) { auto name = getFileName(path); updateLoadingProgress(name); auto audio_data = loadAudioData(path); - JA_Music_t* music = nullptr; + Ja::Music* music = nullptr; if (!audio_data.data.empty()) { - music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size()); + music = Ja::loadMusic(audio_data.data.data(), audio_data.data.size()); } else { - music = JA_LoadMusic(path.c_str()); + music = Ja::loadMusic(path.c_str()); } if (music == nullptr) { std::cout << "Music load failed: " << name << '\n'; @@ -627,10 +594,10 @@ void Resource::createPlayerTextures() { const auto& player = players[player_idx]; // Obtenemos el jugador actual // Encontrar el archivo original de la textura - const auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP); - const auto it = std::ranges::find_if(texture_list, + const auto TEXTURE_LIST = Asset::get()->getListByType(Asset::Type::BITMAP); + const auto IT = std::ranges::find_if(TEXTURE_LIST, [&player](const auto& file) { return getFileName(file) == player.base_texture; }); - const std::string texture_file_path = (it != texture_list.end()) ? *it : std::string{}; + const std::string TEXTURE_FILE_PATH = (IT != TEXTURE_LIST.end()) ? *IT : std::string{}; // Crear las 4 texturas con sus respectivas paletas for (int palette_idx = 0; palette_idx < 4; ++palette_idx) { @@ -646,7 +613,7 @@ void Resource::createPlayerTextures() { texture->setPaletteColor(0, 56, param.player.outline_color[player_idx].TO_UINT32()); } else { // Crear textura nueva desde archivo usando ResourceHelper - texture = std::make_shared(Screen::get()->getRenderer(), texture_file_path); + texture = std::make_shared(Screen::get()->getRenderer(), TEXTURE_FILE_PATH); // Añadir todas las paletas texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[0])); @@ -769,7 +736,7 @@ void Resource::createText() { void Resource::clearSounds() { for (auto& sound : sounds_) { if (sound.sound != nullptr) { - JA_DeleteSound(sound.sound); + Ja::deleteSound(sound.sound); sound.sound = nullptr; } } @@ -780,7 +747,7 @@ void Resource::clearSounds() { void Resource::clearMusics() { for (auto& music : musics_) { if (music.music != nullptr) { - JA_DeleteMusic(music.music); + Ja::deleteMusic(music.music); music.music = nullptr; } } @@ -880,10 +847,10 @@ void Resource::renderProgress() { // Carga los datos para el modo demostración (sin mostrar progreso) void Resource::loadDemoDataQuiet() { - const auto list = Asset::get()->getListByType(Asset::Type::DEMODATA); + const auto LIST = Asset::get()->getListByType(Asset::Type::DEMODATA); demos_.clear(); - demos_.reserve(list.size()); - std::ranges::transform(list, std::back_inserter(demos_), [this](const auto& l) { return loadDemoDataFromFile(l); }); + demos_.reserve(LIST.size()); + std::ranges::transform(LIST, std::back_inserter(demos_), [](const auto& l) { return loadDemoDataFromFile(l); }); } // Inicializa los rectangulos que definen la barra de progreso diff --git a/source/core/resources/resource.hpp b/source/core/resources/resource.hpp index be69c07..0e92e96 100644 --- a/source/core/resources/resource.hpp +++ b/source/core/resources/resource.hpp @@ -3,6 +3,7 @@ #include // Para SDL_FRect #include // Para size_t +#include // Para std::uint8_t #include // Para shared_ptr #include // Para string #include // Para move @@ -13,14 +14,16 @@ #include "core/rendering/texture.hpp" // Para Texture #include "core/system/demo.hpp" // Para DemoData -struct JA_Music_t; -struct JA_Sound_t; +namespace Ja { + struct Music; + struct Sound; +} // namespace Ja // --- Clase Resource: gestiona todos los recursos del juego (singleton) --- class Resource { public: // --- Enum para el modo de carga --- - enum class LoadingMode { + enum class LoadingMode : std::uint8_t { PRELOAD, // Carga todos los recursos al inicio LAZY_LOAD // Carga los recursos bajo demanda }; @@ -31,8 +34,8 @@ class Resource { static auto get() -> Resource*; // Obtiene el puntero al objeto Resource // --- Métodos de acceso a recursos --- - auto getSound(const std::string& name) -> JA_Sound_t*; // Obtiene el sonido por nombre - auto getMusic(const std::string& name) -> JA_Music_t*; // Obtiene la música por nombre + auto getSound(const std::string& name) -> Ja::Sound*; // Obtiene el sonido por nombre + auto getMusic(const std::string& name) -> Ja::Music*; // Obtiene la música por nombre auto getTexture(const std::string& name) -> std::shared_ptr; // Obtiene la textura por nombre auto getTextFile(const std::string& name) -> std::shared_ptr; // Obtiene el fichero de texto por nombre auto getText(const std::string& name) -> std::shared_ptr; // Obtiene el objeto de texto por nombre @@ -58,18 +61,18 @@ class Resource { // --- Estructuras para recursos individuales --- struct ResourceSound { std::string name; // Nombre del sonido - JA_Sound_t* sound; // Objeto con el sonido + Ja::Sound* sound; // Objeto con el sonido - explicit ResourceSound(std::string name, JA_Sound_t* sound = nullptr) + explicit ResourceSound(std::string name, Ja::Sound* sound = nullptr) : name(std::move(name)), sound(sound) {} }; struct ResourceMusic { std::string name; // Nombre de la música - JA_Music_t* music; // Objeto con la música + Ja::Music* music; // Objeto con la música - explicit ResourceMusic(std::string name, JA_Music_t* music = nullptr) + explicit ResourceMusic(std::string name, Ja::Music* music = nullptr) : name(std::move(name)), music(music) {} }; @@ -156,7 +159,7 @@ class Resource { SDL_FRect loading_full_rect_; // --- Estado del cargador incremental --- - enum class LoadStage { + enum class LoadStage : std::uint8_t { SOUNDS, MUSICS, TEXTURES, @@ -187,8 +190,8 @@ class Resource { // --- Métodos para carga perezosa --- void initResourceLists(); // Inicializa las listas de recursos sin cargar el contenido - static auto loadSoundLazy(const std::string& name) -> JA_Sound_t*; // Carga un sonido específico bajo demanda - static auto loadMusicLazy(const std::string& name) -> JA_Music_t*; // Carga una música específica bajo demanda + static auto loadSoundLazy(const std::string& name) -> Ja::Sound*; // Carga un sonido específico bajo demanda + static auto loadMusicLazy(const std::string& name) -> Ja::Music*; // Carga una música específica bajo demanda static auto loadTextureLazy(const std::string& name) -> std::shared_ptr; // Carga una textura específica bajo demanda static auto loadTextFileLazy(const std::string& name) -> std::shared_ptr; // Carga un fichero de texto específico bajo demanda auto loadTextLazy(const std::string& name) -> std::shared_ptr; // Carga un objeto de texto específico bajo demanda @@ -200,6 +203,10 @@ class Resource { void initProgressBar(); // Inicializa los rectangulos que definen la barra de progreso void updateProgressBar(); // Actualiza la barra de estado + // Avança una etapa que descarrega una llista d'assets: si `stage_index_` desborda la mida, + // salta a `next_stage`; si no, crida `load_one` per a l'element actual i incrementa. + void advanceListLoadStage(const std::vector& list, void (Resource::*load_one)(size_t), LoadStage next_stage); + // --- Helpers del cargador incremental (cargan un único recurso) --- void loadOneSound(size_t idx); void loadOneMusic(size_t idx); diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 7920d4e..d090a5f 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -38,6 +38,38 @@ #include "game/ui/service_menu.hpp" // Para ServiceMenu #include "utils/param.hpp" // Para loadParamsFromFile +namespace { + // Llig un camp opcional d'un YAML cap a `dest`. Si no existeix, no toca `dest`. + // Si existeix però el tipus no encaixa, deixa el valor per defecte i avisa per stderr + // (un debug.yaml mal escrit no ha de tombar l'arrencada, però l'usuari ha de saber-ho). + template + void loadYamlField(const fkyaml::node& yaml, const std::string& key, T& dest) { + if (!yaml.contains(key)) { return; } + try { + dest = yaml[key].get_value(); + } catch (...) { + std::cerr << "debug.yaml: valor invàlid per a '" << key << "', es manté el valor per defecte\n"; + } + } + + auto parseInitialSection(const std::string& value) -> Section::Name { + if (value == "logo") { return Section::Name::LOGO; } + if (value == "intro") { return Section::Name::INTRO; } + if (value == "title") { return Section::Name::TITLE; } + if (value == "credits") { return Section::Name::CREDITS; } + if (value == "instructions") { return Section::Name::INSTRUCTIONS; } + if (value == "hiscore") { return Section::Name::HI_SCORE_TABLE; } + return Section::Name::GAME; // "game" i qualsevol valor desconegut + } + + auto parseInitialOptions(const std::string& value) -> Section::Options { + if (value == "none") { return Section::Options::NONE; } + if (value == "2p") { return Section::Options::GAME_PLAY_2P; } + if (value == "both") { return Section::Options::GAME_PLAY_BOTH; } + return Section::Options::GAME_PLAY_1P; // "1p" i qualsevol valor desconegut + } +} // namespace + // Constructor Director::Director() { Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO; @@ -275,78 +307,20 @@ void Director::loadDebugConfig() { file.close(); try { auto yaml = fkyaml::node::deserialize(content); - if (yaml.contains("initial_section")) { - try { - debug_config.initial_section = yaml["initial_section"].get_value(); - } catch (...) {} - } - if (yaml.contains("initial_options")) { - try { - debug_config.initial_options = yaml["initial_options"].get_value(); - } catch (...) {} - } - if (yaml.contains("initial_stage")) { - try { - debug_config.initial_stage = yaml["initial_stage"].get_value(); - } catch (...) {} - } - if (yaml.contains("show_render_info")) { - try { - debug_config.show_render_info = yaml["show_render_info"].get_value(); - } catch (...) {} - } - if (yaml.contains("resource_loading")) { - try { - debug_config.resource_loading = yaml["resource_loading"].get_value(); - } catch (...) {} - } - if (yaml.contains("autoplay")) { - try { - debug_config.autoplay = yaml["autoplay"].get_value(); - } catch (...) {} - } - if (yaml.contains("invincibility")) { - try { - debug_config.invincibility = yaml["invincibility"].get_value(); - } catch (...) {} - } + loadYamlField(yaml, "initial_section", debug_config.initial_section); + loadYamlField(yaml, "initial_options", debug_config.initial_options); + loadYamlField(yaml, "initial_stage", debug_config.initial_stage); + loadYamlField(yaml, "show_render_info", debug_config.show_render_info); + loadYamlField(yaml, "resource_loading", debug_config.resource_loading); + loadYamlField(yaml, "autoplay", debug_config.autoplay); + loadYamlField(yaml, "invincibility", debug_config.invincibility); } catch (...) { std::cout << "Error parsing debug.yaml, using defaults" << '\n'; } } - // Mapear strings a enums - const auto& sec = debug_config.initial_section; - if (sec == "logo") { - Section::name = Section::Name::LOGO; - } else if (sec == "intro") { - Section::name = Section::Name::INTRO; - } else if (sec == "title") { - Section::name = Section::Name::TITLE; - } else if (sec == "game") { - Section::name = Section::Name::GAME; - } else if (sec == "credits") { - Section::name = Section::Name::CREDITS; - } else if (sec == "instructions") { - Section::name = Section::Name::INSTRUCTIONS; - } else if (sec == "hiscore") { - Section::name = Section::Name::HI_SCORE_TABLE; - } else { - Section::name = Section::Name::GAME; - } - - const auto& opt = debug_config.initial_options; - if (opt == "none") { - Section::options = Section::Options::NONE; - } else if (opt == "1p") { - Section::options = Section::Options::GAME_PLAY_1P; - } else if (opt == "2p") { - Section::options = Section::Options::GAME_PLAY_2P; - } else if (opt == "both") { - Section::options = Section::Options::GAME_PLAY_BOTH; - } else { - Section::options = Section::Options::GAME_PLAY_1P; - } + Section::name = parseInitialSection(debug_config.initial_section); + Section::options = parseInitialOptions(debug_config.initial_options); } // Crea la carpeta del sistema donde guardar datos diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index 6d44ad7..3f54159 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -7,8 +7,10 @@ #include "core/system/section.hpp" // Para Section::Name +#include // Para std::uint8_t + namespace Lang { - enum class Code : int; + enum class Code : std::uint8_t; } // Declaraciones adelantadas de las secciones @@ -70,7 +72,7 @@ class Director { // --- Inicialización y cierre del sistema --- void init(); // Inicializa la aplicación (pre-boot) - void finishBoot(); // Post-boot: inicializa lo que depende de recursos cargados + static void finishBoot(); // Post-boot: inicializa lo que depende de recursos cargados static void close(); // Cierra y libera recursos // --- Configuración inicial --- diff --git a/source/core/system/section.hpp b/source/core/system/section.hpp index 158c9be..33f8ab9 100644 --- a/source/core/system/section.hpp +++ b/source/core/system/section.hpp @@ -1,5 +1,7 @@ #pragma once +#include + /* Namespace section: define los estados/secciones principales del programa, así como las opciones y modos especiales (como el Attract Mode). @@ -8,7 +10,7 @@ namespace Section { // --- Enumeraciones de secciones del programa --- - enum class Name { + enum class Name : std::uint8_t { RESET, // Inicialización PRELOAD, // Carga incremental de recursos LOGO, // Pantalla de logo @@ -23,7 +25,7 @@ namespace Section { }; // --- Opciones para la sección actual --- - enum class Options { + enum class Options : std::uint8_t { GAME_PLAY_1P, // Iniciar el juego con el jugador 1 GAME_PLAY_2P, // Iniciar el juego con el jugador 2 GAME_PLAY_BOTH, // Iniciar el juego con los dos jugadores @@ -37,7 +39,7 @@ namespace Section { }; // --- Modos para el Attract Mode --- - enum class AttractMode { + enum class AttractMode : std::uint8_t { TITLE_TO_DEMO, // Pasar de título a demo TITLE_TO_LOGO, // Pasar de título a logo }; diff --git a/source/core/system/shutdown.hpp b/source/core/system/shutdown.hpp index 87c178d..50dc352 100644 --- a/source/core/system/shutdown.hpp +++ b/source/core/system/shutdown.hpp @@ -1,10 +1,12 @@ #pragma once +#include + // --- Namespace SystemShutdown: utilidad multiplataforma para apagar el sistema de forma segura --- namespace SystemShutdown { // --- Enums --- - enum class ShutdownResult { + enum class ShutdownResult : std::uint8_t { SUCCESS = 0, // Éxito ERROR_PERMISSION, // Error de permisos insuficientes ERROR_SYSTEM_CALL, // Error en la llamada al sistema diff --git a/source/core/system/system_utils.hpp b/source/core/system/system_utils.hpp index 23902f3..30afadb 100644 --- a/source/core/system/system_utils.hpp +++ b/source/core/system/system_utils.hpp @@ -1,11 +1,12 @@ #pragma once +#include #include // --- Namespace SystemUtils: utilidades multiplataforma para operaciones del sistema --- namespace SystemUtils { // --- Enums --- - enum class Result { // Códigos de resultado para operaciones del sistema + enum class Result : std::uint8_t { // Códigos de resultado para operaciones del sistema SUCCESS = 0, PERMISSION_DENIED, // Sin permisos para crear la carpeta PATH_TOO_LONG, // Ruta demasiado larga diff --git a/source/game/options.cpp b/source/game/options.cpp index e56c77c..467c5d8 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -26,12 +26,12 @@ namespace Options { Keyboard keyboard; // Opciones para el teclado PendingChanges pending_changes; // Opciones que se aplican al cerrar std::vector postfx_presets = { - {"CRT", 0.15F, 0.7F, 0.2F, 0.5F, 0.1F, 0.0F, 0.0F, 0.0F}, - {"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F}, - {"Curved", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F}, - {"Scanlines", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F}, - {"Subtle", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F}, - {"CRT Live", 0.15F, 0.6F, 0.3F, 0.3F, 0.1F, 0.0F, 0.4F, 0.8F}, + {.name = "CRT", .vignette = 0.15F, .scanlines = 0.7F, .chroma = 0.2F, .mask = 0.5F, .gamma = 0.1F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, + {.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma = 0.2F, .mask = 0.3F, .gamma = 0.3F, .curvature = 0.0F, .bleeding = 0.6F, .flicker = 0.0F}, + {.name = "Curved", .vignette = 0.5F, .scanlines = 0.6F, .chroma = 0.1F, .mask = 0.4F, .gamma = 0.4F, .curvature = 0.8F, .bleeding = 0.0F, .flicker = 0.0F}, + {.name = "Scanlines", .vignette = 0.0F, .scanlines = 0.8F, .chroma = 0.0F, .mask = 0.0F, .gamma = 0.0F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, + {.name = "Subtle", .vignette = 0.3F, .scanlines = 0.4F, .chroma = 0.05F, .mask = 0.0F, .gamma = 0.2F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, + {.name = "CRT Live", .vignette = 0.15F, .scanlines = 0.6F, .chroma = 0.3F, .mask = 0.3F, .gamma = 0.1F, .curvature = 0.0F, .bleeding = 0.4F, .flicker = 0.8F}, }; std::string postfx_file_path; std::vector crtpi_presets; @@ -50,11 +50,17 @@ namespace Options { void setCrtPiFile(const std::string& path) { crtpi_file_path = path; } // Helper: extrae un campo float de un nodo YAML si existe, ignorando errores de conversión - static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) { - if (node.contains(key)) { - try { - target = node[key].get_value(); - } catch (...) {} + // Llig un camp opcional d'un node YAML. Si no existeix, no toca `target`. + // Si existeix però el tipus no encaixa, manté el valor previ i avisa per stderr + // (un fitxer de configuració parcialment malformat no ha de tombar l'arrencada, + // però l'usuari ha de saber quin camp ha quedat ignorat). + template + void parseField(const fkyaml::node& node, const std::string& key, T& target) { + if (!node.contains(key)) { return; } + try { + target = node[key].get_value(); + } catch (...) { + std::cerr << "config YAML: valor invàlid per a '" << key << "', es manté el valor per defecte\n"; } } @@ -80,14 +86,14 @@ namespace Options { if (p.contains("name")) { preset.name = p["name"].get_value(); } - parseFloatField(p, "vignette", preset.vignette); - parseFloatField(p, "scanlines", preset.scanlines); - parseFloatField(p, "chroma", preset.chroma); - parseFloatField(p, "mask", preset.mask); - parseFloatField(p, "gamma", preset.gamma); - parseFloatField(p, "curvature", preset.curvature); - parseFloatField(p, "bleeding", preset.bleeding); - parseFloatField(p, "flicker", preset.flicker); + parseField(p, "vignette", preset.vignette); + parseField(p, "scanlines", preset.scanlines); + parseField(p, "chroma", preset.chroma); + parseField(p, "mask", preset.mask); + parseField(p, "gamma", preset.gamma); + parseField(p, "curvature", preset.curvature); + parseField(p, "bleeding", preset.bleeding); + parseField(p, "flicker", preset.flicker); postfx_presets.push_back(preset); } } @@ -212,24 +218,6 @@ namespace Options { return true; } - // Helper: extrae un campo bool de un nodo YAML si existe, ignorando errores - static void parseBoolField(const fkyaml::node& node, const std::string& key, bool& target) { - if (node.contains(key)) { - try { - target = node[key].get_value(); - } catch (...) {} - } - } - - // Helper: extrae un campo int de un nodo YAML si existe, ignorando errores - static void parseIntField(const fkyaml::node& node, const std::string& key, int& target) { - if (node.contains(key)) { - try { - target = node[key].get_value(); - } catch (...) {} - } - } - // Rellena los presets CrtPi por defecto static void populateDefaultCrtPiPresets() { crtpi_presets.clear(); @@ -289,20 +277,20 @@ namespace Options { if (p.contains("name")) { preset.name = p["name"].get_value(); } - parseFloatField(p, "scanline_weight", preset.scanline_weight); - parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness); - parseFloatField(p, "bloom_factor", preset.bloom_factor); - parseFloatField(p, "input_gamma", preset.input_gamma); - parseFloatField(p, "output_gamma", preset.output_gamma); - parseFloatField(p, "mask_brightness", preset.mask_brightness); - parseFloatField(p, "curvature_x", preset.curvature_x); - parseFloatField(p, "curvature_y", preset.curvature_y); - parseIntField(p, "mask_type", preset.mask_type); - parseBoolField(p, "enable_scanlines", preset.enable_scanlines); - parseBoolField(p, "enable_multisample", preset.enable_multisample); - parseBoolField(p, "enable_gamma", preset.enable_gamma); - parseBoolField(p, "enable_curvature", preset.enable_curvature); - parseBoolField(p, "enable_sharper", preset.enable_sharper); + parseField(p, "scanline_weight", preset.scanline_weight); + parseField(p, "scanline_gap_brightness", preset.scanline_gap_brightness); + parseField(p, "bloom_factor", preset.bloom_factor); + parseField(p, "input_gamma", preset.input_gamma); + parseField(p, "output_gamma", preset.output_gamma); + parseField(p, "mask_brightness", preset.mask_brightness); + parseField(p, "curvature_x", preset.curvature_x); + parseField(p, "curvature_y", preset.curvature_y); + parseField(p, "mask_type", preset.mask_type); + parseField(p, "enable_scanlines", preset.enable_scanlines); + parseField(p, "enable_multisample", preset.enable_multisample); + parseField(p, "enable_gamma", preset.enable_gamma); + parseField(p, "enable_curvature", preset.enable_curvature); + parseField(p, "enable_sharper", preset.enable_sharper); crtpi_presets.push_back(preset); } } @@ -353,98 +341,46 @@ namespace Options { void loadWindowFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("window")) { return; } const auto& win = yaml["window"]; - if (win.contains("zoom")) { - try { - int val = win["zoom"].get_value(); - window.zoom = (val > 0) ? val : window.zoom; - } catch (...) {} - } + int zoom = window.zoom; + parseField(win, "zoom", zoom); + window.zoom = (zoom > 0) ? zoom : window.zoom; } void loadVideoFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("video")) { return; } const auto& vid = yaml["video"]; - if (vid.contains("fullscreen")) { - try { - video.fullscreen = vid["fullscreen"].get_value(); - } catch (...) {} - } - if (vid.contains("scale_mode")) { - try { - video.scale_mode = static_cast(vid["scale_mode"].get_value()); - } catch (...) {} - } - if (vid.contains("vsync")) { - try { - video.vsync = vid["vsync"].get_value(); - } catch (...) {} - } - if (vid.contains("integer_scale")) { - try { - video.integer_scale = vid["integer_scale"].get_value(); - } catch (...) {} - } + parseField(vid, "fullscreen", video.fullscreen); + parseField(vid, "vsync", video.vsync); + parseField(vid, "integer_scale", video.integer_scale); + + int scale_mode_int = static_cast(video.scale_mode); + parseField(vid, "scale_mode", scale_mode_int); + video.scale_mode = static_cast(scale_mode_int); - // --- GPU --- if (vid.contains("gpu")) { const auto& gpu_node = vid["gpu"]; - if (gpu_node.contains("acceleration")) { - try { - video.gpu.acceleration = gpu_node["acceleration"].get_value(); - } catch (...) {} - } - if (gpu_node.contains("preferred_driver")) { - try { - video.gpu.preferred_driver = gpu_node["preferred_driver"].get_value(); - } catch (...) {} - } + parseField(gpu_node, "acceleration", video.gpu.acceleration); + parseField(gpu_node, "preferred_driver", video.gpu.preferred_driver); } - // --- Shader config --- if (vid.contains("shader")) { const auto& sh = vid["shader"]; - if (sh.contains("enabled")) { - try { - video.shader.enabled = sh["enabled"].get_value(); - } catch (...) {} - } - if (sh.contains("current_shader")) { - try { - auto s = sh["current_shader"].get_value(); - video.shader.current_shader = (s == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; - } catch (...) {} - } - if (sh.contains("postfx_preset")) { - try { - video.shader.current_postfx_preset_name = sh["postfx_preset"].get_value(); - } catch (...) {} - } - if (sh.contains("crtpi_preset")) { - try { - video.shader.current_crtpi_preset_name = sh["crtpi_preset"].get_value(); - } catch (...) {} + parseField(sh, "enabled", video.shader.enabled); + parseField(sh, "postfx_preset", video.shader.current_postfx_preset_name); + parseField(sh, "crtpi_preset", video.shader.current_crtpi_preset_name); + std::string shader_name; + parseField(sh, "current_shader", shader_name); + if (!shader_name.empty()) { + video.shader.current_shader = (shader_name == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; } } - // --- Supersampling --- if (vid.contains("supersampling")) { const auto& ss_node = vid["supersampling"]; - if (ss_node.contains("enabled")) { - try { - video.supersampling.enabled = ss_node["enabled"].get_value(); - } catch (...) {} - } - if (ss_node.contains("linear_upscale")) { - try { - video.supersampling.linear_upscale = ss_node["linear_upscale"].get_value(); - } catch (...) {} - } - if (ss_node.contains("downscale_algo")) { - try { - video.supersampling.downscale_algo = ss_node["downscale_algo"].get_value(); - } catch (...) {} - } + parseField(ss_node, "enabled", video.supersampling.enabled); + parseField(ss_node, "linear_upscale", video.supersampling.linear_upscale); + parseField(ss_node, "downscale_algo", video.supersampling.downscale_algo); } } @@ -452,50 +388,30 @@ namespace Options { if (!yaml.contains("audio")) { return; } const auto& aud = yaml["audio"]; - if (aud.contains("enabled")) { - try { - audio.enabled = aud["enabled"].get_value(); - } catch (...) {} - } - if (aud.contains("volume")) { - try { - audio.volume = std::clamp(aud["volume"].get_value(), 0.0F, 1.0F); - } catch (...) {} - } + parseField(aud, "enabled", audio.enabled); + parseField(aud, "volume", audio.volume); + audio.volume = std::clamp(audio.volume, 0.0F, 1.0F); + if (aud.contains("music")) { const auto& mus = aud["music"]; - if (mus.contains("enabled")) { - try { - audio.music.enabled = mus["enabled"].get_value(); - } catch (...) {} - } - if (mus.contains("volume")) { - try { - audio.music.volume = std::clamp(mus["volume"].get_value(), 0.0F, 1.0F); - } catch (...) {} - } + parseField(mus, "enabled", audio.music.enabled); + parseField(mus, "volume", audio.music.volume); + audio.music.volume = std::clamp(audio.music.volume, 0.0F, 1.0F); } if (aud.contains("sound")) { const auto& snd = aud["sound"]; - if (snd.contains("enabled")) { - try { - audio.sound.enabled = snd["enabled"].get_value(); - } catch (...) {} - } - if (snd.contains("volume")) { - try { - audio.sound.volume = std::clamp(snd["volume"].get_value(), 0.0F, 1.0F); - } catch (...) {} - } + parseField(snd, "enabled", audio.sound.enabled); + parseField(snd, "volume", audio.sound.volume); + audio.sound.volume = std::clamp(audio.sound.volume, 0.0F, 1.0F); } } void loadLoadingFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("loading")) { return; } const auto& ld = yaml["loading"]; - parseBoolField(ld, "show", loading.show); - parseBoolField(ld, "show_resource_name", loading.show_resource_name); - parseBoolField(ld, "wait_for_input", loading.wait_for_input); + parseField(ld, "show", loading.show); + parseField(ld, "show_resource_name", loading.show_resource_name); + parseField(ld, "wait_for_input", loading.wait_for_input); } void loadGameFromYaml(const fkyaml::node& yaml) { @@ -503,37 +419,23 @@ namespace Options { const auto& game = yaml["game"]; if (game.contains("language")) { - try { - auto lang = static_cast(game["language"].get_value()); - if (lang == Lang::Code::ENGLISH || lang == Lang::Code::VALENCIAN || lang == Lang::Code::SPANISH) { - settings.language = lang; - } else { - settings.language = Lang::Code::ENGLISH; - } - pending_changes.new_language = settings.language; - } catch (...) {} + int lang_int = static_cast(settings.language); + parseField(game, "language", lang_int); + const auto LANG = static_cast(lang_int); + settings.language = (LANG == Lang::Code::ENGLISH || LANG == Lang::Code::VALENCIAN || LANG == Lang::Code::SPANISH) + ? LANG + : Lang::Code::ENGLISH; + pending_changes.new_language = settings.language; } if (game.contains("difficulty")) { - try { - settings.difficulty = static_cast(game["difficulty"].get_value()); - pending_changes.new_difficulty = settings.difficulty; - } catch (...) {} - } - if (game.contains("autofire")) { - try { - settings.autofire = game["autofire"].get_value(); - } catch (...) {} - } - if (game.contains("shutdown_enabled")) { - try { - settings.shutdown_enabled = game["shutdown_enabled"].get_value(); - } catch (...) {} - } - if (game.contains("params_file")) { - try { - settings.params_file = game["params_file"].get_value(); - } catch (...) {} + int diff_int = static_cast(settings.difficulty); + parseField(game, "difficulty", diff_int); + settings.difficulty = static_cast(diff_int); + pending_changes.new_difficulty = settings.difficulty; } + parseField(game, "autofire", settings.autofire); + parseField(game, "shutdown_enabled", settings.shutdown_enabled); + parseField(game, "params_file", settings.params_file); } void loadControllersFromYaml(const fkyaml::node& yaml) { @@ -543,25 +445,14 @@ namespace Options { size_t i = 0; for (const auto& ctrl : controllers) { if (i >= GamepadManager::size()) { break; } - if (ctrl.contains("name")) { - try { - gamepad_manager[i].name = ctrl["name"].get_value(); - } catch (...) {} - } - if (ctrl.contains("path")) { - try { - gamepad_manager[i].path = ctrl["path"].get_value(); - } catch (...) {} - } - if (ctrl.contains("player")) { - try { - int player_int = ctrl["player"].get_value(); - if (player_int == 1) { - gamepad_manager[i].player_id = Player::Id::PLAYER1; - } else if (player_int == 2) { - gamepad_manager[i].player_id = Player::Id::PLAYER2; - } - } catch (...) {} + parseField(ctrl, "name", gamepad_manager[i].name); + parseField(ctrl, "path", gamepad_manager[i].path); + int player_int = 0; + parseField(ctrl, "player", player_int); + if (player_int == 1) { + gamepad_manager[i].player_id = Player::Id::PLAYER1; + } else if (player_int == 2) { + gamepad_manager[i].player_id = Player::Id::PLAYER2; } ++i; } @@ -570,11 +461,9 @@ namespace Options { void loadKeyboardFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("keyboard")) { return; } const auto& kb = yaml["keyboard"]; - if (kb.contains("player")) { - try { - keyboard.player_id = static_cast(kb["player"].get_value()); - } catch (...) {} - } + int player_int = static_cast(keyboard.player_id); + parseField(kb, "player", player_int); + keyboard.player_id = static_cast(player_int); } // Carga el fichero de configuración @@ -595,11 +484,7 @@ namespace Options { // Comprobar versión: si no coincide, regenerar config por defecto int file_version = 0; - if (yaml.contains("version")) { - try { - file_version = yaml["version"].get_value(); - } catch (...) {} - } + parseField(yaml, "version", file_version); if (file_version != Settings::CURRENT_CONFIG_VERSION) { std::cout << "Config version " << file_version << " != expected " << Settings::CURRENT_CONFIG_VERSION << ". Recreating defaults." << '\n'; init(); @@ -820,14 +705,14 @@ namespace Options { continue; } - const auto it = std::ranges::find_if(physical_gamepads, - [this, &desired_path, &assigned_instances](const auto& pg) { + const auto IT = std::ranges::find_if(physical_gamepads, + [&desired_path, &assigned_instances](const auto& pg) { return pg->path == desired_path && !isGamepadAssigned(pg, assigned_instances); }); - if (it != physical_gamepads.end()) { - gamepads_[i].instance = *it; - gamepads_[i].name = (*it)->name; - assigned_instances.push_back(*it); + if (IT != physical_gamepads.end()) { + gamepads_[i].instance = *IT; + gamepads_[i].name = (*IT)->name; + assigned_instances.push_back(*IT); } } } @@ -849,15 +734,15 @@ namespace Options { continue; } - const auto it = std::ranges::find_if(physical_gamepads, - [this, &desired_name, &assigned_instances](const auto& pg) { + const auto IT = std::ranges::find_if(physical_gamepads, + [&desired_name, &assigned_instances](const auto& pg) { return pg->name == desired_name && !isGamepadAssigned(pg, assigned_instances); }); - if (it != physical_gamepads.end()) { - gamepads_[i].instance = *it; - gamepads_[i].name = (*it)->name; - gamepads_[i].path = (*it)->path; - assigned_instances.push_back(*it); + if (IT != physical_gamepads.end()) { + gamepads_[i].instance = *IT; + gamepads_[i].name = (*IT)->name; + gamepads_[i].path = (*IT)->path; + assigned_instances.push_back(*IT); } } } @@ -871,15 +756,15 @@ namespace Options { continue; } - const auto it = std::ranges::find_if(physical_gamepads, - [this, &assigned_instances](const auto& pg) { + const auto IT = std::ranges::find_if(physical_gamepads, + [&assigned_instances](const auto& pg) { return !isGamepadAssigned(pg, assigned_instances); }); - if (it != physical_gamepads.end()) { - gamepads_[i].instance = *it; - gamepads_[i].name = (*it)->name; - gamepads_[i].path = (*it)->path; - assigned_instances.push_back(*it); + if (IT != physical_gamepads.end()) { + gamepads_[i].instance = *IT; + gamepads_[i].name = (*IT)->name; + gamepads_[i].path = (*IT)->path; + assigned_instances.push_back(*IT); } } }