pantalla de carrega no bloquejant

streaming de audio per evitar precárrega i descompresió a memoria
This commit is contained in:
2026-04-13 19:29:05 +02:00
parent 585c93054e
commit 9b8820ffa3
9 changed files with 587 additions and 261 deletions

View File

@@ -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

View File

@@ -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<int>(JA_MUSIC_LOW_WATER_SECONDS * static_cast<float>(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<Uint8*>(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<int>(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<int>(info.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
return music;
@@ -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(&current_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) {

View File

@@ -2,10 +2,6 @@
#include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h> // Para emscripten_sleep
#endif
#include <algorithm> // Para find_if
#include <cstdlib> // Para exit, size_t
#include <fstream> // 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<Uint8>(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<Uint8>(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<Uint64>(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<TextObjectInfo>& getTextObjectInfos() {
static const std::vector<TextObjectInfo> 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<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(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<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress(1);
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(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<Surface>(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<Surface>(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::Data>(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<Text>(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::Data>(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<ResourceInfo> resources = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
for (const auto& res_info : resources) {
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
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<float>(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

View File

@@ -25,7 +25,13 @@ namespace Resource {
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
auto getRooms() -> std::vector<RoomResource>&;
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<Text> 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

View File

@@ -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<BootLoader>();
break;
case SceneManager::Scene::LOGO:
active_scene_ = std::make_unique<Logo>();
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();

View File

@@ -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<Scene> active_scene_; // Escena activa
SceneManager::Scene current_scene_{SceneManager::Scene::LOGO}; // Tipus d'escena activa
std::unique_ptr<Scene> 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
};

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <SDL3/SDL.h>
#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;
};