33 Commits
v2.3.3 ... main

Author SHA1 Message Date
6246b5d89d normalitzat Audio 2026-04-18 11:42:29 +02:00
34a41ad25c cppcheck 2026-04-18 07:48:05 +02:00
20b9a95619 cppcheck 2026-04-17 22:20:37 +02:00
513eacf356 singletons 2026-04-17 21:27:30 +02:00
5889df2a47 presets en yaml 2026-04-17 19:56:43 +02:00
7f703390f9 modernitzat el sistema d'opcions 2026-04-17 19:36:40 +02:00
1bb0ebdef8 sdl3gpu 2026-04-17 19:04:44 +02:00
5fec0110b3 reestructuració 2026-04-17 17:15:38 +02:00
55caef3210 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
007c1d3554 fix: pack_resources anava a la rel en comptes de build/
Canviar CMAKE_RUNTIME_OUTPUT_DIRECTORY global per set_target_properties
per-target alinea el comportament amb la resta de projectes i evita que
pack_resources aparega a la rel del projecte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:44:26 +02:00
28606a9fe1 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:51 +02:00
294e665b11 fix emscripten 2026-04-15 23:52:52 +02:00
0faa605ad9 resource.pack 2026-04-15 23:26:43 +02:00
c3534ace9c fix: el fade en el titol al anar a jugar es podia interrumpir continuament 2026-04-15 18:36:09 +02:00
517bc2caa1 make controllerdb
nom del mando trimmed
2026-04-15 16:16:49 +02:00
f9b0f64b81 arreglos en screen 2026-04-15 06:30:23 +02:00
e0498d642d corregit bug de fullscreen en emscripten 2026-04-13 21:03:46 +02:00
ccdf9732d1 streaming de audio 2026-04-13 20:12:04 +02:00
1451327fcc fix: la powerball sonava en la demo 2026-04-13 19:58:55 +02:00
a035fecb04 emscripten: fix reset quan fas exit. Eliminades les opcions d'eixida 2026-04-13 17:57:54 +02:00
9d70138855 emscripten: per defecte integer scale false 2026-04-13 17:15:19 +02:00
dfe0a3d4e6 fix: corregit el tractament de mandos connectats 2026-04-13 17:11:27 +02:00
66c3e0089c fix: petada per tancar mal director (supose que introduit per Claude al pasar a sdl_callbacks)
eliminat codi mort d'screen
2026-04-13 16:44:27 +02:00
86323a0e56 afegit un mini-notificador 2026-04-13 16:27:57 +02:00
58cacf7bda - punter del mouse amagat soles
- canvas de wasm mes gran
2026-04-12 22:23:31 +02:00
978cbcc9fc desactivat eixir del joc en la versió WASM (milestone 5)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:03:09 +02:00
fb023df1e1 build wasm a build/wasm i output a dist/wasm
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:57:32 +02:00
555f347375 afegit suport Emscripten/WebAssembly al build system (milestone 4)
- createSystemFolder() adaptat per Emscripten (MEMFS, sense pwd.h/unistd.h)
- initOptions() amb windowSize=1 i videoMode=0 per Emscripten
- CMakeLists.txt: SDL3 via FetchContent per Emscripten, --preload-file data
- Makefile: target wasm amb Docker (emscripten/emsdk)
- Build de Linux verificat, segueix funcionant correctament

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:49:37 +02:00
85a47c1a2b corregits bugs dels sub-bucles aplanats
- Demo ja no entra en pausa ni game over (redirigeix a instruccions)
- Perdre el focus de la finestra només pausa durant el joc actiu (no en demo, game over ni pausa)
- Demo gestionat amb save/restore de section->name per evitar transició del Director

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:45:39 +02:00
06d4712493 migrat a SDL3 Callback API (SDL_AppInit/Iterate/Event/Quit) (milestone 3)
- main.cpp reescrit amb SDL_MAIN_USE_CALLBACKS
- Director convertit a màquina d'estats amb iterate() i handleEvent()
- Seccions (Logo, Intro, Title, Game) amb iterate() i handleEvent()
- Events SDL enrutats via SDL_AppEvent → Director → secció activa
- Eliminat SDL_PollEvent de iterate(), events via handleEvent()
- Transicions entre seccions gestionades per handleSectionTransition()
- Instructions i Game (demo) delegats frame a frame des de Title

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:32:31 +02:00
18c4d6032d aplanat sub-bucles anidats de pausa, game over, instruccions i demo (milestone 2)
- Game::runPausedGame() convertit a enterPausedGame() + despatx directe en run()
- Game::runGameOverScreen() convertit a enterGameOverScreen() + despatx directe
- Eliminada variable static postFade, convertida a membre gameOverPostFade
- Extret SDL_PollEvent de updateGameOverScreen() a checkGameOverEvents()
- Game::run() refactoritzat amb iterate() + hasFinished() per preparar callbacks
- Title::runInstructions() i runDemoGame() convertits a no-bloquejants
- Instructions ara usa finished/quitRequested en lloc de modificar section directament
- Instructions exposa start(), update(), checkEvents(), render(), hasFinished()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:15:54 +02:00
9365f80e8b eliminats tots els SDL_Delay i bucles bloquejants (milestone 1)
- shakeScreen() convertit a màquina d'estats amb SDL_GetTicks (50ms per pas)
- killPlayer() convertit a seqüència de fases (Shaking → Waiting → Done)
- Fade FADE_FULLSCREEN convertit a per-frame amb alpha incremental
- Fade FADE_RANDOM_SQUARE convertit a per-frame (un quadrat cada 100ms)
- Title SUBSECTION_TITLE_2 convertit a no-bloquejant, variables static eliminades
- Corregit so duplicat del crashSound al títol
- Congelat input del jugador durant la seqüència de mort

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:02:44 +02:00
4bd07216f3 corregit make release de windows 2026-04-05 18:57:32 +02:00
111 changed files with 54618 additions and 5226 deletions

View File

@@ -2,8 +2,8 @@ BasedOnStyle: Google
IndentWidth: 4 IndentWidth: 4
NamespaceIndentation: All NamespaceIndentation: All
IndentAccessModifiers: false IndentAccessModifiers: false
ColumnLimit: 0 # Sin limite de longitud de linea ColumnLimit: 0 # Sin límite de longitud de línea
BreakBeforeBraces: Attach # Llaves en la misma linea BreakBeforeBraces: Attach # Llaves en la misma línea
AllowShortIfStatementsOnASingleLine: true AllowShortIfStatementsOnASingleLine: true
AllowShortBlocksOnASingleLine: true AllowShortBlocksOnASingleLine: true
AllowShortFunctionsOnASingleLine: All AllowShortFunctionsOnASingleLine: All

View File

@@ -2,28 +2,23 @@ Checks:
- readability-* - readability-*
- modernize-* - modernize-*
- performance-* - performance-*
- bugprone-unchecked-optional-access - bugprone-*
- 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-identifier-length
- -readability-magic-numbers - -readability-magic-numbers
- -bugprone-narrowing-conversions
- -performance-enum-size
- -performance-inefficient-string-concatenation
- -bugprone-integer-division - -bugprone-integer-division
- -bugprone-easily-swappable-parameters - -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays,-warnings-as-errors - -modernize-avoid-c-arrays,-warnings-as-errors
WarningsAsErrors: '*' WarningsAsErrors: '*'
# Excluye jail_audio.hpp, stb_image.h y stb_vorbis.c del analisis # Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
HeaderFilterRegex: 'source/(?!jail_audio\.hpp|stb_image\.h|stb_vorbis\.c).*' HeaderFilterRegex: 'source/.*'
FormatStyle: file FormatStyle: file
CheckOptions: 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 # Variables locales en snake_case
- { key: readability-identifier-naming.VariableCase, value: lower_case } - { key: readability-identifier-naming.VariableCase, value: lower_case }
@@ -35,17 +30,17 @@ CheckOptions:
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case } - { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Miembros publicos en snake_case (sin sufijo) # Miembros públicos en snake_case (sin sufijo)
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case } - { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# Namespaces en CamelCase # Namespaces en CamelCase
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase } - { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# Variables estaticas privadas como miembros privados # Variables estáticas privadas como miembros privados
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case } - { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ } - { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
# Constantes estaticas sin sufijo # Constantes estáticas sin sufijo
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE } - { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
# Constantes globales en UPPER_CASE # Constantes globales en UPPER_CASE
@@ -71,7 +66,7 @@ CheckOptions:
# Valores de enums en UPPER_CASE # Valores de enums en UPPER_CASE
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# Metodos en camelBack (sin sufijos) # Métodos en camelBack (sin sufijos)
- { key: readability-identifier-naming.MethodCase, value: camelBack } - { key: readability-identifier-naming.MethodCase, value: camelBack }
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack } - { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack } - { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
@@ -80,5 +75,5 @@ CheckOptions:
# Funciones en camelBack # Funciones en camelBack
- { key: readability-identifier-naming.FunctionCase, value: camelBack } - { key: readability-identifier-naming.FunctionCase, value: camelBack }
# Parametros en lower_case # Parámetros en lower_case
- { key: readability-identifier-naming.ParameterCase, value: lower_case } - { key: readability-identifier-naming.ParameterCase, value: lower_case }

3
.gitignore vendored
View File

@@ -1,7 +1,6 @@
.vscode .vscode
build/ build/
dist/ dist/
data/config/config.txt
*.DS_Store *.DS_Store
thumbs.db thumbs.db
*.exe *.exe
@@ -14,3 +13,5 @@ thumbs.db
coffee_crisis coffee_crisis
coffee_crisis_debug coffee_crisis_debug
release/windows/coffee.res release/windows/coffee.res
resources.pack
tools/pack_resources/pack_resources

110
CLAUDE.md
View File

@@ -4,67 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
Coffee Crisis is a C++20 arcade game built with SDL2. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English). Coffee Crisis is a C++20 arcade game built with SDL3. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
## Build Commands ## Build Commands
Dependencies: `libsdl2-dev` and `g++` (Linux) or `clang++` (macOS). Dependencies: `libsdl3-dev` and `g++` (Linux) or `clang++` (macOS). Build system is CMake (driven by `Makefile` wrappers).
```bash ```bash
# Linux make # Release build
make linux # Release build → ./coffee_crisis make debug # Debug build (defines DEBUG and PAUSE)
make linux_debug # Debug build (defines DEBUG and PAUSE) → ./coffee_crisis_debug make release # Empaqueta .tar.gz / .dmg / .zip segons SO
make pack # Regenera resources.pack
# macOS make compile_shaders # Compila shaders GLSL → headers SPIR-V (requereix glslc)
make macos # Release build with clang++ make controllerdb # Descarga gamecontrollerdb.txt
make macos_debug # Debug build make format # clang-format -i
make tidy # clang-tidy
# Windows (MinGW) make cppcheck # cppcheck
make windows # Release build → coffee_crisis.exe
make windows_debug # Debug build
# Release packaging
make linux_release # Builds and creates .tar.gz
make macos_release # Builds Intel + Apple Silicon .dmg files
make windows_release # Builds and creates .zip
``` ```
There is also a CMakeLists.txt available as an alternative build system.
There are no tests or linter configured.
## Architecture ## Architecture
All source code is in `source/`. The game uses a section-based architecture controlled by the **Director** class: Source layout (alineat amb la resta de projectes germans):
- **Director** (`director.h/cpp`): Top-level controller. Initializes SDL, manages the window/renderer, and runs sections in sequence: Logo → Intro → Title → Game → Quit. Owns all shared objects (Screen, Input, Lang, Asset). ```
- **Game** (`game.h/cpp`): Core gameplay logic. Manages players, balloons (enemies), bullets, items, stages, menace level, and collision detection. Contains its own update/render loop plus sub-loops for pause and game over screens. source/
- **Screen** (`screen.h/cpp`): Rendering abstraction. Manages a virtual canvas (256×192) that gets scaled to the actual window. Handles fullscreen/windowed modes, border rendering, and fade effects. ├── main.cpp
- **Input** (`input.h/cpp`): Abstracts keyboard and gamepad input. ├── core/
- **Asset** (`asset.h/cpp`): Resource file index. Files are registered with `add()` and retrieved by name with `get()`. All paths are relative to the executable. │ ├── audio/ jail_audio.hpp
- **Lang** (`lang.h/cpp`): i18n system loading text strings from files in `data/lang/`. │ ├── input/ input.*, mouse.*
│ ├── locale/ lang.*
│ ├── rendering/ screen, fade, text, writer, texture, sprite + animated/moving/smart
│ │ ├── shader_backend.hpp (interfície abstracta de post-procesado)
│ │ └── sdl3gpu/ (pipeline SDL3 GPU)
│ │ ├── sdl3gpu_shader.* (implementació del backend GPU)
│ │ └── spv/ (headers SPIR-V generats — protegits amb dummies `.clang-*`)
│ ├── resources/ asset, resource, resource_pack, resource_loader, resource_helper
│ └── system/ director
├── game/
│ ├── defaults.hpp (constants de gameplay: block size, canvas, áreas, colors)
│ ├── game.* (hub de gameplay)
│ ├── entities/ player, balloon, bullet, item
│ ├── scenes/ logo, intro, title, instructions
│ └── ui/ menu
├── utils/
│ ├── defines.hpp (macros de build)
│ └── utils.* (helpers, enum de dificultat, circle_t, ...)
└── external/ (stb_image, stb_vorbis — protegits amb dummies `.clang-*`)
```
### Sprite hierarchy Flux general controlat per la classe **Director** (`core/system/director.h`): inicialitza SDL, finestra/renderer i executa seccions en seqüència **Logo → Intro → Title → Game → Quit**. Les classes principals:
- **Sprite** → base class for drawing from a PNG spritesheet - **Game** (`game/game.h`): gameplay nuclear. Gestiona jugadors, balloons, bullets, items, stages, nivell d'amenaça i col·lisions. Té el seu bucle d'update/render i sub-bucles per pausa i game-over.
- **AnimatedSprite** → extends Sprite with frame-based animation (loaded from `.ani` files) - **Screen** (`core/rendering/screen.h`): abstracció de render. Canvas virtual 256×192 escalat a la finestra. Fullscreen/windowed, borders, fades.
- **MovingSprite** → sprite with movement - **Input** (`core/input/input.h`): abstracció de teclat i gamepad.
- **SmartSprite** → sprite with autonomous behavior (score popups, thrown items) - **Asset** (`core/resources/asset.h`): índex de fitxers de recurs (`add`/`get` per nom).
- **Lang** (`core/locale/lang.h`): i18n, carrega strings des de `data/lang/`.
### Game entities ### Sprite hierarchy (`core/rendering/`)
- **Player** (`player.h/cpp`): Player character state and rendering - **Sprite** → base per dibuixar des d'un spritesheet PNG
- **Balloon** (`balloon.h/cpp`): Enemy entities with multiple types and split-on-pop behavior - **AnimatedSprite** → afegeix animació per frames (arxius `.ani`)
- **Bullet** (`bullet.h/cpp`): Projectiles fired by the player (left/center/right) - **MovingSprite** → sprite amb posició/velocitat
- **Item** (`item.h/cpp`): Collectible items (points, clock, coffee, power-ups) - **SmartSprite** → sprite autònom (score popups, objectes llençats)
### Audio ### Audio
**jail_audio** (`jail_audio.h/cpp`): Custom audio library wrapping SDL2 audio. Uses stb_vorbis for OGG decoding. Provides `JA_*` functions for music and sound effects with channel-based mixing. **jail_audio** (`core/audio/jail_audio.hpp`): wrapper audio SDL3 first-party. Usa stb_vorbis per OGG. API `JA_*` per música i efectes amb mesclat per canals.
### Key constants ### GPU / shaders (post-procesado)
Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bounds, section/subsection IDs, and color definitions. Pipeline SDL3 GPU portat de `coffee_crisis_arcade_edition`. El canvas 256×192 es pot passar per un backend GPU que aplica PostFX (vinyeta, scanlines, chroma, gamma, mask, curvatura, bleeding, flicker) o CrtPi (scanlines continues amb bloom). Fallback transparent al `SDL_Renderer` clàssic si la GPU falla o si es desactiva.
- **Interfície**: `core/rendering/shader_backend.hpp` (`Rendering::ShaderBackend`).
- **Implementació**: `core/rendering/sdl3gpu/sdl3gpu_shader.*` + shaders GLSL a `data/shaders/` compilats a `spv/*_spv.h` via `glslc` (o precompilats si no hi ha `glslc`).
- **Emscripten**: compile-time `NO_SHADERS` → sempre ruta clàssica.
- **macOS**: shaders Metal (MSL) inline dins `sdl3gpu_shader.cpp`; no SPIR-V.
- **Opcions persistents** a `config.txt` (migració a YAML pendent):
- `videoGpuAcceleration` (bool)
- `videoGpuPreferredDriver` (string, buit = auto)
- `videoShaderEnabled` (bool)
- `videoShaderType` (0=POSTFX, 1=CRTPI)
- **Hotkeys** (provisionals fins que hi hagi menú d'opcions): `F9` toggle GPU · `F10` toggle shader · `F11` alterna POSTFX ↔ CRTPI.
- **API** a `Screen`: `setGpuAcceleration`/`toggleGpuAcceleration`/`isGpuAccelerated`, `setShaderEnabled`/`toggleShaderEnabled`/`isShaderEnabled`, `setActiveShader`/`toggleActiveShader`/`getActiveShader`.
Presets PostFX/CrtPi i cicle de presets encara **no** estan implementats — arribaran amb la migració a YAML. Per ara, valors per defecte hardcoded.
## Data Directory ## Data Directory
@@ -72,8 +95,9 @@ Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bou
- `data/font/` — bitmap font files - `data/font/` — bitmap font files
- `data/music/` and `data/sound/` — audio assets - `data/music/` and `data/sound/` — audio assets
- `data/lang/` — language files (es_ES, ba_BA, en_UK) - `data/lang/` — language files (es_ES, ba_BA, en_UK)
- `data/config/` — gamecontroller DB, demo recording data - `data/demo/` — demo recording data (gamecontrollerdb.txt vive en la raíz del proyecto, fuera del pack)
- `data/menu/` — menu definition files - `data/menu/` — menu definition files
- `data/shaders/` — fonts GLSL per al post-procesado SDL3 GPU (no van al pack; s'empotren al binari via headers SPIR-V)
## Language ## Language

View File

@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
project(coffee_crisis VERSION 1.00) project(coffee_crisis VERSION 1.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()
# Configuración de compilador para MinGW en Windows # Configuración de compilador para MinGW en Windows
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
set(CMAKE_CXX_COMPILER "g++") set(CMAKE_CXX_COMPILER "g++")
@@ -21,8 +26,15 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections
# Define el directorio de los archivos fuente # Define el directorio de los archivos fuente
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source") set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
# Cargar todos los archivos fuente en DIR_SOURCES # Cargar todos los archivos fuente en DIR_SOURCES (recursivo, sin external/)
file(GLOB SOURCES "${DIR_SOURCES}/*.cpp") file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${DIR_SOURCES}/*.cpp")
list(FILTER SOURCES EXCLUDE REGEX "${DIR_SOURCES}/external/.*")
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2).
# Define NO_SHADERS más abajo y filtra el fuente aquí.
if(EMSCRIPTEN)
list(REMOVE_ITEM SOURCES "${DIR_SOURCES}/core/rendering/sdl3gpu/sdl3gpu_shader.cpp")
endif()
# Verificar si se encontraron archivos fuente # Verificar si se encontraron archivos fuente
if(NOT SOURCES) if(NOT SOURCES)
@@ -30,15 +42,107 @@ if(NOT SOURCES)
endif() endif()
# Configuración de SDL3 # Configuración de SDL3
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) if(EMSCRIPTEN)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}") # 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()
# Configuración común de salida de ejecutables en el directorio raíz # --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}) if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADER_VERT_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.vert")
set(SHADER_FRAG_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.frag")
set(SHADER_CRTPI_SRC "${CMAKE_SOURCE_DIR}/data/shaders/crtpi_frag.glsl")
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/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_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 "${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()
foreach(_h IN LISTS ALL_SHADER_HEADERS)
if(NOT EXISTS "${_h}")
message(FATAL_ERROR
"glslc no encontrado y header SPIR-V no existe: ${_h}\n"
" Instala glslc: sudo apt install glslang-tools (Linux)\n"
" choco install vulkan-sdk (Windows)"
)
endif()
endforeach()
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 inline)")
endif()
endif()
# Añadir ejecutable principal # Añadir ejecutable principal
add_executable(${PROJECT_NAME} ${SOURCES}) add_executable(${PROJECT_NAME} ${SOURCES})
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
# Includes relatius a source/ (p.e. `#include "core/rendering/texture.h"`)
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES})
# Configuración de salida: el ejecutable principal va a la raíz del proyecto.
# Per-target (no global) perquè `pack_resources` acabe a `build/` com la resta
# de projectes.
if(NOT EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# Añadir definiciones de compilación dependiendo del tipo de build # Añadir definiciones de compilación dependiendo del tipo de build
target_compile_definitions(${PROJECT_NAME} PRIVATE target_compile_definitions(${PROJECT_NAME} PRIVATE
$<$<CONFIG:DEBUG>:DEBUG PAUSE> $<$<CONFIG:DEBUG>:DEBUG PAUSE>
@@ -66,6 +170,19 @@ elseif(APPLE)
-rpath @executable_path/../Frameworks/ -rpath @executable_path/../Frameworks/
) )
endif() endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# En wasm NO empaquetamos un resources.pack: el propio --preload-file de
# emscripten ya hace el mismo trabajo (bundle del directorio en un .data),
# así que metemos directamente 'data' y dejamos que el Resource lea por
# filesystem (MEMFS). Evita doble empaquetado y el uso de memoria extra.
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE) elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD) target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections) target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)
@@ -77,6 +194,7 @@ endif()
find_program(CLANG_TIDY_EXE NAMES clang-tidy) find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format) find_program(CLANG_FORMAT_EXE NAMES clang-format)
find_program(CPPCHECK_EXE NAMES cppcheck)
# Recopilar todos los archivos fuente para analisis # Recopilar todos los archivos fuente para analisis
file(GLOB_RECURSE ALL_SOURCE_FILES file(GLOB_RECURSE ALL_SOURCE_FILES
@@ -84,17 +202,14 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
"${CMAKE_SOURCE_DIR}/source/*.h" "${CMAKE_SOURCE_DIR}/source/*.h"
) )
# Excluir stb_image.h y stb_vorbis.c del analisis
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES}) set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
# Excluir stb y jail_audio del formateo tambien
set(FORMAT_SOURCES ${ALL_SOURCE_FILES}) set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$") # Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$") set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
list(FILTER CPPCHECK_SOURCES EXCLUDE REGEX ".*/source/external/.*")
# Targets de clang-tidy # Targets de clang-tidy
if(CLANG_TIDY_EXE) if(CLANG_TIDY_EXE)
@@ -139,3 +254,56 @@ if(CLANG_FORMAT_EXE)
else() else()
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles") message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
endif() endif()
# Target 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
--suppress=*:*/source/external/*
--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()

179
Makefile
View File

@@ -10,12 +10,34 @@ DIR_BIN := $(addsuffix /, $(DIR_ROOT))
TARGET_NAME := coffee_crisis TARGET_NAME := coffee_crisis
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME) TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
APP_NAME := Coffee Crisis APP_NAME := Coffee Crisis
VERSION := v2.3.3
DIST_DIR := dist DIST_DIR := dist
RELEASE_FOLDER := dist/_tmp RELEASE_FOLDER := dist/_tmp
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME) RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
RESOURCE_FILE := release/windows/coffee.res RESOURCE_FILE := release/windows/coffee.res
# ==============================================================================
# VERSION (extracted from defines.hpp)
# ==============================================================================
ifeq ($(OS),Windows_NT)
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
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
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
# ============================================================================== # ==============================================================================
# RELEASE NAMES # RELEASE NAMES
# ============================================================================== # ==============================================================================
@@ -52,45 +74,67 @@ endif
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME) WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(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 else
WIN_TARGET_FILE := $(TARGET_FILE) WIN_TARGET_FILE := $(TARGET_FILE)
WIN_RELEASE_FILE := $(RELEASE_FILE) WIN_RELEASE_FILE := $(RELEASE_FILE)
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
endif
# ==============================================================================
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
# ==============================================================================
ifeq ($(OS),Windows_NT)
CMAKE_GEN := -G "MinGW Makefiles"
else
CMAKE_GEN :=
endif endif
# ============================================================================== # ==============================================================================
# COMPILACIÓN CON CMAKE # COMPILACIÓN CON CMAKE
# ============================================================================== # ==============================================================================
all: all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
debug: debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# ==============================================================================
# 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
# ============================================================================== # ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO) # RELEASE AUTOMÁTICO (detecta SO)
# ============================================================================== # ==============================================================================
release: release:
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
@"$(MAKE)" windows_release @"$(MAKE)" _windows_release
else else
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
@$(MAKE) macos_release @$(MAKE) _macos_release
else else
@$(MAKE) linux_release @$(MAKE) _linux_release
endif endif
endif endif
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE) # COMPILACIÓN PARA WINDOWS (RELEASE)
# ============================================================================== # ==============================================================================
windows_release: _windows_release:
@$(MAKE) pack
@echo off @echo off
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Crea carpeta de distribución y carpeta temporal # Crea carpeta de distribución y carpeta temporal
@@ -99,11 +143,12 @@ windows_release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}" @powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros # Copia ficheros
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse -Force" @powershell -Command "Copy-Item 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -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 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'" @powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip # Crea el fichero .zip
@@ -117,14 +162,15 @@ windows_release:
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE) # COMPILACIÓN PARA MACOS (RELEASE)
# ============================================================================== # ==============================================================================
macos_release: _macos_release:
@$(MAKE) pack
@echo "Creando release para macOS - Version: $(VERSION)" @echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario # Verificar e instalar create-dmg si es necesario
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg) @which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Compila la versión para procesadores Intel con cmake # Compila la versión para procesadores Intel con cmake
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON @cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/intel @cmake --build build/intel
# Elimina datos de compilaciones anteriores # Elimina datos de compilaciones anteriores
@@ -140,7 +186,8 @@ macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros # Copia carpetas y ficheros
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks" cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents" cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
@@ -177,7 +224,7 @@ macos_release:
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" @echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila la versión para procesadores Apple Silicon con cmake # Compila la versión para procesadores Apple Silicon con cmake
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON @cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/arm @cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
@@ -210,11 +257,12 @@ macos_release:
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE) # COMPILACIÓN PARA LINUX (RELEASE)
# ============================================================================== # ==============================================================================
linux_release: _linux_release:
@$(MAKE) pack
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Elimina carpeta temporal previa y la recrea # Elimina carpeta temporal previa y la recrea
@@ -222,7 +270,8 @@ linux_release:
$(MKDIR) "$(RELEASE_FOLDER)" $(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros # Copia ficheros
cp -R data "$(RELEASE_FOLDER)" cp resources.pack "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)" cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)" cp README.md "$(RELEASE_FOLDER)"
cp "$(TARGET_FILE)" "$(RELEASE_FILE)" cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
@@ -236,6 +285,83 @@ linux_release:
# Elimina la carpeta temporal # Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
# ==============================================================================
wasm:
@$(MAKE) pack
@echo "Compilando para WebAssembly - Version: $(VERSION)"
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/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm_debug/.
wasm_debug:
@$(MAKE) pack
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
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/"
# ==============================================================================
# ==============================================================================
# CODE QUALITY (delegados a cmake)
# ==============================================================================
format:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format
format-check:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format-check
tidy:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy
tidy-fix:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy-fix
cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# SHADERS (SPIR-V) — sólo Linux/Windows. Requiere glslc en el PATH.
compile_shaders:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target shaders
# 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"
# ============================================================================== # ==============================================================================
# REGLAS ESPECIALES # REGLAS ESPECIALES
# ============================================================================== # ==============================================================================
@@ -252,12 +378,23 @@ help:
@echo "" @echo ""
@echo " Release:" @echo " Release:"
@echo " make release - Crear release (detecta SO automaticamente)" @echo " make release - Crear release (detecta SO automaticamente)"
@echo " make windows_release - Crear release para Windows" @echo " make wasm - Compilar para WebAssembly (requiere Docker) y deploy a maverick"
@echo " make linux_release - Crear release para Linux" @echo " make wasm_debug - Compilar WebAssembly Debug local (sin deploy)"
@echo " make macos_release - Crear release para macOS" @echo ""
@echo " Herramientas:"
@echo " make pack - Empaquetar recursos a resources.pack"
@echo " make compile_shaders - Compilar shaders GLSL a headers SPIR-V (requiere glslc)"
@echo " make controllerdb - Descargar gamecontrollerdb.txt actualizado"
@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 ""
@echo " Otros:" @echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))" @echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda" @echo " make help - Mostrar esta ayuda"
.PHONY: all debug release windows_release macos_release linux_release show_version help .PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug controllerdb pack format format-check tidy tidy-fix cppcheck compile_shaders show_version help

File diff suppressed because it is too large Load Diff

View File

@@ -279,3 +279,9 @@ MODE FORA DE LINEA
## 93 - MENU OPCIONES ## 93 - MENU OPCIONES
TAULER DE PUNTS TAULER DE PUNTS
## 94 - NOTIFICACIO COMANDAMENT
CONNECTAT
## 95 - NOTIFICACIO COMANDAMENT
DESCONNECTAT

View File

@@ -279,3 +279,9 @@ OFFLINE MODE
## 93 - MENU OPCIONES ## 93 - MENU OPCIONES
HISCORE TABLE HISCORE TABLE
## 94 - GAMEPAD NOTIFICATION
CONNECTED
## 95 - GAMEPAD NOTIFICATION
DISCONNECTED

View File

@@ -279,3 +279,9 @@ MODO SIN CONEXION
## 93 - MENU OPCIONES ## 93 - MENU OPCIONES
TABLA DE PUNTUACIONES TABLA DE PUNTUACIONES
## 94 - NOTIFICACION MANDO
CONECTADO
## 95 - NOTIFICACION MANDO
DESCONECTADO

View File

@@ -0,0 +1,152 @@
#version 450
// Vulkan GLSL fragment shader — CRT-Pi PostFX
// Algoritmo de scanlines continuas con pesos gaussianos, bloom y máscara de fósforo.
// Basado en el shader CRT-Pi original (GLSL 3.3), portado a GLSL 4.50 con parámetros uniformes.
//
// Compile: glslc -fshader-stage=frag --target-env=vulkan1.0 crtpi_frag.glsl -o crtpi_frag.spv
// xxd -i crtpi_frag.spv > ../../source/core/rendering/sdl3gpu/crtpi_frag_spv.h
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D Texture;
layout(set = 3, binding = 0) uniform CrtPiBlock {
// vec4 #0
float scanline_weight; // Ajuste gaussiano de scanlines (default 6.0)
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
float bloom_factor; // Factor de brillo en zonas iluminadas (default 3.5)
float input_gamma; // Gamma de entrada — linealización (default 2.4)
// vec4 #1
float output_gamma; // Gamma de salida — codificación (default 2.2)
float mask_brightness; // Brillo sub-píxeles de la máscara (default 0.80)
float curvature_x; // Distorsión barrel eje X (default 0.05)
float curvature_y; // Distorsión barrel eje Y (default 0.10)
// vec4 #2
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
int enable_scanlines; // 0 = off, 1 = on
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico de scanlines)
int enable_gamma; // 0 = off, 1 = on
// vec4 #3
int enable_curvature; // 0 = off, 1 = on
int enable_sharper; // 0 = off, 1 = on
float texture_width; // Ancho del canvas lógico en píxeles
float texture_height; // Alto del canvas lógico en píxeles
} u;
// Distorsión barrel CRT
vec2 distort(vec2 coord, vec2 screen_scale) {
vec2 curvature = vec2(u.curvature_x, u.curvature_y);
vec2 barrel_scale = 1.0 - (0.23 * curvature);
coord *= screen_scale;
coord -= vec2(0.5);
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) {
return vec2(-1.0); // fuera de pantalla
}
coord += vec2(0.5);
coord /= screen_scale;
return coord;
}
float calcScanLineWeight(float dist) {
return max(1.0 - dist * dist * u.scanline_weight, u.scanline_gap_brightness);
}
float calcScanLine(float dy, float filter_width) {
float weight = calcScanLineWeight(dy);
if (u.enable_multisample != 0) {
weight += calcScanLineWeight(dy - filter_width);
weight += calcScanLineWeight(dy + filter_width);
weight *= 0.3333333;
}
return weight;
}
void main() {
vec2 tex_size = vec2(u.texture_width, u.texture_height);
// filterWidth: equivalente al original (768.0 / TextureSize.y) / 3.0
float filter_width = (768.0 / u.texture_height) / 3.0;
vec2 texcoord = v_uv;
// Curvatura barrel opcional
if (u.enable_curvature != 0) {
texcoord = distort(texcoord, vec2(1.0, 1.0));
if (texcoord.x < 0.0) {
out_color = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
}
vec2 texcoord_in_pixels = texcoord * tex_size;
vec2 tc;
float scan_line_weight;
if (u.enable_sharper != 0) {
// Modo SHARPER: filtrado bicúbico-like con subpixel sharpen
vec2 temp_coord = floor(texcoord_in_pixels) + 0.5;
tc = temp_coord / tex_size;
vec2 deltas = texcoord_in_pixels - temp_coord;
scan_line_weight = calcScanLine(deltas.y, filter_width);
vec2 signs = sign(deltas);
deltas.x *= 2.0;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5;
deltas.y *= 8.0;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
// Modo estándar
float temp_y = floor(texcoord_in_pixels.y) + 0.5;
float y_coord = temp_y / tex_size.y;
float dy = texcoord_in_pixels.y - temp_y;
scan_line_weight = calcScanLine(dy, filter_width);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0;
dy /= tex_size.y;
dy *= sign_y;
tc = vec2(texcoord.x, y_coord + dy);
}
vec3 colour = texture(Texture, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) {
colour = pow(colour, vec3(u.input_gamma));
}
colour *= scan_line_weight * u.bloom_factor;
if (u.enable_gamma != 0) {
colour = pow(colour, vec3(1.0 / u.output_gamma));
}
}
// Máscara de fósforo
if (u.mask_type == 1) {
float which_mask = fract(gl_FragCoord.x * 0.5);
vec3 mask = (which_mask < 0.5)
? vec3(u.mask_brightness, 1.0, u.mask_brightness)
: vec3(1.0, u.mask_brightness, 1.0);
colour *= mask;
} else if (u.mask_type == 2) {
float which_mask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(u.mask_brightness);
if (which_mask < 0.3333333)
mask.x = 1.0;
else if (which_mask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
colour *= mask;
}
out_color = vec4(colour, 1.0);
}

View File

@@ -0,0 +1,48 @@
#version 450
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D source;
layout(set = 3, binding = 0) uniform DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2, ±2 taps), 1 = Lanczos3 (ventana 3, ±3 taps)
float pad0;
float pad1;
float pad2;
} u;
// Kernel Lanczos normalizado: sinc(t) * sinc(t/a) para |t| < a, 0 fuera.
float lanczos(float t, float a) {
t = abs(t);
if (t < 0.0001) { return 1.0; }
if (t >= a) { return 0.0; }
const float PI = 3.14159265358979;
float pt = PI * t;
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
}
void main() {
vec2 src_size = vec2(textureSize(source, 0));
// Posición en coordenadas de texel (centros de texel en N+0.5)
vec2 p = v_uv * src_size;
vec2 p_floor = floor(p);
float a = (u.algorithm == 0) ? 2.0 : 3.0;
int win = int(a);
vec4 color = vec4(0.0);
float weight_sum = 0.0;
for (int j = -win; j <= win; j++) {
for (int i = -win; i <= win; i++) {
// Centro del texel (i,j) relativo a p_floor
vec2 tap_center = p_floor + vec2(float(i), float(j)) + 0.5;
vec2 offset = tap_center - p;
float w = lanczos(offset.x, a) * lanczos(offset.y, a);
color += texture(source, tap_center / src_size) * w;
weight_sum += w;
}
}
out_color = (weight_sum > 0.0) ? (color / weight_sum) : vec4(0.0, 0.0, 0.0, 1.0);
}

142
data/shaders/postfx.frag Normal file
View File

@@ -0,0 +1,142 @@
#version 450
// Vulkan GLSL fragment shader — PostFX effects
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
// Compile: glslc postfx.frag -o postfx.frag.spv
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
//
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
// (8 floats, 32 bytes, std140/scalar layout).
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform PostFXUniforms {
float vignette_strength;
float chroma_strength;
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
} u;
// YCbCr helpers for NTSC bleeding
vec3 rgb_to_ycc(vec3 rgb) {
return vec3(
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
);
}
vec3 ycc_to_rgb(vec3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5;
float cr = ycc.z - 0.5;
return clamp(vec3(
y + 1.402*cr,
y - 0.344*cb - 0.714*cr,
y + 1.772*cb
), 0.0, 1.0);
}
void main() {
vec2 uv = v_uv;
// Curvatura barrel CRT
if (u.curvature > 0.0) {
vec2 c = uv - 0.5;
float rsq = dot(c, c);
vec2 dist = vec2(0.05, 0.1) * u.curvature;
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
out_color = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
uv = c + 0.5;
}
// Muestra base
vec3 base = texture(scene, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
vec3 colour;
if (u.bleeding > 0.0) {
float tw = float(textureSize(scene, 0).x);
float step = u.oversample / tw; // 1 pixel lógico en UV
vec3 ycc = rgb_to_ycc(base);
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else {
colour = base;
}
// Aberración cromática (drift animado con time para efecto NTSC real)
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
// Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0) {
vec3 lin = pow(colour, vec3(2.4));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — 1 pixel físico oscuro por fila lógica.
// Modelo sustractivo: las filas de scanline se oscurecen, las demás no cambian.
// Esto evita el efecto de sobrebrillo en contenido con colores vivos.
if (u.scanline_strength > 0.0) {
float ps = max(1.0, round(u.pixel_scale));
float frac_in_row = fract(uv.y * u.screen_height);
float row_pos = floor(frac_in_row * ps);
float is_dark = step(ps - 1.0, row_pos);
float scan = mix(1.0, 0.0, is_dark);
colour *= mix(1.0, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0) {
vec3 enc = pow(colour, vec3(1.0 / 2.2));
colour = mix(colour, enc, u.gamma_strength);
}
// Viñeta
vec2 d = uv - 0.5;
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0, 1.0);
// Máscara de fósforo RGB — después de scanlines (orden original):
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
if (u.mask_strength > 0.0) {
float whichMask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(0.80);
if (whichMask < 0.3333333)
mask.x = 1.0;
else if (whichMask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
colour = mix(colour, colour * mask, u.mask_strength);
}
// Parpadeo de fósforo CRT (~50 Hz)
if (u.flicker > 0.0) {
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
}
out_color = vec4(colour, 1.0);
}

24
data/shaders/postfx.vert Normal file
View File

@@ -0,0 +1,24 @@
#version 450
// Vulkan GLSL vertex shader — postfx full-screen triangle
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
// Compile: glslc postfx.vert -o postfx.vert.spv
// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h
layout(location = 0) out vec2 v_uv;
void main() {
// Full-screen triangle (no vertex buffer needed)
const vec2 positions[3] = vec2[3](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
const vec2 uvs[3] = vec2[3](
vec2(0.0, 1.0),
vec2(2.0, 1.0),
vec2(0.0,-1.0)
);
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}

15
data/shaders/upscale.frag Normal file
View File

@@ -0,0 +1,15 @@
#version 450
// Vulkan GLSL fragment shader — Nearest-neighbour upscale pass
// Used as the first render pass when supersampling is active.
// Compile: glslc upscale.frag -o upscale.frag.spv
// xxd -i upscale.frag.spv > ../../source/core/rendering/sdl3gpu/upscale_frag_spv.h
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D scene;
void main() {
out_color = texture(scene, v_uv);
}

2232
gamecontrollerdb.txt Normal file

File diff suppressed because it is too large Load Diff

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.h"
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

@@ -0,0 +1,42 @@
#include "core/input/global_inputs.hpp"
#include "core/input/input.h"
#include "core/rendering/screen.h"
namespace GlobalInputs {
auto handle() -> bool {
if (Screen::get() == nullptr || Input::get() == nullptr) { return false; }
if (Input::get()->checkInput(input_window_fullscreen, REPEAT_FALSE)) {
Screen::get()->toggleVideoMode();
return true;
}
if (Input::get()->checkInput(input_window_dec_size, REPEAT_FALSE)) {
Screen::get()->decWindowZoom();
return true;
}
if (Input::get()->checkInput(input_window_inc_size, REPEAT_FALSE)) {
Screen::get()->incWindowZoom();
return true;
}
if (Input::get()->checkInput(input_prev_preset, REPEAT_FALSE)) {
Screen::get()->prevPreset();
return true;
}
if (Input::get()->checkInput(input_next_preset, REPEAT_FALSE)) {
Screen::get()->nextPreset();
return true;
}
if (Input::get()->checkInput(input_toggle_shader, REPEAT_FALSE)) {
Screen::get()->toggleShaderEnabled();
return true;
}
if (Input::get()->checkInput(input_toggle_shader_type, REPEAT_FALSE)) {
Screen::get()->toggleActiveShader();
return true;
}
return false;
}
} // namespace GlobalInputs

View File

@@ -0,0 +1,10 @@
#pragma once
namespace GlobalInputs {
// Gestiona els atalls globals disponibles en qualsevol escena: zoom de
// finestra (F1/F2), fullscreen (F3), presets de shader (F8/F9), toggle
// shader (F10) i tipus de shader POSTFX↔CRTPI (F11). Retorna true si ha
// consumit alguna tecla (per si la capa cridant vol suprimir-la del
// processament específic de l'escena).
auto handle() -> bool;
} // namespace GlobalInputs

View File

@@ -1,14 +1,69 @@
#include "input.h" #include "core/input/input.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <iostream> // for basic_ostream, operator<<, cout, basi... #include <algorithm> // for any_of
#include <iostream> // for basic_ostream, operator<<, cout, basi...
// 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
}
// Instancia única
Input *Input::instance = nullptr;
// Singleton API
void Input::init(const std::string &gameControllerDbPath) {
Input::instance = new Input(gameControllerDbPath);
}
void Input::destroy() {
delete Input::instance;
Input::instance = nullptr;
}
auto Input::get() -> Input * {
return Input::instance;
}
// Constructor // Constructor
Input::Input(std::string file) { Input::Input(const std::string &file)
// Fichero gamecontrollerdb.txt : numGamepads(0),
dbPath = file; dbPath(file),
verbose(true),
disabledUntil(d_notDisabled),
enabled(true) {
// Inicializa las variables // Inicializa las variables
keyBindings_t kb; keyBindings_t kb;
kb.scancode = 0; kb.scancode = 0;
@@ -19,9 +74,19 @@ Input::Input(std::string file) {
gcb.button = SDL_GAMEPAD_BUTTON_INVALID; gcb.button = SDL_GAMEPAD_BUTTON_INVALID;
gcb.active = false; gcb.active = false;
gameControllerBindings.resize(input_number_of_inputs, gcb); gameControllerBindings.resize(input_number_of_inputs, gcb);
}
verbose = true; // Destructor
enabled = true; Input::~Input() {
for (auto *pad : connectedControllers) {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
connectedControllers.clear();
connectedControllerIds.clear();
controllerNames.clear();
numGamepads = 0;
} }
// Actualiza el estado del objeto // Actualiza el estado del objeto
@@ -82,7 +147,7 @@ bool Input::checkInput(Uint8 input, bool repeat, int device, int index) {
} }
} }
if (gameControllerFound()) if (gameControllerFound() && index >= 0 && index < (int)connectedControllers.size())
if ((device == INPUT_USE_GAMECONTROLLER) || (device == INPUT_USE_ANY)) { if ((device == INPUT_USE_GAMECONTROLLER) || (device == INPUT_USE_ANY)) {
if (repeat) { if (repeat) {
if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[input].button)) { if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[input].button)) {
@@ -128,7 +193,7 @@ bool Input::checkAnyInput(int device, int index) {
} }
} }
if (gameControllerFound()) { if (gameControllerFound() && index >= 0 && index < (int)connectedControllers.size()) {
if (device == INPUT_USE_GAMECONTROLLER || device == INPUT_USE_ANY) { if (device == INPUT_USE_GAMECONTROLLER || device == INPUT_USE_ANY) {
for (int i = 0; i < (int)gameControllerBindings.size(); ++i) { for (int i = 0; i < (int)gameControllerBindings.size(); ++i) {
if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[i].button)) { if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[i].button)) {
@@ -141,8 +206,40 @@ bool Input::checkAnyInput(int device, int index) {
return false; return false;
} }
// Busca si hay un mando conectado // Construye el nombre visible de un mando.
// Recorta des del primer '(' o '[' (per a evitar coses tipus
// "Retroid Controller (vendor: 1001) ...") i talla a 25 caràcters.
std::string Input::buildControllerName(SDL_Gamepad *pad, int padIndex) {
(void)padIndex;
const char *padName = SDL_GetGamepadName(pad);
std::string name = padName ? padName : "Unknown";
const auto pos = name.find_first_of("([");
if (pos != std::string::npos) {
name.erase(pos);
}
while (!name.empty() && name.back() == ' ') {
name.pop_back();
}
if (name.size() > 25) {
name.resize(25);
}
return name;
}
// Busca si hay un mando conectado. Cierra y limpia el estado previo para
// que la función sea idempotente si se invoca más de una vez.
bool Input::discoverGameController() { bool Input::discoverGameController() {
// Cierra los mandos ya abiertos y limpia los vectores paralelos
for (auto *pad : connectedControllers) {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
connectedControllers.clear();
connectedControllerIds.clear();
controllerNames.clear();
numGamepads = 0;
bool found = false; bool found = false;
if (SDL_WasInit(SDL_INIT_GAMEPAD) != SDL_INIT_GAMEPAD) { if (SDL_WasInit(SDL_INIT_GAMEPAD) != SDL_INIT_GAMEPAD) {
@@ -157,42 +254,39 @@ bool Input::discoverGameController() {
int nJoysticks = 0; int nJoysticks = 0;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&nJoysticks); SDL_JoystickID *joysticks = SDL_GetJoysticks(&nJoysticks);
numGamepads = 0;
if (joysticks) { if (joysticks) {
// Cuenta el numero de mandos int gamepadCount = 0;
for (int i = 0; i < nJoysticks; ++i) { for (int i = 0; i < nJoysticks; ++i) {
if (SDL_IsGamepad(joysticks[i])) { if (SDL_IsGamepad(joysticks[i])) {
numGamepads++; gamepadCount++;
} }
} }
if (verbose) { if (verbose) {
std::cout << "\nChecking for game controllers...\n"; std::cout << "\nChecking for game controllers...\n";
std::cout << nJoysticks << " joysticks found, " << numGamepads << " are gamepads\n"; std::cout << nJoysticks << " joysticks found, " << gamepadCount << " are gamepads\n";
} }
if (numGamepads > 0) { if (gamepadCount > 0) {
found = true; found = true;
int padIndex = 0; int padIndex = 0;
for (int i = 0; i < nJoysticks; i++) { for (int i = 0; i < nJoysticks; i++) {
if (!SDL_IsGamepad(joysticks[i])) continue; if (!SDL_IsGamepad(joysticks[i])) continue;
// Abre el mando y lo añade a la lista installWebStandardMapping(joysticks[i]);
SDL_Gamepad *pad = SDL_OpenGamepad(joysticks[i]); SDL_Gamepad *pad = SDL_OpenGamepad(joysticks[i]);
if (pad != nullptr) { if (pad != nullptr) {
const std::string name = buildControllerName(pad, padIndex);
connectedControllers.push_back(pad); connectedControllers.push_back(pad);
const std::string separator(" #"); connectedControllerIds.push_back(joysticks[i]);
const char *padName = SDL_GetGamepadName(pad); controllerNames.push_back(name);
std::string name = padName ? padName : "Unknown"; numGamepads++;
name.resize(25); padIndex++;
name = name + separator + std::to_string(padIndex);
if (verbose) { if (verbose) {
std::cout << name << std::endl; std::cout << name << std::endl;
} }
controllerNames.push_back(name);
padIndex++;
} else { } else {
if (verbose) { if (verbose) {
std::cout << "SDL_GetError() = " << SDL_GetError() << std::endl; std::cout << "SDL_GetError() = " << SDL_GetError() << std::endl;
@@ -209,6 +303,65 @@ bool Input::discoverGameController() {
return found; return found;
} }
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED
bool Input::handleGamepadAdded(SDL_JoystickID jid, std::string &outName) {
if (!SDL_IsGamepad(jid)) {
return false;
}
// Si el mando ya está registrado no hace nada (ej. evento retroactivo tras el scan inicial)
if (std::any_of(connectedControllerIds.begin(), connectedControllerIds.end(), [jid](SDL_JoystickID existing) { return existing == jid; })) {
return false;
}
installWebStandardMapping(jid);
SDL_Gamepad *pad = SDL_OpenGamepad(jid);
if (pad == nullptr) {
if (verbose) {
std::cout << "Failed to open gamepad " << jid << ": " << SDL_GetError() << std::endl;
}
return false;
}
const int padIndex = (int)connectedControllers.size();
const std::string name = buildControllerName(pad, padIndex);
connectedControllers.push_back(pad);
connectedControllerIds.push_back(jid);
controllerNames.push_back(name);
numGamepads++;
if (verbose) {
std::cout << "Gamepad connected: " << name << std::endl;
}
outName = name;
return true;
}
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED
bool Input::handleGamepadRemoved(SDL_JoystickID jid, std::string &outName) {
for (size_t i = 0; i < connectedControllerIds.size(); ++i) {
if (connectedControllerIds[i] != jid) continue;
outName = controllerNames[i];
if (connectedControllers[i] != nullptr) {
SDL_CloseGamepad(connectedControllers[i]);
}
connectedControllers.erase(connectedControllers.begin() + i);
connectedControllerIds.erase(connectedControllerIds.begin() + i);
controllerNames.erase(controllerNames.begin() + i);
numGamepads--;
if (numGamepads < 0) numGamepads = 0;
if (verbose) {
std::cout << "Gamepad disconnected: " << outName << std::endl;
}
return true;
}
return false;
}
// Comprueba si hay algun mando conectado // Comprueba si hay algun mando conectado
bool Input::gameControllerFound() { bool Input::gameControllerFound() {
if (numGamepads > 0) { if (numGamepads > 0) {

View File

@@ -34,6 +34,12 @@ enum inputs_e {
input_window_inc_size, input_window_inc_size,
input_window_dec_size, input_window_dec_size,
// GPU / shaders (hotkeys provisionales hasta que haya menú de opciones)
input_prev_preset,
input_next_preset,
input_toggle_shader,
input_toggle_shader_type,
// Input obligatorio // Input obligatorio
input_number_of_inputs input_number_of_inputs
}; };
@@ -57,7 +63,8 @@ class Input {
}; };
// Objetos y punteros // Objetos y punteros
std::vector<SDL_Gamepad *> connectedControllers; // Vector con todos los mandos conectados std::vector<SDL_Gamepad *> connectedControllers; // Vector con todos los mandos conectados
std::vector<SDL_JoystickID> connectedControllerIds; // Instance IDs paralelos para mapear eventos
// Variables // Variables
std::vector<keyBindings_t> keyBindings; // Vector con las teclas asociadas a los inputs predefinidos std::vector<keyBindings_t> keyBindings; // Vector con las teclas asociadas a los inputs predefinidos
@@ -69,9 +76,23 @@ class Input {
i_disable_e disabledUntil; // Tiempo que esta deshabilitado i_disable_e disabledUntil; // Tiempo que esta deshabilitado
bool enabled; // Indica si está habilitado bool enabled; // Indica si está habilitado
// Construye el nombre visible de un mando (name truncado + sufijo #N)
std::string buildControllerName(SDL_Gamepad *pad, int padIndex);
// Constructor privado (usar Input::init)
explicit Input(const std::string &file);
// Instancia única
static Input *instance;
public: public:
// Constructor // Singleton API
Input(std::string file); static void init(const std::string &gameControllerDbPath); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Input *; // Obtiene el puntero a la instancia
// Destructor
~Input();
// Actualiza el estado del objeto // Actualiza el estado del objeto
void update(); void update();
@@ -91,6 +112,14 @@ class Input {
// Busca si hay un mando conectado // Busca si hay un mando conectado
bool discoverGameController(); bool discoverGameController();
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED. Devuelve true si el mando se ha añadido
// (no estaba ya registrado) y escribe el nombre visible en outName.
bool handleGamepadAdded(SDL_JoystickID jid, std::string &outName);
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED. Devuelve true si se ha encontrado y
// eliminado, y escribe el nombre visible en outName.
bool handleGamepadRemoved(SDL_JoystickID jid, std::string &outName);
// Comprueba si hay algun mando conectado // Comprueba si hay algun mando conectado
bool gameControllerFound(); bool gameControllerFound();

View File

@@ -0,0 +1,35 @@
#include "core/input/mouse.hpp"
namespace Mouse {
Uint32 cursorHideTime = 3000; // Tiempo en milisegundos para ocultar el cursor por inactividad
Uint32 lastMouseMoveTime = 0; // Última vez que el ratón se movió
bool cursorVisible = true; // Estado del cursor
void handleEvent(const SDL_Event &event, bool fullscreen) {
if (event.type == SDL_EVENT_MOUSE_MOTION) {
lastMouseMoveTime = SDL_GetTicks();
if (!cursorVisible && !fullscreen) {
SDL_ShowCursor();
cursorVisible = true;
}
}
}
void updateCursorVisibility(bool fullscreen) {
// En pantalla completa el cursor siempre está oculto
if (fullscreen) {
if (cursorVisible) {
SDL_HideCursor();
cursorVisible = false;
}
return;
}
// En modo ventana, lo oculta tras el periodo de inactividad
const Uint32 currentTime = SDL_GetTicks();
if (cursorVisible && (currentTime - lastMouseMoveTime > cursorHideTime)) {
SDL_HideCursor();
cursorVisible = false;
}
}
} // namespace Mouse

View File

@@ -0,0 +1,18 @@
#pragma once
#include <SDL3/SDL.h>
namespace Mouse {
extern Uint32 cursorHideTime; // Tiempo en milisegundos para ocultar el cursor por inactividad
extern Uint32 lastMouseMoveTime; // Última vez que el ratón se movió
extern bool cursorVisible; // Estado del cursor
// Procesa un evento de ratón. En pantalla completa ignora el movimiento
// para no volver a mostrar el cursor.
void handleEvent(const SDL_Event &event, bool fullscreen);
// Actualiza la visibilidad del cursor. En modo ventana lo oculta
// después de cursorHideTime ms sin movimiento. En pantalla completa
// lo mantiene siempre oculto.
void updateCursorVisibility(bool fullscreen);
} // namespace Mouse

View File

@@ -0,0 +1,85 @@
#include "core/locale/lang.h"
#include <fstream> // for basic_ifstream, basic_istream, ifstream
#include <sstream>
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource_helper.h"
// Instancia única
Lang *Lang::instance = nullptr;
// Singleton API
void Lang::init() {
Lang::instance = new Lang();
}
void Lang::destroy() {
delete Lang::instance;
Lang::instance = nullptr;
}
auto Lang::get() -> Lang * {
return Lang::instance;
}
// Constructor
Lang::Lang() {
}
// Destructor
Lang::~Lang() {
}
// Inicializa los textos del juego en el idioma seleccionado
bool Lang::setLang(Uint8 lang) {
std::string file;
switch (lang) {
case es_ES:
file = Asset::get()->get("es_ES.txt");
break;
case en_UK:
file = Asset::get()->get("en_UK.txt");
break;
case ba_BA:
file = Asset::get()->get("ba_BA.txt");
break;
default:
file = Asset::get()->get("en_UK.txt");
break;
}
for (int i = 0; i < MAX_TEXT_STRINGS; i++)
mTextStrings[i] = "";
// Lee el fichero via ResourceHelper (pack o filesystem)
auto bytes = ResourceHelper::loadFile(file);
if (bytes.empty()) {
return false;
}
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
std::string line;
int index = 0;
while (std::getline(ss, line)) {
// Almacena solo las lineas que no empiezan por # o no esten vacias
const bool test1 = line.substr(0, 1) != "#";
const bool test2 = !line.empty();
if (test1 && test2) {
mTextStrings[index] = line;
index++;
}
}
return true;
}
// Obtiene la cadena de texto del indice
std::string Lang::getText(int index) {
return mTextStrings[index];
}

View File

@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <string> // for string, basic_string #include <string> // for string, basic_string
class Asset;
// Códigos de idioma // Códigos de idioma
constexpr int es_ES = 0; constexpr int es_ES = 0;
@@ -17,12 +16,19 @@ constexpr int MAX_TEXT_STRINGS = 100;
// Clase Lang // Clase Lang
class Lang { class Lang {
private: private:
Asset *mAsset; // Objeto que gestiona todos los ficheros de recursos
std::string mTextStrings[MAX_TEXT_STRINGS]; // Vector con los textos std::string mTextStrings[MAX_TEXT_STRINGS]; // Vector con los textos
// Constructor privado (usar Lang::init)
Lang();
// Instancia única
static Lang *instance;
public: public:
// Constructor // Singleton API
Lang(Asset *mAsset); static void init(); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Lang *; // Obtiene el puntero a la instancia
// Destructor // Destructor
~Lang(); ~Lang();

View File

@@ -1,170 +1,144 @@
#include "animatedsprite.h" #include "core/rendering/animatedsprite.h"
#include <fstream> // for basic_ostream, operator<<, basic_istream, basic... #include <fstream> // for basic_ostream, operator<<, basic_istream, basic...
#include <iostream> // for cout #include <iostream> // for cout
#include <sstream> // for basic_stringstream #include <sstream> // for basic_stringstream
#include "texture.h" // for Texture #include "core/rendering/texture.h" // for Texture
// Carga la animación desde un fichero // Parser compartido: lee un istream con el formato .ani
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose) { static animatedSprite_t parseAnimationStream(std::istream &file, Texture *texture, const std::string &filename, bool verbose) {
// Inicializa variables
animatedSprite_t as; animatedSprite_t as;
as.texture = texture; as.texture = texture;
int framesPerRow = 0; int framesPerRow = 0;
int frameWidth = 0; int frameWidth = 0;
int frameHeight = 0; int frameHeight = 0;
int maxTiles = 0; int maxTiles = 0;
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
std::ifstream file(filePath);
std::string line; std::string line;
// El fichero se puede abrir if (verbose) {
if (file.good()) { std::cout << "Animation loaded: " << filename << std::endl;
// Procesa el fichero linea a linea }
if (verbose) { while (std::getline(file, line)) {
std::cout << "Animation loaded: " << filename << std::endl; if (line == "[animation]") {
} animation_t buffer;
while (std::getline(file, line)) { buffer.speed = 0;
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación buffer.loop = -1;
if (line == "[animation]") { buffer.counter = 0;
animation_t buffer; buffer.currentFrame = 0;
buffer.counter = 0; buffer.completed = false;
buffer.currentFrame = 0;
buffer.completed = false;
do { do {
std::getline(file, line); std::getline(file, line);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "name") {
buffer.name = line.substr(pos + 1, line.length());
}
else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "loop") {
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frames") {
// Se introducen los valores separados por comas en un vector
std::stringstream ss(line.substr(pos + 1, line.length()));
std::string tmp;
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
while (getline(ss, tmp, ',')) {
// Comprueba que el tile no sea mayor que el maximo indice permitido
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
}
else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
}
} while (line != "[/animation]");
// Añade la animación al vector de animaciones
as.animations.push_back(buffer);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
int pos = line.find("="); int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) { if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") { if (line.substr(0, pos) == "name") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length())); buffer.name = line.substr(pos + 1, line.length());
} } else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
else if (line.substr(0, pos) == "frameWidth") { } else if (line.substr(0, pos) == "loop") {
frameWidth = std::stoi(line.substr(pos + 1, line.length())); buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
} } else if (line.substr(0, pos) == "frames") {
std::stringstream ss(line.substr(pos + 1, line.length()));
else if (line.substr(0, pos) == "frameHeight") { std::string tmp;
frameHeight = std::stoi(line.substr(pos + 1, line.length())); SDL_Rect rect = {0, 0, frameWidth, frameHeight};
} while (getline(ss, tmp, ',')) {
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
else { rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl; std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
} }
}
} while (line != "[/animation]");
// Normaliza valores as.animations.push_back(buffer);
if (framesPerRow == 0 && frameWidth > 0) { } else {
framesPerRow = texture->getWidth() / frameWidth; int pos = line.find("=");
} if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
} else if (line.substr(0, pos) == "frameWidth") {
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
} else if (line.substr(0, pos) == "frameHeight") {
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) { if (framesPerRow == 0 && frameWidth > 0) {
const int w = texture->getWidth() / frameWidth; framesPerRow = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight; }
maxTiles = w * h; if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
} const int w = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight;
maxTiles = w * h;
} }
} }
} }
// Cierra el fichero
file.close();
}
// El fichero no se puede abrir
else {
if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
} }
return as; return as;
} }
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, const std::string &filePath, bool verbose) {
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
std::ifstream file(filePath);
if (!file.good()) {
if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
animatedSprite_t as;
as.texture = texture;
return as;
}
return parseAnimationStream(file, texture, filename, verbose);
}
// Carga la animación desde bytes en memoria
animatedSprite_t loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &nameForLogs, bool verbose) {
if (bytes.empty()) {
animatedSprite_t as;
as.texture = texture;
return as;
}
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
return parseAnimationStream(ss, texture, nameForLogs, verbose);
}
// Constructor // Constructor
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, std::string file, std::vector<std::string> *buffer) { AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, const std::string &file, std::vector<std::string> *buffer)
: currentAnimation(0) {
// Copia los punteros // Copia los punteros
setTexture(texture); setTexture(texture);
setRenderer(renderer); setRenderer(renderer);
// Carga las animaciones // Carga las animaciones
if (file != "") { if (!file.empty()) {
animatedSprite_t as = loadAnimationFromFile(texture, file); animatedSprite_t as = loadAnimationFromFile(texture, file);
// Copia los datos de las animaciones // Copia los datos de las animaciones
for (auto animation : as.animations) { animation.insert(animation.end(), as.animations.begin(), as.animations.end());
this->animation.push_back(animation);
}
} }
else if (buffer) { else if (buffer) {
loadFromVector(buffer); loadFromVector(buffer);
} }
// Inicializa variables
currentAnimation = 0;
} }
// Constructor // Constructor
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation) { AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation)
: currentAnimation(0) {
// Copia los punteros // Copia los punteros
setTexture(animation->texture); setTexture(animation->texture);
setRenderer(renderer); setRenderer(renderer);
// Inicializa variables
currentAnimation = 0;
// Copia los datos de las animaciones // Copia los datos de las animaciones
for (auto a : animation->animations) { this->animation.insert(this->animation.end(), animation->animations.begin(), animation->animations.end());
this->animation.push_back(a);
}
} }
// Destructor // Destructor
@@ -176,10 +150,10 @@ AnimatedSprite::~AnimatedSprite() {
} }
// Obtiene el indice de la animación a partir del nombre // Obtiene el indice de la animación a partir del nombre
int AnimatedSprite::getIndex(std::string name) { int AnimatedSprite::getIndex(const std::string &name) {
int index = -1; int index = -1;
for (auto a : animation) { for (const auto &a : animation) {
index++; index++;
if (a.name == name) { if (a.name == name) {
return index; return index;
@@ -242,12 +216,12 @@ void AnimatedSprite::setCurrentFrame(int num) {
} }
// Establece el valor del contador // Establece el valor del contador
void AnimatedSprite::setAnimationCounter(std::string name, int num) { void AnimatedSprite::setAnimationCounter(const std::string &name, int num) {
animation[getIndex(name)].counter = num; animation[getIndex(name)].counter = num;
} }
// Establece la velocidad de una animación // Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(std::string name, int speed) { void AnimatedSprite::setAnimationSpeed(const std::string &name, int speed) {
animation[getIndex(name)].counter = speed; animation[getIndex(name)].counter = speed;
} }
@@ -257,7 +231,7 @@ void AnimatedSprite::setAnimationSpeed(int index, int speed) {
} }
// Establece si la animación se reproduce en bucle // Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(std::string name, int loop) { void AnimatedSprite::setAnimationLoop(const std::string &name, int loop) {
animation[getIndex(name)].loop = loop; animation[getIndex(name)].loop = loop;
} }
@@ -267,7 +241,7 @@ void AnimatedSprite::setAnimationLoop(int index, int loop) {
} }
// Establece el valor de la variable // Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(std::string name, bool value) { void AnimatedSprite::setAnimationCompleted(const std::string &name, bool value) {
animation[getIndex(name)].completed = value; animation[getIndex(name)].completed = value;
} }
@@ -282,7 +256,7 @@ bool AnimatedSprite::animationIsCompleted() {
} }
// Devuelve el rectangulo de una animación y frame concreto // Devuelve el rectangulo de una animación y frame concreto
SDL_Rect AnimatedSprite::getAnimationClip(std::string name, Uint8 index) { SDL_Rect AnimatedSprite::getAnimationClip(const std::string &name, Uint8 index) {
return animation[getIndex(name)].frames[index]; return animation[getIndex(name)].frames[index];
} }
@@ -312,6 +286,8 @@ bool AnimatedSprite::loadFromVector(std::vector<std::string> *source) {
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación // Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
if (line == "[animation]") { if (line == "[animation]") {
animation_t buffer; animation_t buffer;
buffer.speed = 0;
buffer.loop = -1;
buffer.counter = 0; buffer.counter = 0;
buffer.currentFrame = 0; buffer.currentFrame = 0;
buffer.completed = false; buffer.completed = false;
@@ -411,7 +387,7 @@ bool AnimatedSprite::loadFromVector(std::vector<std::string> *source) {
} }
// Establece la animacion actual // Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(std::string name) { void AnimatedSprite::setCurrentAnimation(const std::string &name) {
const int newAnimation = getIndex(name); const int newAnimation = getIndex(name);
if (currentAnimation != newAnimation) { if (currentAnimation != newAnimation) {
currentAnimation = newAnimation; currentAnimation = newAnimation;

View File

@@ -2,10 +2,11 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string, basic_string #include <string> // for string, basic_string
#include <vector> // for vector #include <vector> // for vector
#include "movingsprite.h" // for MovingSprite #include "core/rendering/movingsprite.h" // for MovingSprite
class Texture; class Texture;
struct animation_t { struct animation_t {
@@ -24,7 +25,10 @@ struct animatedSprite_t {
}; };
// Carga la animación desde un fichero // Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose = false); animatedSprite_t loadAnimationFromFile(Texture *texture, const std::string &filePath, bool verbose = false);
// Carga la animación desde bytes en memoria
animatedSprite_t loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &nameForLogs = "", bool verbose = false);
class AnimatedSprite : public MovingSprite { class AnimatedSprite : public MovingSprite {
private: private:
@@ -34,11 +38,11 @@ class AnimatedSprite : public MovingSprite {
public: public:
// Constructor // Constructor
AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, std::string file = "", std::vector<std::string> *buffer = nullptr); explicit AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, const std::string &file = "", std::vector<std::string> *buffer = nullptr);
AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation); AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation);
// Destructor // Destructor
~AnimatedSprite(); ~AnimatedSprite() override;
// Calcula el frame correspondiente a la animación actual // Calcula el frame correspondiente a la animación actual
void animate(); void animate();
@@ -50,39 +54,39 @@ class AnimatedSprite : public MovingSprite {
void setCurrentFrame(int num); void setCurrentFrame(int num);
// Establece el valor del contador // Establece el valor del contador
void setAnimationCounter(std::string name, int num); void setAnimationCounter(const std::string &name, int num);
// Establece la velocidad de una animación // Establece la velocidad de una animación
void setAnimationSpeed(std::string name, int speed); void setAnimationSpeed(const std::string &name, int speed);
void setAnimationSpeed(int index, int speed); void setAnimationSpeed(int index, int speed);
// Establece el frame al que vuelve la animación al finalizar // Establece el frame al que vuelve la animación al finalizar
void setAnimationLoop(std::string name, int loop); void setAnimationLoop(const std::string &name, int loop);
void setAnimationLoop(int index, int loop); void setAnimationLoop(int index, int loop);
// Establece el valor de la variable // Establece el valor de la variable
void setAnimationCompleted(std::string name, bool value); void setAnimationCompleted(const std::string &name, bool value);
void setAnimationCompleted(int index, bool value); void setAnimationCompleted(int index, bool value);
// Comprueba si ha terminado la animación // Comprueba si ha terminado la animación
bool animationIsCompleted(); bool animationIsCompleted();
// Devuelve el rectangulo de una animación y frame concreto // Devuelve el rectangulo de una animación y frame concreto
SDL_Rect getAnimationClip(std::string name = "default", Uint8 index = 0); SDL_Rect getAnimationClip(const std::string &name = "default", Uint8 index = 0);
SDL_Rect getAnimationClip(int indexA = 0, Uint8 indexF = 0); SDL_Rect getAnimationClip(int indexA = 0, Uint8 indexF = 0);
// Obtiene el indice de la animación a partir del nombre // Obtiene el indice de la animación a partir del nombre
int getIndex(std::string name); int getIndex(const std::string &name);
// Carga la animación desde un vector // Carga la animación desde un vector
bool loadFromVector(std::vector<std::string> *source); bool loadFromVector(std::vector<std::string> *source);
// Establece la animacion actual // Establece la animacion actual
void setCurrentAnimation(std::string name = "default"); void setCurrentAnimation(const std::string &name = "default");
void setCurrentAnimation(int index = 0); void setCurrentAnimation(int index = 0);
// Actualiza las variables del objeto // Actualiza las variables del objeto
void update(); void update() override;
// OLD - Establece el rectangulo para un frame de una animación // OLD - Establece el rectangulo para un frame de una animación
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h); void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h);

View File

@@ -1,16 +1,15 @@
#include "fade.h" #include "core/rendering/fade.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <stdlib.h> // for rand #include <stdlib.h> // for rand
#include <iostream> // for char_traits, basic_ostream, operator<< #include <iostream> // for char_traits, basic_ostream, operator<<
#include "const.h" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH #include "game/defaults.hpp" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
// Constructor // Constructor
Fade::Fade(SDL_Renderer *renderer) { Fade::Fade(SDL_Renderer *renderer)
mRenderer = renderer; : mRenderer(renderer) {
mBackbuffer = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT); mBackbuffer = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
if (mBackbuffer != nullptr) { if (mBackbuffer != nullptr) {
SDL_SetTextureScaleMode(mBackbuffer, SDL_SCALEMODE_NEAREST); SDL_SetTextureScaleMode(mBackbuffer, SDL_SCALEMODE_NEAREST);
@@ -35,6 +34,12 @@ void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
mR = r; mR = r;
mG = g; mG = g;
mB = b; mB = b;
mROriginal = r;
mGOriginal = g;
mBOriginal = b;
mLastSquareTicks = 0;
mSquaresDrawn = 0;
mFullscreenDone = false;
} }
// Pinta una transición en pantalla // Pinta una transición en pantalla
@@ -42,30 +47,34 @@ void Fade::render() {
if (mEnabled && !mFinished) { if (mEnabled && !mFinished) {
switch (mFadeType) { switch (mFadeType) {
case FADE_FULLSCREEN: { case FADE_FULLSCREEN: {
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT}; if (!mFullscreenDone) {
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
for (int i = 0; i < 256; i += 4) { int alpha = mCounter * 4;
// Dibujamos sobre el renderizador if (alpha >= 255) {
SDL_SetRenderTarget(mRenderer, nullptr); mFullscreenDone = true;
// Copia el backbuffer con la imagen que había al renderizador // Deja todos los buffers del mismo color
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr); SDL_SetRenderTarget(mRenderer, mBackbuffer);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, i); SDL_SetRenderTarget(mRenderer, nullptr);
SDL_RenderFillRect(mRenderer, &fRect1); SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
// Vuelca el renderizador en pantalla mFinished = true;
SDL_RenderPresent(mRenderer); } else {
// Dibujamos sobre el renderizador
SDL_SetRenderTarget(mRenderer, nullptr);
// Copia el backbuffer con la imagen que había al renderizador
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, alpha);
SDL_RenderFillRect(mRenderer, &fRect1);
}
} }
// Deja todos los buffers del mismo color
SDL_SetRenderTarget(mRenderer, mBackbuffer);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
SDL_SetRenderTarget(mRenderer, nullptr);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
break; break;
} }
@@ -89,14 +98,17 @@ void Fade::render() {
} }
case FADE_RANDOM_SQUARE: { case FADE_RANDOM_SQUARE: {
SDL_FRect fRs = {0, 0, 32, 32}; Uint32 now = SDL_GetTicks();
if (mSquaresDrawn < 50 && now - mLastSquareTicks >= 100) {
mLastSquareTicks = now;
SDL_FRect fRs = {0, 0, 32, 32};
for (Uint16 i = 0; i < 50; i++) {
// Crea un color al azar // Crea un color al azar
mR = 255 * (rand() % 2); Uint8 r = 255 * (rand() % 2);
mG = 255 * (rand() % 2); Uint8 g = 255 * (rand() % 2);
mB = 255 * (rand() % 2); Uint8 b = 255 * (rand() % 2);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64); SDL_SetRenderDrawColor(mRenderer, r, g, b, 64);
// Dibujamos sobre el backbuffer // Dibujamos sobre el backbuffer
SDL_SetRenderTarget(mRenderer, mBackbuffer); SDL_SetRenderTarget(mRenderer, mBackbuffer);
@@ -108,12 +120,14 @@ void Fade::render() {
// Volvemos a usar el renderizador de forma normal // Volvemos a usar el renderizador de forma normal
SDL_SetRenderTarget(mRenderer, nullptr); SDL_SetRenderTarget(mRenderer, nullptr);
// Copiamos el backbuffer al renderizador mSquaresDrawn++;
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr); }
// Volcamos el renderizador en pantalla // Copiamos el backbuffer al renderizador
SDL_RenderPresent(mRenderer); SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
SDL_Delay(100);
if (mSquaresDrawn >= 50) {
mFinished = true;
} }
break; break;
} }
@@ -140,6 +154,12 @@ void Fade::activateFade() {
mEnabled = true; mEnabled = true;
mFinished = false; mFinished = false;
mCounter = 0; mCounter = 0;
mSquaresDrawn = 0;
mLastSquareTicks = 0;
mFullscreenDone = false;
mR = mROriginal;
mG = mGOriginal;
mB = mBOriginal;
} }
// Comprueba si está activo // Comprueba si está activo

View File

@@ -0,0 +1,54 @@
#pragma once
#include <SDL3/SDL.h>
// Tipos de fundido
constexpr int FADE_FULLSCREEN = 0;
constexpr int FADE_CENTER = 1;
constexpr int FADE_RANDOM_SQUARE = 2;
// Clase Fade
class Fade {
private:
SDL_Renderer *mRenderer = nullptr; // El renderizador de la ventana
SDL_Texture *mBackbuffer = nullptr; // Textura para usar como backbuffer
Uint8 mFadeType = FADE_FULLSCREEN; // Tipo de fade a realizar
Uint16 mCounter = 0; // Contador interno
bool mEnabled = false; // Indica si el fade está activo
bool mFinished = false; // Indica si ha terminado la transición
Uint8 mR = 0, mG = 0, mB = 0; // Colores para el fade
Uint8 mROriginal = 0, mGOriginal = 0, mBOriginal = 0; // Colores originales para FADE_RANDOM_SQUARE
Uint32 mLastSquareTicks = 0; // Ticks del último cuadrado dibujado (FADE_RANDOM_SQUARE)
Uint16 mSquaresDrawn = 0; // Número de cuadrados dibujados (FADE_RANDOM_SQUARE)
bool mFullscreenDone = false; // Indica si el fade fullscreen ha terminado la fase de fundido
SDL_Rect mRect1{}; // Rectangulo usado para crear los efectos de transición
SDL_Rect mRect2{}; // Rectangulo usado para crear los efectos de transición
public:
// Constructor
explicit Fade(SDL_Renderer *renderer);
// Destructor
~Fade();
// Inicializa las variables
void init(Uint8 r, Uint8 g, Uint8 b);
// Pinta una transición en pantalla
void render();
// Actualiza las variables internas
void update();
// Activa el fade
void activateFade();
// Comprueba si ha terminado la transicion
bool hasEnded();
// Comprueba si está activo
bool isEnabled();
// Establece el tipo de fade
void setFadeType(Uint8 fadeType);
};

View File

@@ -1,55 +1,28 @@
#include "movingsprite.h" #include "core/rendering/movingsprite.h"
#include "texture.h" // for Texture #include "core/rendering/texture.h" // for Texture
// Constructor // Constructor
MovingSprite::MovingSprite(float x, float y, int w, int h, float velx, float vely, float accelx, float accely, Texture *texture, SDL_Renderer *renderer) { MovingSprite::MovingSprite(float x, float y, int w, int h, float velx, float vely, float accelx, float accely, Texture *texture, SDL_Renderer *renderer)
// Copia los punteros : Sprite(0, 0, w, h, texture, renderer),
this->texture = texture; x(x),
this->renderer = renderer; y(y),
xPrev(x),
// Establece el alto y el ancho del sprite yPrev(y),
this->w = w; vx(velx),
this->h = h; vy(vely),
ax(accelx),
// Establece la posición X,Y del sprite ay(accely),
this->x = x; zoomW(1),
this->y = y; zoomH(1),
xPrev = x; angle(0.0),
yPrev = y; rotateEnabled(false),
rotateSpeed(0),
// Establece la velocidad X,Y del sprite rotateAmount(0.0),
vx = velx; counter(0),
vy = vely; center(nullptr),
currentFlip(SDL_FLIP_NONE) {
// Establece la aceleración X,Y del sprite }
ax = accelx;
ay = accely;
// Establece el zoom W,H del sprite
zoomW = 1;
zoomH = 1;
// Establece el angulo con el que se dibujará
angle = (double)0;
// Establece los valores de rotacion
rotateEnabled = false;
rotateSpeed = 0;
rotateAmount = (double)0;
// Contador interno
counter = 0;
// Establece el rectangulo de donde coger la imagen
spriteClip = {0, 0, w, h};
// Establece el centro de rotación
center = nullptr;
// Establece el tipo de volteado
currentFlip = SDL_FLIP_NONE;
};
// Reinicia todas las variables // Reinicia todas las variables
void MovingSprite::clear() { void MovingSprite::clear() {
@@ -97,11 +70,13 @@ void MovingSprite::render() {
} }
// Obtiene el valor de la variable // Obtiene el valor de la variable
// cppcheck-suppress duplInheritedMember
float MovingSprite::getPosX() { float MovingSprite::getPosX() {
return x; return x;
} }
// Obtiene el valor de la variable // Obtiene el valor de la variable
// cppcheck-suppress duplInheritedMember
float MovingSprite::getPosY() { float MovingSprite::getPosY() {
return y; return y;
} }

View File

@@ -2,14 +2,16 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "sprite.h" // for Sprite #include "core/rendering/sprite.h" // for Sprite
class Texture; class Texture;
// Clase MovingSprite. Añade posicion y velocidad en punto flotante // Clase MovingSprite. Añade posicion y velocidad en punto flotante
class MovingSprite : public Sprite { class MovingSprite : public Sprite {
protected: protected:
float x; // Posición en el eje X // cppcheck-suppress duplInheritedMember
float y; // Posición en el eje Y float x; // Posición en el eje X (sub-pixel; Sprite::x es int)
// cppcheck-suppress duplInheritedMember
float y; // Posición en el eje Y (sub-pixel; Sprite::y es int)
float xPrev; // Posición anterior en el eje X float xPrev; // Posición anterior en el eje X
float yPrev; // Posición anterior en el eje Y float yPrev; // Posición anterior en el eje Y
@@ -33,7 +35,7 @@ class MovingSprite : public Sprite {
public: public:
// Constructor // Constructor
MovingSprite(float x = 0, float y = 0, int w = 0, int h = 0, float velx = 0, float vely = 0, float accelx = 0, float accely = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); explicit MovingSprite(float x = 0, float y = 0, int w = 0, int h = 0, float velx = 0, float vely = 0, float accelx = 0, float accely = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr);
// Mueve el sprite // Mueve el sprite
void move(); void move();
@@ -42,18 +44,20 @@ class MovingSprite : public Sprite {
void rotate(); void rotate();
// Actualiza las variables internas del objeto // Actualiza las variables internas del objeto
void update(); virtual void update();
// Reinicia todas las variables // Reinicia todas las variables
void clear(); void clear();
// Muestra el sprite por pantalla // Muestra el sprite por pantalla
void render(); void render() override;
// Obten el valor de la variable // Obten el valor de la variable
// cppcheck-suppress duplInheritedMember
float getPosX(); float getPosX();
// Obten el valor de la variable // Obten el valor de la variable
// cppcheck-suppress duplInheritedMember
float getPosY(); float getPosY();
// Obten el valor de la variable // Obten el valor de la variable
@@ -84,7 +88,7 @@ class MovingSprite : public Sprite {
Uint16 getRotateSpeed(); Uint16 getRotateSpeed();
// Establece la posición y el tamaño del objeto // Establece la posición y el tamaño del objeto
void setRect(SDL_Rect rect); void setRect(SDL_Rect rect) override;
// Establece el valor de la variable // Establece el valor de la variable
void setPosX(float value); void setPosX(float value);
@@ -144,7 +148,7 @@ class MovingSprite : public Sprite {
SDL_FlipMode getFlip(); SDL_FlipMode getFlip();
// Devuelve el rectangulo donde está el sprite // Devuelve el rectangulo donde está el sprite
SDL_Rect getRect(); SDL_Rect getRect() override;
// Deshace el último movimiento // Deshace el último movimiento
void undoMove(); void undoMove();

View File

@@ -0,0 +1,722 @@
#include "core/rendering/screen.h"
#include <SDL3/SDL.h>
#include <algorithm> // for max, min
#include <cstring> // for memcpy
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include <string> // for basic_string, char_traits, string
#include "core/input/mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime
#include "core/rendering/text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_STROKE
#include "core/resources/resource.h"
#include "game/defaults.hpp" // for GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT
#include "game/options.hpp" // for Options::video, Options::settings
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // for Rendering::SDL3GPUShader
#endif
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
// --- Fix per a fullscreen/resize en Emscripten ---
//
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
// anterior i el viewport queda desencuadrat.
//
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
// del event loop (el canvas encara no està estable en el moment del callback)
// i cridar setVideoMode() amb el flag de fullscreen actualitzat. La crida
// interna a SDL_SetWindowFullscreen(false) és la peça que realment fa eixir
// SDL del seu estat intern de fullscreen — sense això res més funciona.
namespace {
Screen *g_screen_instance = nullptr;
void deferredCanvasResize(void * /*userData*/) {
if (g_screen_instance) {
g_screen_instance->handleCanvasResized();
}
}
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent *event, void * /*userData*/) {
if (g_screen_instance && event) {
g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
}
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent * /*event*/, void * /*userData*/) {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif // __EMSCRIPTEN__
// Instancia única
Screen *Screen::instance = nullptr;
// Singleton API
void Screen::init(SDL_Window *window, SDL_Renderer *renderer) {
Screen::instance = new Screen(window, renderer);
}
void Screen::destroy() {
delete Screen::instance;
Screen::instance = nullptr;
}
auto Screen::get() -> Screen * {
return Screen::instance;
}
// Constructor
Screen::Screen(SDL_Window *window, SDL_Renderer *renderer)
: borderColor{0x00, 0x00, 0x00} {
// Inicializa variables
this->window = window;
this->renderer = renderer;
gameCanvasWidth = GAMECANVAS_WIDTH;
gameCanvasHeight = GAMECANVAS_HEIGHT;
// Establece el modo de video (fullscreen/ventana + logical presentation)
// ANTES de crear la textura — SDL3 GPU necesita la logical presentation
// del renderer ya aplicada al swapchain quan es reclama la ventana per a GPU.
// Mirror del pattern de jaildoctors_dilemma (que usa exactament 256×192 i
// funciona) on `initSDLVideo` configura la presentation abans de crear cap
// textura.
setVideoMode(Options::video.fullscreen);
// Força al window manager a completar el resize/posicionat abans de passar
// la ventana al dispositiu GPU. Sense açò en Linux/X11 hi ha un race
// condition que deixa el swapchain en estat inestable i fa crashear el
// driver Vulkan en `SDL_CreateGPUGraphicsPipeline`.
SDL_SyncWindow(window);
// Crea la textura donde se dibujan los graficos del juego.
// ARGB8888 per simplificar el readback cap al pipeline SDL3 GPU.
gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gameCanvasWidth, gameCanvasHeight);
if (gameCanvas != nullptr) {
SDL_SetTextureScaleMode(gameCanvas, Options::video.scale_mode);
}
if (gameCanvas == nullptr) {
if (Options::settings.console) {
std::cout << "gameCanvas could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
}
#ifndef NO_SHADERS
// Buffer de readback del gameCanvas (lo dimensionamos una vez)
pixel_buffer_.resize(static_cast<size_t>(gameCanvasWidth) * static_cast<size_t>(gameCanvasHeight));
#endif
// Renderiza una vez la textura vacía al renderer abans d'inicialitzar els
// shaders: jaildoctors_dilemma ho fa així i evita que el driver Vulkan
// crashegi en la creació del pipeline gràfic. `initShaders()` es crida
// després des de `Director` amb el swapchain ja estable.
SDL_RenderTexture(renderer, gameCanvas, nullptr, nullptr);
// Estado inicial de las notificaciones. El Text real se enlaza después vía
// `initNotifications()` quan `Resource` ja estigui inicialitzat. Dividim
// això del constructor perquè `initShaders()` (GPU) ha de cridar-se ABANS
// de carregar recursos: si el SDL_Renderer ha fet abans moltes
// allocacions (carrega de textures), el driver Vulkan crasheja quan
// després es reclama la ventana per al dispositiu GPU.
notificationText = nullptr;
notificationMessage = "";
notificationTextColor = {0xFF, 0xFF, 0xFF};
notificationOutlineColor = {0x00, 0x00, 0x00};
notificationEndTime = 0;
notificationY = 2;
// Registra callbacks natius d'Emscripten per a fullscreen/orientation
registerEmscriptenEventCallbacks();
}
// Enllaça el Text de les notificacions amb el recurs compartit de `Resource`.
// S'ha de cridar després de `Resource::init(...)`.
void Screen::initNotifications() {
notificationText = Resource::get()->getText("8bithud");
}
// Destructor
Screen::~Screen() {
// notificationText es propiedad de Resource — no liberar.
#ifndef NO_SHADERS
shutdownShaders();
#endif
SDL_DestroyTexture(gameCanvas);
}
// Limpia la pantalla
void Screen::clean(color_t color) {
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 0xFF);
SDL_RenderClear(renderer);
}
// Prepara para empezar a dibujar en la textura de juego
void Screen::start() {
SDL_SetRenderTarget(renderer, gameCanvas);
}
// Vuelca el contenido del renderizador en pantalla
void Screen::blit() {
// Dibuja la notificación activa sobre el gameCanvas antes de presentar
SDL_SetRenderTarget(renderer, gameCanvas);
renderNotification();
#ifndef NO_SHADERS
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
// shaders com sense). Seguim el mateix pattern que aee_plus: quan shader
// està desactivat, forcem POSTFX + params a zero només per a aquest frame
// i restaurem el shader actiu, així CRTPI no aplica les seues scanlines
// quan l'usuari ho ha desactivat.
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
SDL_Surface *surface = SDL_RenderReadPixels(renderer, nullptr);
if (surface != nullptr) {
if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
std::memcpy(pixel_buffer_.data(), surface->pixels, pixel_buffer_.size() * sizeof(Uint32));
} else {
SDL_Surface *converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
if (converted != nullptr) {
std::memcpy(pixel_buffer_.data(), converted->pixels, pixel_buffer_.size() * sizeof(Uint32));
SDL_DestroySurface(converted);
}
}
SDL_DestroySurface(surface);
}
SDL_SetRenderTarget(renderer, nullptr);
if (Options::video.shader.enabled) {
// Ruta normal: shader amb els seus params.
shader_backend_->uploadPixels(pixel_buffer_.data(), gameCanvasWidth, gameCanvasHeight);
shader_backend_->render();
} else {
// Shader off: POSTFX amb params zero (passa-per-aquí). CRTPI no
// val perque sempre aplica els seus efectes interns; salvem i
// restaurem el shader actiu.
const auto PREV_SHADER = shader_backend_->getActiveShader();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
shader_backend_->uploadPixels(pixel_buffer_.data(), gameCanvasWidth, gameCanvasHeight);
shader_backend_->render();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(PREV_SHADER);
}
}
return;
}
#endif
// Vuelve a dejar el renderizador en modo normal
SDL_SetRenderTarget(renderer, nullptr);
// Borra el contenido previo
SDL_SetRenderDrawColor(renderer, borderColor.r, borderColor.g, borderColor.b, 0xFF);
SDL_RenderClear(renderer);
// Copia la textura de juego en el renderizador en la posición adecuada
SDL_FRect fdest = {(float)dest.x, (float)dest.y, (float)dest.w, (float)dest.h};
SDL_RenderTexture(renderer, gameCanvas, nullptr, &fdest);
// Muestra por pantalla el renderizador
SDL_RenderPresent(renderer);
}
// ============================================================================
// Video y ventana
// ============================================================================
// Establece el modo de video
void Screen::setVideoMode(bool fullscreen) {
applyFullscreen(fullscreen);
if (fullscreen) {
applyFullscreenLayout();
} else {
applyWindowedLayout();
}
applyLogicalPresentation(fullscreen);
}
// Cambia entre pantalla completa y ventana
void Screen::toggleVideoMode() {
setVideoMode(!Options::video.fullscreen);
}
// Reduce el zoom de la ventana
auto Screen::decWindowZoom() -> bool {
if (Options::video.fullscreen) { return false; }
const int PREV = Options::window.zoom;
Options::window.zoom = std::max(Options::window.zoom - 1, WINDOW_ZOOM_MIN);
if (Options::window.zoom == PREV) { return false; }
setVideoMode(false);
return true;
}
// Aumenta el zoom de la ventana
auto Screen::incWindowZoom() -> bool {
if (Options::video.fullscreen) { return false; }
const int PREV = Options::window.zoom;
Options::window.zoom = std::min(Options::window.zoom + 1, WINDOW_ZOOM_MAX);
if (Options::window.zoom == PREV) { return false; }
setVideoMode(false);
return true;
}
// Establece el zoom de la ventana directamente
auto Screen::setWindowZoom(int zoom) -> bool {
if (Options::video.fullscreen) { return false; }
if (zoom < WINDOW_ZOOM_MIN || zoom > WINDOW_ZOOM_MAX) { return false; }
if (zoom == Options::window.zoom) { return false; }
Options::window.zoom = zoom;
setVideoMode(false);
return true;
}
// Establece el escalado entero
void Screen::setIntegerScale(bool enabled) {
if (Options::video.integer_scale == enabled) { return; }
Options::video.integer_scale = enabled;
setVideoMode(Options::video.fullscreen);
}
// Alterna el escalado entero
void Screen::toggleIntegerScale() {
setIntegerScale(!Options::video.integer_scale);
}
// Establece el V-Sync
void Screen::setVSync(bool enabled) {
Options::video.vsync = enabled;
SDL_SetRenderVSync(renderer, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
#ifndef NO_SHADERS
if (shader_backend_) {
shader_backend_->setVSync(enabled);
}
#endif
}
// Alterna el V-Sync
void Screen::toggleVSync() {
setVSync(!Options::video.vsync);
}
// Cambia el color del borde
void Screen::setBorderColor(color_t color) {
borderColor = color;
}
// ============================================================================
// Helpers privados de setVideoMode
// ============================================================================
// SDL_SetWindowFullscreen + visibilidad del cursor
void Screen::applyFullscreen(bool fullscreen) {
SDL_SetWindowFullscreen(window, fullscreen);
if (fullscreen) {
SDL_HideCursor();
Mouse::cursorVisible = false;
} else {
SDL_ShowCursor();
Mouse::cursorVisible = true;
Mouse::lastMouseMoveTime = SDL_GetTicks();
}
}
// Calcula windowWidth/Height/dest para el modo ventana y aplica SDL_SetWindowSize
void Screen::applyWindowedLayout() {
windowWidth = gameCanvasWidth;
windowHeight = gameCanvasHeight;
dest = {0, 0, gameCanvasWidth, gameCanvasHeight};
#ifdef __EMSCRIPTEN__
windowWidth *= WASM_RENDER_SCALE;
windowHeight *= WASM_RENDER_SCALE;
dest.w *= WASM_RENDER_SCALE;
dest.h *= WASM_RENDER_SCALE;
#endif
// Modifica el tamaño de la ventana
SDL_SetWindowSize(window, windowWidth * Options::window.zoom, windowHeight * Options::window.zoom);
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Obtiene el tamaño de la ventana en fullscreen y calcula el rect del juego
void Screen::applyFullscreenLayout() {
SDL_GetWindowSize(window, &windowWidth, &windowHeight);
computeFullscreenGameRect();
}
// Calcula el rectángulo dest para fullscreen: integer_scale / aspect ratio
void Screen::computeFullscreenGameRect() {
if (Options::video.integer_scale) {
// Calcula el tamaño de la escala máxima
int scale = 0;
while (((gameCanvasWidth * (scale + 1)) <= windowWidth) && ((gameCanvasHeight * (scale + 1)) <= windowHeight)) {
scale++;
}
dest.w = gameCanvasWidth * scale;
dest.h = gameCanvasHeight * scale;
dest.x = (windowWidth - dest.w) / 2;
dest.y = (windowHeight - dest.h) / 2;
} else {
// Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox).
float ratio = (float)gameCanvasWidth / (float)gameCanvasHeight;
if ((windowWidth - gameCanvasWidth) >= (windowHeight - gameCanvasHeight)) {
dest.h = windowHeight;
dest.w = (int)((windowHeight * ratio) + 0.5f);
dest.x = (windowWidth - dest.w) / 2;
dest.y = (windowHeight - dest.h) / 2;
} else {
dest.w = windowWidth;
dest.h = (int)((windowWidth / ratio) + 0.5f);
dest.x = (windowWidth - dest.w) / 2;
dest.y = (windowHeight - dest.h) / 2;
}
}
}
// Aplica la logical presentation y persiste el estado en options
void Screen::applyLogicalPresentation(bool fullscreen) {
SDL_SetRenderLogicalPresentation(renderer, windowWidth, windowHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
Options::video.fullscreen = fullscreen;
}
// ============================================================================
// Notificaciones
// ============================================================================
// Muestra una notificación en la línea superior durante durationMs
void Screen::notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs) {
notificationMessage = text;
notificationTextColor = textColor;
notificationOutlineColor = outlineColor;
notificationEndTime = SDL_GetTicks() + durationMs;
}
// Limpia la notificación actual
void Screen::clearNotification() {
notificationEndTime = 0;
notificationMessage.clear();
}
// Dibuja la notificación activa (si la hay) sobre el gameCanvas
void Screen::renderNotification() {
if (notificationText == nullptr || SDL_GetTicks() >= notificationEndTime) {
return;
}
notificationText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE,
gameCanvasWidth / 2,
notificationY,
notificationMessage,
1,
notificationTextColor,
1,
notificationOutlineColor);
}
// ============================================================================
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
// ============================================================================
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
// La crida a SDL_SetWindowFullscreen + SDL_SetRenderLogicalPresentation
// que fa setVideoMode és l'única manera de resincronitzar l'estat intern
// de SDL amb el canvas HTML real.
setVideoMode(Options::video.fullscreen);
#endif
}
void Screen::syncFullscreenFlagFromBrowser(bool isFullscreen) {
#ifdef __EMSCRIPTEN__
Options::video.fullscreen = isFullscreen;
#else
(void)isFullscreen;
#endif
}
void Screen::registerEmscriptenEventCallbacks() {
#ifdef __EMSCRIPTEN__
// IMPORTANT: NO registrem resize callback. En mòbil, fer scroll fa que el
// navegador oculti/mostri la barra d'URL, disparant un resize del DOM per
// cada scroll. Això portava a cridar setVideoMode per cada scroll, que
// re-aplicava la logical presentation i corrompia el viewport intern de SDL.
g_screen_instance = this;
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// ============================================================================
// GPU / shaders (SDL3 GPU post-procesado). En builds con NO_SHADERS (Emscripten)
// las operaciones son no-op; la ruta clásica sigue siendo la única disponible.
// ============================================================================
#ifndef NO_SHADERS
// Aplica al backend el shader actiu + els seus presets PostFX i CrtPi.
// Només s'ha de cridar quan `videoShaderEnabled=true` (en cas contrari el
// blit() ja força POSTFX+zero params per a desactivar els efectes sense
// tocar els paràmetres emmagatzemats).
void Screen::applyShaderParams() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
return;
}
shader_backend_->setActiveShader(Options::video.shader.current_shader);
applyCurrentPostFXPreset();
applyCurrentCrtPiPreset();
}
#endif
void Screen::initShaders() {
#ifndef NO_SHADERS
if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
const std::string FALLBACK_DRIVER = "none";
shader_backend_->setPreferredDriver(
Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
}
if (!shader_backend_->isHardwareAccelerated()) {
const bool ok = shader_backend_->init(window, gameCanvas, "", "");
if (Options::settings.console) {
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n';
}
}
if (shader_backend_->isHardwareAccelerated()) {
shader_backend_->setScaleMode(Options::video.integer_scale);
shader_backend_->setVSync(Options::video.vsync);
// Resol els índexs de preset a partir del nom emmagatzemat al config.
// Si el nom no existeix (preset esborrat del YAML), es queda en 0.
for (int i = 0; i < static_cast<int>(Options::postfx_presets.size()); ++i) {
if (Options::postfx_presets[i].name == Options::video.shader.current_postfx_preset_name) {
Options::current_postfx_preset = i;
break;
}
}
for (int i = 0; i < static_cast<int>(Options::crtpi_presets.size()); ++i) {
if (Options::crtpi_presets[i].name == Options::video.shader.current_crtpi_preset_name) {
Options::current_crtpi_preset = i;
break;
}
}
applyShaderParams(); // aplica preset del shader actiu
}
#endif
}
void Screen::shutdownShaders() {
#ifndef NO_SHADERS
// Només es crida des del destructor de Screen. Els toggles runtime NO la
// poden cridar: destruir + recrear el dispositiu SDL3 GPU amb la ventana
// ja reclamada és inestable (Vulkan/Radeon crasheja en el següent claim).
if (shader_backend_) {
shader_backend_->cleanup();
shader_backend_.reset();
}
#endif
}
auto Screen::isGpuAccelerated() const -> bool {
#ifndef NO_SHADERS
return shader_backend_ && shader_backend_->isHardwareAccelerated();
#else
return false;
#endif
}
void Screen::setShaderEnabled(bool enabled) {
if (Options::video.shader.enabled == enabled) { return; }
Options::video.shader.enabled = enabled;
#ifndef NO_SHADERS
if (enabled) {
applyShaderParams(); // restaura preset del shader actiu
}
// Si enabled=false, blit() forçarà POSTFX+zero per frame — no cal tocar
// res ara.
#endif
const color_t CYAN = {0x00, 0xFF, 0xFF};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 1500;
notify(enabled ? "Shader: ON" : "Shader: OFF", CYAN, BLACK, DUR_MS);
}
void Screen::toggleShaderEnabled() {
setShaderEnabled(!Options::video.shader.enabled);
}
auto Screen::isShaderEnabled() const -> bool {
return Options::video.shader.enabled;
}
#ifndef NO_SHADERS
void Screen::setActiveShader(Rendering::ShaderType type) {
Options::video.shader.current_shader = type;
if (Options::video.shader.enabled) {
applyShaderParams();
}
const color_t MAGENTA = {0xFF, 0x00, 0xFF};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 1500;
notify(type == Rendering::ShaderType::CRTPI ? "Shader: CRTPI" : "Shader: POSTFX", MAGENTA, BLACK, DUR_MS);
}
auto Screen::getActiveShader() const -> Rendering::ShaderType {
return Options::video.shader.current_shader;
}
#endif
void Screen::toggleActiveShader() {
#ifndef NO_SHADERS
const Rendering::ShaderType NEXT = getActiveShader() == Rendering::ShaderType::POSTFX
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
setActiveShader(NEXT);
#else
Options::video.shader.current_shader = Options::video.shader.current_shader == Rendering::ShaderType::POSTFX
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
#endif
}
// ============================================================================
// Presets de shaders
// ============================================================================
void Screen::applyCurrentPostFXPreset() {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; }
if (Options::postfx_presets.empty()) { return; }
if (Options::current_postfx_preset < 0 || Options::current_postfx_preset >= static_cast<int>(Options::postfx_presets.size())) {
Options::current_postfx_preset = 0;
}
const auto &PRESET = Options::postfx_presets[Options::current_postfx_preset];
Rendering::PostFXParams p;
p.vignette = PRESET.vignette;
p.scanlines = PRESET.scanlines;
p.chroma = PRESET.chroma;
p.mask = PRESET.mask;
p.gamma = PRESET.gamma;
p.curvature = PRESET.curvature;
p.bleeding = PRESET.bleeding;
p.flicker = PRESET.flicker;
shader_backend_->setPostFXParams(p);
#endif
}
void Screen::applyCurrentCrtPiPreset() {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; }
if (Options::crtpi_presets.empty()) { return; }
if (Options::current_crtpi_preset < 0 || Options::current_crtpi_preset >= static_cast<int>(Options::crtpi_presets.size())) {
Options::current_crtpi_preset = 0;
}
const auto &PRESET = Options::crtpi_presets[Options::current_crtpi_preset];
Rendering::CrtPiParams p;
p.scanline_weight = PRESET.scanline_weight;
p.scanline_gap_brightness = PRESET.scanline_gap_brightness;
p.bloom_factor = PRESET.bloom_factor;
p.input_gamma = PRESET.input_gamma;
p.output_gamma = PRESET.output_gamma;
p.mask_brightness = PRESET.mask_brightness;
p.curvature_x = PRESET.curvature_x;
p.curvature_y = PRESET.curvature_y;
p.mask_type = PRESET.mask_type;
p.enable_scanlines = PRESET.enable_scanlines;
p.enable_multisample = PRESET.enable_multisample;
p.enable_gamma = PRESET.enable_gamma;
p.enable_curvature = PRESET.enable_curvature;
p.enable_sharper = PRESET.enable_sharper;
shader_backend_->setCrtPiParams(p);
#endif
}
auto Screen::getCurrentPresetName() const -> const char * {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return "---"; }
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
if (Options::current_postfx_preset >= 0 && Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size())) {
return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
}
} else {
if (Options::current_crtpi_preset >= 0 && Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size())) {
return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
}
}
#endif
return "---";
}
auto Screen::nextPreset() -> bool {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; }
if (!Options::video.shader.enabled) { return false; }
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) { return false; }
const int N = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % N;
Options::video.shader.current_postfx_preset_name =
Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset();
} else {
if (Options::crtpi_presets.empty()) { return false; }
const int N = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % N;
Options::video.shader.current_crtpi_preset_name =
Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset();
}
const color_t GREEN = {0x00, 0xFF, 0x80};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 1500;
notify(std::string("Preset: ") + getCurrentPresetName(), GREEN, BLACK, DUR_MS);
return true;
#else
return false;
#endif
}
auto Screen::prevPreset() -> bool {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; }
if (!Options::video.shader.enabled) { return false; }
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) { return false; }
const int N = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + N) % N;
Options::video.shader.current_postfx_preset_name =
Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset();
} else {
if (Options::crtpi_presets.empty()) { return false; }
const int N = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + N) % N;
Options::video.shader.current_crtpi_preset_name =
Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset();
}
const color_t GREEN = {0x00, 0xFF, 0x80};
const color_t BLACK = {0x00, 0x00, 0x00};
const Uint32 DUR_MS = 1500;
notify(std::string("Preset: ") + getCurrentPresetName(), GREEN, BLACK, DUR_MS);
return true;
#else
return false;
#endif
}

View File

@@ -0,0 +1,139 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // for unique_ptr
#include <string> // for string
#include <vector> // for vector
#include "utils/utils.h" // for color_t
#ifndef NO_SHADERS
#include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType
namespace Rendering {
class ShaderBackend;
}
#endif
class Text;
class Screen {
public:
// Constantes
static constexpr int WINDOW_ZOOM_MIN = 1;
static constexpr int WINDOW_ZOOM_MAX = 4;
#ifdef __EMSCRIPTEN__
// En WASM el tamaño de ventana está fijado a 1x, así que escalamos el
// renderizado por 3 aprovechando el modo NEAREST de la textura del juego
// para que los píxeles salgan nítidos.
static constexpr int WASM_RENDER_SCALE = 3;
#endif
// Singleton API
static void init(SDL_Window *window, SDL_Renderer *renderer); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Screen *; // Obtiene el puntero a la instancia
// Destructor (público por requisitos de `delete` desde destroy())
~Screen();
// Render loop
void clean(color_t color = {0x00, 0x00, 0x00}); // Limpia la pantalla
void start(); // Prepara para empezar a dibujar en la textura de juego
void blit(); // Vuelca el contenido del renderizador en pantalla
// Video y ventana
void setVideoMode(bool fullscreen); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten
void syncFullscreenFlagFromBrowser(bool isFullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten
void toggleIntegerScale(); // Alterna el escalado entero
void setIntegerScale(bool enabled); // Establece el escalado entero
void toggleVSync(); // Alterna el V-Sync
void setVSync(bool enabled); // Establece el V-Sync
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió)
auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió)
// Borde
void setBorderColor(color_t color); // Cambia el color del borde
// Notificaciones
void initNotifications(); // Enllaça el Text de notificacions amb `Resource`. A cridar després de `Resource::init(...)`.
void notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
void clearNotification(); // Limpia la notificación actual
// GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op.
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
void shutdownShaders(); // Libera el backend GPU
auto isGpuAccelerated() const -> bool; // true si el backend existe y reporta hardware OK
void setShaderEnabled(bool enabled); // Activa o desactiva el post-procesado (persiste)
void toggleShaderEnabled(); // Alterna post-procesado
auto isShaderEnabled() const -> bool; // Estado actual (lee options)
#ifndef NO_SHADERS
void setActiveShader(Rendering::ShaderType type); // POSTFX o CRTPI
auto getActiveShader() const -> Rendering::ShaderType;
#endif
void toggleActiveShader(); // Alterna POSTFX ↔ CRTPI
// Presets de shaders (PostFX/CrtPi). Operen sobre el shader actiu.
// Retornen false si GPU off / shaders off / llista buida (igual que a aee_plus).
auto nextPreset() -> bool;
auto prevPreset() -> bool;
auto getCurrentPresetName() const -> const char *;
void applyCurrentPostFXPreset(); // Escriu el preset PostFX actiu al backend
void applyCurrentCrtPiPreset(); // Escriu el preset CrtPi actiu al backend
private:
// Constructor privado (usar Screen::init)
Screen(SDL_Window *window, SDL_Renderer *renderer);
// Instancia única
static Screen *instance;
// Helpers internos de setVideoMode
void applyFullscreen(bool fullscreen); // SDL_SetWindowFullscreen + visibilidad del cursor
void applyWindowedLayout(); // Calcula windowWidth/Height/dest + SDL_SetWindowSize + SDL_SetWindowPosition
void applyFullscreenLayout(); // SDL_GetWindowSize + delegación a computeFullscreenGameRect
void computeFullscreenGameRect(); // Calcula dest en fullscreen (integerScale / keepAspect / stretched)
void applyLogicalPresentation(bool fullscreen); // SDL_SetRenderLogicalPresentation + persistencia a options
// Emscripten
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos de Emscripten para fullscreenchange y orientationchange. No-op fuera de Emscripten
// Notificaciones
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
#ifndef NO_SHADERS
// Aplica els paràmetres actuals del shader al backend segons options
// (pass-through si `videoShaderEnabled==false`, preset per defecte si true).
void applyShaderParams();
#endif
// Objetos y punteros
SDL_Window *window; // Ventana de la aplicación
SDL_Renderer *renderer; // El renderizador de la ventana
SDL_Texture *gameCanvas; // Textura para completar la ventana de juego hasta la pantalla completa
// Variables
int windowWidth; // Ancho de la pantalla o ventana
int windowHeight; // Alto de la pantalla o ventana
int gameCanvasWidth; // Resolución interna del juego. Es el ancho de la textura donde se dibuja el juego
int gameCanvasHeight; // Resolución interna del juego. Es el alto de la textura donde se dibuja el juego
SDL_Rect dest; // Coordenadas donde se va a dibujar la textura del juego sobre la pantalla o ventana
color_t borderColor; // Color del borde añadido a la textura de juego para rellenar la pantalla
// Notificaciones - una sola activa, sin apilación ni animaciones
Text *notificationText; // Fuente 8bithud dedicada a las notificaciones
std::string notificationMessage; // Texto a mostrar
color_t notificationTextColor; // Color del texto
color_t notificationOutlineColor; // Color del outline
Uint32 notificationEndTime; // SDL_GetTicks() hasta el cual se muestra
int notificationY; // Fila vertical en el canvas virtual
#ifndef NO_SHADERS
// GPU / shaders
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend GPU (nullptr si inactivo)
std::vector<Uint32> pixel_buffer_; // Buffer de readback del gameCanvas (ARGB8888)
#endif
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include "core/rendering/shader_backend.hpp"
// PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
struct PostFXUniforms {
float vignette_strength; // 0 = none, ~0.8 = subtle
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
float scanline_strength; // 0 = off, 1 = full
float screen_height; // logical height in pixels (used by bleeding effect)
float mask_strength; // 0 = off, 1 = full phosphor dot mask
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float curvature; // 0 = flat, 1 = max barrel distortion
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
};
// CrtPi uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
struct CrtPiUniforms {
// vec4 #0
float scanline_weight; // Ajuste gaussiano (default 6.0)
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
float input_gamma; // Gamma de entrada (default 2.4)
// vec4 #1
float output_gamma; // Gamma de salida (default 2.2)
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
float curvature_x; // Distorsión barrel X (default 0.05)
float curvature_y; // Distorsión barrel Y (default 0.10)
// vec4 #2
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
int enable_scanlines; // 0 = off, 1 = on
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
int enable_gamma; // 0 = off, 1 = on
// vec4 #3
int enable_curvature; // 0 = off, 1 = on
int enable_sharper; // 0 = off, 1 = on
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
float texture_height; // Alto del canvas en píxeles (inyectado en render)
};
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
struct DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
float pad0;
float pad1;
float pad2;
};
namespace Rendering {
/**
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
*
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
* → PostFX render pass → swapchain → present
*/
class SDL3GPUShader : public ShaderBackend {
public:
SDL3GPUShader() = default;
~SDL3GPUShader() override;
auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override {}
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
void uploadPixels(const Uint32* pixels, int width, int height) override;
// Actualiza los parámetros de intensidad de los efectos PostFX
void setPostFXParams(const PostFXParams& p) override;
// Activa/desactiva VSync en el swapchain
void setVSync(bool vsync) override;
// Activa/desactiva escalado entero (integer scale)
void setScaleMode(bool integer_scale) override;
// Establece factor de supersampling (1 = off, 3 = 3×SS)
void setOversample(int factor) override;
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
void setLinearUpscale(bool linear) override;
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
void setDownscaleAlgo(int algo) override;
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
void setActiveShader(ShaderType type) override;
// Actualiza los parámetros del shader CRT-Pi
void setCrtPiParams(const CrtPiParams& p) override;
// Devuelve el shader activo
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
private:
static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
static auto createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool;
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
int game_width_ = 0; // Dimensiones originales del canvas
int game_height_ = 0;
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false;
bool vsync_ = true;
bool integer_scale_ = false;
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
};
} // namespace Rendering

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: ''

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,633 @@
#pragma once
#include <cstddef>
#include <cstdint>
static const uint8_t kupscale_frag_spv[] = {
0x03,
0x02,
0x23,
0x07,
0x00,
0x00,
0x01,
0x00,
0x0b,
0x00,
0x0d,
0x00,
0x14,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x00,
0x02,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x06,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x4c,
0x53,
0x4c,
0x2e,
0x73,
0x74,
0x64,
0x2e,
0x34,
0x35,
0x30,
0x00,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x07,
0x00,
0x04,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x10,
0x00,
0x03,
0x00,
0x04,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x03,
0x00,
0x03,
0x00,
0x02,
0x00,
0x00,
0x00,
0xc2,
0x01,
0x00,
0x00,
0x04,
0x00,
0x0a,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x63,
0x70,
0x70,
0x5f,
0x73,
0x74,
0x79,
0x6c,
0x65,
0x5f,
0x6c,
0x69,
0x6e,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x00,
0x04,
0x00,
0x08,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x69,
0x6e,
0x63,
0x6c,
0x75,
0x64,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x05,
0x00,
0x04,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x05,
0x00,
0x05,
0x00,
0x09,
0x00,
0x00,
0x00,
0x6f,
0x75,
0x74,
0x5f,
0x63,
0x6f,
0x6c,
0x6f,
0x72,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x73,
0x63,
0x65,
0x6e,
0x65,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x76,
0x5f,
0x75,
0x76,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x13,
0x00,
0x02,
0x00,
0x02,
0x00,
0x00,
0x00,
0x21,
0x00,
0x03,
0x00,
0x03,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x16,
0x00,
0x03,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x07,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x19,
0x00,
0x09,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1b,
0x00,
0x03,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x36,
0x00,
0x05,
0x00,
0x02,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0xf8,
0x00,
0x02,
0x00,
0x05,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x57,
0x00,
0x05,
0x00,
0x07,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x3e,
0x00,
0x03,
0x00,
0x09,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0xfd,
0x00,
0x01,
0x00,
0x38,
0x00,
0x01,
0x00};
static const size_t kupscale_frag_spv_size = 628;

View File

@@ -0,0 +1,175 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <utility>
namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */
enum class ShaderType { POSTFX,
CRTPI };
/**
* @brief Parámetros de intensidad de los efectos PostFX
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
*/
struct PostFXParams {
float vignette = 0.0F; // Intensidad de la viñeta
float scanlines = 0.0F; // Intensidad de las scanlines
float chroma = 0.0F; // Aberración cromática
float mask = 0.0F; // Máscara de fósforo RGB
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
float curvature = 0.0F; // Curvatura barrel CRT
float bleeding = 0.0F; // Sangrado de color NTSC
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
};
/**
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
*/
struct CrtPiParams {
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
float input_gamma{2.4F}; // Gamma de entrada (linealización)
float output_gamma{2.2F}; // Gamma de salida (codificación)
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
float curvature_x{0.05F}; // Distorsión barrel eje X
float curvature_y{0.10F}; // Distorsión barrel eje Y
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
bool enable_scanlines{true}; // Activar efecto de scanlines
bool enable_multisample{true}; // Antialiasing analítico de scanlines
bool enable_gamma{true}; // Corrección gamma
bool enable_curvature{false}; // Distorsión barrel CRT
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
};
/**
* @brief Interfaz abstracta para backends de renderizado con shaders
*
* Esta interfaz define el contrato que todos los backends de shaders
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
*/
class ShaderBackend {
public:
virtual ~ShaderBackend() = default;
/**
* @brief Inicializa el backend de shaders
* @param window Ventana SDL
* @param texture Textura de backbuffer a la que aplicar shaders
* @param vertex_source Código fuente del vertex shader
* @param fragment_source Código fuente del fragment shader
* @return true si la inicialización fue exitosa
*/
virtual auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool = 0;
/**
* @brief Renderiza la textura con los shaders aplicados
*/
virtual void render() = 0;
/**
* @brief Establece el tamaño de la textura como parámetro del shader
* @param width Ancho de la textura
* @param height Alto de la textura
*/
virtual void setTextureSize(float width, float height) = 0;
/**
* @brief Limpia y libera recursos del backend
*/
virtual void cleanup() = 0;
/**
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
*/
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
/**
* @brief Establece los parámetros de intensidad de los efectos PostFX
* @param p Struct con todos los parámetros PostFX
*/
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
/**
* @brief Activa o desactiva VSync en el swapchain del GPU device
*/
virtual void setVSync(bool /*vsync*/) {}
/**
* @brief Activa o desactiva el escalado entero (integer scale)
*/
virtual void setScaleMode(bool /*integer_scale*/) {}
/**
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
* Con factor > 1, la textura GPU se crea a game×factor resolución y
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
*/
virtual void setOversample(int /*factor*/) {}
/**
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
*/
virtual void setLinearUpscale(bool /*linear*/) {}
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
/**
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
*/
virtual void setDownscaleAlgo(int /*algo*/) {}
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
/**
* @brief Devuelve las dimensiones de la textura de supersampling.
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
*/
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
/**
* @brief Verifica si el backend está usando aceleración por hardware
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
*/
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
/**
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
* @return Cadena vacía si no disponible
*/
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
/**
* @brief Establece el driver GPU preferido antes de init().
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
*/
virtual void setPreferredDriver(const std::string& /*driver*/) {}
/**
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
* Debe llamarse antes de render(). No recrea pipelines.
*/
virtual void setActiveShader(ShaderType /*type*/) {}
/**
* @brief Establece los parámetros del shader CRT-Pi.
*/
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
/**
* @brief Devuelve el shader de post-procesado activo.
*/
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
};
} // namespace Rendering

View File

@@ -1,6 +1,6 @@
#include "smartsprite.h" #include "core/rendering/smartsprite.h"
#include "movingsprite.h" // for MovingSprite #include "core/rendering/movingsprite.h" // for MovingSprite
class Texture; class Texture;
// Constructor // Constructor
@@ -46,16 +46,6 @@ void SmartSprite::render() {
} }
} }
// Obtiene el valor de la variable
bool SmartSprite::isEnabled() {
return enabled;
}
// Establece el valor de la variable
void SmartSprite::setEnabled(bool enabled) {
this->enabled = enabled;
}
// Obtiene el valor de la variable // Obtiene el valor de la variable
int SmartSprite::getEnabledCounter() { int SmartSprite::getEnabledCounter() {
return enabledCounter; return enabledCounter;

View File

@@ -2,14 +2,13 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "animatedsprite.h" // for AnimatedSprite #include "core/rendering/animatedsprite.h" // for AnimatedSprite
class Texture; class Texture;
// Clase SmartSprite // Clase SmartSprite
class SmartSprite : public AnimatedSprite { class SmartSprite : public AnimatedSprite {
private: private:
// Variables // Variables
bool enabled; // Indica si esta habilitado
bool onDestination; // Indica si está en el destino bool onDestination; // Indica si está en el destino
int destX; // Posicion de destino en el eje X int destX; // Posicion de destino en el eje X
int destY; // Posicion de destino en el eje Y int destY; // Posicion de destino en el eje Y
@@ -30,16 +29,10 @@ class SmartSprite : public AnimatedSprite {
void init(); void init();
// Actualiza la posición y comprueba si ha llegado a su destino // Actualiza la posición y comprueba si ha llegado a su destino
void update(); void update() override;
// Pinta el objeto en pantalla // Pinta el objeto en pantalla
void render(); void render() override;
// Obtiene el valor de la variable
bool isEnabled();
// Establece el valor de la variable
void setEnabled(bool enabled);
// Obtiene el valor de la variable // Obtiene el valor de la variable
int getEnabledCounter(); int getEnabledCounter();

View File

@@ -1,50 +1,28 @@
#include "sprite.h" #include "core/rendering/sprite.h"
#include "texture.h" // for Texture #include "core/rendering/texture.h" // for Texture
// Constructor // Constructor
Sprite::Sprite(int x, int y, int w, int h, Texture *texture, SDL_Renderer *renderer) { Sprite::Sprite(int x, int y, int w, int h, Texture *texture, SDL_Renderer *renderer)
// Establece la posición X,Y del sprite : x(x),
this->x = x; y(y),
this->y = y; w(w),
h(h),
// Establece el alto y el ancho del sprite renderer(renderer),
this->w = w; texture(texture),
this->h = h; spriteClip{0, 0, w, h},
enabled(true) {
// Establece el puntero al renderizador de la ventana
this->renderer = renderer;
// Establece la textura donde están los gráficos para el sprite
this->texture = texture;
// Establece el rectangulo de donde coger la imagen
spriteClip = {0, 0, w, h};
// Inicializa variables
enabled = true;
} }
Sprite::Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer) { Sprite::Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer)
// Establece la posición X,Y del sprite : x(rect.x),
x = rect.x; y(rect.y),
y = rect.y; w(rect.w),
h(rect.h),
// Establece el alto y el ancho del sprite renderer(renderer),
w = rect.w; texture(texture),
h = rect.h; spriteClip{0, 0, rect.w, rect.h},
enabled(true) {
// Establece el puntero al renderizador de la ventana
this->renderer = renderer;
// Establece la textura donde están los gráficos para el sprite
this->texture = texture;
// Establece el rectangulo de donde coger la imagen
spriteClip = {0, 0, w, h};
// Inicializa variables
enabled = true;
} }
// Destructor // Destructor

View File

@@ -19,14 +19,14 @@ class Sprite {
public: public:
// Constructor // Constructor
Sprite(int x = 0, int y = 0, int w = 0, int h = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); explicit Sprite(int x = 0, int y = 0, int w = 0, int h = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr);
Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer); Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer);
// Destructor // Destructor
~Sprite(); virtual ~Sprite();
// Muestra el sprite por pantalla // Muestra el sprite por pantalla
void render(); virtual void render();
// Obten el valor de la variable // Obten el valor de la variable
int getPosX(); int getPosX();
@@ -77,14 +77,14 @@ class Sprite {
void setRenderer(SDL_Renderer *renderer); void setRenderer(SDL_Renderer *renderer);
// Establece el valor de la variable // Establece el valor de la variable
void setEnabled(bool value); virtual void setEnabled(bool value);
// Comprueba si el objeto está habilitado // Comprueba si el objeto está habilitado
bool isEnabled(); virtual bool isEnabled();
// Devuelve el rectangulo donde está el sprite // Devuelve el rectangulo donde está el sprite
SDL_Rect getRect(); virtual SDL_Rect getRect();
// Establece los valores de posición y tamaño del sprite // Establece los valores de posición y tamaño del sprite
void setRect(SDL_Rect rect); virtual void setRect(SDL_Rect rect);
}; };

View File

@@ -1,79 +1,93 @@
#include "text.h" #include "core/rendering/text.h"
#include <fstream> // for char_traits, basic_ostream, basic_ifstream, ope... #include <fstream> // for char_traits, basic_ostream, basic_ifstream, ope...
#include <iostream> // for cout #include <iostream> // for cout
#include <sstream>
#include "sprite.h" // for Sprite #include "core/rendering/sprite.h" // for Sprite
#include "texture.h" // for Texture #include "core/rendering/texture.h" // for Texture
#include "utils.h" // for color_t #include "utils/utils.h" // for color_t
// Parser compartido: rellena un textFile_t desde cualquier istream
static void parseTextFileStream(std::istream &rfile, textFile_t &tf) {
std::string buffer;
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxWidth = std::stoi(buffer);
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxHeight = std::stoi(buffer);
int index = 32;
int line_read = 0;
while (std::getline(rfile, buffer)) {
if (line_read % 2 == 1) {
tf.offset[index++].w = std::stoi(buffer);
}
buffer.clear();
line_read++;
}
}
static void computeTextFileOffsets(textFile_t &tf) {
for (int i = 32; i < 128; ++i) {
tf.offset[i].x = ((i - 32) % 15) * tf.boxWidth;
tf.offset[i].y = ((i - 32) / 15) * tf.boxHeight;
}
}
// Llena una estructuta textFile_t desde un fichero // Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose) { textFile_t LoadTextFile(const std::string &file, bool verbose) {
textFile_t tf; textFile_t tf;
tf.boxWidth = 0;
// Inicializa a cero el vector con las coordenadas tf.boxHeight = 0;
for (int i = 0; i < 128; ++i) { for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0; tf.offset[i].x = 0;
tf.offset[i].y = 0; tf.offset[i].y = 0;
tf.offset[i].w = 0; tf.offset[i].w = 0;
} }
// Abre el fichero para leer los valores
const std::string filename = file.substr(file.find_last_of("\\/") + 1).c_str(); const std::string filename = file.substr(file.find_last_of("\\/") + 1).c_str();
std::ifstream rfile(file); std::ifstream rfile(file);
if (rfile.is_open() && rfile.good()) { if (rfile.is_open() && rfile.good()) {
std::string buffer; parseTextFileStream(rfile, tf);
// Lee los dos primeros valores del fichero
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxWidth = std::stoi(buffer);
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxHeight = std::stoi(buffer);
// lee el resto de datos del fichero
int index = 32;
int line_read = 0;
while (std::getline(rfile, buffer)) {
// Almacena solo las lineas impares
if (line_read % 2 == 1) {
tf.offset[index++].w = std::stoi(buffer);
}
// Limpia el buffer
buffer.clear();
line_read++;
};
// Cierra el fichero
if (verbose) { if (verbose) {
std::cout << "Text loaded: " << filename.c_str() << std::endl; std::cout << "Text loaded: " << filename.c_str() << std::endl;
} }
rfile.close(); } else if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
} }
// El fichero no se puede abrir computeTextFileOffsets(tf);
else { return tf;
}
// Llena una estructura textFile_t desde bytes en memoria
textFile_t LoadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose) {
textFile_t tf;
tf.boxWidth = 0;
tf.boxHeight = 0;
for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0;
tf.offset[i].y = 0;
tf.offset[i].w = 0;
}
if (!bytes.empty()) {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
parseTextFileStream(ss, tf);
if (verbose) { if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl; std::cout << "Text loaded from memory" << std::endl;
} }
} }
computeTextFileOffsets(tf);
// Establece las coordenadas para cada caracter ascii de la cadena y su ancho
for (int i = 32; i < 128; ++i) {
tf.offset[i].x = ((i - 32) % 15) * tf.boxWidth;
tf.offset[i].y = ((i - 32) / 15) * tf.boxHeight;
}
return tf; return tf;
} }
// Constructor // Constructor
Text::Text(std::string bitmapFile, std::string textFile, SDL_Renderer *renderer) { Text::Text(const std::string &bitmapFile, const std::string &textFile, SDL_Renderer *renderer) {
// Carga los offsets desde el fichero // Carga los offsets desde el fichero
textFile_t tf = LoadTextFile(textFile); textFile_t tf = LoadTextFile(textFile);
@@ -95,7 +109,7 @@ Text::Text(std::string bitmapFile, std::string textFile, SDL_Renderer *renderer)
} }
// Constructor // Constructor
Text::Text(std::string textFile, Texture *texture, SDL_Renderer *renderer) { Text::Text(const std::string &textFile, Texture *texture, SDL_Renderer *renderer) {
// Carga los offsets desde el fichero // Carga los offsets desde el fichero
textFile_t tf = LoadTextFile(textFile); textFile_t tf = LoadTextFile(textFile);
@@ -135,6 +149,24 @@ Text::Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer) {
fixedWidth = false; fixedWidth = false;
} }
// Constructor desde bytes
Text::Text(const std::vector<uint8_t> &pngBytes, const std::vector<uint8_t> &txtBytes, SDL_Renderer *renderer) {
textFile_t tf = LoadTextFileFromMemory(txtBytes);
boxHeight = tf.boxHeight;
boxWidth = tf.boxWidth;
for (int i = 0; i < 128; ++i) {
offset[i].x = tf.offset[i].x;
offset[i].y = tf.offset[i].y;
offset[i].w = tf.offset[i].w;
}
// Crea la textura desde bytes (Text es dueño en este overload)
texture = new Texture(renderer, pngBytes);
sprite = new Sprite({0, 0, boxWidth, boxHeight}, texture, renderer);
fixedWidth = false;
}
// Destructor // Destructor
Text::~Text() { Text::~Text() {
delete sprite; delete sprite;
@@ -144,7 +176,7 @@ Text::~Text() {
} }
// Escribe texto en pantalla // Escribe texto en pantalla
void Text::write(int x, int y, std::string text, int kerning, int lenght) { void Text::write(int x, int y, const std::string &text, int kerning, int lenght) {
int shift = 0; int shift = 0;
if (lenght == -1) { if (lenght == -1) {
@@ -164,14 +196,14 @@ void Text::write(int x, int y, std::string text, int kerning, int lenght) {
} }
// Escribe el texto con colores // Escribe el texto con colores
void Text::writeColored(int x, int y, std::string text, color_t color, int kerning, int lenght) { void Text::writeColored(int x, int y, const std::string &text, color_t color, int kerning, int lenght) {
sprite->getTexture()->setColor(color.r, color.g, color.b); sprite->getTexture()->setColor(color.r, color.g, color.b);
write(x, y, text, kerning, lenght); write(x, y, text, kerning, lenght);
sprite->getTexture()->setColor(255, 255, 255); sprite->getTexture()->setColor(255, 255, 255);
} }
// Escribe el texto con sombra // Escribe el texto con sombra
void Text::writeShadowed(int x, int y, std::string text, color_t color, Uint8 shadowDistance, int kerning, int lenght) { void Text::writeShadowed(int x, int y, const std::string &text, color_t color, Uint8 shadowDistance, int kerning, int lenght) {
sprite->getTexture()->setColor(color.r, color.g, color.b); sprite->getTexture()->setColor(color.r, color.g, color.b);
write(x + shadowDistance, y + shadowDistance, text, kerning, lenght); write(x + shadowDistance, y + shadowDistance, text, kerning, lenght);
sprite->getTexture()->setColor(255, 255, 255); sprite->getTexture()->setColor(255, 255, 255);
@@ -179,13 +211,13 @@ void Text::writeShadowed(int x, int y, std::string text, color_t color, Uint8 sh
} }
// Escribe el texto centrado en un punto x // Escribe el texto centrado en un punto x
void Text::writeCentered(int x, int y, std::string text, int kerning, int lenght) { void Text::writeCentered(int x, int y, const std::string &text, int kerning, int lenght) {
x -= (Text::lenght(text, kerning) / 2); x -= (Text::lenght(text, kerning) / 2);
write(x, y, text, kerning, lenght); write(x, y, text, kerning, lenght);
} }
// Escribe texto con extras // Escribe texto con extras
void Text::writeDX(Uint8 flags, int x, int y, std::string text, int kerning, color_t textColor, Uint8 shadowDistance, color_t shadowColor, int lenght) { void Text::writeDX(Uint8 flags, int x, int y, const std::string &text, int kerning, color_t textColor, Uint8 shadowDistance, color_t shadowColor, int lenght) {
const bool centered = ((flags & TXT_CENTER) == TXT_CENTER); const bool centered = ((flags & TXT_CENTER) == TXT_CENTER);
const bool shadowed = ((flags & TXT_SHADOW) == TXT_SHADOW); const bool shadowed = ((flags & TXT_SHADOW) == TXT_SHADOW);
const bool colored = ((flags & TXT_COLOR) == TXT_COLOR); const bool colored = ((flags & TXT_COLOR) == TXT_COLOR);
@@ -217,7 +249,7 @@ void Text::writeDX(Uint8 flags, int x, int y, std::string text, int kerning, col
} }
// Obtiene la longitud en pixels de una cadena // Obtiene la longitud en pixels de una cadena
int Text::lenght(std::string text, int kerning) { int Text::lenght(const std::string &text, int kerning) {
int shift = 0; int shift = 0;
for (int i = 0; i < (int)text.length(); ++i) for (int i = 0; i < (int)text.length(); ++i)

View File

@@ -2,10 +2,12 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string #include <string> // for string
#include <vector>
class Sprite; class Sprite;
class Texture; class Texture;
#include "utils.h" #include "utils/utils.h"
// Opciones de texto // Opciones de texto
constexpr int TXT_COLOR = 1; constexpr int TXT_COLOR = 1;
@@ -26,7 +28,10 @@ struct textFile_t {
}; };
// Llena una estructuta textFile_t desde un fichero // Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose = false); textFile_t LoadTextFile(const std::string &file, bool verbose = false);
// Llena una estructura textFile_t desde bytes en memoria
textFile_t LoadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose = false);
// Clase texto. Pinta texto en pantalla a partir de un bitmap // Clase texto. Pinta texto en pantalla a partir de un bitmap
class Text { class Text {
@@ -43,30 +48,37 @@ class Text {
public: public:
// Constructor // Constructor
Text(std::string bitmapFile, std::string textFile, SDL_Renderer *renderer); Text(const std::string &bitmapFile, const std::string &textFile, SDL_Renderer *renderer);
Text(std::string textFile, Texture *texture, SDL_Renderer *renderer); Text(const std::string &textFile, Texture *texture, SDL_Renderer *renderer);
Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer); Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer);
// Constructor desde bytes en memoria: comparte ownership del texture (no lo libera)
Text(const std::vector<uint8_t> &pngBytes, const std::vector<uint8_t> &txtBytes, SDL_Renderer *renderer);
// Destructor // Destructor
~Text(); ~Text();
// No copiable (gestiona memoria dinámica)
Text(const Text &) = delete;
Text &operator=(const Text &) = delete;
// Escribe el texto en pantalla // Escribe el texto en pantalla
void write(int x, int y, std::string text, int kerning = 1, int lenght = -1); void write(int x, int y, const std::string &text, int kerning = 1, int lenght = -1);
// Escribe el texto con colores // Escribe el texto con colores
void writeColored(int x, int y, std::string text, color_t color, int kerning = 1, int lenght = -1); void writeColored(int x, int y, const std::string &text, color_t color, int kerning = 1, int lenght = -1);
// Escribe el texto con sombra // Escribe el texto con sombra
void writeShadowed(int x, int y, std::string text, color_t color, Uint8 shadowDistance = 1, int kerning = 1, int lenght = -1); void writeShadowed(int x, int y, const std::string &text, color_t color, Uint8 shadowDistance = 1, int kerning = 1, int lenght = -1);
// Escribe el texto centrado en un punto x // Escribe el texto centrado en un punto x
void writeCentered(int x, int y, std::string text, int kerning = 1, int lenght = -1); void writeCentered(int x, int y, const std::string &text, int kerning = 1, int lenght = -1);
// Escribe texto con extras // Escribe texto con extras
void writeDX(Uint8 flags, int x, int y, std::string text, int kerning = 1, color_t textColor = color_t(255, 255, 255), Uint8 shadowDistance = 1, color_t shadowColor = color_t(0, 0, 0), int lenght = -1); void writeDX(Uint8 flags, int x, int y, const std::string &text, int kerning = 1, color_t textColor = color_t(255, 255, 255), Uint8 shadowDistance = 1, color_t shadowColor = color_t(0, 0, 0), int lenght = -1);
// Obtiene la longitud en pixels de una cadena // Obtiene la longitud en pixels de una cadena
int lenght(std::string text, int kerning = 1); int lenght(const std::string &text, int kerning = 1);
// Devuelve el valor de la variable // Devuelve el valor de la variable
int getCharacterSize(); int getCharacterSize();

View File

@@ -1,12 +1,12 @@
#include "texture.h" #include "core/rendering/texture.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <stdlib.h> // for exit #include <stdlib.h> // for exit
#include <iostream> // for basic_ostream, operator<<, cout, endl #include <iostream> // for basic_ostream, operator<<, cout, endl
#define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" // for stbi_failure_reason, stbi_image_free #include "external/stb_image.h" // for stbi_failure_reason, stbi_image_free
SDL_ScaleMode Texture::currentScaleMode = SDL_SCALEMODE_NEAREST; SDL_ScaleMode Texture::currentScaleMode = SDL_SCALEMODE_NEAREST;
@@ -15,91 +15,97 @@ void Texture::setGlobalScaleMode(SDL_ScaleMode mode) {
} }
// Constructor // Constructor
Texture::Texture(SDL_Renderer *renderer, std::string path, bool verbose) { Texture::Texture(SDL_Renderer *renderer, const std::string &path, bool verbose)
// Copia punteros : texture(nullptr),
this->renderer = renderer; renderer(renderer),
this->path = path; width(0),
height(0),
// Inicializa path(path) {
texture = nullptr;
width = 0;
height = 0;
// Carga el fichero en la textura // Carga el fichero en la textura
if (path != "") { if (!path.empty()) {
loadFromFile(path, renderer, verbose); loadFromFile(path, renderer, verbose);
} }
} }
// Constructor desde bytes
Texture::Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose)
: texture(nullptr),
renderer(renderer),
width(0),
height(0),
path("") {
if (!bytes.empty()) {
loadFromMemory(bytes.data(), bytes.size(), renderer, verbose);
}
}
// Destructor // Destructor
Texture::~Texture() { Texture::~Texture() {
// Libera memoria // Libera memoria
unload(); unload();
} }
// Helper: convierte píxeles RGBA decodificados por stbi en SDL_Texture
static SDL_Texture *createTextureFromPixels(SDL_Renderer *renderer, unsigned char *data, int w, int h, int *out_w, int *out_h) {
const int pitch = 4 * w;
SDL_Surface *loadedSurface = SDL_CreateSurfaceFrom(w, h, SDL_PIXELFORMAT_RGBA32, static_cast<void *>(data), pitch);
if (loadedSurface == nullptr) {
return nullptr;
}
SDL_Texture *newTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
if (newTexture != nullptr) {
*out_w = loadedSurface->w;
*out_h = loadedSurface->h;
SDL_SetTextureScaleMode(newTexture, Texture::currentScaleMode);
}
SDL_DestroySurface(loadedSurface);
return newTexture;
}
// Carga una imagen desde un fichero // Carga una imagen desde un fichero
bool Texture::loadFromFile(std::string path, SDL_Renderer *renderer, bool verbose) { bool Texture::loadFromFile(const std::string &path, SDL_Renderer *renderer, bool verbose) {
const std::string filename = path.substr(path.find_last_of("\\/") + 1); const std::string filename = path.substr(path.find_last_of("\\/") + 1);
int req_format = STBI_rgb_alpha; int req_format = STBI_rgb_alpha;
int width, height, orig_format; int w, h, orig_format;
unsigned char *data = stbi_load(path.c_str(), &width, &height, &orig_format, req_format); unsigned char *data = stbi_load(path.c_str(), &w, &h, &orig_format, req_format);
if (data == nullptr) { if (data == nullptr) {
SDL_Log("Loading image failed: %s", stbi_failure_reason()); SDL_Log("Loading image failed: %s", stbi_failure_reason());
exit(1); exit(1);
} else { } else if (verbose) {
if (verbose) { std::cout << "Image loaded: " << filename.c_str() << std::endl;
std::cout << "Image loaded: " << filename.c_str() << std::endl;
}
} }
int pitch;
SDL_PixelFormat pixel_format;
if (req_format == STBI_rgb) {
pitch = 3 * width; // 3 bytes por pixel * pixels per linea
pixel_format = SDL_PIXELFORMAT_RGB24;
} else { // STBI_rgb_alpha (RGBA)
pitch = 4 * width;
pixel_format = SDL_PIXELFORMAT_RGBA32;
}
// Limpia
unload(); unload();
SDL_Texture *newTexture = createTextureFromPixels(renderer, data, w, h, &this->width, &this->height);
// La textura final if (newTexture == nullptr && verbose) {
SDL_Texture *newTexture = nullptr; std::cout << "Unable to load image " << path.c_str() << std::endl;
// Carga la imagen desde una ruta específica
SDL_Surface *loadedSurface = SDL_CreateSurfaceFrom(width, height, pixel_format, (void *)data, pitch);
if (loadedSurface == nullptr) {
if (verbose) {
std::cout << "Unable to load image " << path.c_str() << std::endl;
}
} else {
// Crea la textura desde los pixels de la surface
newTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
if (newTexture == nullptr) {
if (verbose) {
std::cout << "Unable to create texture from " << path.c_str() << "! SDL Error: " << SDL_GetError() << std::endl;
}
} else {
// Obtiene las dimensiones de la imagen
this->width = loadedSurface->w;
this->height = loadedSurface->h;
// Aplica el modo de escalado
SDL_SetTextureScaleMode(newTexture, currentScaleMode);
}
// Elimina la textura cargada
SDL_DestroySurface(loadedSurface);
} }
// Return success
stbi_image_free(data); stbi_image_free(data);
texture = newTexture; texture = newTexture;
return texture != nullptr; return texture != nullptr;
} }
// Carga una imagen desde bytes en memoria
bool Texture::loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose) {
int w, h, orig_format;
unsigned char *pixels = stbi_load_from_memory(data, (int)size, &w, &h, &orig_format, STBI_rgb_alpha);
if (pixels == nullptr) {
SDL_Log("Loading image from memory failed: %s", stbi_failure_reason());
return false;
}
unload();
SDL_Texture *newTexture = createTextureFromPixels(renderer, pixels, w, h, &this->width, &this->height);
if (newTexture == nullptr && verbose) {
std::cout << "Unable to create texture from memory" << std::endl;
}
stbi_image_free(pixels);
texture = newTexture;
return texture != nullptr;
}
// Crea una textura en blanco // Crea una textura en blanco
bool Texture::createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess access) { bool Texture::createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess access) {
// Crea una textura sin inicializar // Crea una textura sin inicializar

View File

@@ -2,7 +2,9 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for basic_string, string #include <string> // for basic_string, string
#include <vector>
class Texture { class Texture {
private: private:
@@ -22,13 +24,19 @@ class Texture {
static void setGlobalScaleMode(SDL_ScaleMode mode); static void setGlobalScaleMode(SDL_ScaleMode mode);
// Constructor // Constructor
Texture(SDL_Renderer *renderer, std::string path = "", bool verbose = false); explicit Texture(SDL_Renderer *renderer, const std::string &path = "", bool verbose = false);
// Constructor desde bytes (PNG en memoria)
Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose = false);
// Destructor // Destructor
~Texture(); ~Texture();
// Carga una imagen desde un fichero // Carga una imagen desde un fichero
bool loadFromFile(std::string path, SDL_Renderer *renderer, bool verbose = false); bool loadFromFile(const std::string &path, SDL_Renderer *renderer, bool verbose = false);
// Carga una imagen desde bytes en memoria
bool loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose = false);
// Crea una textura en blanco // Crea una textura en blanco
bool createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess = SDL_TEXTUREACCESS_STREAMING); bool createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess = SDL_TEXTUREACCESS_STREAMING);

View File

@@ -1,25 +1,22 @@
#include "writer.h" #include "core/rendering/writer.h"
#include "text.h" // for Text #include "core/rendering/text.h" // for Text
// Constructor // Constructor
Writer::Writer(Text *text) { Writer::Writer(Text *text)
// Copia los punteros : text(text),
this->text = text; posX(0),
posY(0),
// Inicializa variables kerning(0),
posX = 0; caption(""),
posY = 0; speed(0),
kerning = 0; writingCounter(0),
caption = ""; index(0),
speed = 0; lenght(0),
writingCounter = 0; completed(false),
index = 0; enabled(false),
lenght = 0; enabledCounter(0),
completed = false; finished(false) {
enabled = false;
enabledCounter = 0;
finished = false;
} }
// Actualiza el objeto // Actualiza el objeto
@@ -73,7 +70,7 @@ void Writer::setKerning(int value) {
} }
// Establece el valor de la variable // Establece el valor de la variable
void Writer::setCaption(std::string text) { void Writer::setCaption(const std::string &text) {
caption = text; caption = text;
lenght = text.length(); lenght = text.length();
} }

View File

@@ -25,7 +25,7 @@ class Writer {
public: public:
// Constructor // Constructor
Writer(Text *text); explicit Writer(Text *text);
// Actualiza el objeto // Actualiza el objeto
void update(); void update();
@@ -43,7 +43,7 @@ class Writer {
void setKerning(int value); void setKerning(int value);
// Establece el valor de la variable // Establece el valor de la variable
void setCaption(std::string text); void setCaption(const std::string &text);
// Establece el valor de la variable // Establece el valor de la variable
void setSpeed(int value); void setSpeed(int value);

View File

@@ -1,19 +1,38 @@
#include "asset.h" #include "core/resources/asset.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <stddef.h> // for size_t #include <stddef.h> // for size_t
#include <iostream> // for basic_ostream, operator<<, cout, endl #include <iostream> // for basic_ostream, operator<<, cout, endl
#include "core/resources/resource_helper.h"
// Instancia única
Asset *Asset::instance = nullptr;
// Singleton API
void Asset::init(const std::string &executablePath) {
Asset::instance = new Asset(executablePath);
}
void Asset::destroy() {
delete Asset::instance;
Asset::instance = nullptr;
}
auto Asset::get() -> Asset * {
return Asset::instance;
}
// Constructor // Constructor
Asset::Asset(std::string executablePath) { Asset::Asset(const std::string &executablePath)
this->executablePath = executablePath.substr(0, executablePath.find_last_of("\\/")); : longestName(0),
longestName = 0; executablePath(executablePath.substr(0, executablePath.find_last_of("\\/"))),
verbose = true; verbose(true) {
} }
// Añade un elemento a la lista // Añade un elemento a la lista
void Asset::add(std::string file, enum assetType type, bool required, bool absolute) { void Asset::add(const std::string &file, enum assetType type, bool required, bool absolute) {
item_t temp; item_t temp;
temp.file = absolute ? file : executablePath + file; temp.file = absolute ? file : executablePath + file;
temp.type = type; temp.type = type;
@@ -25,8 +44,8 @@ void Asset::add(std::string file, enum assetType type, bool required, bool absol
} }
// Devuelve el fichero de un elemento de la lista a partir de una cadena // Devuelve el fichero de un elemento de la lista a partir de una cadena
std::string Asset::get(std::string text) { std::string Asset::get(const std::string &text) {
for (auto f : fileList) { for (const auto &f : fileList) {
const size_t lastIndex = f.file.find_last_of("/") + 1; const size_t lastIndex = f.file.find_last_of("/") + 1;
const std::string file = f.file.substr(lastIndex, std::string::npos); const std::string file = f.file.substr(lastIndex, std::string::npos);
@@ -57,7 +76,7 @@ bool Asset::check() {
// Comprueba si hay ficheros de ese tipo // Comprueba si hay ficheros de ese tipo
bool any = false; bool any = false;
for (auto f : fileList) { for (const auto &f : fileList) {
if ((f.required) && (f.type == type)) { if ((f.required) && (f.type == type)) {
any = true; any = true;
} }
@@ -69,7 +88,7 @@ bool Asset::check() {
std::cout << "\n>> " << getTypeName(type).c_str() << " FILES" << std::endl; std::cout << "\n>> " << getTypeName(type).c_str() << " FILES" << std::endl;
} }
for (auto f : fileList) { for (const auto &f : fileList) {
if ((f.required) && (f.type == type)) { if ((f.required) && (f.type == type)) {
success &= checkFile(f.file); success &= checkFile(f.file);
} }
@@ -92,18 +111,25 @@ bool Asset::check() {
} }
// Comprueba que existe un fichero // Comprueba que existe un fichero
bool Asset::checkFile(std::string path) { bool Asset::checkFile(const std::string &path) {
bool success = false; bool success = false;
std::string result = "ERROR"; std::string result = "ERROR";
// Comprueba si existe el fichero // Comprueba si existe el fichero (pack o filesystem)
const std::string filename = path.substr(path.find_last_of("\\/") + 1); const std::string filename = path.substr(path.find_last_of("\\/") + 1);
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb"); if (ResourceHelper::shouldUseResourcePack(path)) {
auto bytes = ResourceHelper::loadFile(path);
if (file != nullptr) { if (!bytes.empty()) {
result = "OK"; result = "OK";
success = true; success = true;
SDL_CloseIO(file); }
} else {
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
if (file != nullptr) {
result = "OK";
success = true;
SDL_CloseIO(file);
}
} }
if (verbose) { if (verbose) {

View File

@@ -18,15 +18,15 @@ enum assetType {
// Clase Asset // Clase Asset
class Asset { class Asset {
private: public:
// Estructura para definir un item // Estructura para definir un item
struct item_t { struct item_t {
std::string file; // Ruta del fichero desde la raiz del directorio std::string file; // Ruta del fichero desde la raiz del directorio
enum assetType type; // Indica el tipo de recurso enum assetType type; // Indica el tipo de recurso
bool required; // Indica si es un fichero que debe de existir bool required; // Indica si es un fichero que debe de existir
// bool absolute; // Indica si la ruta que se ha proporcionado es una ruta absoluta
}; };
private:
// Variables // Variables
int longestName; // Contiene la longitud del nombre de fichero mas largo int longestName; // Contiene la longitud del nombre de fichero mas largo
std::vector<item_t> fileList; // Listado con todas las rutas a los ficheros std::vector<item_t> fileList; // Listado con todas las rutas a los ficheros
@@ -34,20 +34,31 @@ class Asset {
bool verbose; // Indica si ha de mostrar información por pantalla bool verbose; // Indica si ha de mostrar información por pantalla
// Comprueba que existe un fichero // Comprueba que existe un fichero
bool checkFile(std::string executablePath); bool checkFile(const std::string &executablePath);
// Devuelve el nombre del tipo de recurso // Devuelve el nombre del tipo de recurso
std::string getTypeName(int type); std::string getTypeName(int type);
// Constructor privado (usar Asset::init)
explicit Asset(const std::string &path);
// Instancia única
static Asset *instance;
public: public:
// Constructor // Singleton API
Asset(std::string path); static void init(const std::string &executablePath); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Asset *; // Obtiene el puntero a la instancia
// Añade un elemento a la lista // Añade un elemento a la lista
void add(std::string file, enum assetType type, bool required = true, bool absolute = false); void add(const std::string &file, enum assetType type, bool required = true, bool absolute = false);
// Devuelve un elemento de la lista a partir de una cadena // Devuelve un elemento de la lista a partir de una cadena
std::string get(std::string text); std::string get(const std::string &text);
// Devuelve toda la lista de items registrados
const std::vector<item_t> &getAll() const { return fileList; }
// Comprueba que existen todos los elementos // Comprueba que existen todos los elementos
bool check(); bool check();

View File

@@ -0,0 +1,219 @@
#include "core/resources/resource.h"
#include <algorithm>
#include <filesystem>
#include <iostream>
#include <sstream>
#include "core/audio/jail_audio.hpp"
#include "core/rendering/text.h"
#include "core/rendering/texture.h"
#include "core/resources/asset.h"
#include "core/resources/resource_helper.h"
#include "game/ui/menu.h"
// Nota: Asset::get() e Input::get() se consultan en preloadAll y al construir
// los menús; no se guardan punteros en el objeto Resource.
Resource *Resource::instance_ = nullptr;
static std::string basename(const std::string &path) {
return path.substr(path.find_last_of("\\/") + 1);
}
static std::string stem(const std::string &path) {
std::string b = basename(path);
size_t dot = b.find_last_of('.');
if (dot == std::string::npos) return b;
return b.substr(0, dot);
}
void Resource::init(SDL_Renderer *renderer) {
if (instance_ == nullptr) {
instance_ = new Resource(renderer);
instance_->preloadAll();
}
}
void Resource::destroy() {
delete instance_;
instance_ = nullptr;
}
Resource *Resource::get() {
return instance_;
}
Resource::Resource(SDL_Renderer *renderer)
: renderer_(renderer) {}
Resource::~Resource() {
for (auto &[name, m] : menus_) delete m;
menus_.clear();
for (auto &[name, t] : texts_) delete t;
texts_.clear();
for (auto &[name, t] : textures_) delete t;
textures_.clear();
for (auto &[name, s] : sounds_) JA_DeleteSound(s);
sounds_.clear();
for (auto &[name, m] : musics_) JA_DeleteMusic(m);
musics_.clear();
}
void Resource::preloadAll() {
const auto &items = Asset::get()->getAll();
// Pass 1: texturas, sonidos, músicas, animaciones (raw lines), demo, lenguajes
for (const auto &it : items) {
if (!ResourceHelper::shouldUseResourcePack(it.file) && it.type != t_lang) {
// Ficheros absolutos (config.txt, score.bin, systemFolder) — no se precargan
continue;
}
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) continue;
const std::string bname = basename(it.file);
switch (it.type) {
case t_bitmap: {
auto *tex = new Texture(renderer_, bytes);
textures_[bname] = tex;
break;
}
case t_sound: {
JA_Sound_t *s = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
if (s) sounds_[bname] = s;
break;
}
case t_music: {
JA_Music_t *m = JA_LoadMusic(bytes.data(), (Uint32)bytes.size());
if (m) musics_[bname] = m;
break;
}
case t_data: {
if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".ani") {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
std::vector<std::string> lines;
std::string line;
while (std::getline(ss, line)) {
lines.push_back(line);
}
animationLines_[bname] = std::move(lines);
} else if (bname == "demo.bin") {
demoBytes_ = bytes;
} else if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".men") {
// Menús: se construyen en pass 2 porque dependen de textos y sonidos
}
break;
}
case t_font:
// Fonts: se emparejan en pass 2
break;
case t_lang:
// Lenguaje: lo sigue leyendo la clase Lang a través de ResourceHelper
break;
default:
break;
}
}
// Pass 2: Text (fuentes emparejadas png+txt) y Menus (dependen de Text+sonidos)
// Fuentes: construimos un Text por cada par basename.png + basename.txt
// Acumulamos los bytes encontrados por stem (basename sin ext.)
std::unordered_map<std::string, std::vector<uint8_t>> fontPngs;
std::unordered_map<std::string, std::vector<uint8_t>> fontTxts;
for (const auto &it : items) {
if (it.type != t_font) continue;
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) continue;
const std::string s = stem(it.file);
const std::string bname = basename(it.file);
if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".png") {
fontPngs[s] = std::move(bytes);
} else if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".txt") {
fontTxts[s] = std::move(bytes);
}
}
for (const auto &[s, png] : fontPngs) {
auto itTxt = fontTxts.find(s);
if (itTxt == fontTxts.end()) continue;
Text *t = new Text(png, itTxt->second, renderer_);
texts_[s] = t;
}
// Menus: usan aún Menu::loadFromBytes que internamente llama a asset->get() y
// Text/JA_LoadSound por path. Funciona en modo fallback; en pack estricto
// requiere que Menu se adapte a cargar desde ResourceHelper. Por ahora
// lo dejamos así y será una migración del paso 7.
for (const auto &it : items) {
if (it.type != t_data) continue;
const std::string bname = basename(it.file);
if (bname.size() < 4 || bname.substr(bname.size() - 4) != ".men") continue;
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) continue;
Menu *m = new Menu(renderer_, "");
m->loadFromBytes(bytes, bname);
const std::string s = stem(it.file);
menus_[s] = m;
}
}
Texture *Resource::getTexture(const std::string &name) {
auto it = textures_.find(name);
if (it == textures_.end()) {
std::cerr << "Resource::getTexture: missing " << name << '\n';
return nullptr;
}
return it->second;
}
JA_Sound_t *Resource::getSound(const std::string &name) {
auto it = sounds_.find(name);
if (it == sounds_.end()) {
std::cerr << "Resource::getSound: missing " << name << '\n';
return nullptr;
}
return it->second;
}
JA_Music_t *Resource::getMusic(const std::string &name) {
auto it = musics_.find(name);
if (it == musics_.end()) {
std::cerr << "Resource::getMusic: missing " << name << '\n';
return nullptr;
}
return it->second;
}
std::vector<std::string> &Resource::getAnimationLines(const std::string &name) {
auto it = animationLines_.find(name);
if (it == animationLines_.end()) {
static std::vector<std::string> empty;
std::cerr << "Resource::getAnimationLines: missing " << name << '\n';
return empty;
}
return it->second;
}
Text *Resource::getText(const std::string &name) {
auto it = texts_.find(name);
if (it == texts_.end()) {
std::cerr << "Resource::getText: missing " << name << '\n';
return nullptr;
}
return it->second;
}
Menu *Resource::getMenu(const std::string &name) {
auto it = menus_.find(name);
if (it == menus_.end()) {
std::cerr << "Resource::getMenu: missing " << name << '\n';
return nullptr;
}
return it->second;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
class Menu;
class Text;
class Texture;
struct JA_Music_t;
struct JA_Sound_t;
// Precarga y posee todos los recursos del juego durante toda la vida de la app.
// Singleton inicializado desde Director; las escenas consultan handles via get*().
class Resource {
public:
static void init(SDL_Renderer *renderer);
static void destroy();
static Resource *get();
Texture *getTexture(const std::string &name);
JA_Sound_t *getSound(const std::string &name);
JA_Music_t *getMusic(const std::string &name);
std::vector<std::string> &getAnimationLines(const std::string &name);
Text *getText(const std::string &name); // name sin extensión: "smb2", "nokia2", ...
Menu *getMenu(const std::string &name); // name sin extensión: "title", "options", ...
const std::vector<uint8_t> &getDemoBytes() const { return demoBytes_; }
private:
explicit Resource(SDL_Renderer *renderer);
~Resource();
void preloadAll();
SDL_Renderer *renderer_;
std::unordered_map<std::string, Texture *> textures_;
std::unordered_map<std::string, JA_Sound_t *> sounds_;
std::unordered_map<std::string, JA_Music_t *> musics_;
std::unordered_map<std::string, std::vector<std::string>> animationLines_;
std::unordered_map<std::string, Text *> texts_;
std::unordered_map<std::string, Menu *> menus_;
std::vector<uint8_t> demoBytes_;
static Resource *instance_;
};

View File

@@ -0,0 +1,77 @@
#include "core/resources/resource_helper.h"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <iostream>
#include "core/resources/resource_loader.h"
namespace ResourceHelper {
static bool resource_system_initialized = false;
bool initializeResourceSystem(const std::string& pack_file, bool enable_fallback) {
auto& loader = ResourceLoader::getInstance();
bool ok = loader.initialize(pack_file, enable_fallback);
resource_system_initialized = ok;
if (ok && loader.getLoadedResourceCount() > 0) {
std::cout << "Resource system initialized with pack: " << pack_file << '\n';
} else if (ok) {
std::cout << "Resource system using fallback mode (filesystem only)" << '\n';
}
return ok;
}
void shutdownResourceSystem() {
if (resource_system_initialized) {
ResourceLoader::getInstance().shutdown();
resource_system_initialized = false;
}
}
std::vector<uint8_t> loadFile(const std::string& filepath) {
if (resource_system_initialized && shouldUseResourcePack(filepath)) {
auto& loader = ResourceLoader::getInstance();
std::string pack_path = getPackPath(filepath);
auto data = loader.loadResource(pack_path);
if (!data.empty()) {
return data;
}
}
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
return {};
}
return data;
}
bool shouldUseResourcePack(const std::string& filepath) {
// Solo entran al pack los ficheros dentro de data/
return filepath.find("data/") != std::string::npos;
}
std::string getPackPath(const std::string& asset_path) {
std::string pack_path = asset_path;
std::replace(pack_path.begin(), pack_path.end(), '\\', '/');
// Toma la última aparición de "data/" como prefijo a quitar
size_t last_data = pack_path.rfind("data/");
if (last_data != std::string::npos) {
pack_path = pack_path.substr(last_data + 5);
}
return pack_path;
}
} // namespace ResourceHelper

View File

@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace ResourceHelper {
bool initializeResourceSystem(const std::string& pack_file = "resources.pack", bool enable_fallback = true);
void shutdownResourceSystem();
std::vector<uint8_t> loadFile(const std::string& filepath);
bool shouldUseResourcePack(const std::string& filepath);
std::string getPackPath(const std::string& asset_path);
} // namespace ResourceHelper

View File

@@ -0,0 +1,133 @@
#include "core/resources/resource_loader.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include "core/resources/resource_pack.h"
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
ResourceLoader::ResourceLoader()
: resource_pack_(nullptr),
fallback_to_files_(true) {}
ResourceLoader& ResourceLoader::getInstance() {
if (!instance) {
instance = std::unique_ptr<ResourceLoader>(new ResourceLoader());
}
return *instance;
}
ResourceLoader::~ResourceLoader() {
shutdown();
}
bool ResourceLoader::initialize(const std::string& pack_file, bool enable_fallback) {
shutdown();
fallback_to_files_ = enable_fallback;
pack_path_ = pack_file;
if (std::filesystem::exists(pack_file)) {
resource_pack_ = new ResourcePack();
if (resource_pack_->loadPack(pack_file)) {
return true;
}
delete resource_pack_;
resource_pack_ = nullptr;
std::cerr << "Failed to load resource pack: " << pack_file << '\n';
}
if (fallback_to_files_) {
return true;
}
std::cerr << "Resource pack not found and fallback disabled: " << pack_file << '\n';
return false;
}
void ResourceLoader::shutdown() {
if (resource_pack_ != nullptr) {
delete resource_pack_;
resource_pack_ = nullptr;
}
}
std::vector<uint8_t> ResourceLoader::loadResource(const std::string& filename) {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return resource_pack_->getResource(filename);
}
if (fallback_to_files_) {
return loadFromFile(filename);
}
std::cerr << "Resource not found: " << filename << '\n';
return {};
}
bool ResourceLoader::resourceExists(const std::string& filename) {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return true;
}
if (fallback_to_files_) {
std::string full_path = getDataPath(filename);
return std::filesystem::exists(full_path);
}
return false;
}
std::vector<uint8_t> ResourceLoader::loadFromFile(const std::string& filename) {
std::string full_path = getDataPath(filename);
std::ifstream file(full_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << full_path << '\n';
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << full_path << '\n';
return {};
}
return data;
}
std::string ResourceLoader::getDataPath(const std::string& filename) {
return "data/" + filename;
}
size_t ResourceLoader::getLoadedResourceCount() const {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceCount();
}
return 0;
}
std::vector<std::string> ResourceLoader::getAvailableResources() const {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceList();
}
std::vector<std::string> result;
if (fallback_to_files_ && std::filesystem::exists("data")) {
for (const auto& entry : std::filesystem::recursive_directory_iterator("data")) {
if (entry.is_regular_file()) {
std::string filename = std::filesystem::relative(entry.path(), "data").string();
std::replace(filename.begin(), filename.end(), '\\', '/');
result.push_back(filename);
}
}
}
return result;
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class ResourcePack;
class ResourceLoader {
private:
static std::unique_ptr<ResourceLoader> instance;
ResourcePack* resource_pack_;
std::string pack_path_;
bool fallback_to_files_;
ResourceLoader();
public:
static ResourceLoader& getInstance();
~ResourceLoader();
bool initialize(const std::string& pack_file, bool enable_fallback = true);
void shutdown();
std::vector<uint8_t> loadResource(const std::string& filename);
bool resourceExists(const std::string& filename);
void setFallbackToFiles(bool enable) { fallback_to_files_ = enable; }
bool getFallbackToFiles() const { return fallback_to_files_; }
size_t getLoadedResourceCount() const;
std::vector<std::string> getAvailableResources() const;
private:
static std::vector<uint8_t> loadFromFile(const std::string& filename);
static std::string getDataPath(const std::string& filename);
};

View File

@@ -0,0 +1,221 @@
#include "core/resources/resource_pack.h"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <numeric>
#include <utility>
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "CCRS_RESOURCES__2026";
ResourcePack::ResourcePack()
: loaded_(false) {}
ResourcePack::~ResourcePack() {
clear();
}
uint32_t ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) {
return std::accumulate(data.begin(), data.end(), uint32_t(0x12345678), [](uint32_t acc, uint8_t b) { return ((acc << 5) + acc) + b; });
}
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) return;
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
encryptData(data, key);
}
bool ResourcePack::loadPack(const std::string& pack_file) {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open pack file: " << pack_file << '\n';
return false;
}
std::array<char, 4> header;
file.read(header.data(), 4);
if (std::string(header.data(), 4) != "CCRS") {
std::cerr << "Error: Invalid pack file format" << '\n';
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != 1) {
std::cerr << "Error: Unsupported pack version: " << version << '\n';
return false;
}
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (uint32_t i = 0; i < resource_count; ++i) {
uint32_t filename_length;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
decryptData(data_, DEFAULT_ENCRYPT_KEY);
loaded_ = true;
return true;
}
bool ResourcePack::savePack(const std::string& pack_file) {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not create pack file: " << pack_file << '\n';
return false;
}
file.write("CCRS", 4);
uint32_t version = 1;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
uint32_t resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
uint32_t filename_length = static_cast<uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
uint64_t data_size = encrypted_data.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
return true;
}
bool ResourcePack::addFile(const std::string& filename, const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << filepath << '\n';
return false;
}
ResourceEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
bool ResourcePack::addDirectory(const std::string& directory) {
if (!std::filesystem::exists(directory)) {
std::cerr << "Error: Directory does not exist: " << directory << '\n';
return false;
}
// cppcheck-suppress useStlAlgorithm
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (entry.is_regular_file()) {
std::string filepath = entry.path().string();
std::string filename = std::filesystem::relative(entry.path(), directory).string();
std::replace(filename.begin(), filename.end(), '\\', '/');
if (!addFile(filename, filepath)) {
return false;
}
}
}
return true;
}
std::vector<uint8_t> ResourcePack::getResource(const std::string& filename) {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "Error: Resource not found: " << filename << '\n';
return {};
}
const ResourceEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "Error: Invalid resource data: " << filename << '\n';
return {};
}
std::vector<uint8_t> result(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "Warning: Checksum mismatch for resource: " << filename << '\n';
}
return result;
}
bool ResourcePack::hasResource(const std::string& filename) const {
return resources_.find(filename) != resources_.end();
}
void ResourcePack::clear() {
resources_.clear();
data_.clear();
loaded_ = false;
}
size_t ResourcePack::getResourceCount() const {
return resources_.size();
}
std::vector<std::string> ResourcePack::getResourceList() const {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
struct ResourceEntry {
std::string filename;
uint64_t offset;
uint64_t size;
uint32_t checksum;
};
class ResourcePack {
private:
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
bool loaded_;
static uint32_t calculateChecksum(const std::vector<uint8_t>& data);
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
public:
ResourcePack();
~ResourcePack();
bool loadPack(const std::string& pack_file);
bool savePack(const std::string& pack_file);
bool addFile(const std::string& filename, const std::string& filepath);
bool addDirectory(const std::string& directory);
std::vector<uint8_t> getResource(const std::string& filename);
bool hasResource(const std::string& filename) const;
void clear();
size_t getResourceCount() const;
std::vector<std::string> getResourceList() const;
static const std::string DEFAULT_ENCRYPT_KEY;
};

View File

@@ -0,0 +1,654 @@
#include "core/system/director.h"
#include <SDL3/SDL.h>
#include <errno.h> // for errno, EEXIST, EACCES, ENAMETOO...
#include <stdio.h> // for printf, perror
#include <string.h> // for strcmp
#ifndef __EMSCRIPTEN__
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
#include <unistd.h> // for getuid
#endif
#include <cstdlib> // for exit, EXIT_FAILURE, srand
#include <filesystem>
#include <fstream> // for basic_ostream, operator<<, basi...
#include <iostream> // for cout
#include <memory>
#include <string> // for basic_string, operator+, char_t...
#include <vector> // for vector
#include "core/audio/audio.hpp" // for Audio::init, Audio::destroy
#include "core/input/input.h" // for Input, inputs_e, INPUT_USE_GAME...
#include "core/input/mouse.hpp" // for Mouse::handleEvent, Mouse::upda...
#include "core/locale/lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
#include "core/rendering/screen.h" // for Screen
#include "core/rendering/texture.h" // for Texture
#include "core/resources/asset.h" // for Asset, assetType
#include "core/resources/resource.h"
#include "core/resources/resource_helper.h"
#include "game/defaults.hpp" // for SECTION_PROG_LOGO, GAMECANVAS_H...
#include "game/game.h" // for Game
#include "game/options.hpp" // for Options::init, loadFromFile...
#include "game/scenes/intro.h" // for Intro
#include "game/scenes/logo.h" // for Logo
#include "game/scenes/title.h" // for Title
#include "utils/utils.h" // for input_t, boolToString
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
#include <pwd.h>
#endif
// Constructor
Director::Director(int argc, const char *argv[]) {
std::cout << "Game start" << std::endl;
// Inicializa variables
section = new section_t();
section->name = SECTION_PROG_LOGO;
// Inicializa las opciones del programa (defaults + dispositivos d'entrada)
Options::init();
// Comprueba los parametros del programa (pot activar console)
checkProgramArguments(argc, argv);
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
#ifndef DEBUG
createSystemFolder("jailgames/coffee_crisis");
#else
createSystemFolder("jailgames/coffee_crisis_debug");
#endif
// Estableix el fitxer de configuració i carrega les opcions (o crea el
// YAML amb defaults si no existeix).
Options::setConfigFile(systemFolder + "/config.yaml");
Options::loadFromFile();
// Presets de shaders (creats amb defaults si no existeixen).
Options::setPostFXFile(systemFolder + "/postfx.yaml");
Options::loadPostFXFromFile();
Options::setCrtPiFile(systemFolder + "/crtpi.yaml");
Options::loadCrtPiFromFile();
// Inicializa el sistema de recursos (pack + fallback).
// En wasm siempre se usa filesystem (MEMFS) porque el propio --preload-file
// de emscripten ya empaqueta data/ — no hay resources.pack.
{
#if defined(__EMSCRIPTEN__)
const bool enable_fallback = true;
#elif defined(RELEASE_BUILD)
const bool enable_fallback = false;
#else
const bool enable_fallback = true;
#endif
if (!ResourceHelper::initializeResourceSystem("resources.pack", enable_fallback)) {
std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << std::endl;
exit(EXIT_FAILURE);
}
}
// Crea el objeto que controla los ficheros de recursos
Asset::init(executablePath);
Asset::get()->setVerbose(Options::settings.console);
// Si falta algún fichero no inicia el programa
if (!setFileList()) {
exit(EXIT_FAILURE);
}
// Inicializa SDL
initSDL();
// Inicializa JailAudio
initJailAudio();
// Establece el modo de escalado de texturas
Texture::setGlobalScaleMode(Options::video.scale_mode);
// Crea los objetos
Lang::init();
Lang::get()->setLang(Options::settings.language);
#ifdef __EMSCRIPTEN__
Input::init("/gamecontrollerdb.txt");
#else
{
const std::string binDir = std::filesystem::path(executablePath).parent_path().string();
#ifdef MACOS_BUNDLE
Input::init(binDir + "/../Resources/gamecontrollerdb.txt");
#else
Input::init(binDir + "/gamecontrollerdb.txt");
#endif
}
#endif
initInput();
// Orden importante: Screen + initShaders ANTES de Resource::init.
// Si `Resource::init` se ejecuta primero, carga ~100 texturas vía
// `SDL_CreateTexture` que dejan el SDL_Renderer con el swapchain en un
// estado que hace crashear al driver Vulkan cuando después `initShaders`
// intenta reclamar la ventana para el dispositivo SDL3 GPU.
//
// Por eso el constructor de Screen NO carga notificationText desde
// Resource; se enlaza después vía `Screen::get()->initNotifications()`.
Screen::init(window, renderer);
#ifndef NO_SHADERS
if (Options::video.gpu.acceleration) {
Screen::get()->initShaders();
}
#endif
// Ahora sí, precarga todos los recursos en memoria (texturas, sonidos,
// música, ...). Vivirán durante toda la vida de la app.
Resource::init(renderer);
// Completa el enlazado de Screen con recursos que necesitan Resource
// inicializado (actualmente sólo el Text de las notificaciones).
Screen::get()->initNotifications();
activeSection = ActiveSection::None;
}
Director::~Director() {
Options::saveToFile();
// Libera las secciones primero: sus destructores tocan audio/render SDL
// (p.ej. Intro::~Intro llama a JA_DeleteMusic) y deben ejecutarse antes
// de SDL_Quit().
logo.reset();
intro.reset();
title.reset();
game.reset();
// Screen puede tener referencias a Text propiedad de Resource: destruir
// Screen antes que Resource.
Screen::destroy();
// Libera todos los recursos precargados antes de cerrar SDL.
Resource::destroy();
Asset::destroy();
Input::destroy();
Lang::destroy();
delete section;
Audio::destroy();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
ResourceHelper::shutdownResourceSystem();
std::cout << "\nBye!" << std::endl;
}
// Inicializa el objeto input
void Director::initInput() {
// Establece si ha de mostrar mensajes
Input::get()->setVerbose(Options::settings.console);
// Busca si hay un mando conectado
Input::get()->discoverGameController();
// Teclado - Movimiento del jugador
Input::get()->bindKey(input_up, SDL_SCANCODE_UP);
Input::get()->bindKey(input_down, SDL_SCANCODE_DOWN);
Input::get()->bindKey(input_left, SDL_SCANCODE_LEFT);
Input::get()->bindKey(input_right, SDL_SCANCODE_RIGHT);
Input::get()->bindKey(input_fire_left, SDL_SCANCODE_Q);
Input::get()->bindKey(input_fire_center, SDL_SCANCODE_W);
Input::get()->bindKey(input_fire_right, SDL_SCANCODE_E);
// Teclado - Otros
Input::get()->bindKey(input_accept, SDL_SCANCODE_RETURN);
Input::get()->bindKey(input_cancel, SDL_SCANCODE_ESCAPE);
Input::get()->bindKey(input_pause, SDL_SCANCODE_ESCAPE);
Input::get()->bindKey(input_exit, SDL_SCANCODE_ESCAPE);
Input::get()->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
Input::get()->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
Input::get()->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
Input::get()->bindKey(input_prev_preset, SDL_SCANCODE_F8);
Input::get()->bindKey(input_next_preset, SDL_SCANCODE_F9);
Input::get()->bindKey(input_toggle_shader, SDL_SCANCODE_F10);
Input::get()->bindKey(input_toggle_shader_type, SDL_SCANCODE_F11);
// Mando - Movimiento del jugador
Input::get()->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
Input::get()->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
Input::get()->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
Input::get()->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
Input::get()->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST);
Input::get()->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH);
Input::get()->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST);
// Mando - Otros
// SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK.
Input::get()->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
#ifdef GAME_CONSOLE
Input::get()->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
Input::get()->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
#else
Input::get()->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START);
Input::get()->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK);
#endif
}
// Inicializa JailAudio
void Director::initJailAudio() {
Audio::init();
}
// Arranca SDL y crea la ventana
bool Director::initSDL() {
// Indicador de éxito
bool success = true;
// Inicializa SDL
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
if (Options::settings.console) {
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Inicia el generador de numeros aleatorios
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
// Crea la ventana
window = SDL_CreateWindow(
Options::window.caption.c_str(),
GAMECANVAS_WIDTH * Options::window.zoom,
GAMECANVAS_HEIGHT * Options::window.zoom,
0);
if (window == nullptr) {
if (Options::settings.console) {
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Crea un renderizador para la ventana
renderer = SDL_CreateRenderer(window, NULL);
if (renderer == nullptr) {
if (Options::settings.console) {
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Modo de blending por defecto (consistente con CCAE):
// permite alpha blending para fades y notificaciones.
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Activa vsync si es necesario
if (Options::video.vsync) {
SDL_SetRenderVSync(renderer, 1);
}
// Inicializa el color de renderizado
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
// Establece el tamaño del buffer de renderizado
SDL_SetRenderLogicalPresentation(renderer, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
// Establece el modo de mezcla
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}
}
}
if (Options::settings.console) {
std::cout << std::endl;
}
return success;
}
// Crea el indice de ficheros
bool Director::setFileList() {
#ifdef MACOS_BUNDLE
const std::string prefix = "/../Resources";
#else
const std::string prefix = "";
#endif
// Ficheros de configuración
Asset::get()->add(systemFolder + "/score.bin", t_data, false, true);
Asset::get()->add(prefix + "/data/demo/demo.bin", t_data);
// Musicas
Asset::get()->add(prefix + "/data/music/intro.ogg", t_music);
Asset::get()->add(prefix + "/data/music/playing.ogg", t_music);
Asset::get()->add(prefix + "/data/music/title.ogg", t_music);
// Sonidos
Asset::get()->add(prefix + "/data/sound/balloon.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/bubble1.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/bubble2.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/bubble3.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/bubble4.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/bullet.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/coffeeout.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/hiscore.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/itemdrop.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/itempickup.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/menu_move.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/menu_select.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/player_collision.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/stage_change.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/title.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/clock.wav", t_sound);
Asset::get()->add(prefix + "/data/sound/powerball.wav", t_sound);
// Texturas
Asset::get()->add(prefix + "/data/gfx/balloon1.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/balloon1.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/balloon2.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/balloon2.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/balloon3.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/balloon3.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/balloon4.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/balloon4.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/bullet.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/game_buildings.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/game_clouds.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/game_grass.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/game_text.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/intro.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/logo.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_points1_disk.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/item_clock.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_clock.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/item_coffee.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_coffee.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/title_coffee.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/title_crisis.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/title_dust.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/title_dust.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/title_gradient.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_head.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/player_body.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/player_legs.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/player_death.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/player_fire.ani", t_data);
Asset::get()->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap);
Asset::get()->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap);
// Fuentes
Asset::get()->add(prefix + "/data/font/8bithud.png", t_font);
Asset::get()->add(prefix + "/data/font/8bithud.txt", t_font);
Asset::get()->add(prefix + "/data/font/nokia.png", t_font);
Asset::get()->add(prefix + "/data/font/nokia_big2.png", t_font);
Asset::get()->add(prefix + "/data/font/nokia.txt", t_font);
Asset::get()->add(prefix + "/data/font/nokia2.png", t_font);
Asset::get()->add(prefix + "/data/font/nokia2.txt", t_font);
Asset::get()->add(prefix + "/data/font/nokia_big2.txt", t_font);
Asset::get()->add(prefix + "/data/font/smb2_big.png", t_font);
Asset::get()->add(prefix + "/data/font/smb2_big.txt", t_font);
Asset::get()->add(prefix + "/data/font/smb2.png", t_font);
Asset::get()->add(prefix + "/data/font/smb2.txt", t_font);
// Textos
Asset::get()->add(prefix + "/data/lang/es_ES.txt", t_lang);
Asset::get()->add(prefix + "/data/lang/en_UK.txt", t_lang);
Asset::get()->add(prefix + "/data/lang/ba_BA.txt", t_lang);
// Menus
Asset::get()->add(prefix + "/data/menu/title.men", t_data);
Asset::get()->add(prefix + "/data/menu/title_gc.men", t_data);
Asset::get()->add(prefix + "/data/menu/options.men", t_data);
Asset::get()->add(prefix + "/data/menu/options_gc.men", t_data);
Asset::get()->add(prefix + "/data/menu/pause.men", t_data);
Asset::get()->add(prefix + "/data/menu/gameover.men", t_data);
Asset::get()->add(prefix + "/data/menu/player_select.men", t_data);
return Asset::get()->check();
}
// Comprueba los parametros del programa
void Director::checkProgramArguments(int argc, const char *argv[]) {
// Establece la ruta del programa
executablePath = argv[0];
// Comprueba el resto de parametros
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--console") == 0) {
Options::settings.console = true;
}
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string &folder) {
#ifdef __EMSCRIPTEN__
// En Emscripten usamos una carpeta en MEMFS (no persistente)
systemFolder = "/config/" + folder;
#elif _WIN32
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
#elif __linux__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/.config/" + folder;
{
// Intenta crear ".config", per si no existeix
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR CREATING CONFIG BASE FOLDER.");
exit(EXIT_FAILURE);
}
}
#endif
#ifdef __EMSCRIPTEN__
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
(void)folder;
#else
struct stat st = {0};
if (stat(systemFolder.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(systemFolder.c_str());
#else
int ret = mkdir(systemFolder.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("the parent directory does not allow write");
exit(EXIT_FAILURE);
case EEXIST:
printf("pathname already exists");
exit(EXIT_FAILURE);
case ENAMETOOLONG:
printf("pathname is too long");
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
#endif
}
// Gestiona las transiciones entre secciones
void Director::handleSectionTransition() {
// Determina qué sección debería estar activa
ActiveSection targetSection = ActiveSection::None;
switch (section->name) {
case SECTION_PROG_LOGO:
targetSection = ActiveSection::Logo;
break;
case SECTION_PROG_INTRO:
targetSection = ActiveSection::Intro;
break;
case SECTION_PROG_TITLE:
targetSection = ActiveSection::Title;
break;
case SECTION_PROG_GAME:
targetSection = ActiveSection::Game;
break;
}
// Si no ha cambiado, no hay nada que hacer
if (targetSection == activeSection) return;
// Destruye la sección anterior
logo.reset();
intro.reset();
title.reset();
game.reset();
// Crea la nueva sección
activeSection = targetSection;
switch (activeSection) {
case ActiveSection::Logo:
logo = std::make_unique<Logo>(renderer, section);
break;
case ActiveSection::Intro:
intro = std::make_unique<Intro>(renderer, section);
break;
case ActiveSection::Title:
title = std::make_unique<Title>(renderer, section);
break;
case ActiveSection::Game: {
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
game = std::make_unique<Game>(numPlayers, 0, renderer, false, section);
break;
}
case ActiveSection::None:
break;
}
}
// Ejecuta un frame del juego
SDL_AppResult Director::iterate() {
#ifdef __EMSCRIPTEN__
// En WASM no se puede salir: reinicia al logo
if (section->name == SECTION_PROG_QUIT) {
section->name = SECTION_PROG_LOGO;
}
#else
if (section->name == SECTION_PROG_QUIT) {
return SDL_APP_SUCCESS;
}
#endif
// Actualiza la visibilidad del cursor del ratón
Mouse::updateCursorVisibility(Options::video.fullscreen);
// Gestiona las transiciones entre secciones
handleSectionTransition();
// Ejecuta un frame de la sección activa
switch (activeSection) {
case ActiveSection::Logo:
logo->iterate();
break;
case ActiveSection::Intro:
intro->iterate();
break;
case ActiveSection::Title:
title->iterate();
break;
case ActiveSection::Game:
game->iterate();
break;
case ActiveSection::None:
break;
}
return SDL_APP_CONTINUE;
}
// Procesa un evento
SDL_AppResult Director::handleEvent(SDL_Event *event) {
#ifndef __EMSCRIPTEN__
// Evento de salida de la aplicación
if (event->type == SDL_EVENT_QUIT) {
section->name = SECTION_PROG_QUIT;
return SDL_APP_SUCCESS;
}
#endif
// Hot-plug de mandos
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
std::string name;
if (Input::get()->handleGamepadAdded(event->gdevice.which, name)) {
Screen::get()->notify(name + " " + Lang::get()->getText(94),
color_t{0x40, 0xFF, 0x40},
color_t{0, 0, 0},
2500);
}
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
std::string name;
if (Input::get()->handleGamepadRemoved(event->gdevice.which, name)) {
Screen::get()->notify(name + " " + Lang::get()->getText(95),
color_t{0xFF, 0x50, 0x50},
color_t{0, 0, 0},
2500);
}
}
// Gestiona la visibilidad del cursor según el movimiento del ratón
Mouse::handleEvent(*event, Options::video.fullscreen);
// Reenvía el evento a la sección activa
switch (activeSection) {
case ActiveSection::Logo:
logo->handleEvent(event);
break;
case ActiveSection::Intro:
intro->handleEvent(event);
break;
case ActiveSection::Title:
title->handleEvent(event);
break;
case ActiveSection::Game:
game->handleEvent(event);
break;
case ActiveSection::None:
break;
}
return SDL_APP_CONTINUE;
}

View File

@@ -0,0 +1,74 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include <string> // for string, basic_string
class Game;
class Intro;
class Logo;
class Title;
struct section_t;
// Secciones activas del Director
enum class ActiveSection { None,
Logo,
Intro,
Title,
Game };
class Director {
private:
// Objetos y punteros
SDL_Window *window; // La ventana donde dibujamos
SDL_Renderer *renderer; // El renderizador de la ventana
section_t *section; // Sección y subsección actual del programa;
// Secciones del juego
ActiveSection activeSection;
std::unique_ptr<Logo> logo;
std::unique_ptr<Intro> intro;
std::unique_ptr<Title> title;
std::unique_ptr<Game> game;
// Variables
std::string executablePath; // Path del ejecutable
std::string systemFolder; // Carpeta del sistema donde guardar datos
// Inicializa jail_audio
void initJailAudio();
// Arranca SDL y crea la ventana
bool initSDL();
// Inicializa el objeto input
void initInput();
// Crea el indice de ficheros
bool setFileList();
// Comprueba los parametros del programa
void checkProgramArguments(int argc, const char *argv[]);
// Crea la carpeta del sistema donde guardar datos
void createSystemFolder(const std::string &folder);
// Gestiona las transiciones entre secciones
void handleSectionTransition();
public:
// Constructor
Director(int argc, const char *argv[]);
// Destructor
~Director();
Director(const Director &) = delete;
Director &operator=(const Director &) = delete;
// Ejecuta un frame del juego
SDL_AppResult iterate();
// Procesa un evento
SDL_AppResult handleEvent(SDL_Event *event);
};

View File

@@ -1,696 +0,0 @@
#include "director.h"
#include <SDL3/SDL.h>
#include <errno.h> // for errno, EEXIST, EACCES, ENAMETOO...
#include <stdio.h> // for printf, perror
#include <string.h> // for strcmp
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
#include <unistd.h> // for getuid
#include <cstdlib> // for exit, EXIT_FAILURE, srand
#include <fstream> // for basic_ostream, operator<<, basi...
#include <iostream> // for cout
#include <memory>
#include <string> // for basic_string, operator+, char_t...
#include <vector> // for vector
#include "asset.h" // for Asset, assetType
#include "const.h" // for SECTION_PROG_LOGO, GAMECANVAS_H...
#include "game.h" // for Game
#include "input.h" // for Input, inputs_e, INPUT_USE_GAME...
#include "intro.h" // for Intro
#include "jail_audio.hpp" // for JA_Init
#include "lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
#include "logo.h" // for Logo
#include "screen.h" // for FILTER_NEAREST, Screen, FILTER_...
#include "texture.h" // for Texture
#include "title.h" // for Title
#include "utils.h" // for options_t, input_t, boolToString
#ifndef _WIN32
#include <pwd.h>
#endif
// Constructor
Director::Director(int argc, const char *argv[]) {
std::cout << "Game start" << std::endl;
// Inicializa variables
section = new section_t();
section->name = SECTION_PROG_LOGO;
// Inicializa las opciones del programa
initOptions();
// Comprueba los parametros del programa
checkProgramArguments(argc, argv);
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
#ifndef DEBUG
createSystemFolder("jailgames/coffee_crisis");
#else
createSystemFolder("jailgames/coffee_crisis_debug");
#endif
// Crea el objeto que controla los ficheros de recursos
asset = new Asset(executablePath);
asset->setVerbose(options->console);
// Si falta algún fichero no inicia el programa
if (!setFileList()) {
exit(EXIT_FAILURE);
}
// Carga el fichero de configuración
loadConfigFile();
// Inicializa SDL
initSDL();
// Inicializa JailAudio
initJailAudio();
// Establece el modo de escalado de texturas
Texture::setGlobalScaleMode(options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
// Crea los objetos
lang = new Lang(asset);
lang->setLang(options->language);
input = new Input(asset->get("gamecontrollerdb.txt"));
initInput();
screen = new Screen(window, renderer, asset, options);
}
Director::~Director() {
saveConfigFile();
delete asset;
delete input;
delete screen;
delete lang;
delete options;
delete section;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "\nBye!" << std::endl;
}
// Inicializa el objeto input
void Director::initInput() {
// Establece si ha de mostrar mensajes
input->setVerbose(options->console);
// Busca si hay un mando conectado
input->discoverGameController();
// Teclado - Movimiento del jugador
input->bindKey(input_up, SDL_SCANCODE_UP);
input->bindKey(input_down, SDL_SCANCODE_DOWN);
input->bindKey(input_left, SDL_SCANCODE_LEFT);
input->bindKey(input_right, SDL_SCANCODE_RIGHT);
input->bindKey(input_fire_left, SDL_SCANCODE_Q);
input->bindKey(input_fire_center, SDL_SCANCODE_W);
input->bindKey(input_fire_right, SDL_SCANCODE_E);
// Teclado - Otros
input->bindKey(input_accept, SDL_SCANCODE_RETURN);
input->bindKey(input_cancel, SDL_SCANCODE_ESCAPE);
input->bindKey(input_pause, SDL_SCANCODE_ESCAPE);
input->bindKey(input_exit, SDL_SCANCODE_ESCAPE);
input->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
input->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
// Mando - Movimiento del jugador
input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
input->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
input->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
input->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
input->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST);
input->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH);
input->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST);
// Mando - Otros
input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
input->bindGameControllerButton(input_cancel, SDL_GAMEPAD_BUTTON_SOUTH);
#ifdef GAME_CONSOLE
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
#else
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START);
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK);
#endif
}
// Inicializa JailAudio
void Director::initJailAudio() {
JA_Init(48000, SDL_AUDIO_S16, 2);
}
// Arranca SDL y crea la ventana
bool Director::initSDL() {
// Indicador de éxito
bool success = true;
// Inicializa SDL
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
if (options->console) {
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Inicia el generador de numeros aleatorios
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
// Crea la ventana
int incW = 0;
int incH = 0;
if (options->borderEnabled) {
incW = options->borderWidth * 2;
incH = options->borderHeight * 2;
}
window = SDL_CreateWindow(WINDOW_CAPTION, (options->gameWidth + incW) * options->windowSize, (options->gameHeight + incH) * options->windowSize, 0);
if (window == nullptr) {
if (options->console) {
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Crea un renderizador para la ventana
renderer = SDL_CreateRenderer(window, NULL);
if (renderer == nullptr) {
if (options->console) {
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Activa vsync si es necesario
if (options->vSync) {
SDL_SetRenderVSync(renderer, 1);
}
// Inicializa el color de renderizado
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
// Establece el tamaño del buffer de renderizado
SDL_SetRenderLogicalPresentation(renderer, options->gameWidth, options->gameHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
// Establece el modo de mezcla
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}
}
}
if (options->console) {
std::cout << std::endl;
}
return success;
}
// Crea el indice de ficheros
bool Director::setFileList() {
#ifdef MACOS_BUNDLE
const std::string prefix = "/../Resources";
#else
const std::string prefix = "";
#endif
// Ficheros de configuración
asset->add(systemFolder + "/config.txt", t_data, false, true);
asset->add(systemFolder + "/score.bin", t_data, false, true);
asset->add(prefix + "/data/config/demo.bin", t_data);
asset->add(prefix + "/data/config/gamecontrollerdb.txt", t_data);
// Musicas
asset->add(prefix + "/data/music/intro.ogg", t_music);
asset->add(prefix + "/data/music/playing.ogg", t_music);
asset->add(prefix + "/data/music/title.ogg", t_music);
// Sonidos
asset->add(prefix + "/data/sound/balloon.wav", t_sound);
asset->add(prefix + "/data/sound/bubble1.wav", t_sound);
asset->add(prefix + "/data/sound/bubble2.wav", t_sound);
asset->add(prefix + "/data/sound/bubble3.wav", t_sound);
asset->add(prefix + "/data/sound/bubble4.wav", t_sound);
asset->add(prefix + "/data/sound/bullet.wav", t_sound);
asset->add(prefix + "/data/sound/coffeeout.wav", t_sound);
asset->add(prefix + "/data/sound/hiscore.wav", t_sound);
asset->add(prefix + "/data/sound/itemdrop.wav", t_sound);
asset->add(prefix + "/data/sound/itempickup.wav", t_sound);
asset->add(prefix + "/data/sound/menu_move.wav", t_sound);
asset->add(prefix + "/data/sound/menu_select.wav", t_sound);
asset->add(prefix + "/data/sound/player_collision.wav", t_sound);
asset->add(prefix + "/data/sound/stage_change.wav", t_sound);
asset->add(prefix + "/data/sound/title.wav", t_sound);
asset->add(prefix + "/data/sound/clock.wav", t_sound);
asset->add(prefix + "/data/sound/powerball.wav", t_sound);
// Texturas
asset->add(prefix + "/data/gfx/balloon1.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon1.ani", t_data);
asset->add(prefix + "/data/gfx/balloon2.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon2.ani", t_data);
asset->add(prefix + "/data/gfx/balloon3.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon3.ani", t_data);
asset->add(prefix + "/data/gfx/balloon4.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon4.ani", t_data);
asset->add(prefix + "/data/gfx/bullet.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_buildings.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_clouds.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_grass.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_text.png", t_bitmap);
asset->add(prefix + "/data/gfx/intro.png", t_bitmap);
asset->add(prefix + "/data/gfx/logo.png", t_bitmap);
asset->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap);
asset->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points1_disk.ani", t_data);
asset->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data);
asset->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data);
asset->add(prefix + "/data/gfx/item_clock.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_clock.ani", t_data);
asset->add(prefix + "/data/gfx/item_coffee.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_coffee.ani", t_data);
asset->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data);
asset->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_coffee.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_crisis.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_dust.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_dust.ani", t_data);
asset->add(prefix + "/data/gfx/title_gradient.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_head.ani", t_data);
asset->add(prefix + "/data/gfx/player_body.ani", t_data);
asset->add(prefix + "/data/gfx/player_legs.ani", t_data);
asset->add(prefix + "/data/gfx/player_death.ani", t_data);
asset->add(prefix + "/data/gfx/player_fire.ani", t_data);
asset->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap);
// Fuentes
asset->add(prefix + "/data/font/8bithud.png", t_font);
asset->add(prefix + "/data/font/8bithud.txt", t_font);
asset->add(prefix + "/data/font/nokia.png", t_font);
asset->add(prefix + "/data/font/nokia_big2.png", t_font);
asset->add(prefix + "/data/font/nokia.txt", t_font);
asset->add(prefix + "/data/font/nokia2.png", t_font);
asset->add(prefix + "/data/font/nokia2.txt", t_font);
asset->add(prefix + "/data/font/nokia_big2.txt", t_font);
asset->add(prefix + "/data/font/smb2_big.png", t_font);
asset->add(prefix + "/data/font/smb2_big.txt", t_font);
asset->add(prefix + "/data/font/smb2.png", t_font);
asset->add(prefix + "/data/font/smb2.txt", t_font);
// Textos
asset->add(prefix + "/data/lang/es_ES.txt", t_lang);
asset->add(prefix + "/data/lang/en_UK.txt", t_lang);
asset->add(prefix + "/data/lang/ba_BA.txt", t_lang);
// Menus
asset->add(prefix + "/data/menu/title.men", t_data);
asset->add(prefix + "/data/menu/title_gc.men", t_data);
asset->add(prefix + "/data/menu/options.men", t_data);
asset->add(prefix + "/data/menu/options_gc.men", t_data);
asset->add(prefix + "/data/menu/pause.men", t_data);
asset->add(prefix + "/data/menu/gameover.men", t_data);
asset->add(prefix + "/data/menu/player_select.men", t_data);
return asset->check();
}
// Inicializa las opciones del programa
void Director::initOptions() {
// Crea el puntero a la estructura de opciones
options = new options_t;
// Pone unos valores por defecto para las opciones de control
options->input.clear();
input_t inp;
inp.id = 0;
inp.name = "KEYBOARD";
inp.deviceType = INPUT_USE_KEYBOARD;
options->input.push_back(inp);
inp.id = 0;
inp.name = "GAME CONTROLLER";
inp.deviceType = INPUT_USE_GAMECONTROLLER;
options->input.push_back(inp);
// Opciones de video
options->gameWidth = GAMECANVAS_WIDTH;
options->gameHeight = GAMECANVAS_HEIGHT;
options->videoMode = 0;
options->windowSize = 3;
options->filter = FILTER_NEAREST;
options->vSync = true;
options->integerScale = true;
options->keepAspect = true;
options->borderWidth = 0;
options->borderHeight = 0;
options->borderEnabled = false;
// Opciones varios
options->playerSelected = 0;
options->difficulty = DIFFICULTY_NORMAL;
options->language = ba_BA;
options->console = false;
}
// Comprueba los parametros del programa
void Director::checkProgramArguments(int argc, const char *argv[]) {
// Establece la ruta del programa
executablePath = argv[0];
// Comprueba el resto de parametros
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--console") == 0) {
options->console = true;
}
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string &folder) {
#ifdef _WIN32
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
#elif __linux__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/.config/" + folder;
{
// Intenta crear ".config", per si no existeix
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR CREATING CONFIG BASE FOLDER.");
exit(EXIT_FAILURE);
}
}
#endif
struct stat st = {0};
if (stat(systemFolder.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(systemFolder.c_str());
#else
int ret = mkdir(systemFolder.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("the parent directory does not allow write");
exit(EXIT_FAILURE);
case EEXIST:
printf("pathname already exists");
exit(EXIT_FAILURE);
case ENAMETOOLONG:
printf("pathname is too long");
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
}
// Carga el fichero de configuración
bool Director::loadConfigFile() {
// Indicador de éxito en la carga
bool success = true;
// Variables para manejar el fichero
const std::string filePath = "config.txt";
std::string line;
std::ifstream file(asset->get(filePath));
// Si el fichero se puede abrir
if (file.good()) {
// Procesa el fichero linea a linea
if (options->console) {
std::cout << "Reading file " << filePath << std::endl;
}
while (std::getline(file, line)) {
// Comprueba que la linea no sea un comentario
if (line.substr(0, 1) != "#") {
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (!setOptions(options, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
if (options->console) {
std::cout << "Warning: file " << filePath << std::endl;
std::cout << "Unknown parameter " << line.substr(0, pos).c_str() << std::endl;
}
success = false;
}
}
}
// Cierra el fichero
if (options->console) {
std::cout << "Closing file " << filePath << std::endl;
}
file.close();
}
// El fichero no existe
else { // Crea el fichero con los valores por defecto
saveConfigFile();
}
// Normaliza los valores
if (options->videoMode != 0 && options->videoMode != SDL_WINDOW_FULLSCREEN) {
options->videoMode = 0;
}
if (options->windowSize < 1 || options->windowSize > 4) {
options->windowSize = 3;
}
if (options->language < 0 || options->language > MAX_LANGUAGES) {
options->language = en_UK;
}
return success;
}
// Guarda el fichero de configuración
bool Director::saveConfigFile() {
bool success = true;
// Crea y abre el fichero de texto
std::ofstream file(asset->get("config.txt"));
if (file.good()) {
if (options->console) {
std::cout << asset->get("config.txt") << " open for writing" << std::endl;
}
} else {
if (options->console) {
std::cout << asset->get("config.txt") << " can't be opened" << std::endl;
}
}
// Opciones g´raficas
file << "## VISUAL OPTIONS\n";
if (options->videoMode == 0) {
file << "videoMode=0\n";
}
else if (options->videoMode == SDL_WINDOW_FULLSCREEN) {
file << "videoMode=SDL_WINDOW_FULLSCREEN\n";
}
file << "windowSize=" + std::to_string(options->windowSize) + "\n";
if (options->filter == FILTER_NEAREST) {
file << "filter=FILTER_NEAREST\n";
} else {
file << "filter=FILTER_LINEAL\n";
}
file << "vSync=" + boolToString(options->vSync) + "\n";
file << "integerScale=" + boolToString(options->integerScale) + "\n";
file << "keepAspect=" + boolToString(options->keepAspect) + "\n";
file << "borderEnabled=" + boolToString(options->borderEnabled) + "\n";
file << "borderWidth=" + std::to_string(options->borderWidth) + "\n";
file << "borderHeight=" + std::to_string(options->borderHeight) + "\n";
// Otras opciones del programa
file << "\n## OTHER OPTIONS\n";
file << "language=" + std::to_string(options->language) + "\n";
file << "difficulty=" + std::to_string(options->difficulty) + "\n";
file << "input0=" + std::to_string(options->input[0].deviceType) + "\n";
file << "input1=" + std::to_string(options->input[1].deviceType) + "\n";
// Cierra el fichero
file.close();
return success;
}
void Director::runLogo() {
auto logo = std::make_unique<Logo>(renderer, screen, asset, input, section);
logo->run();
}
void Director::runIntro() {
auto intro = std::make_unique<Intro>(renderer, screen, asset, input, lang, section);
intro->run();
}
void Director::runTitle() {
auto title = std::make_unique<Title>(renderer, screen, input, asset, options, lang, section);
title->run();
}
void Director::runGame() {
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
auto game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, options, section);
game->run();
}
int Director::run() {
// Bucle principal
while (section->name != SECTION_PROG_QUIT) {
switch (section->name) {
case SECTION_PROG_LOGO:
runLogo();
break;
case SECTION_PROG_INTRO:
runIntro();
break;
case SECTION_PROG_TITLE:
runTitle();
break;
case SECTION_PROG_GAME:
runGame();
break;
}
}
return 0;
}
// Asigna variables a partir de dos cadenas
bool Director::setOptions(options_t *options, std::string var, std::string value) {
// Indicador de éxito en la asignación
bool success = true;
// Opciones de video
if (var == "videoMode") {
if (value == "SDL_WINDOW_FULLSCREEN" || value == "SDL_WINDOW_FULLSCREEN_DESKTOP") {
options->videoMode = SDL_WINDOW_FULLSCREEN;
} else {
options->videoMode = 0;
}
}
else if (var == "windowSize") {
options->windowSize = std::stoi(value);
if ((options->windowSize < 1) || (options->windowSize > 4)) {
options->windowSize = 3;
}
}
else if (var == "filter") {
if (value == "FILTER_LINEAL") {
options->filter = FILTER_LINEAL;
} else {
options->filter = FILTER_NEAREST;
}
}
else if (var == "vSync") {
options->vSync = stringToBool(value);
}
else if (var == "integerScale") {
options->integerScale = stringToBool(value);
}
else if (var == "keepAspect") {
options->keepAspect = stringToBool(value);
}
else if (var == "borderEnabled") {
options->borderEnabled = stringToBool(value);
}
else if (var == "borderWidth") {
options->borderWidth = std::stoi(value);
}
else if (var == "borderHeight") {
options->borderHeight = std::stoi(value);
}
// Opciones varias
else if (var == "language") {
options->language = std::stoi(value);
}
else if (var == "difficulty") {
options->difficulty = std::stoi(value);
}
else if (var == "input0") {
options->input[0].deviceType = std::stoi(value);
}
else if (var == "input1") {
options->input[1].deviceType = std::stoi(value);
}
// Lineas vacias o que empiezan por comentario
else if (var == "" || var.substr(0, 1) == "#") {
}
else {
success = false;
}
return success;
}

View File

@@ -1,87 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string, basic_string
class Asset;
class Game;
class Input;
class Intro;
class Lang;
class Logo;
class Screen;
class Title;
struct options_t;
struct section_t;
// Textos
constexpr const char *WINDOW_CAPTION = "© 2020 Coffee Crisis — JailDesigner";
class Director {
private:
// Objetos y punteros
SDL_Window *window; // La ventana donde dibujamos
SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla
Input *input; // Objeto Input para gestionar las entradas
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
section_t *section; // Sección y subsección actual del programa;
// Variables
struct options_t *options; // Variable con todas las opciones del programa
std::string executablePath; // Path del ejecutable
std::string systemFolder; // Carpeta del sistema donde guardar datos
// Inicializa jail_audio
void initJailAudio();
// Arranca SDL y crea la ventana
bool initSDL();
// Inicializa el objeto input
void initInput();
// Inicializa las opciones del programa
void initOptions();
// Asigna variables a partir de dos cadenas
bool setOptions(options_t *options, std::string var, std::string value);
// Crea el indice de ficheros
bool setFileList();
// Carga el fichero de configuración
bool loadConfigFile();
// Guarda el fichero de configuración
bool saveConfigFile();
// Comprueba los parametros del programa
void checkProgramArguments(int argc, const char *argv[]);
// Crea la carpeta del sistema donde guardar datos
void createSystemFolder(const std::string &folder);
// Ejecuta la seccion de juego con el logo
void runLogo();
// Ejecuta la seccion de juego de la introducción
void runIntro();
// Ejecuta la seccion de juego con el titulo y los menus
void runTitle();
// Ejecuta la seccion de juego donde se juega
void runGame();
public:
// Constructor
Director(int argc, const char *argv[]);
// Destructor
~Director();
// Bucle principal
int run();
};

2
source/external/.clang-format vendored Normal file
View File

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

4
source/external/.clang-tidy vendored Normal file
View File

@@ -0,0 +1,4 @@
# source/external/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''

14726
source/external/fkyaml_node.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Ogg Vorbis audio decoder - v1.20 - public domain // Ogg Vorbis audio decoder - v1.22 - public domain
// http://nothings.org/stb_vorbis/ // http://nothings.org/stb_vorbis/
// //
// Original version written by Sean Barrett in 2007. // Original version written by Sean Barrett in 2007.
@@ -29,12 +29,15 @@
// Bernhard Wodo Evan Balster github:alxprd // Bernhard Wodo Evan Balster github:alxprd
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot // Tom Beaumont Ingo Leitgeb Nicolas Guillemot
// Phillip Bennefall Rohit Thiago Goulart // Phillip Bennefall Rohit Thiago Goulart
// github:manxorist saga musix github:infatum // github:manxorist Saga Musix github:infatum
// Timur Gagiev Maxwell Koo Peter Waller // Timur Gagiev Maxwell Koo Peter Waller
// github:audinowho Dougall Johnson David Reid // github:audinowho Dougall Johnson David Reid
// github:Clownacy Pedro J. Estebanez Remi Verschelde // github:Clownacy Pedro J. Estebanez Remi Verschelde
// AnthoFoxo github:morlat Gabriel Ravier
// //
// Partial history: // Partial history:
// 1.22 - 2021-07-11 - various small fixes
// 1.21 - 2021-07-02 - fix bug for files with no comments
// 1.20 - 2020-07-11 - several small fixes // 1.20 - 2020-07-11 - several small fixes
// 1.19 - 2020-02-05 - warnings // 1.19 - 2020-02-05 - warnings
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc. // 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
// channel. In other words, (*output)[0][0] contains the first sample from // channel. In other words, (*output)[0][0] contains the first sample from
// the first channel, and (*output)[1][0] contains the first sample from // the first channel, and (*output)[1][0] contains the first sample from
// the second channel. // the second channel.
//
// *output points into stb_vorbis's internal output buffer storage; these
// buffers are owned by stb_vorbis and application code should not free
// them or modify their contents. They are transient and will be overwritten
// once you ask for more data to get decoded, so be sure to grab any data
// you need before then.
extern void stb_vorbis_flush_pushdata(stb_vorbis *f); extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
// inform stb_vorbis that your next datablock will not be contiguous with // inform stb_vorbis that your next datablock will not be contiguous with
@@ -579,7 +588,7 @@ enum STBVorbisError
#if defined(_MSC_VER) || defined(__MINGW32__) #if defined(_MSC_VER) || defined(__MINGW32__)
#include <malloc.h> #include <malloc.h>
#endif #endif
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__) #if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#include <alloca.h> #include <alloca.h>
#endif #endif
#else // STB_VORBIS_NO_CRT #else // STB_VORBIS_NO_CRT
@@ -646,6 +655,12 @@ typedef signed int int32;
typedef float codetype; typedef float codetype;
#ifdef _MSC_VER
#define STBV_NOTUSED(v) (void)(v)
#else
#define STBV_NOTUSED(v) (void)sizeof(v)
#endif
// @NOTE // @NOTE
// //
// Some arrays below are tagged "//varies", which means it's actually // Some arrays below are tagged "//varies", which means it's actually
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
uint32 sign = x & 0x80000000; uint32 sign = x & 0x80000000;
uint32 exp = (x & 0x7fe00000) >> 21; uint32 exp = (x & 0x7fe00000) >> 21;
double res = sign ? -(double)mantissa : (double)mantissa; double res = sign ? -(double)mantissa : (double)mantissa;
return (float) ldexp((float)res, exp-788); return (float) ldexp((float)res, (int)exp-788);
} }
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
// find the first entry // find the first entry
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break; for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
if (k == n) { assert(c->sorted_entries == 0); return TRUE; } if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
assert(len[k] < 32); // no error return required, code reading lens checks this
// add to the list // add to the list
add_entry(c, 0, k, m++, len[k], values); add_entry(c, 0, k, m++, len[k], values);
// add all available leaves // add all available leaves
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
uint32 res; uint32 res;
int z = len[i], y; int z = len[i], y;
if (z == NO_CODE) continue; if (z == NO_CODE) continue;
assert(z < 32); // no error return required, code reading lens checks this
// find lowest available leaf (should always be earliest, // find lowest available leaf (should always be earliest,
// which is what the specification calls for) // which is what the specification calls for)
// note that this property, and the fact we can never have // note that this property, and the fact we can never have
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
while (z > 0 && !available[z]) --z; while (z > 0 && !available[z]) --z;
if (z == 0) { return FALSE; } if (z == 0) { return FALSE; }
res = available[z]; res = available[z];
assert(z >= 0 && z < 32);
available[z] = 0; available[z] = 0;
add_entry(c, bit_reverse(res), i, m++, len[i], values); add_entry(c, bit_reverse(res), i, m++, len[i], values);
// propagate availability up the tree // propagate availability up the tree
if (z != len[i]) { if (z != len[i]) {
assert(len[i] >= 0 && len[i] < 32);
for (y=len[i]; y > z; --y) { for (y=len[i]; y > z; --y) {
assert(available[y] == 0); assert(available[y] == 0);
available[y] = res + (1 << (32-y)); available[y] = res + (1 << (32-y));
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
while (z > base) { while (z > base) {
float k00,k11; float k00,k11;
float l00,l11;
k00 = z[-0] - z[-8]; k00 = z[-0] - z[ -8];
k11 = z[-1] - z[-9]; k11 = z[-1] - z[ -9];
z[-0] = z[-0] + z[-8]; l00 = z[-2] - z[-10];
z[-1] = z[-1] + z[-9]; l11 = z[-3] - z[-11];
z[-8] = k00; z[ -0] = z[-0] + z[ -8];
z[-9] = k11 ; z[ -1] = z[-1] + z[ -9];
z[ -2] = z[-2] + z[-10];
z[ -3] = z[-3] + z[-11];
z[ -8] = k00;
z[ -9] = k11;
z[-10] = (l00+l11) * A2;
z[-11] = (l11-l00) * A2;
k00 = z[ -2] - z[-10]; k00 = z[ -4] - z[-12];
k11 = z[ -3] - z[-11];
z[ -2] = z[ -2] + z[-10];
z[ -3] = z[ -3] + z[-11];
z[-10] = (k00+k11) * A2;
z[-11] = (k11-k00) * A2;
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
k11 = z[ -5] - z[-13]; k11 = z[ -5] - z[-13];
l00 = z[ -6] - z[-14];
l11 = z[ -7] - z[-15];
z[ -4] = z[ -4] + z[-12]; z[ -4] = z[ -4] + z[-12];
z[ -5] = z[ -5] + z[-13]; z[ -5] = z[ -5] + z[-13];
z[-12] = k11;
z[-13] = k00;
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
k11 = z[ -7] - z[-15];
z[ -6] = z[ -6] + z[-14]; z[ -6] = z[ -6] + z[-14];
z[ -7] = z[ -7] + z[-15]; z[ -7] = z[ -7] + z[-15];
z[-14] = (k00+k11) * A2; z[-12] = k11;
z[-15] = (k00-k11) * A2; z[-13] = -k00;
z[-14] = (l11-l00) * A2;
z[-15] = (l00+l11) * -A2;
iter_54(z); iter_54(z);
iter_54(z-8); iter_54(z-8);
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
for (q=1; q < g->values; ++q) { for (q=1; q < g->values; ++q) {
j = g->sorted_order[q]; j = g->sorted_order[q];
#ifndef STB_VORBIS_NO_DEFER_FLOOR #ifndef STB_VORBIS_NO_DEFER_FLOOR
STBV_NOTUSED(step2_flag);
if (finalY[j] >= 0) if (finalY[j] >= 0)
#else #else
if (step2_flag[j]) if (step2_flag[j])
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// WINDOWING // WINDOWING
STBV_NOTUSED(left_end);
n = f->blocksize[m->blockflag]; n = f->blocksize[m->blockflag];
map = &f->mapping[m->mapping]; map = &f->mapping[m->mapping];
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// this isn't to spec, but spec would require us to read ahead // this isn't to spec, but spec would require us to read ahead
// and decode the size of all current frames--could be done, // and decode the size of all current frames--could be done,
// but presumably it's not a commonly used feature // but presumably it's not a commonly used feature
f->current_loc = -n2; // start of first frame is positioned for discard f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
// we might have to discard samples "from" the next frame too, // we might have to discard samples "from" the next frame too,
// if we're lapping a large block then a small at the start? // if we're lapping a large block then a small at the start?
f->discard_samples_deferred = n - right_end; f->discard_samples_deferred = n - right_end;
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
f->vendor[len] = (char)'\0'; f->vendor[len] = (char)'\0';
//user comments //user comments
f->comment_list_length = get32_packet(f); f->comment_list_length = get32_packet(f);
if (f->comment_list_length > 0) { f->comment_list = NULL;
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length)); if (f->comment_list_length > 0)
if (f->comment_list == NULL) return error(f, VORBIS_outofmem); {
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
} }
for(i=0; i < f->comment_list_length; ++i) { for(i=0; i < f->comment_list_length; ++i) {
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
unsigned int div=1; unsigned int div=1;
for (k=0; k < c->dimensions; ++k) { for (k=0; k < c->dimensions; ++k) {
int off = (z / div) % c->lookup_values; int off = (z / div) % c->lookup_values;
float val = mults[off]; float val = mults[off]*c->delta_value + c->minimum_value + last;
val = mults[off]*c->delta_value + c->minimum_value + last;
c->multiplicands[j*c->dimensions + k] = val; c->multiplicands[j*c->dimensions + k] = val;
if (c->sequence_p) if (c->sequence_p)
last = val; last = val;
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup); if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
} }
for (k=0; k < 1 << g->class_subclasses[j]; ++k) { for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
g->subclass_books[j][k] = get_bits(f,8)-1; g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup); if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
} }
} }
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
*error = VORBIS_need_more_data; *error = VORBIS_need_more_data;
else else
*error = p.error; *error = p.error;
vorbis_deinit(&p);
return NULL; return NULL;
} }
f = vorbis_alloc(&p); f = vorbis_alloc(&p);
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
header[i] = get8(f); header[i] = get8(f);
if (f->eof) return 0; if (f->eof) return 0;
if (header[4] != 0) goto invalid; if (header[4] != 0) goto invalid;
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24); goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
for (i=22; i < 26; ++i) for (i=22; i < 26; ++i)
header[i] = 0; header[i] = 0;
crc = 0; crc = 0;
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
// set. whoops! // set. whoops!
break; break;
} }
previous_safe = last_page_loc+1; //previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
last_page_loc = stb_vorbis_get_file_offset(f); last_page_loc = stb_vorbis_get_file_offset(f);
} }
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc) stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
{ {
stb_vorbis *f, p; stb_vorbis *f, p;
if (data == NULL) return NULL; if (!data) {
if (error) *error = VORBIS_unexpected_eof;
return NULL;
}
vorbis_init(&p, alloc); vorbis_init(&p, alloc);
p.stream = (uint8 *) data; p.stream = (uint8 *) data;
p.stream_end = (uint8 *) data + len; p.stream_end = (uint8 *) data + len;
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len) static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
{ {
#define BUFFER_SIZE 32 #define STB_BUFFER_SIZE 32
float buffer[BUFFER_SIZE]; float buffer[STB_BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE; int i,j,o,n = STB_BUFFER_SIZE;
check_endianness(); check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE) { for (o = 0; o < len; o += STB_BUFFER_SIZE) {
memset(buffer, 0, sizeof(buffer)); memset(buffer, 0, sizeof(buffer));
if (o + n > len) n = len - o; if (o + n > len) n = len - o;
for (j=0; j < num_c; ++j) { for (j=0; j < num_c; ++j) {
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
output[o+i] = v; output[o+i] = v;
} }
} }
#undef STB_BUFFER_SIZE
} }
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len) static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
{ {
#define BUFFER_SIZE 32 #define STB_BUFFER_SIZE 32
float buffer[BUFFER_SIZE]; float buffer[STB_BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE >> 1; int i,j,o,n = STB_BUFFER_SIZE >> 1;
// o is the offset in the source data // o is the offset in the source data
check_endianness(); check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE >> 1) { for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
// o2 is the offset in the output data // o2 is the offset in the output data
int o2 = o << 1; int o2 = o << 1;
memset(buffer, 0, sizeof(buffer)); memset(buffer, 0, sizeof(buffer));
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
output[o2+i] = v; output[o2+i] = v;
} }
} }
#undef STB_BUFFER_SIZE
} }
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples) static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
float **outputs; float **outputs;
int len = num_shorts / channels; int len = num_shorts / channels;
int n=0; int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) { while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start; int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n; if (n+k >= len) k = len - n;
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
{ {
float **outputs; float **outputs;
int n=0; int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) { while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start; int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n; if (n+k >= len) k = len - n;

View File

@@ -1,50 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
// Tipos de fundido
constexpr int FADE_FULLSCREEN = 0;
constexpr int FADE_CENTER = 1;
constexpr int FADE_RANDOM_SQUARE = 2;
// Clase Fade
class Fade {
private:
SDL_Renderer *mRenderer; // El renderizador de la ventana
SDL_Texture *mBackbuffer; // Textura para usar como backbuffer
Uint8 mFadeType; // Tipo de fade a realizar
Uint16 mCounter; // Contador interno
bool mEnabled; // Indica si el fade está activo
bool mFinished; // Indica si ha terminado la transición
Uint8 mR, mG, mB; // Colores para el fade
SDL_Rect mRect1; // Rectangulo usado para crear los efectos de transición
SDL_Rect mRect2; // Rectangulo usado para crear los efectos de transición
public:
// Constructor
Fade(SDL_Renderer *renderer);
// Destructor
~Fade();
// Inicializa las variables
void init(Uint8 r, Uint8 g, Uint8 b);
// Pinta una transición en pantalla
void render();
// Actualiza las variables internas
void update();
// Activa el fade
void activateFade();
// Comprueba si ha terminado la transicion
bool hasEnded();
// Comprueba si está activo
bool isEnabled();
// Establece el tipo de fade
void setFadeType(Uint8 fadeType);
};

View File

@@ -2,8 +2,58 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "lang.h" #include "core/locale/lang.h"
#include "utils.h" #include "utils/utils.h"
// =============================================================================
// Defaults per a Options (alineats amb coffee_crisis_arcade_edition).
// =============================================================================
namespace Defaults::Window {
constexpr const char *CAPTION = "© 2020 Coffee Crisis — JailDesigner";
constexpr int ZOOM = 3;
constexpr int MAX_ZOOM = 4;
} // namespace Defaults::Window
namespace Defaults::Video {
constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST;
constexpr bool FULLSCREEN = false;
constexpr bool VSYNC = true;
constexpr bool INTEGER_SCALE = true;
constexpr bool GPU_ACCELERATION = true;
constexpr const char *GPU_PREFERRED_DRIVER = "";
constexpr bool SHADER_ENABLED = false;
constexpr bool SUPERSAMPLING = false;
constexpr bool LINEAR_UPSCALE = false;
constexpr int DOWNSCALE_ALGO = 1;
} // namespace Defaults::Video
namespace Defaults::Audio {
constexpr bool ENABLED = true;
constexpr float VOLUME = 1.0F;
} // namespace Defaults::Audio
namespace Defaults::Music {
constexpr bool ENABLED = true;
constexpr float VOLUME = 0.8F;
} // namespace Defaults::Music
namespace Defaults::Sound {
constexpr bool ENABLED = true;
constexpr float VOLUME = 1.0F;
} // namespace Defaults::Sound
namespace Defaults::Loading {
constexpr bool SHOW = false;
constexpr bool SHOW_RESOURCE_NAME = true;
constexpr bool WAIT_FOR_INPUT = false;
} // namespace Defaults::Loading
namespace Defaults::Settings {
constexpr int DIFFICULTY = DIFFICULTY_NORMAL;
constexpr int LANGUAGE = ba_BA;
constexpr palette_e PALETTE = p_zxspectrum;
} // namespace Defaults::Settings
// Tamaño de bloque // Tamaño de bloque
constexpr int BLOCK = 8; constexpr int BLOCK = 8;

View File

@@ -1,12 +1,12 @@
#include "balloon.h" #include "game/entities/balloon.h"
#include <math.h> // for abs #include <math.h> // for abs
#include "animatedsprite.h" // for AnimatedSprite #include "core/rendering/animatedsprite.h" // for AnimatedSprite
#include "const.h" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR... #include "core/rendering/movingsprite.h" // for MovingSprite
#include "movingsprite.h" // for MovingSprite #include "core/rendering/sprite.h" // for Sprite
#include "sprite.h" // for Sprite #include "core/rendering/texture.h" // for Texture
#include "texture.h" // for Texture #include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...
// Constructor // Constructor
Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer) { Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer) {
@@ -261,15 +261,13 @@ Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 c
stoppedCounter = 0; stoppedCounter = 0;
blinking = false; blinking = false;
visible = true; visible = true;
invulnerable = true;
beingCreated = true;
creationCounter = creationtimer; creationCounter = creationtimer;
creationCounterIni = creationtimer; creationCounterIni = creationtimer;
popping = false; popping = false;
// Actualiza valores // Valores iniciales dependentes del timer
beingCreated = creationCounter == 0 ? false : true; beingCreated = creationCounter != 0;
invulnerable = beingCreated == false ? false : true; invulnerable = beingCreated;
counter = 0; counter = 0;
travelY = 1.0f; travelY = 1.0f;

View File

@@ -5,7 +5,7 @@
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "utils.h" // for circle_t #include "utils/utils.h" // for circle_t
class AnimatedSprite; class AnimatedSprite;
class Texture; class Texture;
@@ -146,6 +146,9 @@ class Balloon {
// Destructor // Destructor
~Balloon(); ~Balloon();
Balloon(const Balloon &) = delete;
Balloon &operator=(const Balloon &) = delete;
// Centra el globo en la posición X // Centra el globo en la posición X
void allignTo(int x); void allignTo(int x);

View File

@@ -1,7 +1,7 @@
#include "bullet.h" #include "game/entities/bullet.h"
#include "const.h" // for NO_KIND, PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_A... #include "core/rendering/sprite.h" // for Sprite
#include "sprite.h" // for Sprite #include "game/defaults.hpp" // for NO_KIND, PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_A...
class Texture; class Texture;
// Constructor // Constructor

View File

@@ -2,7 +2,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "utils.h" // for circle_t #include "utils/utils.h" // for circle_t
class Sprite; class Sprite;
class Texture; class Texture;
@@ -42,6 +42,9 @@ class Bullet {
// Destructor // Destructor
~Bullet(); ~Bullet();
Bullet(const Bullet &) = delete;
Bullet &operator=(const Bullet &) = delete;
// Pinta el objeto en pantalla // Pinta el objeto en pantalla
void render(); void render();

View File

@@ -1,9 +1,9 @@
#include "item.h" #include "game/entities/item.h"
#include <stdlib.h> // for rand #include <stdlib.h> // for rand
#include "animatedsprite.h" // for AnimatedSprite #include "core/rendering/animatedsprite.h" // for AnimatedSprite
#include "const.h" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR... #include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...
class Texture; class Texture;
// Constructor // Constructor
@@ -107,9 +107,6 @@ void Item::move() {
// Si el objeto se sale por la parte inferior // Si el objeto se sale por la parte inferior
if (posY + height > PLAY_AREA_BOTTOM) { if (posY + height > PLAY_AREA_BOTTOM) {
// Corrige
posY -= velY;
// Detiene el objeto // Detiene el objeto
velY = 0; velY = 0;
velX = 0; velX = 0;

View File

@@ -5,7 +5,7 @@
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "utils.h" // for circle_t #include "utils/utils.h" // for circle_t
class AnimatedSprite; class AnimatedSprite;
class Texture; class Texture;
@@ -52,6 +52,9 @@ class Item {
// Destructor // Destructor
~Item(); ~Item();
Item(const Item &) = delete;
Item &operator=(const Item &) = delete;
// Centra el objeto en la posición X // Centra el objeto en la posición X
void allignTo(int x); void allignTo(int x);

View File

@@ -1,14 +1,14 @@
#include "player.h" #include "game/entities/player.h"
#include <stdlib.h> // for rand #include <stdlib.h> // for rand
#include "animatedsprite.h" // for AnimatedSprite #include "core/input/input.h" // for inputs_e
#include "const.h" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT #include "core/rendering/animatedsprite.h" // for AnimatedSprite
#include "input.h" // for inputs_e #include "core/rendering/texture.h" // for Texture
#include "texture.h" // for Texture #include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT
// Constructor // Constructor
Player::Player(float x, int y, SDL_Renderer *renderer, std::vector<Texture *> texture, std::vector<std::vector<std::string> *> animations) { Player::Player(float x, int y, SDL_Renderer *renderer, const std::vector<Texture *> &texture, const std::vector<std::vector<std::string> *> &animations) {
// Copia los punteros // Copia los punteros
this->renderer = renderer; this->renderer = renderer;
@@ -189,19 +189,12 @@ void Player::render() {
// Establece el estado del jugador cuando camina // Establece el estado del jugador cuando camina
void Player::setWalkingStatus(Uint8 status) { void Player::setWalkingStatus(Uint8 status) {
// Si cambiamos de estado, reiniciamos la animación statusWalking = status;
if (statusWalking != status) {
statusWalking = status;
// legsSprite->setCurrentFrame(0);
}
} }
// Establece el estado del jugador cuando dispara // Establece el estado del jugador cuando dispara
void Player::setFiringStatus(Uint8 status) { void Player::setFiringStatus(Uint8 status) {
// Si cambiamos de estado, reiniciamos la animación statusFiring = status;
if (statusFiring != status) {
statusFiring = status;
}
} }
// Establece la animación correspondiente al estado // Establece la animación correspondiente al estado
@@ -521,7 +514,7 @@ void Player::updatePowerUpHeadOffset() {
} }
// Pone las texturas del jugador // Pone las texturas del jugador
void Player::setPlayerTextures(std::vector<Texture *> texture) { void Player::setPlayerTextures(const std::vector<Texture *> &texture) {
headSprite->setTexture(texture[0]); headSprite->setTexture(texture[0]);
bodySprite->setTexture(texture[1]); bodySprite->setTexture(texture[1]);
legsSprite->setTexture(texture[2]); legsSprite->setTexture(texture[2]);

View File

@@ -5,7 +5,7 @@
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "utils.h" // for circle_t #include "utils/utils.h" // for circle_t
class AnimatedSprite; class AnimatedSprite;
class Texture; class Texture;
@@ -81,11 +81,14 @@ class Player {
public: public:
// Constructor // Constructor
Player(float x, int y, SDL_Renderer *renderer, std::vector<Texture *> texture, std::vector<std::vector<std::string> *> animations); Player(float x, int y, SDL_Renderer *renderer, const std::vector<Texture *> &texture, const std::vector<std::vector<std::string> *> &animations);
// Destructor // Destructor
~Player(); ~Player();
Player(const Player &) = delete;
Player &operator=(const Player &) = delete;
// Iniciador // Iniciador
void init(); void init();
@@ -96,7 +99,7 @@ class Player {
void render(); void render();
// Pone las texturas del jugador // Pone las texturas del jugador
void setPlayerTextures(std::vector<Texture *> texture); void setPlayerTextures(const std::vector<Texture *> &texture);
// Actua en consecuencia de la entrada recibida // Actua en consecuencia de la entrada recibida
void setInput(Uint8 input); void setInput(Uint8 input);

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,14 @@
#include <string> // for string, basic_string #include <string> // for string, basic_string
#include <vector> // for vector #include <vector> // for vector
#include "utils.h" // for demoKeys_t, color_t #include "utils/utils.h" // for demoKeys_t, color_t
class Asset;
class Balloon; class Balloon;
class Bullet; class Bullet;
class Fade; class Fade;
class Input;
class Item; class Item;
class Lang;
class Menu; class Menu;
class MovingSprite; class MovingSprite;
class Player; class Player;
class Screen;
class SmartSprite; class SmartSprite;
class Sprite; class Sprite;
class Text; class Text;
@@ -88,6 +84,26 @@ class Game {
Uint8 shakeCounter; // Contador para medir el tiempo que dura el efecto Uint8 shakeCounter; // Contador para medir el tiempo que dura el efecto
}; };
// Estado para el efecto de agitación intensa (muerte del jugador)
struct deathShake_t {
bool active; // Indica si el efecto está activo
Uint8 step; // Paso actual del efecto (0-7)
Uint32 lastStepTicks; // Ticks del último paso
};
// Fases de la secuencia de muerte del jugador
enum class DeathPhase { None,
Shaking,
Waiting,
Done };
// Estado de la secuencia de muerte del jugador
struct deathSequence_t {
DeathPhase phase; // Fase actual
Uint32 phaseStartTicks; // Ticks del inicio de la fase actual
Player *player; // Jugador que está muriendo
};
struct helper_t { struct helper_t {
bool needCoffee; // Indica si se necesitan cafes bool needCoffee; // Indica si se necesitan cafes
bool needCoffeeMachine; // Indica si se necesita PowerUp bool needCoffeeMachine; // Indica si se necesita PowerUp
@@ -111,10 +127,6 @@ class Game {
// Objetos y punteros // Objetos y punteros
SDL_Renderer *renderer; // El renderizador de la ventana SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
Input *input; // Manejador de entrada
section_t *section; // Seccion actual dentro del juego section_t *section; // Seccion actual dentro del juego
std::vector<Player *> players; // Vector con los jugadores std::vector<Player *> players; // Vector con los jugadores
@@ -214,6 +226,8 @@ class Game {
float enemySpeed; // Velocidad a la que se mueven los enemigos float enemySpeed; // Velocidad a la que se mueven los enemigos
float defaultEnemySpeed; // Velocidad base de los enemigos, sin incrementar float defaultEnemySpeed; // Velocidad base de los enemigos, sin incrementar
effect_t effect; // Variable para gestionar los efectos visuales effect_t effect; // Variable para gestionar los efectos visuales
deathShake_t deathShake; // Variable para gestionar el efecto de agitación intensa
deathSequence_t deathSequence; // Variable para gestionar la secuencia de muerte
helper_t helper; // Variable para gestionar las ayudas helper_t helper; // Variable para gestionar las ayudas
bool powerBallEnabled; // Indica si hay una powerball ya activa bool powerBallEnabled; // Indica si hay una powerball ya activa
Uint8 powerBallCounter; // Contador de formaciones enemigas entre la aparicion de una PowerBall y otra Uint8 powerBallCounter; // Contador de formaciones enemigas entre la aparicion de una PowerBall y otra
@@ -223,16 +237,18 @@ class Game {
Uint8 difficulty; // Dificultad del juego Uint8 difficulty; // Dificultad del juego
float difficultyScoreMultiplier; // Multiplicador de puntos en función de la dificultad float difficultyScoreMultiplier; // Multiplicador de puntos en función de la dificultad
color_t difficultyColor; // Color asociado a la dificultad color_t difficultyColor; // Color asociado a la dificultad
struct options_t *options; // Variable con todas las variables de las opciones del programa
Uint8 onePlayerControl; // Variable para almacenar el valor de las opciones Uint8 onePlayerControl; // Variable para almacenar el valor de las opciones
enemyFormation_t enemyFormation[NUMBER_OF_ENEMY_FORMATIONS]; // Vector con todas las formaciones enemigas enemyFormation_t enemyFormation[NUMBER_OF_ENEMY_FORMATIONS]; // Vector con todas las formaciones enemigas
enemyPool_t enemyPool[10]; // Variable con los diferentes conjuntos de formaciones enemigas enemyPool_t enemyPool[10]; // Variable con los diferentes conjuntos de formaciones enemigas
Uint8 lastStageReached; // Contiene el numero de la última pantalla que se ha alcanzado Uint8 lastStageReached; // Contiene el numero de la última pantalla que se ha alcanzado
demo_t demo; // Variable con todas las variables relacionadas con el modo demo demo_t demo; // Variable con todas las variables relacionadas con el modo demo
int totalPowerToCompleteGame; // La suma del poder necesario para completar todas las fases int totalPowerToCompleteGame; // La suma del poder necesario para completar todas las fases
int cloudsSpeed; // Velocidad a la que se desplazan las nubes int cloudsSpeed{0}; // Velocidad a la que se desplazan las nubes
int pauseCounter; // Contador para salir del menu de pausa y volver al juego int pauseCounter; // Contador para salir del menu de pausa y volver al juego
bool leavingPauseMenu; // Indica si esta saliendo del menu de pausa para volver al juego bool leavingPauseMenu; // Indica si esta saliendo del menu de pausa para volver al juego
bool pauseInitialized; // Indica si la pausa ha sido inicializada
bool gameOverInitialized; // Indica si el game over ha sido inicializado
int gameOverPostFade; // Opción a realizar cuando termina el fundido del game over
#ifdef PAUSE #ifdef PAUSE
bool pause; bool pause;
#endif #endif
@@ -243,9 +259,6 @@ class Game {
// Dibuja el juego // Dibuja el juego
void render(); void render();
// Comprueba los eventos que hay en cola
void checkEvents();
// Inicializa las variables necesarias para la sección 'Game' // Inicializa las variables necesarias para la sección 'Game'
void init(); void init();
@@ -324,9 +337,6 @@ class Game {
// Incrementa la velocidad de los globos // Incrementa la velocidad de los globos
void incBalloonSpeed(); void incBalloonSpeed();
// Decrementa la velocidad de los globos
void decBalloonSpeed();
// Actualiza la velocidad de los globos en funcion del poder acumulado de la fase // Actualiza la velocidad de los globos en funcion del poder acumulado de la fase
void updateBalloonSpeed(); void updateBalloonSpeed();
@@ -336,9 +346,6 @@ class Game {
// Explosiona un globo. Lo destruye // Explosiona un globo. Lo destruye
void destroyBalloon(Balloon *balloon); void destroyBalloon(Balloon *balloon);
// Explosiona todos los globos
void popAllBalloons();
// Destruye todos los globos // Destruye todos los globos
void destroyAllBalloons(); void destroyAllBalloons();
@@ -348,9 +355,6 @@ class Game {
// Pone en marcha todos los globos // Pone en marcha todos los globos
void startAllBalloons(); void startAllBalloons();
// Obtiene el numero de globos activos
Uint8 countBalloons();
// Vacia el vector de globos // Vacia el vector de globos
void freeBalloons(); void freeBalloons();
@@ -391,7 +395,7 @@ class Game {
void freeItems(); void freeItems();
// Crea un objeto SmartSprite // Crea un objeto SmartSprite
void createItemScoreSprite(int x, int y, SmartSprite *sprite); void createItemScoreSprite(int x, int y, const SmartSprite *sprite);
// Vacia el vector de smartsprites // Vacia el vector de smartsprites
void freeSmartSprites(); void freeSmartSprites();
@@ -459,17 +463,26 @@ class Game {
// Deshabilita el efecto del item de detener el tiempo // Deshabilita el efecto del item de detener el tiempo
void disableTimeStopItem(); void disableTimeStopItem();
// Agita la pantalla // Inicia el efecto de agitación intensa de la pantalla
void shakeScreen(); void shakeScreen();
// Actualiza el efecto de agitación intensa
void updateDeathShake();
// Indica si el efecto de agitación intensa está activo
bool isDeathShaking();
// Actualiza la secuencia de muerte del jugador
void updateDeathSequence();
// Actualiza las variables del menu de pausa del juego // Actualiza las variables del menu de pausa del juego
void updatePausedGame(); void updatePausedGame();
// Dibuja el menu de pausa del juego // Dibuja el menu de pausa del juego
void renderPausedGame(); void renderPausedGame();
// Bucle para el menu de pausa del juego // Inicializa el estado de pausa del juego
void runPausedGame(); void enterPausedGame();
// Actualiza los elementos de la pantalla de game over // Actualiza los elementos de la pantalla de game over
void updateGameOverScreen(); void updateGameOverScreen();
@@ -477,8 +490,8 @@ class Game {
// Dibuja los elementos de la pantalla de game over // Dibuja los elementos de la pantalla de game over
void renderGameOverScreen(); void renderGameOverScreen();
// Bucle para la pantalla de game over // Inicializa el estado de game over
void runGameOverScreen(); void enterGameOverScreen();
// Indica si se puede crear una powerball // Indica si se puede crear una powerball
bool canPowerBallBeCreated(); bool canPowerBallBeCreated();
@@ -498,25 +511,31 @@ class Game {
// Comprueba si todos los jugadores han muerto // Comprueba si todos los jugadores han muerto
bool allPlayersAreDead(); bool allPlayersAreDead();
// Carga las animaciones
void loadAnimations(std::string filePath, std::vector<std::string> *buffer);
// Elimina todos los objetos contenidos en vectores // Elimina todos los objetos contenidos en vectores
void deleteAllVectorObjects(); void deleteAllVectorObjects();
// Recarga las texturas
void reloadTextures();
// Establece la máxima puntuación desde fichero o desde las puntuaciones online // Establece la máxima puntuación desde fichero o desde las puntuaciones online
void setHiScore(); void setHiScore();
public: public:
// Constructor // Constructor
Game(int numPlayers, int currentStage, SDL_Renderer *renderer, Screen *screen, Asset *asset, Lang *lang, Input *input, bool demo, options_t *options, section_t *section); Game(int numPlayers, int currentStage, SDL_Renderer *renderer, bool demo, section_t *section);
// Destructor // Destructor
~Game(); ~Game();
Game(const Game &) = delete;
Game &operator=(const Game &) = delete;
// Bucle para el juego // Bucle para el juego
void run(); void run();
// Ejecuta un frame del juego
void iterate();
// Indica si el juego ha terminado
bool hasFinished() const;
// Procesa un evento
void handleEvent(const SDL_Event *event);
}; };

574
source/game/options.cpp Normal file
View File

@@ -0,0 +1,574 @@
#include "game/options.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <string>
#include "core/input/input.h" // for INPUT_USE_KEYBOARD, INPUT_USE_GAMECONTROLLER
#include "core/locale/lang.h" // for MAX_LANGUAGES, en_UK
#include "external/fkyaml_node.hpp" // for fkyaml::node
#include "utils/utils.h" // for boolToString
namespace Options {
// --- Variables globales ---
Window window;
Video video;
Audio audio;
Loading loading;
Settings settings;
std::vector<input_t> inputs;
std::vector<PostFXPreset> postfx_presets;
std::string postfx_file_path;
int current_postfx_preset = 0;
std::vector<CrtPiPreset> crtpi_presets;
std::string crtpi_file_path;
int current_crtpi_preset = 0;
// --- Helpers locals ---
namespace {
void parseBoolField(const fkyaml::node &node, const std::string &key, bool &target) {
if (node.contains(key)) {
try {
target = node[key].get_value<bool>();
} catch (...) {}
}
}
void parseIntField(const fkyaml::node &node, const std::string &key, int &target) {
if (node.contains(key)) {
try {
target = node[key].get_value<int>();
} catch (...) {}
}
}
void parseStringField(const fkyaml::node &node, const std::string &key, std::string &target) {
if (node.contains(key)) {
try {
target = node[key].get_value<std::string>();
} catch (...) {}
}
}
void loadWindowFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("window")) { return; }
const auto &win = yaml["window"];
parseIntField(win, "zoom", window.zoom);
if (window.zoom < 1 || window.zoom > window.max_zoom) {
window.zoom = Defaults::Window::ZOOM;
}
}
void loadVideoFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("video")) { return; }
const auto &vid = yaml["video"];
parseBoolField(vid, "fullscreen", video.fullscreen);
parseBoolField(vid, "vsync", video.vsync);
parseBoolField(vid, "integer_scale", video.integer_scale);
if (vid.contains("scale_mode")) {
try {
video.scale_mode = static_cast<SDL_ScaleMode>(vid["scale_mode"].get_value<int>());
} catch (...) {}
}
if (vid.contains("gpu")) {
const auto &gpu = vid["gpu"];
parseBoolField(gpu, "acceleration", video.gpu.acceleration);
parseStringField(gpu, "preferred_driver", video.gpu.preferred_driver);
}
if (vid.contains("supersampling")) {
const auto &ss = vid["supersampling"];
parseBoolField(ss, "enabled", video.supersampling.enabled);
parseBoolField(ss, "linear_upscale", video.supersampling.linear_upscale);
parseIntField(ss, "downscale_algo", video.supersampling.downscale_algo);
}
if (vid.contains("shader")) {
const auto &sh = vid["shader"];
parseBoolField(sh, "enabled", video.shader.enabled);
if (sh.contains("current_shader")) {
try {
auto s = sh["current_shader"].get_value<std::string>();
video.shader.current_shader = (s == "crtpi" || s == "CRTPI")
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
} catch (...) {}
}
parseStringField(sh, "current_postfx_preset", video.shader.current_postfx_preset_name);
parseStringField(sh, "current_crtpi_preset", video.shader.current_crtpi_preset_name);
}
}
void loadAudioFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("audio")) { return; }
const auto &aud = yaml["audio"];
parseBoolField(aud, "enabled", audio.enabled);
if (aud.contains("volume")) {
try {
audio.volume = std::clamp(aud["volume"].get_value<float>(), 0.0F, 1.0F);
} catch (...) {}
}
if (aud.contains("music")) {
const auto &mus = aud["music"];
parseBoolField(mus, "enabled", audio.music.enabled);
if (mus.contains("volume")) {
try {
audio.music.volume = std::clamp(mus["volume"].get_value<float>(), 0.0F, 1.0F);
} catch (...) {}
}
}
if (aud.contains("sound")) {
const auto &snd = aud["sound"];
parseBoolField(snd, "enabled", audio.sound.enabled);
if (snd.contains("volume")) {
try {
audio.sound.volume = std::clamp(snd["volume"].get_value<float>(), 0.0F, 1.0F);
} catch (...) {}
}
}
}
void loadLoadingFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("loading")) { return; }
const auto &ld = yaml["loading"];
parseBoolField(ld, "show", loading.show);
parseBoolField(ld, "show_resource_name", loading.show_resource_name);
parseBoolField(ld, "wait_for_input", loading.wait_for_input);
}
void loadSettingsFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("settings")) { return; }
const auto &st = yaml["settings"];
parseIntField(st, "difficulty", settings.difficulty);
parseIntField(st, "language", settings.language);
if (settings.language < 0 || settings.language > MAX_LANGUAGES) {
settings.language = en_UK;
}
if (st.contains("palette")) {
try {
settings.palette = static_cast<palette_e>(st["palette"].get_value<int>());
} catch (...) {}
}
parseIntField(st, "player_selected", settings.player_selected);
}
void loadInputsFromYaml(const fkyaml::node &yaml) {
if (!yaml.contains("input") || inputs.size() < 2) { return; }
const auto &ins = yaml["input"];
size_t i = 0;
for (const auto &entry : ins) {
if (i >= inputs.size()) { break; }
if (entry.contains("device_type")) {
try {
inputs[i].deviceType = static_cast<Uint8>(entry["device_type"].get_value<int>());
} catch (...) {}
}
++i;
}
}
} // namespace
// --- Funciones públiques ---
void setConfigFile(const std::string &file_path) {
settings.config_file = file_path;
}
void init() {
// Reinicia structs a defaults (els member-initializers ho fan sols).
window = Window{};
video = Video{};
audio = Audio{};
loading = Loading{};
// Preserva config_file si ja s'ha establert abans.
const std::string PREV_CONFIG_FILE = settings.config_file;
settings = Settings{};
settings.config_file = PREV_CONFIG_FILE;
#ifdef __EMSCRIPTEN__
// En Emscripten la ventana la gestiona el navegador
window.zoom = 4;
video.fullscreen = false;
video.integer_scale = true;
#endif
// Dispositius d'entrada per defecte
inputs.clear();
input_t kb;
kb.id = 0;
kb.name = "KEYBOARD";
kb.deviceType = INPUT_USE_KEYBOARD;
inputs.push_back(kb);
input_t gc;
gc.id = 0;
gc.name = "GAME CONTROLLER";
gc.deviceType = INPUT_USE_GAMECONTROLLER;
inputs.push_back(gc);
}
auto loadFromFile() -> bool {
init();
std::ifstream file(settings.config_file);
if (!file.is_open()) {
// Primera execució: crea el YAML amb defaults.
return saveToFile();
}
const std::string CONTENT((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
int file_version = 0;
if (yaml.contains("config_version")) {
try {
file_version = yaml["config_version"].get_value<int>();
} catch (...) {}
}
if (file_version != Settings::CURRENT_CONFIG_VERSION) {
std::cout << "Config version " << file_version
<< " != expected " << Settings::CURRENT_CONFIG_VERSION
<< ". Recreating defaults.\n";
init();
return saveToFile();
}
loadWindowFromYaml(yaml);
loadVideoFromYaml(yaml);
loadAudioFromYaml(yaml);
loadLoadingFromYaml(yaml);
loadSettingsFromYaml(yaml);
loadInputsFromYaml(yaml);
} catch (const fkyaml::exception &e) {
std::cout << "Error parsing YAML config: " << e.what() << ". Using defaults.\n";
init();
return saveToFile();
}
return true;
}
auto saveToFile() -> bool {
if (settings.config_file.empty()) { return false; }
std::ofstream file(settings.config_file);
if (!file.is_open()) {
std::cout << "Error: " << settings.config_file << " can't be opened for writing\n";
return false;
}
file << "# Coffee Crisis - Configuration file\n";
file << "# Auto-generated, managed by the game.\n\n";
file << "config_version: " << settings.config_version << "\n\n";
// WINDOW
file << "# WINDOW\n";
file << "window:\n";
file << " zoom: " << window.zoom << "\n";
file << " max_zoom: " << window.max_zoom << "\n\n";
// VIDEO
file << "# VIDEO\n";
file << "video:\n";
file << " fullscreen: " << boolToString(video.fullscreen) << "\n";
file << " vsync: " << boolToString(video.vsync) << "\n";
file << " integer_scale: " << boolToString(video.integer_scale) << "\n";
file << " scale_mode: " << static_cast<int>(video.scale_mode)
<< " # " << static_cast<int>(SDL_SCALEMODE_NEAREST) << ": nearest, "
<< static_cast<int>(SDL_SCALEMODE_LINEAR) << ": linear\n";
file << " gpu:\n";
file << " acceleration: " << boolToString(video.gpu.acceleration) << "\n";
file << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
file << " supersampling:\n";
file << " enabled: " << boolToString(video.supersampling.enabled) << "\n";
file << " linear_upscale: " << boolToString(video.supersampling.linear_upscale) << "\n";
file << " downscale_algo: " << video.supersampling.downscale_algo << "\n";
file << " shader:\n";
file << " enabled: " << boolToString(video.shader.enabled) << "\n";
file << " current_shader: "
<< (video.shader.current_shader == Rendering::ShaderType::CRTPI ? "crtpi" : "postfx")
<< "\n";
file << " current_postfx_preset: \"" << video.shader.current_postfx_preset_name << "\"\n";
file << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset_name << "\"\n\n";
// AUDIO
file << "# AUDIO (volume range: 0.0..1.0)\n";
file << "audio:\n";
file << " enabled: " << boolToString(audio.enabled) << "\n";
file << " volume: " << audio.volume << "\n";
file << " music:\n";
file << " enabled: " << boolToString(audio.music.enabled) << "\n";
file << " volume: " << audio.music.volume << "\n";
file << " sound:\n";
file << " enabled: " << boolToString(audio.sound.enabled) << "\n";
file << " volume: " << audio.sound.volume << "\n\n";
// LOADING
file << "# LOADING SCREEN\n";
file << "loading:\n";
file << " show: " << boolToString(loading.show) << "\n";
file << " show_resource_name: " << boolToString(loading.show_resource_name) << "\n";
file << " wait_for_input: " << boolToString(loading.wait_for_input) << "\n\n";
// SETTINGS
file << "# SETTINGS\n";
file << "settings:\n";
file << " difficulty: " << settings.difficulty << "\n";
file << " language: " << settings.language << "\n";
file << " palette: " << static_cast<int>(settings.palette) << "\n";
file << " player_selected: " << settings.player_selected << "\n\n";
// INPUT
file << "# INPUT DEVICES (device_type: "
<< static_cast<int>(INPUT_USE_KEYBOARD) << "=KEYBOARD, "
<< static_cast<int>(INPUT_USE_GAMECONTROLLER) << "=GAMECONTROLLER)\n";
file << "input:\n";
for (size_t i = 0; i < inputs.size(); ++i) {
file << " - slot: " << i << "\n";
file << " device_type: " << static_cast<int>(inputs[i].deviceType) << "\n";
}
file.close();
return true;
}
// ========================================================================
// Presets de shaders (postfx.yaml / crtpi.yaml)
//
// Els defaults viuen en una única font (defaultPostFXPresets / defaultCrtPiPresets).
// Generem el YAML a partir d'ells el primer cop i els usem també com a
// fallback si el YAML és absent o corrupte. Si algú toca els valors, ho fa
// en un sol lloc.
// ========================================================================
namespace {
void parseFloatField(const fkyaml::node &node, const std::string &key, float &target) {
if (node.contains(key)) {
try {
target = node[key].get_value<float>();
} catch (...) {}
}
}
auto defaultPostFXPresets() -> const std::vector<PostFXPreset> & {
static const std::vector<PostFXPreset> DEFAULTS = {
{"CRT", 0.6F, 0.7F, 0.15F, 0.6F, 0.8F},
{"NTSC", 0.4F, 0.5F, 0.2F, 0.4F, 0.5F, 0.0F, 0.6F},
{"CURVED", 0.5F, 0.6F, 0.1F, 0.5F, 0.7F, 0.8F},
{"SCANLINES", 0.0F, 0.8F},
{"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F},
{"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F},
};
return DEFAULTS;
}
auto defaultCrtPiPresets() -> const std::vector<CrtPiPreset> & {
static const std::vector<CrtPiPreset> DEFAULTS = {
{"Default", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false},
{"Curved", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false},
{"Sharp", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true},
{"Minimal", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false},
};
return DEFAULTS;
}
void writePostFXDefaults(std::ostream &out) {
out << "# Coffee Crisis - PostFX Shader Presets\n\n";
out << "presets:\n";
for (const auto &p : defaultPostFXPresets()) {
out << " - name: \"" << p.name << "\"\n";
out << " vignette: " << p.vignette << "\n";
out << " scanlines: " << p.scanlines << "\n";
out << " chroma: " << p.chroma << "\n";
out << " mask: " << p.mask << "\n";
out << " gamma: " << p.gamma << "\n";
out << " curvature: " << p.curvature << "\n";
out << " bleeding: " << p.bleeding << "\n";
out << " flicker: " << p.flicker << "\n";
}
}
void writeCrtPiDefaults(std::ostream &out) {
out << "# Coffee Crisis - CrtPi Shader Presets\n\n";
out << "presets:\n";
for (const auto &p : defaultCrtPiPresets()) {
out << " - name: \"" << p.name << "\"\n";
out << " scanline_weight: " << p.scanline_weight << "\n";
out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n";
out << " bloom_factor: " << p.bloom_factor << "\n";
out << " input_gamma: " << p.input_gamma << "\n";
out << " output_gamma: " << p.output_gamma << "\n";
out << " mask_brightness: " << p.mask_brightness << "\n";
out << " curvature_x: " << p.curvature_x << "\n";
out << " curvature_y: " << p.curvature_y << "\n";
out << " mask_type: " << p.mask_type << "\n";
out << " enable_scanlines: " << boolToString(p.enable_scanlines) << "\n";
out << " enable_multisample: " << boolToString(p.enable_multisample) << "\n";
out << " enable_gamma: " << boolToString(p.enable_gamma) << "\n";
out << " enable_curvature: " << boolToString(p.enable_curvature) << "\n";
out << " enable_sharper: " << boolToString(p.enable_sharper) << "\n";
}
}
} // namespace
void setPostFXFile(const std::string &path) {
postfx_file_path = path;
}
auto loadPostFXFromFile() -> bool {
postfx_presets.clear();
current_postfx_preset = 0;
std::ifstream file(postfx_file_path);
if (!file.is_open()) {
// No existeix: escriu el YAML a partir dels defaults i copia'ls a memòria.
std::ofstream out(postfx_file_path);
if (out.is_open()) {
writePostFXDefaults(out);
out.close();
}
postfx_presets = defaultPostFXPresets();
return true;
}
const std::string CONTENT((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
if (yaml.contains("presets")) {
for (const auto &p : yaml["presets"]) {
PostFXPreset preset;
if (p.contains("name")) {
try {
preset.name = p["name"].get_value<std::string>();
} catch (...) {}
}
parseFloatField(p, "vignette", preset.vignette);
parseFloatField(p, "scanlines", preset.scanlines);
parseFloatField(p, "chroma", preset.chroma);
parseFloatField(p, "mask", preset.mask);
parseFloatField(p, "gamma", preset.gamma);
parseFloatField(p, "curvature", preset.curvature);
parseFloatField(p, "bleeding", preset.bleeding);
parseFloatField(p, "flicker", preset.flicker);
postfx_presets.push_back(preset);
}
}
std::cout << "PostFX loaded: " << postfx_presets.size() << " preset(s)\n";
} catch (const fkyaml::exception &e) {
std::cout << "Error parsing PostFX YAML: " << e.what() << ". Using defaults.\n";
postfx_presets = defaultPostFXPresets();
return false;
}
if (postfx_presets.empty()) {
postfx_presets = defaultPostFXPresets();
}
return true;
}
void setCrtPiFile(const std::string &path) {
crtpi_file_path = path;
}
auto loadCrtPiFromFile() -> bool {
crtpi_presets.clear();
current_crtpi_preset = 0;
std::ifstream file(crtpi_file_path);
if (!file.is_open()) {
std::ofstream out(crtpi_file_path);
if (out.is_open()) {
writeCrtPiDefaults(out);
out.close();
}
crtpi_presets = defaultCrtPiPresets();
return true;
}
const std::string CONTENT((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
if (yaml.contains("presets")) {
for (const auto &p : yaml["presets"]) {
CrtPiPreset preset;
if (p.contains("name")) {
try {
preset.name = p["name"].get_value<std::string>();
} catch (...) {}
}
parseFloatField(p, "scanline_weight", preset.scanline_weight);
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
parseFloatField(p, "bloom_factor", preset.bloom_factor);
parseFloatField(p, "input_gamma", preset.input_gamma);
parseFloatField(p, "output_gamma", preset.output_gamma);
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
if (p.contains("mask_type")) {
try {
preset.mask_type = p["mask_type"].get_value<int>();
} catch (...) {}
}
if (p.contains("enable_scanlines")) {
try {
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_multisample")) {
try {
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_gamma")) {
try {
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_curvature")) {
try {
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_sharper")) {
try {
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
} catch (...) {}
}
crtpi_presets.push_back(preset);
}
}
std::cout << "CrtPi loaded: " << crtpi_presets.size() << " preset(s)\n";
} catch (const fkyaml::exception &e) {
std::cout << "Error parsing CrtPi YAML: " << e.what() << ". Using defaults.\n";
crtpi_presets = defaultCrtPiPresets();
return false;
}
if (crtpi_presets.empty()) {
crtpi_presets = defaultCrtPiPresets();
}
return true;
}
} // namespace Options

153
source/game/options.hpp Normal file
View File

@@ -0,0 +1,153 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <vector>
#include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType
#include "game/defaults.hpp"
#include "utils/utils.h" // for input_t, palette_e
// =============================================================================
// Opciones del programa, alineades amb coffee_crisis_arcade_edition.
// L'estat viu en globals dins el namespace Options:: (window, video, audio,
// loading, settings, inputs). La persistència usa fkyaml i es guarda a
// config.yaml dins la carpeta de configuració del sistema.
// =============================================================================
namespace Options {
struct Window {
std::string caption = Defaults::Window::CAPTION;
int zoom = Defaults::Window::ZOOM;
int max_zoom = Defaults::Window::MAX_ZOOM;
};
struct GPU {
bool acceleration = Defaults::Video::GPU_ACCELERATION;
std::string preferred_driver = Defaults::Video::GPU_PREFERRED_DRIVER;
};
struct Supersampling {
bool enabled = Defaults::Video::SUPERSAMPLING;
bool linear_upscale = Defaults::Video::LINEAR_UPSCALE;
int downscale_algo = Defaults::Video::DOWNSCALE_ALGO;
};
struct ShaderConfig {
bool enabled = Defaults::Video::SHADER_ENABLED;
Rendering::ShaderType current_shader = Rendering::ShaderType::POSTFX;
std::string current_postfx_preset_name = "CRT";
std::string current_crtpi_preset_name = "Default";
int current_postfx_preset = 0;
int current_crtpi_preset = 0;
};
struct Video {
SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE;
bool fullscreen = Defaults::Video::FULLSCREEN;
bool vsync = Defaults::Video::VSYNC;
bool integer_scale = Defaults::Video::INTEGER_SCALE;
GPU gpu;
Supersampling supersampling;
ShaderConfig shader;
};
struct Music {
bool enabled = Defaults::Music::ENABLED;
float volume = Defaults::Music::VOLUME;
};
struct Sound {
bool enabled = Defaults::Sound::ENABLED;
float volume = Defaults::Sound::VOLUME;
};
struct Audio {
bool enabled = Defaults::Audio::ENABLED;
float volume = Defaults::Audio::VOLUME;
Music music;
Sound sound;
};
struct Loading {
bool show = Defaults::Loading::SHOW;
bool show_resource_name = Defaults::Loading::SHOW_RESOURCE_NAME;
bool wait_for_input = Defaults::Loading::WAIT_FOR_INPUT;
};
struct Settings {
static constexpr int CURRENT_CONFIG_VERSION = 1;
int config_version = CURRENT_CONFIG_VERSION;
int difficulty = Defaults::Settings::DIFFICULTY;
int language = Defaults::Settings::LANGUAGE;
palette_e palette = Defaults::Settings::PALETTE;
bool console = false;
int player_selected = 0;
std::string config_file;
};
// Preset PostFX
struct PostFXPreset {
std::string name;
float vignette{0.0F};
float scanlines{0.0F};
float chroma{0.0F};
float mask{0.0F};
float gamma{0.0F};
float curvature{0.0F};
float bleeding{0.0F};
float flicker{0.0F};
};
// Preset CrtPi
struct CrtPiPreset {
std::string name;
float scanline_weight{6.0F};
float scanline_gap_brightness{0.12F};
float bloom_factor{3.5F};
float input_gamma{2.4F};
float output_gamma{2.2F};
float mask_brightness{0.80F};
float curvature_x{0.05F};
float curvature_y{0.10F};
int mask_type{2};
bool enable_scanlines{true};
bool enable_multisample{true};
bool enable_gamma{true};
bool enable_curvature{false};
bool enable_sharper{false};
};
// --- Variables globales ---
extern Window window;
extern Video video;
extern Audio audio;
extern Loading loading;
extern Settings settings;
extern std::vector<input_t> inputs; // [0]=KEYBOARD, [1]=GAMECONTROLLER per defecte
// Presets de shaders (carregats de postfx.yaml / crtpi.yaml al config folder)
extern std::vector<PostFXPreset> postfx_presets;
extern std::string postfx_file_path;
extern int current_postfx_preset; // Índex dins `postfx_presets`
extern std::vector<CrtPiPreset> crtpi_presets;
extern std::string crtpi_file_path;
extern int current_crtpi_preset; // Índex dins `crtpi_presets`
// --- Funciones ---
void init(); // Reinicia a defaults i omple `inputs`
void setConfigFile(const std::string &file_path); // Ruta del config.yaml
auto loadFromFile() -> bool; // Carrega el YAML; si no existeix, crea'l amb defaults
auto saveToFile() -> bool; // Guarda el YAML
// Presets de shaders. Si el fitxer no existeix, l'escriu amb els defaults
// i deixa els presets carregats en memòria.
void setPostFXFile(const std::string &path);
auto loadPostFXFromFile() -> bool;
void setCrtPiFile(const std::string &path);
auto loadCrtPiFromFile() -> bool;
} // namespace Options

View File

@@ -1,4 +1,4 @@
#include "instructions.h" #include "game/scenes/instructions.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
@@ -6,52 +6,37 @@
#include <iostream> // for char_traits, basic_ostream, operator<< #include <iostream> // for char_traits, basic_ostream, operator<<
#include <string> // for basic_string #include <string> // for basic_string
#include "asset.h" // for Asset #include "core/audio/audio.hpp" // for Audio::update
#include "const.h" // for shdwTxtColor, GAMECANVAS_CENTER_X, GAME... #include "core/input/global_inputs.hpp" // for GlobalInputs::handle
#include "input.h" // for Input, REPEAT_FALSE, inputs_e #include "core/input/input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_StopMusic #include "core/locale/lang.h" // for Lang
#include "lang.h" // for Lang #include "core/rendering/screen.h" // for Screen
#include "screen.h" // for Screen #include "core/rendering/sprite.h" // for Sprite
#include "sprite.h" // for Sprite #include "core/rendering/text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_SHADOW
#include "text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_SHADOW #include "core/rendering/texture.h" // for Texture
#include "texture.h" // for Texture #include "core/resources/resource.h"
#include "utils.h" // for color_t, section_t #include "game/defaults.hpp" // for shdwTxtColor, GAMECANVAS_CENTER_X, GAME...
#include "utils/utils.h" // for color_t, section_t
const Uint8 SELF = 0;
// Constructor // Constructor
Instructions::Instructions(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, Lang *lang, section_t *section) { Instructions::Instructions(SDL_Renderer *renderer, section_t *section) {
// Copia los punteros // Copia los punteros
this->renderer = renderer; this->renderer = renderer;
this->screen = screen;
this->asset = asset;
this->input = input;
this->lang = lang;
this->section = section; this->section = section;
// Reserva memoria para los punteros // Texturas (handles compartidos de Resource)
Texture *item1 = new Texture(renderer, asset->get("item_points1_disk.png")); Resource *R = Resource::get();
itemTextures.push_back(item1); itemTextures.push_back(R->getTexture("item_points1_disk.png"));
itemTextures.push_back(R->getTexture("item_points2_gavina.png"));
Texture *item2 = new Texture(renderer, asset->get("item_points2_gavina.png")); itemTextures.push_back(R->getTexture("item_points3_pacmar.png"));
itemTextures.push_back(item2); itemTextures.push_back(R->getTexture("item_clock.png"));
itemTextures.push_back(R->getTexture("item_coffee.png"));
Texture *item3 = new Texture(renderer, asset->get("item_points3_pacmar.png")); itemTextures.push_back(R->getTexture("item_coffee_machine.png"));
itemTextures.push_back(item3);
Texture *item4 = new Texture(renderer, asset->get("item_clock.png"));
itemTextures.push_back(item4);
Texture *item5 = new Texture(renderer, asset->get("item_coffee.png"));
itemTextures.push_back(item5);
Texture *item6 = new Texture(renderer, asset->get("item_coffee_machine.png"));
itemTextures.push_back(item6);
eventHandler = new SDL_Event(); eventHandler = new SDL_Event();
sprite = new Sprite(0, 0, 16, 16, itemTextures[0], renderer); sprite = new Sprite(0, 0, 16, 16, itemTextures[0], renderer);
text = new Text(asset->get("smb2.png"), asset->get("smb2.txt"), renderer); text = R->getText("smb2");
// Crea un backbuffer para el renderizador // Crea un backbuffer para el renderizador
backbuffer = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT); backbuffer = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
@@ -62,31 +47,32 @@ Instructions::Instructions(SDL_Renderer *renderer, Screen *screen, Asset *asset,
} }
// Inicializa variables // Inicializa variables
section->name = SELF;
ticks = 0; ticks = 0;
ticksSpeed = 15; ticksSpeed = 15;
manualQuit = false; manualQuit = false;
counter = 0; counter = 0;
counterEnd = 600; counterEnd = 600;
finished = false;
quitRequested = false;
} }
// Destructor // Destructor
Instructions::~Instructions() { Instructions::~Instructions() {
for (auto texture : itemTextures) { // itemTextures y text son propiedad de Resource — no liberar.
texture->unload();
delete texture;
}
itemTextures.clear(); itemTextures.clear();
delete sprite; delete sprite;
delete eventHandler; delete eventHandler;
delete text;
SDL_DestroyTexture(backbuffer); SDL_DestroyTexture(backbuffer);
} }
// Actualiza las variables // Actualiza las variables
void Instructions::update() { void Instructions::update() {
// Bombea el stream de música: si no se llama, el buffer se vacía y la
// música se corta hasta que volvamos a una escena que sí lo haga.
Audio::update();
// Comprueba las entradas // Comprueba las entradas
checkInput(); checkInput();
@@ -99,15 +85,13 @@ void Instructions::update() {
counter++; counter++;
if (counter == counterEnd) { if (counter == counterEnd) {
section->name = SECTION_PROG_TITLE; finished = true;
section->subsection = SUBSECTION_TITLE_1;
} }
} else { // Modo manual } else { // Modo manual
++counter %= 60000; ++counter %= 60000;
if (manualQuit) { if (manualQuit) {
section->name = SECTION_PROG_TITLE; finished = true;
section->subsection = SUBSECTION_TITLE_3;
} }
} }
} }
@@ -133,21 +117,21 @@ void Instructions::render() {
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
// Escribe el texto // Escribe el texto
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 8, lang->getText(11), 1, orangeColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 8, Lang::get()->getText(11), 1, orangeColor, 1, shdwTxtColor);
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 24, lang->getText(12), 1, noColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 24, Lang::get()->getText(12), 1, noColor, 1, shdwTxtColor);
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 34, lang->getText(13), 1, noColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 34, Lang::get()->getText(13), 1, noColor, 1, shdwTxtColor);
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 48, lang->getText(14), 1, noColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 48, Lang::get()->getText(14), 1, noColor, 1, shdwTxtColor);
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 58, lang->getText(15), 1, noColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 58, Lang::get()->getText(15), 1, noColor, 1, shdwTxtColor);
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 75, lang->getText(16), 1, orangeColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, 75, Lang::get()->getText(16), 1, orangeColor, 1, shdwTxtColor);
text->writeShadowed(84, 92, lang->getText(17), shdwTxtColor); text->writeShadowed(84, 92, Lang::get()->getText(17), shdwTxtColor);
text->writeShadowed(84, 108, lang->getText(18), shdwTxtColor); text->writeShadowed(84, 108, Lang::get()->getText(18), shdwTxtColor);
text->writeShadowed(84, 124, lang->getText(19), shdwTxtColor); text->writeShadowed(84, 124, Lang::get()->getText(19), shdwTxtColor);
text->writeShadowed(84, 140, lang->getText(20), shdwTxtColor); text->writeShadowed(84, 140, Lang::get()->getText(20), shdwTxtColor);
text->writeShadowed(84, 156, lang->getText(21), shdwTxtColor); text->writeShadowed(84, 156, Lang::get()->getText(21), shdwTxtColor);
if ((mode == m_manual) && (counter % 50 > 14)) { if ((mode == m_manual) && (counter % 50 > 14)) {
text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, GAMECANVAS_HEIGHT - 12, lang->getText(22), 1, orangeColor, 1, shdwTxtColor); text->writeDX(TXT_CENTER | TXT_COLOR | TXT_SHADOW, GAMECANVAS_CENTER_X, GAMECANVAS_HEIGHT - 12, Lang::get()->getText(22), 1, orangeColor, 1, shdwTxtColor);
} }
// Disquito // Disquito
@@ -189,10 +173,10 @@ void Instructions::render() {
SDL_SetRenderTarget(renderer, nullptr); SDL_SetRenderTarget(renderer, nullptr);
// Prepara para empezar a dibujar en la textura de juego // Prepara para empezar a dibujar en la textura de juego
screen->start(); Screen::get()->start();
// Limpia la pantalla // Limpia la pantalla
screen->clean(bgColor); Screen::get()->clean(bgColor);
// Establece la ventana del backbuffer // Establece la ventana del backbuffer
if (mode == m_auto) { if (mode == m_auto) {
@@ -206,44 +190,38 @@ void Instructions::render() {
SDL_RenderTexture(renderer, backbuffer, nullptr, &fWindow); SDL_RenderTexture(renderer, backbuffer, nullptr, &fWindow);
// Vuelca el contenido del renderizador en pantalla // Vuelca el contenido del renderizador en pantalla
screen->blit(); Screen::get()->blit();
} }
// Comprueba los eventos // Comprueba los eventos
void Instructions::checkEvents() { void Instructions::checkEvents() {
#ifndef __EMSCRIPTEN__
// Comprueba los eventos que hay en la cola // Comprueba los eventos que hay en la cola
while (SDL_PollEvent(eventHandler) != 0) { while (SDL_PollEvent(eventHandler) != 0) {
// Evento de salida de la aplicación // Evento de salida de la aplicación
if (eventHandler->type == SDL_EVENT_QUIT) { if (eventHandler->type == SDL_EVENT_QUIT) {
section->name = SECTION_PROG_QUIT; quitRequested = true;
finished = true;
break; break;
} }
} }
#endif
} }
// Comprueba las entradas // Comprueba las entradas
void Instructions::checkInput() { void Instructions::checkInput() {
if (input->checkInput(input_exit, REPEAT_FALSE)) { #ifndef __EMSCRIPTEN__
section->name = SECTION_PROG_QUIT; if (Input::get()->checkInput(input_exit, REPEAT_FALSE)) {
quitRequested = true;
finished = true;
return;
} }
#endif
if (GlobalInputs::handle()) { return; }
else if (input->checkInput(input_window_fullscreen, REPEAT_FALSE)) { if (Input::get()->checkInput(input_pause, REPEAT_FALSE) || Input::get()->checkInput(input_accept, REPEAT_FALSE) || Input::get()->checkInput(input_fire_left, REPEAT_FALSE) || Input::get()->checkInput(input_fire_center, REPEAT_FALSE) || Input::get()->checkInput(input_fire_right, REPEAT_FALSE)) {
screen->switchVideoMode();
}
else if (input->checkInput(input_window_dec_size, REPEAT_FALSE)) {
screen->decWindowSize();
}
else if (input->checkInput(input_window_inc_size, REPEAT_FALSE)) {
screen->incWindowSize();
}
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
if (mode == m_auto) { if (mode == m_auto) {
JA_StopMusic(); finished = true;
section->name = SECTION_PROG_TITLE;
section->subsection = SUBSECTION_TITLE_1;
} else { } else {
if (counter > 30) { if (counter > 30) {
manualQuit = true; manualQuit = true;
@@ -252,13 +230,41 @@ void Instructions::checkInput() {
} }
} }
// Bucle para la pantalla de instrucciones // Bucle para la pantalla de instrucciones (compatibilidad)
void Instructions::run(mode_e mode) { void Instructions::run(mode_e mode) {
this->mode = mode; start(mode);
while (section->name == SELF) { while (!finished) {
update(); update();
checkEvents(); checkEvents();
render(); render();
} }
// Aplica los cambios de sección según el resultado
if (quitRequested) {
section->name = SECTION_PROG_QUIT;
} else {
section->name = SECTION_PROG_TITLE;
section->subsection = (mode == m_auto) ? SUBSECTION_TITLE_1 : SUBSECTION_TITLE_3;
}
}
// Inicia las instrucciones (sin bucle)
void Instructions::start(mode_e mode) {
this->mode = mode;
finished = false;
quitRequested = false;
manualQuit = false;
counter = 0;
ticks = 0;
}
// Indica si las instrucciones han terminado
bool Instructions::hasFinished() const {
return finished;
}
// Indica si se ha solicitado salir de la aplicación
bool Instructions::isQuitRequested() const {
return quitRequested;
} }

View File

@@ -3,10 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <vector> // for vector #include <vector> // for vector
class Asset;
class Input;
class Lang;
class Screen;
class Sprite; class Sprite;
class Text; class Text;
class Texture; class Texture;
@@ -22,24 +18,41 @@ class Instructions {
private: private:
// Objetos y punteros // Objetos y punteros
SDL_Renderer *renderer; // El renderizador de la ventana SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla
std::vector<Texture *> itemTextures; // Vector con las texturas de los items std::vector<Texture *> itemTextures; // Vector con las texturas de los items
SDL_Event *eventHandler; // Manejador de eventos SDL_Event *eventHandler; // Manejador de eventos
SDL_Texture *backbuffer; // Textura para usar como backbuffer SDL_Texture *backbuffer; // Textura para usar como backbuffer
Sprite *sprite; // Sprite con la textura de las instrucciones Sprite *sprite; // Sprite con la textura de las instrucciones
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
Input *input; // Objeto pata gestionar la entrada
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
Text *text; // Objeto para escribir texto Text *text; // Objeto para escribir texto
section_t *section; // Estado del bucle principal para saber si continua o se sale section_t *section; // Estado del bucle principal para saber si continua o se sale
// Variables // Variables
Uint16 counter; // Contador Uint16 counter; // Contador
Uint16 counterEnd; // Valor final para el contador Uint16 counterEnd; // Valor final para el contador
Uint32 ticks; // Contador de ticks para ajustar la velocidad del programa Uint32 ticks; // Contador de ticks para ajustar la velocidad del programa
Uint32 ticksSpeed; // Velocidad a la que se repiten los bucles del programa Uint32 ticksSpeed; // Velocidad a la que se repiten los bucles del programa
bool manualQuit; // Indica si se quiere salir del modo manual bool manualQuit; // Indica si se quiere salir del modo manual
mode_e mode; // Modo en el que se van a ejecutar las instrucciones mode_e mode{m_auto}; // Modo en el que se van a ejecutar las instrucciones
bool finished; // Indica si las instrucciones han terminado
bool quitRequested; // Indica si se ha solicitado salir de la aplicación
// Comprueba las entradas
void checkInput();
public:
// Constructor
Instructions(SDL_Renderer *renderer, section_t *section);
// Destructor
~Instructions();
Instructions(const Instructions &) = delete;
Instructions &operator=(const Instructions &) = delete;
// Bucle principal
void run(mode_e mode);
// Inicia las instrucciones (sin bucle)
void start(mode_e mode);
// Actualiza las variables // Actualiza las variables
void update(); void update();
@@ -50,16 +63,9 @@ class Instructions {
// Comprueba los eventos // Comprueba los eventos
void checkEvents(); void checkEvents();
// Comprueba las entradas // Indica si las instrucciones han terminado
void checkInput(); bool hasFinished() const;
public: // Indica si se ha solicitado salir de la aplicación
// Constructor bool isQuitRequested() const;
Instructions(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, Lang *lang, section_t *section);
// Destructor
~Instructions();
// Bucle principal
void run(mode_e mode);
}; };

View File

@@ -1,38 +1,33 @@
#include "intro.h" #include "game/scenes/intro.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <string> // for basic_string #include <string> // for basic_string
#include "asset.h" // for Asset #include "core/audio/audio.hpp" // for Audio::get, Audio::update
#include "const.h" // for GAMECANVAS_CENTER_X, GAMECANVAS_FIRST_QU... #include "core/input/global_inputs.hpp" // for GlobalInputs::handle
#include "input.h" // for Input, REPEAT_FALSE, inputs_e #include "core/input/input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_StopMusic, JA_DeleteMusic, JA_LoadMusic #include "core/locale/lang.h" // for Lang
#include "lang.h" // for Lang #include "core/rendering/screen.h" // for Screen
#include "screen.h" // for Screen #include "core/rendering/smartsprite.h" // for SmartSprite
#include "smartsprite.h" // for SmartSprite #include "core/rendering/text.h" // for Text
#include "text.h" // for Text #include "core/rendering/texture.h" // for Texture
#include "texture.h" // for Texture #include "core/rendering/writer.h" // for Writer
#include "utils.h" // for section_t, color_t #include "core/resources/resource.h"
#include "writer.h" // for Writer #include "game/defaults.hpp" // for GAMECANVAS_CENTER_X, GAMECANVAS_FIRST_QU...
#include "utils/utils.h" // for section_t, color_t
// Constructor // Constructor
Intro::Intro(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, Lang *lang, section_t *section) { Intro::Intro(SDL_Renderer *renderer, section_t *section) {
// Copia los punteros // Copia los punteros
this->renderer = renderer; this->renderer = renderer;
this->screen = screen;
this->lang = lang;
this->asset = asset;
this->input = input;
this->section = section; this->section = section;
// Reserva memoria para los objetos // Reserva memoria para los objetos
eventHandler = new SDL_Event(); eventHandler = new SDL_Event();
texture = new Texture(renderer, asset->get("intro.png")); texture = Resource::get()->getTexture("intro.png");
text = new Text(asset->get("nokia.png"), asset->get("nokia.txt"), renderer); text = Resource::get()->getText("nokia");
music = Resource::get()->getMusic("intro.ogg");
// Carga los recursos
loadMedia();
// Inicializa variables // Inicializa variables
section->name = SECTION_PROG_INTRO; section->name = SECTION_PROG_INTRO;
@@ -115,104 +110,74 @@ Intro::Intro(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input,
} }
// Un dia qualsevol de l'any 2000 // Un dia qualsevol de l'any 2000
texts[0]->setCaption(lang->getText(27)); texts[0]->setCaption(Lang::get()->getText(27));
texts[0]->setSpeed(8); texts[0]->setSpeed(8);
// Tot esta tranquil a la UPV // Tot esta tranquil a la UPV
texts[1]->setCaption(lang->getText(28)); texts[1]->setCaption(Lang::get()->getText(28));
texts[1]->setSpeed(8); texts[1]->setSpeed(8);
// Fins que un desaprensiu... // Fins que un desaprensiu...
texts[2]->setCaption(lang->getText(29)); texts[2]->setCaption(Lang::get()->getText(29));
texts[2]->setSpeed(12); texts[2]->setSpeed(12);
// HEY! ME ANE A FERME UN CORTAET... // HEY! ME ANE A FERME UN CORTAET...
texts[3]->setCaption(lang->getText(30)); texts[3]->setCaption(Lang::get()->getText(30));
texts[3]->setSpeed(8); texts[3]->setSpeed(8);
// UAAAAAAAAAAAAA!!! // UAAAAAAAAAAAAA!!!
texts[4]->setCaption(lang->getText(31)); texts[4]->setCaption(Lang::get()->getText(31));
texts[4]->setSpeed(1); texts[4]->setSpeed(1);
// Espera un moment... // Espera un moment...
texts[5]->setCaption(lang->getText(32)); texts[5]->setCaption(Lang::get()->getText(32));
texts[5]->setSpeed(16); texts[5]->setSpeed(16);
// Si resulta que no tinc solt! // Si resulta que no tinc solt!
texts[6]->setCaption(lang->getText(33)); texts[6]->setCaption(Lang::get()->getText(33));
texts[6]->setSpeed(2); texts[6]->setSpeed(2);
// MERDA DE MAQUINA! // MERDA DE MAQUINA!
texts[7]->setCaption(lang->getText(34)); texts[7]->setCaption(Lang::get()->getText(34));
texts[7]->setSpeed(3); texts[7]->setSpeed(3);
// Blop... blop... blop... // Blop... blop... blop...
texts[8]->setCaption(lang->getText(35)); texts[8]->setCaption(Lang::get()->getText(35));
texts[8]->setSpeed(16); texts[8]->setSpeed(16);
for (auto text : texts) { for (auto *t : texts) {
text->center(GAMECANVAS_CENTER_X); t->center(GAMECANVAS_CENTER_X);
} }
Audio::get()->playMusic(music, 0);
} }
// Destructor // Destructor
Intro::~Intro() { Intro::~Intro() {
delete eventHandler; delete eventHandler;
texture->unload(); // texture, text, music son propiedad de Resource — no liberar aquí.
delete texture;
for (auto bitmap : bitmaps) { for (auto bitmap : bitmaps) {
delete bitmap; delete bitmap;
} }
for (auto text : texts) { for (auto t : texts) {
delete text; delete t;
}
JA_DeleteMusic(music);
}
// Carga los recursos
bool Intro::loadMedia() {
// Musicas
music = JA_LoadMusic(asset->get("intro.ogg").c_str());
return true;
}
// Comprueba los eventos
void Intro::checkEvents() {
// Comprueba los eventos que hay en la cola
while (SDL_PollEvent(eventHandler) != 0) {
// Evento de salida de la aplicación
if (eventHandler->type == SDL_EVENT_QUIT) {
section->name = SECTION_PROG_QUIT;
break;
}
} }
} }
// Comprueba las entradas // Comprueba las entradas
void Intro::checkInput() { void Intro::checkInput() {
if (input->checkInput(input_exit, REPEAT_FALSE)) { #ifndef __EMSCRIPTEN__
if (Input::get()->checkInput(input_exit, REPEAT_FALSE)) {
section->name = SECTION_PROG_QUIT; section->name = SECTION_PROG_QUIT;
return;
} }
#endif
if (GlobalInputs::handle()) { return; }
else if (input->checkInput(input_window_fullscreen, REPEAT_FALSE)) { if (Input::get()->checkInput(input_pause, REPEAT_FALSE) || Input::get()->checkInput(input_accept, REPEAT_FALSE) || Input::get()->checkInput(input_fire_left, REPEAT_FALSE) || Input::get()->checkInput(input_fire_center, REPEAT_FALSE) || Input::get()->checkInput(input_fire_right, REPEAT_FALSE)) {
screen->switchVideoMode(); Audio::get()->stopMusic();
}
else if (input->checkInput(input_window_dec_size, REPEAT_FALSE)) {
screen->decWindowSize();
}
else if (input->checkInput(input_window_inc_size, REPEAT_FALSE)) {
screen->incWindowSize();
}
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
JA_StopMusic();
section->name = SECTION_PROG_TITLE; section->name = SECTION_PROG_TITLE;
section->subsection = SUBSECTION_TITLE_1; section->subsection = SUBSECTION_TITLE_1;
} }
@@ -342,7 +307,7 @@ void Intro::updateScenes() {
if (bitmaps[5]->hasFinished() && texts[8]->hasFinished()) { if (bitmaps[5]->hasFinished() && texts[8]->hasFinished()) {
bitmaps[5]->setEnabled(false); bitmaps[5]->setEnabled(false);
texts[8]->setEnabled(false); texts[8]->setEnabled(false);
JA_StopMusic(); Audio::get()->stopMusic();
section->name = SECTION_PROG_TITLE; section->name = SECTION_PROG_TITLE;
section->subsection = SUBSECTION_TITLE_1; section->subsection = SUBSECTION_TITLE_1;
} }
@@ -356,7 +321,7 @@ void Intro::updateScenes() {
// Actualiza las variables del objeto // Actualiza las variables del objeto
void Intro::update() { void Intro::update() {
JA_Update(); Audio::update();
checkInput(); checkInput();
if (SDL_GetTicks() - ticks > ticksSpeed) { if (SDL_GetTicks() - ticks > ticksSpeed) {
@@ -368,8 +333,8 @@ void Intro::update() {
bitmap->update(); bitmap->update();
} }
for (auto text : texts) { for (auto *t : texts) {
text->update(); t->update();
} }
// Actualiza las escenas de la intro // Actualiza las escenas de la intro
@@ -380,31 +345,40 @@ void Intro::update() {
// Dibuja el objeto en pantalla // Dibuja el objeto en pantalla
void Intro::render() { void Intro::render() {
// Prepara para empezar a dibujar en la textura de juego // Prepara para empezar a dibujar en la textura de juego
screen->start(); Screen::get()->start();
// Limpia la pantalla // Limpia la pantalla
screen->clean(bgColor); Screen::get()->clean(bgColor);
// Dibuja los objetos // Dibuja los objetos
for (auto bitmap : bitmaps) { for (auto bitmap : bitmaps) {
bitmap->render(); bitmap->render();
} }
for (auto text : texts) { for (auto *t : texts) {
text->render(); t->render();
} }
// Vuelca el contenido del renderizador en pantalla // Vuelca el contenido del renderizador en pantalla
screen->blit(); Screen::get()->blit();
} }
// Bucle principal // Bucle principal
void Intro::run() { void Intro::run() {
JA_PlayMusic(music, 0); Audio::get()->playMusic(music, 0);
while (section->name == SECTION_PROG_INTRO) { while (section->name == SECTION_PROG_INTRO) {
update(); iterate();
checkEvents();
render();
} }
} }
// Ejecuta un frame
void Intro::iterate() {
update();
render();
}
// Procesa un evento individual
void Intro::handleEvent(const SDL_Event *event) {
// SDL_EVENT_QUIT ya lo maneja Director
}

View File

@@ -3,10 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <vector> // for vector #include <vector> // for vector
class Asset;
class Input;
class Lang;
class Screen;
class SmartSprite; class SmartSprite;
class Text; class Text;
class Texture; class Texture;
@@ -19,12 +15,8 @@ class Intro {
private: private:
// Objetos y punteros // Objetos y punteros
SDL_Renderer *renderer; // El renderizador de la ventana SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla
Texture *texture; // Textura con los graficos Texture *texture; // Textura con los graficos
SDL_Event *eventHandler; // Manejador de eventos SDL_Event *eventHandler; // Manejador de eventos
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
Input *input; // Objeto pata gestionar la entrada
std::vector<SmartSprite *> bitmaps; // Vector con los sprites inteligentes para los dibujos de la intro std::vector<SmartSprite *> bitmaps; // Vector con los sprites inteligentes para los dibujos de la intro
std::vector<Writer *> texts; // Textos de la intro std::vector<Writer *> texts; // Textos de la intro
Text *text; // Textos de la intro Text *text; // Textos de la intro
@@ -42,12 +34,6 @@ class Intro {
// Dibuja el objeto en pantalla // Dibuja el objeto en pantalla
void render(); void render();
// Carga los recursos
bool loadMedia();
// Comprueba los eventos
void checkEvents();
// Comprueba las entradas // Comprueba las entradas
void checkInput(); void checkInput();
@@ -56,11 +42,20 @@ class Intro {
public: public:
// Constructor // Constructor
Intro(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, Lang *lang, section_t *section); Intro(SDL_Renderer *renderer, section_t *section);
// Destructor // Destructor
~Intro(); ~Intro();
Intro(const Intro &) = delete;
Intro &operator=(const Intro &) = delete;
// Bucle principal // Bucle principal
void run(); void run();
// Ejecuta un frame
void iterate();
// Procesa un evento
void handleEvent(const SDL_Event *event);
}; };

View File

@@ -1,35 +1,34 @@
#include "logo.h" #include "game/scenes/logo.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> // for min #include <algorithm> // for min
#include <string> // for basic_string #include <string> // for basic_string
#include "asset.h" // for Asset #include "core/audio/audio.hpp" // for Audio::get, Audio::update
#include "const.h" // for bgColor, SECTION_PROG_LOGO, SECTION_PROG... #include "core/input/global_inputs.hpp" // for GlobalInputs::handle
#include "input.h" // for Input, REPEAT_FALSE, inputs_e #include "core/input/input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_StopMusic #include "core/rendering/screen.h" // for Screen
#include "screen.h" // for Screen #include "core/rendering/sprite.h" // for Sprite
#include "sprite.h" // for Sprite #include "core/rendering/texture.h" // for Texture
#include "texture.h" // for Texture #include "core/resources/asset.h" // for Asset
#include "utils.h" // for section_t, color_t #include "core/resources/resource.h"
#include "game/defaults.hpp" // for bgColor, SECTION_PROG_LOGO, SECTION_PROG...
#include "utils/utils.h" // for section_t, color_t
// Valores de inicialización y fin // Valores de inicialización y fin
constexpr int INIT_FADE = 100; constexpr int INIT_FADE = 100;
constexpr int END_LOGO = 200; constexpr int END_LOGO = 200;
// Constructor // Constructor
Logo::Logo(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, section_t *section) { Logo::Logo(SDL_Renderer *renderer, section_t *section) {
// Copia la dirección de los objetos // Copia la dirección de los objetos
this->renderer = renderer; this->renderer = renderer;
this->screen = screen;
this->asset = asset;
this->input = input;
this->section = section; this->section = section;
// Reserva memoria para los punteros // Reserva memoria para los punteros
eventHandler = new SDL_Event(); eventHandler = new SDL_Event();
texture = new Texture(renderer, asset->get("logo.png")); texture = Resource::get()->getTexture("logo.png");
sprite = new Sprite(14, 75, 226, 44, texture, renderer); sprite = new Sprite(14, 75, 226, 44, texture, renderer);
// Inicializa variables // Inicializa variables
@@ -38,13 +37,13 @@ Logo::Logo(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, s
section->subsection = 0; section->subsection = 0;
ticks = 0; ticks = 0;
ticksSpeed = 15; ticksSpeed = 15;
Audio::get()->stopMusic();
} }
// Destructor // Destructor
Logo::~Logo() { Logo::~Logo() {
texture->unload(); // texture es propiedad de Resource — no liberar aquí.
delete texture;
delete sprite; delete sprite;
delete eventHandler; delete eventHandler;
} }
@@ -57,37 +56,17 @@ void Logo::checkLogoEnd() {
} }
} }
// Comprueba los eventos
void Logo::checkEvents() {
// Comprueba los eventos que hay en la cola
while (SDL_PollEvent(eventHandler) != 0) {
// Evento de salida de la aplicación
if (eventHandler->type == SDL_EVENT_QUIT) {
section->name = SECTION_PROG_QUIT;
break;
}
}
}
// Comprueba las entradas // Comprueba las entradas
void Logo::checkInput() { void Logo::checkInput() {
if (input->checkInput(input_exit, REPEAT_FALSE)) { #ifndef __EMSCRIPTEN__
if (Input::get()->checkInput(input_exit, REPEAT_FALSE)) {
section->name = SECTION_PROG_QUIT; section->name = SECTION_PROG_QUIT;
return;
} }
#endif
if (GlobalInputs::handle()) { return; }
else if (input->checkInput(input_window_fullscreen, REPEAT_FALSE)) { if (Input::get()->checkInput(input_pause, REPEAT_FALSE) || Input::get()->checkInput(input_accept, REPEAT_FALSE) || Input::get()->checkInput(input_fire_left, REPEAT_FALSE) || Input::get()->checkInput(input_fire_center, REPEAT_FALSE) || Input::get()->checkInput(input_fire_right, REPEAT_FALSE)) {
screen->switchVideoMode();
}
else if (input->checkInput(input_window_dec_size, REPEAT_FALSE)) {
screen->decWindowSize();
}
else if (input->checkInput(input_window_inc_size, REPEAT_FALSE)) {
screen->incWindowSize();
}
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
section->name = SECTION_PROG_TITLE; section->name = SECTION_PROG_TITLE;
section->subsection = SUBSECTION_TITLE_1; section->subsection = SUBSECTION_TITLE_1;
} }
@@ -106,7 +85,7 @@ void Logo::renderFade() {
// Actualiza las variables del objeto // Actualiza las variables del objeto
void Logo::update() { void Logo::update() {
JA_Update(); Audio::update();
checkInput(); checkInput();
if (SDL_GetTicks() - ticks > ticksSpeed) { if (SDL_GetTicks() - ticks > ticksSpeed) {
@@ -124,10 +103,10 @@ void Logo::update() {
// Dibuja el objeto en pantalla // Dibuja el objeto en pantalla
void Logo::render() { void Logo::render() {
// Prepara para empezar a dibujar en la textura de juego // Prepara para empezar a dibujar en la textura de juego
screen->start(); Screen::get()->start();
// Limpia la pantalla // Limpia la pantalla
screen->clean({238, 238, 238}); Screen::get()->clean({238, 238, 238});
// Dibuja los objetos // Dibuja los objetos
sprite->render(); sprite->render();
@@ -136,16 +115,25 @@ void Logo::render() {
renderFade(); renderFade();
// Vuelca el contenido del renderizador en pantalla // Vuelca el contenido del renderizador en pantalla
screen->blit(); Screen::get()->blit();
} }
// Bucle para el logo del juego // Bucle para el logo del juego
void Logo::run() { void Logo::run() {
JA_StopMusic(); Audio::get()->stopMusic();
while (section->name == SECTION_PROG_LOGO) { while (section->name == SECTION_PROG_LOGO) {
update(); iterate();
checkEvents();
render();
} }
} }
// Ejecuta un frame
void Logo::iterate() {
update();
render();
}
// Procesa un evento individual
void Logo::handleEvent(const SDL_Event *event) {
// SDL_EVENT_QUIT ya lo maneja Director
}

View File

@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
class Asset;
class Input;
class Screen;
class Sprite; class Sprite;
class Texture; class Texture;
struct section_t; struct section_t;
@@ -13,9 +10,6 @@ class Logo {
private: private:
// Objetos y punteros // Objetos y punteros
SDL_Renderer *renderer; // El renderizador de la ventana SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
Input *input; // Objeto pata gestionar la entrada
Texture *texture; // Textura con los graficos Texture *texture; // Textura con los graficos
SDL_Event *eventHandler; // Manejador de eventos SDL_Event *eventHandler; // Manejador de eventos
Sprite *sprite; // Sprite con la textura del logo Sprite *sprite; // Sprite con la textura del logo
@@ -35,9 +29,6 @@ class Logo {
// Comprueba si ha terminado el logo // Comprueba si ha terminado el logo
void checkLogoEnd(); void checkLogoEnd();
// Comprueba los eventos
void checkEvents();
// Comprueba las entradas // Comprueba las entradas
void checkInput(); void checkInput();
@@ -46,11 +37,20 @@ class Logo {
public: public:
// Constructor // Constructor
Logo(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, section_t *section); Logo(SDL_Renderer *renderer, section_t *section);
// Destructor // Destructor
~Logo(); ~Logo();
Logo(const Logo &) = delete;
Logo &operator=(const Logo &) = delete;
// Bucle principal // Bucle principal
void run(); void run();
// Ejecuta un frame
void iterate();
// Procesa un evento
void handleEvent(const SDL_Event *event);
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -4,16 +4,13 @@
#include <vector> // for vector #include <vector> // for vector
#include "instructions.h" // for mode_e #include "game/options.hpp" // for Options::Video, Options::Window (per snapshot cancel)
#include "utils.h" // for input_t, options_t, section_t #include "game/scenes/instructions.h" // for mode_e
#include "utils/utils.h" // for input_t, section_t
class AnimatedSprite; class AnimatedSprite;
class Asset;
class Fade; class Fade;
class Game; class Game;
class Input;
class Lang;
class Menu; class Menu;
class Screen;
class SmartSprite; class SmartSprite;
class Sprite; class Sprite;
class Text; class Text;
@@ -22,7 +19,7 @@ struct JA_Music_t;
struct JA_Sound_t; struct JA_Sound_t;
// Textos // Textos
constexpr const char *TEXT_COPYRIGHT = "@2020 JailDesigner (v2.3.3)"; constexpr const char *TEXT_COPYRIGHT = "@2020 JailDesigner (v2.3.4)";
// Contadores // Contadores
constexpr int TITLE_COUNTER = 800; constexpr int TITLE_COUNTER = 800;
@@ -41,15 +38,11 @@ class Title {
}; };
// Objetos y punteros // Objetos y punteros
SDL_Renderer *renderer; // El renderizador de la ventana SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla Instructions *instructions{nullptr}; // Objeto para la sección de las instrucciones
Asset *asset; // Objeto que gestiona todos los ficheros de recursos Game *demoGame{nullptr}; // Objeto para lanzar la demo del juego
Input *input; // Objeto para leer las entradas de teclado o mando SDL_Event *eventHandler; // Manejador de eventos
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas section_t *section; // Indicador para el bucle del titulo
Instructions *instructions; // Objeto para la sección de las instrucciones
Game *demoGame; // Objeto para lanzar la demo del juego
SDL_Event *eventHandler; // Manejador de eventos
section_t *section; // Indicador para el bucle del titulo
Texture *dustTexture; // Textura con los graficos del polvo Texture *dustTexture; // Textura con los graficos del polvo
Texture *coffeeTexture; // Textura con los graficos de la palabra coffee Texture *coffeeTexture; // Textura con los graficos de la palabra coffee
@@ -72,24 +65,39 @@ class Title {
Fade *fade; // Objeto para realizar fundidos en pantalla Fade *fade; // Objeto para realizar fundidos en pantalla
// Variable // Variable
JA_Music_t *titleMusic; // Musica para el titulo JA_Music_t *titleMusic; // Musica para el titulo
JA_Sound_t *crashSound; // Sonido con el impacto del título JA_Sound_t *crashSound; // Sonido con el impacto del título
int backgroundCounter; // Temporizador para el fondo de tiles de la pantalla de titulo int backgroundCounter; // Temporizador para el fondo de tiles de la pantalla de titulo
int counter; // Temporizador para la pantalla de titulo int counter; // Temporizador para la pantalla de titulo
Uint32 ticks; // Contador de ticks para ajustar la velocidad del programa Uint32 ticks; // Contador de ticks para ajustar la velocidad del programa
Uint8 backgroundMode; // Variable para almacenar el tipo de efecto que hará el fondo de la pantalla de titulo Uint8 backgroundMode; // Variable para almacenar el tipo de efecto que hará el fondo de la pantalla de titulo
float sin[360]; // Vector con los valores del seno precalculados float sin[360]; // Vector con los valores del seno precalculados
bool menuVisible; // Indicador para saber si se muestra el menu del titulo o la frase intermitente bool menuVisible; // Indicador para saber si se muestra el menu del titulo o la frase intermitente
bool demo; // Indica si el modo demo estará activo bool demo; // Indica si el modo demo estará activo
section_t nextSection; // Indica cual es la siguiente sección a cargar cuando termine el contador del titulo section_t nextSection; // Indica cual es la siguiente sección a cargar cuando termine el contador del titulo
Uint32 ticksSpeed; // Velocidad a la que se repiten los bucles del programa Uint32 ticksSpeed; // Velocidad a la que se repiten los bucles del programa
Uint8 postFade; // Opción a realizar cuando termina el fundido Uint8 postFade; // Opción a realizar cuando termina el fundido
menu_t menu; // Variable con todos los objetos menus y sus variables menu_t menu; // Variable con todos los objetos menus y sus variables
struct options_t *options; // Variable con todas las variables de las opciones del programa // Snapshot per a permetre CANCEL al menú d'opcions.
options_t optionsPrevious; // Variable de respaldo para las opciones Options::Video prevVideo;
Options::Window prevWindow;
Options::Settings prevSettings;
std::vector<input_t> prevInputs;
std::vector<input_t> availableInputDevices; // Vector con todos los metodos de control disponibles std::vector<input_t> availableInputDevices; // Vector con todos los metodos de control disponibles
std::vector<int> deviceIndex; // Indice para el jugador [i] del vector de dispositivos de entrada disponibles std::vector<int> deviceIndex; // Indice para el jugador [i] del vector de dispositivos de entrada disponibles
// Variables para la vibración del título (SUBSECTION_TITLE_2)
int vibrationStep; // Paso actual de la vibración
int vibrationCoffeeBaseX{0}; // Posición X base del bitmap Coffee
int vibrationCrisisBaseX{0}; // Posición X base del bitmap Crisis
bool vibrationInitialized; // Indica si se han capturado las posiciones base
// Variables para sub-estados delegados (instrucciones y demo)
bool instructionsActive; // Indica si las instrucciones están activas
bool demoGameActive; // Indica si el juego demo está activo
mode_e instructionsMode{m_auto}; // Modo de las instrucciones activas
bool demoThenInstructions; // Indica si tras la demo hay que mostrar instrucciones
// Inicializa los valores // Inicializa los valores
void init(); void init();
@@ -99,9 +107,6 @@ class Title {
// Dibuja el objeto en pantalla // Dibuja el objeto en pantalla
void render(); void render();
// Comprueba los eventos
void checkEvents();
// Comprueba las entradas // Comprueba las entradas
void checkInput(); void checkInput();
@@ -137,11 +142,20 @@ class Title {
public: public:
// Constructor // Constructor
Title(SDL_Renderer *renderer, Screen *screen, Input *input, Asset *asset, options_t *options, Lang *lang, section_t *section); Title(SDL_Renderer *renderer, section_t *section);
// Destructor // Destructor
~Title(); ~Title();
Title(const Title &) = delete;
Title &operator=(const Title &) = delete;
// Bucle para el titulo del juego // Bucle para el titulo del juego
void run(); void run();
// Ejecuta un frame
void iterate();
// Procesa un evento
void handleEvent(const SDL_Event *event);
}; };

View File

@@ -1,20 +1,24 @@
#include "menu.h" #include "game/ui/menu.h"
#include <algorithm> // for max, min #include <algorithm> // for max, min
#include <fstream> // for char_traits, basic_ifstream, basic_istream #include <fstream> // for char_traits, basic_ifstream, basic_istream
#include <numeric> // for accumulate
#include <sstream> // for basic_stringstream #include <sstream> // for basic_stringstream
#include "asset.h" // for Asset #include "core/audio/audio.hpp" // for Audio::get (playSound)
#include "input.h" // for Input, REPEAT_FALSE, inputs_e #include "core/audio/jail_audio.hpp" // for JA_LoadSound, JA_DeleteSound (propietat local)
#include "jail_audio.hpp" // for JA_LoadSound, JA_PlaySound, JA_DeleteSound #include "core/input/input.h" // for Input, REPEAT_FALSE, inputs_e
#include "text.h" // for Text #include "core/rendering/text.h" // for Text
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource_helper.h"
// Constructor // Constructor
Menu::Menu(SDL_Renderer *renderer, Asset *asset, Input *input, std::string file) { Menu::Menu(SDL_Renderer *renderer, const std::string &file)
: colorGreyed{128, 128, 128},
font_png(""),
font_txt("") {
// Copia punteros // Copia punteros
this->renderer = renderer; this->renderer = renderer;
this->asset = asset;
this->input = input;
// Inicializa punteros // Inicializa punteros
soundMove = nullptr; soundMove = nullptr;
@@ -22,7 +26,6 @@ Menu::Menu(SDL_Renderer *renderer, Asset *asset, Input *input, std::string file)
soundCancel = nullptr; soundCancel = nullptr;
// Inicializa variables // Inicializa variables
name = "";
selector.index = 0; selector.index = 0;
selector.previousIndex = 0; selector.previousIndex = 0;
itemSelected = MENU_NO_OPTION; itemSelected = MENU_NO_OPTION;
@@ -39,10 +42,7 @@ Menu::Menu(SDL_Renderer *renderer, Asset *asset, Input *input, std::string file)
centerX = 0; centerX = 0;
centerY = 0; centerY = 0;
widestItem = 0; widestItem = 0;
colorGreyed = {128, 128, 128};
defaultActionWhenCancel = 0; defaultActionWhenCancel = 0;
font_png = "";
font_txt = "";
// Selector // Selector
selector.originY = 0; selector.originY = 0;
@@ -61,22 +61,18 @@ Menu::Menu(SDL_Renderer *renderer, Asset *asset, Input *input, std::string file)
selector.itemColor = {0, 0, 0}; selector.itemColor = {0, 0, 0};
selector.a = 255; selector.a = 255;
// Inicializa las variables desde un fichero // Inicializa las variables desde un fichero. Si no se pasa fichero, el
if (file != "") { // llamante (p.ej. Resource::preloadAll) usará loadFromBytes después —
// y ese método ya llama a setSelectorItemColors() y reset() al final.
if (!file.empty()) {
load(file); load(file);
setSelectorItemColors();
reset();
} }
// Calcula los colores del selector para el degradado
setSelectorItemColors();
// Deja el cursor en el primer elemento
reset();
} }
Menu::~Menu() { Menu::~Menu() {
renderer = nullptr; renderer = nullptr;
asset = nullptr;
input = nullptr;
if (soundMove) { if (soundMove) {
JA_DeleteSound(soundMove); JA_DeleteSound(soundMove);
@@ -95,84 +91,75 @@ Menu::~Menu() {
} }
} }
// Carga la configuración del menu desde un archivo de texto // Parser compartido (recibe cualquier istream)
bool Menu::load(std::string file_path) { bool Menu::parseFromStream(std::istream &file, const std::string &filename) {
// Indicador de éxito en la carga
bool success = true; bool success = true;
// Indica si se ha creado ya el objeto de texto
bool textAllocated = false; bool textAllocated = false;
const std::string filename = file_path.substr(file_path.find_last_of("\\/") + 1);
std::string line; std::string line;
std::ifstream file(file_path); (void)filename;
// El fichero se puede abrir while (std::getline(file, line)) {
if (file.good()) { if (line == "[item]") {
// Procesa el fichero linea a linea item_t newItem;
// std::cout << "Reading file " << filename.c_str() << std::endl; newItem.label = "";
while (std::getline(file, line)) { newItem.hPaddingDown = 1;
if (line == "[item]") { newItem.selectable = true;
item_t item; newItem.greyed = false;
item.label = ""; newItem.linkedDown = false;
item.hPaddingDown = 1; newItem.visible = true;
item.selectable = true; newItem.line = false;
item.greyed = false;
item.linkedDown = false;
item.visible = true;
item.line = false;
do { do {
// Lee la siguiente linea std::getline(file, line);
std::getline(file, line);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (!setItem(&item, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
// std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
success = false;
}
} while (line != "[/item]");
addItem(item);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
int pos = line.find("="); int pos = line.find("=");
// Procesa las dos subcadenas if (!setItem(&newItem, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
if (!setVars(line.substr(0, pos), line.substr(pos + 1, line.length()))) {
// std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
success = false; success = false;
} }
} while (line != "[/item]");
// Crea el objeto text tan pronto como se pueda. Necesario para añadir items addItem(newItem);
if (font_png != "" && font_txt != "" && !textAllocated) { } else {
text = new Text(asset->get(font_png), asset->get(font_txt), renderer); int pos = line.find("=");
textAllocated = true; if (!setVars(line.substr(0, pos), line.substr(pos + 1, line.length()))) {
} success = false;
}
// Crea el objeto text tan pronto como se pueda. Necesario para añadir items.
// Carga via ResourceHelper para que funcione tanto con pack como con filesystem.
if (font_png != "" && font_txt != "" && !textAllocated) {
auto pngBytes = ResourceHelper::loadFile(Asset::get()->get(font_png));
auto txtBytes = ResourceHelper::loadFile(Asset::get()->get(font_txt));
text = new Text(pngBytes, txtBytes, renderer);
textAllocated = true;
} }
} }
// Cierra el fichero
// std::cout << "Closing file " << filename.c_str() << std::endl;
file.close();
} }
// El fichero no se puede abrir
else {
// std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
success = false;
}
return success; return success;
} }
// Carga la configuración del menu desde un archivo de texto
bool Menu::load(const std::string &file_path) {
const std::string filename = file_path.substr(file_path.find_last_of("\\/") + 1);
std::ifstream file(file_path);
if (!file.good()) {
return false;
}
return parseFromStream(file, filename);
}
// Carga el menu desde bytes en memoria
bool Menu::loadFromBytes(const std::vector<uint8_t> &bytes, const std::string &nameForLogs) {
if (bytes.empty()) return false;
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
bool ok = parseFromStream(ss, nameForLogs);
setSelectorItemColors();
reset();
return ok;
}
// Asigna variables a partir de dos cadenas // Asigna variables a partir de dos cadenas
bool Menu::setItem(item_t *item, std::string var, std::string value) { bool Menu::setItem(item_t *item, const std::string &var, const std::string &value) {
// Indicador de éxito en la asignación // Indicador de éxito en la asignación
bool success = true; bool success = true;
@@ -215,7 +202,7 @@ bool Menu::setItem(item_t *item, std::string var, std::string value) {
} }
// Asigna variables a partir de dos cadenas // Asigna variables a partir de dos cadenas
bool Menu::setVars(std::string var, std::string value) { bool Menu::setVars(const std::string &var, const std::string &value) {
// Indicador de éxito en la asignación // Indicador de éxito en la asignación
bool success = true; bool success = true;
@@ -228,15 +215,18 @@ bool Menu::setVars(std::string var, std::string value) {
} }
else if (var == "sound_cancel") { else if (var == "sound_cancel") {
soundCancel = JA_LoadSound(asset->get(value).c_str()); auto bytes = ResourceHelper::loadFile(Asset::get()->get(value));
if (!bytes.empty()) soundCancel = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
} }
else if (var == "sound_accept") { else if (var == "sound_accept") {
soundAccept = JA_LoadSound(asset->get(value).c_str()); auto bytes = ResourceHelper::loadFile(Asset::get()->get(value));
if (!bytes.empty()) soundAccept = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
} }
else if (var == "sound_move") { else if (var == "sound_move") {
soundMove = JA_LoadSound(asset->get(value).c_str()); auto bytes = ResourceHelper::loadFile(Asset::get()->get(value));
if (!bytes.empty()) soundMove = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
} }
else if (var == "name") { else if (var == "name") {
@@ -330,7 +320,7 @@ bool Menu::setVars(std::string var, std::string value) {
} }
// Carga los ficheros de audio // Carga los ficheros de audio
void Menu::loadAudioFile(std::string file, int sound) { void Menu::loadAudioFile(const std::string &file, int sound) {
switch (sound) { switch (sound) {
case SOUND_ACCEPT: case SOUND_ACCEPT:
soundAccept = JA_LoadSound(file.c_str()); soundAccept = JA_LoadSound(file.c_str());
@@ -350,7 +340,7 @@ void Menu::loadAudioFile(std::string file, int sound) {
} }
// Obtiene el nombre del menu // Obtiene el nombre del menu
std::string Menu::getName() { const std::string &Menu::getName() const {
return name; return name;
} }
@@ -441,14 +431,7 @@ void Menu::setSelectorPos(int index) {
// Obtiene la anchura del elemento más ancho del menu // Obtiene la anchura del elemento más ancho del menu
int Menu::getWidestItem() { int Menu::getWidestItem() {
int result = 0; return std::accumulate(item.begin(), item.end(), 0, [](int acc, const item_t &i) { return std::max(acc, i.rect.w); });
// Obtenemos la anchura del item mas ancho
for (auto &i : item) {
result = std::max(result, i.rect.w);
}
return result;
} }
// Deja el menu apuntando al primer elemento // Deja el menu apuntando al primer elemento
@@ -485,7 +468,7 @@ void Menu::reorganize() {
} }
// Deja el menu apuntando al siguiente elemento // Deja el menu apuntando al siguiente elemento
bool Menu::increaseSelectorIndex() { void Menu::increaseSelectorIndex() {
// Guarda el indice actual antes de modificarlo // Guarda el indice actual antes de modificarlo
selector.previousIndex = selector.index; selector.previousIndex = selector.index;
@@ -510,12 +493,10 @@ bool Menu::increaseSelectorIndex() {
if (selector.incH != 0) { if (selector.incH != 0) {
selector.resizing = true; selector.resizing = true;
} }
return true;
} }
// Deja el menu apuntando al elemento anterior // Deja el menu apuntando al elemento anterior
bool Menu::decreaseSelectorIndex() { void Menu::decreaseSelectorIndex() {
// Guarda el indice actual antes de modificarlo // Guarda el indice actual antes de modificarlo
selector.previousIndex = selector.index; selector.previousIndex = selector.index;
@@ -549,8 +530,6 @@ bool Menu::decreaseSelectorIndex() {
if (selector.incH != 0) { if (selector.incH != 0) {
selector.resizing = true; selector.resizing = true;
} }
return true;
} }
// Actualiza la logica del menu // Actualiza la logica del menu
@@ -763,7 +742,7 @@ void Menu::addItem(item_t temp) {
} }
// Cambia el texto de un item // Cambia el texto de un item
void Menu::setItemCaption(int index, std::string text) { void Menu::setItemCaption(int index, const std::string &text) {
item[index].label = text; item[index].label = text;
item[index].rect.w = this->text->lenght(item[index].label); item[index].rect.w = this->text->lenght(item[index].label);
item[index].rect.h = this->text->getCharacterSize(); item[index].rect.h = this->text->getCharacterSize();
@@ -777,33 +756,31 @@ void Menu::setDefaultActionWhenCancel(int item) {
// Gestiona la entrada de teclado y mando durante el menu // Gestiona la entrada de teclado y mando durante el menu
void Menu::checkInput() { void Menu::checkInput() {
if (input->checkInput(input_up, REPEAT_FALSE)) { if (Input::get()->checkInput(input_up, REPEAT_FALSE)) {
if (decreaseSelectorIndex()) { decreaseSelectorIndex();
if (soundMove) { if (soundMove) {
JA_PlaySound(soundMove); Audio::get()->playSound(soundMove);
}
} }
} }
if (input->checkInput(input_down, REPEAT_FALSE)) { if (Input::get()->checkInput(input_down, REPEAT_FALSE)) {
if (increaseSelectorIndex()) { increaseSelectorIndex();
if (soundMove) { if (soundMove) {
JA_PlaySound(soundMove); Audio::get()->playSound(soundMove);
}
} }
} }
if (input->checkInput(input_accept, REPEAT_FALSE)) { if (Input::get()->checkInput(input_accept, REPEAT_FALSE)) {
itemSelected = selector.index; itemSelected = selector.index;
if (soundAccept) { if (soundAccept) {
JA_PlaySound(soundAccept); Audio::get()->playSound(soundAccept);
} }
} }
if (input->checkInput(input_cancel, REPEAT_FALSE)) { if (Input::get()->checkInput(input_cancel, REPEAT_FALSE)) {
itemSelected = defaultActionWhenCancel; itemSelected = defaultActionWhenCancel;
if (soundCancel) { if (soundCancel) {
JA_PlaySound(soundCancel); Audio::get()->playSound(soundCancel);
} }
} }
} }
@@ -815,12 +792,7 @@ int Menu::findWidth() {
// Calcula el alto del menu // Calcula el alto del menu
int Menu::findHeight() { int Menu::findHeight() {
int height = 0; const int height = std::accumulate(item.begin(), item.end(), 0, [](int acc, const item_t &i) { return acc + i.rect.h + i.hPaddingDown; });
// Obtenemos la altura de la suma de alturas de los items
for (auto &i : item) {
height += i.rect.h + i.hPaddingDown;
}
return height - item.back().hPaddingDown; return height - item.back().hPaddingDown;
} }
@@ -864,7 +836,7 @@ int Menu::getSelectorHeight(int value) {
} }
// Establece el nombre del menu // Establece el nombre del menu
void Menu::setName(std::string name) { void Menu::setName(const std::string &name) {
this->name = name; this->name = name;
} }
@@ -880,9 +852,9 @@ void Menu::setBackgroundType(int value) {
} }
// Establece la fuente de texto que se utilizará // Establece la fuente de texto que se utilizará
void Menu::setText(std::string font_png, std::string font_txt) { void Menu::setText(const std::string &font_png, const std::string &font_txt) {
if (!text) { if (!text) {
text = new Text(asset->get(font_png), asset->get(font_txt), renderer); text = new Text(Asset::get()->get(font_png), Asset::get()->get(font_txt), renderer);
} }
} }

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