Compare commits
16 Commits
4c1ed1cf9b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e49f35d3b | |||
| b9cee1bc70 | |||
| 3390d01ef6 | |||
| 561028ff04 | |||
| 671583ebbe | |||
| e941502615 | |||
| 8bab2da2ed | |||
| 5ef278ce70 | |||
| 14103175a9 | |||
| 10a3e2fedd | |||
| 25a36d5064 | |||
| 8706b2c7fb | |||
| d493ebf4f0 | |||
| 3e795998d1 | |||
| c694781f38 | |||
| 1a2298963d |
@@ -11,9 +11,8 @@ Checks:
|
||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
||||
|
||||
WarningsAsErrors: '*'
|
||||
# Solo incluir archivos de tu código fuente (external tiene su propio .clang-tidy)
|
||||
# Excluye los headers SPIR-V generados en rendering/sdl3gpu/
|
||||
HeaderFilterRegex: 'source/(?!external/|rendering/sdl3gpu/.*_spv\.h).*'
|
||||
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
|
||||
HeaderFilterRegex: 'source/.*'
|
||||
FormatStyle: file
|
||||
|
||||
CheckOptions:
|
||||
|
||||
51
.vscode/c_cpp_properties.json
vendored
Normal file
51
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Mac",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/source/**",
|
||||
"${workspaceFolder}/build"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"_NO_AUDIO",
|
||||
"MACOS_BUILD"
|
||||
],
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "macos-clang-arm64"
|
||||
},
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/source/**",
|
||||
"${workspaceFolder}/build"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"_NO_AUDIO",
|
||||
"LINUX_BUILD"
|
||||
],
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "linux-gcc-x64"
|
||||
},
|
||||
{
|
||||
"name": "Win32",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/source/**",
|
||||
"${workspaceFolder}/build"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"_NO_AUDIO",
|
||||
"WINDOWS_BUILD",
|
||||
"_WIN32"
|
||||
],
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "windows-msvc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
@@ -3,6 +3,11 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(coffee_crisis_arcade_edition VERSION 2.00)
|
||||
|
||||
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||
endif()
|
||||
|
||||
# Establecer estándar de C++
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
@@ -10,17 +15,24 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
|
||||
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
|
||||
find_package(Git QUIET)
|
||||
if(GIT_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
else()
|
||||
set(GIT_HASH "unknown")
|
||||
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
|
||||
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
|
||||
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
|
||||
# resuelve aquí ejecutando git directamente.
|
||||
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||
find_package(Git QUIET)
|
||||
if(GIT_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
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()
|
||||
|
||||
# Configurar archivo de versión
|
||||
@@ -32,6 +44,7 @@ set(APP_SOURCES
|
||||
|
||||
# --- core/audio ---
|
||||
source/core/audio/audio.cpp
|
||||
source/core/audio/audio_adapter.cpp
|
||||
|
||||
# --- core/input ---
|
||||
source/core/input/define_buttons.cpp
|
||||
@@ -104,6 +117,7 @@ set(APP_SOURCES
|
||||
source/game/scenes/instructions.cpp
|
||||
source/game/scenes/intro.cpp
|
||||
source/game/scenes/logo.cpp
|
||||
source/game/scenes/preload.cpp
|
||||
source/game/scenes/title.cpp
|
||||
|
||||
# --- game/ui ---
|
||||
@@ -127,7 +141,7 @@ if(EMSCRIPTEN)
|
||||
FetchContent_Declare(
|
||||
SDL3
|
||||
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
|
||||
GIT_TAG release-3.2.12
|
||||
GIT_TAG release-3.4.4
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
@@ -150,11 +164,11 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||
set(SHADER_UPSCALE_SRC "${CMAKE_SOURCE_DIR}/data/shaders/upscale.frag")
|
||||
set(SHADER_DOWNSCALE_SRC "${CMAKE_SOURCE_DIR}/data/shaders/downscale.frag")
|
||||
|
||||
set(SHADER_VERT_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/postfx_vert_spv.h")
|
||||
set(SHADER_FRAG_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/postfx_frag_spv.h")
|
||||
set(SHADER_CRTPI_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/crtpi_frag_spv.h")
|
||||
set(SHADER_UPSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/upscale_frag_spv.h")
|
||||
set(SHADER_DOWNSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/downscale_frag_spv.h")
|
||||
set(SHADER_VERT_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/postfx_vert_spv.h")
|
||||
set(SHADER_FRAG_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/postfx_frag_spv.h")
|
||||
set(SHADER_CRTPI_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/crtpi_frag_spv.h")
|
||||
set(SHADER_UPSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/upscale_frag_spv.h")
|
||||
set(SHADER_DOWNSCALE_H "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv/downscale_frag_spv.h")
|
||||
|
||||
set(ALL_SHADER_SOURCES "${SHADER_VERT_SRC}" "${SHADER_FRAG_SRC}" "${SHADER_CRTPI_SRC}" "${SHADER_UPSCALE_SRC}" "${SHADER_DOWNSCALE_SRC}")
|
||||
set(ALL_SHADER_HEADERS "${SHADER_VERT_H}" "${SHADER_FRAG_H}" "${SHADER_CRTPI_H}" "${SHADER_UPSCALE_H}" "${SHADER_DOWNSCALE_H}")
|
||||
@@ -308,7 +322,6 @@ list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
|
||||
# cuyo uso vive en un .cpp distinto.
|
||||
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
|
||||
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
|
||||
list(FILTER CPPCHECK_SOURCES EXCLUDE REGEX ".*_spv\\.h$")
|
||||
|
||||
# Targets de clang-tidy
|
||||
if(CLANG_TIDY_EXE)
|
||||
@@ -388,3 +401,35 @@ if(CPPCHECK_EXE)
|
||||
else()
|
||||
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
|
||||
endif()
|
||||
|
||||
# --- EINA STANDALONE: pack_resources ---
|
||||
# Executable auxiliar que empaqueta `data/` a `resources.pack`.
|
||||
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
|
||||
# cmake --build build --target pack_resources
|
||||
# Després executar: ./build/pack_resources data resources.pack
|
||||
if(NOT EMSCRIPTEN)
|
||||
add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||
tools/pack_resources/pack_resources.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
)
|
||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||
target_compile_options(pack_resources PRIVATE -Wall)
|
||||
|
||||
# Regeneració automàtica de resources.pack en cada build si canvia data/.
|
||||
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${RESOURCE_PACK}
|
||||
COMMAND $<TARGET_FILE:pack_resources>
|
||||
"${CMAKE_SOURCE_DIR}/data"
|
||||
"${RESOURCE_PACK}"
|
||||
DEPENDS pack_resources ${DATA_FILES}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Empaquetant data/ → resources.pack"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||
endif()
|
||||
|
||||
380
Makefile
380
Makefile
@@ -2,7 +2,6 @@
|
||||
# DIRECTORIES
|
||||
# ==============================================================================
|
||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
|
||||
|
||||
# ==============================================================================
|
||||
# TARGET NAMES
|
||||
@@ -22,16 +21,33 @@ RESOURCE_FILE := release/windows/coffee.res
|
||||
# ==============================================================================
|
||||
# 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
|
||||
SHADERS_DIR := $(DIR_ROOT)data/shaders
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GLSLC := $(shell where glslc 2>NUL)
|
||||
else
|
||||
GLSLC := $(shell command -v glslc 2>/dev/null)
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# VERSION (fecha actual)
|
||||
# VERSION (extraída de defines.hpp)
|
||||
# ==============================================================================
|
||||
ifeq ($(OS),Windows_NT)
|
||||
VERSION := $(shell powershell -Command "Get-Date -Format 'yyyy-MM-dd'")
|
||||
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
|
||||
else
|
||||
VERSION := $(shell date +%Y-%m-%d)
|
||||
VERSION := $(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||
endif
|
||||
|
||||
# Hash del commit actual, computado en el host. Se pasa a CMake via -DGIT_HASH
|
||||
# para que el build en docker/emscripten no falle por "dubious ownership" de Git.
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
|
||||
else
|
||||
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
|
||||
endif
|
||||
ifeq ($(GIT_HASH),)
|
||||
GIT_HASH := unknown
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
@@ -47,9 +63,13 @@ endif
|
||||
ifeq ($(OS),Windows_NT)
|
||||
WIN_TARGET_FILE := $(DIR_ROOT)$(APP_NAME)
|
||||
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
|
||||
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
|
||||
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
|
||||
else
|
||||
WIN_TARGET_FILE := $(TARGET_FILE)
|
||||
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
||||
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
@@ -59,7 +79,6 @@ WINDOWS_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-win32-x64.z
|
||||
MACOS_INTEL_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-intel.dmg
|
||||
MACOS_APPLE_SILICON_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-apple-silicon.dmg
|
||||
LINUX_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux.tar.gz
|
||||
RASPI_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-raspberry.tar.gz
|
||||
|
||||
# ==============================================================================
|
||||
# PLATAFORMA
|
||||
@@ -76,15 +95,24 @@ else
|
||||
UNAME_S := $(shell uname -s)
|
||||
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
|
||||
# ==============================================================================
|
||||
all:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build
|
||||
|
||||
debug:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
||||
@cmake --build build
|
||||
|
||||
# ==============================================================================
|
||||
@@ -92,41 +120,43 @@ debug:
|
||||
# ==============================================================================
|
||||
release:
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@"$(MAKE)" windows_release
|
||||
@"$(MAKE)" _windows_release
|
||||
else
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
@$(MAKE) macos_release
|
||||
@$(MAKE) _macos_release
|
||||
else
|
||||
@$(MAKE) linux_release
|
||||
@$(MAKE) _linux_release
|
||||
endif
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK
|
||||
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
|
||||
# ==============================================================================
|
||||
pack_tool:
|
||||
@$(MAKE) -C $(DIR_PACK_TOOL)
|
||||
|
||||
resources.pack:
|
||||
@$(MAKE) -C $(DIR_PACK_TOOL) pack
|
||||
pack:
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build --target pack_resources
|
||||
@./build/pack_resources data resources.pack
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN DE SHADERS
|
||||
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
|
||||
# ==============================================================================
|
||||
spirv:
|
||||
@echo "Compilando shaders SPIR-V..."
|
||||
$(SHADER_SCRIPT)
|
||||
compile_shaders:
|
||||
ifdef GLSLC
|
||||
@cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
|
||||
else
|
||||
@echo "glslc no encontrado - asegurate de que los headers SPIR-V precompilados existen"
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||
# ==============================================================================
|
||||
windows_release:
|
||||
@$(MAKE) resources.pack
|
||||
_windows_release:
|
||||
@$(MAKE) pack
|
||||
@echo off
|
||||
@echo Creando release para Windows - Version: $(VERSION)
|
||||
|
||||
# Compila con cmake
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build
|
||||
|
||||
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
|
||||
@@ -142,7 +172,7 @@ windows_release:
|
||||
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '$(WIN_RELEASE_FILE).exe'"
|
||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
|
||||
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||
|
||||
# Crea el fichero .zip
|
||||
@@ -156,16 +186,32 @@ windows_release:
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||
# ==============================================================================
|
||||
macos_release:
|
||||
@$(MAKE) resources.pack
|
||||
_macos_release:
|
||||
@$(MAKE) pack
|
||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||
|
||||
# Verificar e instalar create-dmg si es necesario
|
||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
||||
|
||||
# Compila la versión para procesadores Intel con cmake
|
||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
|
||||
@cmake --build build/intel
|
||||
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " Falta la dependencia: create-dmg"; \
|
||||
echo "============================================"; \
|
||||
if command -v brew >/dev/null 2>&1; then \
|
||||
echo " Instalando con: brew install create-dmg"; \
|
||||
brew install create-dmg || { \
|
||||
echo ""; \
|
||||
echo " ERROR: 'brew install create-dmg' ha fallado."; \
|
||||
echo " Ejecuta el comando manualmente y vuelve a probar."; \
|
||||
exit 1; \
|
||||
}; \
|
||||
else \
|
||||
echo " Homebrew no está instalado."; \
|
||||
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
|
||||
echo " brew install create-dmg"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
}
|
||||
|
||||
# Elimina datos de compilaciones anteriores
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
@@ -189,30 +235,55 @@ macos_release:
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
|
||||
# Copia el ejecutable Intel al bundle
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
# Actualiza versión en Info.plist
|
||||
@echo "Actualizando Info.plist con versión $(VERSION)..."
|
||||
@RAW_VERSION=$$(echo "$(VERSION)" | sed 's/^v//'); \
|
||||
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
|
||||
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||
|
||||
# Firma la aplicación
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
||||
|
||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
||||
@echo "Creando DMG Intel con iconos de 96x96..."
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--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)"
|
||||
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||
# DMG Intel y continúa con la build de Apple Silicon).
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Intel (x86_64)"
|
||||
@echo "============================================"
|
||||
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||
-DMACOS_BUNDLE=ON \
|
||||
&& cmake --build build/intel; then \
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||
echo "Creando DMG Intel con iconos de 96x96..."; \
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--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
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Apple Silicon (arm64)"
|
||||
@echo "============================================"
|
||||
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
|
||||
@cmake --build build/arm
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
@@ -246,12 +317,12 @@ macos_release:
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA LINUX (RELEASE)
|
||||
# ==============================================================================
|
||||
linux_release:
|
||||
@$(MAKE) resources.pack
|
||||
_linux_release:
|
||||
@$(MAKE) pack
|
||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||
|
||||
# Compila con cmake
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build
|
||||
|
||||
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
|
||||
@@ -274,175 +345,76 @@ linux_release:
|
||||
# Elimina la carpeta temporal
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA LINUX (RELEASE CON INTEGRACIÓN DESKTOP)
|
||||
# ==============================================================================
|
||||
linux_release_desktop:
|
||||
@$(MAKE) resources.pack
|
||||
@echo "Creando release con integracion desktop para Linux - Version: $(VERSION)"
|
||||
|
||||
# Compila con cmake
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build
|
||||
|
||||
# Elimina carpetas previas y recrea (crea dist/ si no existe)
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# Crea la estructura de directorios estándar para Linux
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)"
|
||||
|
||||
# Copia ficheros del juego
|
||||
cp -R config "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)/"
|
||||
cp resources.pack "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/$(TARGET_NAME)/"
|
||||
cp LICENSE "$(RELEASE_FOLDER)/$(TARGET_NAME)/"
|
||||
cp README.md "$(RELEASE_FOLDER)/$(TARGET_NAME)/"
|
||||
|
||||
# Copia el ejecutable
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin/$(TARGET_NAME)"
|
||||
strip -s -R .comment -R .gnu.version "$(RELEASE_FOLDER)/$(TARGET_NAME)/bin/$(TARGET_NAME)" --strip-unneeded
|
||||
|
||||
# Crea el archivo .desktop
|
||||
@echo '[Desktop Entry]' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Version=1.0' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Type=Application' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Name=$(APP_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Comment=Arcade action game - defend Earth from alien invasion!' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Exec=/opt/$(TARGET_NAME)/bin/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Icon=$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Path=/opt/$(TARGET_NAME)/share/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Terminal=false' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'StartupNotify=true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Categories=Game;ArcadeGame;' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
@echo 'Keywords=arcade;action;shooter;retro;' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop"
|
||||
|
||||
# Copia el icono (si existe) y lo redimensiona si es necesario
|
||||
@if [ -f "release/icons/icon.png" ]; then \
|
||||
if command -v magick >/dev/null 2>&1; then \
|
||||
magick "release/icons/icon.png" -resize 256x256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
|
||||
echo "Icono redimensionado de release/icons/icon.png (usando ImageMagick)"; \
|
||||
elif command -v convert >/dev/null 2>&1; then \
|
||||
convert "release/icons/icon.png" -resize 256x256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
|
||||
echo "Icono redimensionado de release/icons/icon.png (usando ImageMagick legacy)"; \
|
||||
elif command -v ffmpeg >/dev/null 2>&1; then \
|
||||
ffmpeg -i "release/icons/icon.png" -vf scale=256:256 "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png" -y -loglevel quiet; \
|
||||
echo "Icono redimensionado de release/icons/icon.png (usando ffmpeg)"; \
|
||||
else \
|
||||
cp "release/icons/icon.png" "$(RELEASE_FOLDER)/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png"; \
|
||||
echo "Icono copiado sin redimensionar (instalar ImageMagick o ffmpeg para redimensionado automatico)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo "Advertencia: No se encontró release/icons/icon.png - crear icono manualmente"; \
|
||||
fi
|
||||
|
||||
# Crea script de instalación
|
||||
@echo '#!/bin/bash' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'echo "Instalando $(APP_NAME)..."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo mkdir -p /opt/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo cp -R bin /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo cp -R share /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo cp LICENSE /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo cp README.md /opt/$(TARGET_NAME)/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo mkdir -p /usr/share/applications' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo mkdir -p /usr/share/icons/hicolor/256x256/apps' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo cp /opt/$(TARGET_NAME)/share/applications/$(TARGET_NAME).desktop /usr/share/applications/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo cp /opt/$(TARGET_NAME)/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png /usr/share/icons/hicolor/256x256/apps/' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo update-desktop-database /usr/share/applications 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'sudo gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'echo "$(APP_NAME) instalado correctamente!"' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
@echo 'echo "Ya puedes encontrarlo en el menu de aplicaciones en la categoria Juegos."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
chmod +x "$(RELEASE_FOLDER)/$(TARGET_NAME)/install.sh"
|
||||
|
||||
# Crea script de desinstalación
|
||||
@echo '#!/bin/bash' > "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'echo "Desinstalando $(APP_NAME)..."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'sudo rm -rf /opt/$(TARGET_NAME)' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'sudo rm -f /usr/share/applications/$(TARGET_NAME).desktop' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'sudo rm -f /usr/share/icons/hicolor/256x256/apps/$(TARGET_NAME).png' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'sudo update-desktop-database /usr/share/applications 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'sudo gtk-update-icon-cache /usr/share/icons/hicolor 2>/dev/null || true' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
@echo 'echo "$(APP_NAME) desinstalado correctamente."' >> "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
chmod +x "$(RELEASE_FOLDER)/$(TARGET_NAME)/uninstall.sh"
|
||||
|
||||
# Empaqueta ficheros
|
||||
$(RMFILE) "$(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz"
|
||||
tar -czvf "$(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz" -C "$(RELEASE_FOLDER)" .
|
||||
@echo "Release con integracion desktop creado: $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux-desktop.tar.gz"
|
||||
@echo "Para instalar: extraer y ejecutar ./$(TARGET_NAME)/install.sh"
|
||||
|
||||
# Elimina la carpeta temporal
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA RASPBERRY PI (RELEASE)
|
||||
# ==============================================================================
|
||||
raspi_release:
|
||||
@$(MAKE) resources.pack
|
||||
@echo "Creando release para Raspberry Pi - Version: $(VERSION)"
|
||||
|
||||
# Compila con cmake
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build
|
||||
|
||||
# Elimina carpetas previas y recrea (crea dist/ si no existe)
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# Copia ficheros
|
||||
cp -R config "$(RELEASE_FOLDER)"
|
||||
cp resources.pack "$(RELEASE_FOLDER)"
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||
strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded
|
||||
|
||||
# Empaqueta ficheros
|
||||
$(RMFILE) "$(RASPI_RELEASE)"
|
||||
tar -czvf "$(RASPI_RELEASE)" -C "$(RELEASE_FOLDER)" .
|
||||
@echo "Release creado: $(RASPI_RELEASE)"
|
||||
|
||||
# Elimina la carpeta temporal
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
|
||||
# ==============================================================================
|
||||
wasm:
|
||||
@echo "Compilando para WebAssembly - Version: $(VERSION)"
|
||||
@echo "Compilando para WebAssembly - Version: $(VERSION) ($(GIT_HASH))"
|
||||
docker run --rm \
|
||||
--user $(shell id -u):$(shell id -g) \
|
||||
-v $(DIR_ROOT):/src \
|
||||
-w /src \
|
||||
emscripten/emsdk:latest \
|
||||
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release && 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"
|
||||
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
|
||||
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
|
||||
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
|
||||
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
|
||||
@echo "Output: $(DIST_DIR)/wasm/"
|
||||
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
|
||||
maverick:/home/sergio/gitea/web_jailgames/static/games/coffee-crisis-arcade-edition/wasm/
|
||||
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
|
||||
@echo "Deployed to maverick"
|
||||
|
||||
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm_debug/.
|
||||
wasm_debug:
|
||||
@echo "Compilando WebAssembly Debug - Version: $(VERSION) ($(GIT_HASH))"
|
||||
docker run --rm \
|
||||
--user $(shell id -u):$(shell id -g) \
|
||||
-v $(DIR_ROOT):/src \
|
||||
-w /src \
|
||||
emscripten/emsdk:latest \
|
||||
bash -c "emcmake cmake -S . -B build/wasm_debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm_debug"
|
||||
$(MKDIR) "$(DIST_DIR)/wasm_debug"
|
||||
cp build/wasm_debug/$(TARGET_NAME).html $(DIST_DIR)/wasm_debug/
|
||||
cp build/wasm_debug/$(TARGET_NAME).js $(DIST_DIR)/wasm_debug/
|
||||
cp build/wasm_debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm_debug/
|
||||
cp build/wasm_debug/$(TARGET_NAME).data $(DIST_DIR)/wasm_debug/
|
||||
@echo "Output: $(DIST_DIR)/wasm_debug/"
|
||||
|
||||
# ==============================================================================
|
||||
# CODE QUALITY (delegados a cmake)
|
||||
# ==============================================================================
|
||||
format:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build --target format
|
||||
|
||||
format-check:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build --target format-check
|
||||
|
||||
tidy:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build --target tidy
|
||||
|
||||
tidy-fix:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake --build build --target tidy-fix
|
||||
|
||||
cppcheck:
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@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
|
||||
# ==============================================================================
|
||||
@@ -459,26 +431,22 @@ help:
|
||||
@echo ""
|
||||
@echo " Release:"
|
||||
@echo " make release - Crear release (detecta SO automaticamente)"
|
||||
@echo " make windows_release - Crear release para Windows"
|
||||
@echo " make linux_release - Crear release basico para Linux"
|
||||
@echo " make linux_release_desktop - Crear release con integracion desktop para Linux"
|
||||
@echo " make macos_release - Crear release para macOS"
|
||||
@echo " make raspi_release - Crear release para Raspberry Pi"
|
||||
@echo " make wasm - Crear build WebAssembly (requiere Docker) en dist/wasm"
|
||||
@echo " make wasm_debug - Build WebAssembly Debug local (sin deploy)"
|
||||
@echo ""
|
||||
@echo " Herramientas:"
|
||||
@echo " make spirv - Compilar shaders SPIR-V"
|
||||
@echo " make pack_tool - Compilar herramienta de empaquetado"
|
||||
@echo " make resources.pack - Generar pack de recursos desde data/"
|
||||
@echo " make compile_shaders - Compilar shaders SPIR-V"
|
||||
@echo " make pack - Empaquetar recursos a resources.pack"
|
||||
@echo ""
|
||||
@echo " Calidad de codigo:"
|
||||
@echo " make format - Formatear codigo con clang-format"
|
||||
@echo " make format-check - Verificar formato sin modificar"
|
||||
@echo " make tidy - Analisis estatico con clang-tidy"
|
||||
@echo " make tidy-fix - Analisis estatico con auto-fix"
|
||||
@echo " make cppcheck - Analisis estatico con cppcheck"
|
||||
@echo ""
|
||||
@echo " Otros:"
|
||||
@echo " make show_version - Mostrar version actual ($(VERSION))"
|
||||
@echo " make help - Mostrar esta ayuda"
|
||||
|
||||
.PHONY: all debug release windows_release macos_release linux_release linux_release_desktop raspi_release wasm pack_tool resources.pack spirv format format-check tidy tidy-fix show_version help
|
||||
.PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug pack compile_shaders format format-check tidy tidy-fix cppcheck controllerdb show_version help
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"[NOTIFICATIONS] DISCONNECTED": "desconectat",
|
||||
|
||||
"[RESOURCE] LOADING": "Carregant",
|
||||
"[RESOURCE] PRESS_TO_CONTINUE": "Prem una tecla per continuar",
|
||||
|
||||
"[SERVICE_MENU] TITLE": "Menu de servei",
|
||||
"[SERVICE_MENU] RESET": "Reiniciar",
|
||||
@@ -85,6 +86,7 @@
|
||||
"[SERVICE_MENU] SUPERSAMPLING": "Supermostreig",
|
||||
"[SERVICE_MENU] VSYNC": "Sincronisme vertical",
|
||||
"[SERVICE_MENU] INTEGER_SCALE": "Escalat sencer",
|
||||
"[SERVICE_MENU] FILTER": "Filtre",
|
||||
"[SERVICE_MENU] MAIN_VOLUME": "Volumen general",
|
||||
"[SERVICE_MENU] MUSIC_VOLUME": "Volumen de la musica",
|
||||
"[SERVICE_MENU] SFX_VOLUME": "Volumen dels sons",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"[NOTIFICATIONS] DISCONNECTED": "disconnected",
|
||||
|
||||
"[RESOURCE] LOADING": "Loading",
|
||||
"[RESOURCE] PRESS_TO_CONTINUE": "Press any key to continue",
|
||||
|
||||
"[SERVICE_MENU] TITLE": "Service Menu",
|
||||
"[SERVICE_MENU] RESET": "Reset",
|
||||
@@ -84,6 +85,7 @@
|
||||
"[SERVICE_MENU] SUPERSAMPLING": "Supersampling",
|
||||
"[SERVICE_MENU] VSYNC": "V-Sync",
|
||||
"[SERVICE_MENU] INTEGER_SCALE": "Integer Scale",
|
||||
"[SERVICE_MENU] FILTER": "Filter",
|
||||
"[SERVICE_MENU] MAIN_VOLUME": "Main Volume",
|
||||
"[SERVICE_MENU] MUSIC_VOLUME": "Music Volume",
|
||||
"[SERVICE_MENU] SFX_VOLUME": "Sound Volume",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"[NOTIFICATIONS] DISCONNECTED": "desconectado",
|
||||
|
||||
"[RESOURCE] LOADING": "Cargando",
|
||||
"[RESOURCE] PRESS_TO_CONTINUE": "Pulsa una tecla para continuar",
|
||||
|
||||
"[SERVICE_MENU] TITLE": "Menu de servicio",
|
||||
"[SERVICE_MENU] RESET": "Reiniciar",
|
||||
@@ -84,6 +85,7 @@
|
||||
"[SERVICE_MENU] SUPERSAMPLING": "Supersampling",
|
||||
"[SERVICE_MENU] VSYNC": "Sincronismo vertical",
|
||||
"[SERVICE_MENU] INTEGER_SCALE": "Escalado proporcional",
|
||||
"[SERVICE_MENU] FILTER": "Filtro",
|
||||
"[SERVICE_MENU] MAIN_VOLUME": "Volumen general",
|
||||
"[SERVICE_MENU] MUSIC_VOLUME": "Volumen de la musica",
|
||||
"[SERVICE_MENU] SFX_VOLUME": "Volumen de los efectos",
|
||||
|
||||
2232
gamecontrollerdb.txt
Normal file
2232
gamecontrollerdb.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
#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 <iostream> // Para std::cout
|
||||
@@ -8,9 +8,9 @@
|
||||
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
// stb_vorbis.h filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
|
||||
// perquè xocarien amb noms de paràmetres de plantilla en json.hpp i altres.
|
||||
#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
|
||||
@@ -19,9 +19,9 @@
|
||||
#undef PLAYBACK_RIGHT
|
||||
// clang-format on
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
|
||||
#include "core/resources/resource.hpp" // Para Resource
|
||||
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
|
||||
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_*
|
||||
#include "game/options.hpp" // Para Options::audio
|
||||
|
||||
// Singleton
|
||||
Audio* Audio::instance = nullptr;
|
||||
@@ -30,7 +30,10 @@ Audio* Audio::instance = nullptr;
|
||||
void Audio::init() { Audio::instance = new Audio(); }
|
||||
|
||||
// Libera la instancia
|
||||
void Audio::destroy() { delete Audio::instance; }
|
||||
void Audio::destroy() {
|
||||
delete Audio::instance;
|
||||
Audio::instance = nullptr;
|
||||
}
|
||||
|
||||
// Obtiene la instancia
|
||||
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||
@@ -46,17 +49,57 @@ Audio::~Audio() {
|
||||
// Método principal
|
||||
void Audio::update() {
|
||||
JA_Update();
|
||||
|
||||
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
|
||||
if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) {
|
||||
instance->music_.state = MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce la música
|
||||
void Audio::playMusic(const std::string& name, const int loop) {
|
||||
music_.name = name;
|
||||
music_.loop = (loop != 0);
|
||||
// Reproduce la música por nombre (con crossfade opcional)
|
||||
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
|
||||
bool new_loop = (loop != 0);
|
||||
|
||||
if (music_enabled_ && music_.state != MusicState::PLAYING) {
|
||||
JA_PlayMusic(Resource::get()->getMusic(name), loop);
|
||||
music_.state = MusicState::PLAYING;
|
||||
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
|
||||
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!music_enabled_) return;
|
||||
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) return;
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(resource, crossfade_ms, loop);
|
||||
} else {
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic();
|
||||
}
|
||||
JA_PlayMusic(resource, loop);
|
||||
}
|
||||
|
||||
music_.name = name;
|
||||
music_.loop = new_loop;
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Reproduce la música por puntero (con crossfade opcional)
|
||||
void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) return;
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(music, crossfade_ms, loop);
|
||||
} else {
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic();
|
||||
}
|
||||
JA_PlayMusic(music, loop);
|
||||
}
|
||||
|
||||
music_.name.clear(); // nom desconegut quan es passa per punter
|
||||
music_.loop = (loop != 0);
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Pausa la música
|
||||
@@ -83,10 +126,17 @@ void Audio::stopMusic() {
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido
|
||||
// Reproduce un sonido por nombre
|
||||
void Audio::playSound(const std::string& name, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(Resource::get()->getSound(name), 0, static_cast<int>(group));
|
||||
JA_PlaySound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por puntero directo
|
||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||
if (sound_enabled_ && sound != nullptr) {
|
||||
JA_PlaySound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,20 +170,20 @@ auto Audio::getRealMusicState() -> MusicState {
|
||||
}
|
||||
}
|
||||
|
||||
// Establece el volumen de los sonidos
|
||||
void Audio::setSoundVolume(int sound_volume, Group group) const {
|
||||
// Establece el volumen de los sonidos (float 0.0..1.0)
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = (sound_volume / 100.0F) * (Options::audio.volume / 100.0F);
|
||||
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
|
||||
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Establece el volumen de la música
|
||||
void Audio::setMusicVolume(int music_volume) const {
|
||||
// Establece el volumen de la música (float 0.0..1.0)
|
||||
void Audio::setMusicVolume(float music_volume) const {
|
||||
if (music_enabled_) {
|
||||
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = (music_volume / 100.0F) * (Options::audio.volume / 100.0F);
|
||||
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
|
||||
JA_SetMusicVolume(CONVERTED_VOLUME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para int8_t, uint8_t
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
// --- Clase Audio: gestor de audio (singleton) ---
|
||||
// Implementació canònica, byte-idèntica entre projectes.
|
||||
// Els volums es manegen internament com a float 0.0–1.0; la capa de
|
||||
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
|
||||
// per mostrar 0–100 a l'usuari.
|
||||
class Audio {
|
||||
public:
|
||||
// --- Enums ---
|
||||
enum class Group : int {
|
||||
enum class Group : std::int8_t {
|
||||
ALL = -1, // Todos los grupos
|
||||
GAME = 0, // Sonidos del juego
|
||||
INTERFACE = 1 // Sonidos de la interfaz
|
||||
};
|
||||
|
||||
enum class MusicState {
|
||||
enum class MusicState : std::uint8_t {
|
||||
PLAYING, // Reproduciendo música
|
||||
PAUSED, // Música pausada
|
||||
STOPPED, // Música detenida
|
||||
};
|
||||
|
||||
// --- Constantes ---
|
||||
static constexpr int MAX_VOLUME = 100; // Volumen máximo
|
||||
static constexpr int MIN_VOLUME = 0; // Volumen mínimo
|
||||
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
|
||||
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
|
||||
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
|
||||
static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%)
|
||||
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
|
||||
static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms)
|
||||
|
||||
// --- Métodos de singleton ---
|
||||
// --- Singleton ---
|
||||
static void init(); // Inicializa el objeto Audio
|
||||
static void destroy(); // Libera el objeto Audio
|
||||
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
|
||||
Audio(const Audio&) = delete; // Evitar copia
|
||||
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
|
||||
|
||||
// --- Método principal ---
|
||||
static void update();
|
||||
static void update(); // Actualización del sistema de audio
|
||||
|
||||
// --- Control de Música ---
|
||||
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
// --- Control de música ---
|
||||
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
|
||||
void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
|
||||
// --- Control de Sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
// --- Control de sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
|
||||
// --- Configuración General ---
|
||||
// --- Control de volumen (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
void setMusicVolume(float volume) const; // Ajustar volumen de música
|
||||
|
||||
// --- Helpers de conversió per a la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
static constexpr auto toPercent(float volume) -> int {
|
||||
return static_cast<int>(volume * 100.0F + 0.5F);
|
||||
}
|
||||
static constexpr auto fromPercent(int percent) -> float {
|
||||
return static_cast<float>(percent) / 100.0F;
|
||||
}
|
||||
|
||||
// --- Configuración general ---
|
||||
void enable(bool value); // Establecer estado general
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
|
||||
void applySettings(); // Aplica la configuración
|
||||
|
||||
// --- Configuración de Sonidos ---
|
||||
// --- Configuración de sonidos ---
|
||||
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
|
||||
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
|
||||
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
|
||||
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
|
||||
|
||||
// --- Configuración de Música ---
|
||||
// --- Configuración de música ---
|
||||
void enableMusic() { music_enabled_ = true; } // Habilitar música
|
||||
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
|
||||
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
|
||||
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
|
||||
|
||||
// --- Control de Volumen ---
|
||||
void setSoundVolume(int volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
void setMusicVolume(int volume) const; // Ajustar volumen de música
|
||||
|
||||
// --- Getters para debug ---
|
||||
// --- Consultas de estado ---
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
|
||||
[[nodiscard]] static auto getRealMusicState() -> MusicState; // Consulta directamente a jailaudio
|
||||
[[nodiscard]] static auto getRealMusicState() -> MusicState;
|
||||
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||
|
||||
private:
|
||||
// --- Estructuras privadas ---
|
||||
// --- Tipos anidados ---
|
||||
struct Music {
|
||||
MusicState state; // Estado actual de la música (reproduciendo, detenido, en pausa)
|
||||
std::string name; // Última pista de música reproducida
|
||||
bool loop; // Indica si la última pista de música se debe reproducir en bucle
|
||||
|
||||
// Constructor para inicializar la música con valores predeterminados
|
||||
Music()
|
||||
: state(MusicState::STOPPED),
|
||||
loop(false) {}
|
||||
|
||||
// Constructor para inicializar con valores específicos
|
||||
Music(MusicState init_state, std::string init_name, bool init_loop)
|
||||
: state(init_state),
|
||||
name(std::move(init_name)),
|
||||
loop(init_loop) {}
|
||||
MusicState state{MusicState::STOPPED}; // Estado actual de la música
|
||||
std::string name; // Última pista de música reproducida
|
||||
bool loop{false}; // Indica si se reproduce en bucle
|
||||
};
|
||||
|
||||
// --- Variables de estado ---
|
||||
Music music_; // Estado de la música
|
||||
bool enabled_ = true; // Estado general del audio
|
||||
bool sound_enabled_ = true; // Estado de los efectos de sonido
|
||||
bool music_enabled_ = true; // Estado de la música
|
||||
|
||||
// --- Métodos internos ---
|
||||
// --- Métodos ---
|
||||
Audio(); // Constructor privado
|
||||
~Audio(); // Destructor privado
|
||||
void initSDLAudio(); // Inicializa SDL Audio
|
||||
|
||||
// --- Constructores y destructor privados (singleton) ---
|
||||
Audio(); // Constructor privado
|
||||
~Audio(); // Destructor privado
|
||||
|
||||
// --- Instancia singleton ---
|
||||
// --- Variables miembro ---
|
||||
static Audio* instance; // Instancia única de Audio
|
||||
|
||||
Music music_; // Estado de la música
|
||||
bool enabled_{true}; // Estado general del audio
|
||||
bool sound_enabled_{true}; // Estado de los efectos de sonido
|
||||
bool music_enabled_{true}; // Estado de la música
|
||||
};
|
||||
13
source/core/audio/audio_adapter.cpp
Normal file
13
source/core/audio/audio_adapter.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "core/audio/audio_adapter.hpp"
|
||||
|
||||
#include "core/resources/resource.hpp"
|
||||
|
||||
namespace AudioResource {
|
||||
JA_Music_t* getMusic(const std::string& name) {
|
||||
return Resource::get()->getMusic(name);
|
||||
}
|
||||
|
||||
JA_Sound_t* getSound(const std::string& name) {
|
||||
return Resource::get()->getSound(name);
|
||||
}
|
||||
} // namespace AudioResource
|
||||
17
source/core/audio/audio_adapter.hpp
Normal file
17
source/core/audio/audio_adapter.hpp
Normal 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
|
||||
@@ -3,26 +3,41 @@
|
||||
// --- Includes ---
|
||||
#include <SDL3/SDL.h>
|
||||
#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 <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
|
||||
#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 ---
|
||||
enum JA_Channel_state { JA_CHANNEL_INVALID,
|
||||
enum JA_Channel_state {
|
||||
JA_CHANNEL_INVALID,
|
||||
JA_CHANNEL_FREE,
|
||||
JA_CHANNEL_PLAYING,
|
||||
JA_CHANNEL_PAUSED,
|
||||
JA_SOUND_DISABLED };
|
||||
enum JA_Music_state { JA_MUSIC_INVALID,
|
||||
JA_SOUND_DISABLED,
|
||||
};
|
||||
enum JA_Music_state {
|
||||
JA_MUSIC_INVALID,
|
||||
JA_MUSIC_PLAYING,
|
||||
JA_MUSIC_PAUSED,
|
||||
JA_MUSIC_STOPPED,
|
||||
JA_MUSIC_DISABLED };
|
||||
JA_MUSIC_DISABLED,
|
||||
};
|
||||
|
||||
// --- Struct Definitions ---
|
||||
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
|
||||
@@ -31,7 +46,9 @@ enum JA_Music_state { JA_MUSIC_INVALID,
|
||||
struct JA_Sound_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
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 {
|
||||
@@ -46,21 +63,22 @@ struct JA_Channel_t {
|
||||
struct JA_Music_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
|
||||
// OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una
|
||||
// sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming.
|
||||
Uint8* ogg_data{nullptr};
|
||||
Uint32 ogg_length{0};
|
||||
stb_vorbis* vorbis{nullptr}; // Handle del decoder, viu tot el cicle del JA_Music_t
|
||||
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
|
||||
// d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks
|
||||
// per streaming. Com que stb_vorbis guarda un punter persistent al
|
||||
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
|
||||
// (una reallocation invalidaria el punter que el decoder conserva).
|
||||
std::vector<Uint8> ogg_data;
|
||||
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t
|
||||
|
||||
char* filename{nullptr};
|
||||
std::string filename;
|
||||
|
||||
int times{0}; // Loops restants (-1 = infinit, 0 = un sol play)
|
||||
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Music_state state{JA_MUSIC_INVALID};
|
||||
};
|
||||
|
||||
// --- Internal Global State ---
|
||||
// Marcado 'inline' (C++17) para asegurar una única instancia.
|
||||
// --- Internal Global State (inline, C++17) ---
|
||||
|
||||
inline JA_Music_t* current_music{nullptr};
|
||||
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
|
||||
@@ -72,15 +90,27 @@ inline bool JA_musicEnabled{true};
|
||||
inline bool JA_soundEnabled{true};
|
||||
inline SDL_AudioDeviceID sdlAudioDevice{0};
|
||||
|
||||
inline bool fading{false};
|
||||
inline int fade_start_time{0};
|
||||
inline int fade_duration{0};
|
||||
inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float'
|
||||
// --- Crossfade / Fade State ---
|
||||
struct JA_FadeState {
|
||||
bool active{false};
|
||||
Uint64 start_time{0};
|
||||
int duration_ms{0};
|
||||
float initial_volume{0.0f};
|
||||
};
|
||||
|
||||
struct JA_OutgoingMusic {
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_FadeState fade;
|
||||
};
|
||||
|
||||
inline JA_OutgoingMusic outgoing_music;
|
||||
inline JA_FadeState incoming_fade;
|
||||
|
||||
// --- Forward Declarations ---
|
||||
inline void JA_StopMusic();
|
||||
inline void JA_StopChannel(const int channel);
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
|
||||
inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1);
|
||||
|
||||
// --- Music streaming internals ---
|
||||
// Bytes-per-sample per canal (sempre s16)
|
||||
@@ -98,15 +128,15 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) {
|
||||
if (!music || !music->vorbis || !music->stream) return 0;
|
||||
|
||||
short chunk[JA_MUSIC_CHUNK_SHORTS];
|
||||
const int channels = music->spec.channels;
|
||||
const int num_channels = music->spec.channels;
|
||||
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
|
||||
music->vorbis,
|
||||
channels,
|
||||
num_channels,
|
||||
chunk,
|
||||
JA_MUSIC_CHUNK_SHORTS);
|
||||
if (samples_per_channel <= 0) return 0;
|
||||
|
||||
const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, chunk, bytes);
|
||||
return samples_per_channel;
|
||||
}
|
||||
@@ -133,20 +163,51 @@ inline void JA_PumpMusic(JA_Music_t* music) {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
|
||||
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
|
||||
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
|
||||
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
|
||||
inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
|
||||
if (!music || !music->vorbis || !music->stream) return;
|
||||
|
||||
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
const int needed_bytes = static_cast<int>((static_cast<int64_t>(duration_ms) * bytes_per_second) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) {
|
||||
const int decoded = JA_FeedMusicChunk(music);
|
||||
if (decoded <= 0) break; // EOF: deixem drenar el que hi haja
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Functions ---
|
||||
|
||||
inline void JA_Update() {
|
||||
// --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
|
||||
if (outgoing_music.stream && outgoing_music.fade.active) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
Uint64 elapsed = now - outgoing_music.fade.start_time;
|
||||
if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
} else {
|
||||
float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms;
|
||||
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Current music ---
|
||||
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
|
||||
if (fading) {
|
||||
int time = SDL_GetTicks();
|
||||
if (time > (fade_start_time + fade_duration)) {
|
||||
fading = false;
|
||||
JA_StopMusic();
|
||||
return;
|
||||
// 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 {
|
||||
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));
|
||||
float percent = (float)elapsed / (float)incoming_fade.duration_ms;
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,12 +219,13 @@ inline void JA_Update() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sound channels ---
|
||||
if (JA_soundEnabled) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
if (channels[i].times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, 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--;
|
||||
}
|
||||
} else {
|
||||
@@ -174,12 +236,8 @@ inline void JA_Update() {
|
||||
}
|
||||
|
||||
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};
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||
@@ -187,7 +245,11 @@ inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -196,26 +258,25 @@ inline void JA_Quit() {
|
||||
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
if (!buffer || length == 0) return nullptr;
|
||||
|
||||
// Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta
|
||||
// memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres.
|
||||
Uint8* ogg_copy = static_cast<Uint8*>(SDL_malloc(length));
|
||||
if (!ogg_copy) return nullptr;
|
||||
SDL_memcpy(ogg_copy, buffer, length);
|
||||
// Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
|
||||
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||
// persistent al buffer; com que ací no el resize'jem, el .data() és
|
||||
// estable durant tot el cicle de vida del music.
|
||||
auto* music = new JA_Music_t();
|
||||
music->ogg_data.assign(buffer, buffer + length);
|
||||
|
||||
int error = 0;
|
||||
stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast<int>(length), &error, nullptr);
|
||||
if (!vorbis) {
|
||||
SDL_free(ogg_copy);
|
||||
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;
|
||||
}
|
||||
|
||||
auto* music = new JA_Music_t();
|
||||
music->ogg_data = ogg_copy;
|
||||
music->ogg_length = length;
|
||||
music->vorbis = vorbis;
|
||||
|
||||
const stb_vorbis_info info = stb_vorbis_get_info(vorbis);
|
||||
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;
|
||||
@@ -224,31 +285,36 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
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) {
|
||||
// [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");
|
||||
if (!f) return NULL; // Añadida comprobación de apertura
|
||||
if (!f) return nullptr;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
|
||||
if (!buffer) { // Añadida comprobación de malloc
|
||||
if (!buffer) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
if (fread(buffer, fsize, 1, f) != 1) {
|
||||
fclose(f);
|
||||
free(buffer);
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
JA_Music_t* music = JA_LoadMusic(buffer, fsize);
|
||||
if (music) { // Comprobar que JA_LoadMusic tuvo éxito
|
||||
music->filename = static_cast<char*>(malloc(strlen(filename) + 1));
|
||||
if (music->filename) {
|
||||
strcpy(music->filename, filename);
|
||||
}
|
||||
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
|
||||
if (music) {
|
||||
music->filename = filename;
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
@@ -280,18 +346,20 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
|
||||
JA_PumpMusic(current_music);
|
||||
|
||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
||||
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) return nullptr; // Añadida comprobación
|
||||
return music->filename;
|
||||
if (!music || music->filename.empty()) return nullptr;
|
||||
return music->filename.c_str();
|
||||
}
|
||||
|
||||
inline void JA_PauseMusic() {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
|
||||
|
||||
current_music->state = JA_MUSIC_PAUSED;
|
||||
SDL_UnbindAudioStream(current_music->stream);
|
||||
@@ -299,13 +367,21 @@ inline void JA_PauseMusic() {
|
||||
|
||||
inline void JA_ResumeMusic() {
|
||||
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;
|
||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||
}
|
||||
|
||||
inline void JA_StopMusic() {
|
||||
// Limpiar outgoing crossfade si existe
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
}
|
||||
incoming_fade.active = false;
|
||||
|
||||
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
||||
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
@@ -318,17 +394,73 @@ inline void JA_StopMusic() {
|
||||
if (current_music->vorbis) {
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
}
|
||||
// No liberem filename aquí; es fa en JA_DeleteMusic.
|
||||
}
|
||||
|
||||
inline void JA_FadeOutMusic(const int milliseconds) {
|
||||
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;
|
||||
fade_start_time = SDL_GetTicks();
|
||||
fade_duration = milliseconds;
|
||||
fade_initial_volume = JA_musicVolume;
|
||||
// Destruir outgoing anterior si existe
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
}
|
||||
|
||||
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
|
||||
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
|
||||
JA_PreFillOutgoing(current_music, milliseconds);
|
||||
|
||||
// Robar el stream del current_music al outgoing
|
||||
outgoing_music.stream = current_music->stream;
|
||||
outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume};
|
||||
|
||||
// Dejar current_music sin stream (ya lo tiene outgoing)
|
||||
current_music->stream = nullptr;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
|
||||
incoming_fade.active = false;
|
||||
}
|
||||
|
||||
inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) {
|
||||
if (!JA_musicEnabled || !music || !music->vorbis) return;
|
||||
|
||||
// Destruir outgoing anterior si existe (crossfade durante crossfade)
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
}
|
||||
|
||||
// Robar el stream de la musica actual al outgoing para el fade-out.
|
||||
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
|
||||
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
|
||||
if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) {
|
||||
JA_PreFillOutgoing(current_music, crossfade_ms);
|
||||
outgoing_music.stream = current_music->stream;
|
||||
outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume};
|
||||
current_music->stream = nullptr;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
|
||||
}
|
||||
|
||||
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
|
||||
current_music = music;
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
current_music->times = loop;
|
||||
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_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() {
|
||||
@@ -346,8 +478,8 @@ inline void JA_DeleteMusic(JA_Music_t* music) {
|
||||
}
|
||||
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
||||
if (music->vorbis) stb_vorbis_close(music->vorbis);
|
||||
SDL_free(music->ogg_data);
|
||||
free(music->filename); // filename es libera aquí
|
||||
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
|
||||
// al destructor de JA_Music_t.
|
||||
delete music;
|
||||
}
|
||||
|
||||
@@ -360,49 +492,40 @@ inline float JA_SetMusicVolume(float volume) {
|
||||
}
|
||||
|
||||
inline void JA_SetMusicPosition(float /*value*/) {
|
||||
// No implementat amb el backend de streaming. Mai va arribar a usar-se
|
||||
// en el codi existent, així que es manté com a stub.
|
||||
// No implementat amb el backend de streaming.
|
||||
}
|
||||
|
||||
inline float JA_GetMusicPosition() {
|
||||
// Veure nota a JA_SetMusicPosition.
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
inline void JA_EnableMusic(const bool value) {
|
||||
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
||||
|
||||
JA_musicEnabled = value;
|
||||
}
|
||||
|
||||
// --- Sound Functions ---
|
||||
|
||||
inline JA_Sound_t* JA_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) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
auto sound = std::make_unique<JA_Sound_t>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
|
||||
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
|
||||
delete sound;
|
||||
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) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
auto sound = std::make_unique<JA_Sound_t>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
|
||||
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
|
||||
delete sound;
|
||||
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) {
|
||||
@@ -422,12 +545,12 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
|
||||
if (!JA_soundEnabled || !sound) 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].times = loop;
|
||||
channels[channel].pos = 0;
|
||||
channels[channel].group = group; // Asignar grupo
|
||||
channels[channel].group = group;
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||
|
||||
@@ -437,7 +560,7 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
|
||||
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_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||
|
||||
@@ -449,7 +572,7 @@ inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||
}
|
||||
SDL_free(sound->buffer);
|
||||
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
|
||||
delete sound;
|
||||
}
|
||||
|
||||
@@ -495,7 +618,7 @@ inline void JA_StopChannel(const int channel) {
|
||||
channels[i].stream = nullptr;
|
||||
channels[i].state = JA_CHANNEL_FREE;
|
||||
channels[i].pos = 0;
|
||||
channels[i].sound = NULL;
|
||||
channels[i].sound = nullptr;
|
||||
}
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
@@ -504,7 +627,7 @@ inline void JA_StopChannel(const int channel) {
|
||||
channels[channel].stream = nullptr;
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
channels[channel].pos = 0;
|
||||
channels[channel].sound = NULL;
|
||||
channels[channel].sound = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,8 +639,7 @@ inline JA_Channel_state JA_GetChannelState(const int channel) {
|
||||
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);
|
||||
|
||||
if (group == -1) {
|
||||
@@ -527,10 +649,10 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t
|
||||
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
||||
JA_soundVolume[group] = v;
|
||||
} 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++) {
|
||||
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
||||
if (group == -1 || channels[i].group == group) {
|
||||
@@ -545,13 +667,13 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t
|
||||
|
||||
inline void JA_EnableSound(const bool value) {
|
||||
if (!value) {
|
||||
JA_StopChannel(-1); // Detener todos los canales
|
||||
JA_StopChannel(-1);
|
||||
}
|
||||
JA_soundEnabled = value;
|
||||
}
|
||||
|
||||
inline float JA_SetVolume(float volume) {
|
||||
float v = JA_SetMusicVolume(volume);
|
||||
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
|
||||
JA_SetSoundVolume(v, -1);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
namespace GlobalInputs {
|
||||
// Termina
|
||||
void quit() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// En la versión web no se permite salir: el navegador gestiona el cierre.
|
||||
return;
|
||||
#else
|
||||
const std::string CODE = "QUIT";
|
||||
if (Notifier::get()->checkCode(CODE)) {
|
||||
// Si la notificación de salir está activa, cambia de sección
|
||||
@@ -30,6 +34,7 @@ namespace GlobalInputs {
|
||||
// Si la notificación de salir no está activa, muestra la notificación
|
||||
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 01"), std::string()}, -1, CODE);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reinicia
|
||||
|
||||
@@ -8,6 +8,41 @@
|
||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||
#include <utility> // Para pair, move
|
||||
|
||||
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
|
||||
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
|
||||
// parser extreu valors escombraries, el GUID resultant no està a la db i el
|
||||
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
|
||||
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
|
||||
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
|
||||
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
|
||||
static void installWebStandardMapping(SDL_JoystickID jid) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
|
||||
char guidStr[33];
|
||||
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
|
||||
const char* name = SDL_GetJoystickNameForID(jid);
|
||||
if (!name || !*name) name = "Standard Gamepad";
|
||||
|
||||
char mapping[512];
|
||||
SDL_snprintf(mapping, sizeof(mapping),
|
||||
"%s,%s,"
|
||||
"a:b0,b:b1,x:b2,y:b3,"
|
||||
"leftshoulder:b4,rightshoulder:b5,"
|
||||
"lefttrigger:b6,righttrigger:b7,"
|
||||
"back:b8,start:b9,"
|
||||
"leftstick:b10,rightstick:b11,"
|
||||
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
|
||||
"guide:b16,"
|
||||
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
|
||||
"platform:Emscripten",
|
||||
guidStr,
|
||||
name);
|
||||
SDL_AddGamepadMapping(mapping);
|
||||
#else
|
||||
(void)jid;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Singleton
|
||||
Input* Input::instance = nullptr;
|
||||
|
||||
@@ -303,20 +338,6 @@ void Input::addGamepadMappingsFromFile() {
|
||||
}
|
||||
}
|
||||
|
||||
void Input::discoverGamepads() {
|
||||
// Enumera los gamepads ya conectados sin drenar la cola de eventos de SDL
|
||||
// (necesario con SDL_MAIN_USE_CALLBACKS, que entrega los eventos por SDL_AppEvent).
|
||||
int count = 0;
|
||||
SDL_JoystickID* joysticks = SDL_GetGamepads(&count);
|
||||
if (joysticks == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
addGamepad(joysticks[i]);
|
||||
}
|
||||
SDL_free(joysticks);
|
||||
}
|
||||
|
||||
void Input::initSDLGamePad() {
|
||||
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
@@ -324,7 +345,9 @@ void Input::initSDLGamePad() {
|
||||
} else {
|
||||
addGamepadMappingsFromFile();
|
||||
loadGamepadConfigs();
|
||||
discoverGamepads();
|
||||
// Los mandos ya conectados llegan como SDL_EVENT_GAMEPAD_ADDED en el
|
||||
// primer pase del pump de eventos (antes del primer SDL_AppIterate),
|
||||
// por lo que no hace falta enumerarlos aquí a mano.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,6 +403,7 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string {
|
||||
}
|
||||
|
||||
auto Input::addGamepad(int device_index) -> std::string {
|
||||
installWebStandardMapping(device_index);
|
||||
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
|
||||
if (pad == nullptr) {
|
||||
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
|
||||
@@ -444,6 +468,27 @@ void Input::applyGamepadConfig(std::shared_ptr<Gamepad> gamepad) {
|
||||
return config.path == gamepad->path;
|
||||
});
|
||||
|
||||
// Fallback por nombre: si el mismo dispositivo se enchufa a otro puerto, su
|
||||
// path cambia pero el nombre suele mantenerse. Recuperamos su configuración
|
||||
// y actualizamos el path guardado al actual. Solo se acepta un match cuyo
|
||||
// path guardado NO corresponda a otro mando ya conectado, para no arruinar
|
||||
// setups con varios mandos idénticos en puertos distintos (cabinet arcade).
|
||||
if (config_it == gamepad_configs_.end() && !gamepad->name.empty()) {
|
||||
auto is_path_active = [this](const std::string& query_path) -> bool {
|
||||
return std::ranges::any_of(gamepads_, [&query_path](const std::shared_ptr<Gamepad>& g) -> bool {
|
||||
return g && g->path == query_path;
|
||||
});
|
||||
};
|
||||
config_it = std::ranges::find_if(gamepad_configs_, [&gamepad, &is_path_active](const GamepadConfig& config) -> bool {
|
||||
return config.name == gamepad->name && !is_path_active(config.path);
|
||||
});
|
||||
if (config_it != gamepad_configs_.end()) {
|
||||
std::cout << "Gamepad '" << gamepad->name << "' found by name, refreshing path to: " << gamepad->path << '\n';
|
||||
config_it->path = gamepad->path;
|
||||
saveGamepadConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
if (config_it != gamepad_configs_.end()) {
|
||||
// Se encontró una configuración específica para este puerto/dispositivo. La aplicamos.
|
||||
std::cout << "Applying custom config for gamepad at path: " << gamepad->path << '\n';
|
||||
@@ -453,7 +498,6 @@ void Input::applyGamepadConfig(std::shared_ptr<Gamepad> gamepad) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Opcional: Podrías añadir un fallback para buscar por nombre si no se encuentra por ruta.
|
||||
}
|
||||
|
||||
void Input::saveGamepadConfigFromGamepad(std::shared_ptr<Gamepad> gamepad) {
|
||||
|
||||
@@ -105,10 +105,20 @@ class Input {
|
||||
std::string path;
|
||||
std::unordered_map<Action, ButtonState> bindings;
|
||||
|
||||
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
|
||||
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
|
||||
static auto trimName(const char* raw) -> std::string {
|
||||
std::string s(raw != nullptr ? raw : "");
|
||||
const auto pos = s.find_first_of("([");
|
||||
if (pos != std::string::npos) { s.erase(pos); }
|
||||
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
|
||||
return s;
|
||||
}
|
||||
|
||||
Gamepad(SDL_Gamepad* gamepad)
|
||||
: pad(gamepad),
|
||||
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
|
||||
name(std::string(SDL_GetGamepadName(gamepad))),
|
||||
name(trimName(SDL_GetGamepadName(gamepad))),
|
||||
path(std::string(SDL_GetGamepadPath(pad))),
|
||||
bindings{
|
||||
// Movimiento del jugador
|
||||
@@ -206,7 +216,6 @@ class Input {
|
||||
auto addGamepad(int device_index) -> std::string;
|
||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||
void addGamepadMappingsFromFile();
|
||||
void discoverGamepads();
|
||||
|
||||
// --- Métodos para integración con GamepadConfigManager ---
|
||||
void loadGamepadConfigs();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
|
||||
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_SyncWindow, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm> // Para min, max
|
||||
#include <cstring> // Para memcpy
|
||||
@@ -9,24 +13,61 @@
|
||||
#include <string> // Para basic_string, operator+, char_traits, to_string, string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||
#ifndef NO_SHADERS
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||
#endif
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/rendering/texture.hpp" // Para Texture
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#include "core/resources/resource.hpp" // Para Resource
|
||||
#include "core/system/director.hpp" // Para Director::debug_config
|
||||
#include "game/options.hpp" // Para Video, video, Window, window
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
#include "utils/param.hpp" // Para Param, param, ParamGame, ParamDebug
|
||||
#include "utils/utils.hpp" // Para toLower
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/rendering/texture.hpp" // Para Texture
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#include "core/resources/resource.hpp" // Para Resource
|
||||
#include "core/system/director.hpp" // Para Director::debug_config
|
||||
#include "game/options.hpp" // Para Video, video, Window, window
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
#include "utils/param.hpp" // Para Param, param, ParamGame, ParamDebug
|
||||
#include "utils/utils.hpp" // Para toLower
|
||||
|
||||
// Singleton
|
||||
Screen* Screen::instance = nullptr;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// ============================================================================
|
||||
// Restauración del canvas en wasm/Emscripten
|
||||
// ============================================================================
|
||||
// SDL3 + Emscripten no notifica de forma fiable los cambios de estado del
|
||||
// canvas HTML (fullscreen exit vía Esc, rotación del dispositivo, etc.).
|
||||
// Registramos callbacks nativos de Emscripten que delegan en
|
||||
// Screen::handleCanvasResized(), el cual re-aplica el modo de fullscreen y
|
||||
// reajusta la ventana para que SDL salga de su estado interno de fullscreen.
|
||||
//
|
||||
// Los callbacks difieren el trabajo con emscripten_async_call(0ms) porque el
|
||||
// navegador todavía no ha acabado de redimensionar el canvas cuando el evento
|
||||
// se dispara; posponer al siguiente tick garantiza valores estables.
|
||||
//
|
||||
// Referencias: libsdl-org/SDL#13300, libsdl-org/SDL#11389.
|
||||
// ============================================================================
|
||||
namespace {
|
||||
void deferredCanvasResize(void* /*user_data*/) {
|
||||
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
|
||||
}
|
||||
|
||||
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
|
||||
// Sincronizamos Options::video.fullscreen con el estado real del navegador
|
||||
// antes de diferir la restauración: cuando el usuario sale con Esc no pasa
|
||||
// por toggleFullscreen() y el estado interno quedaría desincronizado.
|
||||
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
|
||||
emscripten_async_call(deferredCanvasResize, nullptr, 0);
|
||||
return EM_FALSE;
|
||||
}
|
||||
|
||||
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
|
||||
emscripten_async_call(deferredCanvasResize, nullptr, 0);
|
||||
return EM_FALSE;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Screen::init() {
|
||||
Screen::instance = new Screen();
|
||||
@@ -53,7 +94,7 @@ Screen::Screen()
|
||||
|
||||
// Crea la textura de destino
|
||||
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
|
||||
SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST);
|
||||
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
|
||||
|
||||
// Inicializar buffer de píxeles para SDL3GPU
|
||||
pixel_buffer_.resize(static_cast<size_t>(param.game.width) * static_cast<size_t>(param.game.height));
|
||||
@@ -69,6 +110,14 @@ Screen::Screen()
|
||||
// Renderizar una vez la textura vacía para que tenga contenido válido antes de inicializar los shaders (evita pantalla negra)
|
||||
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
|
||||
|
||||
// Aplicar la configuración inicial completa (vsync + logical presentation +
|
||||
// fullscreen + tamaño de ventana). En Emscripten es necesario porque el
|
||||
// canvas HTML tiene un tamaño propio y SDL_CreateWindow solo no basta para
|
||||
// que SDL sincronice su viewport interno con el canvas real: sin este
|
||||
// applySettings el canvas inicial sale descolocado con barras negras a los
|
||||
// lados y el juego pequeño en el centro hasta el primer toggle de fullscreen.
|
||||
applySettings();
|
||||
|
||||
// Limpiar renderer
|
||||
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
|
||||
SDL_RenderClear(renderer_);
|
||||
@@ -145,10 +194,39 @@ void Screen::setFullscreenMode() {
|
||||
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||
}
|
||||
|
||||
// Camibia entre pantalla completa y ventana
|
||||
// Cambia entre pantalla completa y ventana. Usamos applySettings en vez de
|
||||
// setFullscreenMode porque applySettings también re-aplica la logical
|
||||
// presentation — sin eso, al entrar en fullscreen SDL no recalcula el viewport
|
||||
// y el juego se ve pequeño (especialmente en Android).
|
||||
void Screen::toggleFullscreen() {
|
||||
Options::video.fullscreen = !Options::video.fullscreen;
|
||||
setFullscreenMode();
|
||||
applySettings();
|
||||
}
|
||||
|
||||
// Re-sincroniza SDL con el estado real del canvas del navegador. Lo invocan los
|
||||
// callbacks nativos de Emscripten (vegeu el bloc al principi del fitxer) cuando
|
||||
// se detecta un fullscreenchange o un orientationchange. Re-aplica fullscreen y
|
||||
// reajusta la ventana porque SDL no emite SDL_EVENT_WINDOW_LEAVE_FULLSCREEN en
|
||||
// Emscripten y su estado interno queda desincronizado al salir con Esc.
|
||||
// Fuera de Emscripten es un no-op (desktop sí emite los events correctamente).
|
||||
void Screen::handleCanvasResized() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// SDL_SetWindowFullscreen es imprescindible para sacar a SDL de su estado
|
||||
// interno de fullscreen cuando el usuario ha salido sin pasar por
|
||||
// toggleFullscreen (onEmFullscreenChange ya ha actualizado Options).
|
||||
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||
SDL_SyncWindow(window_);
|
||||
adjustWindowSize();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
|
||||
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
|
||||
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
|
||||
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Cambia el tamaño de la ventana
|
||||
@@ -424,6 +502,8 @@ auto Screen::initSDLVideo() -> bool {
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
|
||||
registerEmscriptenEventCallbacks();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -590,6 +670,25 @@ void Screen::toggleVSync() {
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica Options::video.scale_mode a la textura del canvas de juego
|
||||
void Screen::applyFilter() {
|
||||
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
|
||||
}
|
||||
|
||||
// Alterna el modo de filtrado entre nearest y linear
|
||||
void Screen::toggleFilter() {
|
||||
Options::video.scale_mode = (Options::video.scale_mode == SDL_SCALEMODE_NEAREST)
|
||||
? SDL_SCALEMODE_LINEAR
|
||||
: SDL_SCALEMODE_NEAREST;
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
// Devuelve true si el backend SDL3GPU está activo y con aceleración hardware
|
||||
auto Screen::isHardwareAccelerated() -> bool {
|
||||
auto* self = Screen::get();
|
||||
return self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated();
|
||||
}
|
||||
|
||||
// Establece el estado del V-Sync
|
||||
void Screen::setVSync(bool enabled) {
|
||||
Options::video.vsync = enabled;
|
||||
@@ -611,12 +710,17 @@ void Screen::getSingletons() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Aplica los valores de las opciones
|
||||
// Aplica los valores de las opciones.
|
||||
// IMPORTANTE: el orden importa. SDL_SetRenderLogicalPresentation calcula el
|
||||
// viewport en función del tamaño actual de la ventana SDL, así que DEBE llamarse
|
||||
// DESPUÉS de setFullscreenMode/adjustWindowSize — si no, al entrar en fullscreen
|
||||
// el viewport queda cacheado al tamaño de la ventana pequeña previa y el juego
|
||||
// se ve pequeño y centrado con barras negras alrededor.
|
||||
void Screen::applySettings() {
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
SDL_SetRenderLogicalPresentation(Screen::get()->getRenderer(), param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
setFullscreenMode();
|
||||
adjustWindowSize();
|
||||
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
}
|
||||
|
||||
// Crea el objeto de texto
|
||||
|
||||
@@ -34,6 +34,7 @@ class Screen {
|
||||
// --- Configuración de ventana y render ---
|
||||
void setFullscreenMode(); // Establece el modo de pantalla completa
|
||||
void toggleFullscreen(); // Cambia entre pantalla completa y ventana
|
||||
void handleCanvasResized(); // Restaura el canvas cuando SDL3 no reporta el cambio (emscripten only: salida de fullscreen con Esc, rotación); no-op fuera de emscripten
|
||||
void setWindowZoom(int zoom); // Cambia el tamaño de la ventana
|
||||
auto decWindowSize() -> bool; // Reduce el tamaño de la ventana
|
||||
auto incWindowSize() -> bool; // Aumenta el tamaño de la ventana
|
||||
@@ -48,6 +49,8 @@ class Screen {
|
||||
static void nextPostFXPreset(); // Avanza al siguiente preset PostFX
|
||||
static void nextCrtPiPreset(); // Avanza al siguiente preset CrtPi
|
||||
static void toggleSupersampling(); // Alterna supersampling
|
||||
void toggleFilter(); // Alterna SDL_SCALEMODE_NEAREST ↔ SDL_SCALEMODE_LINEAR
|
||||
void applyFilter(); // Aplica Options::video.scale_mode a game_canvas_
|
||||
void toggleIntegerScale();
|
||||
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||
void setVSync(bool enabled); // Establece el estado del V-Sync
|
||||
@@ -59,6 +62,7 @@ class Screen {
|
||||
void hide() { SDL_HideWindow(window_); } // Oculta la ventana
|
||||
void getSingletons(); // Obtiene los punteros a los singletones
|
||||
[[nodiscard]] static auto getVSync() -> bool { return Options::video.vsync; } // Obtiene el valor de V-Sync
|
||||
[[nodiscard]] static auto isHardwareAccelerated() -> bool; // True si SDL3GPU está activo
|
||||
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; } // Obtiene el puntero al texto de Screen
|
||||
|
||||
// --- Display Monitor getters ---
|
||||
@@ -232,18 +236,19 @@ class Screen {
|
||||
#endif
|
||||
|
||||
// --- Métodos internos ---
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void renderFlash(); // Dibuja el efecto de flash en la pantalla
|
||||
void renderShake(); // Aplica el efecto de agitar la pantalla
|
||||
void renderInfo() const; // Muestra información por pantalla
|
||||
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
|
||||
void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica el preset CrtPi activo al backend
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
void renderOverlays(); // Renderiza todos los overlays y efectos
|
||||
void renderAttenuate(); // Atenúa la pantalla
|
||||
void createText(); // Crea el objeto de texto
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void registerEmscriptenEventCallbacks(); // Registra callbacks nativos para restaurar el canvas en wasm (no-op fuera de emscripten)
|
||||
void renderFlash(); // Dibuja el efecto de flash en la pantalla
|
||||
void renderShake(); // Aplica el efecto de agitar la pantalla
|
||||
void renderInfo() const; // Muestra información por pantalla
|
||||
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
|
||||
void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica el preset CrtPi activo al backend
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
void renderOverlays(); // Renderiza todos los overlays y efectos
|
||||
void renderAttenuate(); // Atenúa la pantalla
|
||||
void createText(); // Crea el objeto de texto
|
||||
|
||||
// --- Constructores y destructor privados (singleton) ---
|
||||
Screen(); // Constructor privado
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#include <iostream> // Para std::cout
|
||||
|
||||
#ifndef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/crtpi_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/downscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/postfx_vert_spv.h"
|
||||
#include "core/rendering/sdl3gpu/upscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
2
source/core/rendering/sdl3gpu/spv/.clang-format
Normal file
2
source/core/rendering/sdl3gpu/spv/.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
DisableFormat: true
|
||||
SortIncludes: Never
|
||||
4
source/core/rendering/sdl3gpu/spv/.clang-tidy
Normal file
4
source/core/rendering/sdl3gpu/spv/.clang-tidy
Normal file
@@ -0,0 +1,4 @@
|
||||
# source/core/rendering/sdl3gpu/spv/.clang-tidy
|
||||
Checks: '-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
@@ -18,7 +18,9 @@
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/asset.hpp" // Para Asset
|
||||
#include "core/resources/resource_helper.hpp" // Para loadFile
|
||||
#include "game/options.hpp" // Para Options::loading
|
||||
#include "utils/color.hpp" // Para Color, NO_COLOR_MOD
|
||||
#include "utils/defines.hpp" // Para Texts::VERSION
|
||||
#include "utils/param.hpp" // Para Param, param, ParamPlayer, ParamResource, ParamGame
|
||||
#include "utils/utils.hpp" // Para getFileName
|
||||
#include "version.h" // Para APP_NAME, GIT_HASH
|
||||
@@ -65,7 +67,8 @@ Resource::Resource(LoadingMode mode)
|
||||
Screen::get()->show();
|
||||
if (loading_mode_ == LoadingMode::PRELOAD) {
|
||||
loading_text_ = Screen::get()->getText();
|
||||
load();
|
||||
// Ya NO llamamos load() aquí: Director bombea beginLoad() + loadStep()
|
||||
// desde iterate() para mantener vivo el bucle SDL3 durante la carga.
|
||||
} else {
|
||||
// En modo lazy, cargamos lo mínimo indispensable
|
||||
initResourceLists();
|
||||
@@ -411,31 +414,128 @@ void Resource::clear() {
|
||||
demos_.clear();
|
||||
}
|
||||
|
||||
// Carga todos los recursos del juego y muestra el progreso de carga
|
||||
// Carga síncrona completa: usado por Resource::reload() (hot-reload en debug).
|
||||
// En arranque normal la carga la bombea Director::iterate() vía loadStep().
|
||||
void Resource::load() {
|
||||
// Prepara la gestión del progreso de carga
|
||||
beginLoad();
|
||||
while (!loadStep(INT_MAX)) {
|
||||
// Presupuesto infinito: una sola pasada carga todo
|
||||
}
|
||||
Screen::get()->setVSync(saved_vsync_);
|
||||
}
|
||||
|
||||
// Prepara el estado del cargador incremental. No carga nada todavía.
|
||||
void Resource::beginLoad() {
|
||||
calculateTotalResources();
|
||||
initProgressBar();
|
||||
|
||||
// Muerstra la ventana y desactiva el sincronismo vertical
|
||||
auto* screen = Screen::get();
|
||||
auto vsync = Screen::getVSync();
|
||||
screen->setVSync(false);
|
||||
saved_vsync_ = Screen::getVSync();
|
||||
Screen::get()->setVSync(false); // Maximiza FPS durante el preload
|
||||
|
||||
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** LOADING RESOURCES");
|
||||
loadSounds(); // Carga sonidos
|
||||
loadMusics(); // Carga músicas
|
||||
loadTextures(); // Carga texturas
|
||||
loadTextFiles(); // Carga ficheros de texto
|
||||
loadAnimations(); // Carga animaciones
|
||||
loadDemoData(); // Carga datos de demo
|
||||
createText(); // Crea objetos de texto
|
||||
createTextTextures(); // Crea texturas a partir de texto
|
||||
createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta
|
||||
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** RESOURCES LOADED");
|
||||
stage_ = LoadStage::SOUNDS;
|
||||
stage_index_ = 0;
|
||||
}
|
||||
|
||||
// Restablece el sincronismo vertical a su valor original
|
||||
screen->setVSync(vsync);
|
||||
auto Resource::isLoadDone() const -> bool {
|
||||
return stage_ == LoadStage::DONE;
|
||||
}
|
||||
|
||||
// Bombea la máquina de etapas hasta agotar el presupuesto de tiempo o completar la carga.
|
||||
// Devuelve true cuando ya no queda nada por cargar.
|
||||
auto Resource::loadStep(int budget_ms) -> bool {
|
||||
if (stage_ == LoadStage::DONE) { return true; }
|
||||
|
||||
const Uint64 start_ns = SDL_GetTicksNS();
|
||||
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
|
||||
|
||||
while (stage_ != LoadStage::DONE) {
|
||||
switch (stage_) {
|
||||
case LoadStage::SOUNDS: {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::SOUND);
|
||||
if (stage_index_ == 0) { sounds_.clear(); }
|
||||
if (stage_index_ >= list.size()) {
|
||||
stage_ = LoadStage::MUSICS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneSound(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::MUSICS: {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::MUSIC);
|
||||
if (stage_index_ == 0) { musics_.clear(); }
|
||||
if (stage_index_ >= list.size()) {
|
||||
stage_ = LoadStage::TEXTURES;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneMusic(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::TEXTURES: {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::BITMAP);
|
||||
if (stage_index_ == 0) { textures_.clear(); }
|
||||
if (stage_index_ >= list.size()) {
|
||||
stage_ = LoadStage::TEXT_FILES;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneTexture(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::TEXT_FILES: {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::FONT);
|
||||
if (stage_index_ == 0) { text_files_.clear(); }
|
||||
if (stage_index_ >= list.size()) {
|
||||
stage_ = LoadStage::ANIMATIONS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneTextFile(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::ANIMATIONS: {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::ANIMATION);
|
||||
if (stage_index_ == 0) { animations_.clear(); }
|
||||
if (stage_index_ >= list.size()) {
|
||||
stage_ = LoadStage::DEMO_DATA;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneAnimation(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::DEMO_DATA: {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
|
||||
if (stage_index_ == 0) { demos_.clear(); }
|
||||
if (stage_index_ >= list.size()) {
|
||||
stage_ = LoadStage::CREATE_TEXT;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneDemoData(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::CREATE_TEXT:
|
||||
createText();
|
||||
stage_ = LoadStage::CREATE_TEXT_TEXTURES;
|
||||
break;
|
||||
case LoadStage::CREATE_TEXT_TEXTURES:
|
||||
createTextTextures();
|
||||
stage_ = LoadStage::CREATE_PLAYER_TEXTURES;
|
||||
break;
|
||||
case LoadStage::CREATE_PLAYER_TEXTURES:
|
||||
createPlayerTextures();
|
||||
stage_ = LoadStage::DONE;
|
||||
break;
|
||||
case LoadStage::DONE:
|
||||
break;
|
||||
}
|
||||
|
||||
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) { break; }
|
||||
}
|
||||
|
||||
return stage_ == LoadStage::DONE;
|
||||
}
|
||||
|
||||
// Recarga todos los recursos (limpia y vuelve a cargar)
|
||||
@@ -448,96 +548,78 @@ void Resource::reload() {
|
||||
}
|
||||
}
|
||||
|
||||
// Carga los sonidos del juego
|
||||
void Resource::loadSounds() {
|
||||
// Carga un sonido concreto desde la lista de assets
|
||||
void Resource::loadOneSound(size_t idx) {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::SOUND);
|
||||
sounds_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
auto name = getFileName(l);
|
||||
updateLoadingProgress(name);
|
||||
auto audio_data = loadAudioData(l);
|
||||
JA_Sound_t* sound = nullptr;
|
||||
if (!audio_data.data.empty()) {
|
||||
sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size());
|
||||
} else {
|
||||
sound = JA_LoadSound(l.c_str());
|
||||
}
|
||||
if (sound == nullptr) {
|
||||
std::cout << "Sound load failed: " << name << '\n';
|
||||
}
|
||||
sounds_.emplace_back(name, sound);
|
||||
const auto& path = list[idx];
|
||||
auto name = getFileName(path);
|
||||
updateLoadingProgress(name);
|
||||
auto audio_data = loadAudioData(path);
|
||||
JA_Sound_t* sound = nullptr;
|
||||
if (!audio_data.data.empty()) {
|
||||
sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size());
|
||||
} else {
|
||||
sound = JA_LoadSound(path.c_str());
|
||||
}
|
||||
if (sound == nullptr) {
|
||||
std::cout << "Sound load failed: " << name << '\n';
|
||||
}
|
||||
sounds_.emplace_back(name, sound);
|
||||
}
|
||||
|
||||
// Carga las músicas del juego
|
||||
void Resource::loadMusics() {
|
||||
// Carga una música concreta desde la lista de assets
|
||||
void Resource::loadOneMusic(size_t idx) {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::MUSIC);
|
||||
musics_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
auto name = getFileName(l);
|
||||
updateLoadingProgress(name);
|
||||
auto audio_data = loadAudioData(l);
|
||||
JA_Music_t* music = nullptr;
|
||||
if (!audio_data.data.empty()) {
|
||||
music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size());
|
||||
} else {
|
||||
music = JA_LoadMusic(l.c_str());
|
||||
}
|
||||
if (music == nullptr) {
|
||||
std::cout << "Music load failed: " << name << '\n';
|
||||
}
|
||||
musics_.emplace_back(name, music);
|
||||
const auto& path = list[idx];
|
||||
auto name = getFileName(path);
|
||||
updateLoadingProgress(name);
|
||||
auto audio_data = loadAudioData(path);
|
||||
JA_Music_t* music = nullptr;
|
||||
if (!audio_data.data.empty()) {
|
||||
music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size());
|
||||
} else {
|
||||
music = JA_LoadMusic(path.c_str());
|
||||
}
|
||||
if (music == nullptr) {
|
||||
std::cout << "Music load failed: " << name << '\n';
|
||||
}
|
||||
musics_.emplace_back(name, music);
|
||||
}
|
||||
|
||||
// Carga las texturas del juego
|
||||
void Resource::loadTextures() {
|
||||
// Carga una textura concreta desde la lista de assets
|
||||
void Resource::loadOneTexture(size_t idx) {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::BITMAP);
|
||||
textures_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
auto name = getFileName(l);
|
||||
updateLoadingProgress(name);
|
||||
textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), l));
|
||||
}
|
||||
const auto& path = list[idx];
|
||||
auto name = getFileName(path);
|
||||
updateLoadingProgress(name);
|
||||
textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), path));
|
||||
}
|
||||
|
||||
// Carga los ficheros de texto del juego
|
||||
void Resource::loadTextFiles() {
|
||||
// Carga un fichero de texto concreto desde la lista de assets
|
||||
void Resource::loadOneTextFile(size_t idx) {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::FONT);
|
||||
text_files_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
auto name = getFileName(l);
|
||||
updateLoadingProgress(name);
|
||||
text_files_.emplace_back(name, Text::loadFile(l));
|
||||
}
|
||||
const auto& path = list[idx];
|
||||
auto name = getFileName(path);
|
||||
updateLoadingProgress(name);
|
||||
text_files_.emplace_back(name, Text::loadFile(path));
|
||||
}
|
||||
|
||||
// Carga las animaciones del juego
|
||||
void Resource::loadAnimations() {
|
||||
// Carga una animación concreta desde la lista de assets
|
||||
void Resource::loadOneAnimation(size_t idx) {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::ANIMATION);
|
||||
animations_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
auto name = getFileName(l);
|
||||
updateLoadingProgress(name);
|
||||
animations_.emplace_back(name, loadAnimationsFromFile(l));
|
||||
}
|
||||
const auto& path = list[idx];
|
||||
auto name = getFileName(path);
|
||||
updateLoadingProgress(name);
|
||||
animations_.emplace_back(name, loadAnimationsFromFile(path));
|
||||
}
|
||||
|
||||
// Carga los datos para el modo demostración
|
||||
void Resource::loadDemoData() {
|
||||
// Carga un fichero de datos de demo concreto desde la lista de assets
|
||||
void Resource::loadOneDemoData(size_t idx) {
|
||||
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
|
||||
demos_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
auto name = getFileName(l);
|
||||
updateLoadingProgress(name);
|
||||
demos_.emplace_back(loadDemoDataFromFile(l));
|
||||
}
|
||||
const auto& path = list[idx];
|
||||
auto name = getFileName(path);
|
||||
updateLoadingProgress(name);
|
||||
demos_.emplace_back(loadDemoDataFromFile(path));
|
||||
}
|
||||
|
||||
// Crea las texturas de jugadores con todas sus variantes de paleta
|
||||
@@ -754,6 +836,16 @@ void Resource::renderProgress() {
|
||||
screen->start();
|
||||
screen->clean();
|
||||
|
||||
// Si la pantalla de carga está desactivada, dejamos todo en negro.
|
||||
// wait_for_input solo tiene efecto cuando la pantalla está visible.
|
||||
if (!Options::loading.show) {
|
||||
screen->coreRender();
|
||||
return;
|
||||
}
|
||||
|
||||
// Estamos en la fase de espera explícita al usuario tras terminar la carga
|
||||
const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input;
|
||||
|
||||
auto text_color = param.resource.color;
|
||||
auto bar_color = param.resource.color.DARKEN(100);
|
||||
const auto TEXT_HEIGHT = loading_text_->getCharacterSize();
|
||||
@@ -766,14 +858,20 @@ void Resource::renderProgress() {
|
||||
SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a);
|
||||
SDL_RenderRect(renderer, &loading_wired_rect_);
|
||||
|
||||
// Escribe el texto de carga encima de la barra
|
||||
/*
|
||||
loading_text_->writeColored(
|
||||
loading_wired_rect_.x,
|
||||
loading_wired_rect_.y - 9,
|
||||
Lang::getText("[RESOURCE] LOADING") + " : " + loading_resource_name_,
|
||||
text_color);
|
||||
*/
|
||||
// Texto centrado sobre la barra: mientras carga, el nombre del recurso;
|
||||
// al terminar en modo wait_for_input, el prompt traducido.
|
||||
const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT
|
||||
? Lang::getText("[RESOURCE] PRESS_TO_CONTINUE")
|
||||
: loading_resource_name_;
|
||||
if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) {
|
||||
loading_text_->writeDX(
|
||||
Text::CENTER | Text::COLOR,
|
||||
loading_wired_rect_.x + (loading_wired_rect_.w / 2),
|
||||
loading_wired_rect_.y - TEXT_HEIGHT - 2,
|
||||
OVER_BAR_TEXT,
|
||||
1,
|
||||
text_color);
|
||||
}
|
||||
|
||||
// Muestra nombre de la aplicación
|
||||
loading_text_->writeDX(
|
||||
@@ -784,32 +882,15 @@ void Resource::renderProgress() {
|
||||
1,
|
||||
text_color);
|
||||
|
||||
// Muestra la versión
|
||||
// Muestra la versión y el hash del commit
|
||||
loading_text_->writeDX(
|
||||
Text::CENTER | Text::COLOR,
|
||||
param.game.game_area.center_x,
|
||||
param.game.game_area.center_y + TEXT_HEIGHT,
|
||||
"(" + std::string(Version::GIT_HASH) + ")",
|
||||
"ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")",
|
||||
1,
|
||||
text_color);
|
||||
|
||||
// Muestra información del monitor desplazada hacia abajo
|
||||
/*loading_text_->writeColored(
|
||||
X_PADDING,
|
||||
Y_PADDING + 18,
|
||||
screen->getDisplayMonitorName(),
|
||||
text_color);
|
||||
loading_text_->writeColored(
|
||||
X_PADDING,
|
||||
Y_PADDING + 27,
|
||||
std::to_string(screen->getDisplayMonitorWidth()) + "x" + std::to_string(screen->getDisplayMonitorHeight()),
|
||||
text_color);
|
||||
loading_text_->writeColored(
|
||||
X_PADDING,
|
||||
Y_PADDING + 36,
|
||||
std::to_string(screen->getDisplayMonitorRefreshRate()) + "Hz",
|
||||
text_color);*/
|
||||
|
||||
// Renderiza el frame en pantalla
|
||||
screen->coreRender();
|
||||
}
|
||||
@@ -826,21 +907,22 @@ void Resource::loadDemoDataQuiet() {
|
||||
|
||||
// Inicializa los rectangulos que definen la barra de progreso
|
||||
void Resource::initProgressBar() {
|
||||
const float BAR_Y_POSITION = param.game.height - BAR_HEIGHT - Y_PADDING;
|
||||
const float WIRED_BAR_WIDTH = param.game.width * BAR_WIDTH_RATIO;
|
||||
const float BAR_X_POSITION = (param.game.width - WIRED_BAR_WIDTH) / 2.0F;
|
||||
const float BAR_Y_POSITION = (param.game.height * BAR_Y_RATIO) - (BAR_HEIGHT / 2.0F);
|
||||
|
||||
const float WIRED_BAR_WIDTH = param.game.width - (X_PADDING * 2);
|
||||
loading_wired_rect_ = {.x = X_PADDING, .y = BAR_Y_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
loading_wired_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
|
||||
const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * loading_count_.getPercentage();
|
||||
loading_full_rect_ = {.x = X_PADDING, .y = BAR_Y_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
loading_full_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
}
|
||||
|
||||
// Actualiza el progreso de carga y muestra la barra
|
||||
// Actualiza el estado del progreso. No renderiza: el repintado lo hace
|
||||
// Preload::iterate una vez por frame llamando a renderProgress().
|
||||
void Resource::updateLoadingProgress(std::string name) {
|
||||
loading_resource_name_ = std::move(name);
|
||||
loading_count_.increase();
|
||||
updateProgressBar();
|
||||
renderProgress();
|
||||
}
|
||||
|
||||
// Actualiza la barra de estado
|
||||
|
||||
@@ -42,6 +42,15 @@ class Resource {
|
||||
// --- Métodos de recarga de recursos ---
|
||||
void reload(); // Recarga todos los recursos
|
||||
|
||||
// --- Cargador incremental ---
|
||||
// beginLoad prepara el estado; loadStep carga recursos hasta agotar el presupuesto;
|
||||
// devuelve true cuando ya no queda nada. renderProgress se llama una vez por frame
|
||||
// desde la escena Preload.
|
||||
void beginLoad();
|
||||
auto loadStep(int budget_ms) -> bool;
|
||||
[[nodiscard]] auto isLoadDone() const -> bool;
|
||||
void renderProgress();
|
||||
|
||||
// --- Método para obtener el modo de carga actual ---
|
||||
[[nodiscard]] auto getLoadingMode() const -> LoadingMode { return loading_mode_; }
|
||||
|
||||
@@ -117,12 +126,15 @@ class Resource {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Constantes para la pantalla de carga ---
|
||||
static constexpr float X_PADDING = 60.0F;
|
||||
static constexpr float Y_PADDING = 20.0F;
|
||||
// --- Constantes para la barra de progreso de la pantalla de carga ---
|
||||
// BAR_WIDTH_RATIO: fracción del ancho de la pantalla ocupada por la barra (centrada)
|
||||
// 1.0 = ancho completo, 0.5 = la mitad del ancho
|
||||
// BAR_Y_RATIO: posición vertical del CENTRO de la barra en fracción de la altura
|
||||
// 0.0 = centro en el borde superior, 1.0 = centro en el borde inferior
|
||||
// BAR_HEIGHT: grosor de la barra en píxeles
|
||||
static constexpr float BAR_WIDTH_RATIO = 0.5F;
|
||||
static constexpr float BAR_Y_RATIO = 0.85F;
|
||||
static constexpr float BAR_HEIGHT = 5.0F;
|
||||
static constexpr Color BAR_COLOR = Color(128, 128, 128);
|
||||
static constexpr Color TEXT_COLOR = Color(255, 255, 255);
|
||||
|
||||
// --- Modo de carga ---
|
||||
LoadingMode loading_mode_;
|
||||
@@ -143,13 +155,24 @@ class Resource {
|
||||
SDL_FRect loading_wired_rect_;
|
||||
SDL_FRect loading_full_rect_;
|
||||
|
||||
// --- Estado del cargador incremental ---
|
||||
enum class LoadStage {
|
||||
SOUNDS,
|
||||
MUSICS,
|
||||
TEXTURES,
|
||||
TEXT_FILES,
|
||||
ANIMATIONS,
|
||||
DEMO_DATA,
|
||||
CREATE_TEXT,
|
||||
CREATE_TEXT_TEXTURES,
|
||||
CREATE_PLAYER_TEXTURES,
|
||||
DONE
|
||||
};
|
||||
LoadStage stage_{LoadStage::DONE};
|
||||
size_t stage_index_{0};
|
||||
bool saved_vsync_{false}; // Vsync previo a beginLoad, restaurado por finishBoot/load
|
||||
|
||||
// --- Métodos internos de carga y gestión ---
|
||||
void loadSounds(); // Carga los sonidos
|
||||
void loadMusics(); // Carga las músicas
|
||||
void loadTextures(); // Carga las texturas
|
||||
void loadTextFiles(); // Carga los ficheros de texto
|
||||
void loadAnimations(); // Carga las animaciones
|
||||
void loadDemoData(); // Carga los datos para el modo demostración
|
||||
void loadDemoDataQuiet(); // Carga los datos de demo sin mostrar progreso (para modo lazy)
|
||||
void loadEssentialResources(); // Carga recursos esenciales en modo lazy
|
||||
void loadEssentialTextures(); // Carga solo las texturas esenciales (fuentes)
|
||||
@@ -173,11 +196,18 @@ class Resource {
|
||||
|
||||
// --- Métodos internos para gestionar el progreso ---
|
||||
void calculateTotalResources(); // Calcula el número de recursos para cargar
|
||||
void renderProgress(); // Muestra el progreso de carga
|
||||
void updateLoadingProgress(std::string name); // Actualiza el progreso de carga
|
||||
void initProgressBar(); // Inicializa los rectangulos que definen la barra de progreso
|
||||
void updateProgressBar(); // Actualiza la barra de estado
|
||||
|
||||
// --- Helpers del cargador incremental (cargan un único recurso) ---
|
||||
void loadOneSound(size_t idx);
|
||||
void loadOneMusic(size_t idx);
|
||||
void loadOneTexture(size_t idx);
|
||||
void loadOneTextFile(size_t idx);
|
||||
void loadOneAnimation(size_t idx);
|
||||
void loadOneDemoData(size_t idx);
|
||||
|
||||
// --- Constructores y destructor privados (singleton) ---
|
||||
explicit Resource(LoadingMode mode); // Constructor privado con modo de carga
|
||||
~Resource(); // Destructor privado
|
||||
|
||||
@@ -195,17 +195,17 @@ namespace Defaults::Video {
|
||||
|
||||
namespace Defaults::Music {
|
||||
constexpr bool ENABLED = true;
|
||||
constexpr int VOLUME = 100;
|
||||
constexpr float VOLUME = 0.8F;
|
||||
} // namespace Defaults::Music
|
||||
|
||||
namespace Defaults::Sound {
|
||||
constexpr bool ENABLED = true;
|
||||
constexpr int VOLUME = 100;
|
||||
constexpr float VOLUME = 1.0F;
|
||||
} // namespace Defaults::Sound
|
||||
|
||||
namespace Defaults::Audio {
|
||||
constexpr bool ENABLED = true;
|
||||
constexpr int VOLUME = 100;
|
||||
constexpr float VOLUME = 1.0F;
|
||||
} // namespace Defaults::Audio
|
||||
|
||||
namespace Defaults::Settings {
|
||||
@@ -213,3 +213,9 @@ namespace Defaults::Settings {
|
||||
constexpr bool SHUTDOWN_ENABLED = false;
|
||||
constexpr const char* PARAMS_FILE = "param_320x256.txt";
|
||||
} // namespace Defaults::Settings
|
||||
|
||||
namespace Defaults::Loading {
|
||||
constexpr bool SHOW = false;
|
||||
constexpr bool SHOW_RESOURCE_NAME = true;
|
||||
constexpr bool WAIT_FOR_INPUT = false;
|
||||
} // namespace Defaults::Loading
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "game/scenes/instructions.hpp" // Para Instructions
|
||||
#include "game/scenes/intro.hpp" // Para Intro
|
||||
#include "game/scenes/logo.hpp" // Para Logo
|
||||
#include "game/scenes/preload.hpp" // Para Preload
|
||||
#include "game/scenes/title.hpp" // Para Title
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
@@ -119,14 +120,17 @@ void Director::init() {
|
||||
Options::loadCrtPiFromFile(); // Carga los presets CrtPi
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// En la versión web el navegador gestiona la ventana: forzamos zoom=1,
|
||||
// fullscreen para ocupar el canvas, e integer scale para píxeles nítidos.
|
||||
Options::window.zoom = 1;
|
||||
Options::video.fullscreen = true;
|
||||
// En la versión web el navegador gestiona la ventana: ventana (no
|
||||
// fullscreen — el canvas ya marca el área), integer scale para píxeles nítidos.
|
||||
Options::window.zoom = 3;
|
||||
Options::video.fullscreen = false;
|
||||
Options::video.integer_scale = true;
|
||||
// Precarga silenciosa: pantalla negra mientras el .data termina de descargarse.
|
||||
Options::loading.show = false;
|
||||
Options::loading.wait_for_input = false;
|
||||
#endif
|
||||
loadParams(); // Carga los parámetros del programa
|
||||
loadScoreFile(); // Carga el archivo de puntuaciones
|
||||
loadParams(); // Carga los parámetros del programa
|
||||
loadScoreFile(); // Carga el archivo de puntuaciones
|
||||
|
||||
// Inicialización de subsistemas principales
|
||||
Lang::setLanguage(Options::settings.language); // Carga el archivo de idioma
|
||||
@@ -140,9 +144,40 @@ void Director::init() {
|
||||
#else
|
||||
Resource::init(Resource::LoadingMode::PRELOAD);
|
||||
#endif
|
||||
|
||||
if (Resource::get()->getLoadingMode() == Resource::LoadingMode::PRELOAD) {
|
||||
// Guarda la sección destino (la que fijó loadDebugConfig o el default)
|
||||
// y redirige el arranque a la escena PRELOAD hasta que loadStep termine.
|
||||
Section::post_preload = Section::name;
|
||||
Section::name = Section::Name::PRELOAD;
|
||||
Resource::get()->beginLoad();
|
||||
} else {
|
||||
// LAZY_LOAD: el constructor de Resource ya cargó lo esencial síncronamente;
|
||||
// no hay fase de preload, pasamos directamente a post-boot.
|
||||
finishBoot();
|
||||
boot_loading_ = false;
|
||||
}
|
||||
|
||||
// ServiceMenu/Notifier/getSingletons se mueven a finishBoot() — dependen
|
||||
// de Resource y se inicializan al terminar la carga incremental.
|
||||
}
|
||||
|
||||
// Inicializaciones que dependen del Resource cargado. Se llama desde iterate()
|
||||
// cuando Resource::loadStep() devuelve true, con la ventana y el bucle vivos.
|
||||
void Director::finishBoot() {
|
||||
ServiceMenu::init(); // Inicializa el menú de servicio
|
||||
Notifier::init(std::string(), Resource::get()->getText("8bithud")); // Inicialización del sistema de notificaciones
|
||||
Screen::get()->getSingletons(); // Obtiene los punteros al resto de singletones
|
||||
|
||||
// Restaura el vsync a la preferencia del usuario (beginLoad lo había puesto a false)
|
||||
Screen::get()->setVSync(Options::video.vsync);
|
||||
|
||||
// Si NO estamos en modo "wait for input", transiciona ya al destino.
|
||||
// Si wait_for_input está activo (y la pantalla es visible), nos quedamos
|
||||
// en PRELOAD hasta que el usuario pulse tecla/botón.
|
||||
if (!(Options::loading.show && Options::loading.wait_for_input)) {
|
||||
Section::name = Section::post_preload;
|
||||
}
|
||||
}
|
||||
|
||||
// Cierra todo y libera recursos del sistema y de los singletons
|
||||
@@ -230,6 +265,8 @@ void Director::loadDebugConfig() {
|
||||
out << "initial_stage: 0\n";
|
||||
out << "show_render_info: true\n";
|
||||
out << "resource_loading: preload\n";
|
||||
out << "autoplay: false\n";
|
||||
out << "invincibility: false\n";
|
||||
out.close();
|
||||
}
|
||||
// Usar defaults de DebugConfig
|
||||
@@ -263,6 +300,16 @@ void Director::loadDebugConfig() {
|
||||
debug_config.resource_loading = yaml["resource_loading"].get_value<std::string>();
|
||||
} catch (...) {}
|
||||
}
|
||||
if (yaml.contains("autoplay")) {
|
||||
try {
|
||||
debug_config.autoplay = yaml["autoplay"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
}
|
||||
if (yaml.contains("invincibility")) {
|
||||
try {
|
||||
debug_config.invincibility = yaml["invincibility"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
}
|
||||
} catch (...) {
|
||||
std::cout << "Error parsing debug.yaml, using defaults" << '\n';
|
||||
}
|
||||
@@ -315,6 +362,7 @@ void Director::createSystemFolder(const std::string& folder) {
|
||||
|
||||
// Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez)
|
||||
void Director::resetActiveSection() {
|
||||
preload_.reset();
|
||||
logo_.reset();
|
||||
intro_.reset();
|
||||
title_.reset();
|
||||
@@ -341,6 +389,10 @@ void Director::handleSectionTransition() {
|
||||
|
||||
// Construye la nueva
|
||||
switch (Section::name) {
|
||||
case Section::Name::PRELOAD:
|
||||
preload_ = std::make_unique<Preload>();
|
||||
break;
|
||||
|
||||
case Section::Name::LOGO:
|
||||
logo_ = std::make_unique<Logo>();
|
||||
break;
|
||||
@@ -423,11 +475,32 @@ auto Director::iterate() -> SDL_AppResult {
|
||||
return SDL_APP_SUCCESS;
|
||||
}
|
||||
|
||||
// Fase de boot: carga incremental frame a frame con presupuesto de 50ms.
|
||||
// Durante esta fase la escena activa es Preload (una barra de progreso).
|
||||
if (boot_loading_) {
|
||||
try {
|
||||
if (Resource::get()->loadStep(50 /*ms*/)) {
|
||||
finishBoot();
|
||||
boot_loading_ = false;
|
||||
// Los SDL_EVENT_GAMEPAD_ADDED iniciales ya los ha drenado la rama
|
||||
// durante la carga: marcamos startup completo ahora para que los
|
||||
// ADDED/REMOVED posteriores sí generen notificación.
|
||||
GlobalEvents::markStartupComplete();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Fatal error during resource load: " << e.what() << '\n';
|
||||
Section::name = Section::Name::QUIT;
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Gestiona las transiciones entre secciones (destruye la anterior y construye la nueva)
|
||||
handleSectionTransition();
|
||||
|
||||
// Ejecuta un frame de la sección activa
|
||||
if (logo_) {
|
||||
if (preload_) {
|
||||
preload_->iterate();
|
||||
} else if (logo_) {
|
||||
logo_->iterate();
|
||||
} else if (intro_) {
|
||||
intro_->iterate();
|
||||
@@ -452,7 +525,9 @@ auto Director::handleEvent(SDL_Event& event) -> SDL_AppResult {
|
||||
GlobalEvents::handle(event);
|
||||
|
||||
// Reenvía a la sección activa
|
||||
if (logo_) {
|
||||
if (preload_) {
|
||||
preload_->handleEvent(event);
|
||||
} else if (logo_) {
|
||||
logo_->handleEvent(event);
|
||||
} else if (intro_) {
|
||||
intro_->handleEvent(event);
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Lang {
|
||||
}
|
||||
|
||||
// Declaraciones adelantadas de las secciones
|
||||
class Preload;
|
||||
class Logo;
|
||||
class Intro;
|
||||
class Title;
|
||||
@@ -38,6 +39,8 @@ class Director {
|
||||
int initial_stage = 0;
|
||||
bool show_render_info = true;
|
||||
std::string resource_loading;
|
||||
bool autoplay = false;
|
||||
bool invincibility = false;
|
||||
|
||||
DebugConfig()
|
||||
: initial_section("game"),
|
||||
@@ -52,6 +55,7 @@ class Director {
|
||||
std::string system_folder_; // Carpeta del sistema para almacenar datos
|
||||
|
||||
// --- Sección activa (una y sólo una viva en cada momento) ---
|
||||
std::unique_ptr<Preload> preload_;
|
||||
std::unique_ptr<Logo> logo_;
|
||||
std::unique_ptr<Intro> intro_;
|
||||
std::unique_ptr<Title> title_;
|
||||
@@ -61,8 +65,12 @@ class Director {
|
||||
std::unique_ptr<Credits> credits_;
|
||||
Section::Name last_built_section_name_ = Section::Name::RESET;
|
||||
|
||||
// --- Fase de arranque no bloqueante ---
|
||||
bool boot_loading_ = true; // True mientras Resource::loadStep está cargando incremental
|
||||
|
||||
// --- Inicialización y cierre del sistema ---
|
||||
void init(); // Inicializa la aplicación
|
||||
void init(); // Inicializa la aplicación (pre-boot)
|
||||
void finishBoot(); // Post-boot: inicializa lo que depende de recursos cargados
|
||||
static void close(); // Cierra y libera recursos
|
||||
|
||||
// --- Configuración inicial ---
|
||||
|
||||
@@ -17,12 +17,32 @@
|
||||
#include "game/ui/service_menu.hpp" // Para ServiceMenu
|
||||
|
||||
namespace GlobalEvents {
|
||||
namespace {
|
||||
// Durante el arranque se drenan los SDL_EVENT_GAMEPAD_ADDED de los mandos
|
||||
// ya conectados. Esos eventos sí deben reasignar mandos a jugadores, pero
|
||||
// no deben mostrar notificación: no son hotplug, son detección inicial.
|
||||
bool startup_in_progress = true;
|
||||
} // namespace
|
||||
|
||||
// Comprueba los eventos de Input y muestra notificaciones
|
||||
void handleInputEvents(const SDL_Event& event) {
|
||||
if (event.type != SDL_EVENT_GAMEPAD_ADDED && event.type != SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
return;
|
||||
}
|
||||
|
||||
static auto* input_ = Input::get();
|
||||
auto message = input_->handleEvent(event);
|
||||
|
||||
if (message.empty()) {
|
||||
// Reasignar siempre: tanto en arranque como en hotplug en caliente.
|
||||
Options::gamepad_manager.assignAndLinkGamepads();
|
||||
Options::gamepad_manager.resyncGamepadsWithPlayers();
|
||||
|
||||
// Durante el preload ServiceMenu aún no existe: solo refresca si está vivo.
|
||||
if (ServiceMenu::get() != nullptr) {
|
||||
ServiceMenu::get()->refresh();
|
||||
}
|
||||
|
||||
if (startup_in_progress || message.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,10 +55,15 @@ namespace GlobalEvents {
|
||||
message.replace(pos, std::string(" DISCONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] DISCONNECTED"));
|
||||
}
|
||||
|
||||
Options::gamepad_manager.assignAndLinkGamepads();
|
||||
Options::gamepad_manager.resyncGamepadsWithPlayers();
|
||||
Notifier::get()->show({message});
|
||||
ServiceMenu::get()->refresh();
|
||||
// Notifier también puede no existir todavía si la notificación se
|
||||
// disparase antes de finishBoot(). Protegido por si acaso.
|
||||
if (Notifier::get() != nullptr) {
|
||||
Notifier::get()->show({message});
|
||||
}
|
||||
}
|
||||
|
||||
void markStartupComplete() {
|
||||
startup_in_progress = false;
|
||||
}
|
||||
|
||||
// Comprueba los eventos que se pueden producir en cualquier sección del juego
|
||||
@@ -62,7 +87,10 @@ namespace GlobalEvents {
|
||||
break;
|
||||
}
|
||||
|
||||
ServiceMenu::get()->handleEvent(event);
|
||||
// Durante el preload ServiceMenu aún no existe
|
||||
if (ServiceMenu::get() != nullptr) {
|
||||
ServiceMenu::get()->handleEvent(event);
|
||||
}
|
||||
Mouse::handleEvent(event);
|
||||
handleInputEvents(event);
|
||||
}
|
||||
|
||||
@@ -6,4 +6,10 @@
|
||||
namespace GlobalEvents {
|
||||
// --- Funciones ---
|
||||
void handle(const SDL_Event& event); // Comprueba los eventos que se pueden producir en cualquier sección del juego
|
||||
|
||||
// Marca el fin del arranque: a partir de aquí, los eventos de mando
|
||||
// generan notificaciones en pantalla. Se debe llamar desde Director::iterate
|
||||
// en el primer fotograma, después de que SDL haya drenado los
|
||||
// SDL_EVENT_GAMEPAD_ADDED de los mandos ya conectados al iniciar.
|
||||
void markStartupComplete();
|
||||
} // namespace GlobalEvents
|
||||
@@ -10,6 +10,7 @@ namespace Section {
|
||||
// --- Enumeraciones de secciones del programa ---
|
||||
enum class Name {
|
||||
RESET, // Inicialización
|
||||
PRELOAD, // Carga incremental de recursos
|
||||
LOGO, // Pantalla de logo
|
||||
INTRO, // Introducción
|
||||
TITLE, // Pantalla de título/menú principal
|
||||
@@ -43,6 +44,7 @@ namespace Section {
|
||||
|
||||
// --- Variables globales de estado ---
|
||||
inline Name name = Name::RESET;
|
||||
inline Name post_preload = Name::LOGO; // Sección a la que transiciona PRELOAD al terminar
|
||||
inline Options options = Options::NONE;
|
||||
inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;
|
||||
} // namespace Section
|
||||
5584
source/external/stb_vorbis.c
vendored
Normal file
5584
source/external/stb_vorbis.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5631
source/external/stb_vorbis.h
vendored
5631
source/external/stb_vorbis.h
vendored
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ namespace Options {
|
||||
Settings settings; // Opciones del juego
|
||||
Video video; // Opciones de vídeo
|
||||
Audio audio; // Opciones de audio
|
||||
Loading loading; // Opciones de la pantalla de carga
|
||||
GamepadManager gamepad_manager; // Opciones de mando para cada jugador
|
||||
Keyboard keyboard; // Opciones para el teclado
|
||||
PendingChanges pending_changes; // Opciones que se aplican al cerrar
|
||||
@@ -458,7 +459,7 @@ namespace Options {
|
||||
}
|
||||
if (aud.contains("volume")) {
|
||||
try {
|
||||
audio.volume = std::clamp(aud["volume"].get_value<int>(), 0, 100);
|
||||
audio.volume = std::clamp(aud["volume"].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) {}
|
||||
}
|
||||
if (aud.contains("music")) {
|
||||
@@ -470,7 +471,7 @@ namespace Options {
|
||||
}
|
||||
if (mus.contains("volume")) {
|
||||
try {
|
||||
audio.music.volume = std::clamp(mus["volume"].get_value<int>(), 0, 100);
|
||||
audio.music.volume = std::clamp(mus["volume"].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
@@ -483,12 +484,20 @@ namespace Options {
|
||||
}
|
||||
if (snd.contains("volume")) {
|
||||
try {
|
||||
audio.sound.volume = std::clamp(snd["volume"].get_value<int>(), 0, 100);
|
||||
audio.sound.volume = std::clamp(snd["volume"].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadLoadingFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("loading")) { return; }
|
||||
const auto& ld = yaml["loading"];
|
||||
parseBoolField(ld, "show", loading.show);
|
||||
parseBoolField(ld, "show_resource_name", loading.show_resource_name);
|
||||
parseBoolField(ld, "wait_for_input", loading.wait_for_input);
|
||||
}
|
||||
|
||||
void loadGameFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("game")) { return; }
|
||||
const auto& game = yaml["game"];
|
||||
@@ -601,6 +610,7 @@ namespace Options {
|
||||
loadWindowFromYaml(yaml);
|
||||
loadVideoFromYaml(yaml);
|
||||
loadAudioFromYaml(yaml);
|
||||
loadLoadingFromYaml(yaml);
|
||||
loadGameFromYaml(yaml);
|
||||
loadControllersFromYaml(yaml);
|
||||
loadKeyboardFromYaml(yaml);
|
||||
@@ -682,6 +692,14 @@ namespace Options {
|
||||
file << " volume: " << audio.sound.volume << "\n";
|
||||
file << "\n";
|
||||
|
||||
// LOADING
|
||||
file << "# LOADING SCREEN\n";
|
||||
file << "loading:\n";
|
||||
file << " show: " << boolToString(loading.show) << "\n";
|
||||
file << " show_resource_name: " << boolToString(loading.show_resource_name) << "\n";
|
||||
file << " wait_for_input: " << boolToString(loading.wait_for_input) << "\n";
|
||||
file << "\n";
|
||||
|
||||
// GAME
|
||||
file << "# GAME\n";
|
||||
file << "game:\n";
|
||||
@@ -763,18 +781,30 @@ namespace Options {
|
||||
}
|
||||
|
||||
// Asigna los mandos físicos basándose en la configuración actual.
|
||||
//
|
||||
// Tres pases en orden decreciente de prioridad:
|
||||
// 1) Match por path exacto: resuelve el caso arcade (dos sticks idénticos
|
||||
// enchufados siempre a los mismos puertos USB).
|
||||
// 2) Match por nombre cuando el path no coincidió: recoloca un mando
|
||||
// conocido que se ha reenchufado a otro puerto (ej. DualSense). En ese
|
||||
// caso se refresca el path guardado al nuevo.
|
||||
// 3) Reparto en orden de los mandos físicos sobrantes a los slots que
|
||||
// sigan libres.
|
||||
void GamepadManager::assignAndLinkGamepads() {
|
||||
auto physical_gamepads = Input::get()->getGamepads();
|
||||
|
||||
std::array<std::string, MAX_PLAYERS> desired_paths;
|
||||
std::array<std::string, MAX_PLAYERS> desired_names;
|
||||
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
||||
desired_paths[i] = gamepads_[i].path;
|
||||
desired_names[i] = gamepads_[i].name;
|
||||
gamepads_[i].instance = nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Input::Gamepad>> assigned_instances;
|
||||
|
||||
assignGamepadsByPath(desired_paths, physical_gamepads, assigned_instances);
|
||||
assignGamepadsByName(desired_names, physical_gamepads, assigned_instances);
|
||||
assignRemainingGamepads(physical_gamepads, assigned_instances);
|
||||
clearUnassignedGamepadSlots();
|
||||
}
|
||||
@@ -802,7 +832,37 @@ namespace Options {
|
||||
}
|
||||
}
|
||||
|
||||
// --- SEGUNDA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
|
||||
// --- SEGUNDA PASADA: Match por nombre para slots cuyo path guardado no casó ---
|
||||
// Si un slot tiene nombre guardado pero el path no ha coincidido (mando reenchufado
|
||||
// a otro puerto, o path del sistema cambiado), lo enlazamos por nombre y
|
||||
// refrescamos el path guardado al del dispositivo físico actual.
|
||||
void GamepadManager::assignGamepadsByName(
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_names,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
||||
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
|
||||
if (gamepads_[i].instance != nullptr) {
|
||||
continue;
|
||||
}
|
||||
const std::string& desired_name = desired_names[i];
|
||||
if (desired_name.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& physical_gamepad : physical_gamepads) {
|
||||
if (physical_gamepad->name == desired_name && !isGamepadAssigned(physical_gamepad, assigned_instances)) {
|
||||
gamepads_[i].instance = physical_gamepad;
|
||||
gamepads_[i].name = physical_gamepad->name;
|
||||
gamepads_[i].path = physical_gamepad->path;
|
||||
|
||||
assigned_instances.push_back(physical_gamepad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- TERCERA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
|
||||
void GamepadManager::assignRemainingGamepads(
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
|
||||
@@ -824,7 +884,7 @@ namespace Options {
|
||||
}
|
||||
}
|
||||
|
||||
// --- TERCERA PASADA: Limpia la información "fantasma" de los slots no asignados ---
|
||||
// --- LIMPIEZA FINAL: Limpia la información "fantasma" de los slots no asignados ---
|
||||
void GamepadManager::clearUnassignedGamepadSlots() {
|
||||
for (auto& gamepad_config : gamepads_) {
|
||||
if (gamepad_config.instance == nullptr) {
|
||||
|
||||
@@ -94,19 +94,25 @@ namespace Options {
|
||||
|
||||
struct Music {
|
||||
bool enabled = Defaults::Music::ENABLED; // Indica si la música suena o no
|
||||
int volume = Defaults::Music::VOLUME; // Volumen de la música
|
||||
float volume = Defaults::Music::VOLUME; // Volumen de la música (0.0..1.0)
|
||||
};
|
||||
|
||||
struct Sound {
|
||||
bool enabled = Defaults::Sound::ENABLED; // Indica si los sonidos suenan o no
|
||||
int volume = Defaults::Sound::VOLUME; // Volumen de los sonidos
|
||||
float volume = Defaults::Sound::VOLUME; // Volumen de los sonidos (0.0..1.0)
|
||||
};
|
||||
|
||||
struct Audio {
|
||||
Music music; // Opciones para la música
|
||||
Sound sound; // Opciones para los efectos de sonido
|
||||
bool enabled = Defaults::Audio::ENABLED; // Indica si el audio está activo o no
|
||||
int volume = Defaults::Audio::VOLUME; // Volumen general del audio
|
||||
float volume = Defaults::Audio::VOLUME; // Volumen general del audio (0.0..1.0)
|
||||
};
|
||||
|
||||
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 en curso sobre la barra de progreso
|
||||
bool wait_for_input = Defaults::Loading::WAIT_FOR_INPUT; // Al terminar la carga, espera a que el usuario pulse una tecla/botón (solo si show=true)
|
||||
};
|
||||
|
||||
struct Settings {
|
||||
@@ -293,6 +299,10 @@ namespace Options {
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_paths,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
|
||||
void assignGamepadsByName(
|
||||
const std::array<std::string, MAX_PLAYERS>& desired_names,
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
|
||||
void assignRemainingGamepads(
|
||||
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
|
||||
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
|
||||
@@ -329,6 +339,7 @@ namespace Options {
|
||||
extern Settings settings; // Opciones del juego
|
||||
extern Video video; // Opciones de vídeo
|
||||
extern Audio audio; // Opciones de audio
|
||||
extern Loading loading; // Opciones de la pantalla de carga
|
||||
extern GamepadManager gamepad_manager; // Manager de mandos para cada jugador
|
||||
extern Keyboard keyboard; // Opciones para el teclado
|
||||
extern PendingChanges pending_changes; // Opciones que se aplican al cerrar
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
#ifdef _DEBUG
|
||||
#include <iostream> // Para basic_ostream, basic_ostream::operator<<, operator<<, cout
|
||||
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "core/system/director.hpp" // Para Director::debug_config
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
@@ -805,6 +806,12 @@ void Game::handlePlayerCollision(std::shared_ptr<Player>& player, std::shared_pt
|
||||
return; // Si no está jugando o tiene inmunidad, no hace nada
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
if (Director::debug_config.invincibility) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Si tiene cafes
|
||||
if (player->hasExtraHit()) {
|
||||
// Lo pierde
|
||||
@@ -1255,7 +1262,17 @@ void Game::checkInput() {
|
||||
// Comprueba las entradas si no está el menú de servicio activo
|
||||
if (!ServiceMenu::get()->isEnabled()) {
|
||||
checkPauseInput();
|
||||
demo_.enabled ? demoHandlePassInput() : handlePlayersInput();
|
||||
if (demo_.enabled) {
|
||||
demoHandlePassInput();
|
||||
} else {
|
||||
#ifdef _DEBUG
|
||||
if (!autoplay_) {
|
||||
handlePlayersInput();
|
||||
}
|
||||
#else
|
||||
handlePlayersInput();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Mueve los jugadores en el modo demo
|
||||
@@ -1263,6 +1280,12 @@ void Game::checkInput() {
|
||||
demoHandleInput();
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
if (autoplay_) {
|
||||
autoplayHandleInput();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Verifica los inputs globales.
|
||||
GlobalInputs::check();
|
||||
}
|
||||
@@ -1568,6 +1591,29 @@ void Game::initDemo(Player::Id player_id) {
|
||||
demo_.recording = false;
|
||||
#endif
|
||||
demo_.index = 0;
|
||||
|
||||
#ifdef _DEBUG
|
||||
autoplay_ = Director::debug_config.autoplay && !demo_.enabled;
|
||||
if (autoplay_) {
|
||||
auto const NUM_DEMOS = Asset::get()->getListByType(Asset::Type::DEMODATA).size();
|
||||
for (size_t num_demo = 0; num_demo < NUM_DEMOS; ++num_demo) {
|
||||
autoplay_data_.emplace_back(Resource::get()->getDemoData(num_demo));
|
||||
}
|
||||
|
||||
std::vector<size_t> demo_indices(NUM_DEMOS);
|
||||
std::iota(demo_indices.begin(), demo_indices.end(), 0);
|
||||
std::random_device rd;
|
||||
std::default_random_engine rng(rd());
|
||||
std::shuffle(demo_indices.begin(), demo_indices.end(), rng);
|
||||
|
||||
for (size_t i = 0; i < players_.size(); ++i) {
|
||||
size_t demo_index = demo_indices[i % NUM_DEMOS];
|
||||
players_.at(i)->setDemoFile(demo_index);
|
||||
}
|
||||
autoplay_elapsed_s_ = 0.0F;
|
||||
autoplay_index_ = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Inicializa el marcador
|
||||
@@ -1812,6 +1858,14 @@ void Game::updateGameStatePlaying(float delta_time) {
|
||||
if (auto_pop_balloons_) {
|
||||
stage_manager_->addPower(2);
|
||||
}
|
||||
if (autoplay_) {
|
||||
autoplay_elapsed_s_ += delta_time;
|
||||
autoplay_index_ = static_cast<int>(autoplay_elapsed_s_ * 60.0F);
|
||||
if (autoplay_index_ >= TOTAL_DEMO_DATA) {
|
||||
autoplay_elapsed_s_ = 0.0F;
|
||||
autoplay_index_ = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
updatePlayers(delta_time);
|
||||
checkPlayersStatusPlaying();
|
||||
@@ -2073,6 +2127,31 @@ void Game::bringPlayerToFront(Players& elements, const std::shared_ptr<Player>&
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Alimenta input de demo a los jugadores durante el autoplay
|
||||
void Game::autoplayHandleInput() {
|
||||
for (const auto& player : players_) {
|
||||
if (player->isPlaying()) {
|
||||
const auto& demo_data = autoplay_data_.at(player->getDemoFile()).at(autoplay_index_);
|
||||
|
||||
if (demo_data.left == 1) {
|
||||
player->setInput(Input::Action::LEFT);
|
||||
} else if (demo_data.right == 1) {
|
||||
player->setInput(Input::Action::RIGHT);
|
||||
} else if (demo_data.no_input == 1) {
|
||||
player->setInput(Input::Action::NONE);
|
||||
}
|
||||
|
||||
if (demo_data.fire == 1) {
|
||||
handleFireInput(player, Bullet::Type::UP);
|
||||
} else if (demo_data.fire_left == 1) {
|
||||
handleFireInput(player, Bullet::Type::LEFT);
|
||||
} else if (demo_data.fire_right == 1) {
|
||||
handleFireInput(player, Bullet::Type::RIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba los eventos en el modo DEBUG
|
||||
void Game::handleDebugEvents(const SDL_Event& event) {
|
||||
static int formation_id_ = 0;
|
||||
|
||||
@@ -198,6 +198,10 @@ class Game {
|
||||
|
||||
#ifdef _DEBUG
|
||||
bool auto_pop_balloons_ = false; // Si es true, incrementa automaticamente los globos explotados
|
||||
bool autoplay_ = false; // Si es true, los jugadores se mueven solos con datos de demo
|
||||
float autoplay_elapsed_s_ = 0.0F;
|
||||
int autoplay_index_ = 0;
|
||||
std::vector<DemoData> autoplay_data_;
|
||||
#endif
|
||||
|
||||
// --- Ciclo principal del juego ---
|
||||
@@ -349,5 +353,6 @@ class Game {
|
||||
// --- Depuración (solo en modo DEBUG) ---
|
||||
#ifdef _DEBUG
|
||||
void handleDebugEvents(const SDL_Event& event); // Comprueba los eventos en el modo DEBUG
|
||||
void autoplayHandleInput(); // Alimenta input de demo a los jugadores en autoplay
|
||||
#endif
|
||||
};
|
||||
26
source/game/scenes/preload.cpp
Normal file
26
source/game/scenes/preload.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#include "game/scenes/preload.hpp"
|
||||
|
||||
#include "core/resources/resource.hpp" // Para Resource
|
||||
#include "core/system/section.hpp" // Para Section
|
||||
#include "game/options.hpp" // Para Options::loading
|
||||
|
||||
void Preload::iterate() {
|
||||
// El repintado es independiente del avance de la carga: Director::iterate
|
||||
// ya ha bombeado Resource::loadStep() antes de llamar aquí.
|
||||
Resource::get()->renderProgress();
|
||||
}
|
||||
|
||||
void Preload::handleEvent(const SDL_Event& event) {
|
||||
// Solo aceptamos input si la carga ha terminado y estamos en modo
|
||||
// wait_for_input (que además exige que la pantalla de carga sea visible).
|
||||
if (!Resource::get()->isLoadDone() || !Options::loading.wait_for_input || !Options::loading.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pulsación limpia: ignoramos auto-repeat y eventos de hotplug de mando.
|
||||
const bool IS_KEY = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
|
||||
const bool IS_BUTTON = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
if (IS_KEY || IS_BUTTON) {
|
||||
Section::name = Section::post_preload;
|
||||
}
|
||||
}
|
||||
19
source/game/scenes/preload.hpp
Normal file
19
source/game/scenes/preload.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_Event
|
||||
|
||||
// --- Clase Preload: escena mínima durante la carga incremental de recursos ---
|
||||
//
|
||||
// No avanza la carga — de eso se encarga Director::iterate() llamando a
|
||||
// Resource::loadStep(budget_ms) antes de despachar la escena. Aquí solo se
|
||||
// repinta la barra de progreso y, si Options::loading.wait_for_input está
|
||||
// activo, se detecta la pulsación que transiciona a la siguiente sección.
|
||||
class Preload {
|
||||
public:
|
||||
Preload() = default;
|
||||
~Preload() = default;
|
||||
|
||||
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
|
||||
void iterate(); // Repinta la barra de progreso
|
||||
void handleEvent(const SDL_Event& event); // Detecta pulsación en modo wait_for_input
|
||||
};
|
||||
@@ -106,6 +106,40 @@ class IntOption : public MenuOption {
|
||||
int min_value_, max_value_, step_value_;
|
||||
};
|
||||
|
||||
// VolumeOption: emmagatzema un float 0.0..1.0 però es mostra/edita com a int 0..100.
|
||||
// Pensat per als sliders de volum que l'usuari veu com percentatge però que
|
||||
// internament viuen en float (API unificada del motor d'àudio).
|
||||
class VolumeOption : public MenuOption {
|
||||
public:
|
||||
VolumeOption(const std::string& cap, ServiceMenu::SettingsGroup grp, float* var, int step_percent = 5)
|
||||
: MenuOption(cap, grp),
|
||||
linked_variable_(var),
|
||||
step_value_(step_percent) {}
|
||||
|
||||
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
|
||||
[[nodiscard]] auto getValueAsString() const -> std::string override {
|
||||
int pct = static_cast<int>(*linked_variable_ * 100.0F + 0.5F);
|
||||
return std::to_string(pct);
|
||||
}
|
||||
void adjustValue(bool adjust_up) override {
|
||||
int current = static_cast<int>(*linked_variable_ * 100.0F + 0.5F);
|
||||
int new_value = std::clamp(current + (adjust_up ? step_value_ : -step_value_), 0, 100);
|
||||
*linked_variable_ = static_cast<float>(new_value) / 100.0F;
|
||||
}
|
||||
auto getMaxValueWidth(Text* text_renderer) const -> int override {
|
||||
int max_width = 0;
|
||||
for (int value = 0; value <= 100; value += step_value_) {
|
||||
int width = text_renderer->length(std::to_string(value), -2);
|
||||
max_width = std::max(max_width, width);
|
||||
}
|
||||
return max_width;
|
||||
}
|
||||
|
||||
private:
|
||||
float* linked_variable_;
|
||||
int step_value_;
|
||||
};
|
||||
|
||||
class ListOption : public MenuOption {
|
||||
public:
|
||||
ListOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::vector<std::string> values, std::function<std::string()> current_value_getter, std::function<void(const std::string&)> new_value_setter)
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "game/ui/menu_option.hpp"
|
||||
#include "utils/color.hpp"
|
||||
#include "utils/defines.hpp" // Para Texts::VERSION
|
||||
#include "utils/param.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
#include "version.h" // Para Version::GIT_HASH
|
||||
|
||||
// --- Implementación de las estructuras de animación ---
|
||||
|
||||
@@ -74,6 +76,17 @@ void MenuRenderer::render(const ServiceMenu* menu_state) {
|
||||
|
||||
// Solo renderizar contenido si la animación lo permite
|
||||
if (shouldShowContent()) {
|
||||
// Recorta todas las operaciones de texto y líneas al rect actual del panel
|
||||
// para que durante el resize animation nada se pinte fuera del borde.
|
||||
auto* renderer = Screen::get()->getRenderer();
|
||||
const SDL_Rect CLIP_RECT = {
|
||||
.x = static_cast<int>(rect_.x),
|
||||
.y = static_cast<int>(rect_.y),
|
||||
.w = static_cast<int>(rect_.w),
|
||||
.h = static_cast<int>(rect_.h),
|
||||
};
|
||||
SDL_SetRenderClipRect(renderer, &CLIP_RECT);
|
||||
|
||||
// Dibuja el título
|
||||
float y = rect_.y + title_padding_;
|
||||
title_text_->writeDX(Text::COLOR | Text::CENTER, rect_.x + (rect_.w / 2.0F), y, menu_state->getTitle(), -4, param.service_menu.title_color);
|
||||
@@ -83,27 +96,52 @@ void MenuRenderer::render(const ServiceMenu* menu_state) {
|
||||
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), BORDER_COLOR.r, BORDER_COLOR.g, BORDER_COLOR.b, 255);
|
||||
SDL_RenderLine(Screen::get()->getRenderer(), rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y);
|
||||
|
||||
// Dibuja el subtítulo del grupo (versión + hash en SYSTEM)
|
||||
if (groupHasSubtitle(menu_state->getCurrentGroup())) {
|
||||
const std::string SUBTITLE = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")";
|
||||
element_text_->writeDX(
|
||||
Text::CENTER | Text::COLOR,
|
||||
rect_.x + (rect_.w / 2.0F),
|
||||
y + options_padding_,
|
||||
SUBTITLE,
|
||||
-2,
|
||||
param.service_menu.text_color);
|
||||
}
|
||||
|
||||
// Dibuja las opciones
|
||||
y = options_y_;
|
||||
const auto& option_pairs = menu_state->getOptionPairs();
|
||||
const float ROW_HEIGHT = static_cast<float>(options_height_ + options_padding_);
|
||||
|
||||
for (size_t i = 0; i < option_pairs.size(); ++i) {
|
||||
const bool IS_SELECTED = (i == menu_state->getSelectedIndex());
|
||||
const Color& current_color = IS_SELECTED ? param.service_menu.selected_color : param.service_menu.text_color;
|
||||
|
||||
// Offset Y del valor durante la animación de swap: va desde la fila
|
||||
// origen (la otra implicada) hasta su fila natural con easeOut.
|
||||
float value_y_offset = 0.0F;
|
||||
if (swap_animation_.active && (i == swap_animation_.idx_a || i == swap_animation_.idx_b)) {
|
||||
const size_t OTHER = (i == swap_animation_.idx_a) ? swap_animation_.idx_b : swap_animation_.idx_a;
|
||||
const float PROGRESS = easeOut(swap_animation_.elapsed / swap_animation_.duration);
|
||||
const float DELTA_ROWS = static_cast<float>(OTHER) - static_cast<float>(i);
|
||||
value_y_offset = DELTA_ROWS * ROW_HEIGHT * (1.0F - PROGRESS);
|
||||
}
|
||||
|
||||
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
|
||||
const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) - element_text_->length(option_pairs.at(i).first, -2) - ServiceMenu::MIN_GAP_OPTION_VALUE;
|
||||
std::string truncated_value = getTruncatedValue(option_pairs.at(i).second, AVAILABLE_WIDTH);
|
||||
element_text_->writeColored(rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, option_pairs.at(i).first, current_color, -2);
|
||||
const int X = rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING - element_text_->length(truncated_value, -2);
|
||||
element_text_->writeColored(X, y, truncated_value, current_color, -2);
|
||||
element_text_->writeColored(X, static_cast<int>(y + value_y_offset), truncated_value, current_color, -2);
|
||||
} else {
|
||||
const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
|
||||
std::string truncated_caption = getTruncatedValue(option_pairs.at(i).first, AVAILABLE_WIDTH);
|
||||
element_text_->writeDX(Text::CENTER | Text::COLOR, rect_.x + (rect_.w / 2.0F), y, truncated_caption, -2, current_color);
|
||||
}
|
||||
y += options_height_ + options_padding_;
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
|
||||
SDL_SetRenderClipRect(renderer, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +239,8 @@ auto MenuRenderer::calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect
|
||||
width_ = std::min(static_cast<size_t>(getMenuWidthForGroup(menu_state->getCurrentGroup())), max_menu_width_);
|
||||
const auto& display_options = menu_state->getDisplayOptions();
|
||||
lower_height_ = ((!display_options.empty() ? display_options.size() - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2);
|
||||
height_ = std::min(upper_height_ + lower_height_, max_menu_height_);
|
||||
subtitle_offset_ = getGroupSubtitleHeight(menu_state);
|
||||
height_ = std::min(upper_height_ + subtitle_offset_ + lower_height_, max_menu_height_);
|
||||
|
||||
SDL_FRect new_rect = {.x = 0, .y = 0, .w = static_cast<float>(width_), .h = static_cast<float>(height_)};
|
||||
|
||||
@@ -209,6 +248,18 @@ auto MenuRenderer::calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect
|
||||
return new_rect;
|
||||
}
|
||||
|
||||
// Grupos que muestran una línea de subtítulo bajo el título (antes de las opciones).
|
||||
auto MenuRenderer::groupHasSubtitle(ServiceMenu::SettingsGroup group) -> bool {
|
||||
return group == ServiceMenu::SettingsGroup::SYSTEM;
|
||||
}
|
||||
|
||||
// Altura extra reservada para el subtítulo del grupo actual.
|
||||
// Reservamos el alto de una línea de element_text_ más un pequeño padding.
|
||||
auto MenuRenderer::getGroupSubtitleHeight(const ServiceMenu* menu_state) const -> size_t {
|
||||
if (!groupHasSubtitle(menu_state->getCurrentGroup())) { return 0; }
|
||||
return element_text_->getCharacterSize() + options_padding_;
|
||||
}
|
||||
|
||||
void MenuRenderer::resize(const ServiceMenu* menu_state) {
|
||||
SDL_FRect new_rect = calculateNewRect(menu_state);
|
||||
|
||||
@@ -219,7 +270,7 @@ void MenuRenderer::resize(const ServiceMenu* menu_state) {
|
||||
// Si no hay cambio de tamaño, solo actualizamos la posición
|
||||
updatePosition();
|
||||
}
|
||||
options_y_ = rect_.y + upper_height_ + lower_padding_;
|
||||
options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
|
||||
}
|
||||
|
||||
void MenuRenderer::setSize(const ServiceMenu* menu_state) {
|
||||
@@ -231,16 +282,35 @@ void MenuRenderer::setSize(const ServiceMenu* menu_state) {
|
||||
resize_animation_.stop();
|
||||
|
||||
updatePosition();
|
||||
options_y_ = rect_.y + upper_height_ + lower_padding_;
|
||||
options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
|
||||
border_rect_ = {.x = rect_.x - 1, .y = rect_.y + 1, .w = rect_.w + 2, .h = rect_.h - 2};
|
||||
}
|
||||
|
||||
// --- Métodos de animación y posición ---
|
||||
|
||||
void MenuRenderer::startSwapAnimation(size_t idx_a, size_t idx_b) {
|
||||
if (idx_a == idx_b) { return; }
|
||||
swap_animation_.active = true;
|
||||
swap_animation_.idx_a = idx_a;
|
||||
swap_animation_.idx_b = idx_b;
|
||||
swap_animation_.elapsed = 0.0F;
|
||||
}
|
||||
|
||||
void MenuRenderer::updateSwapAnimation(float delta_time) {
|
||||
swap_animation_.elapsed += delta_time;
|
||||
if (swap_animation_.elapsed >= swap_animation_.duration) {
|
||||
swap_animation_.active = false;
|
||||
swap_animation_.elapsed = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void MenuRenderer::updateAnimations(float delta_time) {
|
||||
if (show_hide_animation_.active) {
|
||||
updateShowHideAnimation(delta_time);
|
||||
}
|
||||
if (swap_animation_.active) {
|
||||
updateSwapAnimation(delta_time);
|
||||
}
|
||||
if (resize_animation_.active) {
|
||||
updateResizeAnimation(delta_time);
|
||||
}
|
||||
@@ -272,7 +342,7 @@ void MenuRenderer::updateShowHideAnimation(float delta_time) {
|
||||
}
|
||||
updatePosition();
|
||||
}
|
||||
options_y_ = rect_.y + upper_height_ + lower_padding_;
|
||||
options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
|
||||
}
|
||||
|
||||
void MenuRenderer::updateResizeAnimation(float delta_time) {
|
||||
@@ -290,7 +360,7 @@ void MenuRenderer::updateResizeAnimation(float delta_time) {
|
||||
rect_.h = resize_animation_.start_height + ((resize_animation_.target_height - resize_animation_.start_height) * progress);
|
||||
updatePosition();
|
||||
}
|
||||
options_y_ = rect_.y + upper_height_ + lower_padding_;
|
||||
options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
|
||||
}
|
||||
|
||||
void MenuRenderer::updatePosition() {
|
||||
@@ -341,6 +411,12 @@ void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<Menu
|
||||
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
|
||||
total_width += ServiceMenu::MIN_GAP_OPTION_VALUE + max_value_width;
|
||||
}
|
||||
// Si el grupo tiene subtítulo, ensanchamos lo necesario para que quepa
|
||||
if (groupHasSubtitle(sg)) {
|
||||
const std::string SUBTITLE = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")";
|
||||
const size_t SUBTITLE_WIDTH = static_cast<size_t>(element_text_->length(SUBTITLE, -2)) + (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
|
||||
total_width = std::max(total_width, SUBTITLE_WIDTH);
|
||||
}
|
||||
group_menu_widths_[group] = std::min(std::max(static_cast<int>(ServiceMenu::MIN_WIDTH), static_cast<int>(total_width)), static_cast<int>(max_menu_width_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ class MenuRenderer {
|
||||
void onLayoutChanged(const ServiceMenu* menu_state);
|
||||
void setLayout(const ServiceMenu* menu_state);
|
||||
|
||||
// Animación de intercambio de valores entre dos filas del menú actual
|
||||
void startSwapAnimation(size_t idx_a, size_t idx_b);
|
||||
|
||||
// Getters
|
||||
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
|
||||
|
||||
@@ -63,6 +66,7 @@ class MenuRenderer {
|
||||
size_t upper_height_ = 0;
|
||||
size_t lower_height_ = 0;
|
||||
size_t lower_padding_ = 0;
|
||||
size_t subtitle_offset_ = 0; // Altura reservada para subtítulo del grupo actual
|
||||
Uint32 color_counter_ = 0;
|
||||
bool visible_ = false;
|
||||
|
||||
@@ -102,6 +106,16 @@ class MenuRenderer {
|
||||
void stop();
|
||||
} show_hide_animation_;
|
||||
|
||||
// Animación de intercambio: interpola la Y del VALOR de dos filas
|
||||
// durante su intercambio. El caption se queda estático.
|
||||
struct SwapAnimation {
|
||||
bool active = false;
|
||||
size_t idx_a = 0;
|
||||
size_t idx_b = 0;
|
||||
float elapsed = 0.0F;
|
||||
float duration = 0.4F;
|
||||
} swap_animation_;
|
||||
|
||||
// --- Anchos precalculados ---
|
||||
std::array<int, ServiceMenu::SETTINGS_GROUP_SIZE> group_menu_widths_ = {};
|
||||
|
||||
@@ -112,9 +126,14 @@ class MenuRenderer {
|
||||
void resize(const ServiceMenu* menu_state);
|
||||
void setSize(const ServiceMenu* menu_state);
|
||||
|
||||
// Altura extra ocupada por el subtítulo del grupo actual (versión + hash en SYSTEM)
|
||||
[[nodiscard]] auto getGroupSubtitleHeight(const ServiceMenu* menu_state) const -> size_t;
|
||||
[[nodiscard]] static auto groupHasSubtitle(ServiceMenu::SettingsGroup group) -> bool;
|
||||
|
||||
void updateAnimations(float delta_time);
|
||||
void updateResizeAnimation(float delta_time);
|
||||
void updateShowHideAnimation(float delta_time);
|
||||
void updateSwapAnimation(float delta_time);
|
||||
void updatePosition();
|
||||
|
||||
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls)
|
||||
|
||||
@@ -347,6 +347,22 @@ void ServiceMenu::initializeOptions() {
|
||||
Options::gamepad_manager.swapPlayers();
|
||||
adjustListValues(); // Sincroniza el valor de las opciones de lista (como MANDO1) con los datos reales
|
||||
updateOptionPairs(); // Actualiza los pares de texto <opción, valor> que se van a dibujar
|
||||
|
||||
// Feedback visual: anima el intercambio de los valores entre
|
||||
// las filas de MANDO 1 y MANDO 2, imprescindible cuando los dos
|
||||
// mandos tienen el mismo nombre (el texto no cambia al swap).
|
||||
const std::string CAPTION1 = Lang::getText("[SERVICE_MENU] CONTROLLER1");
|
||||
const std::string CAPTION2 = Lang::getText("[SERVICE_MENU] CONTROLLER2");
|
||||
size_t idx1 = display_options_.size();
|
||||
size_t idx2 = display_options_.size();
|
||||
for (size_t i = 0; i < display_options_.size(); ++i) {
|
||||
const auto& caption = display_options_[i]->getCaption();
|
||||
if (caption == CAPTION1) { idx1 = i; }
|
||||
if (caption == CAPTION2) { idx2 = i; }
|
||||
}
|
||||
if (idx1 < display_options_.size() && idx2 < display_options_.size()) {
|
||||
renderer_->startSwapAnimation(idx1, idx2);
|
||||
}
|
||||
}));
|
||||
|
||||
// VIDEO
|
||||
@@ -450,34 +466,46 @@ void ServiceMenu::initializeOptions() {
|
||||
SettingsGroup::VIDEO,
|
||||
&Options::video.integer_scale));
|
||||
|
||||
// FILTER: Nearest / Linear (solo visible en el fallback SDL, sin GPU acelerada)
|
||||
{
|
||||
std::vector<std::string> filter_values = {"Nearest", "Linear"};
|
||||
auto filter_getter = []() -> std::string {
|
||||
return (Options::video.scale_mode == SDL_SCALEMODE_LINEAR) ? "Linear" : "Nearest";
|
||||
};
|
||||
auto filter_setter = [](const std::string& val) {
|
||||
Options::video.scale_mode = (val == "Linear") ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST;
|
||||
if (Screen::get() != nullptr) { Screen::get()->applyFilter(); }
|
||||
};
|
||||
options_.push_back(std::make_unique<ListOption>(
|
||||
Lang::getText("[SERVICE_MENU] FILTER"),
|
||||
SettingsGroup::VIDEO,
|
||||
filter_values,
|
||||
filter_getter,
|
||||
filter_setter));
|
||||
}
|
||||
|
||||
// AUDIO
|
||||
options_.push_back(std::make_unique<BoolOption>(
|
||||
Lang::getText("[SERVICE_MENU] AUDIO"),
|
||||
SettingsGroup::AUDIO,
|
||||
&Options::audio.enabled));
|
||||
|
||||
options_.push_back(std::make_unique<IntOption>(
|
||||
options_.push_back(std::make_unique<VolumeOption>(
|
||||
Lang::getText("[SERVICE_MENU] MAIN_VOLUME"),
|
||||
SettingsGroup::AUDIO,
|
||||
&Options::audio.volume,
|
||||
0,
|
||||
100,
|
||||
5));
|
||||
|
||||
options_.push_back(std::make_unique<IntOption>(
|
||||
options_.push_back(std::make_unique<VolumeOption>(
|
||||
Lang::getText("[SERVICE_MENU] MUSIC_VOLUME"),
|
||||
SettingsGroup::AUDIO,
|
||||
&Options::audio.music.volume,
|
||||
0,
|
||||
100,
|
||||
5));
|
||||
|
||||
options_.push_back(std::make_unique<IntOption>(
|
||||
options_.push_back(std::make_unique<VolumeOption>(
|
||||
Lang::getText("[SERVICE_MENU] SFX_VOLUME"),
|
||||
SettingsGroup::AUDIO,
|
||||
&Options::audio.sound.volume,
|
||||
0,
|
||||
100,
|
||||
5));
|
||||
|
||||
// SETTINGS
|
||||
@@ -628,20 +656,53 @@ void ServiceMenu::setHiddenOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
// Las opciones de shader solo tienen sentido cuando SDL3GPU está activo.
|
||||
// Las opciones de filtro SDL solo tienen sentido en el fallback sin GPU.
|
||||
// Ambos checks son runtime para cubrir tanto builds sin shaders como fallos de inicialización.
|
||||
const bool HW_ACCEL = Screen::isHardwareAccelerated();
|
||||
|
||||
{
|
||||
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] FILTER"));
|
||||
if (option != nullptr) {
|
||||
option->setHidden(HW_ACCEL);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHADER"));
|
||||
if (option != nullptr) {
|
||||
option->setHidden(!HW_ACCEL);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHADER_PRESET"));
|
||||
if (option != nullptr) {
|
||||
option->setHidden(!Options::video.shader.enabled);
|
||||
option->setHidden(!HW_ACCEL || !Options::video.shader.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SUPERSAMPLING"));
|
||||
if (option != nullptr) {
|
||||
option->setHidden(!Options::video.shader.enabled || Options::video.shader.current_shader != Rendering::ShaderType::POSTFX);
|
||||
option->setHidden(!HW_ACCEL || !Options::video.shader.enabled || Options::video.shader.current_shader != Rendering::ShaderType::POSTFX);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// En la versión web no tiene sentido exponer: permitir apagar el sistema
|
||||
// (no aplica al navegador) ni salir del juego (lo gestiona el navegador).
|
||||
// SHADER ya queda oculto por el check de HW_ACCEL de arriba.
|
||||
for (const auto& caption : {
|
||||
Lang::getText("[SERVICE_MENU] ENABLE_SHUTDOWN"),
|
||||
Lang::getText("[SERVICE_MENU] QUIT")}) {
|
||||
auto* option = getOptionByCaption(caption);
|
||||
if (option != nullptr) {
|
||||
option->setHidden(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
updateMenu(); // El menú debe refrescarse si algo se oculta
|
||||
}
|
||||
|
||||
|
||||
5
source/utils/defines.hpp
Normal file
5
source/utils/defines.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace Texts {
|
||||
constexpr const char* VERSION = "2026.04.14"; // Versión del juego (también usada por el Makefile)
|
||||
} // namespace Texts
|
||||
@@ -1,85 +0,0 @@
|
||||
# Makefile para herramientas de Coffee Crisis Arcade Edition
|
||||
# =========================================================
|
||||
|
||||
# Variables
|
||||
CXX := g++
|
||||
CXXFLAGS := -std=c++20 -Wall -Os -I../../source
|
||||
SOURCES := pack_resources.cpp ../../source/core/resources/resource_pack.cpp
|
||||
TARGET := pack_resources
|
||||
CLEAN_FILES := pack_resources *.pack *.o
|
||||
|
||||
# Detectar sistema operativo
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DETECTED_OS := Windows
|
||||
TARGET := $(TARGET).exe
|
||||
CLEAN_CMD := del /Q
|
||||
FixPath = $(subst /,\\,$1)
|
||||
else
|
||||
DETECTED_OS := $(shell uname -s)
|
||||
CLEAN_CMD := rm -f
|
||||
FixPath = $1
|
||||
endif
|
||||
|
||||
# Reglas principales
|
||||
.PHONY: all pack_tool pack clean help test_pack
|
||||
|
||||
# Compilar herramienta de empaquetado
|
||||
all: pack_tool
|
||||
|
||||
pack_tool:
|
||||
@echo "Compilando herramienta de empaquetado para $(DETECTED_OS)..."
|
||||
$(CXX) $(CXXFLAGS) $(SOURCES) -o $(TARGET)
|
||||
@echo "✓ Herramienta compilada: $(TARGET)"
|
||||
|
||||
# Limpiar archivos generados
|
||||
clean:
|
||||
@echo "Limpiando archivos generados..."
|
||||
$(CLEAN_CMD) $(call FixPath,$(CLEAN_FILES))
|
||||
@echo "✓ Archivos limpiados"
|
||||
|
||||
# Crear pack de recursos final (invocado desde Makefile raíz)
|
||||
pack: pack_tool
|
||||
ifeq ($(OS),Windows_NT)
|
||||
.\$(TARGET) ..\..\data ..\..\resources.pack
|
||||
else
|
||||
./$(TARGET) ../../data ../../resources.pack
|
||||
endif
|
||||
|
||||
# Crear pack de recursos de prueba
|
||||
test_pack: pack_tool
|
||||
@echo "Creando pack de recursos de prueba..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
.\$(TARGET) ..\..\data test_resources.pack
|
||||
else
|
||||
./$(TARGET) ../../data test_resources.pack
|
||||
endif
|
||||
@echo "✓ Pack de prueba creado: test_resources.pack"
|
||||
|
||||
# Crear pack de recursos final
|
||||
create_pack: pack_tool
|
||||
@echo "Creando pack de recursos final..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
.\$(TARGET) ..\..\data ..\..\resources.pack
|
||||
else
|
||||
./$(TARGET) ../../data ../../resources.pack
|
||||
endif
|
||||
@echo "✓ Pack final creado: ../../resources.pack"
|
||||
|
||||
# Mostrar ayuda
|
||||
help:
|
||||
@echo "Makefile para herramientas de Coffee Crisis Arcade Edition"
|
||||
@echo "========================================================="
|
||||
@echo ""
|
||||
@echo "Comandos disponibles:"
|
||||
@echo " all - Compilar herramienta de empaquetado (por defecto)"
|
||||
@echo " pack_tool - Compilar herramienta de empaquetado"
|
||||
@echo " test_pack - Crear pack de recursos de prueba"
|
||||
@echo " create_pack - Crear pack de recursos final"
|
||||
@echo " clean - Limpiar archivos generados"
|
||||
@echo " help - Mostrar esta ayuda"
|
||||
@echo ""
|
||||
@echo "Ejemplos de uso:"
|
||||
@echo " make # Compilar herramienta"
|
||||
@echo " make test_pack # Crear pack de prueba"
|
||||
@echo " make create_pack # Crear pack final"
|
||||
@echo " make clean # Limpiar archivos"
|
||||
101
tools/shaders/compile_spirv.cmake
Normal file
101
tools/shaders/compile_spirv.cmake
Normal file
@@ -0,0 +1,101 @@
|
||||
# compile_spirv.cmake
|
||||
# Compila shaders GLSL a SPIR-V y genera headers C++ embebibles.
|
||||
# Multiplataforma: Windows, macOS, Linux (no requiere bash, xxd ni /tmp/).
|
||||
#
|
||||
# Invocado por CMakeLists.txt con:
|
||||
# cmake -D GLSLC=<path> -D SHADERS_DIR=<path> -D HEADERS_DIR=<path> -P compile_spirv.cmake
|
||||
#
|
||||
# También puede ejecutarse manualmente desde la raíz del proyecto:
|
||||
# cmake -D GLSLC=glslc -D SHADERS_DIR=data/shaders -D HEADERS_DIR=source/core/rendering/sdl3gpu -P tools/shaders/compile_spirv.cmake
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
cmake_policy(SET CMP0007 NEW)
|
||||
|
||||
# Lista de shaders: fuente relativa a SHADERS_DIR
|
||||
set(SHADER_SOURCES
|
||||
"postfx.vert"
|
||||
"postfx.frag"
|
||||
"upscale.frag"
|
||||
"downscale.frag"
|
||||
"crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
# Nombre de la variable C++ para cada shader (mismo orden)
|
||||
set(SHADER_VARS
|
||||
"kpostfx_vert_spv"
|
||||
"kpostfx_frag_spv"
|
||||
"kupscale_frag_spv"
|
||||
"kdownscale_frag_spv"
|
||||
"kcrtpi_frag_spv"
|
||||
)
|
||||
|
||||
# Flags extra de glslc para cada shader (vacío si no hay)
|
||||
set(SHADER_FLAGS
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
"-fshader-stage=frag"
|
||||
)
|
||||
|
||||
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
||||
math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
|
||||
|
||||
foreach(IDX RANGE ${LAST_IDX})
|
||||
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
|
||||
list(GET SHADER_VARS ${IDX} VAR)
|
||||
list(GET SHADER_FLAGS ${IDX} EXTRA_FLAG)
|
||||
|
||||
# Derivar nombre del header desde la variable: kpostfx_vert_spv → postfx_vert_spv.h
|
||||
string(REGEX REPLACE "^k" "" HDR_BASE "${VAR}")
|
||||
set(SRC "${SHADERS_DIR}/${SRC_NAME}")
|
||||
set(SPV "${HEADERS_DIR}/${HDR_BASE}.spv")
|
||||
set(HDR "${HEADERS_DIR}/${HDR_BASE}.h")
|
||||
|
||||
message(STATUS "Compilando ${SRC} ...")
|
||||
|
||||
if(EXTRA_FLAG)
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" "${EXTRA_FLAG}" "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
else()
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT GLSLC_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "glslc falló para ${SRC}:\n${GLSLC_ERROR}")
|
||||
endif()
|
||||
|
||||
# Leer binario SPV como hex (sin separadores: "0302230700...")
|
||||
file(READ "${SPV}" HEX_DATA HEX)
|
||||
# Dividir en pares de caracteres hex → lista de bytes
|
||||
string(REGEX MATCHALL ".." BYTES "${HEX_DATA}")
|
||||
list(LENGTH BYTES NUM_BYTES)
|
||||
|
||||
# Construir el cuerpo del array C++ con un byte por línea
|
||||
set(ARRAY_BODY "")
|
||||
foreach(BYTE ${BYTES})
|
||||
string(APPEND ARRAY_BODY " 0x${BYTE},\n")
|
||||
endforeach()
|
||||
|
||||
file(WRITE "${HDR}"
|
||||
"#pragma once\n"
|
||||
"#include <cstddef>\n"
|
||||
"#include <cstdint>\n"
|
||||
"static const uint8_t ${VAR}[] = {\n"
|
||||
"${ARRAY_BODY}"
|
||||
"};\n"
|
||||
"static const size_t ${VAR}_size = ${NUM_BYTES};\n"
|
||||
)
|
||||
|
||||
file(REMOVE "${SPV}")
|
||||
message(STATUS " -> ${HDR} (${NUM_BYTES} bytes)")
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Shaders SPIR-V compilados correctamente.")
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile Vulkan GLSL shaders to SPIR-V and update the C++ headers used by SDL3GPUShader.
|
||||
# Required: glslc (from Vulkan SDK or: brew install glslang / apt install glslang-tools)
|
||||
#
|
||||
# Run from the project root: tools/shaders/compile_spirv.sh
|
||||
|
||||
set -e
|
||||
|
||||
SHADERS_DIR="data/shaders"
|
||||
HEADERS_DIR="source/core/rendering/sdl3gpu"
|
||||
|
||||
if ! command -v glslc &> /dev/null; then
|
||||
echo "ERROR: glslc not found. Install Vulkan SDK or run:"
|
||||
echo " macOS: brew install glslang"
|
||||
echo " Linux: sudo apt install glslang-tools"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Compiling SPIR-V shaders..."
|
||||
|
||||
glslc "${SHADERS_DIR}/postfx.vert" -o /tmp/postfx.vert.spv
|
||||
glslc "${SHADERS_DIR}/postfx.frag" -o /tmp/postfx.frag.spv
|
||||
glslc -fshader-stage=fragment "${SHADERS_DIR}/crtpi_frag.glsl" -o /tmp/crtpi_frag.spv
|
||||
glslc "${SHADERS_DIR}/upscale.frag" -o /tmp/upscale.frag.spv
|
||||
glslc "${SHADERS_DIR}/downscale.frag" -o /tmp/downscale.frag.spv
|
||||
|
||||
echo "Generating C++ headers..."
|
||||
|
||||
xxd -i /tmp/postfx.vert.spv | \
|
||||
sed 's/unsigned char .*postfx_vert_spv\[\]/static const uint8_t kpostfx_vert_spv[]/' | \
|
||||
sed 's/unsigned int .*postfx_vert_spv_len/static const size_t kpostfx_vert_spv_size/' \
|
||||
> "${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
|
||||
xxd -i /tmp/postfx.frag.spv | \
|
||||
sed 's/unsigned char .*postfx_frag_spv\[\]/static const uint8_t kpostfx_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*postfx_frag_spv_len/static const size_t kpostfx_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
|
||||
xxd -i /tmp/crtpi_frag.spv | \
|
||||
sed 's/unsigned char .*crtpi_frag_spv\[\]/static const uint8_t kcrtpi_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*crtpi_frag_spv_len/static const size_t kcrtpi_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||
|
||||
xxd -i /tmp/upscale.frag.spv | \
|
||||
sed 's/unsigned char .*upscale_frag_spv\[\]/static const uint8_t kupscale_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*upscale_frag_spv_len/static const size_t kupscale_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/upscale_frag_spv.h"
|
||||
|
||||
xxd -i /tmp/downscale.frag.spv | \
|
||||
sed 's/unsigned char .*downscale_frag_spv\[\]/static const uint8_t kdownscale_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*downscale_frag_spv_len/static const size_t kdownscale_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/downscale_frag_spv.h"
|
||||
|
||||
# Prepend required includes to the headers
|
||||
for f in "${HEADERS_DIR}/postfx_vert_spv.h" "${HEADERS_DIR}/postfx_frag_spv.h" "${HEADERS_DIR}/crtpi_frag_spv.h" "${HEADERS_DIR}/upscale_frag_spv.h" "${HEADERS_DIR}/downscale_frag_spv.h"; do
|
||||
echo -e "#pragma once\n#include <cstdint>\n#include <cstddef>\n$(cat "$f")" > "$f"
|
||||
done
|
||||
|
||||
echo "Done. Headers updated in ${HEADERS_DIR}/"
|
||||
echo " postfx_vert_spv.h"
|
||||
echo " postfx_frag_spv.h"
|
||||
echo " crtpi_frag_spv.h"
|
||||
echo " upscale_frag_spv.h"
|
||||
echo " downscale_frag_spv.h"
|
||||
echo "Rebuild the project to use the new shaders."
|
||||
Reference in New Issue
Block a user