diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b2bdba..2be98d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ set(APP_SOURCES source/game/gameplay/tilemap_renderer.cpp # Game - Scenes + source/game/scenes/boot_loader.cpp source/game/scenes/credits.cpp source/game/scenes/ending.cpp source/game/scenes/ending2.cpp diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp index 3ef686c..44e5bb7 100644 --- a/source/core/audio/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -43,12 +43,16 @@ struct JA_Channel_t { struct JA_Music_t { SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; - Uint32 length{0}; - Uint8* buffer{nullptr}; + + // OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una + // sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming. + Uint8* ogg_data{nullptr}; + Uint32 ogg_length{0}; + stb_vorbis* vorbis{nullptr}; // Handle del decoder, viu tot el cicle del JA_Music_t + char* filename{nullptr}; - int pos{0}; - int times{0}; + int times{0}; // Loops restants (-1 = infinit, 0 = un sol play) SDL_AudioStream* stream{nullptr}; JA_Music_state state{JA_MUSIC_INVALID}; }; @@ -76,6 +80,57 @@ 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); +// --- 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; + +// 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; + + short chunk[JA_MUSIC_CHUNK_SHORTS]; + const int channels = music->spec.channels; + const int samples_per_channel = stb_vorbis_get_samples_short_interleaved( + music->vorbis, + 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; + SDL_PutAudioStreamData(music->stream, chunk, bytes); + return samples_per_channel; +} + +// 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_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)); + + while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) { + const int decoded = JA_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; + } + } +} + // --- Core Functions --- inline void JA_Update() { @@ -93,13 +148,11 @@ inline void JA_Update() { } } - if (current_music->times != 0) { - if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) { - SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length); - } - if (current_music->times > 0) current_music->times--; - } else { - if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); + // 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); + if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { + JA_StopMusic(); } } @@ -139,19 +192,31 @@ inline void JA_Quit() { // --- Music Functions --- inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { - JA_Music_t* music = new JA_Music_t(); + if (!buffer || length == 0) return nullptr; - int chan, samplerate; - short* output; - music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2; + // Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta + // memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres. + Uint8* ogg_copy = static_cast(SDL_malloc(length)); + if (!ogg_copy) return nullptr; + SDL_memcpy(ogg_copy, buffer, length); - music->spec.channels = chan; - music->spec.freq = samplerate; + int error = 0; + stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast(length), &error, nullptr); + if (!vorbis) { + SDL_free(ogg_copy); + SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error); + return nullptr; + } + + auto* music = new JA_Music_t(); + music->ogg_data = ogg_copy; + music->ogg_length = length; + music->vorbis = vorbis; + + const stb_vorbis_info info = stb_vorbis_get_info(vorbis); + music->spec.channels = info.channels; + music->spec.freq = static_cast(info.sample_rate); music->spec.format = SDL_AUDIO_S16; - music->buffer = static_cast(SDL_malloc(music->length)); - SDL_memcpy(music->buffer, output, music->length); - free(output); - music->pos = 0; music->state = JA_MUSIC_STOPPED; return music; @@ -190,23 +255,29 @@ inline JA_Music_t* JA_LoadMusic(const char* filename) { } inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { - if (!JA_musicEnabled || !music) return; // Añadida comprobación de music + if (!JA_musicEnabled || !music || !music->vorbis) return; JA_StopMusic(); current_music = music; - current_music->pos = 0; 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) { // Comprobar creación de stream + if (!current_music->stream) { SDL_Log("Failed to create audio stream!"); current_music->state = JA_MUSIC_STOPPED; return; } - if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n"); SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); + + // 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"); } @@ -235,13 +306,17 @@ inline void JA_ResumeMusic() { inline void JA_StopMusic() { if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return; - current_music->pos = 0; current_music->state = JA_MUSIC_STOPPED; if (current_music->stream) { SDL_DestroyAudioStream(current_music->stream); current_music->stream = nullptr; } - // No liberamos filename aquí, se debería liberar en JA_DeleteMusic + // 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) { + stb_vorbis_seek_start(current_music->vorbis); + } + // No liberem filename aquí; es fa en JA_DeleteMusic. } inline void JA_FadeOutMusic(const int milliseconds) { @@ -267,9 +342,10 @@ inline void JA_DeleteMusic(JA_Music_t* music) { JA_StopMusic(); current_music = nullptr; } - SDL_free(music->buffer); if (music->stream) SDL_DestroyAudioStream(music->stream); - free(music->filename); // filename se libera aquí + if (music->vorbis) stb_vorbis_close(music->vorbis); + SDL_free(music->ogg_data); + free(music->filename); // filename es libera aquí delete music; } @@ -281,17 +357,14 @@ inline float JA_SetMusicVolume(float volume) { return JA_musicVolume; } -inline void JA_SetMusicPosition(float value) { - if (!current_music) return; - current_music->pos = value * current_music->spec.freq; - // Nota: Esta implementación de 'pos' no parece usarse en JA_Update para - // el streaming. El streaming siempre parece empezar desde el principio. +inline void JA_SetMusicPosition(float /*value*/) { + // No implementat amb el backend de streaming. Mai va arribar a usar-se + // en el codi existent, així que es manté com a stub. } inline float JA_GetMusicPosition() { - if (!current_music) return 0; - return float(current_music->pos) / float(current_music->spec.freq); - // Nota: Ver `JA_SetMusicPosition` + // Veure nota a JA_SetMusicPosition. + return 0.0f; } inline void JA_EnableMusic(const bool value) { diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp index 488da17..cc3437f 100644 --- a/source/core/resources/resource_cache.cpp +++ b/source/core/resources/resource_cache.cpp @@ -2,10 +2,6 @@ #include -#ifdef __EMSCRIPTEN__ -#include // Para emscripten_sleep -#endif - #include // Para find_if #include // Para exit, size_t #include // Para ifstream, istreambuf_iterator @@ -42,10 +38,10 @@ namespace Resource { // [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él auto Cache::get() -> Cache* { return Cache::cache; } - // Constructor + // Constructor — no dispara la carga. Director llama a beginLoad() + loadStep() + // desde iterate() para que el bucle SDL3 esté vivo durante la carga. Cache::Cache() : loading_text_(Screen::get()->getText()) { - load(); } // Vacia todos los vectores de recursos @@ -57,12 +53,11 @@ namespace Resource { text_files_.clear(); texts_.clear(); animations_.clear(); + rooms_.clear(); } - // Carga todos los recursos + // Carga todos los recursos de golpe (usado solo por reload() en hot-reload de debug) void Cache::load() { - // Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga, - // por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init(). calculateTotal(); Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); std::cout << "\n** LOADING RESOURCES" << '\n'; @@ -77,7 +72,162 @@ namespace Resource { std::cout << "\n** RESOURCES LOADED" << '\n'; } - // Recarga todos los recursos + // Prepara el loader incremental. Director lo llama una vez tras Cache::init(). + void Cache::beginLoad() { + calculateTotal(); + Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); + std::cout << "\n** LOADING RESOURCES (incremental)" << '\n'; + stage_ = LoadStage::SOUNDS; + stage_index_ = 0; + } + + auto Cache::isLoadDone() const -> bool { + return stage_ == LoadStage::DONE; + } + + // Carga assets hasta agotar el presupuesto de tiempo o completar todas las etapas. + // Devuelve true cuando ya no queda nada por cargar. + 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 listOf = [](List::Type t) { return List::get()->getListByType(t); }; + + while (stage_ != LoadStage::DONE) { + switch (stage_) { + case LoadStage::SOUNDS: { + auto list = listOf(List::Type::SOUND); + if (stage_index_ == 0) { + std::cout << "\n>> SOUND FILES" << '\n'; + sounds_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::MUSICS; + stage_index_ = 0; + break; + } + loadOneSound(stage_index_++); + break; + } + case LoadStage::MUSICS: { + auto list = listOf(List::Type::MUSIC); + if (stage_index_ == 0) { + std::cout << "\n>> MUSIC FILES" << '\n'; + musics_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::SURFACES; + stage_index_ = 0; + break; + } + loadOneMusic(stage_index_++); + break; + } + case LoadStage::SURFACES: { + auto list = listOf(List::Type::BITMAP); + if (stage_index_ == 0) { + std::cout << "\n>> SURFACES" << '\n'; + surfaces_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::SURFACES_POST; + stage_index_ = 0; + break; + } + loadOneSurface(stage_index_++); + break; + } + case LoadStage::SURFACES_POST: { + finalizeSurfaces(); + stage_ = LoadStage::PALETTES; + stage_index_ = 0; + break; + } + case LoadStage::PALETTES: { + auto list = listOf(List::Type::PALETTE); + if (stage_index_ == 0) { + std::cout << "\n>> PALETTES" << '\n'; + palettes_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::TEXT_FILES; + stage_index_ = 0; + break; + } + loadOnePalette(stage_index_++); + break; + } + case LoadStage::TEXT_FILES: { + auto list = listOf(List::Type::FONT); + if (stage_index_ == 0) { + std::cout << "\n>> TEXT FILES" << '\n'; + text_files_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::ANIMATIONS; + stage_index_ = 0; + break; + } + loadOneTextFile(stage_index_++); + break; + } + case LoadStage::ANIMATIONS: { + auto list = listOf(List::Type::ANIMATION); + if (stage_index_ == 0) { + std::cout << "\n>> ANIMATIONS" << '\n'; + animations_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::ROOMS; + stage_index_ = 0; + break; + } + loadOneAnimation(stage_index_++); + break; + } + case LoadStage::ROOMS: { + auto list = listOf(List::Type::ROOM); + if (stage_index_ == 0) { + std::cout << "\n>> ROOMS" << '\n'; + rooms_.clear(); + } + if (stage_index_ >= list.size()) { + stage_ = LoadStage::TEXTS; + stage_index_ = 0; + break; + } + loadOneRoom(stage_index_++); + break; + } + case LoadStage::TEXTS: { + // createText itera sobre una lista fija de 5 fuentes + constexpr size_t TEXT_COUNT = 5; + if (stage_index_ == 0) { + std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n'; + texts_.clear(); + } + if (stage_index_ >= TEXT_COUNT) { + stage_ = LoadStage::DONE; + stage_index_ = 0; + std::cout << "\n** RESOURCES LOADED" << '\n'; + break; + } + createOneText(stage_index_++); + break; + } + case LoadStage::DONE: + break; + } + + if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break; + } + + return stage_ == LoadStage::DONE; + } + + // Recarga todos los recursos (síncrono, solo para hot-reload de debug) void Cache::reload() { clear(); load(); @@ -221,96 +371,96 @@ namespace Resource { throw; } - // Carga los sonidos - void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static) - std::cout << "\n>> SOUND FILES" << '\n'; + // Lista fija de text objects. Compartida entre createText() y createOneText(i). + namespace { + struct TextObjectInfo { + std::string key; // Identificador del recurso + std::string texture_file; // Nombre del archivo de textura + std::string text_file; // Nombre del archivo de texto + }; + + const std::vector& getTextObjectInfos() { + static const std::vector info = { + {.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"}, + {.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"}, + {.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"}, + {.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"}, + {.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}}; + return info; + } + } // namespace + + // --- Helpers incrementales (un asset por llamada) --- + + void Cache::loadOneSound(size_t index) { auto list = List::get()->getListByType(List::Type::SOUND); - sounds_.clear(); + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + JA_Sound_t* sound = nullptr; - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - JA_Sound_t* sound = nullptr; - - // Try loading from resource pack first - auto audio_data = Helper::loadFile(l); - if (!audio_data.empty()) { - sound = JA_LoadSound(audio_data.data(), static_cast(audio_data.size())); - } - - // Fallback to file path if memory loading failed - if (sound == nullptr) { - sound = JA_LoadSound(l.c_str()); - } - - if (sound == nullptr) { - throw std::runtime_error("Failed to decode audio file"); - } - - sounds_.emplace_back(SoundResource{.name = name, .sound = sound}); - printWithDots("Sound : ", name, "[ LOADED ]"); - updateLoadingProgress(); - } catch (const std::exception& e) { - throwLoadError("SOUND", l, e); + auto audio_data = Helper::loadFile(l); + if (!audio_data.empty()) { + sound = JA_LoadSound(audio_data.data(), static_cast(audio_data.size())); } + if (sound == nullptr) { + sound = JA_LoadSound(l.c_str()); + } + if (sound == nullptr) { + throw std::runtime_error("Failed to decode audio file"); + } + + sounds_.emplace_back(SoundResource{.name = name, .sound = sound}); + printWithDots("Sound : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("SOUND", l, e); } } - // Carga las musicas - void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static) - std::cout << "\n>> MUSIC FILES" << '\n'; + void Cache::loadOneMusic(size_t index) { auto list = List::get()->getListByType(List::Type::MUSIC); - musics_.clear(); + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + JA_Music_t* music = nullptr; - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - JA_Music_t* music = nullptr; - - // Try loading from resource pack first - auto audio_data = Helper::loadFile(l); - if (!audio_data.empty()) { - music = JA_LoadMusic(audio_data.data(), static_cast(audio_data.size())); - } - - // Fallback to file path if memory loading failed - if (music == nullptr) { - music = JA_LoadMusic(l.c_str()); - } - - if (music == nullptr) { - throw std::runtime_error("Failed to decode music file"); - } - - musics_.emplace_back(MusicResource{.name = name, .music = music}); - printWithDots("Music : ", name, "[ LOADED ]"); - updateLoadingProgress(1); - } catch (const std::exception& e) { - throwLoadError("MUSIC", l, e); + auto audio_data = Helper::loadFile(l); + if (!audio_data.empty()) { + music = JA_LoadMusic(audio_data.data(), static_cast(audio_data.size())); } + if (music == nullptr) { + music = JA_LoadMusic(l.c_str()); + } + if (music == nullptr) { + throw std::runtime_error("Failed to decode music file"); + } + + musics_.emplace_back(MusicResource{.name = name, .music = music}); + printWithDots("Music : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("MUSIC", l, e); } } - // Carga las texturas - void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static) - std::cout << "\n>> SURFACES" << '\n'; + void Cache::loadOneSurface(size_t index) { auto list = List::get()->getListByType(List::Type::BITMAP); - surfaces_.clear(); - - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared(l)}); - surfaces_.back().surface->setTransparentColor(0); - updateLoadingProgress(); - } catch (const std::exception& e) { - throwLoadError("BITMAP", l, e); - } + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared(l)}); + surfaces_.back().surface->setTransparentColor(0); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("BITMAP", l, e); } + } + void Cache::finalizeSurfaces() { // Reconfigura el color transparente de algunas surfaces getSurface("loading_screen_color.gif")->setTransparentColor(); getSurface("ending1.gif")->setTransparentColor(); @@ -321,108 +471,132 @@ namespace Resource { getSurface("standard.gif")->setTransparentColor(16); } - // Carga las paletas - void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static) + void Cache::loadOnePalette(size_t index) { + auto list = List::get()->getListByType(List::Type::PALETTE); + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)}); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("PALETTE", l, e); + } + } + + void Cache::loadOneTextFile(size_t index) { + auto list = List::get()->getListByType(List::Type::FONT); + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)}); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("FONT", l, e); + } + } + + void Cache::loadOneAnimation(size_t index) { + auto list = List::get()->getListByType(List::Type::ANIMATION); + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + + auto yaml_bytes = Helper::loadFile(l); + if (yaml_bytes.empty()) { + throw std::runtime_error("File is empty or could not be loaded"); + } + + animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes}); + printWithDots("Animation : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("ANIMATION", l, e); + } + } + + void Cache::loadOneRoom(size_t index) { + auto list = List::get()->getListByType(List::Type::ROOM); + const auto& l = list[index]; + try { + auto name = getFileName(l); + setCurrentLoading(name); + rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared(Room::loadYAML(l))}); + printWithDots("Room : ", name, "[ LOADED ]"); + updateLoadingProgress(); + } catch (const std::exception& e) { + throwLoadError("ROOM", l, e); + } + } + + void Cache::createOneText(size_t index) { + const auto& infos = getTextObjectInfos(); + const auto& res_info = infos[index]; + texts_.emplace_back(TextResource{ + .name = res_info.key, + .text = std::make_shared(getSurface(res_info.texture_file), getTextFile(res_info.text_file))}); + printWithDots("Text : ", res_info.key, "[ DONE ]"); + } + + // --- Bucles completos (solo usados por reload() síncrono) --- + + void Cache::loadSounds() { + std::cout << "\n>> SOUND FILES" << '\n'; + auto list = List::get()->getListByType(List::Type::SOUND); + sounds_.clear(); + for (size_t i = 0; i < list.size(); ++i) loadOneSound(i); + } + + void Cache::loadMusics() { + std::cout << "\n>> MUSIC FILES" << '\n'; + auto list = List::get()->getListByType(List::Type::MUSIC); + musics_.clear(); + for (size_t i = 0; i < list.size(); ++i) loadOneMusic(i); + } + + void Cache::loadSurfaces() { + std::cout << "\n>> SURFACES" << '\n'; + auto list = List::get()->getListByType(List::Type::BITMAP); + surfaces_.clear(); + for (size_t i = 0; i < list.size(); ++i) loadOneSurface(i); + finalizeSurfaces(); + } + + void Cache::loadPalettes() { std::cout << "\n>> PALETTES" << '\n'; auto list = List::get()->getListByType(List::Type::PALETTE); palettes_.clear(); - - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)}); - updateLoadingProgress(); - } catch (const std::exception& e) { - throwLoadError("PALETTE", l, e); - } - } + for (size_t i = 0; i < list.size(); ++i) loadOnePalette(i); } - // Carga los ficheros de texto - void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static) + void Cache::loadTextFiles() { std::cout << "\n>> TEXT FILES" << '\n'; auto list = List::get()->getListByType(List::Type::FONT); text_files_.clear(); - - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)}); - updateLoadingProgress(); - } catch (const std::exception& e) { - throwLoadError("FONT", l, e); - } - } + for (size_t i = 0; i < list.size(); ++i) loadOneTextFile(i); } - // Carga las animaciones - void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static) + void Cache::loadAnimations() { std::cout << "\n>> ANIMATIONS" << '\n'; auto list = List::get()->getListByType(List::Type::ANIMATION); animations_.clear(); - - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - - // Cargar bytes del archivo YAML sin parsear (carga lazy) - auto yaml_bytes = Helper::loadFile(l); - - if (yaml_bytes.empty()) { - throw std::runtime_error("File is empty or could not be loaded"); - } - - animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes}); - printWithDots("Animation : ", name, "[ LOADED ]"); - updateLoadingProgress(); - } catch (const std::exception& e) { - throwLoadError("ANIMATION", l, e); - } - } + for (size_t i = 0; i < list.size(); ++i) loadOneAnimation(i); } - // Carga las habitaciones desde archivos YAML - void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static) + void Cache::loadRooms() { std::cout << "\n>> ROOMS" << '\n'; auto list = List::get()->getListByType(List::Type::ROOM); rooms_.clear(); - - for (const auto& l : list) { - try { - auto name = getFileName(l); - setCurrentLoading(name); - rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared(Room::loadYAML(l))}); - printWithDots("Room : ", name, "[ LOADED ]"); - updateLoadingProgress(); - } catch (const std::exception& e) { - throwLoadError("ROOM", l, e); - } - } + for (size_t i = 0; i < list.size(); ++i) loadOneRoom(i); } - void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static) - struct ResourceInfo { - std::string key; // Identificador del recurso - std::string texture_file; // Nombre del archivo de textura - std::string text_file; // Nombre del archivo de texto - }; - + void Cache::createText() { std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n'; - - std::vector resources = { - {.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"}, - {.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"}, - {.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"}, - {.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"}, - {.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}}; - - for (const auto& res_info : resources) { - texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared(getSurface(res_info.texture_file), getTextFile(res_info.text_file))}); - printWithDots("Text : ", res_info.key, "[ DONE ]"); - } + texts_.clear(); + const auto& infos = getTextObjectInfos(); + for (size_t i = 0; i < infos.size(); ++i) createOneText(i); } // Vacía el vector de sonidos @@ -512,7 +686,6 @@ namespace Resource { SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT}; surface->fillRect(&rect_full, BAR_COLOR); -#if defined(__EMSCRIPTEN__) || defined(_DEBUG) // Mostra el nom del recurs que està a punt de carregar-se, centrat sobre la barra if (!current_loading_name_.empty()) { const float TEXT_Y = BAR_POSITION - static_cast(TEXT_HEIGHT) - 2.0F; @@ -522,51 +695,19 @@ namespace Resource { current_loading_name_, LOADING_TEXT_COLOR); } -#endif Screen::get()->render(); } - // Desa el nom del recurs que s'està a punt de carregar i repinta immediatament. - // A wasm/debug serveix per veure exactament en quin fitxer es penja la càrrega. + // Guarda el nombre del recurso que se está a punto de cargar. El repintado + // lo hace el BootLoader (una vez por frame) — aquí solo se actualiza el estado. void Cache::setCurrentLoading(const std::string& name) { current_loading_name_ = name; -#if defined(__EMSCRIPTEN__) || defined(_DEBUG) - renderProgress(); - checkEvents(); -#endif -#ifdef __EMSCRIPTEN__ - // Cedeix el control al navegador perquè pinte el canvas i processe - // events. Sense això, el thread principal queda bloquejat durant tota - // la precàrrega i el jugador només veu pantalla negra. - emscripten_sleep(0); -#endif } - // Comprueba los eventos de la pantalla de carga - void Cache::checkEvents() { - SDL_Event event; - while (SDL_PollEvent(&event)) { - switch (event.type) { - case SDL_EVENT_QUIT: - exit(0); - break; - case SDL_EVENT_KEY_DOWN: - if (event.key.key == SDLK_ESCAPE) { - exit(0); - } - break; - } - } - } - - // Actualiza el progreso de carga - void Cache::updateLoadingProgress(int steps) { + // Incrementa el contador de recursos cargados + void Cache::updateLoadingProgress() { count_.add(1); - if (count_.loaded % steps == 0 || count_.loaded == count_.total) { - renderProgress(); - } - checkEvents(); } } // namespace Resource diff --git a/source/core/resources/resource_cache.hpp b/source/core/resources/resource_cache.hpp index 80ee1a7..4a9a212 100644 --- a/source/core/resources/resource_cache.hpp +++ b/source/core/resources/resource_cache.hpp @@ -25,7 +25,13 @@ namespace Resource { auto getRoom(const std::string& name) -> std::shared_ptr; auto getRooms() -> std::vector&; - void reload(); // Recarga todos los recursos + // --- Incremental loading (Director drives this from iterate()) --- + void beginLoad(); // Prepara el estado del loader incremental + auto loadStep(int budget_ms) -> bool; // Carga assets durante budget_ms; devuelve true si ha terminado + void renderProgress(); // Dibuja la barra de progreso (usada por BootLoader) + [[nodiscard]] auto isLoadDone() const -> bool; + + void reload(); // Recarga todos los recursos (síncrono, usado en hot-reload de debug) #ifdef _DEBUG void reloadRoom(const std::string& name); // Recarga una habitación desde disco #endif @@ -47,7 +53,21 @@ namespace Resource { } }; - // Métodos de carga de recursos + // Etapas del loader incremental + enum class LoadStage { + SOUNDS, + MUSICS, + SURFACES, + SURFACES_POST, // Ajuste de transparent colors tras cargar todas las surfaces + PALETTES, + TEXT_FILES, + ANIMATIONS, + ROOMS, + TEXTS, + DONE + }; + + // Métodos de carga de recursos (bucle completo, usados por reload() síncrono) void loadSounds(); void loadMusics(); void loadSurfaces(); @@ -57,18 +77,27 @@ namespace Resource { void loadRooms(); void createText(); + // Helpers incrementales: cargan un único asset de la categoría correspondiente + void loadOneSound(size_t index); + void loadOneMusic(size_t index); + void loadOneSurface(size_t index); + void finalizeSurfaces(); // Ajuste de transparent colors tras cargar surfaces + void loadOnePalette(size_t index); + void loadOneTextFile(size_t index); + void loadOneAnimation(size_t index); + void loadOneRoom(size_t index); + void createOneText(size_t index); + // Métodos de limpieza void clear(); void clearSounds(); void clearMusics(); // Métodos de gestión de carga - void load(); + void load(); // Carga completa síncrona (usado solo por reload()) void calculateTotal(); - void renderProgress(); - static void checkEvents(); - void updateLoadingProgress(int steps = 5); - void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs i repinta (wasm/debug) + void updateLoadingProgress(); + void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs // Helper para mensajes de error de carga [[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e); @@ -92,7 +121,11 @@ namespace Resource { ResourceCount count_{}; // Contador de recursos std::shared_ptr loading_text_; // Texto para la pantalla de carga - std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar (debug/wasm) + std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar + + // Estado del loader incremental + LoadStage stage_{LoadStage::DONE}; // Arranca en DONE hasta que beginLoad() lo cambie + size_t stage_index_{0}; // Cursor dentro de la categoría actual }; } // namespace Resource diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 8b8081c..2d701fa 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -23,6 +23,7 @@ #include "game/gameplay/cheevos.hpp" // Para Cheevos #include "game/options.hpp" // Para Options, options, OptionsVideo #include "game/scene_manager.hpp" // Para SceneManager +#include "game/scenes/boot_loader.hpp" // Para BootLoader #include "game/scenes/credits.hpp" // Para Credits #include "game/scenes/ending.hpp" // Para Ending #include "game/scenes/ending2.hpp" // Para Ending2 @@ -177,12 +178,12 @@ Director::Director() { // Crea los objetos Screen::init(); - // Initialize resources (works for both release and development) + // Inicializa el singleton del cache sin disparar la carga. La carga real + // la hace Director::iterate() llamando a Cache::loadStep() en cada frame, + // de forma que la ventana, los eventos y la barra de progreso están vivos + // desde el primer tick. Resource::Cache::init(); - Notifier::init("", "8bithud"); - RenderInfo::init(); - Console::init("8bithud"); - Screen::get()->setNotificationsEnabled(true); + Resource::Cache::get()->beginLoad(); // Special handling for gamecontrollerdb.txt - SDL needs filesystem path #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) @@ -198,6 +199,22 @@ Director::Director() { Input::get()->applyKeyboardBindingsFromOptions(); Input::get()->applyGamepadBindingsFromOptions(); + std::cout << "\n"; // Fin de inicialización mínima de sistemas + + // Construeix l'escena inicial (BootLoader). finishBoot() la canviarà a + // LOGO (o la que digui Debug) quan Cache::loadStep() complete la càrrega. + SceneManager::current = SceneManager::Scene::BOOT_LOADER; + switchToActiveScene(); +} + +// Inicialitzacions que depenen del cache poblat. Es crida des d'iterate() +// just quan Cache::loadStep() retorna true, amb la finestra i el bucle ja vius. +void Director::finishBoot() { + Notifier::init("", "8bithud"); + RenderInfo::init(); + Console::init("8bithud"); + Screen::get()->setNotificationsEnabled(true); + #ifdef _DEBUG Debug::init(); #ifdef __EMSCRIPTEN__ @@ -210,10 +227,11 @@ Director::Director() { SceneManager::current = Debug::get()->getInitialScene(); #endif MapEditor::init(); +#else + // En release, pasamos a LOGO siempre tras la carga + SceneManager::current = SceneManager::Scene::LOGO; #endif - std::cout << "\n"; // Fin de inicialización de sistemas - // Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos) #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) { @@ -234,15 +252,17 @@ Director::Director() { #else Cheevos::init(Resource::List::get()->get("cheevos.bin")); #endif - - // Construeix la primera escena (LOGO per defecte, o la que digui Debug) - switchToActiveScene(); } Director::~Director() { // Guarda las opciones a un fichero Options::saveToFile(); + // Destruir l'escena activa ABANS dels singletons. Si no, el unique_ptr membre + // destrueix l'escena al final del destructor — quan Audio, Screen, Resource... + // ja són morts — i qualsevol accés en els seus destructors és un UAF. + active_scene_.reset(); + // Destruye los singletones Cheevos::destroy(); Locale::destroy(); @@ -356,6 +376,10 @@ void Director::switchToActiveScene() { active_scene_.reset(); switch (SceneManager::current) { + case SceneManager::Scene::BOOT_LOADER: + active_scene_ = std::make_unique(); + break; + case SceneManager::Scene::LOGO: active_scene_ = std::make_unique(); break; @@ -406,6 +430,26 @@ auto Director::iterate() -> SDL_AppResult { return SDL_APP_SUCCESS; } + // Fase de boot: anem cridant loadStep() fins que el cache estiga ple. + // Durant aquesta fase l'escena activa és BootLoader (una barra de progrés). + if (boot_loading_) { + try { + // Budget de 50ms: durant el boot el joc va a ~15-20 FPS, suficient + // per veure la barra avançar suau i processar events del WM/ESC, + // i evita el 50% d'ineficiència que provocaria un budget < vsync. + if (Resource::Cache::get()->loadStep(50 /*ms*/)) { + finishBoot(); + boot_loading_ = false; + // finishBoot() ja ha fixat SceneManager::current a LOGO (o la que + // digui Debug). El canvi d'escena es fa just a sota. + } + } catch (const std::exception& e) { + std::cerr << "Fatal error during resource load: " << e.what() << '\n'; + SceneManager::current = SceneManager::Scene::QUIT; + return SDL_APP_FAILURE; + } + } + // Si l'escena ha canviat (o s'ha demanat RESTART_CURRENT), canviar-la abans del frame if (SceneManager::current != current_scene_ || SceneManager::current == SceneManager::Scene::RESTART_CURRENT) { switchToActiveScene(); diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index efb939c..3e95819 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -22,11 +22,13 @@ class Director { std::string executable_path_; // Path del ejecutable std::string system_folder_; // Carpeta del sistema donde guardar datos - std::unique_ptr active_scene_; // Escena activa - SceneManager::Scene current_scene_{SceneManager::Scene::LOGO}; // Tipus d'escena activa + std::unique_ptr active_scene_; // Escena activa + SceneManager::Scene current_scene_{SceneManager::Scene::BOOT_LOADER}; // Tipus d'escena activa + bool boot_loading_{true}; // True mientras Cache::loadStep() no haya acabado // --- Funciones --- void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos void setFileList(); // Carga la configuración de assets desde assets.yaml void switchToActiveScene(); // Construeix l'escena segons SceneManager::current + void finishBoot(); // Inits que dependen del cache, ejecutado tras loadStep==done }; diff --git a/source/game/scene_manager.hpp b/source/game/scene_manager.hpp index 89a2f35..81a98ec 100644 --- a/source/game/scene_manager.hpp +++ b/source/game/scene_manager.hpp @@ -11,6 +11,7 @@ namespace SceneManager { // --- Escenas del programa --- enum class Scene { + BOOT_LOADER, // Carga inicial de recursos dirigida por iterate() LOGO, // Pantalla del logo LOADING_SCREEN, // Pantalla de carga TITLE, // Pantalla de título/menú principal @@ -34,7 +35,7 @@ namespace SceneManager { }; // --- Variables de estado globales --- - inline Scene current = Scene::LOGO; // Escena actual (en _DEBUG sobrescrito por Director tras cargar debug.yaml) + inline Scene current = Scene::BOOT_LOADER; // Arranca siempre cargando recursos; Director conmuta a LOGO al terminar inline Options options = Options::LOGO_TO_LOADING_SCREEN; // Opciones de la escena actual inline Scene scene_before_restart = Scene::LOGO; // escena a relanzar tras RESTART_CURRENT diff --git a/source/game/scenes/boot_loader.cpp b/source/game/scenes/boot_loader.cpp new file mode 100644 index 0000000..69999eb --- /dev/null +++ b/source/game/scenes/boot_loader.cpp @@ -0,0 +1,14 @@ +#include "game/scenes/boot_loader.hpp" + +#include "core/resources/resource_cache.hpp" +#include "game/scene_manager.hpp" + +void BootLoader::iterate() { + Resource::Cache::get()->renderProgress(); +} + +void BootLoader::handleEvent(const SDL_Event& event) { + if (event.type == SDL_EVENT_QUIT) { + SceneManager::current = SceneManager::Scene::QUIT; + } +} diff --git a/source/game/scenes/boot_loader.hpp b/source/game/scenes/boot_loader.hpp new file mode 100644 index 0000000..9a59510 --- /dev/null +++ b/source/game/scenes/boot_loader.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "game/scenes/scene.hpp" + +// Escena mínima que Director usa mientras el cache se carga incrementalmente. +// No avanza la carga — lo hace Director::iterate() llamando a Cache::loadStep() +// antes de despachar la escena. Aquí solo se pinta la barra de progreso. +class BootLoader : public Scene { + public: + BootLoader() = default; + ~BootLoader() override = default; + + void iterate() override; + void handleEvent(const SDL_Event& event) override; +};