Compare commits

...

29 Commits

Author SHA1 Message Date
0e49f35d3b correccions en makefile de macos i en el numero de versio (de guionets a punts) 2026-05-03 17:55:54 +02:00
b9cee1bc70 afegit c_cpp_properties.json 2026-05-03 17:48:43 +02:00
3390d01ef6 normalitzat Audio 2026-04-18 11:43:13 +02:00
561028ff04 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
671583ebbe arreglos en make i cmake per estandaritzar amb la resta de projectes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:59:57 +02:00
e941502615 fix: input android amb sdl_joystick 2026-04-15 23:53:47 +02:00
8bab2da2ed filtre nearest o linear per al pipeline sdl 2026-04-15 11:10:00 +02:00
5ef278ce70 make controllerdb
trim del nom del mando
2026-04-15 09:50:42 +02:00
14103175a9 arreglos en screen 2026-04-15 06:31:23 +02:00
10a3e2fedd service menu vitaminat: cliping, swapping animation i versió 2026-04-14 19:41:17 +02:00
25a36d5064 arreglos en service menu per a emscripten 2026-04-14 19:08:45 +02:00
8706b2c7fb càrrega de recursos no bloquejant 2026-04-14 18:10:28 +02:00
d493ebf4f0 afegida versió controlada, no automatica 2026-04-14 17:38:27 +02:00
3e795998d1 arreglos cosmetics en la pantalla de carrega de recursos 2026-04-14 17:31:24 +02:00
c694781f38 correccions en la detecció de mandos 2026-04-14 17:09:09 +02:00
1a2298963d afegit mode autoplay en debug 2026-04-14 16:11:33 +02:00
4c1ed1cf9b path emscripten 2026-04-14 13:54:50 +02:00
f80d0a656e idem 2026-04-14 13:26:30 +02:00
4429cd92c1 reestructuració 2026-04-14 13:26:22 +02:00
4ac34b8583 eliminat soport per a arguments 2026-04-14 13:09:54 +02:00
cf7ea6cc9c eliminat molt de ruido de la consola de log 2026-04-14 13:04:24 +02:00
f5da35bfb2 sdl_callbacks 2026-04-14 12:21:00 +02:00
c0accd25e2 streaming de audio 2026-04-14 08:32:49 +02:00
ad8ad7e756 pasaeta de granera 2026-04-14 08:18:17 +02:00
673587230e corregit make release de windows 2026-04-05 18:39:23 +02:00
8538a1047f corregit makefile per a macos 2026-04-03 21:48:04 +02:00
e150097edc actualitzat makefile 2026-04-03 21:27:00 +02:00
5f0d1f9577 afegit changelog,md 2026-04-03 21:08:46 +02:00
6d8d02f0e4 corregit el bug introduit en c35889a840 2026-04-03 21:01:44 +02:00
202 changed files with 12718 additions and 10031 deletions

View File

@@ -1,28 +1,24 @@
Checks: >
readability-*,
modernize-*,
performance-*,
bugprone-unchecked-optional-access,
bugprone-sizeof-expression,
bugprone-suspicious-missing-comma,
bugprone-suspicious-index,
bugprone-undefined-memory-manipulation,
bugprone-use-after-move,
bugprone-out-of-bound-access,
-readability-identifier-length,
-readability-magic-numbers,
-bugprone-narrowing-conversions,
-performance-enum-size,
-performance-inefficient-string-concatenation,
-bugprone-integer-division,
-bugprone-easily-swappable-parameters,
Checks:
- readability-*
- modernize-*
- performance-*
- bugprone-*
- -readability-identifier-length
- -readability-magic-numbers
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays,-warnings-as-errors
WarningsAsErrors: '*'
# Solo incluir archivos de tu código fuente
HeaderFilterRegex: '^source/(sections|ui)/.*'
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
HeaderFilterRegex: 'source/.*'
FormatStyle: file
CheckOptions:
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
# Variables locales en snake_case
- { key: readability-identifier-naming.VariableCase, value: lower_case }

1
.gitignore vendored
View File

@@ -1,4 +1,3 @@
.vscode/
.claude/
.cache/
build/

51
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}/source/**",
"${workspaceFolder}/build"
],
"defines": [
"_DEBUG",
"_NO_AUDIO",
"MACOS_BUILD"
],
"cStandard": "c17",
"cppStandard": "c++20",
"intelliSenseMode": "macos-clang-arm64"
},
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/source/**",
"${workspaceFolder}/build"
],
"defines": [
"_DEBUG",
"_NO_AUDIO",
"LINUX_BUILD"
],
"cStandard": "c17",
"cppStandard": "c++20",
"intelliSenseMode": "linux-gcc-x64"
},
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/source/**",
"${workspaceFolder}/build"
],
"defines": [
"_DEBUG",
"_NO_AUDIO",
"WINDOWS_BUILD",
"_WIN32"
],
"cStandard": "c17",
"cppStandard": "c++20",
"intelliSenseMode": "windows-msvc-x64"
}
],
"version": 4
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json"
}

200
CHANGELOG.md Normal file
View File

@@ -0,0 +1,200 @@
# CHANGELOG
Historial de canvis i novetats de Coffee Crisis Arcade Edition.
---
## 2026-04-03
- **Nova intro cinematogràfica**: les tarjetes s'llancen des dels costats de la pantalla amb zoom, rotació i rebot, simulant tirar cartes sobre una mesa. Les anteriors ixen despedides girant quan arriba la següent. Sombra amb efecte de perspectiva 2D→3D.
- **Efectes sonors i visuals en la intro**: shake de pantalla i sons configurables a cada impacte de tarjeta.
- **Migració a SDL3 GPU API**: postfx i crtpi migrats a SDL3GPU (Vulkan/Metal/D3D12).
- **Migració de configuració a YAML**: eliminat el format antic, ara tot en YAML.
- **Afegides opcions al Service Menu**.
- **HUD de FPS retocat**, presets per defecte ajustats, finestra a 2x i shader off per defecte.
- **Corregit bug d'input**: revertit un canvi que causava bucle infinit en F3 (pantalla completa) i F12 (service menu) en totes les escenes excepte Game.
- Neteja de codi: eliminades referencies a OpenGL, fitxers GLSL sobrants, normalitzada la carpeta release i el caption de la finestra.
---
## 2025-10-25
- **Migració a delta_time pur** en credits, instructions i hiscore_table. Eliminat un bug que feia que els credits no acabaren mai si no passaves a mà.
- **Corregida deformació subpixel** de textures en instructions i hiscore_table.
- **Detecció de fitxers de puntuació corruptes**.
- **Efecte de pulsos** afegit al scoreboard.
- Al posar nom, el carrusel apareix directament en el caràcter d'acabar si ja havies posat nom abans.
- Integrat jail_audio en la càrrega de resources.pack.
- Pasaeta de linter en múltiples fitxers.
- Nou icon per al joc.
- Corregida la versió release de macOS per a funcionar correctament amb resources.pack.
---
## 2025-08-21
- **Integració amb resources.pack**: textures, animacions, textos, dades de la demo i jail_audio integrats amb ResourceHelper.
- Actualitzat Makefile per a Windows, Linux i macOS.
- Neteja de temporals al acabar.
---
## 2025-08-17
- **Afegit fade RANDOM_SQUARE2** amb timings canviats a mil·lisegons.
- **Outline parametritzat** per als textos dels items.
- **Colors de camiseta parametritzats** per defecte i quan pillen café.
- Creat `defaults.h` amb els valors per defecte de Param.
- Afegit `param_red.txt` amb guardes en setParams.
- Fix: items que es quedaven engantxats a la part de dalt.
- Fix: en el modo demo, assignava cafés al jugador que no jugava.
- Fix: bug en l'estat pre del fade.
- Fix: globos apareixien un frame mal situats al crear-se des d'un pare.
- Afegit suport per a mapejar botons tipus trigger.
---
## 2025-08-10
- **Service Menu complet**: animació d'apertura/tancament, callback per a posar pausa en el joc, refresc visual al canviar mandos.
- **Mandos en calent**: es poden connectar i desconnectar mandos durant el joc, amb notificació visual.
- **PauseManager** afegit al joc.
- **Càrrega de recursos on_demand**.
- Afegit `shutdown.h` i `system_utils.h`.
- Fix: el nom apareixia duplicat en la tabla de records.
- Fix: Game no es desregistrava de ServiceMenu al destruir-se.
- Precàrrega de textures del jugador amb variants de paleta.
- Actualitzats frameworks per a macOS.
---
## 2025-03-25
- **Nova secció Intro** amb escenes seqüencials, animacions de tarjetes i text narratiu.
- **Shaders respecten l'escalat sencer** i SDL_RenderSetLogicalSize en pantalla completa.
- **Tecla per canviar l'integer scale** (F-key).
- Afegit intro03.png i intro04.png.
- Renomenat InputType a InputActions.
- Actualitzat gamecontrollerdb.txt amb mappings de la recreativa.
- Fix: al fer reset des de Game, en Intro no sonava la música.
- Fix: al acabar la partida i vore els records, torna al títol.
- Fix: amb l'àudio mutat, el fade per al soroll de boles el tornava a deixar activat.
---
## 2025-02-07
- **EnterName millorat**: si has omplit tots els slots, apretar una volta mes fixa el nom.
- **Control de repetició per als eixos del joystick**.
- **La tabla de puntuació** mostra amb altre color la puntuació acabada d'afegir i les aconseguides amb 1CC.
- Nova font per a la intro.
- Afegit efecte d'eixida a les instruccions.
- Afegit disparador per a l'aparició de l'enemic nou.
- Duplicada la font 04b_25 per a versió gris i versió negra.
---
## 2025-01-05
- **Optimitzat el circuit de render** en pantalla.
- **Atenuat de pantalla restaurat**: Fade feia dos SDL_SetRenderDrawBlendMode sense restaurar.
- Fix: es podia polsar per a jugar mentre feia el fade cap a la demo.
- Fix: error en la seqüència final de retrocedir en el temps.
- Calibrats els polsos al gust.
- Afegida una lluna i un sol al fondo.
- La powerball ja no es pot destruir fins que no ha fet un rebot.
- Modificada la cadència de foc sense autofire.
- Afegit botó per a activar o desactivar el ratolí.
---
## 2024-12-31
- **Enemic nou** complet: gràfics, comportament, àudio i veus.
- **Fade out sincronitzat** de vídeo i àudio en el títol i el joc.
- **Roidets de col·lisió** per als globos en certs moments.
- La finestra ja es pot fer tan gran com permeta la pantalla (zoom dinàmic).
- Afegides veus al jugador i efectes de so al rebotar quan mor.
- Afegit delay opcional al flash de Screen.
- Afegit botó per activar o desactivar l'autofire.
- Fix: mode demo desactivava els sons permanentment.
- Actualitzat jail_audio.
---
## 2024-12-05
- **Secció Credits acabada** a 320x240 (i per extensió, a qualsevol resolució).
- **Zoom afegit a la classe Sprite** i al subtítol ARCADE EDITION.
- Duplicats fitxers de shaders per a resolucions verticals de 256 i 240.
- Afegit globalInputs::update() a totes les seccions.
- Fix: faltava corregir el flash de destroyAllBalloons().
- Fix: si saltes el logo, talla el so a meitat sonar.
- Canvi d'idioma amb una tecla (i reinicia).
---
## 2024-11-27
- **Secció Credits**: disseny, música, globos amb play_area definida, opció de canviar la paleta al text.
- Afegides traduccions dels credits.
---
## 2024-11-20
- **Nova animació de mort del personatge**: rebots, llengua fora, ulls en X, gràfics de caure derrotat per al segon jugador.
- **Powerball redissenyada**: nous gràfics, nou comportament, ja no mata directament.
- **Globos fills** ja no ixen centrats al pare (evita apilar-se).
- Arreglos en el nom al obtindre la màxima puntuació.
- Acabat BalloonManager.
- CMakeLists.txt crea l'executable en l'arrel del projecte.
- Nova font de text gran amb el doble de definició.
- Fix: paleta verda del primer jugador ajustada a l'original.
---
## 2024-11-03
- **Teclat com a control independent**: ja pot jugar un jugador amb teclat i altre amb mando, o assignar el teclat a qualsevol jugador.
- **Implementat el final del joc** i l'Attract Mode.
- **Nou motor per a textos en pantalla** (game_text amb textures precarregades).
- **Noves animacions** per a deixar de disparar.
- Al redefinir botons, ja no pots repetir botó.
- Fix: l'animació de morir s'actualitzava dos voltes per frame.
- Fix: l'efecte de flash tenia un valor massa xicotet.
---
## 2024-10-28
- **Classe PathSprite completada**: el game_text gasta PathSprites en lloc de SmartSprites.
- **Time stopper redissenyat**.
- La partida sempre comença igual (createTwoBigBalloons).
- Revisades les classes Balloon i Bullet.
- Millorada l'aparició dels game_text.
- Fix: la paleta dels jugadors no s'iniciava correctament.
---
## 2024-10-20
- **Classe Resource creada**: precàrrega de tots els recursos (textures, música, sons, animacions).
- **Paletes de color** per a textures GIF amb shared_ptr.
- Precàrrega i assignació de paletes.
- Implementat comptador per a posar el nom al acabar la partida.
- Classe Notifier independitzada de Screen amb codis identificadors.
- Afegit codi per a apagar el sistema al eixir del joc.
- Fix: globos verds tenien setters mal assignats i velocitat incorrecta.
- Fix: no guardar el fitxer de puntuacions en el mode demo.
---
## 2024-10-14
- **Versió inicial**: clon del repositori de Coffee Crisis, adaptat per a Arcade Edition.
- Pasaeta de include-what-you-use i cppcheck.
- Estandarització de noms segons convencions (CamelCase, camelBack, snake_case).
- Herències de les classes Sprite corregides.
- Canvi a C++ modern amb smart pointers per a la càrrega de surfaces des de GIF.
- Eliminats últims defines i passats a enum class.

View File

@@ -16,17 +16,15 @@ cmake -B build -DCMAKE_BUILD_TYPE=Debug # configure
cmake --build build # build
```
### Makefile (direct compilation, platform-specific targets)
### Makefile (delegates to CMake)
```bash
make linux # build for Linux
make linux_debug # debug build with -DDEBUG -DVERBOSE
make # build Release via cmake
make debug # build Debug via cmake
make release # create release package (auto-detects OS)
make linux_release # release tar.gz with resources.pack
make windows # build for Windows (cross-compile or native)
make windows_debug # Windows debug build
make macos # build for macOS (arm64)
make raspi # build for Raspberry Pi
make anbernic # build for Anbernic (no shaders, arcade mode)
make no_audio # build without audio system
make windows_release # release zip for Windows
make macos_release # release dmg for macOS (Intel + Apple Silicon)
make raspi_release # release tar.gz for Raspberry Pi
```
### Tools & Resources
@@ -46,24 +44,38 @@ make tidy-fix # run clang-tidy with auto-fix
## Architecture
### Singletons (core systems)
- **Director** (`source/director.hpp`) — Application state machine, orchestrates scene transitions (Logo → Intro → Title → Game → Credits/HiScore → Title)
- **Screen** (`source/screen.hpp`) — Window management, SDL3 GPU rendering pipeline, post-processing effects
- **Resource** (`source/resource.hpp`) — Asset loading/caching with PRELOAD and LAZY_LOAD modes, reads from `resources.pack`
- **Audio** (`source/audio.hpp`) — Music and SFX management
- **Input** (`source/input.hpp`) — Keyboard and gamepad input handling
### Source layout
The `source/` tree is organised in the same style as the sibling projects `projecte_2026` and `jaildoctors_dilemma`:
### Scenes (source/sections/)
```
source/
├── core/ # engine: audio, input, locale, rendering (+ sdl3gpu, sprite), resources, system
├── game/ # gameplay: entities, gameplay, scenes, ui, options
├── utils/ # color, param, utils
├── external/ # vendored third-party headers (json, fkyaml, stb_*)
└── main.cpp
```
`#include` paths are absolute relative to `source/` (e.g. `#include "core/audio/audio.hpp"`, `#include "game/scenes/logo.hpp"`). The CMake build adds a single `-I${CMAKE_SOURCE_DIR}/source`.
### Singletons (core systems)
- **Director** (`source/core/system/director.hpp`) — Application state machine, orchestrates scene transitions (Logo → Intro → Title → Game → Credits/HiScore → Title)
- **Screen** (`source/core/rendering/screen.hpp`) — Window management, SDL3 GPU rendering pipeline, post-processing effects
- **Resource** (`source/core/resources/resource.hpp`) — Asset loading/caching with PRELOAD and LAZY_LOAD modes, reads from `resources.pack`
- **Audio** (`source/core/audio/audio.hpp`) — Music and SFX management
- **Input** (`source/core/input/input.hpp`) — Keyboard and gamepad input handling
### Scenes (source/game/scenes/)
Each scene is a self-contained class with update/render lifecycle. Scene flow is managed by Director.
### Entity Managers
- `BalloonManager` / `BulletManager` — Object pool-based entity management
- `Player` — Two-player support (player 1: keyboard, player 2: gamepad)
- `BalloonManager` / `BulletManager` — Object pool-based entity management (`source/game/gameplay/`)
- `Player` — Two-player support (player 1: keyboard, player 2: gamepad) (`source/game/entities/`)
### Rendering Pipeline
- SDL3 GPU API (Vulkan/Metal/D3D12 backends)
- SPIR-V shaders compiled offline from GLSL (`data/shaders/`) via `glslc`
- Compiled shader headers embedded in `source/rendering/sdl3gpu/postfx_*_spv.h`
- Compiled shader headers embedded in `source/core/rendering/sdl3gpu/postfx_*_spv.h`
- macOS uses Metal (no SPIR-V compilation needed)
### Configuration
@@ -74,7 +86,9 @@ Each scene is a self-contained class with update/render lifecycle. Scene flow is
- Gamepad mappings: `config/gamecontrollerdb.txt`
### External Libraries (header-only/vendored in source/external/)
- nlohmann/json, fkyaml (YAML), stb_image, stb_vorbis, jail_audio
- nlohmann/json, fkyaml (YAML), stb_image, stb_vorbis
`jail_audio` lives in `source/core/audio/` and `gif.{hpp,cpp}` in `source/core/rendering/` — these are first-party, not third-party.
## Code Style

View File

@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10)
project(coffee_crisis_arcade_edition VERSION 2.00)
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
endif()
# Establecer estándar de C++
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -10,8 +15,13 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
find_package(Git QUIET)
if(GIT_FOUND)
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
# resuelve aquí ejecutando git directamente.
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
@@ -19,8 +29,10 @@ if(GIT_FOUND)
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
else()
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif()
# Configurar archivo de versión
@@ -28,100 +40,122 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
# --- 1. LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES
# --- Archivos Principales del Sistema ---
source/asset.cpp
source/audio.cpp
source/director.cpp
source/global_events.cpp
source/global_inputs.cpp
source/input.cpp
source/lang.cpp
source/main.cpp
source/param.cpp
source/resource.cpp
source/resource_helper.cpp
source/resource_loader.cpp
source/resource_pack.cpp
source/screen.cpp
source/text.cpp
source/writer.cpp
# --- UI (User Interface) ---
source/ui/menu_option.cpp
source/ui/menu_renderer.cpp
source/ui/notifier.cpp
source/ui/service_menu.cpp
source/ui/ui_message.cpp
source/ui/window_message.cpp
# --- core/audio ---
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# --- Lógica del Juego ---
source/balloon_formations.cpp
source/balloon_manager.cpp
source/balloon.cpp
source/bullet.cpp
source/bullet_manager.cpp
source/enter_name.cpp
source/explosions.cpp
source/game_logo.cpp
source/item.cpp
source/manage_hiscore_table.cpp
source/player.cpp
source/scoreboard.cpp
source/tabe.cpp
# --- core/input ---
source/core/input/define_buttons.cpp
source/core/input/global_inputs.cpp
source/core/input/input.cpp
source/core/input/input_types.cpp
source/core/input/mouse.cpp
# --- Escenas ---
source/sections/credits.cpp
source/sections/game.cpp
source/sections/hiscore_table.cpp
source/sections/instructions.cpp
source/sections/intro.cpp
source/sections/logo.cpp
source/sections/title.cpp
# --- core/locale ---
source/core/locale/lang.cpp
# --- Sprites y Gráficos ---
source/animated_sprite.cpp
source/background.cpp
source/card_sprite.cpp
source/fade.cpp
source/moving_sprite.cpp
source/path_sprite.cpp
source/smart_sprite.cpp
source/sprite.cpp
source/texture.cpp
source/tiled_bg.cpp
# --- core/rendering ---
source/core/rendering/background.cpp
source/core/rendering/fade.cpp
source/core/rendering/gif.cpp
source/core/rendering/screen.cpp
source/core/rendering/text.cpp
source/core/rendering/texture.cpp
source/core/rendering/tiled_bg.cpp
source/core/rendering/writer.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/card_sprite.cpp
source/core/rendering/sprite/moving_sprite.cpp
source/core/rendering/sprite/path_sprite.cpp
source/core/rendering/sprite/smart_sprite.cpp
source/core/rendering/sprite/sprite.cpp
# --- Otros ---
source/color.cpp
source/demo.cpp
source/define_buttons.cpp
source/difficulty.cpp
source/input_types.cpp
source/mouse.cpp
source/options.cpp
source/shutdown.cpp
source/stage.cpp
source/system_utils.cpp
source/utils.cpp
)
# --- core/resources ---
source/core/resources/asset.cpp
source/core/resources/asset_integrated.cpp
source/core/resources/resource.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_loader.cpp
source/core/resources/resource_pack.cpp
# Fuentes de librerías de terceros
set(EXTERNAL_SOURCES
source/external/jail_audio.cpp
source/external/json.hpp
source/external/gif.cpp
)
# --- core/system ---
source/core/system/demo.cpp
source/core/system/director.cpp
source/core/system/global_events.cpp
source/core/system/shutdown.cpp
source/core/system/system_utils.cpp
# Fuentes del sistema de renderizado
set(RENDERING_SOURCES
source/rendering/sdl3gpu/sdl3gpu_shader.cpp
# --- game ---
source/game/options.cpp
# --- game/entities ---
source/game/entities/balloon.cpp
source/game/entities/bullet.cpp
source/game/entities/explosions.cpp
source/game/entities/item.cpp
source/game/entities/player.cpp
source/game/entities/tabe.cpp
# --- game/gameplay ---
source/game/gameplay/balloon_formations.cpp
source/game/gameplay/balloon_manager.cpp
source/game/gameplay/bullet_manager.cpp
source/game/gameplay/difficulty.cpp
source/game/gameplay/enter_name.cpp
source/game/gameplay/game_logo.cpp
source/game/gameplay/manage_hiscore_table.cpp
source/game/gameplay/scoreboard.cpp
source/game/gameplay/stage.cpp
# --- game/scenes ---
source/game/scenes/credits.cpp
source/game/scenes/game.cpp
source/game/scenes/hiscore_table.cpp
source/game/scenes/instructions.cpp
source/game/scenes/intro.cpp
source/game/scenes/logo.cpp
source/game/scenes/preload.cpp
source/game/scenes/title.cpp
# --- game/ui ---
source/game/ui/menu_option.cpp
source/game/ui/menu_renderer.cpp
source/game/ui/notifier.cpp
source/game/ui/service_menu.cpp
source/game/ui/ui_message.cpp
source/game/ui/window_message.cpp
# --- utils ---
source/utils/color.cpp
source/utils/param.cpp
source/utils/utils.cpp
)
# Configuración de SDL3
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
if(EMSCRIPTEN)
# En Emscripten, SDL3 se compila desde source con FetchContent
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3 compilado desde source para Emscripten")
else()
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif()
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal) ---
if(NOT APPLE)
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADER_VERT_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.vert")
@@ -130,23 +164,40 @@ if(NOT APPLE)
set(SHADER_UPSCALE_SRC "${CMAKE_SOURCE_DIR}/data/shaders/upscale.frag")
set(SHADER_DOWNSCALE_SRC "${CMAKE_SOURCE_DIR}/data/shaders/downscale.frag")
set(SHADER_VERT_H "${CMAKE_SOURCE_DIR}/source/rendering/sdl3gpu/postfx_vert_spv.h")
set(SHADER_FRAG_H "${CMAKE_SOURCE_DIR}/source/rendering/sdl3gpu/postfx_frag_spv.h")
set(SHADER_CRTPI_H "${CMAKE_SOURCE_DIR}/source/rendering/sdl3gpu/crtpi_frag_spv.h")
set(SHADER_UPSCALE_H "${CMAKE_SOURCE_DIR}/source/rendering/sdl3gpu/upscale_frag_spv.h")
set(SHADER_DOWNSCALE_H "${CMAKE_SOURCE_DIR}/source/rendering/sdl3gpu/downscale_frag_spv.h")
set(SHADER_VERT_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/postfx_vert_spv.h")
set(SHADER_FRAG_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/postfx_frag_spv.h")
set(SHADER_CRTPI_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/crtpi_frag_spv.h")
set(SHADER_UPSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/upscale_frag_spv.h")
set(SHADER_DOWNSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/downscale_frag_spv.h")
set(ALL_SHADER_SOURCES "${SHADER_VERT_SRC}" "${SHADER_FRAG_SRC}" "${SHADER_CRTPI_SRC}" "${SHADER_UPSCALE_SRC}" "${SHADER_DOWNSCALE_SRC}")
set(ALL_SHADER_HEADERS "${SHADER_VERT_H}" "${SHADER_FRAG_H}" "${SHADER_CRTPI_H}" "${SHADER_UPSCALE_H}" "${SHADER_DOWNSCALE_H}")
if(GLSLC_EXE)
set(COMPILE_SHADER_SCRIPT "${CMAKE_SOURCE_DIR}/tools/shaders/compile_shader.cmake")
macro(add_shader SRC_FILE OUT_H VAR_NAME)
cmake_parse_arguments(S "" "STAGE" "" ${ARGN})
add_custom_command(
OUTPUT ${ALL_SHADER_HEADERS}
COMMAND "${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.sh"
DEPENDS ${ALL_SHADER_SOURCES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Compilando shaders SPIR-V..."
OUTPUT "${OUT_H}"
COMMAND ${CMAKE_COMMAND}
"-DGLSLC=${GLSLC_EXE}"
"-DSRC=${SRC_FILE}"
"-DOUT_H=${OUT_H}"
"-DVAR=${VAR_NAME}"
"-DSTAGE=${S_STAGE}"
-P "${COMPILE_SHADER_SCRIPT}"
DEPENDS "${SRC_FILE}" "${COMPILE_SHADER_SCRIPT}"
COMMENT "Compilando shader: ${VAR_NAME}"
)
endmacro()
add_shader("${SHADER_VERT_SRC}" "${SHADER_VERT_H}" "postfx_vert_spv")
add_shader("${SHADER_FRAG_SRC}" "${SHADER_FRAG_H}" "postfx_frag_spv")
add_shader("${SHADER_CRTPI_SRC}" "${SHADER_CRTPI_H}" "crtpi_frag_spv" STAGE fragment)
add_shader("${SHADER_UPSCALE_SRC}" "${SHADER_UPSCALE_H}" "upscale_frag_spv")
add_shader("${SHADER_DOWNSCALE_SRC}" "${SHADER_DOWNSCALE_H}" "downscale_frag_spv")
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
message(STATUS "glslc encontrado: shaders se compilarán automáticamente")
else()
@@ -163,22 +214,30 @@ if(NOT APPLE)
message(STATUS "glslc no encontrado - usando headers SPIR-V precompilados")
endif()
else()
if(EMSCRIPTEN)
message(STATUS "Emscripten: shaders SPIR-V omitidos (SDL3 GPU no soportado en WebGL2)")
else()
message(STATUS "macOS: shaders SPIR-V omitidos (usa Metal)")
endif()
endif()
# --- 2. AÑADIR EJECUTABLE ---
add_executable(${PROJECT_NAME} ${APP_SOURCES} ${EXTERNAL_SOURCES} ${RENDERING_SOURCES})
if(EMSCRIPTEN)
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2)
set(APP_SOURCES_WASM ${APP_SOURCES})
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
endif()
if(NOT APPLE AND GLSLC_EXE)
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
# --- 3. DIRECTORIOS DE INCLUSIÓN ---
target_include_directories(${PROJECT_NAME} PUBLIC
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_SOURCE_DIR}/source/external"
"${CMAKE_SOURCE_DIR}/source/rendering"
"${CMAKE_SOURCE_DIR}/source/rendering/sdl3gpu"
"${CMAKE_BINARY_DIR}"
)
@@ -204,18 +263,50 @@ if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 mingw32)
elseif(APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES "arm64")
endif()
if(MACOS_BUNDLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUNDLE)
target_link_options(${PROJECT_NAME} PRIVATE
-framework SDL3
-F ${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64
-rpath @executable_path/../Frameworks/
)
endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# -fexceptions: habilita excepciones C++ (fkyaml, std::runtime_error...) — sin esto cualquier throw llama a abort()
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/config@/config"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY solo para permitir emscripten_sleep(0) durante la precarga de recursos
# (el bucle principal del juego ya usa SDL3 Callback API, no depende de ASYNCIFY).
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif()
# Especificar la ubicación del ejecutable
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
if(EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
else()
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# --- 5. STATIC ANALYSIS TARGETS ---
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format)
find_program(CPPCHECK_EXE NAMES cppcheck)
# Recopilar todos los archivos fuente, excluyendo external/
file(GLOB_RECURSE ALL_SOURCE_FILES
@@ -225,6 +316,13 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
)
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
# Si pasamos .hpp como TUs independientes, cppcheck reporta falsos positivos de
# 'unusedStructMember' porque no hace análisis cross-TU y ve miembros de clase
# cuyo uso vive en un .cpp distinto.
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
# Targets de clang-tidy
if(CLANG_TIDY_EXE)
# En macOS con clang-tidy de Homebrew LLVM, es necesario pasar el sysroot
@@ -281,3 +379,57 @@ if(CLANG_FORMAT_EXE)
else()
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
endif()
# Targets de cppcheck
if(CPPCHECK_EXE)
add_custom_target(cppcheck
COMMAND ${CPPCHECK_EXE}
--enable=warning,style,performance,portability
--std=c++20
--language=c++
--inline-suppr
--suppress=missingIncludeSystem
--suppress=toomanyconfigs
-D_DEBUG
-DLINUX_BUILD
--quiet
-I ${CMAKE_SOURCE_DIR}/source
${CPPCHECK_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running cppcheck..."
)
else()
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
endif()
# --- EINA STANDALONE: pack_resources ---
# Executable auxiliar que empaqueta `data/` a `resources.pack`.
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
# cmake --build build --target pack_resources
# Després executar: ./build/pack_resources data resources.pack
if(NOT EMSCRIPTEN)
add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
endif()

666
Makefile
View File

@@ -1,233 +1,217 @@
# Directorios
# ==============================================================================
# DIRECTORIES
# ==============================================================================
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
DIR_BUILD := $(addsuffix /, $(DIR_ROOT)build)
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
# Variables
# ==============================================================================
# TARGET NAMES
# ==============================================================================
TARGET_NAME := coffee_crisis_arcade_edition
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
ifeq ($(OS),Windows_NT)
TARGET_FILE := $(DIR_ROOT)$(TARGET_NAME).exe
else
TARGET_FILE := $(DIR_ROOT)$(TARGET_NAME)
endif
APP_NAME := Coffee Crisis Arcade Edition
DIST_DIR := dist
RELEASE_FOLDER := dist/_tmp
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
RESOURCE_FILE := release/windows/coffee.res
# Variables para herramienta de empaquetado
# ==============================================================================
# TOOLS
# ==============================================================================
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
ifeq ($(OS),Windows_NT)
PACK_TOOL := $(DIR_TOOLS)pack_resources/pack_resources.exe
PACK_CXX := $(CXX)
GLSLC := $(shell where glslc 2>NUL)
else
PACK_TOOL := $(DIR_TOOLS)pack_resources/pack_resources
PACK_CXX := $(CXX)
endif
PACK_SOURCES := $(DIR_TOOLS)pack_resources/pack_resources.cpp $(DIR_SOURCES)resource_pack.cpp
PACK_INCLUDES := -I$(DIR_ROOT) -I$(DIR_BUILD)
# Versión automática basada en la fecha actual (específica por SO)
ifeq ($(OS),Windows_NT)
VERSION := $(shell powershell -Command "Get-Date -Format 'yyyy-MM-dd'")
else
VERSION := $(shell date +%Y-%m-%d)
GLSLC := $(shell command -v glslc 2>/dev/null)
endif
# Variables específicas para Windows (usando APP_NAME)
# ==============================================================================
# VERSION (extraída de defines.hpp)
# ==============================================================================
ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else
VERSION := $(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif
# Hash del commit actual, computado en el host. Se pasa a CMake via -DGIT_HASH
# para que el build en docker/emscripten no falle por "dubious ownership" de Git.
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif
# ==============================================================================
# SHELL (Windows usa cmd.exe)
# ==============================================================================
ifeq ($(OS),Windows_NT)
SHELL := cmd.exe
endif
# ==============================================================================
# WINDOWS-SPECIFIC VARIABLES
# ==============================================================================
ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_ROOT)$(APP_NAME)
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
else
WIN_TARGET_FILE := $(TARGET_FILE)
WIN_RELEASE_FILE := $(RELEASE_FILE)
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
endif
# Nombres para los ficheros de lanzamiento
# ==============================================================================
# RELEASE NAMES
# ==============================================================================
WINDOWS_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-win32-x64.zip
MACOS_INTEL_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-intel.dmg
MACOS_APPLE_SILICON_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-apple-silicon.dmg
LINUX_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux.tar.gz
RASPI_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-raspberry.tar.gz
# Lista completa de archivos fuente (basada en CMakeLists.txt)
APP_SOURCES := \
source/asset.cpp \
source/audio.cpp \
source/director.cpp \
source/global_events.cpp \
source/global_inputs.cpp \
source/input.cpp \
source/lang.cpp \
source/main.cpp \
source/param.cpp \
source/resource.cpp \
source/resource_helper.cpp \
source/resource_loader.cpp \
source/resource_pack.cpp \
source/screen.cpp \
source/text.cpp \
source/writer.cpp \
source/ui/menu_option.cpp \
source/ui/menu_renderer.cpp \
source/ui/notifier.cpp \
source/ui/service_menu.cpp \
source/ui/ui_message.cpp \
source/ui/window_message.cpp \
source/balloon_formations.cpp \
source/balloon_manager.cpp \
source/balloon.cpp \
source/bullet.cpp \
source/bullet_manager.cpp \
source/enter_name.cpp \
source/explosions.cpp \
source/game_logo.cpp \
source/item.cpp \
source/manage_hiscore_table.cpp \
source/player.cpp \
source/scoreboard.cpp \
source/tabe.cpp \
source/sections/credits.cpp \
source/sections/game.cpp \
source/sections/hiscore_table.cpp \
source/sections/instructions.cpp \
source/sections/intro.cpp \
source/sections/logo.cpp \
source/sections/title.cpp \
source/animated_sprite.cpp \
source/background.cpp \
source/card_sprite.cpp \
source/fade.cpp \
source/moving_sprite.cpp \
source/path_sprite.cpp \
source/smart_sprite.cpp \
source/sprite.cpp \
source/texture.cpp \
source/tiled_bg.cpp \
source/color.cpp \
source/demo.cpp \
source/define_buttons.cpp \
source/difficulty.cpp \
source/input_types.cpp \
source/mouse.cpp \
source/options.cpp \
source/shutdown.cpp \
source/stage.cpp \
source/system_utils.cpp \
source/utils.cpp \
source/external/jail_audio.cpp \
source/external/gif.cpp \
source/rendering/sdl3gpu/sdl3gpu_shader.cpp
# Includes
INCLUDES := -Isource -Isource/external -Isource/rendering -Isource/rendering/sdl3gpu -I$(DIR_BUILD)
# Variables según el sistema operativo
# ==============================================================================
# PLATAFORMA
# ==============================================================================
ifeq ($(OS),Windows_NT)
FixPath = $(subst /,\\,$1)
CXXFLAGS := -std=c++20 -Wall -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread -Wl,-Bdynamic -Wl,-subsystem,windows -DWINDOWS_BUILD
CXXFLAGS_DEBUG := -std=c++20 -Wall -g -D_DEBUG -DWINDOWS_BUILD
LDFLAGS := -lmingw32 -lws2_32 -lSDL3
RM := del /Q
MKDIR := mkdir
else
FixPath = $1
CXXFLAGS := -std=c++20 -Wall -Os -ffunction-sections -fdata-sections
CXXFLAGS_DEBUG := -std=c++20 -Wall -g -D_DEBUG
LDFLAGS := -lSDL3
RMFILE := rm -f
RMDIR := rm -rdf
MKDIR := mkdir -p
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
CXXFLAGS += -DLINUX_BUILD
endif
ifeq ($(UNAME_S),Darwin)
CXXFLAGS += -DMACOS_BUILD
CXXFLAGS_DEBUG += -DMACOS_BUILD
# Configurar arquitectura (por defecto arm64, como en CMake)
CXXFLAGS += -arch arm64
CXXFLAGS_DEBUG += -arch arm64
endif
endif
# Reglas para herramienta de empaquetado y resources.pack
$(PACK_TOOL): FORCE
@echo "Compilando herramienta de empaquetado..."
$(PACK_CXX) -std=c++20 -Wall -Os $(PACK_INCLUDES) $(PACK_SOURCES) -o $(PACK_TOOL)
@echo "✓ Herramienta de empaquetado lista: $(PACK_TOOL)"
# ==============================================================================
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
# ==============================================================================
ifeq ($(OS),Windows_NT)
CMAKE_GEN := -G "MinGW Makefiles"
else
CMAKE_GEN :=
endif
pack_tool: $(PACK_TOOL)
# ==============================================================================
# COMPILACIÓN CON CMAKE
# ==============================================================================
all:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
resources.pack: $(PACK_TOOL)
@echo "Generando resources.pack desde directorio data/..."
$(PACK_TOOL) data resources.pack
@echo "✓ resources.pack generado exitosamente"
debug:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug
@cmake --build build
# Reglas para compilación
windows:
@echo off
@echo Compilando para Windows con nombre: "$(APP_NAME).exe"
windres release/windows/coffee.rc -O coff -o $(RESOURCE_FILE)
$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE).exe"
strip -s -R .comment -R .gnu.version "$(WIN_TARGET_FILE).exe" --strip-unneeded
# ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO)
# ==============================================================================
release:
ifeq ($(OS),Windows_NT)
@"$(MAKE)" _windows_release
else
ifeq ($(UNAME_S),Darwin)
@$(MAKE) _macos_release
else
@$(MAKE) _linux_release
endif
endif
windows_rec:
@echo off
@echo Compilando version de grabacion para Windows: "$(APP_NAME)_rec.exe"
$(CXX) $(APP_SOURCES) $(INCLUDES) -DRECORDING $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE)_rec.exe"
# ==============================================================================
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
# ==============================================================================
pack:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data resources.pack
windows_debug:
@echo off
@echo Compilando version debug para Windows: "$(APP_NAME)_debug.exe"
$(CXX) $(APP_SOURCES) $(INCLUDES) -DDEBUG -DVERBOSE $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(WIN_TARGET_FILE)_debug.exe"
# ==============================================================================
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
# ==============================================================================
compile_shaders:
ifdef GLSLC
@cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
else
@echo "glslc no encontrado - asegurate de que los headers SPIR-V precompilados existen"
endif
windows_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
# ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE)
# ==============================================================================
_windows_release:
@$(MAKE) pack
@echo off
@echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
powershell if (-not (Test-Path "$(DIST_DIR)")) {New-Item "$(DIST_DIR)" -ItemType Directory}
powershell if (Test-Path "$(RELEASE_FOLDER)") {Remove-Item "$(RELEASE_FOLDER)" -Recurse -Force}
powershell if (-not (Test-Path "$(RELEASE_FOLDER)")) {New-Item "$(RELEASE_FOLDER)" -ItemType Directory}
@powershell -Command "if (-not (Test-Path '$(DIST_DIR)')) {New-Item '$(DIST_DIR)' -ItemType Directory}"
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia la carpeta 'config' y el archivo 'resources.pack'
powershell Copy-Item -Path "config" -Destination "$(RELEASE_FOLDER)" -recurse -Force
powershell Copy-Item -Path "resources.pack" -Destination "$(RELEASE_FOLDER)"
@powershell -Command "Copy-Item -Path 'config' -Destination '$(RELEASE_FOLDER)' -recurse -Force"
@powershell -Command "Copy-Item -Path 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
# Copia los ficheros que estan en la raíz del proyecto
powershell Copy-Item "LICENSE" -Destination "$(RELEASE_FOLDER)"
powershell Copy-Item "README.md" -Destination "$(RELEASE_FOLDER)"
powershell Copy-Item "release\windows\dll\*.dll" -Destination "$(RELEASE_FOLDER)"
# Compila
windres release/windows/coffee.rc -O coff -o $(RESOURCE_FILE)
$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) -DRELEASE_BUILD $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_RELEASE_FILE).exe"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip
powershell if (Test-Path "$(WINDOWS_RELEASE)") {Remove-Item "$(WINDOWS_RELEASE)"}
powershell Compress-Archive -Path "$(RELEASE_FOLDER)"/* -DestinationPath "$(WINDOWS_RELEASE)"
@powershell -Command "if (Test-Path '$(WINDOWS_RELEASE)') {Remove-Item '$(WINDOWS_RELEASE)'}"
@powershell -Command "Compress-Archive -Path '$(RELEASE_FOLDER)/*' -DestinationPath '$(WINDOWS_RELEASE)'"
@echo Release creado: $(WINDOWS_RELEASE)
# Elimina la carpeta temporal 'RELEASE_FOLDER'
powershell if (Test-Path "$(RELEASE_FOLDER)") {Remove-Item "$(RELEASE_FOLDER)" -Recurse -Force}
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
macos:
@echo "Compilando para macOS: $(TARGET_NAME)"
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
macos_debug:
@echo "Compilando version debug para macOS: $(TARGET_NAME)_debug"
$(CXX) $(APP_SOURCES) $(INCLUDES) -DDEBUG -DVERBOSE $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(TARGET_FILE)_debug"
macos_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
# ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE)
# ==============================================================================
_macos_release:
@$(MAKE) pack
@echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
# con brew; si brew tampoco está, indica el comando exacto al usuario.
@command -v create-dmg >/dev/null 2>&1 || { \
echo ""; \
echo "============================================"; \
echo " Falta la dependencia: create-dmg"; \
echo "============================================"; \
if command -v brew >/dev/null 2>&1; then \
echo " Instalando con: brew install create-dmg"; \
brew install create-dmg || { \
echo ""; \
echo " ERROR: 'brew install create-dmg' ha fallado."; \
echo " Ejecuta el comando manualmente y vuelve a probar."; \
exit 1; \
}; \
else \
echo " Homebrew no está instalado."; \
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
echo " brew install create-dmg"; \
exit 1; \
fi; \
}
# Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)"
@@ -251,15 +235,26 @@ macos_release:
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
# Compila la versión para procesadores Intel
ifdef ENABLE_MACOS_X86_64
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DMACOS_BUILD -DRELEASE_BUILD -std=c++20 -Wall -Os -Wno-deprecated -framework SDL3 -F release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64 -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target x86_64-apple-macos10.15
# Actualiza versión en Info.plist
@echo "Actualizando Info.plist con versión $(VERSION)..."
@RAW_VERSION=$$(echo "$(VERSION)" | sed 's/^v//'); \
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
# Empaqueta el .dmg de la versión Intel con create-dmg
@echo "Creando DMG Intel con iconos de 96x96..."
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
# DMG Intel y continúa con la build de Apple Silicon).
@echo ""
@echo "============================================"
@echo " Compilando version Intel (x86_64)"
@echo "============================================"
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
-DMACOS_BUNDLE=ON \
&& cmake --build build/intel; then \
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
echo "Creando DMG Intel con iconos de 96x96..."; \
create-dmg \
--volname "$(APP_NAME)" \
--window-pos 200 120 \
@@ -272,12 +267,26 @@ ifdef ENABLE_MACOS_X86_64
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
endif
"$(RELEASE_FOLDER)" || true; \
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
else \
echo ""; \
echo "============================================"; \
echo " WARNING: la build Intel ha fallado."; \
echo " Se omite el DMG Intel y se continúa con"; \
echo " la build de Apple Silicon."; \
echo "============================================"; \
echo ""; \
fi
# Compila la versión para procesadores Apple Silicon
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DMACOS_BUILD -DRELEASE_BUILD -DSDL_DISABLE_IMMINTRIN_H -std=c++20 -Wall -Os -Wno-deprecated -framework SDL3 -F release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64 -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos11
# Compila la versión para procesadores Apple Silicon con cmake
@echo ""
@echo "============================================"
@echo " Compilando version Apple Silicon (arm64)"
@echo "============================================"
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
@cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
@@ -301,22 +310,22 @@ endif
# Elimina las carpetas temporales
$(RMDIR) "$(RELEASE_FOLDER)"
$(RMDIR) build/intel
$(RMDIR) build/arm
$(RMFILE) "$(DIST_DIR)"/rw.*
linux:
@echo "Compilando para Linux: $(TARGET_NAME)"
$(CXX) $(APP_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
strip -s -R .comment -R .gnu.version "$(TARGET_FILE)" --strip-unneeded
linux_debug:
@echo "Compilando version debug para Linux: $(TARGET_NAME)_debug"
$(CXX) $(APP_SOURCES) $(INCLUDES) -DDEBUG -DVERBOSE $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(TARGET_FILE)_debug"
linux_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
# ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
_linux_release:
@$(MAKE) pack
@echo "Creando release para Linux - Version: $(VERSION)"
# Elimina carpetas previas y recrea (crea dist/ si no existe)
# Compila con cmake
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)"
$(MKDIR) "$(RELEASE_FOLDER)"
@@ -325,9 +334,7 @@ linux_release:
cp resources.pack "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
# Compila
$(CXX) $(APP_SOURCES) $(INCLUDES) -DRELEASE_BUILD $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FILE)"
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded
# Empaqueta ficheros
@@ -338,189 +345,108 @@ linux_release:
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
linux_release_desktop:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
@echo "Creando release con integracion desktop para Linux - Version: $(VERSION)"
# Elimina carpetas previas y recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)"
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
# ==============================================================================
wasm:
@echo "Compilando para WebAssembly - Version: $(VERSION) ($(GIT_HASH))"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
$(MKDIR) "$(DIST_DIR)/wasm"
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/coffee-crisis-arcade-edition/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Crea la estructura de directorios estándar para Linux
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)"
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin"
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications"
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps"
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)"
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm_debug/.
wasm_debug:
@echo "Compilando WebAssembly Debug - Version: $(VERSION) ($(GIT_HASH))"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm_debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm_debug"
$(MKDIR) "$(DIST_DIR)/wasm_debug"
cp build/wasm_debug/$(TARGET_NAME).html $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).js $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).data $(DIST_DIR)/wasm_debug/
@echo "Output: $(DIST_DIR)/wasm_debug/"
# Copia ficheros del juego
cp -R config "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)/"
cp resources.pack "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)/"
cp LICENSE "$(RELEASE_FOLDER)/$(TARGET_NAME)/"
cp README.md "$(RELEASE_FOLDER)/$(TARGET_NAME)/"
# ==============================================================================
# CODE QUALITY (delegados a cmake)
# ==============================================================================
format:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build --target format
# Compila el ejecutable
$(CXX) $(APP_SOURCES) $(INCLUDES) -DRELEASE_BUILD $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin/$(TARGET_NAME)"
strip -s -R .comment -R .gnu.version "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin/$(TARGET_NAME)" --strip-unneeded
format-check:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build --target format-check
# Crea el archivo .desktop
@echo '[Desktop Entry]' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Version=1.0' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Type=Application' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Name=$(APP_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Comment=Arcade action game - defend Earth from alien invasion!' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Exec=/opt/$(TARGET_NAME)/bin/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Icon=$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Path=/opt/$(TARGET_NAME)/share/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Terminal=false' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'StartupNotify=true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Categories=Game;ArcadeGame;' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
@echo 'Keywords=arcade;action;shooter;retro;' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
tidy:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build --target tidy
# Copia el icono (si existe) y lo redimensiona si es necesario
@if [ -f "release/icons/icon.png" ]; then \
if command -v magick >/dev/null 2>&1; then \
magick "release/icons/icon.png" -resize 256x256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
echo "Icono redimensionado de release/icons/icon.png (usando ImageMagick)"; \
elif command -v convert >/dev/null 2>&1; then \
convert "release/icons/icon.png" -resize 256x256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
echo "Icono redimensionado de release/icons/icon.png (usando ImageMagick legacy)"; \
elif command -v ffmpeg >/dev/null 2>&1; then \
ffmpeg -i "release/icons/icon.png" -vf scale=256:256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png" -y -loglevel quiet; \
echo "Icono redimensionado de release/icons/icon.png (usando ffmpeg)"; \
else \
cp "release/icons/icon.png" "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
echo "Icono copiado sin redimensionar (instalar ImageMagick o ffmpeg para redimensionado automatico)"; \
fi; \
else \
echo "Advertencia: No se encontró release/icons/icon.png - crear icono manualmente"; \
fi
tidy-fix:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build --target tidy-fix
# Crea script de instalación
@echo '#!/bin/bash' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'echo "Instalando $(APP_NAME)..."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo mkdir -p /opt/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo cp -R bin /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo cp -R share /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo cp LICENSE /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo cp README.md /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo mkdir -p /usr/share/applications' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo mkdir -p /usr/share/icons/hicolor/256x256/apps' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo cp /opt/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop /usr/share/applications/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo cp /opt/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png /usr/share/icons/hicolor/256x256/apps/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo update-desktop-database /usr/share/applications 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'sudo gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'echo "$(APP_NAME) instalado correctamente!"' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
@echo 'echo "Ya puedes encontrarlo en el menu de aplicaciones en la categoria Juegos."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
chmod +x "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build --target cppcheck
# Crea script de desinstalación
@echo '#!/bin/bash' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'echo "Desinstalando $(APP_NAME)..."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'sudo rm -rf /opt/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'sudo rm -f /usr/share/applications/$(TARGET_NAME).desktop' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'sudo rm -f /usr/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'sudo update-desktop-database /usr/share/applications 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'sudo gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
@echo 'echo "$(APP_NAME) desinstalado correctamente."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
chmod +x "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
# ==============================================================================
# DESCARGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# Empaqueta ficheros
$(RMFILE) "$(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz"
tar -czvf "$(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz" -C "$(RELEASE_FOLDER)" .
@echo "Release con integracion desktop creado: $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz"
@echo "Para instalar: extraer y ejecutar ./$(TARGET_NAME)/install.sh"
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
raspi:
@echo "Compilando para Raspberry Pi: $(TARGET_NAME)"
$(CXX) $(APP_SOURCES) $(INCLUDES) -DVERBOSE $(CXXFLAGS) $(LDFLAGS) -o $(TARGET_FILE)
strip -s -R .comment -R .gnu.version $(TARGET_FILE) --strip-unneeded
raspi_debug:
@echo "Compilando version debug para Raspberry Pi: $(TARGET_NAME)_debug"
$(CXX) $(APP_SOURCES) $(INCLUDES) -DVERBOSE -DDEBUG $(CXXFLAGS_DEBUG) $(LDFLAGS) -o "$(TARGET_FILE)_debug"
raspi_release:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
@echo "Creando release para Raspberry Pi - Version: $(VERSION)"
# Elimina carpetas previas y recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)"
$(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros
cp -R config "$(RELEASE_FOLDER)"
cp resources.pack "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
# Compila
$(CXX) $(APP_SOURCES) $(INCLUDES) -DRELEASE_BUILD -DVERBOSE $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FILE)"
strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded
# Empaqueta ficheros
$(RMFILE) "$(RASPI_RELEASE)"
tar -czvf "$(RASPI_RELEASE)" -C "$(RELEASE_FOLDER)" .
@echo "Release creado: $(RASPI_RELEASE)"
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
anbernic:
@$(MAKE) pack_tool
@$(MAKE) resources.pack
@echo "Compilando para Anbernic: $(TARGET_NAME)"
# Elimina carpetas previas
$(RMDIR) "$(RELEASE_FOLDER)"_anbernic
# Crea la carpeta temporal para realizar el lanzamiento
$(MKDIR) "$(RELEASE_FOLDER)"_anbernic
# Copia ficheros
cp -R config "$(RELEASE_FOLDER)"_anbernic
cp resources.pack "$(RELEASE_FOLDER)"_anbernic
# Compila
$(CXX) $(APP_SOURCES) $(INCLUDES) -DRELEASE_BUILD -DANBERNIC -DNO_SHADERS -DARCADE -DVERBOSE $(CXXFLAGS) $(LDFLAGS) -o $(RELEASE_FOLDER)_anbernic/$(TARGET_NAME)
# Opción para deshabilitar audio (equivalente a la opción DISABLE_AUDIO de CMake)
no_audio:
@echo "Compilando sin audio: $(TARGET_NAME)_no_audio"
$(CXX) $(filter-out source/external/jail_audio.cpp,$(APP_SOURCES)) $(INCLUDES) -DNO_AUDIO $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)_no_audio"
# Regla para mostrar la versión actual
# ==============================================================================
# REGLAS ESPECIALES
# ==============================================================================
show_version:
@echo "Version actual: $(VERSION)"
# Regla de ayuda
help:
@echo "Makefile para Coffee Crisis Arcade Edition"
@echo "Comandos disponibles:"
@echo " windows - Compilar para Windows"
@echo " windows_debug - Compilar debug para Windows"
@echo " windows_release - Crear release completo para Windows"
@echo " linux - Compilar para Linux"
@echo " linux_debug - Compilar debug para Linux"
@echo " linux_release - Crear release basico para Linux"
@echo " linux_release_desktop - Crear release con integracion desktop para Linux"
@echo " macos - Compilar para macOS"
@echo " macos_debug - Compilar debug para macOS"
@echo " macos_release - Crear release completo para macOS"
@echo " raspi - Compilar para Raspberry Pi"
@echo " raspi_release - Crear release completo para Raspberry Pi"
@echo " anbernic - Compilar para Anbernic"
@echo " no_audio - Compilar sin sistema de audio"
@echo " pack_tool - Compilar herramienta de empaquetado"
@echo " resources.pack - Generar pack de recursos desde data/"
@echo " show_version - Mostrar version actual ($(VERSION))"
@echo " help - Mostrar esta ayuda"
@echo ""
@echo " Compilacion:"
@echo " make - Compilar con cmake (Release)"
@echo " make debug - Compilar con cmake (Debug)"
@echo ""
@echo " Release:"
@echo " make release - Crear release (detecta SO automaticamente)"
@echo " make wasm - Crear build WebAssembly (requiere Docker) en dist/wasm"
@echo " make wasm_debug - Build WebAssembly Debug local (sin deploy)"
@echo ""
@echo " Herramientas:"
@echo " make compile_shaders - Compilar shaders SPIR-V"
@echo " make pack - Empaquetar recursos a resources.pack"
@echo ""
@echo " Calidad de codigo:"
@echo " make format - Formatear codigo con clang-format"
@echo " make format-check - Verificar formato sin modificar"
@echo " make tidy - Analisis estatico con clang-tidy"
@echo " make tidy-fix - Analisis estatico con auto-fix"
@echo " make cppcheck - Analisis estatico con cppcheck"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda"
spirv:
@echo "Compilando shaders SPIR-V..."
tools/shaders/compile_spirv.sh
.PHONY: windows windows_rec windows_debug windows_release macos macos_debug macos_release linux linux_debug linux_release linux_release_desktop raspi raspi_debug raspi_release anbernic no_audio show_version help pack_tool resources.pack spirv
FORCE:
.PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug pack compile_shaders format format-check tidy tidy-fix cppcheck controllerdb show_version help

View File

@@ -72,6 +72,7 @@
"[NOTIFICATIONS] DISCONNECTED": "desconectat",
"[RESOURCE] LOADING": "Carregant",
"[RESOURCE] PRESS_TO_CONTINUE": "Prem una tecla per continuar",
"[SERVICE_MENU] TITLE": "Menu de servei",
"[SERVICE_MENU] RESET": "Reiniciar",
@@ -85,6 +86,7 @@
"[SERVICE_MENU] SUPERSAMPLING": "Supermostreig",
"[SERVICE_MENU] VSYNC": "Sincronisme vertical",
"[SERVICE_MENU] INTEGER_SCALE": "Escalat sencer",
"[SERVICE_MENU] FILTER": "Filtre",
"[SERVICE_MENU] MAIN_VOLUME": "Volumen general",
"[SERVICE_MENU] MUSIC_VOLUME": "Volumen de la musica",
"[SERVICE_MENU] SFX_VOLUME": "Volumen dels sons",

View File

@@ -71,6 +71,7 @@
"[NOTIFICATIONS] DISCONNECTED": "disconnected",
"[RESOURCE] LOADING": "Loading",
"[RESOURCE] PRESS_TO_CONTINUE": "Press any key to continue",
"[SERVICE_MENU] TITLE": "Service Menu",
"[SERVICE_MENU] RESET": "Reset",
@@ -84,6 +85,7 @@
"[SERVICE_MENU] SUPERSAMPLING": "Supersampling",
"[SERVICE_MENU] VSYNC": "V-Sync",
"[SERVICE_MENU] INTEGER_SCALE": "Integer Scale",
"[SERVICE_MENU] FILTER": "Filter",
"[SERVICE_MENU] MAIN_VOLUME": "Main Volume",
"[SERVICE_MENU] MUSIC_VOLUME": "Music Volume",
"[SERVICE_MENU] SFX_VOLUME": "Sound Volume",

View File

@@ -71,6 +71,7 @@
"[NOTIFICATIONS] DISCONNECTED": "desconectado",
"[RESOURCE] LOADING": "Cargando",
"[RESOURCE] PRESS_TO_CONTINUE": "Pulsa una tecla para continuar",
"[SERVICE_MENU] TITLE": "Menu de servicio",
"[SERVICE_MENU] RESET": "Reiniciar",
@@ -84,6 +85,7 @@
"[SERVICE_MENU] SUPERSAMPLING": "Supersampling",
"[SERVICE_MENU] VSYNC": "Sincronismo vertical",
"[SERVICE_MENU] INTEGER_SCALE": "Escalado proporcional",
"[SERVICE_MENU] FILTER": "Filtro",
"[SERVICE_MENU] MAIN_VOLUME": "Volumen general",
"[SERVICE_MENU] MUSIC_VOLUME": "Volumen de la musica",
"[SERVICE_MENU] SFX_VOLUME": "Volumen de los efectos",

2232
gamecontrollerdb.txt Normal file

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,150 +0,0 @@
#include "audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
#include <algorithm> // Para clamp
#include "external/jail_audio.h" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
#include "options.hpp" // Para AudioOptions, audio, MusicOptions
#include "resource.hpp" // Para Resource
#include "ui/logger.hpp" // Para logger
// Singleton
Audio* Audio::instance = nullptr;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia
void Audio::destroy() { delete Audio::instance; }
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
// Constructor
Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
JA_Quit();
}
// Método principal
void Audio::update() {
JA_Update();
}
// Reproduce la música
void Audio::playMusic(const std::string& name, const int loop) {
music_.name = name;
music_.loop = (loop != 0);
if (music_enabled_ && music_.state != MusicState::PLAYING) {
JA_PlayMusic(Resource::get()->getMusic(name), loop);
music_.state = MusicState::PLAYING;
}
}
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
music_.state = MusicState::PAUSED;
}
}
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
music_.state = MusicState::PLAYING;
}
}
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
music_.state = MusicState::STOPPED;
}
}
// Reproduce un sonido
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(Resource::get()->getSound(name), 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
JA_StopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
JA_FadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
JA_Music_state ja_state = JA_GetMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_DISABLED:
default:
return MusicState::STOPPED;
}
}
// Establece el volumen de los sonidos
void Audio::setSoundVolume(int sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = (sound_volume / 100.0F) * (Options::audio.volume / 100.0F);
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
}
// Establece el volumen de la música
void Audio::setMusicVolume(int music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = (music_volume / 100.0F) * (Options::audio.volume / 100.0F);
JA_SetMusicVolume(CONVERTED_VOLUME);
}
}
// Aplica la configuración
void Audio::applySettings() {
enable(Options::audio.enabled);
}
// Establecer estado general
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
}
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
Logger::info("Audio system initialized successfully");
}
}

View File

@@ -1,111 +0,0 @@
#pragma once
#include <string> // Para string
#include <utility> // Para move
// --- Clase Audio: gestor de audio (singleton) ---
class Audio {
public:
// --- Enums ---
enum class Group : int {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr int MAX_VOLUME = 100; // Volumen máximo
static constexpr int MIN_VOLUME = 0; // Volumen mínimo
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
// --- Métodos de singleton ---
static void init(); // Inicializa el objeto Audio
static void destroy(); // Libera el objeto Audio
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
// --- Método principal ---
static void update();
// --- Control de Música ---
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de Sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual
void stopAllSounds() const; // Detener todos los sonidos
// --- Configuración General ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
void applySettings(); // Aplica la configuración
// --- Configuración de Sonidos ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
// --- Configuración de Música ---
void enableMusic() { music_enabled_ = true; } // Habilitar música
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Control de Volumen ---
void setSoundVolume(int volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(int volume) const; // Ajustar volumen de música
// --- Getters para debug ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState; // Consulta directamente a jailaudio
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private:
// --- Estructuras privadas ---
struct Music {
MusicState state; // Estado actual de la música (reproduciendo, detenido, en pausa)
std::string name; // Última pista de música reproducida
bool loop; // Indica si la última pista de música se debe reproducir en bucle
// Constructor para inicializar la música con valores predeterminados
Music()
: state(MusicState::STOPPED),
loop(false) {}
// Constructor para inicializar con valores específicos
Music(MusicState init_state, std::string init_name, bool init_loop)
: state(init_state),
name(std::move(init_name)),
loop(init_loop) {}
};
// --- Variables de estado ---
Music music_; // Estado de la música
bool enabled_ = true; // Estado general del audio
bool sound_enabled_ = true; // Estado de los efectos de sonido
bool music_enabled_ = true; // Estado de la música
// --- Métodos internos ---
void initSDLAudio(); // Inicializa SDL Audio
// --- Constructores y destructor privados (singleton) ---
Audio(); // Constructor privado
~Audio(); // Destructor privado
// --- Instancia singleton ---
static Audio* instance; // Instancia única de Audio
};

212
source/core/audio/audio.cpp Normal file
View File

@@ -0,0 +1,212 @@
#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.c"
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para JA_*
#include "game/options.hpp" // Para Options::audio
// Singleton
Audio* Audio::instance = nullptr;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia
void Audio::destroy() {
delete Audio::instance;
Audio::instance = nullptr;
}
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
// Constructor
Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
JA_Quit();
}
// Método principal
void Audio::update() {
JA_Update();
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
// Reproduce la música por nombre (con crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
return;
}
if (!music_enabled_) return;
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) return;
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
}
JA_PlayMusic(resource, loop);
}
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Reproduce la música por puntero (con crossfade opcional)
void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) return;
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
}
JA_PlayMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
music_.loop = (loop != 0);
music_.state = MusicState::PLAYING;
}
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
music_.state = MusicState::PAUSED;
}
}
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
music_.state = MusicState::PLAYING;
}
}
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
music_.state = MusicState::STOPPED;
}
}
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const {
if (sound_enabled_ && sound != nullptr) {
JA_PlaySound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
JA_StopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
JA_FadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
JA_Music_state ja_state = JA_GetMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_DISABLED:
default:
return MusicState::STOPPED;
}
}
// Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
}
// Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
JA_SetMusicVolume(CONVERTED_VOLUME);
}
}
// Aplica la configuración
void Audio::applySettings() {
enable(Options::audio.enabled);
}
// Establecer estado general
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
}
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
}
}

114
source/core/audio/audio.hpp Normal file
View File

@@ -0,0 +1,114 @@
#pragma once
#include <cstdint> // Para int8_t, uint8_t
#include <string> // Para string
#include <utility> // Para move
// --- Clase Audio: gestor de audio (singleton) ---
// Implementació canònica, byte-idèntica entre projectes.
// Els volums es manegen internament com a float 0.01.0; la capa de
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio {
public:
// --- Enums ---
enum class Group : std::int8_t {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState : std::uint8_t {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%)
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms)
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
static void destroy(); // Libera el objeto Audio
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
static void update(); // Actualización del sistema de audio
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Helpers de conversió per a la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
static constexpr auto toPercent(float volume) -> int {
return static_cast<int>(volume * 100.0F + 0.5F);
}
static constexpr auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
}
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
void applySettings(); // Aplica la configuración
// --- Configuración de sonidos ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
// --- Configuración de música ---
void enableMusic() { music_enabled_ = true; } // Habilitar música
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Consultas de estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState;
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private:
// --- Tipos anidados ---
struct Music {
MusicState state{MusicState::STOPPED}; // Estado actual de la música
std::string name; // Última pista de música reproducida
bool loop{false}; // Indica si se reproduce en bucle
};
// --- Métodos ---
Audio(); // Constructor privado
~Audio(); // Destructor privado
void initSDLAudio(); // Inicializa SDL Audio
// --- Variables miembro ---
static Audio* instance; // Instancia única de Audio
Music music_; // Estado de la música
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};

View File

@@ -0,0 +1,13 @@
#include "core/audio/audio_adapter.hpp"
#include "core/resources/resource.hpp"
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name) {
return Resource::get()->getMusic(name);
}
JA_Sound_t* getSound(const std::string& name) {
return Resource::get()->getSound(name);
}
} // namespace AudioResource

View File

@@ -0,0 +1,17 @@
#pragma once
// --- Audio Resource Adapter ---
// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* /
// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
#include <string> // Para string
struct JA_Music_t;
struct JA_Sound_t;
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name);
JA_Sound_t* getSound(const std::string& name);
} // namespace AudioResource

View File

@@ -0,0 +1,679 @@
#pragma once
// --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <iostream> // Para std::cout
#include <memory> // Para std::unique_ptr
#include <string> // Para std::string
#include <vector> // Para std::vector
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible amb `std::unique_ptr<Uint8[], SDLFreeDeleter>` — zero size
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
struct SDLFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p) SDL_free(p);
}
};
// --- Public Enums ---
enum JA_Channel_state {
JA_CHANNEL_INVALID,
JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED,
};
enum JA_Music_state {
JA_MUSIC_INVALID,
JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED,
};
// --- Struct Definitions ---
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
#define JA_MAX_GROUPS 2
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free.
std::unique_ptr<Uint8[], SDLFreeDeleter> buffer;
};
struct JA_Channel_t {
JA_Sound_t* sound{nullptr};
int pos{0};
int times{0};
int group{0};
SDL_AudioStream* stream{nullptr};
JA_Channel_state state{JA_CHANNEL_FREE};
};
struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
// d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks
// per streaming. Com que stb_vorbis guarda un punter persistent al
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
// (una reallocation invalidaria el punter que el decoder conserva).
std::vector<Uint8> ogg_data;
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t
std::string filename;
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
// --- Internal Global State (inline, C++17) ---
inline JA_Music_t* current_music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
inline float JA_musicVolume{1.0f};
inline float JA_soundVolume[JA_MAX_GROUPS];
inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0};
// --- Crossfade / Fade State ---
struct JA_FadeState {
bool active{false};
Uint64 start_time{0};
int duration_ms{0};
float initial_volume{0.0f};
};
struct JA_OutgoingMusic {
SDL_AudioStream* stream{nullptr};
JA_FadeState fade;
};
inline JA_OutgoingMusic outgoing_music;
inline JA_FadeState incoming_fade;
// --- Forward Declarations ---
inline void JA_StopMusic();
inline void JA_StopChannel(const int channel);
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
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 num_channels = music->spec.channels;
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
num_channels,
chunk,
JA_MUSIC_CHUNK_SHORTS);
if (samples_per_channel <= 0) return 0;
const int bytes = samples_per_channel * num_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;
}
}
}
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
if (!music || !music->vorbis || !music->stream) return;
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int needed_bytes = static_cast<int>((static_cast<int64_t>(duration_ms) * bytes_per_second) / 1000);
while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded <= 0) break; // EOF: deixem drenar el que hi haja
}
}
// --- Core Functions ---
inline void JA_Update() {
// --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
if (outgoing_music.stream && outgoing_music.fade.active) {
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - outgoing_music.fade.start_time;
if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
} else {
float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms;
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent));
}
}
// --- Current music ---
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
// Fade-in (parte de un crossfade)
if (incoming_fade.active) {
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - incoming_fade.start_time;
if (elapsed >= (Uint64)incoming_fade.duration_ms) {
incoming_fade.active = false;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
} else {
float percent = (float)elapsed / (float)incoming_fade.duration_ms;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent);
}
}
// 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();
}
}
// --- Sound channels ---
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
}
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
}
inline void JA_Quit() {
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
// --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
if (!buffer || length == 0) return nullptr;
// Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
// persistent al buffer; com que ací no el resize'jem, el .data() és
// estable durant tot el cicle de vida del music.
auto* music = new JA_Music_t();
music->ogg_data.assign(buffer, buffer + length);
int error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&error,
nullptr);
if (!music->vorbis) {
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << error << ")" << '\n';
delete music;
return nullptr;
}
const stb_vorbis_info info = stb_vorbis_get_info(music->vorbis);
music->spec.channels = info.channels;
music->spec.freq = static_cast<int>(info.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->state = JA_MUSIC_STOPPED;
return music;
}
// Overload amb filename — els callers l'usen per poder comparar la música
// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa.
inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), length);
if (music && filename) music->filename = filename;
return music;
}
inline JA_Music_t* JA_LoadMusic(const char* filename) {
// Carreguem primer el arxiu en memòria i després el descomprimim.
FILE* f = fopen(filename, "rb");
if (!f) return nullptr;
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) {
fclose(f);
return nullptr;
}
if (fread(buffer, fsize, 1, f) != 1) {
fclose(f);
free(buffer);
return nullptr;
}
fclose(f);
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
if (music) {
music->filename = filename;
}
free(buffer);
return music;
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music || !music->vorbis) return;
JA_StopMusic();
current_music = music;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
std::cout << "Failed to create audio stream!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
JA_PumpMusic(current_music);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) {
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
}
}
inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music || music->filename.empty()) return nullptr;
return music->filename.c_str();
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return;
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
// Limpiar outgoing crossfade si existe
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
incoming_fade.active = false;
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// 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);
}
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
// Destruir outgoing anterior si existe
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
JA_PreFillOutgoing(current_music, milliseconds);
// Robar el stream del current_music al outgoing
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume};
// Dejar current_music sin stream (ya lo tiene outgoing)
current_music->stream = nullptr;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
incoming_fade.active = false;
}
inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) {
if (!JA_musicEnabled || !music || !music->vorbis) return;
// Destruir outgoing anterior si existe (crossfade durante crossfade)
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
// Robar el stream de la musica actual al outgoing para el fade-out.
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) {
JA_PreFillOutgoing(current_music, crossfade_ms);
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume};
current_music->stream = nullptr;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
}
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
current_music = music;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
std::cout << "Failed to create audio stream for crossfade!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, 0.0f);
JA_PumpMusic(current_music); // pre-carrega abans de bindejar
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
// Configurar fade-in
incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f};
}
inline JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
inline void JA_DeleteMusic(JA_Music_t* music) {
if (!music) return;
if (current_music == music) {
JA_StopMusic();
current_music = nullptr;
}
if (music->stream) SDL_DestroyAudioStream(music->stream);
if (music->vorbis) stb_vorbis_close(music->vorbis);
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
// al destructor de JA_Music_t.
delete music;
}
inline float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
if (current_music && current_music->stream) {
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
}
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float /*value*/) {
// No implementat amb el backend de streaming.
}
inline float JA_GetMusicPosition() {
return 0.0f;
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline JA_Sound_t* JA_LoadSound(const char* filename) {
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
if (!JA_soundEnabled || !sound) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
// No hay canal libre, reemplazamos el primero
channel = 0;
}
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
std::cout << "Failed to create audio stream for sound!" << '\n';
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
inline void JA_DeleteSound(JA_Sound_t* sound) {
if (!sound) return;
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
delete sound;
}
inline void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
inline void JA_StopChannel(const int channel) {
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) {
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = nullptr;
}
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) {
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = nullptr;
}
}
}
inline JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
inline float JA_SetSoundVolume(float volume, const int group = -1) {
const float v = SDL_clamp(volume, 0.0f, 1.0f);
if (group == -1) {
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
JA_soundVolume[i] = v;
}
} else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v;
} else {
return v;
}
// Aplicar volum als canals actius.
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
if (channels[i].stream) {
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
}
}
}
}
return v;
}
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1);
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1);
return v;
}

View File

@@ -1,16 +1,16 @@
#include "define_buttons.hpp"
#include "core/input/define_buttons.hpp"
#include <algorithm> // Para __all_of_fn, all_of
#include <memory> // Para unique_ptr, allocator, shared_ptr, operator==, make_unique
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText
#include "options.hpp" // Para Gamepad
#include "param.hpp" // Para Param, param, ParamGame, ParamServiceMenu
#include "resource.hpp" // Para Resource
#include "ui/window_message.hpp" // Para WindowMessage
#include "utils.hpp" // Para Zone
#include "core/input/input.hpp" // Para Input
#include "core/input/input_types.hpp" // Para InputAction
#include "core/locale/lang.hpp" // Para getText
#include "core/resources/resource.hpp" // Para Resource
#include "game/options.hpp" // Para Gamepad
#include "game/ui/window_message.hpp" // Para WindowMessage
#include "utils/param.hpp" // Para Param, param, ParamGame, ParamServiceMenu
#include "utils/utils.hpp" // Para Zone
DefineButtons::DefineButtons()
: input_(Input::get()) {

View File

@@ -8,8 +8,8 @@
#include <utility>
#include <vector>
#include "input.hpp"
#include "ui/window_message.hpp"
#include "core/input/input.hpp"
#include "game/ui/window_message.hpp"
namespace Options {
struct Gamepad;

View File

@@ -5,8 +5,8 @@
#include <utility>
#include <vector>
#include "core/input/input_types.hpp" // Solo incluimos los tipos compartidos
#include "external/json.hpp"
#include "input_types.hpp" // Solo incluimos los tipos compartidos
// --- Estructuras ---
struct GamepadConfig {

View File

@@ -1,4 +1,4 @@
#include "global_inputs.hpp"
#include "core/input/global_inputs.hpp"
#include <algorithm> // Para __any_of_fn, any_of
#include <functional> // Para function
@@ -7,20 +7,24 @@
#include <utility> // Para pair
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText, getLangFile, getLangName, getNextLangCode, loadFromFile
#include "options.hpp" // Para Video, video, Settings, settings, Audio, audio, Window, window
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
#include "utils.hpp" // Para boolToOnOff
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/input.hpp" // Para Input
#include "core/input/input_types.hpp" // Para InputAction
#include "core/locale/lang.hpp" // Para getText, getLangFile, getLangName, getNextLangCode, loadFromFile
#include "core/rendering/screen.hpp" // Para Screen
#include "core/system/section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode
#include "game/options.hpp" // Para Video, video, Settings, settings, Audio, audio, Window, window
#include "game/ui/notifier.hpp" // Para Notifier
#include "game/ui/service_menu.hpp" // Para ServiceMenu
#include "utils/utils.hpp" // Para boolToOnOff
namespace GlobalInputs {
// Termina
void quit() {
#ifdef __EMSCRIPTEN__
// En la versión web no se permite salir: el navegador gestiona el cierre.
return;
#else
const std::string CODE = "QUIT";
if (Notifier::get()->checkCode(CODE)) {
// Si la notificación de salir está activa, cambia de sección
@@ -30,6 +34,7 @@ namespace GlobalInputs {
// Si la notificación de salir no está activa, muestra la notificación
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 01"), std::string()}, -1, CODE);
}
#endif
}
// Reinicia

View File

@@ -1,4 +1,4 @@
#include "input.hpp"
#include "core/input/input.hpp"
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
@@ -8,7 +8,40 @@
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para pair, move
#include "ui/logger.hpp" // Para info
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Singleton
Input* Input::instance = nullptr;
@@ -305,33 +338,16 @@ void Input::addGamepadMappingsFromFile() {
}
}
void Input::discoverGamepads() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
handleEvent(event); // Comprueba mandos conectados
}
}
void Input::initSDLGamePad() {
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
std::cout << "SDL_GAMEPAD could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
addGamepadMappingsFromFile();
loadGamepadConfigs();
discoverGamepads();
Logger::info("Input System initialized successfully");
}
}
}
void Input::resetJustPressed() {
for (auto& key : keyboard_.bindings) {
key.second.just_pressed = false;
}
for (auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
binding.second.just_pressed = false;
// Los mandos ya conectados llegan como SDL_EVENT_GAMEPAD_ADDED en el
// primer pase del pump de eventos (antes del primer SDL_AppIterate),
// por lo que no hace falta enumerarlos aquí a mano.
}
}
}
@@ -360,7 +376,7 @@ void Input::update() {
bool key_is_down_now = key_states[binding.second.scancode];
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = binding.second.just_pressed || (key_is_down_now && !binding.second.is_held);
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
binding.second.is_held = key_is_down_now;
}
@@ -378,16 +394,6 @@ void Input::update() {
auto Input::handleEvent(const SDL_Event& event) -> std::string {
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
if (!event.key.repeat) {
for (auto& [action, binding] : keyboard_.bindings) {
if (binding.scancode == event.key.scancode) {
binding.just_pressed = true;
break;
}
}
}
break;
case SDL_EVENT_GAMEPAD_ADDED:
return addGamepad(event.gdevice.which);
case SDL_EVENT_GAMEPAD_REMOVED:
@@ -397,6 +403,7 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string {
}
auto Input::addGamepad(int device_index) -> std::string {
installWebStandardMapping(device_index);
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
@@ -461,6 +468,27 @@ void Input::applyGamepadConfig(std::shared_ptr<Gamepad> gamepad) {
return config.path == gamepad->path;
});
// Fallback por nombre: si el mismo dispositivo se enchufa a otro puerto, su
// path cambia pero el nombre suele mantenerse. Recuperamos su configuración
// y actualizamos el path guardado al actual. Solo se acepta un match cuyo
// path guardado NO corresponda a otro mando ya conectado, para no arruinar
// setups con varios mandos idénticos en puertos distintos (cabinet arcade).
if (config_it == gamepad_configs_.end() && !gamepad->name.empty()) {
auto is_path_active = [this](const std::string& query_path) -> bool {
return std::ranges::any_of(gamepads_, [&query_path](const std::shared_ptr<Gamepad>& g) -> bool {
return g && g->path == query_path;
});
};
config_it = std::ranges::find_if(gamepad_configs_, [&gamepad, &is_path_active](const GamepadConfig& config) -> bool {
return config.name == gamepad->name && !is_path_active(config.path);
});
if (config_it != gamepad_configs_.end()) {
std::cout << "Gamepad '" << gamepad->name << "' found by name, refreshing path to: " << gamepad->path << '\n';
config_it->path = gamepad->path;
saveGamepadConfigs();
}
}
if (config_it != gamepad_configs_.end()) {
// Se encontró una configuración específica para este puerto/dispositivo. La aplicamos.
std::cout << "Applying custom config for gamepad at path: " << gamepad->path << '\n';
@@ -470,7 +498,6 @@ void Input::applyGamepadConfig(std::shared_ptr<Gamepad> gamepad) {
}
}
}
// Opcional: Podrías añadir un fallback para buscar por nombre si no se encuentra por ruta.
}
void Input::saveGamepadConfigFromGamepad(std::shared_ptr<Gamepad> gamepad) {

View File

@@ -9,8 +9,8 @@
#include <utility> // Para pair
#include <vector> // Para vector
#include "gamepad_config_manager.hpp" // for GamepadConfig (ptr only), GamepadConfigs
#include "input_types.hpp" // for InputAction
#include "core/input/gamepad_config_manager.hpp" // for GamepadConfig (ptr only), GamepadConfigs
#include "core/input/input_types.hpp" // for InputAction
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
class Input {
@@ -105,10 +105,20 @@ class Input {
std::string path;
std::unordered_map<Action, ButtonState> bindings;
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
static auto trimName(const char* raw) -> std::string {
std::string s(raw != nullptr ? raw : "");
const auto pos = s.find_first_of("([");
if (pos != std::string::npos) { s.erase(pos); }
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
return s;
}
Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
name(trimName(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento del jugador
@@ -177,7 +187,6 @@ class Input {
// --- Métodos de reseteo de estado de entrada ---
void resetInputStates();
void resetJustPressed();
// --- Eventos ---
auto handleEvent(const SDL_Event& event) -> std::string;
@@ -207,7 +216,6 @@ class Input {
auto addGamepad(int device_index) -> std::string;
auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile();
void discoverGamepads();
// --- Métodos para integración con GamepadConfigManager ---
void loadGamepadConfigs();

View File

@@ -1,4 +1,4 @@
#include "input_types.hpp"
#include "core/input/input_types.hpp"
#include <utility> // Para pair

View File

@@ -1,4 +1,4 @@
#include "mouse.hpp"
#include "core/input/mouse.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, Uint32, SDL_HideCursor, SDL_Show...

View File

@@ -1,4 +1,4 @@
#include "lang.hpp"
#include "core/locale/lang.hpp"
#include <cstddef> // Para size_t
#include <exception> // Para exception
@@ -7,11 +7,11 @@
#include <utility> // Para pair
#include <vector> // Para vector
#include "asset.hpp" // Para Asset
#include "difficulty.hpp" // Para Difficulty
#include "core/resources/asset.hpp" // Para Asset
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "external/json.hpp" // Para basic_json, iteration_proxy_value, oper...
#include "options.hpp" // Para SettingsOpt...
#include "resource_helper.hpp" // Para ResourceHelper
#include "game/gameplay/difficulty.hpp" // Para Difficulty
#include "game/options.hpp" // Para SettingsOpt...
using json = nlohmann::json;

View File

@@ -1,5 +1,5 @@
#define _USE_MATH_DEFINES
#include "background.hpp"
#include "core/rendering/background.hpp"
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_SetRenderTarget, SDL_CreateTexture, SDL_DestroyTexture, SDL_GetRenderTarget, SDL_RenderTexture, SDL_SetTextureAlphaMod, SDL_SetTextureBlendMode, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_RenderClear, SDL_SetRenderDrawColor, SDL_TextureAccess, SDL_FPoint
@@ -8,14 +8,14 @@
#include <string> // Para basic_string
#include <utility> // Para move
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "moving_sprite.hpp" // Para MovingSprite
#include "param.hpp" // Para Param, ParamBackground, param
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para easeOutCubic
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite
#include "core/rendering/sprite/moving_sprite.hpp" // Para MovingSprite
#include "core/rendering/sprite/sprite.hpp" // Para Sprite
#include "core/rendering/texture.hpp" // Para Texture
#include "core/resources/resource.hpp" // Para Resource
#include "utils/param.hpp" // Para Param, ParamBackground, param
#include "utils/utils.hpp" // Para easeOutCubic
// Constructor
Background::Background(float total_progress_to_complete)

View File

@@ -8,7 +8,7 @@
#include <memory> // Para unique_ptr, shared_ptr
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "utils/color.hpp" // Para Color
class MovingSprite;
class Sprite;

View File

@@ -1,4 +1,4 @@
#include "fade.hpp"
#include "core/rendering/fade.hpp"
#include <SDL3/SDL.h>
@@ -6,9 +6,9 @@
#include <cstdlib>
#include <utility>
#include "color.hpp"
#include "param.hpp"
#include "screen.hpp"
#include "core/rendering/screen.hpp"
#include "utils/color.hpp"
#include "utils/param.hpp"
// Constructor
Fade::Fade()

View File

@@ -0,0 +1,253 @@
#include "core/rendering/gif.hpp"
#include <SDL3/SDL.h> // Para SDL_LogError, SDL_LogCategory, SDL_LogInfo
#include <cstring> // Para memcpy, size_t
#include <iostream> // Para std::cout
#include <stdexcept> // Para runtime_error
#include <string> // Para char_traits, operator==, basic_string, string
namespace GIF {
inline void readBytes(const uint8_t *&buffer, void *dst, size_t size) {
std::memcpy(dst, buffer, size);
buffer += size;
}
void Gif::decompress(int code_length, const uint8_t *input, int input_length, uint8_t *out) {
if (code_length < 2 || code_length > 12) {
std::cout << "Invalid LZW code length: " << code_length << '\n';
throw std::runtime_error("Invalid LZW code length");
}
int i, bit;
int prev = -1;
std::vector<DictionaryEntry> dictionary;
int dictionary_ind;
unsigned int mask = 0x01;
int reset_code_length = code_length;
int clear_code = 1 << code_length;
int stop_code = clear_code + 1;
int match_len = 0;
dictionary.resize(1 << (code_length + 1));
for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) {
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
dictionary[dictionary_ind].prev = -1;
dictionary[dictionary_ind].len = 1;
}
dictionary_ind += 2;
while (input_length > 0) {
int code = 0;
for (i = 0; i < (code_length + 1); i++) {
if (input_length <= 0) {
std::cout << "Unexpected end of input in decompress" << '\n';
throw std::runtime_error("Unexpected end of input in decompress");
}
bit = ((*input & mask) != 0) ? 1 : 0;
mask <<= 1;
if (mask == 0x100) {
mask = 0x01;
input++;
input_length--;
}
code |= (bit << i);
}
if (code == clear_code) {
code_length = reset_code_length;
dictionary.resize(1 << (code_length + 1));
for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) {
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
dictionary[dictionary_ind].prev = -1;
dictionary[dictionary_ind].len = 1;
}
dictionary_ind += 2;
prev = -1;
continue;
} else if (code == stop_code) {
break;
}
if (prev > -1 && code_length < 12) {
if (code > dictionary_ind) {
std::cout << "LZW error: code (" << code << ") exceeds dictionary_ind (" << dictionary_ind << ")" << '\n';
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
}
int ptr;
if (code == dictionary_ind) {
ptr = prev;
while (dictionary[ptr].prev != -1)
ptr = dictionary[ptr].prev;
dictionary[dictionary_ind].byte = dictionary[ptr].byte;
} else {
ptr = code;
while (dictionary[ptr].prev != -1)
ptr = dictionary[ptr].prev;
dictionary[dictionary_ind].byte = dictionary[ptr].byte;
}
dictionary[dictionary_ind].prev = prev;
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
dictionary_ind++;
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
code_length++;
dictionary.resize(1 << (code_length + 1));
}
}
prev = code;
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
std::cout << "Invalid LZW code " << code << ", dictionary size " << static_cast<unsigned long>(dictionary.size()) << '\n';
throw std::runtime_error("LZW error: invalid code encountered");
}
int curCode = code;
match_len = dictionary[curCode].len;
while (curCode != -1) {
out[dictionary[curCode].len - 1] = dictionary[curCode].byte;
if (dictionary[curCode].prev == curCode) {
std::cout << "Internal error; self-reference detected." << '\n';
throw std::runtime_error("Internal error in decompress: self-reference");
}
curCode = dictionary[curCode].prev;
}
out += match_len;
}
}
std::vector<uint8_t> Gif::readSubBlocks(const uint8_t *&buffer) {
std::vector<uint8_t> data;
uint8_t block_size = *buffer;
buffer++;
while (block_size != 0) {
data.insert(data.end(), buffer, buffer + block_size);
buffer += block_size;
block_size = *buffer;
buffer++;
}
return data;
}
std::vector<uint8_t> Gif::processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> &gct, int resolution_bits) {
ImageDescriptor image_descriptor;
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
uint8_t lzw_code_size;
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
return uncompressed_data;
}
std::vector<uint32_t> Gif::loadPalette(const uint8_t *buffer) {
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
ScreenDescriptor screen_descriptor;
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
buffer += sizeof(ScreenDescriptor);
std::vector<uint32_t> global_color_table;
if (screen_descriptor.fields & 0x80) {
int global_color_table_size = 1 << (((screen_descriptor.fields & 0x07) + 1));
global_color_table.resize(global_color_table_size);
for (int i = 0; i < global_color_table_size; ++i) {
uint8_t r = buffer[0];
uint8_t g = buffer[1];
uint8_t b = buffer[2];
global_color_table[i] = (r << 16) | (g << 8) | b;
buffer += 3;
}
}
return global_color_table;
}
std::vector<uint8_t> Gif::processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h) {
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
std::string headerStr(reinterpret_cast<char *>(header), 6);
if (headerStr != "GIF87a" && headerStr != "GIF89a") {
std::cout << "Formato de archivo GIF inválido: " << headerStr << '\n';
throw std::runtime_error("Formato de archivo GIF inválido.");
}
ScreenDescriptor screen_descriptor;
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
w = screen_descriptor.width;
h = screen_descriptor.height;
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
std::vector<RGB> global_color_table;
if (screen_descriptor.fields & 0x80) {
int global_color_table_size = 1 << (((screen_descriptor.fields & 0x07) + 1));
global_color_table.resize(global_color_table_size);
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
buffer += 3 * global_color_table_size;
}
uint8_t block_type = *buffer++;
while (block_type != TRAILER) {
if (block_type == EXTENSION_INTRODUCER) {
uint8_t extension_label = *buffer++;
switch (extension_label) {
case GRAPHIC_CONTROL: {
uint8_t blockSize = *buffer++;
buffer += blockSize;
uint8_t subBlockSize = *buffer++;
while (subBlockSize != 0) {
buffer += subBlockSize;
subBlockSize = *buffer++;
}
break;
}
case APPLICATION_EXTENSION:
case COMMENT_EXTENSION:
case PLAINTEXT_EXTENSION: {
uint8_t blockSize = *buffer++;
buffer += blockSize;
uint8_t subBlockSize = *buffer++;
while (subBlockSize != 0) {
buffer += subBlockSize;
subBlockSize = *buffer++;
}
break;
}
default: {
uint8_t blockSize = *buffer++;
buffer += blockSize;
uint8_t subBlockSize = *buffer++;
while (subBlockSize != 0) {
buffer += subBlockSize;
subBlockSize = *buffer++;
}
break;
}
}
} else if (block_type == IMAGE_DESCRIPTOR) {
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
} else {
std::cout << "Unrecognized block type: 0x" << std::hex << static_cast<int>(block_type) << std::dec << '\n';
return std::vector<uint8_t>{};
}
block_type = *buffer++;
}
return std::vector<uint8_t>{};
}
std::vector<uint8_t> Gif::loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h) {
return processGifStream(buffer, w, h);
}
} // namespace GIF

View File

@@ -0,0 +1,92 @@
#pragma once
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
#include <vector> // Para vector
namespace GIF {
// Constantes definidas con constexpr, en lugar de macros
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
constexpr uint8_t TRAILER = 0x3B;
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
#pragma pack(push, 1)
struct ScreenDescriptor {
uint16_t width;
uint16_t height;
uint8_t fields;
uint8_t background_color_index;
uint8_t pixel_aspect_ratio;
};
struct RGB {
uint8_t r, g, b;
};
struct ImageDescriptor {
uint16_t image_left_position;
uint16_t image_top_position;
uint16_t image_width;
uint16_t image_height;
uint8_t fields;
};
#pragma pack(pop)
struct DictionaryEntry {
uint8_t byte;
int prev;
int len;
};
struct Extension {
uint8_t extension_code;
uint8_t block_size;
};
struct GraphicControlExtension {
uint8_t fields;
uint16_t delay_time;
uint8_t transparent_color_index;
};
struct ApplicationExtension {
uint8_t application_id[8];
uint8_t version[3];
};
struct PlaintextExtension {
uint16_t left, top, width, height;
uint8_t cell_width, cell_height;
uint8_t foreground_color, background_color;
};
class Gif {
public:
// Descompone (uncompress) el bloque comprimido usando LZW.
// Este método puede lanzar std::runtime_error en caso de error.
void decompress(int code_length, const uint8_t *input, int input_length, uint8_t *out);
// Carga la paleta (global color table) a partir de un buffer,
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
std::vector<uint32_t> loadPalette(const uint8_t *buffer);
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
// asigna el ancho y alto mediante referencias.
std::vector<uint8_t> loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h);
private:
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
std::vector<uint8_t> readSubBlocks(const uint8_t *&buffer);
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
std::vector<uint8_t> processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> &gct, int resolution_bits);
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
std::vector<uint8_t> processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h);
};
} // namespace GIF

View File

@@ -1,30 +1,73 @@
#include "screen.hpp"
#include "core/rendering/screen.hpp"
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_SyncWindow, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <algorithm> // Para min, max
#include <cstring> // Para memcpy
#include <iostream> // Para std::cout
#include <memory> // Para allocator, shared_ptr, unique_ptr, __shared_ptr_access, make_shared, make_unique
#include <string> // Para basic_string, operator+, char_traits, to_string, string
#include <vector> // Para vector
#include "asset.hpp" // Para Asset
#include "director.hpp" // Para Director::debug_config
#include "mouse.hpp" // Para updateCursorVisibility
#include "options.hpp" // Para Video, video, Window, window
#include "param.hpp" // Para Param, param, ParamGame, ParamDebug
#include "rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "resource.hpp" // Para Resource
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
#include "ui/logger.hpp" // Para info
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
#include "utils.hpp" // Para toLower
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#endif
#include "core/rendering/text.hpp" // Para Text
#include "core/rendering/texture.hpp" // Para Texture
#include "core/resources/asset.hpp" // Para Asset
#include "core/resources/resource.hpp" // Para Resource
#include "core/system/director.hpp" // Para Director::debug_config
#include "game/options.hpp" // Para Video, video, Window, window
#include "game/ui/notifier.hpp" // Para Notifier
#include "game/ui/service_menu.hpp" // Para ServiceMenu
#include "utils/param.hpp" // Para Param, param, ParamGame, ParamDebug
#include "utils/utils.hpp" // Para toLower
// Singleton
Screen* Screen::instance = nullptr;
#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauración del canvas en wasm/Emscripten
// ============================================================================
// SDL3 + Emscripten no notifica de forma fiable los cambios de estado del
// canvas HTML (fullscreen exit vía Esc, rotación del dispositivo, etc.).
// Registramos callbacks nativos de Emscripten que delegan en
// Screen::handleCanvasResized(), el cual re-aplica el modo de fullscreen y
// reajusta la ventana para que SDL salga de su estado interno de fullscreen.
//
// Los callbacks difieren el trabajo con emscripten_async_call(0ms) porque el
// navegador todavía no ha acabado de redimensionar el canvas cuando el evento
// se dispara; posponer al siguiente tick garantiza valores estables.
//
// Referencias: libsdl-org/SDL#13300, libsdl-org/SDL#11389.
// ============================================================================
namespace {
void deferredCanvasResize(void* /*user_data*/) {
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
}
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
// Sincronizamos Options::video.fullscreen con el estado real del navegador
// antes de diferir la restauración: cuando el usuario sale con Esc no pasa
// por toggleFullscreen() y el estado interno quedaría desincronizado.
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif
// Inicializa la instancia única del singleton
void Screen::init() {
Screen::instance = new Screen();
@@ -51,7 +94,7 @@ Screen::Screen()
// Crea la textura de destino
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST);
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
// Inicializar buffer de píxeles para SDL3GPU
pixel_buffer_.resize(static_cast<size_t>(param.game.width) * static_cast<size_t>(param.game.height));
@@ -67,6 +110,14 @@ Screen::Screen()
// Renderizar una vez la textura vacía para que tenga contenido válido antes de inicializar los shaders (evita pantalla negra)
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
// Aplicar la configuración inicial completa (vsync + logical presentation +
// fullscreen + tamaño de ventana). En Emscripten es necesario porque el
// canvas HTML tiene un tamaño propio y SDL_CreateWindow solo no basta para
// que SDL sincronice su viewport interno con el canvas real: sin este
// applySettings el canvas inicial sale descolocado con barras negras a los
// lados y el juego pequeño en el centro hasta el primer toggle de fullscreen.
applySettings();
// Limpiar renderer
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
SDL_RenderClear(renderer_);
@@ -143,10 +194,39 @@ void Screen::setFullscreenMode() {
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
}
// Camibia entre pantalla completa y ventana
// Cambia entre pantalla completa y ventana. Usamos applySettings en vez de
// setFullscreenMode porque applySettings también re-aplica la logical
// presentation — sin eso, al entrar en fullscreen SDL no recalcula el viewport
// y el juego se ve pequeño (especialmente en Android).
void Screen::toggleFullscreen() {
Options::video.fullscreen = !Options::video.fullscreen;
setFullscreenMode();
applySettings();
}
// Re-sincroniza SDL con el estado real del canvas del navegador. Lo invocan los
// callbacks nativos de Emscripten (vegeu el bloc al principi del fitxer) cuando
// se detecta un fullscreenchange o un orientationchange. Re-aplica fullscreen y
// reajusta la ventana porque SDL no emite SDL_EVENT_WINDOW_LEAVE_FULLSCREEN en
// Emscripten y su estado interno queda desincronizado al salir con Esc.
// Fuera de Emscripten es un no-op (desktop sí emite los events correctamente).
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
// SDL_SetWindowFullscreen es imprescindible para sacar a SDL de su estado
// interno de fullscreen cuando el usuario ha salido sin pasar por
// toggleFullscreen (onEmFullscreenChange ya ha actualizado Options).
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
SDL_SyncWindow(window_);
adjustWindowSize();
#endif
}
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// Cambia el tamaño de la ventana
@@ -290,7 +370,7 @@ void Screen::initShaders() {
#ifndef NO_SHADERS
auto* self = Screen::get();
if (self == nullptr) {
SDL_Log("Screen::initShaders: instance is null, skipping");
std::cout << "Screen::initShaders: instance is null, skipping" << '\n';
return;
}
if (!self->shader_backend_) {
@@ -301,7 +381,7 @@ void Screen::initShaders() {
}
if (!self->shader_backend_->isHardwareAccelerated()) {
const bool ok = self->shader_backend_->init(self->window_, self->game_canvas_, "", "");
SDL_Log("Screen::initShaders: SDL3GPUShader::init() = %s", ok ? "OK" : "FAILED");
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n';
}
if (self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
self->shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
@@ -371,9 +451,7 @@ void Screen::renderAttenuate() {
auto Screen::initSDLVideo() -> bool {
// Inicializar SDL
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"FATAL: Failed to initialize SDL_VIDEO! SDL Error: %s",
SDL_GetError());
std::cout << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
return false;
}
@@ -383,8 +461,7 @@ auto Screen::initSDLVideo() -> bool {
// Configurar hint para renderizado
#ifdef __APPLE__
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Failed to set Metal hint!");
std::cout << "Warning: Failed to set Metal hint!" << '\n';
}
#endif
@@ -405,9 +482,7 @@ auto Screen::initSDLVideo() -> bool {
window_flags);
if (window_ == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"FATAL: Failed to create window! SDL Error: %s",
SDL_GetError());
std::cout << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
SDL_Quit();
return false;
}
@@ -415,9 +490,7 @@ auto Screen::initSDLVideo() -> bool {
// Crear renderer
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"FATAL: Failed to create renderer! SDL Error: %s",
SDL_GetError());
std::cout << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
SDL_DestroyWindow(window_);
window_ = nullptr;
SDL_Quit();
@@ -429,24 +502,16 @@ auto Screen::initSDLVideo() -> bool {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
Logger::info("Video system initialized successfully");
registerEmscriptenEventCallbacks();
return true;
}
// Obtiene información sobre la pantalla
void Screen::getDisplayInfo() {
int i;
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr) {
for (i = 0; i < num_displays; ++i) {
SDL_DisplayID instance_id = displays[i];
const char* name = SDL_GetDisplayName(instance_id);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Display %" SDL_PRIu32 ": %s", instance_id, (name != nullptr) ? name : "Unknown");
Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown"));
}
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
// Guarda información del monitor en display_monitor_
@@ -465,13 +530,6 @@ void Screen::getDisplayInfo() {
std::to_string(dm->h) + " @ " +
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Current display mode: %dx%d @ %dHz", static_cast<int>(dm->w), static_cast<int>(dm->h), static_cast<int>(dm->refresh_rate));
Logger::info("Current display mode: " + Options::video.info);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Window resolution: %dx%d x%d", static_cast<int>(param.game.width), static_cast<int>(param.game.height), Options::window.zoom);
Logger::info("Window resolution: " + std::to_string(static_cast<int>(param.game.width)) + "x" + std::to_string(static_cast<int>(param.game.height)) + "x" + std::to_string(Options::window.zoom));
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
const int MAX_ZOOM = std::min(dm->w / param.game.width, (dm->h - WINDOWS_DECORATIONS) / param.game.height);
@@ -563,11 +621,7 @@ void Screen::applyCurrentPostFXPreset() {
p.curvature = preset.curvature;
p.bleeding = preset.bleeding;
p.flicker = preset.flicker;
SDL_Log("Screen::applyCurrentPostFXPreset: preset='%s' scan=%.2f vign=%.2f chroma=%.2f",
preset.name.c_str(),
p.scanlines,
p.vignette,
p.chroma);
std::cout << "Screen::applyCurrentPostFXPreset: preset='" << preset.name << "' scan=" << p.scanlines << " vign=" << p.vignette << " chroma=" << p.chroma << '\n';
}
shader_backend_->setPostFXParams(p);
}
@@ -594,7 +648,7 @@ void Screen::applyCurrentCrtPiPreset() {
.enable_sharper = preset.enable_sharper,
};
shader_backend_->setCrtPiParams(p);
SDL_Log("Screen::applyCurrentCrtPiPreset: preset='%s'", preset.name.c_str());
std::cout << "Screen::applyCurrentCrtPiPreset: preset='" << preset.name << "'" << '\n';
}
}
@@ -616,6 +670,25 @@ void Screen::toggleVSync() {
}
}
// Aplica Options::video.scale_mode a la textura del canvas de juego
void Screen::applyFilter() {
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
}
// Alterna el modo de filtrado entre nearest y linear
void Screen::toggleFilter() {
Options::video.scale_mode = (Options::video.scale_mode == SDL_SCALEMODE_NEAREST)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
applyFilter();
}
// Devuelve true si el backend SDL3GPU está activo y con aceleración hardware
auto Screen::isHardwareAccelerated() -> bool {
auto* self = Screen::get();
return self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated();
}
// Establece el estado del V-Sync
void Screen::setVSync(bool enabled) {
Options::video.vsync = enabled;
@@ -637,12 +710,17 @@ void Screen::getSingletons() {
#endif
}
// Aplica los valores de las opciones
// Aplica los valores de las opciones.
// IMPORTANTE: el orden importa. SDL_SetRenderLogicalPresentation calcula el
// viewport en función del tamaño actual de la ventana SDL, así que DEBE llamarse
// DESPUÉS de setFullscreenMode/adjustWindowSize — si no, al entrar en fullscreen
// el viewport queda cacheado al tamaño de la ventana pequeña previa y el juego
// se ve pequeño y centrado con barras negras alrededor.
void Screen::applySettings() {
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
SDL_SetRenderLogicalPresentation(Screen::get()->getRenderer(), param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
setFullscreenMode();
adjustWindowSize();
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
}
// Crea el objeto de texto

View File

@@ -6,9 +6,9 @@
#include <string> // Para string
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "options.hpp" // Para VideoOptions, video
#include "rendering/shader_backend.hpp" // Para Rendering::ShaderType
#include "core/rendering/shader_backend.hpp" // Para Rendering::ShaderType
#include "game/options.hpp" // Para VideoOptions, video
#include "utils/color.hpp" // Para Color
// Forward declarations
class Notifier;
@@ -34,6 +34,7 @@ class Screen {
// --- Configuración de ventana y render ---
void setFullscreenMode(); // Establece el modo de pantalla completa
void toggleFullscreen(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // Restaura el canvas cuando SDL3 no reporta el cambio (emscripten only: salida de fullscreen con Esc, rotación); no-op fuera de emscripten
void setWindowZoom(int zoom); // Cambia el tamaño de la ventana
auto decWindowSize() -> bool; // Reduce el tamaño de la ventana
auto incWindowSize() -> bool; // Aumenta el tamaño de la ventana
@@ -48,6 +49,8 @@ class Screen {
static void nextPostFXPreset(); // Avanza al siguiente preset PostFX
static void nextCrtPiPreset(); // Avanza al siguiente preset CrtPi
static void toggleSupersampling(); // Alterna supersampling
void toggleFilter(); // Alterna SDL_SCALEMODE_NEAREST ↔ SDL_SCALEMODE_LINEAR
void applyFilter(); // Aplica Options::video.scale_mode a game_canvas_
void toggleIntegerScale();
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
void setVSync(bool enabled); // Establece el estado del V-Sync
@@ -59,6 +62,7 @@ class Screen {
void hide() { SDL_HideWindow(window_); } // Oculta la ventana
void getSingletons(); // Obtiene los punteros a los singletones
[[nodiscard]] static auto getVSync() -> bool { return Options::video.vsync; } // Obtiene el valor de V-Sync
[[nodiscard]] static auto isHardwareAccelerated() -> bool; // True si SDL3GPU está activo
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; } // Obtiene el puntero al texto de Screen
// --- Display Monitor getters ---
@@ -233,6 +237,7 @@ class Screen {
// --- Métodos internos ---
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void registerEmscriptenEventCallbacks(); // Registra callbacks nativos para restaurar el canvas en wasm (no-op fuera de emscripten)
void renderFlash(); // Dibuja el efecto de flash en la pantalla
void renderShake(); // Aplica el efecto de agitar la pantalla
void renderInfo() const; // Muestra información por pantalla

View File

@@ -1,17 +1,18 @@
#include "rendering/sdl3gpu/sdl3gpu_shader.hpp"
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#include <SDL3/SDL_log.h>
#include <algorithm> // std::min, std::max, std::floor
#include <cmath> // std::floor, std::ceil
#include <cstring> // memcpy, strlen
#include <iostream> // Para std::cout
#ifndef __APPLE__
#include "rendering/sdl3gpu/crtpi_frag_spv.h"
#include "rendering/sdl3gpu/downscale_frag_spv.h"
#include "rendering/sdl3gpu/postfx_frag_spv.h"
#include "rendering/sdl3gpu/postfx_vert_spv.h"
#include "rendering/sdl3gpu/upscale_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
#endif
#ifdef __APPLE__
@@ -377,7 +378,7 @@ namespace Rendering {
// 1. GPU disabled by config
if (preferred_driver_ == "none") {
SDL_Log("SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback");
std::cout << "SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback" << '\n';
driver_name_ = "";
return false;
}
@@ -392,18 +393,18 @@ namespace Rendering {
const char* preferred = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
device_ = SDL_CreateGPUDevice(PREFERRED, false, preferred);
if (device_ == nullptr && preferred != nullptr) {
SDL_Log("SDL3GPUShader: driver '%s' not available, falling back to auto", preferred);
std::cout << "SDL3GPUShader: driver '" << preferred << "' not available, falling back to auto" << '\n';
device_ = SDL_CreateGPUDevice(PREFERRED, false, nullptr);
}
if (device_ == nullptr) {
SDL_Log("SDL3GPUShader: SDL_CreateGPUDevice failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: SDL_CreateGPUDevice failed: " << SDL_GetError() << '\n';
return false;
}
driver_name_ = SDL_GetGPUDeviceDriver(device_);
SDL_Log("SDL3GPUShader: driver = %s", driver_name_.c_str());
std::cout << "SDL3GPUShader: driver = " << driver_name_ << '\n';
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
SDL_Log("SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << '\n';
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
@@ -422,7 +423,7 @@ namespace Rendering {
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create scene texture: %s", SDL_GetError());
std::cout << "SDL3GPUShader: failed to create scene texture: " << SDL_GetError() << '\n';
cleanup();
return false;
}
@@ -435,7 +436,7 @@ namespace Rendering {
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create upload buffer: %s", SDL_GetError());
std::cout << "SDL3GPUShader: failed to create upload buffer: " << SDL_GetError() << '\n';
cleanup();
return false;
}
@@ -450,7 +451,7 @@ namespace Rendering {
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
if (sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create sampler: %s", SDL_GetError());
std::cout << "SDL3GPUShader: failed to create sampler: " << SDL_GetError() << '\n';
cleanup();
return false;
}
@@ -464,7 +465,7 @@ namespace Rendering {
lsamp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
linear_sampler_ = SDL_CreateGPUSampler(device_, &lsamp_info);
if (linear_sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create linear sampler: %s", SDL_GetError());
std::cout << "SDL3GPUShader: failed to create linear sampler: " << SDL_GetError() << '\n';
cleanup();
return false;
}
@@ -480,7 +481,7 @@ namespace Rendering {
}
is_initialized_ = true;
SDL_Log("SDL3GPUShader: initialized OK (%dx%d)", game_width_, game_height_);
std::cout << "SDL3GPUShader: initialized OK (" << game_width_ << "x" << game_height_ << ")" << '\n';
return true;
}
@@ -500,7 +501,7 @@ namespace Rendering {
#endif
if ((vert == nullptr) || (frag == nullptr)) {
SDL_Log("SDL3GPUShader: failed to compile PostFX shaders");
std::cout << "SDL3GPUShader: failed to compile PostFX shaders" << '\n';
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false;
@@ -529,7 +530,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, frag);
if (pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: PostFX pipeline creation failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: PostFX pipeline creation failed: " << SDL_GetError() << '\n';
return false;
}
@@ -565,7 +566,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, ufrag);
if (upscale_pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: upscale pipeline creation failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: upscale pipeline creation failed: " << SDL_GetError() << '\n';
return false;
}
@@ -601,7 +602,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, offrag);
if (postfx_offscreen_pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: PostFX offscreen pipeline creation failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: PostFX offscreen pipeline creation failed: " << SDL_GetError() << '\n';
return false;
}
@@ -637,7 +638,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, dfrag);
if (downscale_pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: downscale pipeline creation failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: downscale pipeline creation failed: " << SDL_GetError() << '\n';
return false;
}
@@ -683,7 +684,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, frag);
if (crtpi_pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: CrtPi pipeline creation failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: CrtPi pipeline creation failed: " << SDL_GetError() << '\n';
return false;
}
return true;
@@ -697,7 +698,7 @@ namespace Rendering {
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
if (mapped == nullptr) {
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: SDL_MapGPUTransferBuffer failed: " << SDL_GetError() << '\n';
return;
}
@@ -726,7 +727,7 @@ namespace Rendering {
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (cmd == nullptr) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: " << SDL_GetError() << '\n';
return;
}
@@ -773,7 +774,7 @@ namespace Rendering {
Uint32 sw = 0;
Uint32 sh = 0;
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
std::cout << "SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: " << SDL_GetError() << '\n';
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
@@ -998,7 +999,7 @@ namespace Rendering {
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: MSL shader '%s' failed: %s", entrypoint, SDL_GetError());
std::cout << "SDL3GPUShader: MSL shader '" << entrypoint << "' failed: " << SDL_GetError() << '\n';
}
return shader;
}
@@ -1020,7 +1021,7 @@ namespace Rendering {
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
std::cout << "SDL3GPUShader: SPIRV shader '" << entrypoint << "' failed: " << SDL_GetError() << '\n';
}
return shader;
}
@@ -1135,7 +1136,7 @@ namespace Rendering {
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
std::cout << "SDL3GPUShader: reinit — failed to create scene texture: " << SDL_GetError() << '\n';
return false;
}
@@ -1144,13 +1145,13 @@ namespace Rendering {
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
std::cout << "SDL3GPUShader: reinit — failed to create upload buffer: " << SDL_GetError() << '\n';
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
return false;
}
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s", game_width_, game_height_, oversample_ > 1 ? "on" : "off");
std::cout << "SDL3GPUShader: reinit — scene " << game_width_ << "x" << game_height_ << ", SS " << (oversample_ > 1 ? "on" : "off") << '\n';
return true;
}
@@ -1185,20 +1186,20 @@ namespace Rendering {
scaled_texture_ = SDL_CreateGPUTexture(device_, &info);
if (scaled_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create scaled texture %dx%d: %s", W, H, SDL_GetError());
std::cout << "SDL3GPUShader: failed to create scaled texture " << W << "x" << H << ": " << SDL_GetError() << '\n';
return false;
}
postfx_texture_ = SDL_CreateGPUTexture(device_, &info);
if (postfx_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create postfx texture %dx%d: %s", W, H, SDL_GetError());
std::cout << "SDL3GPUShader: failed to create postfx texture " << W << "x" << H << ": " << SDL_GetError() << '\n';
SDL_ReleaseGPUTexture(device_, scaled_texture_);
scaled_texture_ = nullptr;
return false;
}
ss_factor_ = factor;
SDL_Log("SDL3GPUShader: scaled+postfx textures %dx%d (factor %dx)", W, H, factor);
std::cout << "SDL3GPUShader: scaled+postfx textures " << W << "x" << H << " (factor " << factor << "x)" << '\n';
return true;
}

View File

@@ -6,7 +6,7 @@
#include <string>
#include <utility>
#include "rendering/shader_backend.hpp"
#include "core/rendering/shader_backend.hpp"
// PostFX uniforms pushed to fragment stage each frame.
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.

View File

@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never

View File

@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''

View File

@@ -1,17 +1,17 @@
#include "animated_sprite.hpp"
#include "core/rendering/sprite/animated_sprite.hpp"
#include <SDL3/SDL.h> // Para SDL_LogWarn, SDL_LogCategory, SDL_LogError, SDL_FRect
#include <algorithm> // Para min
#include <cstddef> // Para size_t
#include <fstream> // Para basic_istream, basic_ifstream, istream, basic_ios, ifstream, istringstream, stringstream
#include <iostream> // Para std::cout
#include <sstream> // Para basic_istringstream, basic_stringstream
#include <stdexcept> // Para runtime_error
#include <utility> // Para move, pair
#include "resource_helper.hpp" // Para loadFile
#include "texture.hpp" // Para Texture
#include "ui/logger.hpp" // Para dots
#include "core/rendering/texture.hpp" // Para Texture
#include "core/resources/resource_helper.hpp" // Para loadFile
// Carga las animaciones en un vector(Animations) desde un fichero
auto loadAnimationsFromFile(const std::string& file_path) -> AnimationsFileBuffer {
@@ -31,15 +31,13 @@ auto loadAnimationsFromFile(const std::string& file_path) -> AnimationsFileBuffe
if (!using_resource_data) {
file.open(file_path);
if (!file) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Fichero no encontrado %s", file_path.c_str());
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
}
std::istream& input_stream = using_resource_data ? stream : static_cast<std::istream&>(file);
Logger::dots("Animation : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
std::vector<std::string> buffer;
std::string line;
while (std::getline(input_stream, line)) {
@@ -82,7 +80,7 @@ auto AnimatedSprite::getAnimationIndex(const std::string& name) -> int {
}
// Si no se encuentra, muestra una advertencia y devuelve -1
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "** Warning: could not find \"%s\" animation", name.c_str());
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
return -1;
}
@@ -219,7 +217,7 @@ void AnimatedSprite::processConfigLine(const std::string& line, AnimationConfig&
config.frame_height = value;
updateFrameCalculations(config);
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: unknown parameter %s", key.c_str());
std::cout << "Warning: unknown parameter " << key << '\n';
}
}
@@ -275,7 +273,7 @@ void AnimatedSprite::processAnimationParameter(const std::string& line, Animatio
} else if (key == "frames") {
parseFramesParameter(value, animation, config);
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: unknown parameter %s", key.c_str());
std::cout << "Warning: unknown parameter " << key << '\n';
}
}

View File

@@ -9,7 +9,7 @@
#include <utility> // Para move
#include <vector> // Para vector
#include "moving_sprite.hpp" // for MovingSprite
#include "core/rendering/sprite/moving_sprite.hpp" // for MovingSprite
// Declaración adelantada
class Texture;

View File

@@ -1,11 +1,11 @@
#include "card_sprite.hpp"
#include "core/rendering/sprite/card_sprite.hpp"
#include <algorithm> // Para std::clamp
#include <functional> // Para function
#include <utility> // Para move
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para easeOutBounce, easeOutCubic
#include "core/rendering/texture.hpp" // Para Texture
#include "utils/utils.hpp" // Para easeOutBounce, easeOutCubic
// Constructor
CardSprite::CardSprite(std::shared_ptr<Texture> texture)

View File

@@ -5,7 +5,7 @@
#include <functional> // Para function
#include <memory> // Para shared_ptr
#include "moving_sprite.hpp" // Para MovingSprite
#include "core/rendering/sprite/moving_sprite.hpp" // Para MovingSprite
class Texture;

View File

@@ -1,9 +1,9 @@
#include "moving_sprite.hpp"
#include "core/rendering/sprite/moving_sprite.hpp"
#include <cmath> // Para std::abs
#include <utility>
#include "texture.hpp" // Para Texture
#include "core/rendering/texture.hpp" // Para Texture
// Constructor
MovingSprite::MovingSprite(std::shared_ptr<Texture> texture, SDL_FRect pos, Rotate rotate, float horizontal_zoom, float vertical_zoom, SDL_FlipMode flip)

View File

@@ -4,7 +4,7 @@
#include <memory> // Para shared_ptr
#include "sprite.hpp" // for Sprite
#include "core/rendering/sprite/sprite.hpp" // for Sprite
class Texture;

View File

@@ -1,5 +1,5 @@
// IWYU pragma: no_include <bits/std_abs.h>
#include "path_sprite.hpp"
#include "core/rendering/sprite/path_sprite.hpp"
#include <cmath> // Para abs
#include <functional> // Para function

View File

@@ -7,7 +7,7 @@
#include <utility>
#include <vector> // Para vector
#include "sprite.hpp" // Para Sprite
#include "core/rendering/sprite/sprite.hpp" // Para Sprite
class Texture;

View File

@@ -1,6 +1,6 @@
#include "smart_sprite.hpp"
#include "core/rendering/sprite/smart_sprite.hpp"
#include "moving_sprite.hpp" // Para MovingSprite
#include "core/rendering/sprite/moving_sprite.hpp" // Para MovingSprite
// Actualiza la posición y comprueba si ha llegado a su destino (time-based)
void SmartSprite::update(float delta_time) {

View File

@@ -3,7 +3,7 @@
#include <memory> // Para shared_ptr
#include <utility>
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite
class Texture;

View File

@@ -1,9 +1,9 @@
#include "sprite.hpp"
#include "core/rendering/sprite/sprite.hpp"
#include <utility>
#include <vector> // Para vector
#include "texture.hpp" // Para Texture
#include "core/rendering/texture.hpp" // Para Texture
// Constructor
Sprite::Sprite(std::shared_ptr<Texture> texture, float pos_x, float pos_y, float width, float height)

View File

@@ -1,4 +1,4 @@
#include "text.hpp"
#include "core/rendering/text.hpp"
#include <SDL3/SDL.h> // Para SDL_FRect, Uint8, SDL_GetRenderTarget, SDL_RenderClear, SDL_SetRenderDrawColor, SDL_SetRenderTarget, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_TextureAccess, SDL_GetTextureAlphaMod
@@ -10,13 +10,12 @@
#include <utility> // Para std::cmp_less_equal
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "resource_helper.hpp" // Para loadFile
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
#include "ui/logger.hpp" // Para dots
#include "utils.hpp" // Para getFileName
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/sprite.hpp" // Para Sprite
#include "core/rendering/texture.hpp" // Para Texture
#include "core/resources/resource_helper.hpp" // Para loadFile
#include "utils/color.hpp" // Para Color
#include "utils/utils.hpp" // Para getFileName
// Constructor
Text::Text(const std::shared_ptr<Texture>& texture, const std::string& text_file) {
@@ -432,7 +431,6 @@ auto Text::loadFile(const std::string& file_path) -> std::shared_ptr<Text::File>
line_read++;
};
Logger::dots("Text File : ", getFileName(file_path), "[ LOADED ]");
// Cierra el fichero si se usó
if (!using_resource_data && file.is_open()) {
file.close();

View File

@@ -6,8 +6,8 @@
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string
#include "color.hpp" // Para Color
#include "sprite.hpp" // Para Sprite
#include "core/rendering/sprite/sprite.hpp" // Para Sprite
#include "utils/color.hpp" // Para Color
class Texture;

View File

@@ -1,23 +1,23 @@
#define STB_IMAGE_IMPLEMENTATION
#include "texture.hpp"
#include "core/rendering/texture.hpp"
#include <SDL3/SDL.h> // Para SDL_LogError, SDL_LogCategory, Uint8, SDL_...
#include <cstdint> // Para uint32_t
#include <cstring> // Para memcpy
#include <fstream> // Para basic_ifstream, basic_istream, basic_ios
#include <iostream> // Para std::cout
#include <sstream> // Para basic_istringstream
#include <stdexcept> // Para runtime_error
#include <string> // Para basic_string, char_traits, operator+, string
#include <utility>
#include <vector> // Para vector
#include "color.hpp" // Para getFileName, Color, printWithDots
#include "external/gif.hpp" // Para Gif
#include "resource_helper.hpp" // Para ResourceHelper
#include "stb_image.h" // Para stbi_image_free, stbi_load, STBI_rgb_alpha
#include "ui/logger.hpp"
#include "utils.hpp"
#include "core/rendering/gif.hpp" // Para Gif
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "external/stb_image.h" // Para stbi_image_free, stbi_load, STBI_rgb_alpha
#include "utils/color.hpp" // Para getFileName, Color
#include "utils/utils.hpp"
// Constructor
Texture::Texture(SDL_Renderer* renderer, std::string path)
@@ -39,7 +39,7 @@ Texture::Texture(SDL_Renderer* renderer, std::string path)
surface_ = loadSurface(path_);
// Añade la propia paleta del fichero a la lista
addPaletteFromGifFile(path_, true); // Usar modo silencioso
addPaletteFromGifFile(path_);
// Crea la textura, establece el BlendMode y copia la surface a la textura
createBlank(width_, height_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING);
@@ -80,10 +80,9 @@ auto Texture::loadFromFile(const std::string& file_path) -> bool {
}
if (data == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Fichero no encontrado %s", getFileName(file_path).c_str());
std::cout << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
}
Logger::dots("Texture : ", getFileName(file_path), "[ LOADED ]");
int pitch;
SDL_PixelFormat pixel_format;
@@ -99,12 +98,12 @@ auto Texture::loadFromFile(const std::string& file_path) -> bool {
// Carga la imagen desde una ruta específica
auto* loaded_surface = SDL_CreateSurfaceFrom(width, height, pixel_format, static_cast<void*>(data), pitch);
if (loaded_surface == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to load image %s", file_path.c_str());
std::cout << "Unable to load image " << file_path << '\n';
} else {
// Crea la textura desde los pixels de la surface
new_texture = SDL_CreateTextureFromSurface(renderer_, loaded_surface);
if (new_texture == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create texture from %s! SDL Error: %s", file_path.c_str(), SDL_GetError());
std::cout << "Unable to create texture from " << file_path << "! SDL Error: " << SDL_GetError() << '\n';
} else {
// Obtiene las dimensiones de la imagen
width_ = loaded_surface->w;
@@ -126,7 +125,7 @@ auto Texture::createBlank(int width, int height, SDL_PixelFormat format, SDL_Tex
// Crea una textura sin inicializar
texture_ = SDL_CreateTexture(renderer_, format, access, width, height);
if (texture_ == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create blank texture! SDL Error: %s", SDL_GetError());
std::cout << "Unable to create blank texture! SDL Error: " << SDL_GetError() << '\n';
} else {
width_ = width;
height_ = height;
@@ -236,7 +235,7 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surfa
// Fallback a filesystem directo
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
if (!file) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Fichero no encontrado %s", file_path.c_str());
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
@@ -247,7 +246,7 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surfa
// Leer el contenido del archivo en un buffer
buffer.resize(size);
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al leer el fichero %s", file_path.c_str());
std::cout << "Error al leer el fichero " << file_path << '\n';
throw std::runtime_error("Error al leer el fichero: " + file_path);
}
}
@@ -258,7 +257,7 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surfa
Uint16 h = 0;
std::vector<Uint8> raw_pixels = gif.loadGif(buffer.data(), w, h);
if (raw_pixels.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: No se pudo cargar el GIF %s", file_path.c_str());
std::cout << "Error: No se pudo cargar el GIF " << file_path << '\n';
return nullptr;
}
@@ -273,7 +272,6 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surfa
width_ = w;
height_ = h;
SDL_LogInfo(SDL_LOG_CATEGORY_TEST, "GIF %s cargado correctamente.", file_path.c_str());
return surface;
}
@@ -302,7 +300,7 @@ void Texture::setPaletteColor(int palette, int index, Uint32 color) {
}
// Carga una paleta desde un fichero
auto Texture::loadPaletteFromFile(const std::string& file_path, bool quiet) -> Palette {
auto Texture::loadPaletteFromFile(const std::string& file_path) -> Palette {
Palette palette;
std::vector<Uint8> buffer;
@@ -315,7 +313,7 @@ auto Texture::loadPaletteFromFile(const std::string& file_path, bool quiet) -> P
// Fallback a filesystem directo
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
if (!file) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Fichero no encontrado %s", file_path.c_str());
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
@@ -325,20 +323,16 @@ auto Texture::loadPaletteFromFile(const std::string& file_path, bool quiet) -> P
buffer.resize(size);
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: No se pudo leer completamente el fichero %s", file_path.c_str());
std::cout << "Error: No se pudo leer completamente el fichero " << file_path << '\n';
throw std::runtime_error("Error al leer el fichero: " + file_path);
}
}
if (!quiet) {
Logger::dots("Palette : ", getFileName(file_path), "[ LOADED ]");
}
// Usar la nueva función loadPalette, que devuelve un vector<uint32_t>
GIF::Gif gif;
std::vector<uint32_t> pal = gif.loadPalette(buffer.data());
if (pal.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Advertencia: No se encontró paleta en el archivo %s", file_path.c_str());
std::cout << "Advertencia: No se encontró paleta en el archivo " << file_path << '\n';
return palette; // Devuelve un vector vacío si no hay paleta
}
@@ -347,19 +341,18 @@ auto Texture::loadPaletteFromFile(const std::string& file_path, bool quiet) -> P
palette[i] = (pal[i] << 8) | 0xFF; // Resultado: 0xRRGGBBAA
}
SDL_LogInfo(SDL_LOG_CATEGORY_TEST, "Paleta cargada correctamente desde %s", file_path.c_str());
return palette;
}
// Añade una paleta a la lista
void Texture::addPaletteFromGifFile(const std::string& path, bool quiet) {
palettes_.emplace_back(loadPaletteFromFile(path, quiet));
void Texture::addPaletteFromGifFile(const std::string& path) {
palettes_.emplace_back(loadPaletteFromFile(path));
setPaletteColor(palettes_.size() - 1, 0, 0x00000000);
}
// Añade una paleta a la lista
void Texture::addPaletteFromPalFile(const std::string& path) {
palettes_.emplace_back(readPalFile(path, true)); // Usar modo silencioso
palettes_.emplace_back(readPalFile(path));
setPaletteColor(palettes_.size() - 1, 0, 0x00000000);
}
@@ -375,7 +368,7 @@ void Texture::setPalette(size_t palette) {
auto Texture::getRenderer() -> SDL_Renderer* { return renderer_; }
// Carga una paleta desde un archivo .pal
auto Texture::readPalFile(const std::string& file_path, bool quiet) -> Palette {
auto Texture::readPalFile(const std::string& file_path) -> Palette {
Palette palette{};
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)

View File

@@ -49,7 +49,7 @@ class Texture {
void setAlpha(Uint8 alpha); // Establece el alpha para la modulación
// --- Paletas ---
void addPaletteFromGifFile(const std::string& path, bool quiet = false); // Añade una paleta a la lista
void addPaletteFromGifFile(const std::string& path); // Añade una paleta a la lista
void addPaletteFromPalFile(const std::string& path); // Añade una paleta a la lista
void setPaletteColor(int palette, int index, Uint32 color); // Establece un color de la paleta
void setPalette(size_t palette); // Cambia la paleta de la textura
@@ -76,8 +76,8 @@ class Texture {
// --- Métodos internos ---
auto loadSurface(const std::string& file_path) -> std::shared_ptr<Surface>; // Crea una surface desde un fichero .gif
void flipSurface(); // Vuelca la surface en la textura
static auto loadPaletteFromFile(const std::string& file_path, bool quiet = false) -> Palette; // Carga una paleta desde un fichero
static auto loadPaletteFromFile(const std::string& file_path) -> Palette; // Carga una paleta desde un fichero
void unloadTexture(); // Libera la memoria de la textura
void unloadSurface(); // Libera la surface actual
static auto readPalFile(const std::string& file_path, bool quiet = false) -> Palette; // Carga una paleta desde un archivo .pal
static auto readPalFile(const std::string& file_path) -> Palette; // Carga una paleta desde un archivo .pal
};

View File

@@ -1,4 +1,4 @@
#include "tiled_bg.hpp"
#include "core/rendering/tiled_bg.hpp"
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_CreateTexture, SDL_DestroyTexture, SDL_FRect, SDL_GetRenderTarget, SDL_RenderTexture, SDL_PixelFormat, SDL_TextureAccess
@@ -9,9 +9,9 @@
#include <numbers> // Para pi
#include <string> // Para basic_string
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/sprite.hpp" // Para Sprite
#include "core/resources/resource.hpp" // Para Resource
// Constructor
TiledBG::TiledBG(SDL_FRect pos, TiledBGMode mode)

View File

@@ -2,7 +2,7 @@
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_SetTextureColorMod, SDL_Renderer, SDL_Texture
#include "color.hpp" // Para Color
#include "utils/color.hpp" // Para Color
// --- Enums ---
enum class TiledBGMode : int { // Modos de funcionamiento para el tileado de fondo

View File

@@ -1,6 +1,6 @@
#include "writer.hpp"
#include "core/rendering/writer.hpp"
#include "text.hpp" // Para Text
#include "core/rendering/text.hpp" // Para Text
// Actualiza el objeto (delta_time en ms)
void Writer::update(float delta_time) {

View File

@@ -1,4 +1,4 @@
#include "asset.hpp"
#include "core/resources/asset.hpp"
#include <SDL3/SDL.h> // Para SDL_LogWarn, SDL_LogCategory, SDL_LogError
@@ -11,9 +11,8 @@
#include <sstream> // Para basic_istringstream
#include <stdexcept> // Para runtime_error
#include "resource_helper.hpp" // Para loadFile
#include "ui/logger.hpp" // Para info, section, CYAN
#include "utils.hpp" // Para getFileName
#include "core/resources/resource_helper.hpp" // Para loadFile
#include "utils/utils.hpp" // Para getFileName
// Singleton
Asset* Asset::instance = nullptr;
@@ -37,9 +36,7 @@ void Asset::addToMap(const std::string& file_path, Type type, bool required, boo
// Verificar si ya existe el archivo
if (file_list_.contains(filename)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Asset '%s' already exists, overwriting",
filename.c_str());
std::cout << "Warning: Asset '" << filename << "' already exists, overwriting" << '\n';
}
file_list_.emplace(filename, Item{std::move(full_path), type, required});
@@ -54,9 +51,7 @@ void Asset::add(const std::string& file_path, Type type, bool required, bool abs
void Asset::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) {
std::ifstream file(config_file_path);
if (!file.is_open()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error: Cannot open config file: %s",
config_file_path.c_str());
std::cout << "Error: Cannot open config file: " << config_file_path << '\n';
return;
}
@@ -89,9 +84,7 @@ void Asset::loadFromFile(const std::string& config_file_path, const std::string&
// Verificar que tenemos al menos tipo y ruta
if (parts.size() < 2) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Malformed line %d in config file (insufficient fields)",
line_number);
std::cout << "Warning: Malformed line " << line_number << " in config file (insufficient fields)" << '\n';
continue;
}
@@ -118,14 +111,10 @@ void Asset::loadFromFile(const std::string& config_file_path, const std::string&
addToMap(path, type, required, absolute);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error parsing line %d in config file: %s",
line_number,
e.what());
std::cout << "Error parsing line " << line_number << " in config file: " << e.what() << '\n';
}
}
std::cout << "Loaded " << file_list_.size() << " assets from config file" << '\n';
file.close();
}
@@ -136,7 +125,7 @@ auto Asset::getPath(const std::string& filename) const -> std::string {
return it->second.file;
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: file %s not found", filename.c_str());
std::cout << "Warning: file " << filename << " not found" << '\n';
return "";
}
@@ -147,7 +136,7 @@ auto Asset::loadData(const std::string& filename) const -> std::vector<uint8_t>
return ResourceHelper::loadFile(it->second.file);
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: file %s not found for data loading", filename.c_str());
std::cout << "Warning: file " << filename << " not found for data loading" << '\n';
return {};
}
@@ -160,9 +149,6 @@ auto Asset::exists(const std::string& filename) const -> bool {
auto Asset::check() const -> bool {
bool success = true;
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** CHECKING FILES");
Logger::section("CHECKING FILES", Logger::CYAN);
// Agrupar por tipo para mostrar organizado
std::unordered_map<Type, std::vector<const Item*>> by_type;
@@ -177,19 +163,11 @@ auto Asset::check() const -> bool {
Type asset_type = static_cast<Type>(type);
if (by_type.contains(asset_type)) {
Logger::info(getTypeName(asset_type) + " FILES");
bool type_success = true;
for (const auto* item : by_type[asset_type]) {
if (!checkFile(item->file)) {
success = false;
type_success = false;
}
}
if (type_success) {
Logger::info("All files are OK.\n");
}
}
}
return success;
@@ -215,9 +193,7 @@ auto Asset::checkFile(const std::string& path) const -> bool {
file.close();
if (!success) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error: Could not open file: %s",
path.c_str());
std::cout << "Error: Could not open file: " << path << '\n';
}
return success;

View File

@@ -1,4 +1,4 @@
#include "asset_integrated.hpp"
#include "core/resources/asset_integrated.hpp"
#include <filesystem>
#include <fstream>

View File

@@ -2,8 +2,8 @@
#include <memory>
#include "asset.hpp"
#include "resource_loader.hpp"
#include "core/resources/asset.hpp"
#include "core/resources/resource_loader.hpp"
// Extensión de Asset que integra ResourceLoader
class AssetIntegrated : public Asset {

View File

@@ -1,4 +1,4 @@
#include "resource.hpp"
#include "core/resources/resource.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_LogError, SDL_SetRenderDrawColor, SDL_EventType, SDL_PollEvent, SDL_RenderFillRect, SDL_RenderRect, SDLK_ESCAPE, SDL_Event
@@ -7,20 +7,22 @@
#include <exception> // Para exception
#include <filesystem> // Para exists, path, remove
#include <fstream> // Para basic_ofstream, basic_ios, basic_ostream::write, ios, ofstream
#include <iostream> // Para std::cout
#include <ranges> // Para __find_if_fn, find_if, __find_fn, find
#include <stdexcept> // Para runtime_error
#include <utility> // Para move
#include "asset.hpp" // Para Asset
#include "color.hpp" // Para Color, NO_COLOR_MOD
#include "external/jail_audio.h" // Para JA_LoadMusic, JA_LoadSound, JA_DeleteMusic, JA_DeleteSound
#include "lang.hpp" // Para getText
#include "param.hpp" // Para Param, param, ParamPlayer, ParamResource, ParamGame
#include "resource_helper.hpp" // Para loadFile
#include "screen.hpp" // Para Screen
#include "text.hpp" // Para Text
#include "ui/logger.hpp" // Para info, CR, dots
#include "utils.hpp" // Para getFileName
#include "core/audio/jail_audio.hpp" // Para JA_LoadMusic, JA_LoadSound, JA_DeleteMusic, JA_DeleteSound
#include "core/locale/lang.hpp" // Para getText
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/asset.hpp" // Para Asset
#include "core/resources/resource_helper.hpp" // Para loadFile
#include "game/options.hpp" // Para Options::loading
#include "utils/color.hpp" // Para Color, NO_COLOR_MOD
#include "utils/defines.hpp" // Para Texts::VERSION
#include "utils/param.hpp" // Para Param, param, ParamPlayer, ParamResource, ParamGame
#include "utils/utils.hpp" // Para getFileName
#include "version.h" // Para APP_NAME, GIT_HASH
struct JA_Music_t; // lines 11-11
@@ -65,7 +67,8 @@ Resource::Resource(LoadingMode mode)
Screen::get()->show();
if (loading_mode_ == LoadingMode::PRELOAD) {
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 {
// En modo lazy, cargamos lo mínimo indispensable
initResourceLists();
@@ -80,21 +83,16 @@ Resource::~Resource() {
// Carga los recursos esenciales que siempre se necesitan (modo lazy)
void Resource::loadEssentialResources() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading essential resources for lazy mode");
// Cargar recursos de texto básicos que se usan para crear texturas
loadTextFilesQuiet(); // <- VERSIÓN SILENCIOSA
loadEssentialTextures(); // Ya es silenciosa
createText(); // Crear objetos de texto
createTextTextures(); // Crear texturas generadas (game_text_xxx)
createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Essential resources loaded");
}
// Carga los ficheros de texto del juego (versión silenciosa)
void Resource::loadTextFilesQuiet() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n>> TEXT FILES (quiet load)");
auto list = Asset::get()->getListByType(Asset::Type::FONT);
for (const auto& l : list) {
@@ -103,7 +101,6 @@ void Resource::loadTextFilesQuiet() {
auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> auto { return t.name == name; });
if (it != text_files_.end()) {
it->text_file = Text::loadFile(l);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Text file loaded: %s", name.c_str());
}
}
}
@@ -141,8 +138,6 @@ void Resource::loadEssentialTextures() {
// Inicializa las listas de recursos sin cargar el contenido (modo lazy)
void Resource::initResourceLists() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Initializing resource lists for lazy loading");
// Inicializa lista de sonidos
auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND);
sounds_.clear();
@@ -200,8 +195,6 @@ void Resource::initResourceLists() {
for (const auto& text_name : TEXT_OBJECTS) {
texts_.emplace_back(text_name); // Constructor con nullptr por defecto
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Resource lists initialized for lazy loading");
}
// Obtiene el sonido a partir de un nombre (con carga perezosa)
@@ -216,7 +209,7 @@ auto Resource::getSound(const std::string& name) -> JA_Sound_t* {
return it->sound;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Sonido no encontrado %s", name.c_str());
std::cout << "Error: Sonido no encontrado " << name << '\n';
throw std::runtime_error("Sonido no encontrado: " + name);
}
@@ -232,7 +225,7 @@ auto Resource::getMusic(const std::string& name) -> JA_Music_t* {
return it->music;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Música no encontrada %s", name.c_str());
std::cout << "Error: Música no encontrada " << name << '\n';
throw std::runtime_error("Música no encontrada: " + name);
}
@@ -248,7 +241,7 @@ auto Resource::getTexture(const std::string& name) -> std::shared_ptr<Texture> {
return it->texture;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Imagen no encontrada %s", name.c_str());
std::cout << "Error: Imagen no encontrada " << name << '\n';
throw std::runtime_error("Imagen no encontrada: " + name);
}
@@ -264,7 +257,7 @@ auto Resource::getTextFile(const std::string& name) -> std::shared_ptr<Text::Fil
return it->text_file;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: TextFile no encontrado %s", name.c_str());
std::cout << "Error: TextFile no encontrado " << name << '\n';
throw std::runtime_error("TextFile no encontrado: " + name);
}
@@ -280,7 +273,7 @@ auto Resource::getText(const std::string& name) -> std::shared_ptr<Text> {
return it->text;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Text no encontrado %s", name.c_str());
std::cout << "Error: Text no encontrado " << name << '\n';
throw std::runtime_error("Text no encontrado: " + name);
}
@@ -296,14 +289,14 @@ auto Resource::getAnimation(const std::string& name) -> AnimationsFileBuffer& {
return it->animation;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Animación no encontrada %s", name.c_str());
std::cout << "Error: Animación no encontrada " << name << '\n';
throw std::runtime_error("Animación no encontrada: " + name);
}
// Obtiene el fichero con los datos para el modo demostración a partir de un índice
auto Resource::getDemoData(int index) -> DemoData& {
if (index < 0 || std::cmp_greater_equal(index, demos_.size())) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Index %d out of range for demo data (size: %d)", index, static_cast<int>(demos_.size()));
std::cout << "Index " << index << " out of range for demo data (size: " << static_cast<int>(demos_.size()) << ")" << '\n';
static DemoData empty_demo_;
return empty_demo_;
}
@@ -313,7 +306,6 @@ auto Resource::getDemoData(int index) -> DemoData& {
// --- Métodos de carga perezosa ---
auto Resource::loadSoundLazy(const std::string& name) -> JA_Sound_t* {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading sound lazily: %s", name.c_str());
auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND);
for (const auto& file : sound_list) {
if (getFileName(file) == name) {
@@ -329,7 +321,6 @@ auto Resource::loadSoundLazy(const std::string& name) -> JA_Sound_t* {
}
auto Resource::loadMusicLazy(const std::string& name) -> JA_Music_t* {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading music lazily: %s", name.c_str());
auto music_list = Asset::get()->getListByType(Asset::Type::MUSIC);
for (const auto& file : music_list) {
if (getFileName(file) == name) {
@@ -345,7 +336,6 @@ auto Resource::loadMusicLazy(const std::string& name) -> JA_Music_t* {
}
auto Resource::loadTextureLazy(const std::string& name) -> std::shared_ptr<Texture> {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading texture lazily: %s", name.c_str());
auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP);
for (const auto& file : texture_list) {
if (getFileName(file) == name) {
@@ -356,7 +346,6 @@ auto Resource::loadTextureLazy(const std::string& name) -> std::shared_ptr<Textu
}
auto Resource::loadTextFileLazy(const std::string& name) -> std::shared_ptr<Text::File> {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading text file lazily: %s", name.c_str());
auto text_file_list = Asset::get()->getListByType(Asset::Type::FONT);
for (const auto& file : text_file_list) {
if (getFileName(file) == name) {
@@ -367,8 +356,6 @@ auto Resource::loadTextFileLazy(const std::string& name) -> std::shared_ptr<Text
}
auto Resource::loadTextLazy(const std::string& name) -> std::shared_ptr<Text> {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading text object lazily: %s", name.c_str());
// Mapeo de objetos de texto a sus recursos
struct TextMapping {
std::string key;
@@ -406,7 +393,6 @@ auto Resource::loadTextLazy(const std::string& name) -> std::shared_ptr<Text> {
}
auto Resource::loadAnimationLazy(const std::string& name) -> AnimationsFileBuffer {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loading animation lazily: %s", name.c_str());
auto animation_list = Asset::get()->getListByType(Asset::Type::ANIMATION);
for (const auto& file : animation_list) {
if (getFileName(file) == name) {
@@ -428,31 +414,128 @@ void Resource::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() {
// 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();
initProgressBar();
// Muerstra la ventana y desactiva el sincronismo vertical
auto* screen = Screen::get();
auto vsync = Screen::getVSync();
screen->setVSync(false);
saved_vsync_ = Screen::getVSync();
Screen::get()->setVSync(false); // Maximiza FPS durante el preload
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** LOADING RESOURCES");
loadSounds(); // Carga sonidos
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");
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
}
// Restablece el sincronismo vertical a su valor original
screen->setVSync(vsync);
auto Resource::isLoadDone() const -> bool {
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)
@@ -465,112 +548,82 @@ void Resource::reload() {
}
}
// Carga los sonidos del juego
void Resource::loadSounds() {
Logger::info("SOUND FILES");
// Carga un sonido concreto desde la lista de assets
void Resource::loadOneSound(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::SOUND);
sounds_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
const auto& path = list[idx];
auto name = getFileName(path);
updateLoadingProgress(name);
auto audio_data = loadAudioData(l);
auto audio_data = loadAudioData(path);
JA_Sound_t* sound = nullptr;
if (!audio_data.data.empty()) {
sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size());
} else {
// Fallback a cargar desde disco si no está en pack
sound = JA_LoadSound(l.c_str());
sound = JA_LoadSound(path.c_str());
}
if (sound == nullptr) {
std::cout << "Sound load failed: " << name << '\n';
}
sounds_.emplace_back(name, sound);
Logger::dots("Sound : ", name, "[ LOADED ]");
}
}
// Carga las músicas del juego
void Resource::loadMusics() {
Logger::cr();
Logger::info("MUSIC FILES");
// Carga una música concreta desde la lista de assets
void Resource::loadOneMusic(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::MUSIC);
musics_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
const auto& path = list[idx];
auto name = getFileName(path);
updateLoadingProgress(name);
auto audio_data = loadAudioData(l);
auto audio_data = loadAudioData(path);
JA_Music_t* music = nullptr;
if (!audio_data.data.empty()) {
music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size());
} else {
// Fallback a cargar desde disco si no está en pack
music = JA_LoadMusic(l.c_str());
music = JA_LoadMusic(path.c_str());
}
if (music == nullptr) {
std::cout << "Music load failed: " << name << '\n';
}
musics_.emplace_back(name, music);
Logger::dots("Music : ", name, "[ LOADED ]");
}
}
// Carga las texturas del juego
void Resource::loadTextures() {
Logger::cr();
Logger::info("TEXTURES");
// Carga una textura concreta desde la lista de assets
void Resource::loadOneTexture(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::BITMAP);
textures_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
const auto& path = list[idx];
auto name = getFileName(path);
updateLoadingProgress(name);
textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), l));
}
textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), path));
}
// Carga los ficheros de texto del juego
void Resource::loadTextFiles() {
Logger::cr();
Logger::info("TEXT FILES");
// Carga un fichero de texto concreto desde la lista de assets
void Resource::loadOneTextFile(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::FONT);
text_files_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
const auto& path = list[idx];
auto name = getFileName(path);
updateLoadingProgress(name);
text_files_.emplace_back(name, Text::loadFile(l));
}
text_files_.emplace_back(name, Text::loadFile(path));
}
// Carga las animaciones del juego
void Resource::loadAnimations() {
Logger::cr();
Logger::info("ANIMATIONS");
// Carga una animación concreta desde la lista de assets
void Resource::loadOneAnimation(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::ANIMATION);
animations_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
const auto& path = list[idx];
auto name = getFileName(path);
updateLoadingProgress(name);
animations_.emplace_back(name, loadAnimationsFromFile(l));
}
animations_.emplace_back(name, loadAnimationsFromFile(path));
}
// Carga los datos para el modo demostración
void Resource::loadDemoData() {
Logger::cr();
Logger::info("DEMO FILES");
// Carga un fichero de datos de demo concreto desde la lista de assets
void Resource::loadOneDemoData(size_t idx) {
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
demos_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
const auto& path = list[idx];
auto name = getFileName(path);
updateLoadingProgress(name);
demos_.emplace_back(loadDemoDataFromFile(l));
}
demos_.emplace_back(loadDemoDataFromFile(path));
}
// Crea las texturas de jugadores con todas sus variantes de paleta
void Resource::createPlayerTextures() {
Logger::cr();
Logger::info("CREATING PLAYER TEXTURES");
// Configuración de jugadores y sus paletas
struct PlayerConfig {
std::string base_texture;
@@ -641,7 +694,6 @@ void Resource::createPlayerTextures() {
// Guardar con nombre específico
std::string texture_name = player.name_prefix + "_pal" + std::to_string(palette_idx);
textures_.emplace_back(texture_name, texture);
Logger::dots("Player Texture : ", texture_name, "[ DONE ]");
}
}
}
@@ -657,9 +709,6 @@ void Resource::createTextTextures() {
text(std::move(text_init)) {}
};
Logger::cr();
Logger::info("CREATING TEXTURES");
// Texturas de tamaño normal con outline
std::vector<NameAndText> strings1 = {
{"game_text_1000_points", "1.000"},
@@ -673,7 +722,6 @@ void Resource::createTextTextures() {
auto text1 = getText("04b_25_enhanced");
for (const auto& s : strings1) {
textures_.emplace_back(s.name, text1->writeDXToTexture(Text::STROKE, s.text, -2, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color));
Logger::dots("Texture : ", s.name, "[ DONE ]");
}
// Texturas de tamaño doble
@@ -688,7 +736,6 @@ void Resource::createTextTextures() {
auto text2 = getText("04b_25_2x_enhanced");
for (const auto& s : strings2) {
textures_.emplace_back(s.name, text2->writeDXToTexture(Text::STROKE, s.text, -4, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color));
Logger::dots("Texture : ", s.name, "[ DONE ]");
}
}
@@ -707,9 +754,6 @@ void Resource::createText() {
white_texture_file(std::move(w_file)) {}
};
Logger::cr();
Logger::info("CREATING TEXT OBJECTS");
std::vector<ResourceInfo> resources = {
{"04b_25", "04b_25.png", "04b_25.txt"},
{"04b_25_enhanced", "04b_25.png", "04b_25.txt", "04b_25_white.png"}, // Nueva fuente con textura blanca
@@ -735,7 +779,6 @@ void Resource::createText() {
// Crear texto normal
texts_.emplace_back(resource.key, std::make_shared<Text>(getTexture(resource.texture_file), getTextFile(resource.text_file)));
}
Logger::dots("Text : ", resource.key, "[ DONE ]");
}
}
@@ -793,6 +836,16 @@ void Resource::renderProgress() {
screen->start();
screen->clean();
// 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) {
screen->coreRender();
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 bar_color = param.resource.color.DARKEN(100);
const auto TEXT_HEIGHT = loading_text_->getCharacterSize();
@@ -805,14 +858,20 @@ void Resource::renderProgress() {
SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a);
SDL_RenderRect(renderer, &loading_wired_rect_);
// Escribe el texto de carga encima de la barra
/*
loading_text_->writeColored(
loading_wired_rect_.x,
loading_wired_rect_.y - 9,
Lang::getText("[RESOURCE] LOADING") + " : " + loading_resource_name_,
// Texto centrado sobre la barra: mientras carga, el nombre del recurso;
// 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(
Text::CENTER | Text::COLOR,
loading_wired_rect_.x + (loading_wired_rect_.w / 2),
loading_wired_rect_.y - TEXT_HEIGHT - 2,
OVER_BAR_TEXT,
1,
text_color);
*/
}
// Muestra nombre de la aplicación
loading_text_->writeDX(
@@ -823,56 +882,21 @@ void Resource::renderProgress() {
1,
text_color);
// Muestra la versión
// Muestra la versión y el hash del commit
loading_text_->writeDX(
Text::CENTER | Text::COLOR,
param.game.game_area.center_x,
param.game.game_area.center_y + TEXT_HEIGHT,
"(" + std::string(Version::GIT_HASH) + ")",
"ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")",
1,
text_color);
// Muestra información del monitor desplazada hacia abajo
/*loading_text_->writeColored(
X_PADDING,
Y_PADDING + 18,
screen->getDisplayMonitorName(),
text_color);
loading_text_->writeColored(
X_PADDING,
Y_PADDING + 27,
std::to_string(screen->getDisplayMonitorWidth()) + "x" + std::to_string(screen->getDisplayMonitorHeight()),
text_color);
loading_text_->writeColored(
X_PADDING,
Y_PADDING + 36,
std::to_string(screen->getDisplayMonitorRefreshRate()) + "Hz",
text_color);*/
// Renderiza el frame en pantalla
screen->coreRender();
}
// Comprueba los eventos durante la carga (permite salir con ESC o cerrar ventana)
void Resource::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;
}
}
}
// Carga los datos para el modo demostración (sin mostrar progreso)
void Resource::loadDemoDataQuiet() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n>> DEMO FILES (quiet load)");
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
demos_.clear();
@@ -883,22 +907,22 @@ void Resource::loadDemoDataQuiet() {
// Inicializa los rectangulos que definen la barra de progreso
void Resource::initProgressBar() {
const float BAR_Y_POSITION = param.game.height - BAR_HEIGHT - Y_PADDING;
const float WIRED_BAR_WIDTH = param.game.width * BAR_WIDTH_RATIO;
const float BAR_X_POSITION = (param.game.width - WIRED_BAR_WIDTH) / 2.0F;
const float BAR_Y_POSITION = (param.game.height * BAR_Y_RATIO) - (BAR_HEIGHT / 2.0F);
const float WIRED_BAR_WIDTH = param.game.width - (X_PADDING * 2);
loading_wired_rect_ = {.x = X_PADDING, .y = BAR_Y_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
loading_wired_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * loading_count_.getPercentage();
loading_full_rect_ = {.x = X_PADDING, .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, muestra la barra y procesa eventos
// 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) {
loading_resource_name_ = std::move(name);
loading_count_.increase();
updateProgressBar();
renderProgress();
checkEvents();
}
// Actualiza la barra de estado

View File

@@ -8,10 +8,10 @@
#include <utility> // Para move
#include <vector> // Para vector
#include "animated_sprite.hpp" // Para AnimationsFileBuffer
#include "demo.hpp" // Para DemoData
#include "text.hpp" // Para Text, TextFile
#include "texture.hpp" // Para Texture
#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimationsFileBuffer
#include "core/rendering/text.hpp" // Para Text, TextFile
#include "core/rendering/texture.hpp" // Para Texture
#include "core/system/demo.hpp" // Para DemoData
struct JA_Music_t;
struct JA_Sound_t;
@@ -42,6 +42,15 @@ class Resource {
// --- Métodos de recarga de 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 ---
[[nodiscard]] auto getLoadingMode() const -> LoadingMode { return loading_mode_; }
@@ -117,12 +126,15 @@ class Resource {
}
};
// --- Constantes para la pantalla de carga ---
static constexpr float X_PADDING = 60.0F;
static constexpr float Y_PADDING = 20.0F;
// --- Constantes para la barra de progreso de la pantalla de carga ---
// BAR_WIDTH_RATIO: fracción del ancho de la pantalla ocupada por la barra (centrada)
// 1.0 = ancho completo, 0.5 = la mitad del ancho
// BAR_Y_RATIO: posición vertical del CENTRO de la barra en fracción de la altura
// 0.0 = centro en el borde superior, 1.0 = centro en el borde inferior
// BAR_HEIGHT: grosor de la barra en píxeles
static constexpr float BAR_WIDTH_RATIO = 0.5F;
static constexpr float BAR_Y_RATIO = 0.85F;
static constexpr float BAR_HEIGHT = 5.0F;
static constexpr Color BAR_COLOR = Color(128, 128, 128);
static constexpr Color TEXT_COLOR = Color(255, 255, 255);
// --- Modo de carga ---
LoadingMode loading_mode_;
@@ -143,13 +155,24 @@ class Resource {
SDL_FRect loading_wired_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 ---
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 loadEssentialResources(); // Carga recursos esenciales en modo lazy
void loadEssentialTextures(); // Carga solo las texturas esenciales (fuentes)
@@ -173,12 +196,18 @@ class Resource {
// --- Métodos internos para gestionar el progreso ---
void calculateTotalResources(); // Calcula el número de recursos para cargar
void renderProgress(); // Muestra el progreso de carga
static void checkEvents(); // Comprueba los eventos durante la carga
void updateLoadingProgress(std::string name); // Actualiza el progreso de carga
void initProgressBar(); // Inicializa los rectangulos que definen la barra de progreso
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) ---
explicit Resource(LoadingMode mode); // Constructor privado con modo de carga
~Resource(); // Destructor privado

View File

@@ -1,11 +1,11 @@
#include "resource_helper.hpp"
#include "core/resources/resource_helper.hpp"
#include <algorithm> // Para replace
#include <cstddef> // Para size_t
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ios, operator<<, ios, basic_istream, endl, operator|, basic_istream::read, basic_istream::seekg, basic_istream::tellg, ifstream, streamsize
#include <iostream> // Para cout
#include "resource_loader.hpp" // Para ResourceLoader
#include "core/resources/resource_loader.hpp" // Para ResourceLoader
namespace ResourceHelper {
static bool resource_system_initialized = false;

View File

@@ -1,11 +1,11 @@
#include "resource_loader.hpp"
#include "core/resources/resource_loader.hpp"
#include <algorithm> // Para replace
#include <filesystem> // Para exists, path, recursive_directory_iterator, directory_entry, relative
#include <fstream> // Para basic_ostream, basic_ifstream, operator<<, basic_ios, endl, ios, basic_istream, operator|, basic_istream::read, basic_istream::seekg, basic_istream::tellg, ifstream, streamsize
#include <iostream> // Para cerr, cout
#include "resource_pack.hpp" // Para ResourcePack
#include "core/resources/resource_pack.hpp" // Para ResourcePack
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
@@ -33,8 +33,6 @@ auto ResourceLoader::initialize(const std::string& pack_file, bool enable_fallba
if (std::filesystem::exists(pack_file)) {
resource_pack_ = new ResourcePack();
if (resource_pack_->loadPack(pack_file)) {
std::cout << "Resource pack loaded successfully: " << pack_file << '\n';
std::cout << "Resources available: " << resource_pack_->getResourceCount() << '\n';
return true;
}
delete resource_pack_;
@@ -43,7 +41,6 @@ auto ResourceLoader::initialize(const std::string& pack_file, bool enable_fallba
}
if (fallback_to_files_) {
std::cout << "Using fallback mode: loading resources from data/ directory" << '\n';
return true;
}

View File

@@ -1,4 +1,4 @@
#include "resource_pack.hpp"
#include "core/resources/resource_pack.hpp"
#include <algorithm> // Para replace
#include <array> // Para array

View File

@@ -4,7 +4,7 @@
#include <array>
#include "ui/notifier.hpp" // Para Notifier::Position
#include "game/ui/notifier.hpp" // Para Notifier::Position
namespace Defaults::Game {
constexpr float WIDTH = 320.0F;
@@ -195,17 +195,17 @@ namespace Defaults::Video {
namespace Defaults::Music {
constexpr bool ENABLED = true;
constexpr int VOLUME = 100;
constexpr float VOLUME = 0.8F;
} // namespace Defaults::Music
namespace Defaults::Sound {
constexpr bool ENABLED = true;
constexpr int VOLUME = 100;
constexpr float VOLUME = 1.0F;
} // namespace Defaults::Sound
namespace Defaults::Audio {
constexpr bool ENABLED = true;
constexpr int VOLUME = 100;
constexpr float VOLUME = 1.0F;
} // namespace Defaults::Audio
namespace Defaults::Settings {
@@ -213,3 +213,9 @@ namespace Defaults::Settings {
constexpr bool SHUTDOWN_ENABLED = false;
constexpr const char* PARAMS_FILE = "param_320x256.txt";
} // namespace Defaults::Settings
namespace Defaults::Loading {
constexpr bool SHOW = false;
constexpr bool SHOW_RESOURCE_NAME = true;
constexpr bool WAIT_FOR_INPUT = false;
} // namespace Defaults::Loading

View File

@@ -1,12 +1,12 @@
#include "demo.hpp"
#include "core/system/demo.hpp"
#include <SDL3/SDL.h> // Para SDL_IOStream, SDL_IOFromConstMem, SDL_IOFromFile, SDL_ReadIO, SDL_WriteIO, SDL_CloseIO
#include <iostream> // Para std::cout
#include <stdexcept> // Para runtime_error
#include "resource_helper.hpp" // Para ResourceHelper
#include "ui/logger.hpp"
#include "utils.hpp" // Para printWithDots, getFileName
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "utils/utils.hpp" // Para getFileName
// Carga el fichero de datos para la demo
auto loadDemoDataFromFile(const std::string& file_path) -> DemoData {
@@ -24,10 +24,9 @@ auto loadDemoDataFromFile(const std::string& file_path) -> DemoData {
}
if (file == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Fichero no encontrado %s", file_path.c_str());
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
Logger::dots("DemoData : ", getFileName(file_path), "[ LOADED ]");
// Lee todos los datos del fichero y los deja en el destino
for (int i = 0; i < TOTAL_DEMO_DATA; ++i) {
@@ -52,18 +51,15 @@ bool saveDemoFile(const std::string& file_path, const DemoData& dd) {
// Guarda los datos
for (const auto& data : dd) {
if (SDL_WriteIO(file, &data, sizeof(DemoKeys)) != sizeof(DemoKeys)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error al escribir el fichero %s", getFileName(file_path).c_str());
std::cout << "Error al escribir el fichero " << getFileName(file_path) << '\n';
success = false;
break;
}
}
if (success) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Writing file %s", getFileName(file_path).c_str());
}
// Cierra el fichero
SDL_CloseIO(file);
} else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Unable to save %s file! %s", getFileName(file_path).c_str(), SDL_GetError());
std::cout << "Error: Unable to save " << getFileName(file_path) << " file! " << SDL_GetError() << '\n';
success = false;
}

View File

@@ -0,0 +1,558 @@
// IWYU pragma: no_include <bits/chrono.h>
#include "core/system/director.hpp"
#include <SDL3/SDL.h> // Para SDL_SetLogPriority, SDL_LogCategory, SDL_LogPriority, SDL_Quit
#include <cstdlib> // Para srand, exit, rand, EXIT_FAILURE
#include <ctime> // Para time
#include <fstream> // Para ifstream, ofstream
#include <iostream> // Para basic_ostream, operator<<, cerr
#include <memory> // Para make_unique, unique_ptr
#include <stdexcept> // Para runtime_error
#include <string> // Para allocator, basic_string, char_traits, operator+, string, operator==
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/input.hpp" // Para Input
#include "core/locale/lang.hpp" // Para setLanguage
#include "core/rendering/screen.hpp" // Para Screen
#include "core/resources/asset.hpp" // Para Asset
#include "core/resources/resource.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para initializeResourceSystem
#include "core/system/global_events.hpp" // Para GlobalEvents::handle
#include "core/system/section.hpp" // Para Name, Options, name, options, AttractMode, attract_mode
#include "core/system/shutdown.hpp" // Para resultToString, shutdownSystem, ShutdownResult
#include "core/system/system_utils.hpp" // Para createApplicationFolder, resultToString, Result
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/entities/player.hpp" // Para Player
#include "game/gameplay/manage_hiscore_table.hpp" // Para ManageHiScoreTable
#include "game/options.hpp" // Para Settings, loadFromFile, saveToFile, settings, setConfigFile, setControllersFile
#include "game/scenes/credits.hpp" // Para Credits
#include "game/scenes/game.hpp" // Para Game
#include "game/scenes/hiscore_table.hpp" // Para HiScoreTable
#include "game/scenes/instructions.hpp" // Para Instructions
#include "game/scenes/intro.hpp" // Para Intro
#include "game/scenes/logo.hpp" // Para Logo
#include "game/scenes/preload.hpp" // Para Preload
#include "game/scenes/title.hpp" // Para Title
#include "game/ui/notifier.hpp" // Para Notifier
#include "game/ui/service_menu.hpp" // Para ServiceMenu
#include "utils/param.hpp" // Para loadParamsFromFile
// Constructor
Director::Director() {
Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO;
// Establece el nivel de prioridad de la categoría de registro
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
SDL_SetLogPriority(SDL_LOG_CATEGORY_TEST, SDL_LOG_PRIORITY_ERROR);
// Inicia la semilla aleatoria usando el tiempo actual en segundos
std::srand(static_cast<unsigned int>(std::time(nullptr)));
std::cout << "Game start\n";
// Obtener la ruta del ejecutable desde SDL
#ifdef __EMSCRIPTEN__
// En Emscripten los assets viven en la raíz del MEMFS virtual (/data, /config),
// preloaded vía --preload-file en el linker. No hay ruta de ejecutable.
executable_path_ = "";
#else
const char* base_path = SDL_GetBasePath();
executable_path_ = (base_path != nullptr) ? base_path : "";
#endif
// Crea la carpeta del sistema donde guardar los datos persistentes
createSystemFolder("jailgames");
createSystemFolder("jailgames/coffee_crisis_arcade_edition");
// Establecer sección inicial según modo de compilación
#ifdef RECORDING
Section::name = Section::Name::GAME;
Section::options = Section::Options::GAME_PLAY_1P;
#elif _DEBUG
loadDebugConfig();
#else
Section::name = Section::Name::LOGO;
Section::options = Section::Options::NONE;
#endif
init();
}
Director::~Director() {
// Libera las secciones primero: sus destructores pueden tocar Audio/Resource/Screen,
// que close() destruye a continuación.
resetActiveSection();
close();
}
// Inicializa todo
void Director::init() {
// Configuración inicial de parametros
Asset::init(executable_path_); // Inicializa el sistema de gestión de archivos
// Determinar ruta del pack según la plataforma
#ifdef MACOS_BUNDLE
std::string pack_path = executable_path_ + "../Resources/resources.pack";
#else
std::string pack_path = executable_path_ + "resources.pack";
#endif
// Inicializar sistema de recursos con o sin fallback según el tipo de build
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// Release nativo: Sin fallback - Solo resources.pack (estricto)
ResourceHelper::initializeResourceSystem(pack_path, false);
#else
// Desarrollo o Emscripten: Con fallback - carga desde filesystem/MEMFS
ResourceHelper::initializeResourceSystem(pack_path, true);
#endif
loadAssets(); // Crea el índice de archivos
Input::init(Asset::get()->getPath("gamecontrollerdb.txt"), Asset::get()->getPath("controllers.json")); // Carga configuración de controles
Options::setConfigFile(Asset::get()->getPath("config.yaml")); // Establece el fichero de configuración
Options::setControllersFile(Asset::get()->getPath("controllers.json")); // Establece el fichero de configuración de mandos
Options::setPostFXFile(Asset::get()->getPath("postfx.yaml")); // Establece el fichero de presets PostFX
Options::setCrtPiFile(Asset::get()->getPath("crtpi.yaml")); // Establece el fichero de presets CrtPi
Options::loadFromFile(); // Carga el archivo de configuración
Options::loadPostFXFromFile(); // Carga los presets PostFX
Options::loadCrtPiFromFile(); // Carga los presets CrtPi
#ifdef __EMSCRIPTEN__
// En la versión web el navegador gestiona la ventana: ventana (no
// fullscreen — el canvas ya marca el área), integer scale para píxeles nítidos.
Options::window.zoom = 3;
Options::video.fullscreen = false;
Options::video.integer_scale = true;
// Precarga silenciosa: pantalla negra mientras el .data termina de descargarse.
Options::loading.show = false;
Options::loading.wait_for_input = false;
#endif
loadParams(); // Carga los parámetros del programa
loadScoreFile(); // Carga el archivo de puntuaciones
// Inicialización de subsistemas principales
Lang::setLanguage(Options::settings.language); // Carga el archivo de idioma
Screen::init(); // Inicializa la pantalla y el sistema de renderizado
Audio::init(); // Activa el sistema de audio
#ifdef _DEBUG
Resource::init(debug_config.resource_loading == "lazy" ? Resource::LoadingMode::LAZY_LOAD : Resource::LoadingMode::PRELOAD);
#else
Resource::init(Resource::LoadingMode::PRELOAD);
#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
Notifier::init(std::string(), Resource::get()->getText("8bithud")); // Inicialización del sistema de notificaciones
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
void Director::close() {
// Guarda las opciones actuales en el archivo de configuración
Options::saveToFile();
// Libera los singletons y recursos en orden inverso al de inicialización
Notifier::destroy(); // Libera el sistema de notificaciones
ServiceMenu::destroy(); // Libera el sistema de menú de servicio
Input::destroy(); // Libera el sistema de entrada
Resource::destroy(); // Libera el sistema de recursos gráficos y de texto
Audio::destroy(); // Libera el sistema de audio
Screen::destroy(); // Libera el sistema de pantalla y renderizado
Asset::destroy(); // Libera el gestor de archivos
std::cout << "\nBye!\n";
// Libera todos los recursos de SDL
SDL_Quit();
// Apaga el sistema
shutdownSystem(Section::options == Section::Options::SHUTDOWN);
}
// Carga los parametros
void Director::loadParams() {
// Carga los parametros para configurar el juego
#ifdef ANBERNIC
const std::string PARAM_FILE_PATH = Asset::get()->getPath("param_320x240.txt");
#else
const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_file);
#endif
loadParamsFromFile(PARAM_FILE_PATH);
}
// Carga el fichero de puntuaciones
void Director::loadScoreFile() {
auto manager = std::make_unique<ManageHiScoreTable>(Options::settings.hi_score_table);
#ifdef _DEBUG
manager->clear();
#else
manager->loadFromFile(Asset::get()->getPath("score.bin"));
#endif
}
// Carga el indice de ficheros desde un fichero
void Director::loadAssets() {
#ifdef MACOS_BUNDLE
const std::string PREFIX = "/../Resources";
#else
const std::string PREFIX;
#endif
// Cargar la configuración de assets (también aplicar el prefijo al archivo de configuración)
std::string config_path = executable_path_ + PREFIX + "/config/assets.txt";
Asset::get()->loadFromFile(config_path, PREFIX, system_folder_);
// Si falta algun fichero, sale del programa
if (!Asset::get()->check()) {
throw std::runtime_error("Falta algun fichero");
}
}
// Carga debug.yaml desde la carpeta del sistema (solo en _DEBUG)
void Director::loadDebugConfig() {
const std::string DEBUG_FILE = system_folder_ + "/debug.yaml";
std::ifstream file(DEBUG_FILE);
if (!file.good()) {
// Crear fichero por defecto
std::ofstream out(DEBUG_FILE);
if (out.is_open()) {
out << "# Coffee Crisis Arcade Edition - Debug Configuration\n";
out << "# This file is only read in DEBUG builds.\n";
out << "#\n";
out << "# initial_section: logo, intro, title, game, credits, instructions, hiscore\n";
out << "# initial_options: none, 1p, 2p, both\n";
out << "# initial_stage: 0-based stage index (only when section is game)\n";
out << "# show_render_info: show FPS/driver/preset overlay\n";
out << "# resource_loading: preload, lazy\n";
out << "\n";
out << "initial_section: game\n";
out << "initial_options: 1p\n";
out << "initial_stage: 0\n";
out << "show_render_info: true\n";
out << "resource_loading: preload\n";
out << "autoplay: false\n";
out << "invincibility: false\n";
out.close();
}
// Usar defaults de DebugConfig
} else {
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("initial_section")) {
try {
debug_config.initial_section = yaml["initial_section"].get_value<std::string>();
} catch (...) {}
}
if (yaml.contains("initial_options")) {
try {
debug_config.initial_options = yaml["initial_options"].get_value<std::string>();
} catch (...) {}
}
if (yaml.contains("initial_stage")) {
try {
debug_config.initial_stage = yaml["initial_stage"].get_value<int>();
} catch (...) {}
}
if (yaml.contains("show_render_info")) {
try {
debug_config.show_render_info = yaml["show_render_info"].get_value<bool>();
} catch (...) {}
}
if (yaml.contains("resource_loading")) {
try {
debug_config.resource_loading = yaml["resource_loading"].get_value<std::string>();
} catch (...) {}
}
if (yaml.contains("autoplay")) {
try {
debug_config.autoplay = yaml["autoplay"].get_value<bool>();
} catch (...) {}
}
if (yaml.contains("invincibility")) {
try {
debug_config.invincibility = yaml["invincibility"].get_value<bool>();
} catch (...) {}
}
} catch (...) {
std::cout << "Error parsing debug.yaml, using defaults" << '\n';
}
}
// Mapear strings a enums
const auto& sec = debug_config.initial_section;
if (sec == "logo") {
Section::name = Section::Name::LOGO;
} else if (sec == "intro") {
Section::name = Section::Name::INTRO;
} else if (sec == "title") {
Section::name = Section::Name::TITLE;
} else if (sec == "game") {
Section::name = Section::Name::GAME;
} else if (sec == "credits") {
Section::name = Section::Name::CREDITS;
} else if (sec == "instructions") {
Section::name = Section::Name::INSTRUCTIONS;
} else if (sec == "hiscore") {
Section::name = Section::Name::HI_SCORE_TABLE;
} else {
Section::name = Section::Name::GAME;
}
const auto& opt = debug_config.initial_options;
if (opt == "none") {
Section::options = Section::Options::NONE;
} else if (opt == "1p") {
Section::options = Section::Options::GAME_PLAY_1P;
} else if (opt == "2p") {
Section::options = Section::Options::GAME_PLAY_2P;
} else if (opt == "both") {
Section::options = Section::Options::GAME_PLAY_BOTH;
} else {
Section::options = Section::Options::GAME_PLAY_1P;
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) {
auto result = SystemUtils::createApplicationFolder(folder, system_folder_);
if (result != SystemUtils::Result::SUCCESS) {
std::cerr << "Error creando carpeta del sistema: "
<< SystemUtils::resultToString(result) << '\n';
exit(EXIT_FAILURE);
}
}
// Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez)
void Director::resetActiveSection() {
preload_.reset();
logo_.reset();
intro_.reset();
title_.reset();
game_.reset();
instructions_.reset();
hi_score_table_.reset();
credits_.reset();
}
// Destruye la sección anterior y construye la nueva cuando Section::name cambia
void Director::handleSectionTransition() {
// RESET: recarga recursos y vuelve a LOGO (el propio reset() cambia Section::name)
if (Section::name == Section::Name::RESET) {
resetActiveSection(); // libera recursos actuales antes del reload
reset();
}
if (Section::name == last_built_section_name_) {
return; // ya tenemos la sección correcta viva
}
// Destruye la sección anterior
resetActiveSection();
// Construye la nueva
switch (Section::name) {
case Section::Name::PRELOAD:
preload_ = std::make_unique<Preload>();
break;
case Section::Name::LOGO:
logo_ = std::make_unique<Logo>();
break;
case Section::Name::INTRO:
intro_ = std::make_unique<Intro>();
break;
case Section::Name::TITLE:
title_ = std::make_unique<Title>();
break;
case Section::Name::GAME: {
Player::Id player_id = Player::Id::PLAYER1;
switch (Section::options) {
case Section::Options::GAME_PLAY_1P:
player_id = Player::Id::PLAYER1;
break;
case Section::Options::GAME_PLAY_2P:
player_id = Player::Id::PLAYER2;
break;
case Section::Options::GAME_PLAY_BOTH:
player_id = Player::Id::BOTH_PLAYERS;
break;
default:
break;
}
#ifdef _DEBUG
const int CURRENT_STAGE = debug_config.initial_stage;
#else
constexpr int CURRENT_STAGE = 0;
#endif
game_ = std::make_unique<Game>(player_id, CURRENT_STAGE, Game::DEMO_OFF);
break;
}
case Section::Name::GAME_DEMO: {
const auto PLAYER_ID = static_cast<Player::Id>((rand() % 2) + 1);
constexpr auto CURRENT_STAGE = 0;
game_ = std::make_unique<Game>(PLAYER_ID, CURRENT_STAGE, Game::DEMO_ON);
break;
}
case Section::Name::INSTRUCTIONS:
instructions_ = std::make_unique<Instructions>();
break;
case Section::Name::CREDITS:
credits_ = std::make_unique<Credits>();
break;
case Section::Name::HI_SCORE_TABLE:
hi_score_table_ = std::make_unique<HiScoreTable>();
break;
case Section::Name::RESET:
case Section::Name::QUIT:
default:
break;
}
last_built_section_name_ = Section::name;
}
// Reinicia objetos y vuelve a la sección inicial
void Director::reset() {
Options::saveToFile();
Options::loadFromFile();
Lang::setLanguage(Options::settings.language);
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
Resource::get()->reload();
ServiceMenu::get()->reset();
Section::name = Section::Name::LOGO;
}
// Avanza un frame de la sección activa (llamado desde SDL_AppIterate)
auto Director::iterate() -> SDL_AppResult {
if (Section::name == Section::Name::QUIT) {
return SDL_APP_SUCCESS;
}
// Fase de boot: carga incremental frame a frame con presupuesto de 50ms.
// Durante esta fase la escena activa es Preload (una barra de progreso).
if (boot_loading_) {
try {
if (Resource::get()->loadStep(50 /*ms*/)) {
finishBoot();
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)
handleSectionTransition();
// Ejecuta un frame de la sección activa
if (preload_) {
preload_->iterate();
} else if (logo_) {
logo_->iterate();
} else if (intro_) {
intro_->iterate();
} else if (title_) {
title_->iterate();
} else if (game_) {
game_->iterate();
} else if (instructions_) {
instructions_->iterate();
} else if (hi_score_table_) {
hi_score_table_->iterate();
} else if (credits_) {
credits_->iterate();
}
return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
// Procesa un evento SDL (llamado desde SDL_AppEvent)
auto Director::handleEvent(SDL_Event& event) -> SDL_AppResult {
// Eventos globales (SDL_EVENT_QUIT, resize, render target reset, hotplug, service menu, ratón)
GlobalEvents::handle(event);
// Reenvía a la sección activa
if (preload_) {
preload_->handleEvent(event);
} else if (logo_) {
logo_->handleEvent(event);
} else if (intro_) {
intro_->handleEvent(event);
} else if (title_) {
title_->handleEvent(event);
} else if (game_) {
game_->handleEvent(event);
} else if (instructions_) {
instructions_->handleEvent(event);
} else if (hi_score_table_) {
hi_score_table_->handleEvent(event);
} else if (credits_) {
credits_->handleEvent(event);
}
return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
// Apaga el sistema de forma segura
void Director::shutdownSystem(bool should_shutdown) {
if (should_shutdown) {
auto result = SystemShutdown::shutdownSystem(5, true); // 5 segundos, forzar apps
if (result != SystemShutdown::ShutdownResult::SUCCESS) {
std::cerr << SystemShutdown::resultToString(result) << '\n';
}
}
}

View File

@@ -0,0 +1,95 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_AppResult, SDL_Event
#include <memory> // Para unique_ptr
#include <string> // Para string
#include "core/system/section.hpp" // Para Section::Name
namespace Lang {
enum class Code : int;
}
// Declaraciones adelantadas de las secciones
class Preload;
class Logo;
class Intro;
class Title;
class Game;
class Instructions;
class HiScoreTable;
class Credits;
// --- Clase Director: gestor principal de la aplicación ---
class Director {
public:
// --- Constructor y destructor ---
Director();
~Director();
// --- Callbacks para SDL_MAIN_USE_CALLBACKS ---
auto iterate() -> SDL_AppResult; // Avanza un frame de la sección activa
auto handleEvent(SDL_Event& event) -> SDL_AppResult; // Procesa un evento SDL
// --- Debug config (accesible desde otras clases) ---
struct DebugConfig {
std::string initial_section;
std::string initial_options;
int initial_stage = 0;
bool show_render_info = true;
std::string resource_loading;
bool autoplay = false;
bool invincibility = false;
DebugConfig()
: initial_section("game"),
initial_options("1p"),
resource_loading("preload") {}
};
static inline DebugConfig debug_config;
private:
// --- Variables internas ---
std::string executable_path_; // Ruta del ejecutable
std::string system_folder_; // Carpeta del sistema para almacenar datos
// --- 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<Intro> intro_;
std::unique_ptr<Title> title_;
std::unique_ptr<Game> game_;
std::unique_ptr<Instructions> instructions_;
std::unique_ptr<HiScoreTable> hi_score_table_;
std::unique_ptr<Credits> credits_;
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 ---
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
// --- Configuración inicial ---
static void loadParams(); // Carga los parámetros del programa
static void loadScoreFile(); // Carga el fichero de puntuaciones
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema
void loadDebugConfig(); // Carga debug.yaml (solo en _DEBUG)
// --- Gestión de entrada y archivos ---
void loadAssets(); // Crea el índice de archivos disponibles
// --- Gestión de secciones ---
void handleSectionTransition(); // Destruye la sección anterior y construye la nueva si Section::name ha cambiado
void resetActiveSection(); // Libera todos los unique_ptr de sección
static void reset(); // Reinicia objetos y vuelve a la sección inicial
// --- Gestión de archivos de idioma ---
auto getLangFile(Lang::Code code) -> std::string; // Obtiene un fichero de idioma según el código
// --- Apagado del sistema ---
static void shutdownSystem(bool should_shutdown); // Apaga el sistema
};

View File

@@ -0,0 +1,97 @@
#include "core/system/global_events.hpp"
#include <SDL3/SDL.h> // Para SDL_EventType, SDL_Event, SDL_LogInfo, SDL_LogCategory
#include <cstddef> // Para size_t
#include <iostream> // Para std::cout
#include <string> // Para allocator, operator+, string
#include <vector> // Para vector
#include "core/input/input.hpp" // Para Input
#include "core/input/mouse.hpp" // Para handleEvent
#include "core/locale/lang.hpp" // Para getText
#include "core/rendering/screen.hpp" // Para Screen
#include "core/system/section.hpp" // Para Name, Options, name, options
#include "game/options.hpp" // Para GamepadManager, gamepad_manager
#include "game/ui/notifier.hpp" // Para Notifier
#include "game/ui/service_menu.hpp" // Para ServiceMenu
namespace GlobalEvents {
namespace {
// Durante el arranque se drenan los SDL_EVENT_GAMEPAD_ADDED de los mandos
// ya conectados. Esos eventos sí deben reasignar mandos a jugadores, pero
// no deben mostrar notificación: no son hotplug, son detección inicial.
bool startup_in_progress = true;
} // namespace
// Comprueba los eventos de Input y muestra notificaciones
void handleInputEvents(const SDL_Event& event) {
if (event.type != SDL_EVENT_GAMEPAD_ADDED && event.type != SDL_EVENT_GAMEPAD_REMOVED) {
return;
}
static auto* input_ = Input::get();
auto message = input_->handleEvent(event);
// Reasignar siempre: tanto en arranque como en hotplug en caliente.
Options::gamepad_manager.assignAndLinkGamepads();
Options::gamepad_manager.resyncGamepadsWithPlayers();
// 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()) {
return;
}
// Reemplazo de palabras clave por texto localizado
size_t pos;
while ((pos = message.find(" CONNECTED")) != std::string::npos) {
message.replace(pos, std::string(" CONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] CONNECTED"));
}
while ((pos = message.find(" DISCONNECTED")) != std::string::npos) {
message.replace(pos, std::string(" DISCONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] DISCONNECTED"));
}
// 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() {
startup_in_progress = false;
}
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_QUIT: // Evento de salida de la aplicación
Section::name = Section::Name::QUIT;
Section::options = Section::Options::NONE;
return;
case SDL_EVENT_RENDER_DEVICE_RESET:
case SDL_EVENT_RENDER_TARGETS_RESET:
std::cout << "SDL_RENDER_TARGETS_RESET" << '\n';
break;
case SDL_EVENT_WINDOW_RESIZED:
Screen::initShaders();
break;
default:
break;
}
// Durante el preload ServiceMenu aún no existe
if (ServiceMenu::get() != nullptr) {
ServiceMenu::get()->handleEvent(event);
}
Mouse::handleEvent(event);
handleInputEvents(event);
}
} // namespace GlobalEvents

View File

@@ -0,0 +1,15 @@
#pragma once
#include <SDL3/SDL.h>
// --- Namespace GlobalEvents: maneja eventos globales del juego ---
namespace GlobalEvents {
// --- Funciones ---
void handle(const SDL_Event& event); // Comprueba los eventos que se pueden producir en cualquier sección del juego
// Marca el fin del arranque: a partir de aquí, los eventos de mando
// generan notificaciones en pantalla. Se debe llamar desde Director::iterate
// en el primer fotograma, después de que SDL haya drenado los
// SDL_EVENT_GAMEPAD_ADDED de los mandos ya conectados al iniciar.
void markStartupComplete();
} // namespace GlobalEvents

View File

@@ -10,6 +10,7 @@ namespace Section {
// --- Enumeraciones de secciones del programa ---
enum class Name {
RESET, // Inicialización
PRELOAD, // Carga incremental de recursos
LOGO, // Pantalla de logo
INTRO, // Introducción
TITLE, // Pantalla de título/menú principal
@@ -43,6 +44,7 @@ namespace Section {
// --- Variables globales de estado ---
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 AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;
} // namespace Section

View File

@@ -1,4 +1,4 @@
#include "shutdown.hpp"
#include "core/system/shutdown.hpp"
#include <sys/types.h> // Para pid_t

View File

@@ -1,4 +1,4 @@
#include "system_utils.hpp"
#include "core/system/system_utils.hpp"
#include <sys/stat.h> // Para stat, mkdir, S_ISDIR

Some files were not shown because too many files have changed in this diff Show More