Compare commits

..

30 Commits

Author SHA1 Message Date
3e6fcfeb72 correccions en el makefile de macos 2026-05-03 18:03:30 +02:00
2474283e07 afegit suppress a cppcheck 2026-04-18 12:28:19 +02:00
d58c0303e9 surface: hallazgo 1 — SurfaceData::width/height de float a int
Las dimensiones en píxeles son enteros por naturaleza. Convertidos los
miembros y constructores a int, y ajustados getWidth()/getHeight() para
devolver int. Eliminados los static_cast<int>(...->width/height) y
static_cast<int>(surface->getWidth/getHeight()) redundantes que sólo
existían para compensar el tipo erróneo.

Los callers que inicializan SDL_FRect directamente con getWidth/getHeight
requieren static_cast<float> explícito (sprite.cpp, animated_sprite.cpp,
notifier.cpp, title.cpp) por las reglas de narrowing de list-init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:22:19 +02:00
5f0b4355e4 surface: hallazgo 4 — elimina render(6 floats) sin callers
La sobrecarga render(float dx, float dy, float sx, float sy, float w,
float h) no tenía un solo caller en el proyecto. Las otras dos
sobrecargas (con SDL_FRect) cubren todos los casos de uso reales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:16:46 +02:00
40ac657f74 surface: hallazgo 8 — elimina setSurfaceData muerto y documenta shared_ptr
setSurfaceData() no tenía callers. El shared_ptr<SurfaceData> se queda
porque render() puede aliasar el SurfaceData propio con el del renderer
surface (self-blit). Migrar a unique_ptr requeriría tocar Screen y
dissolve_sprite sin simplificación real.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:01:51 +02:00
6a12294a36 surface: hallazgo 5 — renderWithColorReplace aplica sub_palette_
Coherencia con render() y renderWithVerticalFade(): el píxel no
sustituido pasa por sub_palette_ en vez de copiarse crudo. Hoy es
no-op (las surfaces que usan color replace no hacen fadeSubPalette)
pero cierra la divergencia de API y previene regresiones futuras.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:58:46 +02:00
1f5b1ad1ab surface: hallazgo 2 — drawLine con Bresenham en enteros
El bucle usaba floats con comparación de igualdad exacta (x1==x2 &&
y1==y2) como condición de parada, con incrementos ±1.0f acumulados:
bug latente. Convertidos los endpoints de entrada con std::lround y
reescrito el algoritmo con ints. Firma pública float preservada para
no tocar callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:55:12 +02:00
b1413bbf8a surface: hallazgo 3 — sustituye sizeof check por static_assert en fade*Palette
palette_ y sub_palette_ son std::array de tamaño fijo, así que el check
en runtime nunca podía fallar. Movido a static_assert sobre tuple_size_v.
El throw asociado era código muerto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:54:33 +02:00
eaf9d87d6d surface: hallazgo 6 — elimina doble std::min en render(int,int,...)
Las dos líneas de clamp contra el destino estaban duplicadas. Fusionado
el comentario y dejado un único bloque que limita contra origen y destino.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:53:59 +02:00
60adfc8fbb surface: hallazgo 7 — elimina NOLINT obsoleto en loadSurface
loadSurface es static en declaración y definición, así que el
NOLINT(readability-convert-member-functions-to-static) era dead noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:53:33 +02:00
0dbf38f506 normalitzat Audio 2026-04-18 11:43:45 +02:00
53c2b345c9 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
74e19e9951 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 13:00:02 +02:00
7480616c07 fix: android input amb sdl_joystick 2026-04-15 23:54:15 +02:00
6a76c65771 make controllerdb
trim del nom del mando
2026-04-15 09:49:58 +02:00
9f22e1c58b opcions per amagar la pantalla de carrega 2026-04-15 09:23:36 +02:00
727e55af03 arreglos en screen 2026-04-15 06:31:43 +02:00
6e0d9235a3 undo android test 2026-04-13 21:26:41 +02:00
98f251d155 android test 2026-04-13 21:17:31 +02:00
acc1b0e8a1 canvis de paleta amb L i R del mando 2026-04-13 21:05:52 +02:00
49fb895984 fix: no restaurava la pantalla en emscripten al eixir de fullscreen 2026-04-13 20:42:40 +02:00
9047bd7d1f tone a commitar pa provar el canvi de pantalla en emscripten 2026-04-13 20:09:18 +02:00
9b8820ffa3 pantalla de carrega no bloquejant
streaming de audio per evitar precárrega i descompresió a memoria
2026-04-13 19:29:05 +02:00
585c93054e commit per a provar les coses rares de la pantalla en wasm 2026-04-13 18:31:16 +02:00
8bfc32de40 emscripten: no anava back en game 2026-04-13 18:06:54 +02:00
40766ad122 fix: en console faltava SCENE DEMO 2026-04-13 18:02:04 +02:00
e67aeb10fe fix: controls en el mando 2026-04-13 17:57:13 +02:00
5f293cbddf reordenades les layers del overlay
consola ara tanca i obri per temps en lloc de velocitat
2026-04-13 14:03:45 +02:00
7f470361cc soport de gamepad per a wasm 2026-04-13 13:20:50 +02:00
d9c41f420b fix: arrancar amb el borde desactivat feia crash al activarlo 2026-04-13 11:57:01 +02:00
57 changed files with 2107 additions and 1054 deletions

View File

@@ -2,29 +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: '*'
# Solo incluir archivos de tu código fuente (external tiene su propio .clang-tidy) # Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
# Excluye jail_audio.hpp del análisis HeaderFilterRegex: 'source/.*'
HeaderFilterRegex: 'source/(?!core/audio/jail_audio\.hpp|core/rendering/sdl3gpu/.*_spv\.h).*'
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 }
@@ -45,7 +39,7 @@ CheckOptions:
# Variables estáticas 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 estáticas sin sufijo # Constantes estáticas sin sufijo
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE } - { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }

View File

@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
project(jaildoctors_dilemma VERSION 1.00) project(jaildoctors_dilemma 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()
# Establecer estándar de C++ # Establecer estándar de C++
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -11,17 +16,24 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA --- # --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
find_package(Git QUIET) # Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
if(GIT_FOUND) # lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
execute_process( # "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD # resuelve aquí ejecutando git directamente.
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
OUTPUT_VARIABLE GIT_HASH find_package(Git QUIET)
OUTPUT_STRIP_TRAILING_WHITESPACE if(GIT_FOUND)
ERROR_QUIET execute_process(
) COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
else() WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
set(GIT_HASH "unknown") OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif() endif()
# Configurar archivo de versión # Configurar archivo de versión
@@ -31,6 +43,7 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
set(APP_SOURCES set(APP_SOURCES
# Core - Audio # Core - Audio
source/core/audio/audio.cpp source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# Core - Input # Core - Input
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
@@ -87,6 +100,7 @@ set(APP_SOURCES
source/game/gameplay/tilemap_renderer.cpp source/game/gameplay/tilemap_renderer.cpp
# Game - Scenes # Game - Scenes
source/game/scenes/boot_loader.cpp
source/game/scenes/credits.cpp source/game/scenes/credits.cpp
source/game/scenes/ending.cpp source/game/scenes/ending.cpp
source/game/scenes/ending2.cpp source/game/scenes/ending2.cpp
@@ -133,7 +147,7 @@ if(EMSCRIPTEN)
FetchContent_Declare( FetchContent_Declare(
SDL3 SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.2.12 GIT_TAG release-3.4.4
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
) )
set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE)
@@ -151,7 +165,7 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc) find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders") set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu") set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
set(SHADER_POSTFX_VERT_SRC "${SHADERS_DIR}/postfx.vert") set(SHADER_POSTFX_VERT_SRC "${SHADERS_DIR}/postfx.vert")
set(SHADER_POSTFX_FRAG_SRC "${SHADERS_DIR}/postfx.frag") set(SHADER_POSTFX_FRAG_SRC "${SHADERS_DIR}/postfx.frag")
@@ -277,6 +291,9 @@ elseif(EMSCRIPTEN)
-sMAX_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864 -sINITIAL_MEMORY=67108864
-sASSERTIONS=1 -sASSERTIONS=1
# ASYNCIFY només per permetre emscripten_sleep(0) durant la precàrrega de recursos
# (el bucle principal del joc ja usa SDL3 Callback API, no depèn d'ASYNCIFY).
-sASYNCIFY=1
) )
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html") set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE) elseif(UNIX AND NOT APPLE)
@@ -293,6 +310,7 @@ endif()
# Buscar herramientas de análisis estático # Buscar herramientas de análisis estático
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 formateo # Recopilar todos los archivos fuente para formateo
file(GLOB_RECURSE ALL_SOURCE_FILES file(GLOB_RECURSE ALL_SOURCE_FILES
@@ -304,10 +322,11 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
# Excluir directorio external del análisis # Excluir directorio external del análisis
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*") list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
# Para clang-tidy, también excluir jail_audio.hpp
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES}) set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*_spv\\.h$") # Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
# Targets de clang-tidy # Targets de clang-tidy
if(CLANG_TIDY_EXE) if(CLANG_TIDY_EXE)
@@ -353,31 +372,55 @@ 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()
# --- 6. PACK RESOURCES TARGETS (no en Emscripten: s'utilitza --preload-file) --- # 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/*
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
--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()
# --- 6. EINA STANDALONE: pack_resources (no en Emscripten: s'utilitza --preload-file) ---
# 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
if(NOT EMSCRIPTEN) if(NOT EMSCRIPTEN)
set(PACK_TOOL_SOURCES add_executable(pack_resources EXCLUDE_FROM_ALL
${CMAKE_SOURCE_DIR}/tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
${CMAKE_SOURCE_DIR}/source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
add_executable(pack_tool ${PACK_TOOL_SOURCES}) # Regeneració automàtica de resources.pack en cada build si canvia data/.
target_include_directories(pack_tool PRIVATE ${CMAKE_SOURCE_DIR}/source) file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set_target_properties(pack_tool PROPERTIES set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/tools/pack_resources
)
file(GLOB_RECURSE DATA_FILES "${CMAKE_SOURCE_DIR}/data/*")
add_custom_command( add_custom_command(
OUTPUT "${CMAKE_SOURCE_DIR}/resources.pack" OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_tool> COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data" "${CMAKE_SOURCE_DIR}/data"
"${CMAKE_SOURCE_DIR}/resources.pack" "${RESOURCE_PACK}"
DEPENDS pack_tool ${DATA_FILES} DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Generando resources.pack desde data/..." COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
) )
add_custom_target(pack DEPENDS "${CMAKE_SOURCE_DIR}/resources.pack") add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} pack) add_dependencies(${PROJECT_NAME} resource_pack)
endif() endif()

233
Makefile
View File

@@ -4,7 +4,6 @@
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST))) DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source) DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
DIR_BIN := $(addsuffix /, $(DIR_ROOT)) DIR_BIN := $(addsuffix /, $(DIR_ROOT))
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
# ============================================================================== # ==============================================================================
# TARGET NAMES # TARGET NAMES
@@ -20,8 +19,6 @@ RESOURCE_FILE := release/windows/jdd.res
# ============================================================================== # ==============================================================================
# TOOLS # TOOLS
# ============================================================================== # ==============================================================================
DIR_PACK_TOOL := $(DIR_TOOLS)pack_resources
SHADER_SCRIPT := $(DIR_ROOT)tools/shaders/compile_spirv.sh
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADERS_DIR := $(DIR_ROOT)data/shaders SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
@@ -35,9 +32,23 @@ endif
# VERSION (extracted from defines.hpp) # VERSION (extracted from defines.hpp)
# ============================================================================== # ==============================================================================
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
VERSION := v$(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value") VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/') 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 endif
# ============================================================================== # ==============================================================================
@@ -84,15 +95,24 @@ else
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
endif endif
# ==============================================================================
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
# ==============================================================================
ifeq ($(OS),Windows_NT)
CMAKE_GEN := -G "MinGW Makefiles"
else
CMAKE_GEN :=
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
# ============================================================================== # ==============================================================================
@@ -100,12 +120,12 @@ debug:
# ============================================================================== # ==============================================================================
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
@@ -120,23 +140,22 @@ else
endif endif
# ============================================================================== # ==============================================================================
# REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK # EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
# ============================================================================== # ==============================================================================
pack_tool: pack:
@$(MAKE) -C $(DIR_PACK_TOOL) @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
resources.pack: pack_tool @./build/pack_resources data resources.pack
@$(MAKE) -C $(DIR_PACK_TOOL) pack
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE) # COMPILACIÓN PARA WINDOWS (RELEASE)
# ============================================================================== # ==============================================================================
windows_release: _windows_release:
@echo off @echo off
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake (genera shaders, resources.pack y ejecutable) # Compila con cmake (genera shaders, resources.pack y ejecutable)
@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 'RELEASE_FOLDER' # Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@@ -164,15 +183,31 @@ windows_release:
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE) # COMPILACIÓN PARA MACOS (RELEASE)
# ============================================================================== # ==============================================================================
macos_release: _macos_release:
@echo "Creando release para macOS - Version: $(VERSION)" @echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario # Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg) # con brew; si brew tampoco está, indica el comando exacto al usuario.
@command -v create-dmg >/dev/null 2>&1 || { \
# Compila la versión para procesadores Intel con cmake (genera shaders y resources.pack) echo ""; \
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON echo "============================================"; \
@cmake --build build/intel echo " Falta la dependencia: create-dmg"; \
echo "============================================"; \
if command -v brew >/dev/null 2>&1; then \
echo " Instalando con: brew install create-dmg"; \
brew install create-dmg || { \
echo ""; \
echo " ERROR: 'brew install create-dmg' ha fallado."; \
echo " Ejecuta el comando manualmente y vuelve a probar."; \
exit 1; \
}; \
else \
echo " Homebrew no está instalado."; \
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
echo " brew install create-dmg"; \
exit 1; \
fi; \
}
# Elimina datos de compilaciones anteriores # Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
@@ -186,12 +221,11 @@ macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros # Copia carpetas y ficheros del bundle (resources.pack se generará al compilar)
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"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp LICENSE "$(RELEASE_FOLDER)" cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)" cp README.md "$(RELEASE_FOLDER)"
@@ -201,32 +235,53 @@ macos_release:
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \ sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist" sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Copia el ejecutable Intel al bundle # Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" # DMG Intel y continúa con la build de Apple Silicon).
@echo ""
# Firma la aplicación @echo "============================================"
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app" @echo " Compilando version Intel (x86_64)"
@echo "============================================"
# Empaqueta el .dmg de la versión Intel con create-dmg @if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
@echo "Creando DMG Intel con iconos de 96x96..." -DCMAKE_OSX_ARCHITECTURES=x86_64 \
create-dmg \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
--volname "$(APP_NAME)" \ -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
--window-pos 200 120 \ && cmake --build build/intel; then \
--window-size 720 300 \ cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"; \
--icon-size 96 \ cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
--text-size 12 \ codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
--icon "$(APP_NAME).app" 278 102 \ echo "Creando DMG Intel con iconos de 96x96..."; \
--icon "LICENSE" 441 102 \ create-dmg \
--icon "README.md" 604 102 \ --volname "$(APP_NAME)" \
--app-drop-link 115 102 \ --window-pos 200 120 \
--hide-extension "$(APP_NAME).app" \ --window-size 720 300 \
"$(MACOS_INTEL_RELEASE)" \ --icon-size 96 \
"$(RELEASE_FOLDER)" || true --text-size 12 \
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" --icon "$(APP_NAME).app" 278 102 \
--icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true; \
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
else \
echo ""; \
echo "============================================"; \
echo " WARNING: la build Intel ha fallado."; \
echo " Se omite el DMG Intel y se continúa con"; \
echo " la build de Apple Silicon."; \
echo "============================================"; \
echo ""; \
fi
# Compila la versión para procesadores Apple Silicon 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 @echo ""
@echo "============================================"
@echo " Compilando version Apple Silicon (arm64)"
@echo "============================================"
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/arm @cmake --build build/arm
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación # Firma la aplicación
@@ -258,11 +313,11 @@ macos_release:
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE) # COMPILACIÓN PARA LINUX (RELEASE)
# ============================================================================== # ==============================================================================
linux_release: _linux_release:
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake (genera shaders, resources.pack y ejecutable) # Compila con cmake (genera shaders, resources.pack y ejecutable)
@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 (crea dist/ si no existe) # Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
@@ -291,16 +346,69 @@ linux_release:
wasm: wasm:
@echo "Compilando para WebAssembly - Version: $(VERSION)" @echo "Compilando para WebAssembly - Version: $(VERSION)"
docker run --rm \ docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \ -v $(DIR_ROOT):/src \
-w /src \ -w /src \
emscripten/emsdk:latest \ emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release && cmake --build build/wasm" bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
$(MKDIR) "$(DIST_DIR)/wasm" $(MKDIR) "$(DIST_DIR)/wasm"
cp build/wasm/$(TARGET_NAME).html $(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).js $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/ cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/ cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(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/jaildoctors-dilemma/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versió Debug del build wasm: arrenca directament a la GAME (sense logo/loading/title)
# i activa l'editor i la consola. Sortida a dist/wasm_debug/.
wasm_debug:
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
docker run --rm \
-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
# 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
@@ -320,18 +428,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 linux_release - Crear release para Linux"
@echo " make macos_release - Crear release para macOS"
@echo " make wasm - Crear release per a WebAssembly (requereix Docker)" @echo " make wasm - Crear release per a WebAssembly (requereix Docker)"
@echo " make wasm_debug - Crear build Debug per a WebAssembly (entra directe a la GAME)"
@echo "" @echo ""
@echo " Herramientas:" @echo " Herramientas:"
@echo " make compile_shaders - Compilar shaders SPIR-V" @echo " make compile_shaders - Compilar shaders SPIR-V"
@echo " make pack_tool - Compilar herramienta de empaquetado" @echo " make pack - Empaquetar recursos a resources.pack"
@echo " make resources.pack - Generar pack de recursos desde data/" @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 wasm compile_shaders pack_tool resources.pack show_version help .PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug compile_shaders pack controllerdb format format-check tidy tidy-fix cppcheck show_version help

View File

@@ -201,7 +201,7 @@ categories:
DEBUG: [MODE, START] DEBUG: [MODE, START]
DEBUG MODE: [ON, OFF] DEBUG MODE: [ON, OFF]
DEBUG START: [HERE, ROOM, POS, SCENE] DEBUG START: [HERE, ROOM, POS, SCENE]
DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2] DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, DEMO, ENDING, ENDING2]
- keyword: ITEMS - keyword: ITEMS
handler: cmd_items handler: cmd_items
@@ -220,9 +220,9 @@ categories:
- keyword: SCENE - keyword: SCENE
handler: cmd_scene handler: cmd_scene
description: Change scene description: Change scene
usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]" usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|DEMO|ENDING|ENDING2|RESTART]"
completions: completions:
SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2, RESTART] SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, DEMO, ENDING, ENDING2, RESTART]
- keyword: EDIT - keyword: EDIT
handler: cmd_edit handler: cmd_edit

View File

@@ -8,6 +8,7 @@ title:
keyboard: "2. REDEFINIR TECLES" keyboard: "2. REDEFINIR TECLES"
joystick: "3. REDEFINIR MANDO" joystick: "3. REDEFINIR MANDO"
projects: "4. PROJECTES" projects: "4. PROJECTES"
press_to_play: "PREM PER JUGAR"
keys: keys:
prompt0: "PREM UNA TECLA PER A ESQUERRA" prompt0: "PREM UNA TECLA PER A ESQUERRA"
prompt1: "PREM UNA TECLA PER A DRETA" prompt1: "PREM UNA TECLA PER A DRETA"
@@ -103,6 +104,8 @@ achievements:
ui: ui:
press_again_menu: "PREM DE NOU PER TORNAR AL MENÚ" press_again_menu: "PREM DE NOU PER TORNAR AL MENÚ"
press_again_exit: "PREM DE NOU PER EIXIR" press_again_exit: "PREM DE NOU PER EIXIR"
gamepad_connected: "CONNECTAT"
gamepad_disconnected: "DESCONNECTAT"
border_enabled: "VORA ACTIVADA" border_enabled: "VORA ACTIVADA"
border_disabled: "VORA DESACTIVADA" border_disabled: "VORA DESACTIVADA"
fullscreen_enabled: "PANTALLA COMPLETA ACTIVADA" fullscreen_enabled: "PANTALLA COMPLETA ACTIVADA"

View File

@@ -8,6 +8,7 @@ title:
keyboard: "2. REDEFINE KEYBOARD" keyboard: "2. REDEFINE KEYBOARD"
joystick: "3. REDEFINE JOYSTICK" joystick: "3. REDEFINE JOYSTICK"
projects: "4. PROJECTS" projects: "4. PROJECTS"
press_to_play: "PRESS TO PLAY"
keys: keys:
prompt0: "PRESS KEY FOR LEFT" prompt0: "PRESS KEY FOR LEFT"
prompt1: "PRESS KEY FOR RIGHT" prompt1: "PRESS KEY FOR RIGHT"
@@ -103,6 +104,8 @@ achievements:
ui: ui:
press_again_menu: "PRESS AGAIN TO RETURN TO MENU" press_again_menu: "PRESS AGAIN TO RETURN TO MENU"
press_again_exit: "PRESS AGAIN TO EXIT" press_again_exit: "PRESS AGAIN TO EXIT"
gamepad_connected: "CONNECTED"
gamepad_disconnected: "DISCONNECTED"
border_enabled: "BORDER ENABLED" border_enabled: "BORDER ENABLED"
border_disabled: "BORDER DISABLED" border_disabled: "BORDER DISABLED"
fullscreen_enabled: "FULLSCREEN ENABLED" fullscreen_enabled: "FULLSCREEN ENABLED"

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,27 @@
#include "audio.hpp" #include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G... #include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp #include <algorithm> // Para clamp
#include <iostream> // Para std::cout #include <iostream> // Para std::cout
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp) // Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off // clang-format off
#undef STB_VORBIS_HEADER_ONLY #undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h" #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 // clang-format on
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM... #include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/resources/resource_cache.hpp" // Para Resource #include "core/audio/jail_audio.hpp" // Para JA_*
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions #include "game/options.hpp" // Para Options::audio
// Singleton // Singleton
Audio* Audio::instance = nullptr; Audio* Audio::instance = nullptr;
@@ -22,7 +30,10 @@ Audio* Audio::instance = nullptr;
void Audio::init() { Audio::instance = new Audio(); } void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia // Libera la instancia
void Audio::destroy() { delete Audio::instance; } void Audio::destroy() {
delete Audio::instance;
Audio::instance = nullptr;
}
// Obtiene la instancia // Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; } auto Audio::get() -> Audio* { return Audio::instance; }
@@ -38,10 +49,15 @@ Audio::~Audio() {
// Método principal // Método principal
void Audio::update() { void Audio::update() {
JA_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 // Reproduce la música por nombre (con crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(readability-convert-member-functions-to-static) void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0); bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada // Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
@@ -49,29 +65,45 @@ void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(read
return; return;
} }
// Intentar obtener recurso; si falla, no tocar estado if (!music_enabled_) return;
auto* resource = Resource::Cache::get()->getMusic(name);
if (resource == nullptr) { auto* resource = AudioResource::getMusic(name);
// manejo de error opcional if (resource == nullptr) return;
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);
} }
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
if (music_.state == MusicState::PLAYING) {
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
}
// Llamada al motor para reproducir la nueva pista
JA_PlayMusic(resource, loop);
// Actualizar estado y metadatos después de iniciar con éxito
music_.name = name; music_.name = name;
music_.loop = new_loop; music_.loop = new_loop;
music_.state = MusicState::PLAYING; 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 // Pausa la música
void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-static) void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) { if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic(); JA_PauseMusic();
music_.state = MusicState::PAUSED; music_.state = MusicState::PAUSED;
@@ -79,7 +111,7 @@ void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-st
} }
// Continua la música pausada // Continua la música pausada
void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-static) void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) { if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic(); JA_ResumeMusic();
music_.state = MusicState::PLAYING; music_.state = MusicState::PLAYING;
@@ -87,7 +119,7 @@ void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-s
} }
// Detiene la música // Detiene la música
void Audio::stopMusic() { // NOLINT(readability-make-member-function-const) void Audio::stopMusic() {
if (music_enabled_) { if (music_enabled_) {
JA_StopMusic(); JA_StopMusic();
music_.state = MusicState::STOPPED; music_.state = MusicState::STOPPED;
@@ -97,13 +129,13 @@ void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
// Reproduce un sonido por nombre // Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const { void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) { if (sound_enabled_) {
JA_PlaySound(Resource::Cache::get()->getSound(name), 0, static_cast<int>(group)); JA_PlaySound(AudioResource::getSound(name), 0, static_cast<int>(group));
} }
} }
// Reproduce un sonido por puntero directo // Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const { void Audio::playSound(JA_Sound_t* sound, Group group) const {
if (sound_enabled_) { if (sound_enabled_ && sound != nullptr) {
JA_PlaySound(sound, 0, static_cast<int>(group)); JA_PlaySound(sound, 0, static_cast<int>(group));
} }
} }
@@ -138,7 +170,7 @@ auto Audio::getRealMusicState() -> MusicState {
} }
} }
// Establece el volumen de los sonidos // Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const { void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) { if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME); sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
@@ -147,7 +179,7 @@ void Audio::setSoundVolume(float sound_volume, Group group) const {
} }
} }
// Establece el volumen de la música // Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const { void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) { if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME); music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
@@ -172,24 +204,9 @@ void Audio::enable(bool value) {
// Inicializa SDL Audio // Inicializa SDL Audio
void Audio::initSDLAudio() { void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) { if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError()); std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else { } else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2); JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled); enable(Options::audio.enabled);
// Aplicar estado de música y sonido guardado en las opciones.
// enable() ya aplica los volúmenes, pero no toca music_enabled_/sound_enabled_.
// Si alguno está desactivado, hay que forzar el volumen a 0 en el backend.
if (!Options::audio.music.enabled) {
setMusicVolume(0.0F); // music_enabled_=true aún → llega a JA
enableMusic(false);
}
if (!Options::audio.sound.enabled) {
setSoundVolume(0.0F); // sound_enabled_=true aún → llega a JA
enableSound(false);
}
std::cout << "\n** AUDIO SYSTEM **\n";
std::cout << "Audio system initialized successfully\n";
} }
} }

View File

@@ -1,28 +1,35 @@
#pragma once #pragma once
#include <cstdint> // Para int8_t, uint8_t
#include <string> // Para string #include <string> // Para string
#include <utility> // Para move #include <utility> // Para move
// --- Clase Audio: gestor de audio (singleton) --- // --- 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 { class Audio {
public: public:
// --- Enums --- // --- Enums ---
enum class Group : int { enum class Group : std::int8_t {
ALL = -1, // Todos los grupos ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz INTERFACE = 1 // Sonidos de la interfaz
}; };
enum class MusicState { enum class MusicState : std::uint8_t {
PLAYING, // Reproduciendo música PLAYING, // Reproduciendo música
PAUSED, // Música pausada PAUSED, // Música pausada
STOPPED, // Música detenida STOPPED, // Música detenida
}; };
// --- Constantes --- // --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
static constexpr int FREQUENCY = 48000; // Frecuencia de audio 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 --- // --- Singleton ---
static void init(); // Inicializa el objeto Audio static void init(); // Inicializa el objeto Audio
@@ -34,21 +41,31 @@ class Audio {
static void update(); // Actualización del sistema de audio static void update(); // Actualización del sistema de audio
// --- Control de música --- // --- Control de música ---
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void pauseMusic(); // Pausar reproducción de música void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void resumeMusic(); // Continua la música pausada void pauseMusic(); // Pausar reproducción de música
void stopMusic(); // Detener completamente la música void resumeMusic(); // Continua la música pausada
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos --- // --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre 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 playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen --- // --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música 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 --- // --- Configuración general ---
void enable(bool value); // Establecer estado general void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
@@ -94,4 +111,4 @@ class Audio {
bool enabled_{true}; // Estado general del audio bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música 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_cache.hpp"
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name) {
return Resource::Cache::get()->getMusic(name);
}
JA_Sound_t* getSound(const std::string& name) {
return Resource::Cache::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

@@ -3,24 +3,41 @@
// --- Includes --- // --- Includes ---
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <stdint.h> // Para uint32_t, uint8_t #include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET #include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc #include <stdlib.h> // Para free, malloc
#include <string.h> // Para strcpy, strlen
#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 #define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory #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 --- // --- Public Enums ---
enum JA_Channel_state { JA_CHANNEL_INVALID, enum JA_Channel_state {
JA_CHANNEL_INVALID,
JA_CHANNEL_FREE, JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING, JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED, JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED }; JA_SOUND_DISABLED,
enum JA_Music_state { JA_MUSIC_INVALID, };
enum JA_Music_state {
JA_MUSIC_INVALID,
JA_MUSIC_PLAYING, JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED, JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED, JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED }; JA_MUSIC_DISABLED,
};
// --- Struct Definitions --- // --- Struct Definitions ---
#define JA_MAX_SIMULTANEOUS_CHANNELS 20 #define JA_MAX_SIMULTANEOUS_CHANNELS 20
@@ -29,7 +46,9 @@ enum JA_Music_state { JA_MUSIC_INVALID,
struct JA_Sound_t { struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0}; Uint32 length{0};
Uint8* buffer{NULL}; // 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 { struct JA_Channel_t {
@@ -43,18 +62,23 @@ struct JA_Channel_t {
struct JA_Music_t { struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
char* filename{nullptr};
int pos{0}; // OGG comprimit en memòria. Propietat nostra; es copia des del buffer
int times{0}; // 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}; SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID}; JA_Music_state state{JA_MUSIC_INVALID};
}; };
// --- Internal Global State --- // --- Internal Global State (inline, C++17) ---
// Marcado 'inline' (C++17) para asegurar una única instancia.
inline JA_Music_t* current_music{nullptr}; inline JA_Music_t* current_music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
@@ -66,49 +90,142 @@ inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true}; inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0}; inline SDL_AudioDeviceID sdlAudioDevice{0};
inline bool fading{false}; // --- Crossfade / Fade State ---
inline int fade_start_time{0}; struct JA_FadeState {
inline int fade_duration{0}; bool active{false};
inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float' 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 --- // --- Forward Declarations ---
inline void JA_StopMusic(); inline void JA_StopMusic();
inline void JA_StopChannel(const int channel); 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 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 --- // --- Core Functions ---
inline void JA_Update() { inline void JA_Update() {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { // --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
if (fading) { if (outgoing_music.stream && outgoing_music.fade.active) {
int time = SDL_GetTicks(); Uint64 now = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) { Uint64 elapsed = now - outgoing_music.fade.start_time;
fading = false; if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
JA_StopMusic(); SDL_DestroyAudioStream(outgoing_music.stream);
return; outgoing_music.stream = nullptr;
} else { outgoing_music.fade.active = false;
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
}
}
if (current_music->times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times > 0) current_music->times--;
} else { } else {
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); 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) { if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) { if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) { if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) { if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length); SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--; if (channels[i].times > 0) channels[i].times--;
} }
} else { } else {
@@ -119,69 +236,85 @@ inline void JA_Update() {
} }
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef _DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq}; JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!"); 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_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f; for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
} }
inline void JA_Quit() { inline void JA_Quit() {
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0; sdlAudioDevice = 0;
} }
// --- Music Functions --- // --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
JA_Music_t* music = new JA_Music_t(); if (!buffer || length == 0) return nullptr;
int chan, samplerate; // Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
short* output; // com a propietari del OGG comprimit. stb_vorbis guarda un punter
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2; // 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);
music->spec.channels = chan; int error = 0;
music->spec.freq = samplerate; 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->spec.format = SDL_AUDIO_S16;
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED; music->state = JA_MUSIC_STOPPED;
return music; 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) { inline JA_Music_t* JA_LoadMusic(const char* filename) {
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid. // Carreguem primer el arxiu en memòria i després el descomprimim.
FILE* f = fopen(filename, "rb"); FILE* f = fopen(filename, "rb");
if (!f) return NULL; // Añadida comprobación de apertura if (!f) return nullptr;
fseek(f, 0, SEEK_END); fseek(f, 0, SEEK_END);
long fsize = ftell(f); long fsize = ftell(f);
fseek(f, 0, SEEK_SET); fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1)); auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) { // Añadida comprobación de malloc if (!buffer) {
fclose(f); fclose(f);
return NULL; return nullptr;
} }
if (fread(buffer, fsize, 1, f) != 1) { if (fread(buffer, fsize, 1, f) != 1) {
fclose(f); fclose(f);
free(buffer); free(buffer);
return NULL; return nullptr;
} }
fclose(f); fclose(f);
JA_Music_t* music = JA_LoadMusic(buffer, fsize); JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
if (music) { // Comprobar que JA_LoadMusic tuvo éxito if (music) {
music->filename = static_cast<char*>(malloc(strlen(filename) + 1)); music->filename = filename;
if (music->filename) {
strcpy(music->filename, filename);
}
} }
free(buffer); free(buffer);
@@ -190,35 +323,43 @@ inline JA_Music_t* JA_LoadMusic(const char* filename) {
} }
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music if (!JA_musicEnabled || !music || !music->vorbis) return;
JA_StopMusic(); JA_StopMusic();
current_music = music; current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING; current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop; 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); current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) { // Comprobar creación de stream if (!current_music->stream) {
SDL_Log("Failed to create audio stream!"); std::cout << "Failed to create audio stream!" << '\n';
current_music->state = JA_MUSIC_STOPPED; current_music->state = JA_MUSIC_STOPPED;
return; return;
} }
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
// 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 char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) { inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music; if (!music) music = current_music;
if (!music) return nullptr; // Añadida comprobación if (!music || music->filename.empty()) return nullptr;
return music->filename; return music->filename.c_str();
} }
inline void JA_PauseMusic() { inline void JA_PauseMusic() {
if (!JA_musicEnabled) return; if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
current_music->state = JA_MUSIC_PAUSED; current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream); SDL_UnbindAudioStream(current_music->stream);
@@ -226,32 +367,100 @@ inline void JA_PauseMusic() {
inline void JA_ResumeMusic() { inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return; if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada if (!current_music || current_music->state != JA_MUSIC_PAUSED) return;
current_music->state = JA_MUSIC_PLAYING; current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream); SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
} }
inline void JA_StopMusic() { 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; if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED; current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) { if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream); SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr; current_music->stream = nullptr;
} }
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic // Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic.
// Rebobinem perquè un futur JA_PlayMusic comence des del principi.
if (current_music->vorbis) {
stb_vorbis_seek_start(current_music->vorbis);
}
} }
inline void JA_FadeOutMusic(const int milliseconds) { inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return; if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return; if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
fading = true; // Destruir outgoing anterior si existe
fade_start_time = SDL_GetTicks(); if (outgoing_music.stream) {
fade_duration = milliseconds; SDL_DestroyAudioStream(outgoing_music.stream);
fade_initial_volume = JA_musicVolume; 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() { inline JA_Music_state JA_GetMusicState() {
@@ -267,9 +476,10 @@ inline void JA_DeleteMusic(JA_Music_t* music) {
JA_StopMusic(); JA_StopMusic();
current_music = nullptr; current_music = nullptr;
} }
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream); if (music->stream) SDL_DestroyAudioStream(music->stream);
free(music->filename); // filename se libera aquí 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; delete music;
} }
@@ -281,53 +491,41 @@ inline float JA_SetMusicVolume(float volume) {
return JA_musicVolume; return JA_musicVolume;
} }
inline void JA_SetMusicPosition(float value) { inline void JA_SetMusicPosition(float /*value*/) {
if (!current_music) return; // No implementat amb el backend de streaming.
current_music->pos = value * current_music->spec.freq;
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
// el streaming. El streaming siempre parece empezar desde el principio.
} }
inline float JA_GetMusicPosition() { inline float JA_GetMusicPosition() {
if (!current_music) return 0; return 0.0f;
return float(current_music->pos) / float(current_music->spec.freq);
// Nota: Ver `JA_SetMusicPosition`
} }
inline void JA_EnableMusic(const bool value) { inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic(); if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value; JA_musicEnabled = value;
} }
// --- Sound Functions --- // --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
// Nota: spec se queda con los valores por defecto.
return sound;
}
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) { inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t(); auto sound = std::make_unique<JA_Sound_t>();
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) { Uint8* raw = nullptr;
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError()); if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
delete sound; std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
return nullptr; return nullptr;
} }
return sound; sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
} }
inline JA_Sound_t* JA_LoadSound(const char* filename) { inline JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t(); auto sound = std::make_unique<JA_Sound_t>();
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) { Uint8* raw = nullptr;
SDL_Log("Failed to load WAV file: %s", SDL_GetError()); if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
delete sound; std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
return nullptr; return nullptr;
} }
return sound; 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) { inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
@@ -347,22 +545,22 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
if (!JA_soundEnabled || !sound) return -1; if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1; if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso JA_StopChannel(channel);
channels[channel].sound = sound; channels[channel].sound = sound;
channels[channel].times = loop; channels[channel].times = loop;
channels[channel].pos = 0; channels[channel].pos = 0;
channels[channel].group = group; // Asignar grupo channels[channel].group = group;
channels[channel].state = JA_CHANNEL_PLAYING; channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) { if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!"); std::cout << "Failed to create audio stream for sound!" << '\n';
channels[channel].state = JA_CHANNEL_FREE; channels[channel].state = JA_CHANNEL_FREE;
return -1; return -1;
} }
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]); SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
@@ -374,7 +572,7 @@ inline void JA_DeleteSound(JA_Sound_t* sound) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i); if (channels[i].sound == sound) JA_StopChannel(i);
} }
SDL_free(sound->buffer); // buffer es destrueix automàticament via RAII (SDLFreeDeleter).
delete sound; delete sound;
} }
@@ -420,7 +618,7 @@ inline void JA_StopChannel(const int channel) {
channels[i].stream = nullptr; channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE; channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0; channels[i].pos = 0;
channels[i].sound = NULL; channels[i].sound = nullptr;
} }
} }
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
@@ -429,7 +627,7 @@ inline void JA_StopChannel(const int channel) {
channels[channel].stream = nullptr; channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE; channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0; channels[channel].pos = 0;
channels[channel].sound = NULL; channels[channel].sound = nullptr;
} }
} }
} }
@@ -441,8 +639,7 @@ inline JA_Channel_state JA_GetChannelState(const int channel) {
return channels[channel].state; return channels[channel].state;
} }
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos inline float JA_SetSoundVolume(float volume, const int group = -1) {
{
const float v = SDL_clamp(volume, 0.0f, 1.0f); const float v = SDL_clamp(volume, 0.0f, 1.0f);
if (group == -1) { if (group == -1) {
@@ -452,10 +649,10 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t
} else if (group >= 0 && group < JA_MAX_GROUPS) { } else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v; JA_soundVolume[group] = v;
} else { } else {
return v; // Grupo inválido return v;
} }
// Aplicar volumen a canales activos // Aplicar volum als canals actius.
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) { if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) { if (group == -1 || channels[i].group == group) {
@@ -470,13 +667,13 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t
inline void JA_EnableSound(const bool value) { inline void JA_EnableSound(const bool value) {
if (!value) { if (!value) {
JA_StopChannel(-1); // Detener todos los canales JA_StopChannel(-1);
} }
JA_soundEnabled = value; JA_soundEnabled = value;
} }
inline float JA_SetVolume(float volume) { inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume); float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido JA_SetSoundVolume(v, -1);
return v; return v;
} }

View File

@@ -9,6 +9,7 @@
#include "core/locale/locale.hpp" // Para Locale #include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/screen.hpp" // Para Screen
#include "core/system/global_events.hpp" // Para GlobalEvents::consumeGamepadButtonPressed
#include "game/options.hpp" // Para Options, options, OptionsVideo, Section #include "game/options.hpp" // Para Options, options, OptionsVideo, Section
#include "game/scene_manager.hpp" // Para SceneManager #include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console #include "game/ui/console.hpp" // Para Console
@@ -20,11 +21,8 @@ namespace GlobalInputs {
// Funciones internas // Funciones internas
namespace { namespace {
void handleQuit() { void handleQuit() {
#ifdef __EMSCRIPTEN__ // En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko):
// A la versió web no es pot eixir del joc // Escape torna al menu principal. Això també és vàlid en la versió web.
return;
#else
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko)
if (SceneManager::current == SceneManager::Scene::GAME) { if (SceneManager::current == SceneManager::Scene::GAME) {
const std::string CODE = "PRESS AGAIN TO RETURN TO MENU"; const std::string CODE = "PRESS AGAIN TO RETURN TO MENU";
if (stringInVector(Notifier::get()->getCodes(), CODE)) { if (stringInVector(Notifier::get()->getCodes(), CODE)) {
@@ -45,6 +43,11 @@ namespace GlobalInputs {
return; return;
} }
#ifdef __EMSCRIPTEN__
// A la versió web no es pot eixir del joc des de fora de l'escena GAME
// (el navegador gestiona la pestanya; Escape no tanca res).
return;
#else
// Comportamiento normal fuera del modo kiosko // Comportamiento normal fuera del modo kiosko
const std::string CODE = "PRESS AGAIN TO EXIT"; const std::string CODE = "PRESS AGAIN TO EXIT";
if (stringInVector(Notifier::get()->getCodes(), CODE)) { if (stringInVector(Notifier::get()->getCodes(), CODE)) {
@@ -150,6 +153,13 @@ namespace GlobalInputs {
// Detecta qué acción global ha sido presionada (si alguna) // Detecta qué acción global ha sido presionada (si alguna)
auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity) auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity)
// Qualsevol botó del comandament actua com a ACCEPT (saltar escenes
// d'attract mode: logo, loading, credits, demo, ending...). El botó
// BACK queda filtrat prèviament a GlobalEvents per no colidir amb EXIT
// (excepte en emscripten, on BACK no pot sortir i sí pot saltar).
if (GlobalEvents::consumeGamepadButtonPressed()) {
return InputAction::ACCEPT;
}
if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) { if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::EXIT; return InputAction::EXIT;
} }

View File

@@ -10,6 +10,41 @@
#include "game/options.hpp" // Para Options::controls #include "game/options.hpp" // Para Options::controls
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Singleton // Singleton
Input* Input::instance = nullptr; Input* Input::instance = nullptr;
@@ -390,12 +425,21 @@ void Input::update() { // NOLINT(readability-convert-member-functions-to-static
// --- MANDOS --- // --- MANDOS ---
for (const auto& gamepad : gamepads_) { for (const auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) { for (auto& [action, state] : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0; bool is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(state.button))) != 0;
// JUMP accepta qualsevol dels 4 botons frontals (South/East/North/West)
if (action == Action::JUMP) {
is_down_now = is_down_now ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_SOUTH) != 0) ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_EAST) != 0) ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_NORTH) != 0) ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_WEST) != 0);
}
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo // El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = button_is_down_now && !binding.second.is_held; state.just_pressed = is_down_now && !state.is_held;
binding.second.is_held = button_is_down_now; state.is_held = is_down_now;
} }
} }
} }
@@ -411,6 +455,7 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string { // NOLINT(read
} }
auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability-convert-member-functions-to-static) auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
installWebStandardMapping(device_index);
SDL_Gamepad* pad = SDL_OpenGamepad(device_index); SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) { if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n'; std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
@@ -421,7 +466,13 @@ auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability
auto name = gamepad->name; auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n'; std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad)); gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
// Aplica els bindings d'Options al nou gamepad (en hot-plug/wasm el ctor
// ja ha cridat applyGamepadBindingsFromOptions però llavors gamepads_
// estava buit i no s'ha fet res).
applyGamepadBindingsFromOptions();
return name;
} }
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static) auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
@@ -433,7 +484,7 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readabi
std::string name = (*it)->name; std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n'; std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it); gamepads_.erase(it);
return name + " DISCONNECTED"; return name;
} }
std::cerr << "No se encontró el gamepad con ID " << id << '\n'; std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {}; return {};

View File

@@ -51,16 +51,34 @@ class Input {
std::string path; // Ruta del dispositivo std::string path; // Ruta del dispositivo
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
static auto trimName(const char* raw) -> std::string {
std::string s(raw != nullptr ? raw : "");
const auto pos = s.find_first_of("([");
if (pos != std::string::npos) { s.erase(pos); }
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
return s;
}
explicit Gamepad(SDL_Gamepad* gamepad) explicit Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad), : pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))), instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))), name(trimName(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))), path(std::string(SDL_GetGamepadPath(pad))),
bindings{ bindings{
// Movimiento del jugador // Movimiento del jugador
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, {Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}}, {Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}} {} {Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
// Botó BACK del mando → sortir escena / tancar joc
{Action::EXIT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}},
{Action::CANCEL, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}},
// Botó START del mando → pausa
{Action::PAUSE, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
// Shoulders → paleta següent / mode d'ordenació de paleta següent
{Action::NEXT_PALETTE, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)}},
{Action::NEXT_PALETTE_SORT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)}}} {}
~Gamepad() { ~Gamepad() {
if (pad != nullptr) { if (pad != nullptr) {

View File

@@ -119,9 +119,10 @@ void RenderInfo::render() const {
// Fuente: preferir la de la consola si está disponible // Fuente: preferir la de la consola si está disponible
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText(); auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
// Posición Y: debajo de la consola + offset animado propio // Posición Y: debajo de la consola + altura animada de la pila de notificaciones + offset animado propio
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0; const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
const int Y = CONSOLE_Y + static_cast<int>(y_); const int NOTIFIER_Y = (Notifier::get() != nullptr) ? Notifier::get()->getVisibleHeight() : 0;
const int Y = CONSOLE_Y + NOTIFIER_Y + static_cast<int>(y_);
// Rectángulo de fondo: ancho completo, alto ajustado al texto // Rectángulo de fondo: ancho completo, alto ajustado al texto
const SDL_FRect RECT = { const SDL_FRect RECT = {
@@ -141,17 +142,15 @@ void RenderInfo::render() const {
MSG_COLOR); MSG_COLOR);
} }
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset // Activa o desactiva el overlay (la posición Y se calcula pull-side en render())
void RenderInfo::toggle() { void RenderInfo::toggle() {
switch (status_) { switch (status_) {
case Status::HIDDEN: case Status::HIDDEN:
status_ = Status::RISING; status_ = Status::RISING;
Screen::get()->updateZoomFactor(); Screen::get()->updateZoomFactor();
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
break; break;
case Status::ACTIVE: case Status::ACTIVE:
status_ = Status::VANISHING; status_ = Status::VANISHING;
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
break; break;
default: default:
break; break;

View File

@@ -1,6 +1,10 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <algorithm> // Para max, min, transform #include <algorithm> // Para max, min, transform
#include <cctype> // Para toupper #include <cctype> // Para toupper
@@ -11,23 +15,80 @@
#include <iterator> // Para istreambuf_iterator, operator== #include <iterator> // Para istreambuf_iterator, operator==
#include <string> // Para char_traits, string, operator+, operator== #include <string> // Para char_traits, string, operator+, operator==
#include "core/input/mouse.hpp" // Para updateCursorVisibility #include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/render_info.hpp" // Para RenderInfo
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader (no suportat a WebGL2) #include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader (no suportat a WebGL2)
#endif #endif
#include "core/rendering/surface.hpp" // Para Surface, readPalFile #include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text #include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_cache.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para ResourceHelper #include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "core/resources/resource_list.hpp" // Para Asset, AssetType #include "core/resources/resource_list.hpp" // Para Asset, AssetType
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border #include "game/options.hpp" // Para Options, options, OptionsVideo, Border
#include "game/ui/console.hpp" // Para Console #include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier #include "game/ui/notifier.hpp" // Para Notifier
// [SINGLETON] // [SINGLETON]
Screen* Screen::screen = nullptr; Screen* Screen::screen = nullptr;
#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauració del canvas en wasm/Emscripten
// ============================================================================
//
// Problema: SDL3 + Emscripten no notifica de manera fiable els canvis de mida
// del canvas HTML. En concret:
// - SDL_EVENT_WINDOW_LEAVE_FULLSCREEN no s'emet quan l'usuari surt de
// fullscreen amb Esc/F11 (libsdl-org/SDL#13300).
// - SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED i SDL_EVENT_DISPLAY_ORIENTATION
// tampoc es disparen de manera fiable (libsdl-org/SDL#11389).
// - Resultat: en sortir de fullscreen el canvas queda a la mida correcta
// però SDL encara creu que està en fullscreen amb la resolució anterior,
// i el joc es veu minúscul fins que l'usuari força un refresh manual
// (p. ex., canviant el mode d'escalat amb F7).
//
// Solució: registrem callbacks natius d'Emscripten (fullscreenchange,
// orientationchange) que re-sincronitzen SDL amb l'estat real del navegador.
// NO registrem resize callback: en mòbil el scroll fa que el navegador oculti/
// mostri la barra d'URL, disparant un resize del DOM per cada scroll.
// Tots delegen en Screen::handleCanvasResized(), que crida setVideoMode()
// amb l'estat de fullscreen actualitzat — això és el que realment restaura
// la finestra SDL perquè dins setVideoMode es crida SDL_SetWindowFullscreen,
// que és imprescindible per treure SDL del seu estat intern de fullscreen.
//
// Els callbacks diferixen la feina amb emscripten_async_call(0ms) perquè
// quan l'event es dispara el navegador encara no ha acabat de redimensionar
// el canvas: llegir la mida en aquest instant donaria un valor obsolet.
// Posposar al següent tick del event loop garanteix que el canvas ja està
// estable quan actuem.
//
// Referències:
// - https://github.com/libsdl-org/SDL/issues/13300
// - https://github.com/libsdl-org/SDL/issues/11389
// - https://discourse.libsdl.org/t/sdl-emscripten-allow-resize-events-on-fullscreen-windows/66279
// ============================================================================
namespace {
void deferredCanvasResize(void* /*user_data*/) {
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
}
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
// Actualitzem Options::video.fullscreen amb l'estat real del navegador
// abans de diferir la restauració: quan l'usuari surt amb Esc no passem
// per setVideoMode() i l'estat intern quedaria desincronitzat.
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif
// [SINGLETON] Crearemos el objeto con esta función estática // [SINGLETON] Crearemos el objeto con esta función estática
void Screen::init() { void Screen::init() {
Screen::screen = new Screen(); Screen::screen = new Screen();
@@ -168,6 +229,20 @@ void Screen::toggleVideoMode() {
setVideoMode(Options::video.fullscreen); setVideoMode(Options::video.fullscreen);
} }
// Re-sincronitza SDL amb l'estat real del canvas del navegador. L'invoquen els
// callbacks natius d'Emscripten definits a dalt (vegeu el bloc de documentació
// just després dels includes) quan es detecta un fullscreenchange, resize o
// orientationchange. Delegar a setVideoMode() és el que realment restaura la
// finestra: per sota crida SDL_SetWindowFullscreen, imprescindible perquè
// SDL tregui el seu estat intern de fullscreen quan l'usuari ha sortit amb
// Esc sense passar pel nostre toggleVideoMode(). No fem res fora d'emscripten
// perquè en desktop SDL ja emet SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED correctament.
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
setVideoMode(Options::video.fullscreen);
#endif
}
// Reduce el tamaño de la ventana // Reduce el tamaño de la ventana
auto Screen::decWindowZoom() -> bool { auto Screen::decWindowZoom() -> bool {
if (static_cast<int>(Options::video.fullscreen) == 0) { if (static_cast<int>(Options::video.fullscreen) == 0) {
@@ -295,16 +370,16 @@ void Screen::adjustWindowSize() {
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0); window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0); window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
// Reservamos memoria una sola vez. // border_surface_ sempre té el tamany complet del borde (game + 2*border_w/h),
// Si el buffer es más pequeño que la superficie, crash asegurado. // independentment de si el borde està visible o no. El buffer ARGB que l'ombra
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_)); // ha de ser ALMENYS tan gran com la surface; si no, toARGBBuffer() escriu fora
// de bounds i corromp el heap (bug latent a desktop fins a disparar-lo un toggleBorder).
const size_t FULL_BORDER_BUFFER_SIZE =
static_cast<size_t>(Options::game.width + (Options::video.border.width * 2)) *
static_cast<size_t>(Options::game.height + (Options::video.border.height * 2));
border_pixel_buffer_.resize(FULL_BORDER_BUFFER_SIZE);
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height)); game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
if (Options::video.border.enabled) {
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
}
// Lógica de centrado y redimensionado de ventana SDL // Lógica de centrado y redimensionado de ventana SDL
if (static_cast<int>(Options::video.fullscreen) == 0) { if (static_cast<int>(Options::video.fullscreen) == 0) {
int old_w; int old_w;
@@ -743,10 +818,25 @@ auto Screen::initSDLVideo() -> bool {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED); SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
registerEmscriptenEventCallbacks();
std::cout << "Video system initialized successfully\n"; std::cout << "Video system initialized successfully\n";
return true; return true;
} }
// Registra els callbacks natius d'Emscripten que restauren el canvas quan
// SDL3 no emet els events equivalents. Fora d'Emscripten és un no-op.
// Vegeu el bloc de documentació a dalt del fitxer per al context complet.
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
// 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,
// i això portaria a cridar setVideoMode innecessàriament. Alineat amb CC i CCAE.
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// Crea el objeto de texto // Crea el objeto de texto
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static) void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
// Carga la surface de la fuente directamente del archivo // Carga la surface de la fuente directamente del archivo

View File

@@ -38,6 +38,7 @@ class Screen {
// Video y ventana // Video y ventana
void setVideoMode(bool mode); // Establece el modo de video void setVideoMode(bool mode); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // Restaura el canvas quan SDL3 no reporta el canvi (emscripten only: sortida de fullscreen amb Esc, rotació, resize); no-op fora d'emscripten
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
@@ -125,18 +126,19 @@ class Screen {
static Screen* screen; static Screen* screen;
// Métodos privados // Métodos privados
void renderNotifications() const; // Dibuja las notificaciones void renderNotifications() const; // Dibuja las notificaciones
void adjustWindowSize(); // Calcula el tamaño de la ventana void adjustWindowSize(); // Calcula el tamaño de la ventana
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
void surfaceToTexture(); // Copia la surface a la textura void surfaceToTexture(); // Copia la surface a la textura
void textureToRenderer(); // Copia la textura al renderizador void textureToRenderer(); // Copia la textura al renderizador
void renderOverlays(); // Renderiza todos los overlays void renderOverlays(); // Renderiza todos los overlays
void initShaders(); // Inicializa los shaders void initShaders(); // Inicializa los shaders
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void createText(); // Crea el objeto de texto void registerEmscriptenEventCallbacks(); // Registra els callbacks natius per restaurar el canvas en wasm (no-op fora d'emscripten)
void createText(); // Crea el objeto de texto
// Constructor y destructor // Constructor y destructor
Screen(); Screen();

View File

@@ -8,11 +8,11 @@
#include <iostream> // std::cout #include <iostream> // std::cout
#ifndef __APPLE__ #ifndef __APPLE__
#include "core/rendering/sdl3gpu/crtpi_frag_spv.h" #include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/downscale_frag_spv.h" #include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
#include "core/rendering/sdl3gpu/postfx_frag_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/postfx_vert_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/upscale_frag_spv.h" #include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
#endif #endif
#ifdef __APPLE__ #ifdef __APPLE__

View File

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

View File

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

View File

@@ -219,7 +219,7 @@ AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: MovingSprite(std::move(surface), pos) { : MovingSprite(std::move(surface), pos) {
// animations_ queda buit (protegit per el guard de animate()) // animations_ queda buit (protegit per el guard de animate())
if (surface_) { if (surface_) {
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()}; clip_ = {.x = 0, .y = 0, .w = static_cast<float>(surface_->getWidth()), .h = static_cast<float>(surface_->getHeight())};
} }
} }

View File

@@ -35,8 +35,8 @@ auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDir
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos) DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: AnimatedSprite(std::move(surface), pos) { : AnimatedSprite(std::move(surface), pos) {
if (surface_) { if (surface_) {
const int W = static_cast<int>(surface_->getWidth()); const int W = surface_->getWidth();
const int H = static_cast<int>(surface_->getHeight()); const int H = surface_->getHeight();
surface_display_ = std::make_shared<Surface>(W, H); surface_display_ = std::make_shared<Surface>(W, H);
surface_display_->setTransparentColor(surface_->getTransparentColor()); surface_display_->setTransparentColor(surface_->getTransparentColor());
surface_display_->clear(surface_->getTransparentColor()); surface_display_->clear(surface_->getTransparentColor());
@@ -47,8 +47,8 @@ DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
DissolveSprite::DissolveSprite(const AnimationResource& data) DissolveSprite::DissolveSprite(const AnimationResource& data)
: AnimatedSprite(data) { : AnimatedSprite(data) {
if (surface_) { if (surface_) {
const int W = static_cast<int>(surface_->getWidth()); const int W = surface_->getWidth();
const int H = static_cast<int>(surface_->getHeight()); const int H = surface_->getHeight();
surface_display_ = std::make_shared<Surface>(W, H); surface_display_ = std::make_shared<Surface>(W, H);
surface_display_->setTransparentColor(surface_->getTransparentColor()); surface_display_->setTransparentColor(surface_->getTransparentColor());
// Inicialitza tots els píxels com a transparents // Inicialitza tots els píxels com a transparents
@@ -75,8 +75,8 @@ void DissolveSprite::rebuildDisplaySurface() {
auto src_data = surface_->getSurfaceData(); auto src_data = surface_->getSurfaceData();
auto dst_data = surface_display_->getSurfaceData(); auto dst_data = surface_display_->getSurfaceData();
const int SRC_W = static_cast<int>(src_data->width); const int SRC_W = src_data->width;
const int DST_W = static_cast<int>(dst_data->width); const int DST_W = dst_data->width;
const Uint8 TRANSPARENT = surface_->getTransparentColor(); const Uint8 TRANSPARENT = surface_->getTransparentColor();
// Esborra frame anterior si ha canviat // Esborra frame anterior si ha canviat

View File

@@ -19,7 +19,7 @@ Sprite::Sprite() = default;
Sprite::Sprite(std::shared_ptr<Surface> surface) Sprite::Sprite(std::shared_ptr<Surface> surface)
: surface_(std::move(surface)), : surface_(std::move(surface)),
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()}, pos_{0.0F, 0.0F, static_cast<float>(surface_->getWidth()), static_cast<float>(surface_->getHeight())},
clip_(pos_) {} clip_(pos_) {}
// Muestra el sprite por pantalla // Muestra el sprite por pantalla

View File

@@ -104,7 +104,7 @@ Surface::Surface(const std::string& file_path)
} }
// Carga una superficie desde un archivo // Carga una superficie desde un archivo
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static) auto Surface::loadSurface(const std::string& file_path) -> SurfaceData {
// Load file using ResourceHelper (supports both filesystem and pack) // Load file using ResourceHelper (supports both filesystem and pack)
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path); std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
if (buffer.empty()) { if (buffer.empty()) {
@@ -129,7 +129,7 @@ auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOL
// Crear y devolver directamente el objeto SurfaceData // Crear y devolver directamente el objeto SurfaceData
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]"); printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
return {static_cast<float>(w), static_cast<float>(h), pixels}; return {static_cast<int>(w), static_cast<int>(h), pixels};
} }
// Carga una paleta desde un archivo // Carga una paleta desde un archivo
@@ -149,7 +149,7 @@ void Surface::setColor(int index, Uint32 color) {
// Rellena la superficie con un color // Rellena la superficie con un color
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height; const size_t TOTAL_PIXELS = static_cast<size_t>(surface_data_->width) * static_cast<size_t>(surface_data_->height);
Uint8* data_ptr = surface_data_->data.get(); Uint8* data_ptr = surface_data_->data.get();
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color); std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
} }
@@ -165,19 +165,19 @@ void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-conve
} }
// Obtiene el color de un pixel de la surface_data // Obtiene el color de un pixel de la surface_data
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; } auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * surface_data_->width)]; }
// Dibuja un rectangulo relleno // Dibuja un rectangulo relleno
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
// Limitar los valores del rectángulo al tamaño de la superficie // Limitar los valores del rectángulo al tamaño de la superficie
float x_start = std::max(0.0F, rect->x); float x_start = std::max(0.0F, rect->x);
float y_start = std::max(0.0F, rect->y); float y_start = std::max(0.0F, rect->y);
float x_end = std::min(rect->x + rect->w, surface_data_->width); float x_end = std::min(rect->x + rect->w, static_cast<float>(surface_data_->width));
float y_end = std::min(rect->y + rect->h, surface_data_->height); float y_end = std::min(rect->y + rect->h, static_cast<float>(surface_data_->height));
// Rellenar fila a fila con memset (memoria contigua por fila) // Rellenar fila a fila con memset (memoria contigua por fila)
Uint8* data_ptr = surface_data_->data.get(); Uint8* data_ptr = surface_data_->data.get();
const int SURF_WIDTH = static_cast<int>(surface_data_->width); const int SURF_WIDTH = surface_data_->width;
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start); const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) { for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH); std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
@@ -189,12 +189,12 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
// Limitar los valores del rectángulo al tamaño de la superficie // Limitar los valores del rectángulo al tamaño de la superficie
float x_start = std::max(0.0F, rect->x); float x_start = std::max(0.0F, rect->x);
float y_start = std::max(0.0F, rect->y); float y_start = std::max(0.0F, rect->y);
float x_end = std::min(rect->x + rect->w, surface_data_->width); float x_end = std::min(rect->x + rect->w, static_cast<float>(surface_data_->width));
float y_end = std::min(rect->y + rect->h, surface_data_->height); float y_end = std::min(rect->y + rect->h, static_cast<float>(surface_data_->height));
// Dibujar bordes horizontales con memset (líneas contiguas en memoria) // Dibujar bordes horizontales con memset (líneas contiguas en memoria)
Uint8* data_ptr = surface_data_->data.get(); Uint8* data_ptr = surface_data_->data.get();
const int SURF_WIDTH = static_cast<int>(surface_data_->width); const int SURF_WIDTH = surface_data_->width;
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start); const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH); std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH); std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
@@ -211,68 +211,38 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
} }
} }
// Dibuja una linea // Dibuja una linea (Bresenham en enteros)
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static) void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
// Calcula las diferencias int ix1 = static_cast<int>(std::lround(x1));
float dx = std::abs(x2 - x1); int iy1 = static_cast<int>(std::lround(y1));
float dy = std::abs(y2 - y1); const int IX2 = static_cast<int>(std::lround(x2));
const int IY2 = static_cast<int>(std::lround(y2));
// Determina la dirección del incremento const int DX = std::abs(IX2 - ix1);
float sx = (x1 < x2) ? 1 : -1; const int DY = std::abs(IY2 - iy1);
float sy = (y1 < y2) ? 1 : -1; const int SX = (ix1 < IX2) ? 1 : -1;
const int SY = (iy1 < IY2) ? 1 : -1;
float err = dx - dy; const int SURF_W = surface_data_->width;
const int SURF_H = surface_data_->height;
Uint8* data_ptr = surface_data_->data.get();
int err = DX - DY;
while (true) { while (true) {
// Asegúrate de no dibujar fuera de los límites de la superficie if (ix1 >= 0 && ix1 < SURF_W && iy1 >= 0 && iy1 < SURF_H) {
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) { data_ptr[ix1 + (iy1 * SURF_W)] = color;
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
} }
if (ix1 == IX2 && iy1 == IY2) {
// Si alcanzamos el punto final, salimos
if (x1 == x2 && y1 == y2) {
break; break;
} }
int e2 = 2 * err; int e2 = 2 * err;
if (e2 > -dy) { if (e2 > -DY) {
err -= dy; err -= DY;
x1 += sx; ix1 += SX;
} }
if (e2 < dx) { if (e2 < DX) {
err += dx; err += DX;
y1 += sy; iy1 += SY;
}
}
}
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
// Limitar la región para evitar accesos fuera de rango en origen
w = std::min(w, surface_data_->width - sx);
h = std::min(h, surface_data_->height - sy);
// Limitar la región para evitar accesos fuera de rango en destino
w = std::min(w, surface_data->width - dx);
h = std::min(h, surface_data->height - dy);
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data->data.get();
for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) {
// Verificar que las coordenadas de destino están dentro de los límites
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
int src_x = sx + ix;
int src_y = sy + iy;
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) {
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
}
}
}
} }
} }
} }
@@ -283,18 +253,14 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
// Determina la región de origen (clip) a renderizar // Determina la región de origen (clip) a renderizar
float sx = (src_rect != nullptr) ? src_rect->x : 0; float sx = (src_rect != nullptr) ? src_rect->x : 0;
float sy = (src_rect != nullptr) ? src_rect->y : 0; float sy = (src_rect != nullptr) ? src_rect->y : 0;
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width; float w = (src_rect != nullptr) ? src_rect->w : static_cast<float>(surface_data_->width);
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height; float h = (src_rect != nullptr) ? src_rect->h : static_cast<float>(surface_data_->height);
// Limitar la región para evitar accesos fuera de rango en origen // Limitar la región para evitar accesos fuera de rango (origen y destino)
w = std::min(w, surface_data_->width - sx); w = std::min(w, static_cast<float>(surface_data_->width) - sx);
h = std::min(h, surface_data_->height - sy); h = std::min(h, static_cast<float>(surface_data_->height) - sy);
w = std::min(w, surface_data_dest->width - x); w = std::min(w, static_cast<float>(surface_data_dest->width - x));
h = std::min(h, surface_data_dest->height - y); h = std::min(h, static_cast<float>(surface_data_dest->height - y));
// Limitar la región para evitar accesos fuera de rango en destino
w = std::min(w, surface_data_dest->width - x);
h = std::min(h, surface_data_dest->height - y);
// Renderiza píxel por píxel aplicando el flip si es necesario // Renderiza píxel por píxel aplicando el flip si es necesario
const Uint8* src_ptr = surface_data_->data.get(); const Uint8* src_ptr = surface_data_->data.get();
@@ -322,7 +288,7 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
} }
// Helper para calcular coordenadas con flip // Helper para calcular coordenadas con flip
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) { void Surface::calculateFlippedCoords(int ix, int iy, int sx, int sy, int w, int h, SDL_FlipMode flip, int& src_x, int& src_y) {
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix); src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy); src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
} }
@@ -418,11 +384,11 @@ void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 tar
continue; // Saltar píxeles fuera del rango del destino continue; // Saltar píxeles fuera del rango del destino
} }
// Copia el píxel si no es transparente // Copia el píxel si no es transparente; aplica sub_palette_ como el resto de render*
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))]; Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) { if (color != static_cast<Uint8>(transparent_color_)) {
surface_data->data[dest_x + (dest_y * surface_data->width)] = surface_data->data[dest_x + (dest_y * surface_data->width)] =
(color == source_color) ? target_color : color; (color == source_color) ? target_color : sub_palette_[color];
} }
} }
} }
@@ -452,14 +418,14 @@ static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> f
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const { void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0; const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0; const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width); const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height); const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData(); auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
for (int row = 0; row < SH; row++) { for (int row = 0; row < SH; row++) {
const int SCREEN_Y = y + row; const int SCREEN_Y = y + row;
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) { if (SCREEN_Y < 0 || SCREEN_Y >= surface_data_dest->height) {
continue; continue;
} }
@@ -467,11 +433,11 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
for (int col = 0; col < SW; col++) { for (int col = 0; col < SW; col++) {
const int SCREEN_X = x + col; const int SCREEN_X = x + col;
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) { if (SCREEN_X < 0 || SCREEN_X >= surface_data_dest->width) {
continue; continue;
} }
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)]; const Uint8 COLOR = surface_data_->data[((SY + row) * surface_data_->width) + (SX + col)];
if (COLOR == static_cast<Uint8>(transparent_color_)) { if (COLOR == static_cast<Uint8>(transparent_color_)) {
continue; continue;
} }
@@ -480,7 +446,7 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
continue; // Pixel tapat per la zona de fade continue; // Pixel tapat per la zona de fade
} }
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR]; surface_data_dest->data[SCREEN_X + (SCREEN_Y * surface_data_dest->width)] = sub_palette_[COLOR];
} }
} }
} }
@@ -489,14 +455,14 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const { void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0; const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0; const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width); const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height); const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData(); auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
for (int row = 0; row < SH; row++) { for (int row = 0; row < SH; row++) {
const int SCREEN_Y = y + row; const int SCREEN_Y = y + row;
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) { if (SCREEN_Y < 0 || SCREEN_Y >= surface_data_dest->height) {
continue; continue;
} }
@@ -504,11 +470,11 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
for (int col = 0; col < SW; col++) { for (int col = 0; col < SW; col++) {
const int SCREEN_X = x + col; const int SCREEN_X = x + col;
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) { if (SCREEN_X < 0 || SCREEN_X >= surface_data_dest->width) {
continue; continue;
} }
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)]; const Uint8 COLOR = surface_data_->data[((SY + row) * surface_data_->width) + (SX + col)];
if (COLOR == static_cast<Uint8>(transparent_color_)) { if (COLOR == static_cast<Uint8>(transparent_color_)) {
continue; continue;
} }
@@ -518,7 +484,7 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
} }
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR]; const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR; surface_data_dest->data[SCREEN_X + (SCREEN_Y * surface_data_dest->width)] = OUT_COLOR;
} }
} }
} }
@@ -527,8 +493,8 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
void Surface::toARGBBuffer(Uint32* buffer) const { void Surface::toARGBBuffer(Uint32* buffer) const {
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; } if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
const int WIDTH = static_cast<int>(surface_data_->width); const int WIDTH = surface_data_->width;
const int HEIGHT = static_cast<int>(surface_data_->height); const int HEIGHT = surface_data_->height;
const Uint8* src = surface_data_->data.get(); const Uint8* src = surface_data_->data.get();
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango // Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
@@ -639,11 +605,8 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
// Realiza un efecto de fundido en la paleta principal // Realiza un efecto de fundido en la paleta principal
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static) auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
// Verificar que el tamaño mínimo de palette_ sea adecuado
static constexpr int PALETTE_SIZE = 19; static constexpr int PALETTE_SIZE = 19;
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) { static_assert(std::tuple_size_v<Palette> >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation.");
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
}
// Desplazar colores (pares e impares) // Desplazar colores (pares e impares)
for (int i = 18; i > 1; --i) { for (int i = 18; i > 1; --i) {
@@ -673,11 +636,8 @@ auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-conv
// Actualizar el último tick // Actualizar el último tick
last_tick_ = current_tick; last_tick_ = current_tick;
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
static constexpr int SUB_PALETTE_SIZE = 19; static constexpr int SUB_PALETTE_SIZE = 19;
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) { static_assert(std::tuple_size_v<SubPalette> >= SUB_PALETTE_SIZE, "Sub-palette size is insufficient for fadeSubPalette operation.");
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
}
// Desplazar colores (pares e impares) // Desplazar colores (pares e impares)
for (int i = 18; i > 1; --i) { for (int i = 18; i > 1; --i) {

View File

@@ -22,8 +22,8 @@ auto readPalFile(const std::string& file_path) -> Palette;
struct SurfaceData { struct SurfaceData {
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
float width; // Ancho de la imagen int width; // Ancho de la imagen
float height; // Alto de la imagen int height; // Alto de la imagen
// Constructor por defecto // Constructor por defecto
SurfaceData() SurfaceData()
@@ -32,13 +32,13 @@ struct SurfaceData {
height(0) {} height(0) {}
// Constructor que inicializa dimensiones y asigna memoria // Constructor que inicializa dimensiones y asigna memoria
SurfaceData(float w, float h) SurfaceData(int w, int h)
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())), : data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w) * static_cast<size_t>(h)](), std::default_delete<Uint8[]>())),
width(w), width(w),
height(h) {} height(h) {}
// Constructor para inicializar directamente con datos // Constructor para inicializar directamente con datos
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels) SurfaceData(int w, int h, std::shared_ptr<Uint8[]> pixels)
: data(std::move(pixels)), : data(std::move(pixels)),
width(w), width(w),
height(h) {} height(h) {}
@@ -56,6 +56,9 @@ struct SurfaceData {
class Surface { class Surface {
private: private:
// shared_ptr porque render() accede al SurfaceData propio y al del renderer
// surface (ver getRendererSurface()) de forma efímera; con self-blit ambos
// pueden alias y el refcount evita free accidental durante el recorrido.
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
Palette palette_; // Paleta para volcar la SurfaceData a una Textura Palette palette_; // Paleta para volcar la SurfaceData a una Textura
SubPalette sub_palette_; // Paleta para reindexar colores SubPalette sub_palette_; // Paleta para reindexar colores
@@ -77,7 +80,6 @@ class Surface {
void loadPalette(const Palette& palette); void loadPalette(const Palette& palette);
// Copia una región de la SurfaceData de origen a la SurfaceData de destino // Copia una región de la SurfaceData de origen a la SurfaceData de destino
void render(float dx, float dy, float sx, float sy, float w, float h);
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE); void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE); void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
@@ -127,11 +129,10 @@ class Surface {
// Metodos para gestionar surface_data_ // Metodos para gestionar surface_data_
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; } [[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
// Obtien ancho y alto // Obtien ancho y alto
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; } [[nodiscard]] auto getWidth() const -> int { return surface_data_->width; }
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; } [[nodiscard]] auto getHeight() const -> int { return surface_data_->height; }
// Color transparente // Color transparente
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; } [[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
@@ -146,7 +147,7 @@ class Surface {
private: private:
// Helper para calcular coordenadas con flip // Helper para calcular coordenadas con flip
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y); static void calculateFlippedCoords(int ix, int iy, int sx, int sy, int w, int h, SDL_FlipMode flip, int& src_x, int& src_y);
// Helper para copiar un pixel si no es transparente // Helper para copiar un pixel si no es transparente
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const; void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;

View File

@@ -38,10 +38,10 @@ namespace Resource {
// [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él // [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él
auto Cache::get() -> Cache* { return Cache::cache; } auto Cache::get() -> Cache* { return Cache::cache; }
// Constructor // Constructor — no dispara la carga. Director llama a beginLoad() + loadStep()
// desde iterate() para que el bucle SDL3 esté vivo durante la carga.
Cache::Cache() Cache::Cache()
: loading_text_(Screen::get()->getText()) { : loading_text_(Screen::get()->getText()) {
load();
} }
// Vacia todos los vectores de recursos // Vacia todos los vectores de recursos
@@ -53,12 +53,11 @@ namespace Resource {
text_files_.clear(); text_files_.clear();
texts_.clear(); texts_.clear();
animations_.clear(); animations_.clear();
rooms_.clear();
} }
// Carga todos los recursos // Carga todos los recursos de golpe (usado solo por reload() en hot-reload de debug)
void Cache::load() { void Cache::load() {
// Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga,
// por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init().
calculateTotal(); calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK)); Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES" << '\n'; std::cout << "\n** LOADING RESOURCES" << '\n';
@@ -73,7 +72,162 @@ namespace Resource {
std::cout << "\n** RESOURCES LOADED" << '\n'; std::cout << "\n** RESOURCES LOADED" << '\n';
} }
// Recarga todos los recursos // Prepara el loader incremental. Director lo llama una vez tras Cache::init().
void Cache::beginLoad() {
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES (incremental)" << '\n';
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
}
auto Cache::isLoadDone() const -> bool {
return stage_ == LoadStage::DONE;
}
// Carga assets hasta agotar el presupuesto de tiempo o completar todas las etapas.
// Devuelve true cuando ya no queda nada por cargar.
auto Cache::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) return true;
const Uint64 start_ns = SDL_GetTicksNS();
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
auto listOf = [](List::Type t) { return List::get()->getListByType(t); };
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::SOUNDS: {
auto list = listOf(List::Type::SOUND);
if (stage_index_ == 0) {
std::cout << "\n>> SOUND FILES" << '\n';
sounds_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::MUSICS;
stage_index_ = 0;
break;
}
loadOneSound(stage_index_++);
break;
}
case LoadStage::MUSICS: {
auto list = listOf(List::Type::MUSIC);
if (stage_index_ == 0) {
std::cout << "\n>> MUSIC FILES" << '\n';
musics_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::SURFACES;
stage_index_ = 0;
break;
}
loadOneMusic(stage_index_++);
break;
}
case LoadStage::SURFACES: {
auto list = listOf(List::Type::BITMAP);
if (stage_index_ == 0) {
std::cout << "\n>> SURFACES" << '\n';
surfaces_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::SURFACES_POST;
stage_index_ = 0;
break;
}
loadOneSurface(stage_index_++);
break;
}
case LoadStage::SURFACES_POST: {
finalizeSurfaces();
stage_ = LoadStage::PALETTES;
stage_index_ = 0;
break;
}
case LoadStage::PALETTES: {
auto list = listOf(List::Type::PALETTE);
if (stage_index_ == 0) {
std::cout << "\n>> PALETTES" << '\n';
palettes_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXT_FILES;
stage_index_ = 0;
break;
}
loadOnePalette(stage_index_++);
break;
}
case LoadStage::TEXT_FILES: {
auto list = listOf(List::Type::FONT);
if (stage_index_ == 0) {
std::cout << "\n>> TEXT FILES" << '\n';
text_files_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ANIMATIONS;
stage_index_ = 0;
break;
}
loadOneTextFile(stage_index_++);
break;
}
case LoadStage::ANIMATIONS: {
auto list = listOf(List::Type::ANIMATION);
if (stage_index_ == 0) {
std::cout << "\n>> ANIMATIONS" << '\n';
animations_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ROOMS;
stage_index_ = 0;
break;
}
loadOneAnimation(stage_index_++);
break;
}
case LoadStage::ROOMS: {
auto list = listOf(List::Type::ROOM);
if (stage_index_ == 0) {
std::cout << "\n>> ROOMS" << '\n';
rooms_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXTS;
stage_index_ = 0;
break;
}
loadOneRoom(stage_index_++);
break;
}
case LoadStage::TEXTS: {
// createText itera sobre una lista fija de 5 fuentes
constexpr size_t TEXT_COUNT = 5;
if (stage_index_ == 0) {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
texts_.clear();
}
if (stage_index_ >= TEXT_COUNT) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "\n** RESOURCES LOADED" << '\n';
break;
}
createOneText(stage_index_++);
break;
}
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
}
return stage_ == LoadStage::DONE;
}
// Recarga todos los recursos (síncrono, solo para hot-reload de debug)
void Cache::reload() { void Cache::reload() {
clear(); clear();
load(); load();
@@ -217,96 +371,96 @@ namespace Resource {
throw; throw;
} }
// Carga los sonidos // Lista fija de text objects. Compartida entre createText() y createOneText(i).
void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static) namespace {
std::cout << "\n>> SOUND FILES" << '\n'; struct TextObjectInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
const std::vector<TextObjectInfo>& getTextObjectInfos() {
static const std::vector<TextObjectInfo> info = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
return info;
}
} // namespace
// --- Helpers incrementales (un asset por llamada) ---
void Cache::loadOneSound(size_t index) {
auto list = List::get()->getListByType(List::Type::SOUND); auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear(); const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Sound_t* sound = nullptr;
for (const auto& l : list) { auto audio_data = Helper::loadFile(l);
try { if (!audio_data.empty()) {
auto name = getFileName(l); sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
setCurrentLoading(name);
JA_Sound_t* sound = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
} }
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
} }
} }
// Carga las musicas void Cache::loadOneMusic(size_t index) {
void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> MUSIC FILES" << '\n';
auto list = List::get()->getListByType(List::Type::MUSIC); auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear(); const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Music_t* music = nullptr;
for (const auto& l : list) { auto audio_data = Helper::loadFile(l);
try { if (!audio_data.empty()) {
auto name = getFileName(l); music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
setCurrentLoading(name);
JA_Music_t* music = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress(1);
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
} }
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
} }
} }
// Carga las texturas void Cache::loadOneSurface(size_t index) {
void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SURFACES" << '\n';
auto list = List::get()->getListByType(List::Type::BITMAP); auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear(); const auto& l = list[index];
try {
for (const auto& l : list) { auto name = getFileName(l);
try { setCurrentLoading(name);
auto name = getFileName(l); surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
setCurrentLoading(name); surfaces_.back().surface->setTransparentColor(0);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)}); updateLoadingProgress();
surfaces_.back().surface->setTransparentColor(0); } catch (const std::exception& e) {
updateLoadingProgress(); throwLoadError("BITMAP", l, e);
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
}
} }
}
void Cache::finalizeSurfaces() {
// Reconfigura el color transparente de algunas surfaces // Reconfigura el color transparente de algunas surfaces
getSurface("loading_screen_color.gif")->setTransparentColor(); getSurface("loading_screen_color.gif")->setTransparentColor();
getSurface("ending1.gif")->setTransparentColor(); getSurface("ending1.gif")->setTransparentColor();
@@ -317,108 +471,132 @@ namespace Resource {
getSurface("standard.gif")->setTransparentColor(16); getSurface("standard.gif")->setTransparentColor(16);
} }
// Carga las paletas void Cache::loadOnePalette(size_t index) {
void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static) auto list = List::get()->getListByType(List::Type::PALETTE);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
void Cache::loadOneTextFile(size_t index) {
auto list = List::get()->getListByType(List::Type::FONT);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
void Cache::loadOneAnimation(size_t index) {
auto list = List::get()->getListByType(List::Type::ANIMATION);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
void Cache::loadOneRoom(size_t index) {
auto list = List::get()->getListByType(List::Type::ROOM);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
void Cache::createOneText(size_t index) {
const auto& infos = getTextObjectInfos();
const auto& res_info = infos[index];
texts_.emplace_back(TextResource{
.name = res_info.key,
.text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
// --- Bucles completos (solo usados por reload() síncrono) ---
void Cache::loadSounds() {
std::cout << "\n>> SOUND FILES" << '\n';
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneSound(i);
}
void Cache::loadMusics() {
std::cout << "\n>> MUSIC FILES" << '\n';
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneMusic(i);
}
void Cache::loadSurfaces() {
std::cout << "\n>> SURFACES" << '\n';
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneSurface(i);
finalizeSurfaces();
}
void Cache::loadPalettes() {
std::cout << "\n>> PALETTES" << '\n'; std::cout << "\n>> PALETTES" << '\n';
auto list = List::get()->getListByType(List::Type::PALETTE); auto list = List::get()->getListByType(List::Type::PALETTE);
palettes_.clear(); palettes_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOnePalette(i);
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
} }
// Carga los ficheros de texto void Cache::loadTextFiles() {
void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> TEXT FILES" << '\n'; std::cout << "\n>> TEXT FILES" << '\n';
auto list = List::get()->getListByType(List::Type::FONT); auto list = List::get()->getListByType(List::Type::FONT);
text_files_.clear(); text_files_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneTextFile(i);
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
} }
// Carga las animaciones void Cache::loadAnimations() {
void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> ANIMATIONS" << '\n'; std::cout << "\n>> ANIMATIONS" << '\n';
auto list = List::get()->getListByType(List::Type::ANIMATION); auto list = List::get()->getListByType(List::Type::ANIMATION);
animations_.clear(); animations_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneAnimation(i);
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
// Cargar bytes del archivo YAML sin parsear (carga lazy)
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
} }
// Carga las habitaciones desde archivos YAML void Cache::loadRooms() {
void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> ROOMS" << '\n'; std::cout << "\n>> ROOMS" << '\n';
auto list = List::get()->getListByType(List::Type::ROOM); auto list = List::get()->getListByType(List::Type::ROOM);
rooms_.clear(); rooms_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneRoom(i);
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
} }
void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static) void Cache::createText() {
struct ResourceInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n'; std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
texts_.clear();
std::vector<ResourceInfo> resources = { const auto& infos = getTextObjectInfos();
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"}, for (size_t i = 0; i < infos.size(); ++i) createOneText(i);
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
for (const auto& res_info : resources) {
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
} }
// Vacía el vector de sonidos // Vacía el vector de sonidos
@@ -467,14 +645,20 @@ namespace Resource {
// Muestra el progreso de carga // Muestra el progreso de carga
void Cache::renderProgress() { void Cache::renderProgress() {
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
// Si show=false: pantalla negra y salir
if (!Options::loading.show) {
Screen::get()->render();
return;
}
constexpr float X_PADDING = 60.0F; constexpr float X_PADDING = 60.0F;
constexpr float Y_PADDING = 10.0F; constexpr float Y_PADDING = 10.0F;
constexpr float BAR_HEIGHT = 5.0F; constexpr float BAR_HEIGHT = 5.0F;
const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING; const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING;
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
auto surface = Screen::get()->getRendererSurface(); auto surface = Screen::get()->getRendererSurface();
const auto LOADING_TEXT_COLOR = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE); const auto LOADING_TEXT_COLOR = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE);
const auto BAR_COLOR = static_cast<Uint8>(PaletteColor::WHITE); const auto BAR_COLOR = static_cast<Uint8>(PaletteColor::WHITE);
@@ -508,55 +692,30 @@ namespace Resource {
SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT}; SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
surface->fillRect(&rect_full, BAR_COLOR); surface->fillRect(&rect_full, BAR_COLOR);
#if defined(__EMSCRIPTEN__) || defined(_DEBUG) // Mostra el nom del recurs (o missatge d'espera si ja ha acabat i wait_for_input=true)
// Mostra el nom del recurs que està a punt de carregar-se, centrat sobre la barra const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input;
if (!current_loading_name_.empty()) { const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT ? "PRESS ANY KEY TO CONTINUE" : current_loading_name_;
if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) {
const float TEXT_Y = BAR_POSITION - static_cast<float>(TEXT_HEIGHT) - 2.0F; const float TEXT_Y = BAR_POSITION - static_cast<float>(TEXT_HEIGHT) - 2.0F;
loading_text_->writeColored( loading_text_->writeColored(
CENTER_X - (loading_text_->length(current_loading_name_) / 2), CENTER_X - (loading_text_->length(OVER_BAR_TEXT) / 2),
static_cast<int>(TEXT_Y), static_cast<int>(TEXT_Y),
current_loading_name_, OVER_BAR_TEXT,
LOADING_TEXT_COLOR); LOADING_TEXT_COLOR);
} }
#endif
Screen::get()->render(); Screen::get()->render();
} }
// Desa el nom del recurs que s'està a punt de carregar i repinta immediatament. // Guarda el nombre del recurso que se está a punto de cargar. El repintado
// A wasm/debug serveix per veure exactament en quin fitxer es penja la càrrega. // lo hace el BootLoader (una vez por frame) — aquí solo se actualiza el estado.
void Cache::setCurrentLoading(const std::string& name) { void Cache::setCurrentLoading(const std::string& name) {
current_loading_name_ = name; current_loading_name_ = name;
#if defined(__EMSCRIPTEN__) || defined(_DEBUG)
renderProgress();
checkEvents();
#endif
} }
// Comprueba los eventos de la pantalla de carga // Incrementa el contador de recursos cargados
void Cache::checkEvents() { void Cache::updateLoadingProgress() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
exit(0);
break;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE) {
exit(0);
}
break;
}
}
}
// Actualiza el progreso de carga
void Cache::updateLoadingProgress(int steps) {
count_.add(1); count_.add(1);
if (count_.loaded % steps == 0 || count_.loaded == count_.total) {
renderProgress();
}
checkEvents();
} }
} // namespace Resource } // namespace Resource

View File

@@ -25,7 +25,13 @@ namespace Resource {
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>; auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
auto getRooms() -> std::vector<RoomResource>&; auto getRooms() -> std::vector<RoomResource>&;
void reload(); // Recarga todos los recursos // --- Incremental loading (Director drives this from iterate()) ---
void beginLoad(); // Prepara el estado del loader incremental
auto loadStep(int budget_ms) -> bool; // Carga assets durante budget_ms; devuelve true si ha terminado
void renderProgress(); // Dibuja la barra de progreso (usada por BootLoader)
[[nodiscard]] auto isLoadDone() const -> bool;
void reload(); // Recarga todos los recursos (síncrono, usado en hot-reload de debug)
#ifdef _DEBUG #ifdef _DEBUG
void reloadRoom(const std::string& name); // Recarga una habitación desde disco void reloadRoom(const std::string& name); // Recarga una habitación desde disco
#endif #endif
@@ -47,7 +53,21 @@ namespace Resource {
} }
}; };
// Métodos de carga de recursos // Etapas del loader incremental
enum class LoadStage {
SOUNDS,
MUSICS,
SURFACES,
SURFACES_POST, // Ajuste de transparent colors tras cargar todas las surfaces
PALETTES,
TEXT_FILES,
ANIMATIONS,
ROOMS,
TEXTS,
DONE
};
// Métodos de carga de recursos (bucle completo, usados por reload() síncrono)
void loadSounds(); void loadSounds();
void loadMusics(); void loadMusics();
void loadSurfaces(); void loadSurfaces();
@@ -57,18 +77,27 @@ namespace Resource {
void loadRooms(); void loadRooms();
void createText(); void createText();
// Helpers incrementales: cargan un único asset de la categoría correspondiente
void loadOneSound(size_t index);
void loadOneMusic(size_t index);
void loadOneSurface(size_t index);
void finalizeSurfaces(); // Ajuste de transparent colors tras cargar surfaces
void loadOnePalette(size_t index);
void loadOneTextFile(size_t index);
void loadOneAnimation(size_t index);
void loadOneRoom(size_t index);
void createOneText(size_t index);
// Métodos de limpieza // Métodos de limpieza
void clear(); void clear();
void clearSounds(); void clearSounds();
void clearMusics(); void clearMusics();
// Métodos de gestión de carga // Métodos de gestión de carga
void load(); void load(); // Carga completa síncrona (usado solo por reload())
void calculateTotal(); void calculateTotal();
void renderProgress(); void updateLoadingProgress();
static void checkEvents(); void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs
void updateLoadingProgress(int steps = 5);
void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs i repinta (wasm/debug)
// Helper para mensajes de error de carga // Helper para mensajes de error de carga
[[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e); [[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
@@ -92,7 +121,11 @@ namespace Resource {
ResourceCount count_{}; // Contador de recursos ResourceCount count_{}; // Contador de recursos
std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga
std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar (debug/wasm) std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar
// Estado del loader incremental
LoadStage stage_{LoadStage::DONE}; // Arranca en DONE hasta que beginLoad() lo cambie
size_t stage_index_{0}; // Cursor dentro de la categoría actual
}; };
} // namespace Resource } // namespace Resource

View File

@@ -23,6 +23,7 @@
#include "game/gameplay/cheevos.hpp" // Para Cheevos #include "game/gameplay/cheevos.hpp" // Para Cheevos
#include "game/options.hpp" // Para Options, options, OptionsVideo #include "game/options.hpp" // Para Options, options, OptionsVideo
#include "game/scene_manager.hpp" // Para SceneManager #include "game/scene_manager.hpp" // Para SceneManager
#include "game/scenes/boot_loader.hpp" // Para BootLoader
#include "game/scenes/credits.hpp" // Para Credits #include "game/scenes/credits.hpp" // Para Credits
#include "game/scenes/ending.hpp" // Para Ending #include "game/scenes/ending.hpp" // Para Ending
#include "game/scenes/ending2.hpp" // Para Ending2 #include "game/scenes/ending2.hpp" // Para Ending2
@@ -147,9 +148,15 @@ Director::Director() {
Options::loadFromFile(); Options::loadFromFile();
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
// A la versió web el navegador gestiona la finestra: res de fullscreen ni zoom. // A la versió web el navegador gestiona la finestra: forcem zoom x3
// perquè la textura 256x192 no es vegi minúscula al canvas HTML,
// i desactivem el borde per aprofitar al màxim l'espai del canvas.
Options::video.fullscreen = false; Options::video.fullscreen = false;
Options::window.zoom = 1; Options::video.integer_scale = true;
Options::window.zoom = 4;
Options::video.border.enabled = true;
Options::video.border.height = 8;
Options::video.border.width = 8;
#endif #endif
// Configura la ruta y carga los presets de PostFX // Configura la ruta y carga los presets de PostFX
@@ -171,12 +178,12 @@ Director::Director() {
// Crea los objetos // Crea los objetos
Screen::init(); Screen::init();
// Initialize resources (works for both release and development) // Inicializa el singleton del cache sin disparar la carga. La carga real
// la hace Director::iterate() llamando a Cache::loadStep() en cada frame,
// de forma que la ventana, los eventos y la barra de progreso están vivos
// desde el primer tick.
Resource::Cache::init(); Resource::Cache::init();
Notifier::init("", "8bithud"); Resource::Cache::get()->beginLoad();
RenderInfo::init();
Console::init("8bithud");
Screen::get()->setNotificationsEnabled(true);
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path // Special handling for gamecontrollerdb.txt - SDL needs filesystem path
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
@@ -192,15 +199,38 @@ Director::Director() {
Input::get()->applyKeyboardBindingsFromOptions(); Input::get()->applyKeyboardBindingsFromOptions();
Input::get()->applyGamepadBindingsFromOptions(); Input::get()->applyGamepadBindingsFromOptions();
std::cout << "\n"; // Fin de inicialización mínima de sistemas
// Construeix l'escena inicial (BootLoader). finishBoot() la canviarà a
// LOGO (o la que digui Debug) quan Cache::loadStep() complete la càrrega.
SceneManager::current = SceneManager::Scene::BOOT_LOADER;
switchToActiveScene();
}
// Inicialitzacions que depenen del cache poblat. Es crida des d'iterate()
// just quan Cache::loadStep() retorna true, amb la finestra i el bucle ja vius.
void Director::finishBoot() {
Notifier::init("", "8bithud");
RenderInfo::init();
Console::init("8bithud");
Screen::get()->setNotificationsEnabled(true);
#ifdef _DEBUG #ifdef _DEBUG
Debug::init(); Debug::init();
#ifdef __EMSCRIPTEN__
// A wasm el debug.yaml viu a SYSTEM_FOLDER (MEMFS no persistent) i no està
// disponible. Saltem el loadFromFile i entrem directament a la GAME.
SceneManager::current = SceneManager::Scene::GAME;
#else
Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml")); Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml"));
Debug::get()->loadFromFile(); Debug::get()->loadFromFile();
SceneManager::current = Debug::get()->getInitialScene(); SceneManager::current = Debug::get()->getInitialScene();
MapEditor::init();
#endif #endif
MapEditor::init();
std::cout << "\n"; // Fin de inicialización de sistemas #else
// En release, pasamos a LOGO siempre tras la carga
SceneManager::current = SceneManager::Scene::LOGO;
#endif
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos) // Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
@@ -222,15 +252,17 @@ Director::Director() {
#else #else
Cheevos::init(Resource::List::get()->get("cheevos.bin")); Cheevos::init(Resource::List::get()->get("cheevos.bin"));
#endif #endif
// Construeix la primera escena (LOGO per defecte, o la que digui Debug)
switchToActiveScene();
} }
Director::~Director() { Director::~Director() {
// Guarda las opciones a un fichero // Guarda las opciones a un fichero
Options::saveToFile(); Options::saveToFile();
// Destruir l'escena activa ABANS dels singletons. Si no, el unique_ptr membre
// destrueix l'escena al final del destructor — quan Audio, Screen, Resource...
// ja són morts — i qualsevol accés en els seus destructors és un UAF.
active_scene_.reset();
// Destruye los singletones // Destruye los singletones
Cheevos::destroy(); Cheevos::destroy();
Locale::destroy(); Locale::destroy();
@@ -344,6 +376,10 @@ void Director::switchToActiveScene() {
active_scene_.reset(); active_scene_.reset();
switch (SceneManager::current) { switch (SceneManager::current) {
case SceneManager::Scene::BOOT_LOADER:
active_scene_ = std::make_unique<BootLoader>();
break;
case SceneManager::Scene::LOGO: case SceneManager::Scene::LOGO:
active_scene_ = std::make_unique<Logo>(); active_scene_ = std::make_unique<Logo>();
break; break;
@@ -394,6 +430,30 @@ auto Director::iterate() -> SDL_AppResult {
return SDL_APP_SUCCESS; return SDL_APP_SUCCESS;
} }
// Fase de boot: anem cridant loadStep() fins que el cache estiga ple.
// Durant aquesta fase l'escena activa és BootLoader (una barra de progrés).
if (boot_loading_) {
try {
// Budget de 50ms: durant el boot el joc va a ~15-20 FPS, suficient
// per veure la barra avançar suau i processar events del WM/ESC,
// i evita el 50% d'ineficiència que provocaria un budget < vsync.
if (Resource::Cache::get()->loadStep(50 /*ms*/)) {
if (Options::loading.show && Options::loading.wait_for_input) {
boot_waiting_for_input_ = true; // Esperar tecla antes de continuar
} else {
finishBoot();
}
boot_loading_ = false;
// finishBoot() ja ha fixat SceneManager::current a LOGO (o la que
// digui Debug). El canvi d'escena es fa just a sota.
}
} catch (const std::exception& e) {
std::cerr << "Fatal error during resource load: " << e.what() << '\n';
SceneManager::current = SceneManager::Scene::QUIT;
return SDL_APP_FAILURE;
}
}
// Si l'escena ha canviat (o s'ha demanat RESTART_CURRENT), canviar-la abans del frame // Si l'escena ha canviat (o s'ha demanat RESTART_CURRENT), canviar-la abans del frame
if (SceneManager::current != current_scene_ || SceneManager::current == SceneManager::Scene::RESTART_CURRENT) { if (SceneManager::current != current_scene_ || SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
switchToActiveScene(); switchToActiveScene();
@@ -416,6 +476,17 @@ auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
} }
#endif #endif
// Si estamos esperando input tras la carga: consumir tecla/botón y arrancar
if (boot_waiting_for_input_) {
const bool IS_KEY = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
const bool IS_BUTTON = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
if (IS_KEY || IS_BUTTON) {
boot_waiting_for_input_ = false;
finishBoot();
}
return SDL_APP_CONTINUE;
}
if (active_scene_) { if (active_scene_) {
active_scene_->handleEvent(event); active_scene_->handleEvent(event);
} }

View File

@@ -22,11 +22,14 @@ class Director {
std::string executable_path_; // Path del ejecutable std::string executable_path_; // Path del ejecutable
std::string system_folder_; // Carpeta del sistema donde guardar datos std::string system_folder_; // Carpeta del sistema donde guardar datos
std::unique_ptr<Scene> active_scene_; // Escena activa std::unique_ptr<Scene> active_scene_; // Escena activa
SceneManager::Scene current_scene_{SceneManager::Scene::LOGO}; // Tipus d'escena activa SceneManager::Scene current_scene_{SceneManager::Scene::BOOT_LOADER}; // Tipus d'escena activa
bool boot_loading_{true}; // True mientras Cache::loadStep() no haya acabado
bool boot_waiting_for_input_{false}; // True si la carga acabó y Options::loading.wait_for_input está activo
// --- Funciones --- // --- Funciones ---
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos
void setFileList(); // Carga la configuración de assets desde assets.yaml void setFileList(); // Carga la configuración de assets desde assets.yaml
void switchToActiveScene(); // Construeix l'escena segons SceneManager::current void switchToActiveScene(); // Construeix l'escena segons SceneManager::current
void finishBoot(); // Inits que dependen del cache, ejecutado tras loadStep==done
}; };

View File

@@ -1,10 +1,21 @@
#include "core/system/global_events.hpp" #include "core/system/global_events.hpp"
#include "core/input/input.hpp" // Para Input (gamepad add/remove)
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio #include "core/locale/locale.hpp" // Para Locale
#include "game/ui/console.hpp" // Para Console #include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
#include "game/scene_manager.hpp" // Para SceneManager::current (filtrar BACK a GAME)
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
namespace GlobalEvents { namespace GlobalEvents {
namespace {
// Flag per saber si en aquest frame s'ha rebut un button down del gamepad.
// El consumeix GlobalInputs perquè un botó del comandament salti escenes.
bool gamepad_button_pressed_ = false;
} // namespace
// Comprueba los eventos que se pueden producir en cualquier sección del juego. // Comprueba los eventos que se pueden producir en cualquier sección del juego.
// Nota: SDL_EVENT_QUIT el gestiona Director::handleEvent() directament. // Nota: SDL_EVENT_QUIT el gestiona Director::handleEvent() directament.
void handle(const SDL_Event& event) { void handle(const SDL_Event& event) {
@@ -12,6 +23,43 @@ namespace GlobalEvents {
// reLoadTextures(); // reLoadTextures();
} }
// Connexió/desconnexió de gamepads: cal enrutar-los a Input perquè
// afegisca el dispositiu a gamepads_. Sense això, en wasm els gamepads
// mai es detecten (la Gamepad API del navegador només els exposa
// després que l'usuari els active, més tard que el discoverGamepads
// inicial). En desktop també arregla la connexió en calent.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (Input::get() != nullptr) {
std::string name = Input::get()->handleEvent(event);
if (!name.empty() && Notifier::get() != nullptr && Locale::get() != nullptr) {
const std::string KEY = (event.type == SDL_EVENT_GAMEPAD_ADDED)
? "ui.gamepad_connected"
: "ui.gamepad_disconnected";
Notifier::get()->show({name + " " + Locale::get()->get(KEY)});
}
}
}
// Marcar polsació de qualsevol botó del comandament (els consumirà GlobalInputs
// per saltar escenes d'attract mode). Queden exclosos els botons reservats a
// accions globals perquè el "any button → ACCEPT" no se'ls mengi abans que
// checkAction() els pugui enrutar:
// - BACK → EXIT (a emscripten només a l'escena GAME, ja que no pot tancar).
// - LEFT_SHOULDER / RIGHT_SHOULDER → NEXT_PALETTE / NEXT_PALETTE_SORT.
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
const auto BUTTON = event.gbutton.button;
const bool IS_BACK = (BUTTON == SDL_GAMEPAD_BUTTON_BACK);
const bool IS_SHOULDER = (BUTTON == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER || BUTTON == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
#ifdef __EMSCRIPTEN__
const bool RESERVE_BACK = IS_BACK && SceneManager::current == SceneManager::Scene::GAME;
#else
const bool RESERVE_BACK = IS_BACK;
#endif
if (!RESERVE_BACK && !IS_SHOULDER) {
gamepad_button_pressed_ = true;
}
}
// Enrutar eventos de texto a la consola cuando está activa // Enrutar eventos de texto a la consola cuando está activa
if (Console::get() != nullptr && Console::get()->isActive()) { if (Console::get() != nullptr && Console::get()->isActive()) {
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) { if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {
@@ -22,4 +70,10 @@ namespace GlobalEvents {
Mouse::handleEvent(event); Mouse::handleEvent(event);
} }
auto consumeGamepadButtonPressed() -> bool {
const bool RESULT = gamepad_button_pressed_;
gamepad_button_pressed_ = false;
return RESULT;
}
} // namespace GlobalEvents } // namespace GlobalEvents

View File

@@ -5,4 +5,9 @@
namespace GlobalEvents { namespace GlobalEvents {
// Comprueba los eventos que se pueden producir en cualquier sección del juego // Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event); void handle(const SDL_Event& event);
// True si en aquest frame s'ha rebut un SDL_EVENT_GAMEPAD_BUTTON_DOWN.
// Es consumeix (i es reseteja) per GlobalInputs::getPressedAction perquè
// qualsevol botó del comandament actuï com a "ACCEPT" (saltar escena).
auto consumeGamepadButtonPressed() -> bool;
} // namespace GlobalEvents } // namespace GlobalEvents

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

@@ -89,6 +89,12 @@ namespace Defaults::Localization {
constexpr const char* LANGUAGE = "ca"; // Idioma por defecto (en = inglés, ca = catalán) constexpr const char* LANGUAGE = "ca"; // Idioma por defecto (en = inglés, ca = catalán)
} // namespace Defaults::Localization } // namespace Defaults::Localization
namespace Defaults::Loading {
constexpr bool SHOW = false; // No mostrar la pantalla de carga por defecto
constexpr bool SHOW_RESOURCE_NAME = true; // Mostrar el nombre del recurso por defecto
constexpr bool WAIT_FOR_INPUT = false; // No esperar tecla al terminar por defecto
} // namespace Defaults::Loading
namespace Defaults::Game::Items { namespace Defaults::Game::Items {
constexpr const float PERCENT_TO_OPEN_THE_JAIL = 0.9F; // Porcentaje de items necesarios para abrir la jail constexpr const float PERCENT_TO_OPEN_THE_JAIL = 0.9F; // Porcentaje de items necesarios para abrir la jail
} // namespace Defaults::Game::Items } // namespace Defaults::Game::Items

View File

@@ -39,9 +39,9 @@ void MiniMap::buildTileColorTable(const std::string& tileset_name) {
auto tileset = Resource::Cache::get()->getSurface(tileset_name); auto tileset = Resource::Cache::get()->getSurface(tileset_name);
if (!tileset) { return; } if (!tileset) { return; }
tileset_width_ = static_cast<int>(tileset->getWidth()) / Tile::SIZE; tileset_width_ = tileset->getWidth() / Tile::SIZE;
tileset_transparent_ = tileset->getTransparentColor(); tileset_transparent_ = tileset->getTransparentColor();
int tileset_height = static_cast<int>(tileset->getHeight()) / Tile::SIZE; int tileset_height = tileset->getHeight() / Tile::SIZE;
int total_tiles = tileset_width_ * tileset_height; int total_tiles = tileset_width_ * tileset_height;
tile_colors_.resize(total_tiles, 0); tile_colors_.resize(total_tiles, 0);

View File

@@ -26,8 +26,8 @@ void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_
// Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada) // Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada)
int src_cell = Tile::SIZE + spacing_in_; int src_cell = Tile::SIZE + spacing_in_;
tileset_width_ = static_cast<int>(tileset_->getWidth()) / src_cell; tileset_width_ = tileset_->getWidth() / src_cell;
tileset_height_ = static_cast<int>(tileset_->getHeight()) / src_cell; tileset_height_ = tileset_->getHeight() / src_cell;
// Corregir si el último tile cabe sin spacing // Corregir si el último tile cabe sin spacing
if (tileset_width_ == 0 && tileset_->getWidth() >= Tile::SIZE) { tileset_width_ = 1; } if (tileset_width_ == 0 && tileset_->getWidth() >= Tile::SIZE) { tileset_width_ = 1; }
if (tileset_height_ == 0 && tileset_->getHeight() >= Tile::SIZE) { tileset_height_ = 1; } if (tileset_height_ == 0 && tileset_->getHeight() >= Tile::SIZE) { tileset_height_ = 1; }

View File

@@ -623,6 +623,27 @@ namespace Options {
} }
} }
// Carga opciones de la pantalla de carga desde YAML
void loadLoadingFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("loading")) { return; }
const auto& ld = yaml["loading"];
if (ld.contains("show")) {
try {
loading.show = ld["show"].get_value<bool>();
} catch (...) { loading.show = Defaults::Loading::SHOW; }
}
if (ld.contains("show_resource_name")) {
try {
loading.show_resource_name = ld["show_resource_name"].get_value<bool>();
} catch (...) { loading.show_resource_name = Defaults::Loading::SHOW_RESOURCE_NAME; }
}
if (ld.contains("wait_for_input")) {
try {
loading.wait_for_input = ld["wait_for_input"].get_value<bool>();
} catch (...) { loading.wait_for_input = Defaults::Loading::WAIT_FOR_INPUT; }
}
}
// Establece la ruta del fichero de configuración // Establece la ruta del fichero de configuración
void setConfigFile(const std::string& path) { void setConfigFile(const std::string& path) {
config_file_path = path; config_file_path = path;
@@ -673,6 +694,7 @@ namespace Options {
loadPlayerConfigFromYaml(yaml); loadPlayerConfigFromYaml(yaml);
loadKioskConfigFromYaml(yaml); loadKioskConfigFromYaml(yaml);
loadLocalizationFromYaml(yaml); loadLocalizationFromYaml(yaml);
loadLoadingFromYaml(yaml);
std::cout << "Config file loaded successfully\n\n"; std::cout << "Config file loaded successfully\n\n";
@@ -810,6 +832,14 @@ namespace Options {
file << "localization:\n"; file << "localization:\n";
file << " language: \"" << language << "\"\n"; file << " language: \"" << language << "\"\n";
// LOADING SCREEN
file << "\n";
file << "# LOADING SCREEN\n";
file << "loading:\n";
file << " show: " << (loading.show ? "true" : "false") << "\n";
file << " show_resource_name: " << (loading.show_resource_name ? "true" : "false") << "\n";
file << " wait_for_input: " << (loading.wait_for_input ? "true" : "false") << " # solo si show=true\n";
file.close(); file.close();
std::cout << "Config file saved successfully\n\n"; std::cout << "Config file saved successfully\n\n";

View File

@@ -134,6 +134,13 @@ namespace Options {
float volume{Defaults::Audio::VOLUME}; // Volumen al que suenan el audio (0-128 internamente) float volume{Defaults::Audio::VOLUME}; // Volumen al que suenan el audio (0-128 internamente)
}; };
// Estructura para las opciones de la pantalla de carga
struct Loading {
bool show{Defaults::Loading::SHOW}; // Muestra la pantalla de carga (si no, pantalla en negro)
bool show_resource_name{Defaults::Loading::SHOW_RESOURCE_NAME}; // Muestra el nombre del recurso sobre la barra de progreso
bool wait_for_input{Defaults::Loading::WAIT_FOR_INPUT}; // Al terminar la carga, espera tecla antes de continuar (solo si show=true)
};
// Estructura para las opciones de juego // Estructura para las opciones de juego
struct Game { struct Game {
float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego
@@ -182,6 +189,7 @@ namespace Options {
inline Stats stats{}; // Datos con las estadisticas de juego inline Stats stats{}; // Datos con las estadisticas de juego
inline Window window{}; // Opciones relativas a la ventana inline Window window{}; // Opciones relativas a la ventana
inline Audio audio{}; // Opciones relativas al audio inline Audio audio{}; // Opciones relativas al audio
inline Loading loading{}; // Opciones de la pantalla de carga
inline KeyboardControls keyboard_controls{}; // Teclas usadas para jugar inline KeyboardControls keyboard_controls{}; // Teclas usadas para jugar
inline GamepadControls gamepad_controls{}; // Botones del gamepad usados para jugar inline GamepadControls gamepad_controls{}; // Botones del gamepad usados para jugar
inline Kiosk kiosk{}; // Opciones del modo kiosko inline Kiosk kiosk{}; // Opciones del modo kiosko

View File

@@ -11,6 +11,7 @@ namespace SceneManager {
// --- Escenas del programa --- // --- Escenas del programa ---
enum class Scene { enum class Scene {
BOOT_LOADER, // Carga inicial de recursos dirigida por iterate()
LOGO, // Pantalla del logo LOGO, // Pantalla del logo
LOADING_SCREEN, // Pantalla de carga LOADING_SCREEN, // Pantalla de carga
TITLE, // Pantalla de título/menú principal TITLE, // Pantalla de título/menú principal
@@ -34,7 +35,7 @@ namespace SceneManager {
}; };
// --- Variables de estado globales --- // --- Variables de estado globales ---
inline Scene current = Scene::LOGO; // Escena actual (en _DEBUG sobrescrito por Director tras cargar debug.yaml) inline Scene current = Scene::BOOT_LOADER; // Arranca siempre cargando recursos; Director conmuta a LOGO al terminar
inline Options options = Options::LOGO_TO_LOADING_SCREEN; // Opciones de la escena actual inline Options options = Options::LOGO_TO_LOADING_SCREEN; // Opciones de la escena actual
inline Scene scene_before_restart = Scene::LOGO; // escena a relanzar tras RESTART_CURRENT inline Scene scene_before_restart = Scene::LOGO; // escena a relanzar tras RESTART_CURRENT

View File

@@ -0,0 +1,14 @@
#include "game/scenes/boot_loader.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/scene_manager.hpp"
void BootLoader::iterate() {
Resource::Cache::get()->renderProgress();
}
void BootLoader::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <SDL3/SDL.h>
#include "game/scenes/scene.hpp"
// Escena mínima que Director usa mientras el cache se carga incrementalmente.
// No avanza la carga — lo hace Director::iterate() llamando a Cache::loadStep()
// antes de despachar la escena. Aquí solo se pinta la barra de progreso.
class BootLoader : public Scene {
public:
BootLoader() = default;
~BootLoader() override = default;
void iterate() override;
void handleEvent(const SDL_Event& event) override;
};

View File

@@ -2,12 +2,12 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory> // Para shared_ptr #include <memory> // Para shared_ptr
#include <string> // Para string #include <string> // Para string
#include <vector> // Para vector #include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene #include "game/scenes/scene.hpp" // Para Scene
class AnimatedSprite; // lines 11-11 class AnimatedSprite; // lines 11-11
class Surface; class Surface;
class PixelReveal; class PixelReveal;
class DeltaTimer; class DeltaTimer;

View File

@@ -7,8 +7,8 @@
#include <vector> // Para vector #include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene #include "game/scenes/scene.hpp" // Para Scene
class Sprite; // lines 8-8 class Sprite; // lines 8-8
class Surface; // lines 9-9 class Surface; // lines 9-9
class PixelReveal; class PixelReveal;
class DeltaTimer; class DeltaTimer;

View File

@@ -2,12 +2,12 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory> // Para shared_ptr #include <memory> // Para shared_ptr
#include <vector> // Para vector #include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene #include "game/scenes/scene.hpp" // Para Scene
class AnimatedSprite; // lines 7-7 class AnimatedSprite; // lines 7-7
class DeltaTimer; // Forward declaration class DeltaTimer; // Forward declaration
class GameOver : public Scene { class GameOver : public Scene {
public: public:
@@ -51,9 +51,9 @@ class GameOver : public Scene {
void update(); // Actualiza el objeto void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla void render(); // Dibuja el final en pantalla
static void handleInput(); // Comprueba las entradas static void handleInput(); // Comprueba las entradas
void updateState(); // Actualiza el estado y transiciones void updateState(); // Actualiza el estado y transiciones
void updateColor(); // Actualiza el color usado para renderizar void updateColor(); // Actualiza el color usado para renderizar
void renderSprites(); // Dibuja los sprites void renderSprites(); // Dibuja los sprites
// --- Variables miembro --- // --- Variables miembro ---
// Objetos y punteros a recursos // Objetos y punteros a recursos

View File

@@ -92,6 +92,21 @@ void Title::handleEvent(const SDL_Event& event) {
return; // No procesar más este evento return; // No procesar más este evento
} }
// Qualsevol botó del comandament al menú principal inicia partida directament
// (els bindings ja estan definits, no cal "pulsar 1" amb el teclat). El botó
// BACK queda exclòs perquè es reserva per a EXIT — excepte a emscripten, on
// no es pot sortir del joc i BACK pot actuar com a botó genèric d'inici.
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
state_ == State::MAIN_MENU &&
!is_remapping_keyboard_ && !is_remapping_joystick_
#ifndef __EMSCRIPTEN__
&& event.gbutton.button != SDL_GAMEPAD_BUTTON_BACK
#endif
) {
handleMainMenuKeyPress(SDLK_1); // PLAY
return;
}
if (event.type == SDL_EVENT_KEY_DOWN && !Console::get()->isActive()) { if (event.type == SDL_EVENT_KEY_DOWN && !Console::get()->isActive()) {
// Si estamos en modo remap de teclado, capturar tecla // Si estamos en modo remap de teclado, capturar tecla
if (is_remapping_keyboard_ && !remap_completed_) { if (is_remapping_keyboard_ && !remap_completed_) {
@@ -487,7 +502,7 @@ void Title::createCheevosTexture() { // NOLINT(readability-convert-member-funct
// Crea el sprite para el listado de logros (usa la zona del menu) // Crea el sprite para el listado de logros (usa la zona del menu)
cheevos_sprite_ = std::make_unique<Sprite>(cheevos_surface_, (GameCanvas::WIDTH - cheevos_surface_->getWidth()) / 2, MENU_ZONE_Y, cheevos_surface_->getWidth(), cheevos_surface_->getHeight()); cheevos_sprite_ = std::make_unique<Sprite>(cheevos_surface_, (GameCanvas::WIDTH - cheevos_surface_->getWidth()) / 2, MENU_ZONE_Y, cheevos_surface_->getWidth(), cheevos_surface_->getHeight());
cheevos_surface_view_ = {.x = 0, .y = 0, .w = cheevos_surface_->getWidth(), .h = CHEEVOS_TEXTURE_VIEW_HEIGHT}; cheevos_surface_view_ = {.x = 0, .y = 0, .w = static_cast<float>(cheevos_surface_->getWidth()), .h = CHEEVOS_TEXTURE_VIEW_HEIGHT};
cheevos_sprite_->setClip(cheevos_surface_view_); cheevos_sprite_->setClip(cheevos_surface_view_);
} }

View File

@@ -14,7 +14,7 @@
#include "core/rendering/text.hpp" // Para Text #include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_cache.hpp" // Para Resource
#include "game/options.hpp" // Para Options #include "game/options.hpp" // Para Options
#include "game/ui/notifier.hpp" // Para Notifier #include "utils/easing_functions.hpp" // Para Easing::cubicInOut
// ── Helpers de texto ────────────────────────────────────────────────────────── // ── Helpers de texto ──────────────────────────────────────────────────────────
@@ -194,22 +194,16 @@ void Console::update(float delta_time) { // NOLINT(readability-function-cogniti
// Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE // Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
if (status_ == Status::ACTIVE && height_ != target_height_) { if (status_ == Status::ACTIVE && height_ != target_height_) {
const float PREV_HEIGHT = height_; if (anim_progress_ == 0.0F) {
if (height_ < target_height_) { // Iniciar animación de resize
height_ = std::min(height_ + (SLIDE_SPEED * delta_time), target_height_); anim_start_ = height_;
} else { anim_end_ = target_height_;
height_ = std::max(height_ - (SLIDE_SPEED * delta_time), target_height_);
} }
// Actualizar el Notifier incrementalmente con el delta de altura anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
if (Notifier::get() != nullptr) { height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT); if (anim_progress_ >= 1.0F) {
if (DELTA_PX > 0) { height_ = target_height_;
Notifier::get()->addYOffset(DELTA_PX); anim_progress_ = 0.0F;
notifier_offset_applied_ += DELTA_PX;
} else if (DELTA_PX < 0) {
Notifier::get()->removeYOffset(-DELTA_PX);
notifier_offset_applied_ += DELTA_PX;
}
} }
// Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px) // Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px)
const float WIDTH = Options::game.width; const float WIDTH = Options::game.width;
@@ -220,28 +214,23 @@ void Console::update(float delta_time) { // NOLINT(readability-function-cogniti
// Redibujar texto cada frame // Redibujar texto cada frame
redrawText(); redrawText();
switch (status_) { // Animación de apertura/cierre (basada en tiempo con easing)
case Status::RISING: { if (status_ == Status::RISING || status_ == Status::VANISHING) {
y_ += SLIDE_SPEED * delta_time; anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
if (y_ >= 0.0F) { y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
y_ = 0.0F;
if (anim_progress_ >= 1.0F) {
y_ = anim_end_;
anim_progress_ = 0.0F;
if (status_ == Status::RISING) {
status_ = Status::ACTIVE; status_ = Status::ACTIVE;
} } else {
break;
}
case Status::VANISHING: {
y_ -= SLIDE_SPEED * delta_time;
if (y_ <= -height_) {
y_ = -height_;
status_ = Status::HIDDEN; status_ = Status::HIDDEN;
// Resetear el mensaje una vez completamente oculta // Resetear el mensaje una vez completamente oculta
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)}; msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size())); target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
} }
break;
} }
default:
break;
} }
SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_}; SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
@@ -265,6 +254,9 @@ void Console::toggle() {
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size())); target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
height_ = target_height_; height_ = target_height_;
y_ = -height_; y_ = -height_;
anim_start_ = y_;
anim_end_ = 0.0F;
anim_progress_ = 0.0F;
status_ = Status::RISING; status_ = Status::RISING;
input_line_.clear(); input_line_.clear();
cursor_timer_ = 0.0F; cursor_timer_ = 0.0F;
@@ -273,24 +265,18 @@ void Console::toggle() {
typewriter_chars_ = static_cast<int>(msg_lines_[0].size()); typewriter_chars_ = static_cast<int>(msg_lines_[0].size());
typewriter_timer_ = 0.0F; typewriter_timer_ = 0.0F;
SDL_StartTextInput(SDL_GetKeyboardFocus()); SDL_StartTextInput(SDL_GetKeyboardFocus());
if (Notifier::get() != nullptr) {
const int OFFSET = static_cast<int>(height_);
Notifier::get()->addYOffset(OFFSET);
notifier_offset_applied_ = OFFSET;
}
if (on_toggle) { on_toggle(true); } if (on_toggle) { on_toggle(true); }
break; break;
case Status::ACTIVE: case Status::ACTIVE:
// Al cerrar: mantener el texto visible hasta que esté completamente oculta // Al cerrar: mantener el texto visible hasta que esté completamente oculta
anim_start_ = y_;
anim_end_ = -height_;
anim_progress_ = 0.0F;
status_ = Status::VANISHING; status_ = Status::VANISHING;
target_height_ = height_; // No animar durante VANISHING target_height_ = height_; // No animar altura durante VANISHING
history_index_ = -1; history_index_ = -1;
saved_input_.clear(); saved_input_.clear();
SDL_StopTextInput(SDL_GetKeyboardFocus()); SDL_StopTextInput(SDL_GetKeyboardFocus());
if (Notifier::get() != nullptr) {
Notifier::get()->removeYOffset(notifier_offset_applied_);
notifier_offset_applied_ = 0;
}
if (on_toggle) { on_toggle(false); } if (on_toggle) { on_toggle(false); }
break; break;
default: default:

View File

@@ -51,14 +51,14 @@ class Console {
}; };
// Constantes visuales // Constantes visuales
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
static constexpr Uint8 BORDER_COLOR = 9; // PaletteColor::BRIGHT_GREEN static constexpr Uint8 BORDER_COLOR = 9; // PaletteColor::BRIGHT_GREEN
static constexpr Uint8 MSG_COLOR = 8; // PaletteColor::GREEN static constexpr Uint8 MSG_COLOR = 8; // PaletteColor::GREEN
static constexpr float SLIDE_SPEED = 180.0F; static constexpr float ANIM_DURATION = 0.3F; // Duración de cualquier animación (segundos)
// Constantes de consola // Constantes de consola
static constexpr std::string_view CONSOLE_NAME = "JDD Console"; static constexpr std::string_view CONSOLE_NAME = "JDD Console";
static constexpr std::string_view CONSOLE_VERSION = "v2.2"; static constexpr std::string_view CONSOLE_VERSION = "v2.3";
static constexpr int MAX_LINE_CHARS = 32; static constexpr int MAX_LINE_CHARS = 32;
static constexpr int MAX_HISTORY_SIZE = 20; static constexpr int MAX_HISTORY_SIZE = 20;
static constexpr float CURSOR_ON_TIME = 0.5F; static constexpr float CURSOR_ON_TIME = 0.5F;
@@ -99,9 +99,13 @@ class Console {
int typewriter_chars_{0}; // Caracteres de msg_lines_ actualmente visibles int typewriter_chars_{0}; // Caracteres de msg_lines_ actualmente visibles
float typewriter_timer_{0.0F}; float typewriter_timer_{0.0F};
// Animación basada en tiempo (0→1 en ANIM_DURATION)
float anim_progress_{0.0F}; // Progreso normalizado [0, 1]
float anim_start_{0.0F}; // Valor inicial (y_ o height_)
float anim_end_{0.0F}; // Valor final
// Animación de altura dinámica // Animación de altura dinámica
float target_height_{0.0F}; // Altura objetivo (según número de líneas de mensaje) float target_height_{0.0F}; // Altura objetivo (según número de líneas de mensaje)
int notifier_offset_applied_{0}; // Acumulador del offset enviado al Notifier
// Historial de comandos (navegable con flechas arriba/abajo) // Historial de comandos (navegable con flechas arriba/abajo)
std::deque<std::string> history_; std::deque<std::string> history_;

View File

@@ -491,6 +491,9 @@ static auto cmdDebug(const std::vector<std::string>& args) -> std::string { //
if (args[2] == "GAME") { if (args[2] == "GAME") {
target = SceneManager::Scene::GAME; target = SceneManager::Scene::GAME;
name = "game"; name = "game";
} else if (args[2] == "DEMO") {
target = SceneManager::Scene::DEMO;
name = "demo";
} else if (args[2] == "LOGO") { } else if (args[2] == "LOGO") {
target = SceneManager::Scene::LOGO; target = SceneManager::Scene::LOGO;
name = "logo"; name = "logo";
@@ -651,10 +654,10 @@ static auto cmdItems(const std::vector<std::string>& args) -> std::string {
return "Items: " + std::to_string(count); return "Items: " + std::to_string(count);
} }
// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] // SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|DEMO|ENDING|ENDING2|RESTART]
static auto cmdScene(const std::vector<std::string>& args) -> std::string { static auto cmdScene(const std::vector<std::string>& args) -> std::string {
if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; } if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; }
if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|ending|ending2|restart]"; } if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|demo|ending|ending2|restart]"; }
if (args[0] == "RESTART") { if (args[0] == "RESTART") {
SceneManager::scene_before_restart = SceneManager::current; SceneManager::scene_before_restart = SceneManager::current;
@@ -677,6 +680,7 @@ static auto cmdScene(const std::vector<std::string>& args) -> std::string {
if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); } if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); }
if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); } if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); }
if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); } if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); }
if (args[0] == "DEMO") { return GO_TO(SceneManager::Scene::DEMO, "Demo"); }
if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); } if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); }
if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); } if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); }
return "Unknown scene: " + args[0]; return "Unknown scene: " + args[0];
@@ -948,11 +952,16 @@ static auto cmdKiosk(const std::vector<std::string>& args) -> std::string {
// EXIT / QUIT // EXIT / QUIT
static auto cmdExit(const std::vector<std::string>& args) -> std::string { static auto cmdExit(const std::vector<std::string>& args) -> std::string {
#ifdef __EMSCRIPTEN__
(void)args;
return "Not allowed in web version";
#else
if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) { if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
return "Not allowed in kiosk mode"; return "Not allowed in kiosk mode";
} }
SceneManager::current = SceneManager::Scene::QUIT; SceneManager::current = SceneManager::Scene::QUIT;
return "Quitting..."; return "Quitting...";
#endif
} }
// SIZE // SIZE

View File

@@ -15,6 +15,7 @@
#include "core/rendering/text.hpp" // Para Text, Text::CENTER_FLAG, Text::COLOR_FLAG #include "core/rendering/text.hpp" // Para Text, Text::CENTER_FLAG, Text::COLOR_FLAG
#include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_cache.hpp" // Para Resource
#include "game/options.hpp" // Para Options, options, NotificationPosition #include "game/options.hpp" // Para Options, options, NotificationPosition
#include "game/ui/console.hpp" // Para Console
#include "utils/delta_timer.hpp" // Para DeltaTimer #include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor #include "utils/utils.hpp" // Para PaletteColor
@@ -73,8 +74,11 @@ void Notifier::render() {
// Actualiza el estado de las notificaiones // Actualiza el estado de las notificaiones
void Notifier::update(float delta_time) { void Notifier::update(float delta_time) {
// Base Y leída cada frame: cada notificación se dibuja en rect.y (relativo a BASE) + BASE
const float BASE = static_cast<float>(getStackBaseY());
for (auto& notification : notifications_) { for (auto& notification : notifications_) {
// Si la notificación anterior está "saliendo", no hagas nada // Si la notificación anterior está "entrando", no hagas nada (stall del resto)
if (!notifications_.empty() && &notification != &notifications_.front()) { if (!notifications_.empty() && &notification != &notifications_.front()) {
const auto& previous_notification = *(std::prev(&notification)); const auto& previous_notification = *(std::prev(&notification));
if (previous_notification.state == Status::RISING) { if (previous_notification.state == Status::RISING) {
@@ -84,17 +88,17 @@ void Notifier::update(float delta_time) {
switch (notification.state) { switch (notification.state) {
case Status::RISING: { case Status::RISING: {
const float DISPLACEMENT = SLIDE_SPEED * delta_time; const float TARGET = static_cast<float>(notification.y);
notification.rect.y += DISPLACEMENT; notification.rect.y += SLIDE_SPEED * delta_time;
if (notification.rect.y >= TARGET) {
if (notification.rect.y >= notification.y) { notification.rect.y = TARGET;
notification.rect.y = notification.y;
notification.state = Status::STAY; notification.state = Status::STAY;
notification.elapsed_time = 0.0F; notification.elapsed_time = 0.0F;
} }
break; break;
} }
case Status::STAY: { case Status::STAY: {
notification.rect.y = static_cast<float>(notification.y);
notification.elapsed_time += delta_time; notification.elapsed_time += delta_time;
if (notification.elapsed_time >= notification.display_duration) { if (notification.elapsed_time >= notification.display_duration) {
notification.state = Status::VANISHING; notification.state = Status::VANISHING;
@@ -103,10 +107,8 @@ void Notifier::update(float delta_time) {
} }
case Status::VANISHING: { case Status::VANISHING: {
const float DISPLACEMENT = SLIDE_SPEED * delta_time; const float TARGET_Y = static_cast<float>(notification.y - notification.travel_dist);
notification.rect.y -= DISPLACEMENT; notification.rect.y -= SLIDE_SPEED * delta_time;
const float TARGET_Y = notification.y - notification.travel_dist;
if (notification.rect.y <= TARGET_Y) { if (notification.rect.y <= TARGET_Y) {
notification.rect.y = TARGET_Y; notification.rect.y = TARGET_Y;
notification.state = Status::FINISHED; notification.state = Status::FINISHED;
@@ -120,8 +122,13 @@ void Notifier::update(float delta_time) {
default: default:
break; break;
} }
}
notification.sprite->setPosition(notification.rect); // Refrescar posiciones de sprite cada frame (convierte rect.y relativo a absoluto)
for (auto& notification : notifications_) {
SDL_FRect sprite_rect = notification.rect;
sprite_rect.y += BASE;
notification.sprite->setPosition(sprite_rect);
} }
clearFinishedNotifications(); clearFinishedNotifications();
@@ -170,15 +177,13 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
// Posición horizontal // Posición horizontal
float desp_h = ((Options::game.width / 2) - (WIDTH / 2)); float desp_h = ((Options::game.width / 2) - (WIDTH / 2));
;
// Posición vertical // Offset vertical (relativo a la base de la pila, que se consulta cada frame)
const int DESP_V = y_offset_;
// Offset
const auto TRAVEL_DIST = HEIGHT + PADDING_OUT; const auto TRAVEL_DIST = HEIGHT + PADDING_OUT;
const int TRAVEL_MOD = 1; const int TRAVEL_MOD = 1;
const int OFFSET = !notifications_.empty() ? notifications_.back().y + (TRAVEL_MOD * notifications_.back().travel_dist) : DESP_V; const int OFFSET = !notifications_.empty()
? notifications_.back().y + (TRAVEL_MOD * notifications_.back().travel_dist)
: 0;
// Crea la notificacion // Crea la notificacion
Notification n; Notification n;
@@ -191,8 +196,9 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
n.texts = texts; n.texts = texts;
n.shape = SHAPE; n.shape = SHAPE;
n.display_duration = style.duration; n.display_duration = style.duration;
const float Y_POS = OFFSET + -TRAVEL_DIST; // Posición inicial relativa a la base: arranca "travel_dist" por encima del target (=OFFSET)
n.rect = {.x = desp_h, .y = Y_POS, .w = WIDTH, .h = HEIGHT}; const float Y_POS_REL = static_cast<float>(OFFSET) - TRAVEL_DIST;
n.rect = {.x = desp_h, .y = Y_POS_REL, .w = WIDTH, .h = HEIGHT};
// Crea la textura // Crea la textura
n.surface = std::make_shared<Surface>(WIDTH, HEIGHT); n.surface = std::make_shared<Surface>(WIDTH, HEIGHT);
@@ -219,7 +225,7 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
else if (SHAPE == Shape::SQUARED) { else if (SHAPE == Shape::SQUARED) {
n.surface->clear(style.bg_color); n.surface->clear(style.bg_color);
SDL_FRect squared_rect = {.x = 0, .y = 0, .w = n.surface->getWidth(), .h = n.surface->getHeight()}; SDL_FRect squared_rect = {.x = 0, .y = 0, .w = static_cast<float>(n.surface->getWidth()), .h = static_cast<float>(n.surface->getHeight())};
n.surface->drawRectBorder(&squared_rect, style.border_color); n.surface->drawRectBorder(&squared_rect, style.border_color);
} }
@@ -252,8 +258,10 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
// Deja de dibujar en la textura // Deja de dibujar en la textura
Screen::get()->setRendererSurface(previuos_renderer); Screen::get()->setRendererSurface(previuos_renderer);
// Crea el sprite de la notificación // Crea el sprite de la notificación (rect absoluto a partir del relativo + BASE)
n.sprite = std::make_shared<Sprite>(n.surface, n.rect); SDL_FRect initial_sprite_rect = n.rect;
initial_sprite_rect.y += static_cast<float>(getStackBaseY());
n.sprite = std::make_shared<Sprite>(n.surface, initial_sprite_rect);
// Añade la notificación a la lista // Añade la notificación a la lista
notifications_.emplace_back(n); notifications_.emplace_back(n);
@@ -278,9 +286,21 @@ void Notifier::clearNotifications() {
clearFinishedNotifications(); clearFinishedNotifications();
} }
// Ajusta el offset vertical base // Y absoluta de la base de la pila (justo debajo de Console, o 0 si no hay Console)
void Notifier::addYOffset(int px) { y_offset_ += px; } auto Notifier::getStackBaseY() const -> int {
void Notifier::removeYOffset(int px) { y_offset_ -= px; } return Console::get() != nullptr ? Console::get()->getVisibleHeight() : 0;
}
// Altura animada ocupada por la pila (usa rect.y animado, no el target — para transiciones suaves)
auto Notifier::getVisibleHeight() const -> int {
int bottom = 0;
for (const auto& n : notifications_) {
if (n.state == Status::FINISHED) { continue; }
const int N_BOTTOM = static_cast<int>(n.rect.y + n.rect.h);
if (N_BOTTOM > bottom) { bottom = N_BOTTOM; }
}
return bottom;
}
// Obtiene los códigos de las notificaciones // Obtiene los códigos de las notificaciones
auto Notifier::getCodes() -> std::vector<std::string> { auto Notifier::getCodes() -> std::vector<std::string> {

View File

@@ -59,9 +59,9 @@ class Notifier {
auto isActive() -> bool; // Indica si hay notificaciones activas auto isActive() -> bool; // Indica si hay notificaciones activas
auto getCodes() -> std::vector<std::string>; // Obtiene códigos de notificaciones auto getCodes() -> std::vector<std::string>; // Obtiene códigos de notificaciones
// Offset vertical (para evitar solapamiento con Console y renderInfo) // Altura animada ocupada por la pila de notificaciones, en píxeles (relativa a la base).
void addYOffset(int px); // Suma píxeles al offset base // Crece/decrece suavemente con las animaciones de entrada/salida.
void removeYOffset(int px); // Resta píxeles al offset base [[nodiscard]] auto getVisibleHeight() const -> int;
private: private:
// Tipos anidados // Tipos anidados
@@ -78,8 +78,8 @@ class Notifier {
std::vector<std::string> texts; std::vector<std::string> texts;
Status state{Status::RISING}; Status state{Status::RISING};
Shape shape{Shape::SQUARED}; Shape shape{Shape::SQUARED};
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // rect.y es relativo a la base de la pila
int y{0}; int y{0}; // Top objetivo de la notificación relativo a la base de la pila
int travel_dist{0}; int travel_dist{0};
std::string code; std::string code;
bool can_be_removed{true}; bool can_be_removed{true};
@@ -97,8 +97,9 @@ class Notifier {
static Notifier* notifier; static Notifier* notifier;
// Métodos privados // Métodos privados
void clearFinishedNotifications(); // Elimina las notificaciones finalizadas void clearFinishedNotifications(); // Elimina las notificaciones finalizadas
void clearNotifications(); // Finaliza y elimina todas las notificaciones activas void clearNotifications(); // Finaliza y elimina todas las notificaciones activas
[[nodiscard]] auto getStackBaseY() const -> int; // Y absoluta de la base de la pila (leída de Console)
// Constructor y destructor privados [SINGLETON] // Constructor y destructor privados [SINGLETON]
Notifier(const std::string& icon_file, const std::string& text); Notifier(const std::string& icon_file, const std::string& text);
@@ -111,5 +112,4 @@ class Notifier {
std::vector<Notification> notifications_; // Lista de notificaciones activas std::vector<Notification> notifications_; // Lista de notificaciones activas
bool stack_{false}; // Indica si las notificaciones se apilan bool stack_{false}; // Indica si las notificaciones se apilan
bool has_icons_{false}; // Indica si el notificador tiene textura para iconos bool has_icons_{false}; // Indica si el notificador tiene textura para iconos
int y_offset_{0}; // Offset vertical base (ajustado por Console y renderInfo)
}; };

View File

@@ -6,7 +6,7 @@
namespace Texts { namespace Texts {
constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner"; constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner";
constexpr const char* COPYRIGHT = "@2022 JailDesigner"; constexpr const char* COPYRIGHT = "@2022 JailDesigner";
constexpr const char* VERSION = "1.13"; // Versión por defecto constexpr const char* VERSION = "1.14"; // Versión por defecto
} // namespace Texts } // namespace Texts
// Tamaño de bloque // Tamaño de bloque

View File

@@ -1,53 +0,0 @@
# Makefile for pack_resources tool
# Compiler
CXX := g++
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
# Directories
TOOL_DIR := .
SOURCE_DIR := ../../source/core/resources
# Source files
SOURCES := pack_resources.cpp \
$(SOURCE_DIR)/resource_pack.cpp
# Output
TARGET := pack_resources
# Platform-specific executable extension
ifeq ($(OS),Windows_NT)
TARGET := $(TARGET).exe
endif
# Default target
all: $(TARGET)
# Build the tool
$(TARGET): $(SOURCES)
@echo "Building pack_resources tool..."
$(CXX) $(CXXFLAGS) $(SOURCES) -o $(TARGET)
@echo "Build complete: $(TARGET)"
# Test: create a test pack
test: $(TARGET)
@echo "Creating test pack..."
./$(TARGET) ../../data test_resources.pack
# Create the actual resources.pack
pack: $(TARGET)
@echo "Creating resources.pack..."
./$(TARGET) ../../data ../../resources.pack
# List contents of a pack
list: $(TARGET)
@echo "Listing pack contents..."
./$(TARGET) --list ../../resources.pack
# Clean
clean:
@echo "Cleaning..."
rm -f $(TARGET) test_resources.pack
@echo "Clean complete"
.PHONY: all test pack list clean