càrrega de recursos no bloquejant

This commit is contained in:
2026-04-14 18:10:28 +02:00
parent d493ebf4f0
commit 8706b2c7fb
15 changed files with 356 additions and 118 deletions

View File

@@ -104,6 +104,7 @@ set(APP_SOURCES
source/game/scenes/instructions.cpp source/game/scenes/instructions.cpp
source/game/scenes/intro.cpp source/game/scenes/intro.cpp
source/game/scenes/logo.cpp source/game/scenes/logo.cpp
source/game/scenes/preload.cpp
source/game/scenes/title.cpp source/game/scenes/title.cpp
# --- game/ui --- # --- game/ui ---

View File

@@ -72,6 +72,7 @@
"[NOTIFICATIONS] DISCONNECTED": "desconectat", "[NOTIFICATIONS] DISCONNECTED": "desconectat",
"[RESOURCE] LOADING": "Carregant", "[RESOURCE] LOADING": "Carregant",
"[RESOURCE] PRESS_TO_CONTINUE": "Prem una tecla per continuar",
"[SERVICE_MENU] TITLE": "Menu de servei", "[SERVICE_MENU] TITLE": "Menu de servei",
"[SERVICE_MENU] RESET": "Reiniciar", "[SERVICE_MENU] RESET": "Reiniciar",

View File

@@ -71,6 +71,7 @@
"[NOTIFICATIONS] DISCONNECTED": "disconnected", "[NOTIFICATIONS] DISCONNECTED": "disconnected",
"[RESOURCE] LOADING": "Loading", "[RESOURCE] LOADING": "Loading",
"[RESOURCE] PRESS_TO_CONTINUE": "Press any key to continue",
"[SERVICE_MENU] TITLE": "Service Menu", "[SERVICE_MENU] TITLE": "Service Menu",
"[SERVICE_MENU] RESET": "Reset", "[SERVICE_MENU] RESET": "Reset",

View File

@@ -71,6 +71,7 @@
"[NOTIFICATIONS] DISCONNECTED": "desconectado", "[NOTIFICATIONS] DISCONNECTED": "desconectado",
"[RESOURCE] LOADING": "Cargando", "[RESOURCE] LOADING": "Cargando",
"[RESOURCE] PRESS_TO_CONTINUE": "Pulsa una tecla para continuar",
"[SERVICE_MENU] TITLE": "Menu de servicio", "[SERVICE_MENU] TITLE": "Menu de servicio",
"[SERVICE_MENU] RESET": "Reiniciar", "[SERVICE_MENU] RESET": "Reiniciar",

View File

@@ -67,7 +67,8 @@ Resource::Resource(LoadingMode mode)
Screen::get()->show(); Screen::get()->show();
if (loading_mode_ == LoadingMode::PRELOAD) { if (loading_mode_ == LoadingMode::PRELOAD) {
loading_text_ = Screen::get()->getText(); loading_text_ = Screen::get()->getText();
load(); // Ya NO llamamos load() aquí: Director bombea beginLoad() + loadStep()
// desde iterate() para mantener vivo el bucle SDL3 durante la carga.
} else { } else {
// En modo lazy, cargamos lo mínimo indispensable // En modo lazy, cargamos lo mínimo indispensable
initResourceLists(); initResourceLists();
@@ -413,31 +414,128 @@ void Resource::clear() {
demos_.clear(); demos_.clear();
} }
// Carga todos los recursos del juego y muestra el progreso de carga // Carga síncrona completa: usado por Resource::reload() (hot-reload en debug).
// En arranque normal la carga la bombea Director::iterate() vía loadStep().
void Resource::load() { void Resource::load() {
// Prepara la gestión del progreso de carga beginLoad();
while (!loadStep(INT_MAX)) {
// Presupuesto infinito: una sola pasada carga todo
}
Screen::get()->setVSync(saved_vsync_);
}
// Prepara el estado del cargador incremental. No carga nada todavía.
void Resource::beginLoad() {
calculateTotalResources(); calculateTotalResources();
initProgressBar(); initProgressBar();
// Muerstra la ventana y desactiva el sincronismo vertical saved_vsync_ = Screen::getVSync();
auto* screen = Screen::get(); Screen::get()->setVSync(false); // Maximiza FPS durante el preload
auto vsync = Screen::getVSync();
screen->setVSync(false);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** LOADING RESOURCES"); stage_ = LoadStage::SOUNDS;
loadSounds(); // Carga sonidos stage_index_ = 0;
loadMusics(); // Carga músicas }
loadTextures(); // Carga texturas
loadTextFiles(); // Carga ficheros de texto
loadAnimations(); // Carga animaciones
loadDemoData(); // Carga datos de demo
createText(); // Crea objetos de texto
createTextTextures(); // Crea texturas a partir de texto
createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** RESOURCES LOADED");
// Restablece el sincronismo vertical a su valor original auto Resource::isLoadDone() const -> bool {
screen->setVSync(vsync); return stage_ == LoadStage::DONE;
}
// Bombea la máquina de etapas hasta agotar el presupuesto de tiempo o completar la carga.
// Devuelve true cuando ya no queda nada por cargar.
auto Resource::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) { return true; }
const Uint64 start_ns = SDL_GetTicksNS();
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::SOUNDS: {
auto list = Asset::get()->getListByType(Asset::Type::SOUND);
if (stage_index_ == 0) { sounds_.clear(); }
if (stage_index_ >= list.size()) {
stage_ = LoadStage::MUSICS;
stage_index_ = 0;
break;
}
loadOneSound(stage_index_++);
break;
}
case LoadStage::MUSICS: {
auto list = Asset::get()->getListByType(Asset::Type::MUSIC);
if (stage_index_ == 0) { musics_.clear(); }
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXTURES;
stage_index_ = 0;
break;
}
loadOneMusic(stage_index_++);
break;
}
case LoadStage::TEXTURES: {
auto list = Asset::get()->getListByType(Asset::Type::BITMAP);
if (stage_index_ == 0) { textures_.clear(); }
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXT_FILES;
stage_index_ = 0;
break;
}
loadOneTexture(stage_index_++);
break;
}
case LoadStage::TEXT_FILES: {
auto list = Asset::get()->getListByType(Asset::Type::FONT);
if (stage_index_ == 0) { text_files_.clear(); }
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ANIMATIONS;
stage_index_ = 0;
break;
}
loadOneTextFile(stage_index_++);
break;
}
case LoadStage::ANIMATIONS: {
auto list = Asset::get()->getListByType(Asset::Type::ANIMATION);
if (stage_index_ == 0) { animations_.clear(); }
if (stage_index_ >= list.size()) {
stage_ = LoadStage::DEMO_DATA;
stage_index_ = 0;
break;
}
loadOneAnimation(stage_index_++);
break;
}
case LoadStage::DEMO_DATA: {
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
if (stage_index_ == 0) { demos_.clear(); }
if (stage_index_ >= list.size()) {
stage_ = LoadStage::CREATE_TEXT;
stage_index_ = 0;
break;
}
loadOneDemoData(stage_index_++);
break;
}
case LoadStage::CREATE_TEXT:
createText();
stage_ = LoadStage::CREATE_TEXT_TEXTURES;
break;
case LoadStage::CREATE_TEXT_TEXTURES:
createTextTextures();
stage_ = LoadStage::CREATE_PLAYER_TEXTURES;
break;
case LoadStage::CREATE_PLAYER_TEXTURES:
createPlayerTextures();
stage_ = LoadStage::DONE;
break;
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) { break; }
}
return stage_ == LoadStage::DONE;
} }
// Recarga todos los recursos (limpia y vuelve a cargar) // Recarga todos los recursos (limpia y vuelve a cargar)
@@ -450,96 +548,78 @@ void Resource::reload() {
} }
} }
// Carga los sonidos del juego // Carga un sonido concreto desde la lista de assets
void Resource::loadSounds() { void Resource::loadOneSound(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::SOUND); auto list = Asset::get()->getListByType(Asset::Type::SOUND);
sounds_.clear(); const auto& path = list[idx];
auto name = getFileName(path);
for (const auto& l : list) { updateLoadingProgress(name);
auto name = getFileName(l); auto audio_data = loadAudioData(path);
updateLoadingProgress(name); JA_Sound_t* sound = nullptr;
auto audio_data = loadAudioData(l); if (!audio_data.data.empty()) {
JA_Sound_t* sound = nullptr; sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size());
if (!audio_data.data.empty()) { } else {
sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size()); sound = JA_LoadSound(path.c_str());
} else {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
std::cout << "Sound load failed: " << name << '\n';
}
sounds_.emplace_back(name, sound);
} }
if (sound == nullptr) {
std::cout << "Sound load failed: " << name << '\n';
}
sounds_.emplace_back(name, sound);
} }
// Carga las músicas del juego // Carga una música concreta desde la lista de assets
void Resource::loadMusics() { void Resource::loadOneMusic(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::MUSIC); auto list = Asset::get()->getListByType(Asset::Type::MUSIC);
musics_.clear(); const auto& path = list[idx];
auto name = getFileName(path);
for (const auto& l : list) { updateLoadingProgress(name);
auto name = getFileName(l); auto audio_data = loadAudioData(path);
updateLoadingProgress(name); JA_Music_t* music = nullptr;
auto audio_data = loadAudioData(l); if (!audio_data.data.empty()) {
JA_Music_t* music = nullptr; music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size());
if (!audio_data.data.empty()) { } else {
music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size()); music = JA_LoadMusic(path.c_str());
} else {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
std::cout << "Music load failed: " << name << '\n';
}
musics_.emplace_back(name, music);
} }
if (music == nullptr) {
std::cout << "Music load failed: " << name << '\n';
}
musics_.emplace_back(name, music);
} }
// Carga las texturas del juego // Carga una textura concreta desde la lista de assets
void Resource::loadTextures() { void Resource::loadOneTexture(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::BITMAP); auto list = Asset::get()->getListByType(Asset::Type::BITMAP);
textures_.clear(); const auto& path = list[idx];
auto name = getFileName(path);
for (const auto& l : list) { updateLoadingProgress(name);
auto name = getFileName(l); textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), path));
updateLoadingProgress(name);
textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), l));
}
} }
// Carga los ficheros de texto del juego // Carga un fichero de texto concreto desde la lista de assets
void Resource::loadTextFiles() { void Resource::loadOneTextFile(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::FONT); auto list = Asset::get()->getListByType(Asset::Type::FONT);
text_files_.clear(); const auto& path = list[idx];
auto name = getFileName(path);
for (const auto& l : list) { updateLoadingProgress(name);
auto name = getFileName(l); text_files_.emplace_back(name, Text::loadFile(path));
updateLoadingProgress(name);
text_files_.emplace_back(name, Text::loadFile(l));
}
} }
// Carga las animaciones del juego // Carga una animación concreta desde la lista de assets
void Resource::loadAnimations() { void Resource::loadOneAnimation(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::ANIMATION); auto list = Asset::get()->getListByType(Asset::Type::ANIMATION);
animations_.clear(); const auto& path = list[idx];
auto name = getFileName(path);
for (const auto& l : list) { updateLoadingProgress(name);
auto name = getFileName(l); animations_.emplace_back(name, loadAnimationsFromFile(path));
updateLoadingProgress(name);
animations_.emplace_back(name, loadAnimationsFromFile(l));
}
} }
// Carga los datos para el modo demostración // Carga un fichero de datos de demo concreto desde la lista de assets
void Resource::loadDemoData() { void Resource::loadOneDemoData(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA); auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
demos_.clear(); const auto& path = list[idx];
auto name = getFileName(path);
for (const auto& l : list) { updateLoadingProgress(name);
auto name = getFileName(l); demos_.emplace_back(loadDemoDataFromFile(path));
updateLoadingProgress(name);
demos_.emplace_back(loadDemoDataFromFile(l));
}
} }
// Crea las texturas de jugadores con todas sus variantes de paleta // Crea las texturas de jugadores con todas sus variantes de paleta
@@ -756,12 +836,16 @@ void Resource::renderProgress() {
screen->start(); screen->start();
screen->clean(); screen->clean();
// Si la pantalla de carga está desactivada, dejamos todo en negro // Si la pantalla de carga está desactivada, dejamos todo en negro.
// wait_for_input solo tiene efecto cuando la pantalla está visible.
if (!Options::loading.show) { if (!Options::loading.show) {
screen->coreRender(); screen->coreRender();
return; return;
} }
// Estamos en la fase de espera explícita al usuario tras terminar la carga
const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input;
auto text_color = param.resource.color; auto text_color = param.resource.color;
auto bar_color = param.resource.color.DARKEN(100); auto bar_color = param.resource.color.DARKEN(100);
const auto TEXT_HEIGHT = loading_text_->getCharacterSize(); const auto TEXT_HEIGHT = loading_text_->getCharacterSize();
@@ -774,13 +858,17 @@ void Resource::renderProgress() {
SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a); SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a);
SDL_RenderRect(renderer, &loading_wired_rect_); SDL_RenderRect(renderer, &loading_wired_rect_);
// Escribe el nombre del recurso que se está cargando, centrado sobre la barra // Texto centrado sobre la barra: mientras carga, el nombre del recurso;
if (Options::loading.show_resource_name && !loading_resource_name_.empty()) { // al terminar en modo wait_for_input, el prompt traducido.
const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT
? Lang::getText("[RESOURCE] PRESS_TO_CONTINUE")
: loading_resource_name_;
if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) {
loading_text_->writeDX( loading_text_->writeDX(
Text::CENTER | Text::COLOR, Text::CENTER | Text::COLOR,
loading_wired_rect_.x + (loading_wired_rect_.w / 2), loading_wired_rect_.x + (loading_wired_rect_.w / 2),
loading_wired_rect_.y - TEXT_HEIGHT - 2, loading_wired_rect_.y - TEXT_HEIGHT - 2,
loading_resource_name_, OVER_BAR_TEXT,
1, 1,
text_color); text_color);
} }
@@ -829,12 +917,12 @@ void Resource::initProgressBar() {
loading_full_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT}; loading_full_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
} }
// Actualiza el progreso de carga y muestra la barra // Actualiza el estado del progreso. No renderiza: el repintado lo hace
// Preload::iterate una vez por frame llamando a renderProgress().
void Resource::updateLoadingProgress(std::string name) { void Resource::updateLoadingProgress(std::string name) {
loading_resource_name_ = std::move(name); loading_resource_name_ = std::move(name);
loading_count_.increase(); loading_count_.increase();
updateProgressBar(); updateProgressBar();
renderProgress();
} }
// Actualiza la barra de estado // Actualiza la barra de estado

View File

@@ -42,6 +42,15 @@ class Resource {
// --- Métodos de recarga de recursos --- // --- Métodos de recarga de recursos ---
void reload(); // Recarga todos los recursos void reload(); // Recarga todos los recursos
// --- Cargador incremental ---
// beginLoad prepara el estado; loadStep carga recursos hasta agotar el presupuesto;
// devuelve true cuando ya no queda nada. renderProgress se llama una vez por frame
// desde la escena Preload.
void beginLoad();
auto loadStep(int budget_ms) -> bool;
[[nodiscard]] auto isLoadDone() const -> bool;
void renderProgress();
// --- Método para obtener el modo de carga actual --- // --- Método para obtener el modo de carga actual ---
[[nodiscard]] auto getLoadingMode() const -> LoadingMode { return loading_mode_; } [[nodiscard]] auto getLoadingMode() const -> LoadingMode { return loading_mode_; }
@@ -146,13 +155,24 @@ class Resource {
SDL_FRect loading_wired_rect_; SDL_FRect loading_wired_rect_;
SDL_FRect loading_full_rect_; SDL_FRect loading_full_rect_;
// --- Estado del cargador incremental ---
enum class LoadStage {
SOUNDS,
MUSICS,
TEXTURES,
TEXT_FILES,
ANIMATIONS,
DEMO_DATA,
CREATE_TEXT,
CREATE_TEXT_TEXTURES,
CREATE_PLAYER_TEXTURES,
DONE
};
LoadStage stage_{LoadStage::DONE};
size_t stage_index_{0};
bool saved_vsync_{false}; // Vsync previo a beginLoad, restaurado por finishBoot/load
// --- Métodos internos de carga y gestión --- // --- Métodos internos de carga y gestión ---
void loadSounds(); // Carga los sonidos
void loadMusics(); // Carga las músicas
void loadTextures(); // Carga las texturas
void loadTextFiles(); // Carga los ficheros de texto
void loadAnimations(); // Carga las animaciones
void loadDemoData(); // Carga los datos para el modo demostración
void loadDemoDataQuiet(); // Carga los datos de demo sin mostrar progreso (para modo lazy) void loadDemoDataQuiet(); // Carga los datos de demo sin mostrar progreso (para modo lazy)
void loadEssentialResources(); // Carga recursos esenciales en modo lazy void loadEssentialResources(); // Carga recursos esenciales en modo lazy
void loadEssentialTextures(); // Carga solo las texturas esenciales (fuentes) void loadEssentialTextures(); // Carga solo las texturas esenciales (fuentes)
@@ -176,11 +196,18 @@ class Resource {
// --- Métodos internos para gestionar el progreso --- // --- Métodos internos para gestionar el progreso ---
void calculateTotalResources(); // Calcula el número de recursos para cargar void calculateTotalResources(); // Calcula el número de recursos para cargar
void renderProgress(); // Muestra el progreso de carga
void updateLoadingProgress(std::string name); // Actualiza el progreso de carga void updateLoadingProgress(std::string name); // Actualiza el progreso de carga
void initProgressBar(); // Inicializa los rectangulos que definen la barra de progreso void initProgressBar(); // Inicializa los rectangulos que definen la barra de progreso
void updateProgressBar(); // Actualiza la barra de estado void updateProgressBar(); // Actualiza la barra de estado
// --- Helpers del cargador incremental (cargan un único recurso) ---
void loadOneSound(size_t idx);
void loadOneMusic(size_t idx);
void loadOneTexture(size_t idx);
void loadOneTextFile(size_t idx);
void loadOneAnimation(size_t idx);
void loadOneDemoData(size_t idx);
// --- Constructores y destructor privados (singleton) --- // --- Constructores y destructor privados (singleton) ---
explicit Resource(LoadingMode mode); // Constructor privado con modo de carga explicit Resource(LoadingMode mode); // Constructor privado con modo de carga
~Resource(); // Destructor privado ~Resource(); // Destructor privado

View File

@@ -217,4 +217,5 @@ namespace Defaults::Settings {
namespace Defaults::Loading { namespace Defaults::Loading {
constexpr bool SHOW = false; constexpr bool SHOW = false;
constexpr bool SHOW_RESOURCE_NAME = true; constexpr bool SHOW_RESOURCE_NAME = true;
constexpr bool WAIT_FOR_INPUT = false;
} // namespace Defaults::Loading } // namespace Defaults::Loading

View File

@@ -32,6 +32,7 @@
#include "game/scenes/instructions.hpp" // Para Instructions #include "game/scenes/instructions.hpp" // Para Instructions
#include "game/scenes/intro.hpp" // Para Intro #include "game/scenes/intro.hpp" // Para Intro
#include "game/scenes/logo.hpp" // Para Logo #include "game/scenes/logo.hpp" // Para Logo
#include "game/scenes/preload.hpp" // Para Preload
#include "game/scenes/title.hpp" // Para Title #include "game/scenes/title.hpp" // Para Title
#include "game/ui/notifier.hpp" // Para Notifier #include "game/ui/notifier.hpp" // Para Notifier
#include "game/ui/service_menu.hpp" // Para ServiceMenu #include "game/ui/service_menu.hpp" // Para ServiceMenu
@@ -140,9 +141,40 @@ void Director::init() {
#else #else
Resource::init(Resource::LoadingMode::PRELOAD); Resource::init(Resource::LoadingMode::PRELOAD);
#endif #endif
if (Resource::get()->getLoadingMode() == Resource::LoadingMode::PRELOAD) {
// Guarda la sección destino (la que fijó loadDebugConfig o el default)
// y redirige el arranque a la escena PRELOAD hasta que loadStep termine.
Section::post_preload = Section::name;
Section::name = Section::Name::PRELOAD;
Resource::get()->beginLoad();
} else {
// LAZY_LOAD: el constructor de Resource ya cargó lo esencial síncronamente;
// no hay fase de preload, pasamos directamente a post-boot.
finishBoot();
boot_loading_ = false;
}
// ServiceMenu/Notifier/getSingletons se mueven a finishBoot() — dependen
// de Resource y se inicializan al terminar la carga incremental.
}
// Inicializaciones que dependen del Resource cargado. Se llama desde iterate()
// cuando Resource::loadStep() devuelve true, con la ventana y el bucle vivos.
void Director::finishBoot() {
ServiceMenu::init(); // Inicializa el menú de servicio ServiceMenu::init(); // Inicializa el menú de servicio
Notifier::init(std::string(), Resource::get()->getText("8bithud")); // Inicialización del sistema de notificaciones Notifier::init(std::string(), Resource::get()->getText("8bithud")); // Inicialización del sistema de notificaciones
Screen::get()->getSingletons(); // Obtiene los punteros al resto de singletones Screen::get()->getSingletons(); // Obtiene los punteros al resto de singletones
// Restaura el vsync a la preferencia del usuario (beginLoad lo había puesto a false)
Screen::get()->setVSync(Options::video.vsync);
// Si NO estamos en modo "wait for input", transiciona ya al destino.
// Si wait_for_input está activo (y la pantalla es visible), nos quedamos
// en PRELOAD hasta que el usuario pulse tecla/botón.
if (!(Options::loading.show && Options::loading.wait_for_input)) {
Section::name = Section::post_preload;
}
} }
// Cierra todo y libera recursos del sistema y de los singletons // Cierra todo y libera recursos del sistema y de los singletons
@@ -327,6 +359,7 @@ void Director::createSystemFolder(const std::string& folder) {
// Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez) // Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez)
void Director::resetActiveSection() { void Director::resetActiveSection() {
preload_.reset();
logo_.reset(); logo_.reset();
intro_.reset(); intro_.reset();
title_.reset(); title_.reset();
@@ -353,6 +386,10 @@ void Director::handleSectionTransition() {
// Construye la nueva // Construye la nueva
switch (Section::name) { switch (Section::name) {
case Section::Name::PRELOAD:
preload_ = std::make_unique<Preload>();
break;
case Section::Name::LOGO: case Section::Name::LOGO:
logo_ = std::make_unique<Logo>(); logo_ = std::make_unique<Logo>();
break; break;
@@ -435,20 +472,32 @@ auto Director::iterate() -> SDL_AppResult {
return SDL_APP_SUCCESS; return SDL_APP_SUCCESS;
} }
// En el primer frame, SDL ya ha drenado los SDL_EVENT_GAMEPAD_ADDED de los // Fase de boot: carga incremental frame a frame con presupuesto de 50ms.
// mandos conectados al iniciar. A partir de ahora los eventos sí son hotplug // Durante esta fase la escena activa es Preload (una barra de progreso).
// real y deben mostrar notificación. if (boot_loading_) {
static bool first_iterate = true; try {
if (first_iterate) { if (Resource::get()->loadStep(50 /*ms*/)) {
first_iterate = false; finishBoot();
GlobalEvents::markStartupComplete(); boot_loading_ = false;
// Los SDL_EVENT_GAMEPAD_ADDED iniciales ya los ha drenado la rama
// durante la carga: marcamos startup completo ahora para que los
// ADDED/REMOVED posteriores sí generen notificación.
GlobalEvents::markStartupComplete();
}
} catch (const std::exception& e) {
std::cerr << "Fatal error during resource load: " << e.what() << '\n';
Section::name = Section::Name::QUIT;
return SDL_APP_FAILURE;
}
} }
// Gestiona las transiciones entre secciones (destruye la anterior y construye la nueva) // Gestiona las transiciones entre secciones (destruye la anterior y construye la nueva)
handleSectionTransition(); handleSectionTransition();
// Ejecuta un frame de la sección activa // Ejecuta un frame de la sección activa
if (logo_) { if (preload_) {
preload_->iterate();
} else if (logo_) {
logo_->iterate(); logo_->iterate();
} else if (intro_) { } else if (intro_) {
intro_->iterate(); intro_->iterate();
@@ -473,7 +522,9 @@ auto Director::handleEvent(SDL_Event& event) -> SDL_AppResult {
GlobalEvents::handle(event); GlobalEvents::handle(event);
// Reenvía a la sección activa // Reenvía a la sección activa
if (logo_) { if (preload_) {
preload_->handleEvent(event);
} else if (logo_) {
logo_->handleEvent(event); logo_->handleEvent(event);
} else if (intro_) { } else if (intro_) {
intro_->handleEvent(event); intro_->handleEvent(event);

View File

@@ -12,6 +12,7 @@ namespace Lang {
} }
// Declaraciones adelantadas de las secciones // Declaraciones adelantadas de las secciones
class Preload;
class Logo; class Logo;
class Intro; class Intro;
class Title; class Title;
@@ -54,6 +55,7 @@ class Director {
std::string system_folder_; // Carpeta del sistema para almacenar datos std::string system_folder_; // Carpeta del sistema para almacenar datos
// --- Sección activa (una y sólo una viva en cada momento) --- // --- Sección activa (una y sólo una viva en cada momento) ---
std::unique_ptr<Preload> preload_;
std::unique_ptr<Logo> logo_; std::unique_ptr<Logo> logo_;
std::unique_ptr<Intro> intro_; std::unique_ptr<Intro> intro_;
std::unique_ptr<Title> title_; std::unique_ptr<Title> title_;
@@ -63,8 +65,12 @@ class Director {
std::unique_ptr<Credits> credits_; std::unique_ptr<Credits> credits_;
Section::Name last_built_section_name_ = Section::Name::RESET; Section::Name last_built_section_name_ = Section::Name::RESET;
// --- Fase de arranque no bloqueante ---
bool boot_loading_ = true; // True mientras Resource::loadStep está cargando incremental
// --- Inicialización y cierre del sistema --- // --- Inicialización y cierre del sistema ---
void init(); // Inicializa la aplicación void init(); // Inicializa la aplicación (pre-boot)
void finishBoot(); // Post-boot: inicializa lo que depende de recursos cargados
static void close(); // Cierra y libera recursos static void close(); // Cierra y libera recursos
// --- Configuración inicial --- // --- Configuración inicial ---

View File

@@ -36,7 +36,11 @@ namespace GlobalEvents {
// Reasignar siempre: tanto en arranque como en hotplug en caliente. // Reasignar siempre: tanto en arranque como en hotplug en caliente.
Options::gamepad_manager.assignAndLinkGamepads(); Options::gamepad_manager.assignAndLinkGamepads();
Options::gamepad_manager.resyncGamepadsWithPlayers(); Options::gamepad_manager.resyncGamepadsWithPlayers();
ServiceMenu::get()->refresh();
// Durante el preload ServiceMenu aún no existe: solo refresca si está vivo.
if (ServiceMenu::get() != nullptr) {
ServiceMenu::get()->refresh();
}
if (startup_in_progress || message.empty()) { if (startup_in_progress || message.empty()) {
return; return;
@@ -51,7 +55,11 @@ namespace GlobalEvents {
message.replace(pos, std::string(" DISCONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] DISCONNECTED")); message.replace(pos, std::string(" DISCONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] DISCONNECTED"));
} }
Notifier::get()->show({message}); // Notifier también puede no existir todavía si la notificación se
// disparase antes de finishBoot(). Protegido por si acaso.
if (Notifier::get() != nullptr) {
Notifier::get()->show({message});
}
} }
void markStartupComplete() { void markStartupComplete() {
@@ -79,7 +87,10 @@ namespace GlobalEvents {
break; break;
} }
ServiceMenu::get()->handleEvent(event); // Durante el preload ServiceMenu aún no existe
if (ServiceMenu::get() != nullptr) {
ServiceMenu::get()->handleEvent(event);
}
Mouse::handleEvent(event); Mouse::handleEvent(event);
handleInputEvents(event); handleInputEvents(event);
} }

View File

@@ -10,6 +10,7 @@ namespace Section {
// --- Enumeraciones de secciones del programa --- // --- Enumeraciones de secciones del programa ---
enum class Name { enum class Name {
RESET, // Inicialización RESET, // Inicialización
PRELOAD, // Carga incremental de recursos
LOGO, // Pantalla de logo LOGO, // Pantalla de logo
INTRO, // Introducción INTRO, // Introducción
TITLE, // Pantalla de título/menú principal TITLE, // Pantalla de título/menú principal
@@ -43,6 +44,7 @@ namespace Section {
// --- Variables globales de estado --- // --- Variables globales de estado ---
inline Name name = Name::RESET; inline Name name = Name::RESET;
inline Name post_preload = Name::LOGO; // Sección a la que transiciona PRELOAD al terminar
inline Options options = Options::NONE; inline Options options = Options::NONE;
inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO; inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;
} // namespace Section } // namespace Section

View File

@@ -495,6 +495,7 @@ namespace Options {
const auto& ld = yaml["loading"]; const auto& ld = yaml["loading"];
parseBoolField(ld, "show", loading.show); parseBoolField(ld, "show", loading.show);
parseBoolField(ld, "show_resource_name", loading.show_resource_name); parseBoolField(ld, "show_resource_name", loading.show_resource_name);
parseBoolField(ld, "wait_for_input", loading.wait_for_input);
} }
void loadGameFromYaml(const fkyaml::node& yaml) { void loadGameFromYaml(const fkyaml::node& yaml) {
@@ -696,6 +697,7 @@ namespace Options {
file << "loading:\n"; file << "loading:\n";
file << " show: " << boolToString(loading.show) << "\n"; file << " show: " << boolToString(loading.show) << "\n";
file << " show_resource_name: " << boolToString(loading.show_resource_name) << "\n"; file << " show_resource_name: " << boolToString(loading.show_resource_name) << "\n";
file << " wait_for_input: " << boolToString(loading.wait_for_input) << "\n";
file << "\n"; file << "\n";
// GAME // GAME

View File

@@ -112,6 +112,7 @@ namespace Options {
struct Loading { struct Loading {
bool show = Defaults::Loading::SHOW; // Muestra la pantalla de carga (si no, pantalla en negro) bool show = Defaults::Loading::SHOW; // Muestra la pantalla de carga (si no, pantalla en negro)
bool show_resource_name = Defaults::Loading::SHOW_RESOURCE_NAME; // Muestra el nombre del recurso en curso sobre la barra de progreso bool show_resource_name = Defaults::Loading::SHOW_RESOURCE_NAME; // Muestra el nombre del recurso en curso sobre la barra de progreso
bool wait_for_input = Defaults::Loading::WAIT_FOR_INPUT; // Al terminar la carga, espera a que el usuario pulse una tecla/botón (solo si show=true)
}; };
struct Settings { struct Settings {

View File

@@ -0,0 +1,26 @@
#include "game/scenes/preload.hpp"
#include "core/resources/resource.hpp" // Para Resource
#include "core/system/section.hpp" // Para Section
#include "game/options.hpp" // Para Options::loading
void Preload::iterate() {
// El repintado es independiente del avance de la carga: Director::iterate
// ya ha bombeado Resource::loadStep() antes de llamar aquí.
Resource::get()->renderProgress();
}
void Preload::handleEvent(const SDL_Event& event) {
// Solo aceptamos input si la carga ha terminado y estamos en modo
// wait_for_input (que además exige que la pantalla de carga sea visible).
if (!Resource::get()->isLoadDone() || !Options::loading.wait_for_input || !Options::loading.show) {
return;
}
// Pulsación limpia: ignoramos auto-repeat y eventos de hotplug de mando.
const bool IS_KEY = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
const bool IS_BUTTON = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
if (IS_KEY || IS_BUTTON) {
Section::name = Section::post_preload;
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Event
// --- Clase Preload: escena mínima durante la carga incremental de recursos ---
//
// No avanza la carga — de eso se encarga Director::iterate() llamando a
// Resource::loadStep(budget_ms) antes de despachar la escena. Aquí solo se
// repinta la barra de progreso y, si Options::loading.wait_for_input está
// activo, se detecta la pulsación que transiciona a la siguiente sección.
class Preload {
public:
Preload() = default;
~Preload() = default;
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Repinta la barra de progreso
void handleEvent(const SDL_Event& event); // Detecta pulsación en modo wait_for_input
};