From 94aa69cffe622fc20abb341d26f6a15ff03f4a6a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 18 Apr 2026 11:41:34 +0200 Subject: [PATCH] afegit resource::cache normalitzat Audio --- CMakeLists.txt | 10 +- data/config/assets.yaml | 52 ++++ source/core/audio/audio.cpp | 212 ++++++++++++++ source/core/audio/audio.hpp | 114 ++++++++ source/core/audio/audio_adapter.cpp | 15 + source/core/audio/audio_adapter.hpp | 17 ++ source/core/{jail => audio}/jail_audio.hpp | 232 ++++++++++++--- source/core/jail/jail_audio.cpp | 12 - source/core/jail/jdraw8.cpp | 61 +++- source/core/jail/jdraw8.hpp | 4 + source/core/rendering/menu.cpp | 12 +- source/core/resources/resource_cache.cpp | 272 ++++++++++++++++++ source/core/resources/resource_cache.hpp | 72 +++++ source/core/resources/resource_list.cpp | 114 ++++++++ source/core/resources/resource_list.hpp | 61 ++++ source/core/resources/resource_types.hpp | 61 ++++ source/core/system/director.cpp | 21 +- .../external/{stb_vorbis.h => stb_vorbis.c} | 0 source/game/modulegame.cpp | 23 +- source/game/options.cpp | 32 ++- source/game/options.hpp | 16 +- source/main.cpp | 19 +- source/scenes/banner_scene.cpp | 4 +- source/scenes/boot_loader_scene.cpp | 55 ++++ source/scenes/boot_loader_scene.hpp | 27 ++ source/scenes/credits_scene.cpp | 4 +- source/scenes/scene_utils.cpp | 22 +- source/scenes/secreta_scene.cpp | 4 +- source/scenes/slides_scene.cpp | 6 +- 29 files changed, 1420 insertions(+), 134 deletions(-) create mode 100644 data/config/assets.yaml create mode 100644 source/core/audio/audio.cpp create mode 100644 source/core/audio/audio.hpp create mode 100644 source/core/audio/audio_adapter.cpp create mode 100644 source/core/audio/audio_adapter.hpp rename source/core/{jail => audio}/jail_audio.hpp (68%) delete mode 100644 source/core/jail/jail_audio.cpp create mode 100644 source/core/resources/resource_cache.cpp create mode 100644 source/core/resources/resource_cache.hpp create mode 100644 source/core/resources/resource_list.cpp create mode 100644 source/core/resources/resource_list.hpp create mode 100644 source/core/resources/resource_types.hpp rename source/external/{stb_vorbis.h => stb_vorbis.c} (100%) create mode 100644 source/scenes/boot_loader_scene.cpp create mode 100644 source/scenes/boot_loader_scene.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fec076d..f6e4043 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,18 +41,23 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi # --- LISTA EXPLÍCITA DE FUENTES --- set(APP_SOURCES # Core - Motor original "Jail" (no tocar gameplay) - source/core/jail/jail_audio.cpp source/core/jail/jdraw8.cpp source/core/jail/jfile.cpp source/core/jail/jgame.cpp source/core/jail/jinput.cpp + # Core - Audio (wrapper canònic compartit amb la resta de projectes) + source/core/audio/audio.cpp + source/core/audio/audio_adapter.cpp + # Core - Locale (nova capa) source/core/locale/locale.cpp - # Core - Resources (pack binari AEE1, estil coffee_crisis) + # Core - Resources (pack binari AEE1 + cache d'assets precarregats) source/core/resources/resource_pack.cpp source/core/resources/resource_helper.cpp + source/core/resources/resource_list.cpp + source/core/resources/resource_cache.cpp # Core - Capa de presentación (nueva) source/core/rendering/menu.cpp @@ -81,6 +86,7 @@ set(APP_SOURCES source/scenes/surface_handle.cpp source/scenes/scene_registry.cpp source/scenes/scene_utils.cpp + source/scenes/boot_loader_scene.cpp source/scenes/mort_scene.cpp source/scenes/banner_scene.cpp source/scenes/menu_scene.cpp diff --git a/data/config/assets.yaml b/data/config/assets.yaml new file mode 100644 index 0000000..4b8a5d8 --- /dev/null +++ b/data/config/assets.yaml @@ -0,0 +1,52 @@ +# Aventures En Egipte - Asset Configuration +# Loaded at boot by Resource::List, decoded incrementally by Resource::Cache. +# Paths are relative to the resource pack root (i.e. relative to ./data/ in dev). + +assets: + # FONTS - bitmap font for the overlay (8bithud) + fonts: + BITMAP: + - fonts/8bithud.gif + FONT: + - fonts/8bithud.fnt + + # LOCALE - UI strings + locale: + DATA: + - locale/ca.yaml + + # INPUT - UI key bindings defaults + input: + DATA: + - input/keys.yaml + + # MUSIC - 8 OGG tracks + music: + MUSIC: + - music/banner.ogg + - music/final.ogg + - music/menu.ogg + - music/mort.ogg + - music/piramide_1_4_5.ogg + - music/piramide_2.ogg + - music/piramide_3.ogg + - music/secreta.ogg + + # GFX - 14 GIFs (sprites + cinematic backgrounds) + gfx: + BITMAP: + - gfx/ffase.gif + - gfx/final.gif + - gfx/finals.gif + - gfx/frames.gif + - gfx/frames2.gif + - gfx/gameover.gif + - gfx/intro.gif + - gfx/intro2.gif + - gfx/intro3.gif + - gfx/logo.gif + - gfx/logo_new.gif + - gfx/menu.gif + - gfx/menu2.gif + - gfx/tomba1.gif + - gfx/tomba2.gif diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp new file mode 100644 index 0000000..633a767 --- /dev/null +++ b/source/core/audio/audio.cpp @@ -0,0 +1,212 @@ +#include "core/audio/audio.hpp" + +#include // Para SDL_GetError, SDL_Init + +#include // Para clamp +#include // Para std::cout + +// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp). +// clang-format off +#undef STB_VORBIS_HEADER_ONLY +#include "external/stb_vorbis.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. +#undef L +#undef C +#undef R +#undef PLAYBACK_MONO +#undef PLAYBACK_LEFT +#undef PLAYBACK_RIGHT +// clang-format on + +#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound +#include "core/audio/jail_audio.hpp" // Para JA_* +#include "game/options.hpp" // Para Options::audio + +// Singleton +Audio* Audio::instance = nullptr; + +// Inicializa la instancia única del singleton +void Audio::init() { Audio::instance = new Audio(); } + +// Libera la instancia +void Audio::destroy() { + delete Audio::instance; + Audio::instance = nullptr; +} + +// Obtiene la instancia +auto Audio::get() -> Audio* { return Audio::instance; } + +// Constructor +Audio::Audio() { initSDLAudio(); } + +// Destructor +Audio::~Audio() { + JA_Quit(); +} + +// Método principal +void Audio::update() { + JA_Update(); + + // 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) { + instance->music_.state = MusicState::STOPPED; + } +} + +// Reproduce la música por nombre (con crossfade opcional) +void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) { + bool new_loop = (loop != 0); + + // Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada + if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) { + return; + } + + if (!music_enabled_) return; + + auto* resource = AudioResource::getMusic(name); + if (resource == nullptr) return; + + if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) { + JA_CrossfadeMusic(resource, crossfade_ms, loop); + } else { + if (music_.state == MusicState::PLAYING) { + JA_StopMusic(); + } + JA_PlayMusic(resource, loop); + } + + music_.name = name; + music_.loop = new_loop; + music_.state = MusicState::PLAYING; +} + +// 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; + + if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) { + JA_CrossfadeMusic(music, crossfade_ms, loop); + } else { + if (music_.state == MusicState::PLAYING) { + JA_StopMusic(); + } + JA_PlayMusic(music, loop); + } + + music_.name.clear(); // nom desconegut quan es passa per punter + music_.loop = (loop != 0); + music_.state = MusicState::PLAYING; +} + +// Pausa la música +void Audio::pauseMusic() { + if (music_enabled_ && music_.state == MusicState::PLAYING) { + JA_PauseMusic(); + music_.state = MusicState::PAUSED; + } +} + +// Continua la música pausada +void Audio::resumeMusic() { + if (music_enabled_ && music_.state == MusicState::PAUSED) { + JA_ResumeMusic(); + music_.state = MusicState::PLAYING; + } +} + +// Detiene la música +void Audio::stopMusic() { + if (music_enabled_) { + JA_StopMusic(); + music_.state = MusicState::STOPPED; + } +} + +// Reproduce un sonido por nombre +void Audio::playSound(const std::string& name, Group group) const { + if (sound_enabled_) { + JA_PlaySound(AudioResource::getSound(name), 0, static_cast(group)); + } +} + +// Reproduce un sonido por puntero directo +void Audio::playSound(JA_Sound_t* sound, Group group) const { + if (sound_enabled_ && sound != nullptr) { + JA_PlaySound(sound, 0, static_cast(group)); + } +} + +// Detiene todos los sonidos +void Audio::stopAllSounds() const { + if (sound_enabled_) { + JA_StopChannel(-1); + } +} + +// Realiza un fundido de salida de la música +void Audio::fadeOutMusic(int milliseconds) const { + if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) { + JA_FadeOutMusic(milliseconds); + } +} + +// Consulta directamente el estado real de la música en jailaudio +auto Audio::getRealMusicState() -> MusicState { + JA_Music_state ja_state = JA_GetMusicState(); + switch (ja_state) { + case JA_MUSIC_PLAYING: + return MusicState::PLAYING; + case JA_MUSIC_PAUSED: + return MusicState::PAUSED; + case JA_MUSIC_STOPPED: + case JA_MUSIC_INVALID: + case JA_MUSIC_DISABLED: + default: + return MusicState::STOPPED; + } +} + +// Establece el volumen de los sonidos (float 0.0..1.0) +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)); + } +} + +// Establece el volumen de la música (float 0.0..1.0) +void Audio::setMusicVolume(float music_volume) const { + if (music_enabled_) { + music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME); + const float CONVERTED_VOLUME = music_volume * Options::audio.volume; + JA_SetMusicVolume(CONVERTED_VOLUME); + } +} + +// Aplica la configuración +void Audio::applySettings() { + enable(Options::audio.enabled); +} + +// Establecer estado general +void Audio::enable(bool value) { + enabled_ = value; + + setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME); + setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME); +} + +// Inicializa SDL Audio +void Audio::initSDLAudio() { + if (!SDL_Init(SDL_INIT_AUDIO)) { + std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n'; + } else { + 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 new file mode 100644 index 0000000..9c5ea6f --- /dev/null +++ b/source/core/audio/audio.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include // Para int8_t, uint8_t +#include // Para string +#include // Para move + +// --- Clase Audio: gestor de audio (singleton) --- +// Implementació canònica, byte-idèntica entre projectes. +// Els volums es manegen internament com a float 0.0–1.0; la capa de +// presentació (menús, notificacions) usa les helpers toPercent/fromPercent +// per mostrar 0–100 a l'usuari. +class Audio { + public: + // --- Enums --- + enum class Group : std::int8_t { + ALL = -1, // Todos los grupos + GAME = 0, // Sonidos del juego + INTERFACE = 1 // Sonidos de la interfaz + }; + + enum class MusicState : std::uint8_t { + PLAYING, // Reproduciendo música + PAUSED, // Música pausada + STOPPED, // Música detenida + }; + + // --- Constantes --- + static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1) + static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1) + static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%) + static constexpr int FREQUENCY = 48000; // Frecuencia de audio + static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms) + + // --- Singleton --- + static void init(); // Inicializa el objeto Audio + static void destroy(); // Libera el objeto Audio + static auto get() -> Audio*; // Obtiene el puntero al objeto Audio + Audio(const Audio&) = delete; // Evitar copia + auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación + + static void update(); // Actualización del sistema de audio + + // --- Control de música --- + void playMusic(const std::string& name, int loop = -1, 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 + + // --- Control de sonidos --- + void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre + void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero + void stopAllSounds() const; // Detener todos los sonidos + + // --- Control de volumen (API interna: float 0.0..1.0) --- + void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos + void setMusicVolume(float volume) const; // Ajustar volumen de música + + // --- 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 constexpr auto fromPercent(int percent) -> float { + return static_cast(percent) / 100.0F; + } + + // --- Configuración general --- + void enable(bool value); // Establecer estado general + void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general + void applySettings(); // Aplica la configuración + + // --- Configuración de sonidos --- + void enableSound() { sound_enabled_ = true; } // Habilitar sonidos + void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos + void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos + void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos + + // --- Configuración de música --- + void enableMusic() { music_enabled_ = true; } // Habilitar música + void disableMusic() { music_enabled_ = false; } // Deshabilitar música + void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música + void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música + + // --- Consultas de estado --- + [[nodiscard]] auto isEnabled() const -> bool { return enabled_; } + [[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; } + [[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; } + [[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; } + [[nodiscard]] static auto getRealMusicState() -> MusicState; + [[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; } + + private: + // --- Tipos anidados --- + struct Music { + MusicState state{MusicState::STOPPED}; // Estado actual de la música + std::string name; // Última pista de música reproducida + bool loop{false}; // Indica si se reproduce en bucle + }; + + // --- Métodos --- + Audio(); // Constructor privado + ~Audio(); // Destructor privado + void initSDLAudio(); // Inicializa SDL Audio + + // --- Variables miembro --- + static Audio* instance; // Instancia única de Audio + + Music music_; // Estado de la música + bool enabled_{true}; // Estado general del audio + bool sound_enabled_{true}; // Estado de los efectos de sonido + bool music_enabled_{true}; // Estado de la música +}; diff --git a/source/core/audio/audio_adapter.cpp b/source/core/audio/audio_adapter.cpp new file mode 100644 index 0000000..ddf6575 --- /dev/null +++ b/source/core/audio/audio_adapter.cpp @@ -0,0 +1,15 @@ +#include "core/audio/audio_adapter.hpp" + +#include "core/resources/resource_cache.hpp" + +namespace AudioResource { + + JA_Music_t* getMusic(const std::string& name) { + return Resource::Cache::get()->getMusic(name); + } + + JA_Sound_t* getSound(const std::string& name) { + return Resource::Cache::get()->getSound(name); + } + +} // namespace AudioResource diff --git a/source/core/audio/audio_adapter.hpp b/source/core/audio/audio_adapter.hpp new file mode 100644 index 0000000..a5eb16e --- /dev/null +++ b/source/core/audio/audio_adapter.hpp @@ -0,0 +1,17 @@ +#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 +// 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 AudioResource { + JA_Music_t* getMusic(const std::string& name); + JA_Sound_t* getSound(const std::string& name); +} // namespace AudioResource diff --git a/source/core/jail/jail_audio.hpp b/source/core/audio/jail_audio.hpp similarity index 68% rename from source/core/jail/jail_audio.hpp rename to source/core/audio/jail_audio.hpp index 600eaa5..63308e0 100644 --- a/source/core/jail/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -2,17 +2,17 @@ // --- Includes --- #include -#include -#include -#include -#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 -#include -#include +#include // Para std::cout +#include // Para std::unique_ptr +#include // Para std::string +#include // Para std::vector #define STB_VORBIS_HEADER_ONLY -#include "external/stb_vorbis.h" +#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 @@ -90,15 +90,27 @@ inline bool JA_musicEnabled{true}; inline bool JA_soundEnabled{true}; inline SDL_AudioDeviceID sdlAudioDevice{0}; -inline bool fading{false}; -inline int fade_start_time{0}; -inline int fade_duration{0}; -inline float fade_initial_volume{0.0f}; +// --- Crossfade / Fade State --- +struct JA_FadeState { + bool active{false}; + Uint64 start_time{0}; + int duration_ms{0}; + float initial_volume{0.0f}; +}; + +struct JA_OutgoingMusic { + SDL_AudioStream* stream{nullptr}; + JA_FadeState fade; +}; + +inline JA_OutgoingMusic outgoing_music; +inline JA_FadeState incoming_fade; // --- 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); // --- Music streaming internals --- // Bytes-per-sample per canal (sempre s16) @@ -106,7 +118,7 @@ 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'àudio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a +// 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; @@ -116,15 +128,15 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) { if (!music || !music->vorbis || !music->stream) return 0; short chunk[JA_MUSIC_CHUNK_SHORTS]; - const int channels = music->spec.channels; + const int num_channels = music->spec.channels; const int samples_per_channel = stb_vorbis_get_samples_short_interleaved( music->vorbis, - channels, + num_channels, chunk, JA_MUSIC_CHUNK_SHORTS); if (samples_per_channel <= 0) return 0; - const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE; + const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE; SDL_PutAudioStreamData(music->stream, chunk, bytes); return samples_per_channel; } @@ -151,23 +163,51 @@ inline void JA_PumpMusic(JA_Music_t* music) { } } +// 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; + + 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); + + while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) { + const int decoded = JA_FeedMusicChunk(music); + if (decoded <= 0) break; // EOF: deixem drenar el que hi haja + } +} + // --- Core Functions --- -// Crida-la una vegada per frame des del main loop (Director). Substituïx -// el callback asíncron SDL_AddTimer de la versió antiga — imprescindible -// per al port a emscripten/SDL_AppIterate. 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) { + 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)); + } + } + + // --- Current music --- if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { - if (fading) { - int time = SDL_GetTicks(); - if (time > (fade_start_time + fade_duration)) { - fading = false; - JA_StopMusic(); - return; + // 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 { - const int time_passed = time - fade_start_time; - const float percent = (float)time_passed / (float)fade_duration; - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0f - percent)); + float percent = (float)elapsed / (float)incoming_fade.duration_ms; + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent); } } @@ -179,6 +219,7 @@ inline void JA_Update() { } } + // --- Sound channels --- if (JA_soundEnabled) { for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) if (channels[i].state == JA_CHANNEL_PLAYING) { @@ -195,19 +236,19 @@ inline void JA_Update() { } inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { -#ifdef _DEBUG - SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); -#endif - JA_audioSpec = {format, num_channels, freq}; if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); - if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!"); + 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; } @@ -230,7 +271,7 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { &error, nullptr); if (!music->vorbis) { - SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error); + std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << error << ")" << '\n'; delete music; return nullptr; } @@ -252,6 +293,35 @@ inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filena 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; + } + + free(buffer); + + return music; +} + inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { if (!JA_musicEnabled || !music || !music->vorbis) return; @@ -267,7 +337,7 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); if (!current_music->stream) { - SDL_Log("Failed to create audio stream!"); + std::cout << "Failed to create audio stream!" << '\n'; current_music->state = JA_MUSIC_STOPPED; return; } @@ -276,10 +346,12 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. JA_PumpMusic(current_music); - if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n"); + if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) { + std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n'; + } } -inline const char* JA_GetMusicFilename(JA_Music_t* music = nullptr) { +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(); @@ -302,6 +374,14 @@ inline void JA_ResumeMusic() { } 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; + } + 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; @@ -318,12 +398,69 @@ inline void JA_StopMusic() { inline void JA_FadeOutMusic(const int milliseconds) { if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; + if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; - fading = true; - fade_start_time = SDL_GetTicks(); - fade_duration = milliseconds; - fade_initial_volume = JA_musicVolume; + // Destruir outgoing anterior si existe + if (outgoing_music.stream) { + 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. + 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; + } + + // 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); + outgoing_music.stream = current_music->stream; + outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume}; + current_music->stream = nullptr; + current_music->state = JA_MUSIC_STOPPED; + if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis); + } + + // 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; + + 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() { @@ -373,7 +510,7 @@ 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)) { - SDL_Log("Failed to load WAV from memory: %s", SDL_GetError()); + std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n'; return nullptr; } sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer @@ -384,7 +521,7 @@ 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)) { - SDL_Log("Failed to load WAV file: %s", SDL_GetError()); + std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n'; return nullptr; } sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer @@ -396,7 +533,10 @@ inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = int channel = 0; while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; } - if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0; + if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) { + // No hay canal libre, reemplazamos el primero + channel = 0; + } return JA_PlaySoundOnChannel(sound, channel, loop, group); } @@ -415,7 +555,7 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); if (!channels[channel].stream) { - SDL_Log("Failed to create audio stream for sound!"); + std::cout << "Failed to create audio stream for sound!" << '\n'; channels[channel].state = JA_CHANNEL_FREE; return -1; } diff --git a/source/core/jail/jail_audio.cpp b/source/core/jail/jail_audio.cpp deleted file mode 100644 index 87176fc..0000000 --- a/source/core/jail/jail_audio.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// Aquest fitxer existeix per a albergar la implementació completa de -// stb_vorbis en una única unitat de compilació. Totes les funcions JA_* -// viuen `inline` a jail_audio.hpp (header-only, inspirat en el motor de -// jaildoctors_dilemma). Sense aquest stub tindríem múltiples definicions -// de les funcions de stb_vorbis si més d'un .cpp inclou el header. - -// clang-format off -#undef STB_VORBIS_HEADER_ONLY -#include "external/stb_vorbis.h" -// clang-format on - -#include "core/jail/jail_audio.hpp" diff --git a/source/core/jail/jdraw8.cpp b/source/core/jail/jdraw8.cpp index 0c0840d..99d796e 100644 --- a/source/core/jail/jdraw8.cpp +++ b/source/core/jail/jdraw8.cpp @@ -1,7 +1,9 @@ #include "core/jail/jdraw8.hpp" #include +#include +#include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #if defined(__clang__) #pragma clang diagnostic push @@ -43,25 +45,57 @@ JD8_Surface JD8_NewSurface() { return surface; } -JD8_Surface JD8_LoadSurface(const char* file) { - auto buffer = ResourceHelper::loadFile(file); +// Helper intern: deriva el basename d'una ruta per a buscar al Cache. +static std::string jd8_basename(const char* file) { + std::string s = file; + auto pos = s.find_last_of("/\\"); + return pos == std::string::npos ? s : s.substr(pos + 1); +} +JD8_Surface JD8_LoadSurface(const char* file) { + // Prova primer el Resource::Cache. Si l'asset és precarregat, copiem + // els 64KB des del cache (microsegons) i ens estalviem la decodificació + // GIF. Mantenim el contracte de la funció: el caller rep un buffer + // fresc que ha d'alliberar amb JD8_FreeSurface. + if (Resource::Cache::get() != nullptr) { + try { + const auto& cached = Resource::Cache::get()->getSurfacePixels(jd8_basename(file)); + JD8_Surface image = JD8_NewSurface(); + memcpy(image, cached.data(), 64000); + return image; + } catch (const std::exception&) { + // No està al cache (asset no llistat al manifest). Fallback. + } + } + + auto buffer = ResourceHelper::loadFile(file); unsigned short w, h; Uint8* pixels = LoadGif(buffer.data(), &w, &h); - if (pixels == NULL) { printf("Unable to load bitmap: %s\n", SDL_GetError()); exit(1); } - JD8_Surface image = JD8_NewSurface(); memcpy(image, pixels, 64000); - free(pixels); return image; } JD8_Palette JD8_LoadPalette(const char* file) { + if (Resource::Cache::get() != nullptr) { + try { + const auto& cached = Resource::Cache::get()->getPaletteBytes(jd8_basename(file)); + // Reservem un buffer 768 bytes (256 * RGB) que el caller ha + // d'alliberar amb free() — mateixa convenció que el LoadPalette + // original (retornava un malloc). + JD8_Palette palette = (JD8_Palette)malloc(768); + memcpy(palette, cached.data(), 768); + return palette; + } catch (const std::exception&) { + // No està al cache. + } + } + auto buffer = ResourceHelper::loadFile(file); return (JD8_Palette)LoadPalette(buffer.data()); } @@ -78,6 +112,23 @@ void JD8_FillSquare(int ini, int height, Uint8 color) { memset(&screen[offset], color, size); } +void JD8_FillRect(int x, int y, int w, int h, Uint8 color) { + if (x < 0) { + w += x; + x = 0; + } + if (y < 0) { + h += y; + y = 0; + } + if (x + w > 320) w = 320 - x; + if (y + h > 200) h = 200 - y; + if (w <= 0 || h <= 0) return; + for (int row = y; row < y + h; ++row) { + memset(&screen[x + (row * 320)], color, w); + } +} + void JD8_Blit(JD8_Surface surface) { memcpy(screen, surface, 64000); } diff --git a/source/core/jail/jdraw8.hpp b/source/core/jail/jdraw8.hpp index 23d979f..dc45354 100644 --- a/source/core/jail/jdraw8.hpp +++ b/source/core/jail/jdraw8.hpp @@ -26,6 +26,10 @@ void JD8_SetScreenPalette(JD8_Palette palette); void JD8_FillSquare(int ini, int height, Uint8 color); +// Omple un rectangle arbitrari de la pantalla amb un color paletat. +// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.). +void JD8_FillRect(int x, int y, int w, int h, Uint8 color); + void JD8_Blit(JD8_Surface surface); void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh); diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index 5adc009..6b7aea3 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -249,17 +249,17 @@ namespace Menu { p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr}); - p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music_enabled); }, [](int) { - Options::audio.music_enabled = !Options::audio.music_enabled; + p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music.enabled); }, [](int) { + Options::audio.music.enabled = !Options::audio.music.enabled; Options::applyAudio(); }, nullptr}); - p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music_volume); }, [](int dir) { stepVolume(Options::audio.music_volume, dir); }, nullptr}); + p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr}); - p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound_enabled); }, [](int) { - Options::audio.sound_enabled = !Options::audio.sound_enabled; + p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound.enabled); }, [](int) { + Options::audio.sound.enabled = !Options::audio.sound.enabled; Options::applyAudio(); }, nullptr}); - p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound_volume); }, [](int dir) { stepVolume(Options::audio.sound_volume, dir); }, nullptr}); + p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr}); return p; } diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp new file mode 100644 index 0000000..4a7cde1 --- /dev/null +++ b/source/core/resources/resource_cache.cpp @@ -0,0 +1,272 @@ +#include "core/resources/resource_cache.hpp" + +#include + +#include +#include +#include +#include + +#include "core/audio/jail_audio.hpp" +#include "core/resources/resource_helper.hpp" +#include "core/resources/resource_list.hpp" + +// gif.h ja s'inclou des de jdraw8.cpp i text.cpp; el seu codi no és static +// ni inline, així que no podem tornar-lo a incloure aquí. Ens fiem de les +// declaracions extern dels símbols que ens calen (linkatge C++ normal, +// igual que fa text.cpp). +extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h); +extern unsigned char* LoadPalette(unsigned char* data); + +namespace Resource { + + Cache* Cache::instance = nullptr; + + void Cache::init() { instance = new Cache(); } + void Cache::destroy() { + delete instance; + instance = nullptr; + } + auto Cache::get() -> Cache* { return instance; } + + namespace { + auto basename(const std::string& path) -> std::string { + auto pos = path.find_last_of("/\\"); + return pos == std::string::npos ? path : path.substr(pos + 1); + } + } // namespace + + auto Cache::getMusic(const std::string& name) -> JA_Music_t* { + auto it = std::ranges::find_if(musics_, [&](const auto& m) { return m.name == name; }); + if (it != musics_.end()) { + return it->music.get(); + } + std::cerr << "Resource::Cache: música no trobada: " << name << '\n'; + throw std::runtime_error("Music not found: " + name); + } + + auto Cache::getSound(const std::string& name) -> JA_Sound_t* { + auto it = std::ranges::find_if(sounds_, [&](const auto& s) { return s.name == name; }); + if (it != sounds_.end()) { + return it->sound.get(); + } + std::cerr << "Resource::Cache: so no trobat: " << name << '\n'; + throw std::runtime_error("Sound not found: " + name); + } + + auto Cache::getSurfacePixels(const std::string& name) -> const std::vector& { + auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; }); + if (it != surfaces_.end()) { + return it->pixels; + } + std::cerr << "Resource::Cache: surface no trobada: " << name << '\n'; + throw std::runtime_error("Surface not found: " + name); + } + + auto Cache::getPaletteBytes(const std::string& name) -> const std::vector& { + auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; }); + if (it != surfaces_.end()) { + return it->palette; + } + std::cerr << "Resource::Cache: paleta no trobada: " << name << '\n'; + throw std::runtime_error("Palette not found: " + name); + } + + auto Cache::getTextFile(const std::string& name) -> const std::vector& { + auto it = std::ranges::find_if(text_files_, [&](const auto& t) { return t.name == name; }); + if (it != text_files_.end()) { + return it->bytes; + } + std::cerr << "Resource::Cache: text file no trobat: " << name << '\n'; + throw std::runtime_error("TextFile not found: " + name); + } + + void Cache::calculateTotal() { + auto* list = List::get(); + total_count_ = static_cast( + list->getListByType(List::Type::MUSIC).size() + + list->getListByType(List::Type::SOUND).size() + + list->getListByType(List::Type::BITMAP).size() + + list->getListByType(List::Type::DATA).size() + + list->getListByType(List::Type::FONT).size()); + loaded_count_ = 0; + } + + auto Cache::getProgress() const -> float { + if (total_count_ == 0) return 1.0F; + return static_cast(loaded_count_) / static_cast(total_count_); + } + + void Cache::beginLoad() { + calculateTotal(); + stage_ = LoadStage::MUSICS; + stage_index_ = 0; + std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n"; + } + + auto Cache::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; + auto* list = List::get(); + + while (stage_ != LoadStage::DONE) { + switch (stage_) { + case LoadStage::MUSICS: { + auto items = list->getListByType(List::Type::MUSIC); + if (stage_index_ == 0) musics_.clear(); + if (stage_index_ >= items.size()) { + stage_ = LoadStage::SOUNDS; + stage_index_ = 0; + break; + } + loadOneMusic(stage_index_++); + break; + } + case LoadStage::SOUNDS: { + auto items = list->getListByType(List::Type::SOUND); + if (stage_index_ == 0) sounds_.clear(); + if (stage_index_ >= items.size()) { + stage_ = LoadStage::BITMAPS; + stage_index_ = 0; + break; + } + loadOneSound(stage_index_++); + break; + } + case LoadStage::BITMAPS: { + auto items = list->getListByType(List::Type::BITMAP); + if (stage_index_ == 0) surfaces_.clear(); + if (stage_index_ >= items.size()) { + stage_ = LoadStage::TEXT_FILES; + stage_index_ = 0; + break; + } + loadOneBitmap(stage_index_++); + break; + } + case LoadStage::TEXT_FILES: { + auto data_items = list->getListByType(List::Type::DATA); + auto font_items = list->getListByType(List::Type::FONT); + auto items = data_items; + items.insert(items.end(), font_items.begin(), font_items.end()); + if (stage_index_ == 0) text_files_.clear(); + if (stage_index_ >= items.size()) { + stage_ = LoadStage::DONE; + stage_index_ = 0; + std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n"; + break; + } + loadOneTextFile(stage_index_++); + break; + } + case LoadStage::DONE: + break; + } + if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break; + } + + return stage_ == LoadStage::DONE; + } + + void Cache::loadOneMusic(size_t index) { + auto items = List::get()->getListByType(List::Type::MUSIC); + const auto& path = items[index]; + auto name = basename(path); + current_loading_name_ = name; + + auto bytes = ResourceHelper::loadFile(path); + if (bytes.empty()) { + std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n'; + return; + } + JA_Music_t* music = JA_LoadMusic(bytes.data(), static_cast(bytes.size()), path.c_str()); + if (music == nullptr) { + std::cerr << "Resource::Cache: JA_LoadMusic ha fallat per " << path << '\n'; + return; + } + musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr(music)}); + ++loaded_count_; + std::cout << " [music ] " << name << '\n'; + } + + void Cache::loadOneSound(size_t index) { + auto items = List::get()->getListByType(List::Type::SOUND); + const auto& path = items[index]; + auto name = basename(path); + current_loading_name_ = name; + + auto bytes = ResourceHelper::loadFile(path); + if (bytes.empty()) { + std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n'; + return; + } + JA_Sound_t* sound = JA_LoadSound(bytes.data(), static_cast(bytes.size())); + if (sound == nullptr) { + std::cerr << "Resource::Cache: JA_LoadSound ha fallat per " << path << '\n'; + return; + } + sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr(sound)}); + ++loaded_count_; + std::cout << " [sound ] " << name << '\n'; + } + + void Cache::loadOneBitmap(size_t index) { + auto items = List::get()->getListByType(List::Type::BITMAP); + const auto& path = items[index]; + auto name = basename(path); + current_loading_name_ = name; + + auto bytes = ResourceHelper::loadFile(path); + if (bytes.empty()) { + std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n'; + return; + } + + // Decodifica píxels. + unsigned short w = 0; + unsigned short h = 0; + unsigned char* pixels = LoadGif(bytes.data(), &w, &h); + if (pixels == nullptr) { + std::cerr << "Resource::Cache: LoadGif ha fallat per " << path << '\n'; + return; + } + SurfaceResource res; + res.name = name; + res.pixels.assign(pixels, pixels + 64000); + std::free(pixels); + + // Decodifica paleta des del mateix GIF (necessita una segona passada + // perquè LoadGif no exposa la paleta). + unsigned char* palette = LoadPalette(bytes.data()); + if (palette != nullptr) { + res.palette.assign(palette, palette + 768); + std::free(palette); + } + + surfaces_.push_back(std::move(res)); + ++loaded_count_; + std::cout << " [bitmap] " << name << '\n'; + } + + void Cache::loadOneTextFile(size_t index) { + auto data_items = List::get()->getListByType(List::Type::DATA); + auto font_items = List::get()->getListByType(List::Type::FONT); + auto items = data_items; + items.insert(items.end(), font_items.begin(), font_items.end()); + const auto& path = items[index]; + auto name = basename(path); + current_loading_name_ = name; + + auto bytes = ResourceHelper::loadFile(path); + if (bytes.empty()) { + std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n'; + return; + } + text_files_.push_back(TextFileResource{.name = name, .bytes = std::move(bytes)}); + ++loaded_count_; + std::cout << " [text ] " << name << '\n'; + } + +} // namespace Resource diff --git a/source/core/resources/resource_cache.hpp b/source/core/resources/resource_cache.hpp new file mode 100644 index 0000000..795b979 --- /dev/null +++ b/source/core/resources/resource_cache.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include +#include +#include + +#include "core/resources/resource_types.hpp" + +namespace Resource { + + // Cache singleton: precarga + decode dels assets llistats al + // `Resource::List`. Implementa carrega incremental amb pressupost + // de temps per frame (`loadStep`) per a poder mostrar una barra de + // progrés des de l'escena `BootLoader`. + class Cache { + public: + static void init(); + static void destroy(); + static auto get() -> Cache*; + + Cache(const Cache&) = delete; + auto operator=(const Cache&) -> Cache& = delete; + + // Getters: throw runtime_error si el nom no existeix al cache. + auto getMusic(const std::string& name) -> JA_Music_t*; + auto getSound(const std::string& name) -> JA_Sound_t*; + auto getSurfacePixels(const std::string& name) -> const std::vector&; + auto getPaletteBytes(const std::string& name) -> const std::vector&; + auto getTextFile(const std::string& name) -> const std::vector&; + + // Loader incremental. + void beginLoad(); + auto loadStep(int budget_ms) -> bool; // true → DONE + [[nodiscard]] auto isLoadDone() const -> bool { return stage_ == LoadStage::DONE; } + [[nodiscard]] auto getProgress() const -> float; // 0.0..1.0 + [[nodiscard]] auto getCurrentLoadingName() const -> const std::string& { return current_loading_name_; } + + private: + Cache() = default; + ~Cache() = default; + + enum class LoadStage { + MUSICS, + SOUNDS, + BITMAPS, + TEXT_FILES, + DONE, + }; + + void calculateTotal(); + void loadOneMusic(size_t index); + void loadOneSound(size_t index); + void loadOneBitmap(size_t index); + void loadOneTextFile(size_t index); + + std::vector musics_; + std::vector sounds_; + std::vector surfaces_; + std::vector text_files_; + + LoadStage stage_{LoadStage::DONE}; + size_t stage_index_{0}; + int total_count_{0}; + int loaded_count_{0}; + std::string current_loading_name_; + + static Cache* instance; + }; + +} // namespace Resource diff --git a/source/core/resources/resource_list.cpp b/source/core/resources/resource_list.cpp new file mode 100644 index 0000000..4932ec7 --- /dev/null +++ b/source/core/resources/resource_list.cpp @@ -0,0 +1,114 @@ +#include "core/resources/resource_list.hpp" + +#include +#include +#include + +#include "core/resources/resource_helper.hpp" +#include "external/fkyaml_node.hpp" + +namespace Resource { + + List* List::instance = nullptr; + + void List::init(const std::string& yaml_path) { + instance = new List(); + instance->loadFromYaml(yaml_path); + } + + void List::destroy() { + delete instance; + instance = nullptr; + } + + auto List::get() -> List* { return instance; } + + void List::loadFromYaml(const std::string& yaml_path) { + auto bytes = ResourceHelper::loadFile(yaml_path); + if (bytes.empty()) { + std::cout << "Resource::List: cannot load manifest " << yaml_path << '\n'; + return; + } + std::string content(bytes.begin(), bytes.end()); + loadFromString(content); + } + + void List::loadFromString(const std::string& yaml_content) { + try { + auto yaml = fkyaml::node::deserialize(yaml_content); + if (!yaml.contains("assets")) { + std::cout << "Resource::List: missing 'assets' root key\n"; + return; + } + const auto& assets = yaml["assets"]; + for (auto cat_it = assets.begin(); cat_it != assets.end(); ++cat_it) { + const auto& category_node = cat_it.value(); + if (!category_node.is_mapping()) { + continue; + } + for (auto type_it = category_node.begin(); type_it != category_node.end(); ++type_it) { + auto type_str = type_it.key().get_value(); + Type type = parseAssetType(type_str); + const auto& items = type_it.value(); + if (!items.is_sequence()) { + continue; + } + for (const auto& item : items) { + if (item.is_string()) { + addToMap(item.get_value(), type); + } + } + } + } + std::cout << "Resource::List: loaded " << file_list_.size() << " assets from manifest\n"; + } catch (const std::exception& e) { + std::cout << "Resource::List: YAML parse error: " << e.what() << '\n'; + } + } + + void List::addToMap(const std::string& path, Type type) { + auto key = basename(path); + if (file_list_.contains(key)) { + std::cout << "Resource::List: duplicate asset key '" << key << "', overwriting\n"; + } + file_list_.emplace(key, Item{path, type}); + } + + auto List::get(const std::string& filename) const -> std::string { + auto it = file_list_.find(filename); + if (it != file_list_.end()) { + return it->second.path; + } + return ""; + } + + auto List::getListByType(Type type) const -> std::vector { + std::vector list; + for (const auto& [filename, item] : file_list_) { + if (item.type == type) { + list.push_back(item.path); + } + } + std::ranges::sort(list); + return list; + } + + auto List::exists(const std::string& filename) const -> bool { + return file_list_.contains(filename); + } + + auto List::parseAssetType(const std::string& type_str) -> Type { + if (type_str == "DATA") return Type::DATA; + if (type_str == "BITMAP") return Type::BITMAP; + if (type_str == "MUSIC") return Type::MUSIC; + if (type_str == "SOUND") return Type::SOUND; + if (type_str == "FONT") return Type::FONT; + throw std::runtime_error("Unknown asset type: " + type_str); + } + + auto List::basename(const std::string& path) -> std::string { + auto pos = path.find_last_of("/\\"); + return pos == std::string::npos ? path : path.substr(pos + 1); + } + +} // namespace Resource diff --git a/source/core/resources/resource_list.hpp b/source/core/resources/resource_list.hpp new file mode 100644 index 0000000..db56ba7 --- /dev/null +++ b/source/core/resources/resource_list.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +namespace Resource { + + // Registre lleuger d'assets carregat des de `data/config/assets.yaml`. + // Map per a lookup O(1). Cache l'utilitza per a + // iterar per categoria a l'hora de carregar. + class List { + public: + enum class Type : int { + DATA, + BITMAP, + MUSIC, + SOUND, + FONT, + SIZE, + }; + + static void init(const std::string& yaml_path); + static void destroy(); + static auto get() -> List*; + + List(const List&) = delete; + auto operator=(const List&) -> List& = delete; + + [[nodiscard]] auto get(const std::string& filename) const -> std::string; + [[nodiscard]] auto getListByType(Type type) const -> std::vector; + [[nodiscard]] auto exists(const std::string& filename) const -> bool; + [[nodiscard]] auto totalCount() const -> int { return static_cast(file_list_.size()); } + + private: + struct Item { + std::string path; // ruta relativa al pack (ex: "music/menu.ogg") + Type type; + + Item(std::string p, Type t) + : path(std::move(p)), + type(t) {} + }; + + List() = default; + ~List() = default; + + void loadFromYaml(const std::string& yaml_path); + void loadFromString(const std::string& yaml_content); + void addToMap(const std::string& path, Type type); + + [[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; + [[nodiscard]] static auto basename(const std::string& path) -> std::string; + + std::unordered_map file_list_; + + static List* instance; + }; + +} // namespace Resource diff --git a/source/core/resources/resource_types.hpp b/source/core/resources/resource_types.hpp new file mode 100644 index 0000000..92727f9 --- /dev/null +++ b/source/core/resources/resource_types.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include +#include +#include +#include + +// Forward declarations to keep this header light. +struct JA_Music_t; +struct JA_Sound_t; + +void JA_DeleteMusic(JA_Music_t* music); +void JA_DeleteSound(JA_Sound_t* sound); + +namespace Resource { + + struct MusicDeleter { + void operator()(JA_Music_t* music) const noexcept { + if (music != nullptr) { + JA_DeleteMusic(music); + } + } + }; + + struct SoundDeleter { + void operator()(JA_Sound_t* sound) const noexcept { + if (sound != nullptr) { + JA_DeleteSound(sound); + } + } + }; + + struct MusicResource { + std::string name; + std::unique_ptr music; + }; + + struct SoundResource { + std::string name; + std::unique_ptr sound; + }; + + // Una entrada BITMAP descodifica un GIF i emmagatzema els seus + // 64000 bytes de píxels paletats + la paleta de 256 colors (768 + // bytes RGB). Així `getSurface(name)` i `getPalette(name)` comparteixen + // el mateix decode. + struct SurfaceResource { + std::string name; + std::vector pixels; // 64000 bytes (320 * 200) paletats + std::vector palette; // 768 bytes (256 * R G B) + }; + + // Per a fitxers de text generals (locale.yaml, keys.yaml, *.fnt). + struct TextFileResource { + std::string name; + std::vector bytes; + }; + +} // namespace Resource diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 5e614ab..1fa472f 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -3,12 +3,12 @@ #include #include +#include "core/audio/audio.hpp" #include "core/input/gamepad.hpp" #include "core/input/global_inputs.hpp" #include "core/input/key_config.hpp" #include "core/input/key_remap.hpp" #include "core/input/mouse.hpp" -#include "core/jail/jail_audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jgame.hpp" #include "core/jail/jinput.hpp" @@ -16,10 +16,12 @@ #include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" +#include "core/resources/resource_cache.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" #include "game/options.hpp" #include "scenes/banner_scene.hpp" +#include "scenes/boot_loader_scene.hpp" #include "scenes/credits_scene.hpp" #include "scenes/intro_new_logo_scene.hpp" #include "scenes/intro_scene.hpp" @@ -55,6 +57,13 @@ void Director::initGameContext() { } std::unique_ptr Director::createNextScene() { + // Mentre el Resource::Cache no haja acabat de precarregar, executem + // el BootLoaderScene — pinta una barra de progrés i avança la + // càrrega per pressupost de temps. Quan acaba, retorna i tornem ací + // amb el cache plenament disponible per a la resta d'escenes. + if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) { + return std::make_unique(); + } if (game_state_ == 0) { // Gameplay. ModuleGame és una scenes::Scene des de la Phase A. return std::make_unique(); @@ -118,9 +127,9 @@ auto Director::get() -> Director* { void Director::togglePause() { paused_ = !paused_; if (paused_) { - JA_PauseMusic(); + Audio::get()->pauseMusic(); } else { - JA_ResumeMusic(); + Audio::get()->resumeMusic(); } } @@ -142,8 +151,8 @@ bool Director::iterate() { // l'escena des d'una lambda del menú mentre encara s'està executant. if (restart_requested_) { restart_requested_ = false; - JA_StopMusic(); - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) JA_StopChannel(i); + Audio::get()->stopMusic(); + Audio::get()->stopAllSounds(); // Reinicialitza info::ctx des d'Options (vides, diners, diamants...) // en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort. initGameContext(); @@ -175,7 +184,7 @@ bool Director::iterate() { // Bombeig de l'àudio: reomple l'stream de música i para els canals // drenats. Substituïx el callback de SDL_AddTimer de la versió // antiga — imprescindible per al port a emscripten. - JA_Update(); + Audio::update(); // Dispara els crèdits cinematogràfics la primera vegada que el joc // arriba al menú del títol (info::ctx.num_piramide == 0). diff --git a/source/external/stb_vorbis.h b/source/external/stb_vorbis.c similarity index 100% rename from source/external/stb_vorbis.h rename to source/external/stb_vorbis.c diff --git a/source/game/modulegame.cpp b/source/game/modulegame.cpp index 5275053..5946944 100644 --- a/source/game/modulegame.cpp +++ b/source/game/modulegame.cpp @@ -1,10 +1,9 @@ #include "game/modulegame.hpp" -#include "core/jail/jail_audio.hpp" +#include "core/audio/audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jgame.hpp" #include "core/jail/jinput.hpp" -#include "core/resources/resource_helper.hpp" ModuleGame::ModuleGame() { this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"); @@ -42,18 +41,14 @@ void ModuleGame::onEnter() { // fade interpolarien cap a una paleta amb pantalla buida. this->Draw(); - const char* music = info::ctx.num_piramide == 3 ? "music/piramide_3.ogg" - : info::ctx.num_piramide == 2 ? "music/piramide_2.ogg" - : info::ctx.num_piramide == 6 ? "music/secreta.ogg" - : "music/piramide_1_4_5.ogg"; - const char* current_music = JA_GetMusicFilename(); - if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !current_music || - strcmp(music, current_music) != 0) { - auto buffer = ResourceHelper::loadFile(music); - JA_PlayMusic(JA_LoadMusic(buffer.data(), - static_cast(buffer.size()), - music)); - } + // Audio::playMusic ja és idempotent: si la pista actual coincideix amb la + // demanada, no fa res. Per això podem cridar-lo cada onEnter sense + // desencadenar restarts indesitjats. + const char* music_name = info::ctx.num_piramide == 3 ? "piramide_3.ogg" + : info::ctx.num_piramide == 2 ? "piramide_2.ogg" + : info::ctx.num_piramide == 6 ? "secreta.ogg" + : "piramide_1_4_5.ogg"; + Audio::get()->playMusic(music_name); // Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de // 32) per cada tick; durant aquesta fase el gameplay no corre, diff --git a/source/game/options.cpp b/source/game/options.cpp index da73363..f558d8b 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -4,7 +4,7 @@ #include #include -#include "core/jail/jail_audio.hpp" +#include "core/audio/audio.hpp" #include "external/fkyaml_node.hpp" #include "game/defaults.hpp" #include "game/defines.hpp" @@ -76,12 +76,16 @@ namespace Options { } } + // Delega tots els canvis de l'estat d'àudio al wrapper Audio. Es manté + // com a punt d'entrada únic per als callsites legacy del menú; el cos + // ja no toca jail_audio directament. void applyAudio() { - const float master = audio.enabled ? audio.volume : 0.0F; - JA_EnableMusic(audio.music_enabled); - JA_EnableSound(audio.sound_enabled); - JA_SetMusicVolume(master * audio.music_volume); - JA_SetSoundVolume(master * audio.sound_volume); + if (::Audio::get() == nullptr) return; + ::Audio::get()->enable(audio.enabled); + ::Audio::get()->enableMusic(audio.music.enabled); + ::Audio::get()->enableSound(audio.sound.enabled); + ::Audio::get()->setMusicVolume(audio.music.volume); + ::Audio::get()->setSoundVolume(audio.sound.volume); } // --- Funcions helper de càrrega --- @@ -99,17 +103,17 @@ namespace Options { if (node.contains("music")) { const auto& music = node["music"]; if (music.contains("enabled")) - audio.music_enabled = music["enabled"].get_value(); + audio.music.enabled = music["enabled"].get_value(); if (music.contains("volume")) - audio.music_volume = music["volume"].get_value(); + audio.music.volume = music["volume"].get_value(); } if (node.contains("sound")) { const auto& sound = node["sound"]; if (sound.contains("enabled")) - audio.sound_enabled = sound["enabled"].get_value(); + audio.sound.enabled = sound["enabled"].get_value(); if (sound.contains("volume")) - audio.sound_volume = sound["volume"].get_value(); + audio.sound.volume = sound["volume"].get_value(); } } @@ -352,11 +356,11 @@ namespace Options { file << " enabled: " << (audio.enabled ? "true" : "false") << "\n"; file << " volume: " << audio.volume << "\n"; file << " music:\n"; - file << " enabled: " << (audio.music_enabled ? "true" : "false") << "\n"; - file << " volume: " << audio.music_volume << "\n"; + file << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n"; + file << " volume: " << audio.music.volume << "\n"; file << " sound:\n"; - file << " enabled: " << (audio.sound_enabled ? "true" : "false") << "\n"; - file << " volume: " << audio.sound_volume << "\n"; + file << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n"; + file << " volume: " << audio.sound.volume << "\n"; file << "\n"; // GAME diff --git a/source/game/options.hpp b/source/game/options.hpp index 402dfc0..55e241e 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -59,13 +59,19 @@ namespace Options { Uint32 shadow_color{0xFF005A6B}; // Ombra daurada fosca (ABGR) }; - // Opcions d'àudio + // Opcions d'àudio (estructura compartida amb la resta de projectes) + struct Music { + bool enabled{Defaults::Audio::MUSIC_ENABLED}; + float volume{Defaults::Audio::MUSIC_VOLUME}; + }; + struct Sound { + bool enabled{Defaults::Audio::SOUND_ENABLED}; + float volume{Defaults::Audio::SOUND_VOLUME}; + }; struct Audio { + Music music{}; + Sound sound{}; bool enabled{Defaults::Audio::ENABLED}; // master enable - bool music_enabled{Defaults::Audio::MUSIC_ENABLED}; - float music_volume{Defaults::Audio::MUSIC_VOLUME}; - bool sound_enabled{Defaults::Audio::SOUND_ENABLED}; - float sound_volume{Defaults::Audio::SOUND_VOLUME}; float volume{Defaults::Audio::VOLUME}; }; diff --git a/source/main.cpp b/source/main.cpp index 53115cb..3ba31fb 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -11,8 +11,8 @@ #include #include +#include "core/audio/audio.hpp" #include "core/input/key_config.hpp" -#include "core/jail/jail_audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jfile.hpp" #include "core/jail/jgame.hpp" @@ -20,7 +20,9 @@ #include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" +#include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" +#include "core/resources/resource_list.hpp" #include "core/system/director.hpp" #include "game/options.hpp" @@ -90,10 +92,17 @@ SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) { JG_Init(); Screen::init(); JD8_Init(); - JA_Init(48000, SDL_AUDIO_S16, 2); - Options::applyAudio(); + Audio::init(); // crida internament JA_Init i aplica Options::audio Overlay::init(); Menu::init(); + + // Manifest d'assets (data/config/assets.yaml) + Cache. La precarga + // real es fa al BootLoaderScene, que el Director arrenca automàticament + // mentre `Resource::Cache::isLoadDone()` siga fals. + Resource::List::init("config/assets.yaml"); + Resource::Cache::init(); + Resource::Cache::get()->beginLoad(); + Director::init(); Director::get()->setup(); @@ -133,7 +142,9 @@ void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) { KeyConfig::destroy(); Menu::destroy(); Overlay::destroy(); - JA_Quit(); + Resource::Cache::destroy(); + Resource::List::destroy(); + Audio::destroy(); // el destructor del singleton crida JA_Quit JD8_Quit(); Screen::destroy(); JG_Finalize(); diff --git a/source/scenes/banner_scene.cpp b/source/scenes/banner_scene.cpp index 217020e..beac5a2 100644 --- a/source/scenes/banner_scene.cpp +++ b/source/scenes/banner_scene.cpp @@ -2,7 +2,7 @@ #include -#include "core/jail/jail_audio.hpp" +#include "core/audio/audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jinput.hpp" #include "game/info.hpp" @@ -52,7 +52,7 @@ namespace scenes { remaining_ms_ -= delta_ms; } if (remaining_ms_ <= 0) { - JA_FadeOutMusic(250); + Audio::get()->fadeOutMusic(250); fade_.startFadeOut(); phase_ = Phase::FadingOut; } diff --git a/source/scenes/boot_loader_scene.cpp b/source/scenes/boot_loader_scene.cpp new file mode 100644 index 0000000..6ba9795 --- /dev/null +++ b/source/scenes/boot_loader_scene.cpp @@ -0,0 +1,55 @@ +#include "scenes/boot_loader_scene.hpp" + +#include "core/jail/jdraw8.hpp" +#include "core/resources/resource_cache.hpp" + +namespace scenes { + + namespace { + constexpr int SCREEN_W = 320; + + constexpr Uint8 BG_COLOR = 0; // negre + constexpr Uint8 BAR_COLOR = 1; // blanc + + constexpr int BAR_X = 60; + constexpr int BAR_Y = 170; + constexpr int BAR_W = SCREEN_W - (BAR_X * 2); // 200 + constexpr int BAR_H = 6; + } // namespace + + BootLoaderScene::BootLoaderScene() = default; + + void BootLoaderScene::onEnter() { + // Inicialitza la paleta mínima per a la barra. La resta de + // colors queden a negre — després cada escena del joc carregarà + // la seua pròpia paleta. + JD8_SetPaletteColor(BG_COLOR, 0, 0, 0); + JD8_SetPaletteColor(BAR_COLOR, 63, 63, 63); + } + + void BootLoaderScene::tick(int /*delta_ms*/) { + if (Resource::Cache::get()->loadStep(8)) { + done_ = true; + } + render(); + } + + void BootLoaderScene::render() const { + JD8_ClearScreen(BG_COLOR); + + const float pct = Resource::Cache::get()->getProgress(); + const int filled = static_cast(static_cast(BAR_W) * pct); + + // Vora de la barra (línia 1 píxel a dalt i a baix). + JD8_FillRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, 1, BAR_COLOR); + JD8_FillRect(BAR_X - 1, BAR_Y + BAR_H, BAR_W + 2, 1, BAR_COLOR); + JD8_FillRect(BAR_X - 1, BAR_Y, 1, BAR_H, BAR_COLOR); + JD8_FillRect(BAR_X + BAR_W, BAR_Y, 1, BAR_H, BAR_COLOR); + + // Ompliment proporcional al progrés. + if (filled > 0) { + JD8_FillRect(BAR_X, BAR_Y, filled, BAR_H, BAR_COLOR); + } + } + +} // namespace scenes diff --git a/source/scenes/boot_loader_scene.hpp b/source/scenes/boot_loader_scene.hpp new file mode 100644 index 0000000..94a05a7 --- /dev/null +++ b/source/scenes/boot_loader_scene.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "scenes/scene.hpp" + +namespace scenes { + + // Escena de boot que conduix la càrrega incremental del Resource::Cache. + // tick() crida loadStep amb un pressupost de ~8ms i pinta una barra + // de progrés mentre dura. Quan el Cache marca isLoadDone, l'escena + // marca done() i el Director passa al següent state (intro = 255). + class BootLoaderScene : public Scene { + public: + BootLoaderScene(); + ~BootLoaderScene() override = default; + + void onEnter() override; + void tick(int delta_ms) override; + bool done() const override { return done_; } + int nextState() const override { return 1; } // 1 → SceneRegistry::tryCreate(num_piramide=255 → intro) + + private: + void render() const; + + bool done_{false}; + }; + +} // namespace scenes diff --git a/source/scenes/credits_scene.cpp b/source/scenes/credits_scene.cpp index ba90e67..10ff212 100644 --- a/source/scenes/credits_scene.cpp +++ b/source/scenes/credits_scene.cpp @@ -3,7 +3,7 @@ #include #include -#include "core/jail/jail_audio.hpp" +#include "core/audio/audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jinput.hpp" #include "game/info.hpp" @@ -46,7 +46,7 @@ namespace scenes { // amb piramide_inicial=8) no hi ha res que heretar, així que // arranquem la mateixa pista només si no sona res. Inocu en el // flux normal: JA_MUSIC_PLAYING fa que no la tornem a tocar. - if (JA_GetMusicState() != JA_MUSIC_PLAYING) { + if (Audio::getRealMusicState() != Audio::MusicState::PLAYING) { playMusic("music/final.ogg"); } diff --git a/source/scenes/scene_utils.cpp b/source/scenes/scene_utils.cpp index a4bd539..34a95aa 100644 --- a/source/scenes/scene_utils.cpp +++ b/source/scenes/scene_utils.cpp @@ -1,22 +1,22 @@ #include "scenes/scene_utils.hpp" -#include +#include -#include "core/jail/jail_audio.hpp" -#include "core/resources/resource_helper.hpp" +#include "core/audio/audio.hpp" namespace scenes { + namespace { + std::string basename(const char* path) { + std::string s = path; + auto pos = s.find_last_of("/\\"); + return pos == std::string::npos ? s : s.substr(pos + 1); + } + } // namespace + void playMusic(const char* filename, int loop) { if (!filename) return; - auto buffer = ResourceHelper::loadFile(filename); - if (buffer.empty()) return; - // JA_LoadMusic fa una còpia interna del OGG comprimit (via SDL_malloc) - // per a stb_vorbis. El `buffer` local es destruirà en sortir d'àmbit. - JA_PlayMusic(JA_LoadMusic(buffer.data(), - static_cast(buffer.size()), - filename), - loop); + Audio::get()->playMusic(basename(filename), loop); } } // namespace scenes diff --git a/source/scenes/secreta_scene.cpp b/source/scenes/secreta_scene.cpp index 6cd248d..935e44f 100644 --- a/source/scenes/secreta_scene.cpp +++ b/source/scenes/secreta_scene.cpp @@ -4,7 +4,7 @@ #include #include -#include "core/jail/jail_audio.hpp" +#include "core/audio/audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jinput.hpp" #include "game/info.hpp" @@ -76,7 +76,7 @@ namespace scenes { } void SecretaScene::beginFinalFade() { - JA_FadeOutMusic(250); + Audio::get()->fadeOutMusic(250); fade_.startFadeOut(); phase_ = Phase::FinalFadeOut; } diff --git a/source/scenes/slides_scene.cpp b/source/scenes/slides_scene.cpp index 5cc922d..00c6895 100644 --- a/source/scenes/slides_scene.cpp +++ b/source/scenes/slides_scene.cpp @@ -4,7 +4,7 @@ #include #include -#include "core/jail/jail_audio.hpp" +#include "core/audio/audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jinput.hpp" #include "game/info.hpp" @@ -93,7 +93,7 @@ namespace scenes { void SlidesScene::beginFinalFade() { if (num_piramide_at_start_ != 7) { - JA_FadeOutMusic(250); + Audio::get()->fadeOutMusic(250); } fade_.startFadeOut(); phase_ = Phase::FadeFinal; @@ -105,7 +105,7 @@ namespace scenes { // el final natural crida JA_FadeOutMusic (beginFinalFade() distingeix). if (!skip_triggered_ && JI_AnyKey()) { skip_triggered_ = true; - if (num_piramide_at_start_ != 7) JA_FadeOutMusic(250); + if (num_piramide_at_start_ != 7) Audio::get()->fadeOutMusic(250); fade_.startFadeOut(); phase_ = Phase::FadeFinal; }