esqueleto nivel 1
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
|
build/
|
||||||
|
esqueleto
|
||||||
|
.cache
|
||||||
|
.claude
|
||||||
|
|
||||||
# ---> C++
|
# ---> C++
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
*.d
|
||||||
|
|||||||
49
.vscode/c_cpp_properties.json
vendored
Normal file
49
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Mac",
|
||||||
|
"includePath": [
|
||||||
|
"${default}",
|
||||||
|
"${workspaceFolder}/source/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"MACOS_BUILD"
|
||||||
|
],
|
||||||
|
"compilerPath": "/usr/bin/clang",
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "macos-clang-arm64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Linux",
|
||||||
|
"includePath": [
|
||||||
|
"${default}",
|
||||||
|
"${workspaceFolder}/source/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"LINUX_BUILD"
|
||||||
|
],
|
||||||
|
"compilerPath": "/usr/bin/g++",
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "linux-gcc-x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Win32",
|
||||||
|
"includePath": [
|
||||||
|
"${default}",
|
||||||
|
"${workspaceFolder}/source/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"WINDOWS_BUILD"
|
||||||
|
],
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "windows-msvc-x64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
164
CMakeLists.txt
Normal file
164
CMakeLists.txt
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# CMakeLists.txt — esqueleto SDL3
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(esqueleto VERSION 1.00)
|
||||||
|
|
||||||
|
# Estándar de C++
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
|
|
||||||
|
# Exportar comandos de compilación para herramientas de análisis
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# --- 1. LISTA DE FUENTES ---
|
||||||
|
set(APP_SOURCES
|
||||||
|
# Core - Audio
|
||||||
|
source/core/audio/audio.cpp
|
||||||
|
|
||||||
|
# Core - Input
|
||||||
|
source/core/input/input.cpp
|
||||||
|
source/core/input/input_types.cpp
|
||||||
|
source/core/input/mouse.cpp
|
||||||
|
|
||||||
|
# Core - Rendering
|
||||||
|
source/core/rendering/gif.cpp
|
||||||
|
source/core/rendering/screen.cpp
|
||||||
|
source/core/rendering/surface.cpp
|
||||||
|
source/core/rendering/sprites/animated_sprite.cpp
|
||||||
|
source/core/rendering/sprites/dissolve_sprite.cpp
|
||||||
|
source/core/rendering/sprites/moving_sprite.cpp
|
||||||
|
source/core/rendering/sprites/sprite.cpp
|
||||||
|
source/core/rendering/text.cpp
|
||||||
|
|
||||||
|
# Utils
|
||||||
|
source/utils/delta_timer.cpp
|
||||||
|
source/utils/utils.cpp
|
||||||
|
|
||||||
|
# Main
|
||||||
|
source/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fuentes del sistema de renderizado SDL3 GPU
|
||||||
|
set(RENDERING_SOURCES
|
||||||
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- SDL3 ---
|
||||||
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||||
|
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||||
|
|
||||||
|
# --- SHADER COMPILATION (Linux/Windows — macOS usa Metal) ---
|
||||||
|
if(NOT APPLE)
|
||||||
|
find_program(GLSLC_EXE NAMES glslc)
|
||||||
|
|
||||||
|
set(SHADER_VERT_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.vert")
|
||||||
|
set(SHADER_FRAG_SRC "${CMAKE_SOURCE_DIR}/data/shaders/postfx.frag")
|
||||||
|
set(SHADER_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")
|
||||||
|
|
||||||
|
if(GLSLC_EXE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${SHADER_VERT_H}" "${SHADER_FRAG_H}"
|
||||||
|
COMMAND "${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.sh"
|
||||||
|
DEPENDS "${SHADER_VERT_SRC}" "${SHADER_FRAG_SRC}"
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
COMMENT "Compilando shaders SPIR-V..."
|
||||||
|
)
|
||||||
|
add_custom_target(shaders DEPENDS "${SHADER_VERT_H}" "${SHADER_FRAG_H}")
|
||||||
|
message(STATUS "glslc encontrado: shaders se compilarán automáticamente")
|
||||||
|
else()
|
||||||
|
if(NOT EXISTS "${SHADER_VERT_H}" OR NOT EXISTS "${SHADER_FRAG_H}")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"glslc no encontrado y headers SPIR-V no existen.\n"
|
||||||
|
" Instala glslc: sudo apt install glslang-tools (Linux)\n"
|
||||||
|
" choco install vulkan-sdk (Windows)\n"
|
||||||
|
" O genera los headers manualmente con: tools/shaders/compile_spirv.sh"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "glslc no encontrado - usando headers SPIR-V precompilados")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
message(STATUS "macOS: shaders SPIR-V omitidos (usa Metal)")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- 2. EJECUTABLE ---
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES} ${RENDERING_SOURCES})
|
||||||
|
|
||||||
|
if(NOT APPLE AND GLSLC_EXE)
|
||||||
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- 3. DIRECTORIOS DE INCLUSIÓN ---
|
||||||
|
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 4. ENLACE ---
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||||
|
|
||||||
|
# --- 5. FLAGS DE COMPILACIÓN ---
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
|
||||||
|
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:DEBUG>:_DEBUG>)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:RELEASE_BUILD>)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE WINDOWS_BUILD)
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 mingw32)
|
||||||
|
elseif(APPLE)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated)
|
||||||
|
set(CMAKE_OSX_ARCHITECTURES "arm64")
|
||||||
|
elseif(UNIX AND NOT APPLE)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
|
# --- 6. STATIC ANALYSIS ---
|
||||||
|
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||||
|
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||||
|
"${CMAKE_SOURCE_DIR}/source/*.cpp"
|
||||||
|
"${CMAKE_SOURCE_DIR}/source/*.hpp"
|
||||||
|
"${CMAKE_SOURCE_DIR}/source/*.h"
|
||||||
|
)
|
||||||
|
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
|
||||||
|
|
||||||
|
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
||||||
|
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
|
||||||
|
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*_spv\\.h$")
|
||||||
|
|
||||||
|
if(CLANG_TIDY_EXE)
|
||||||
|
add_custom_target(tidy
|
||||||
|
COMMAND ${CLANG_TIDY_EXE} -p ${CMAKE_BINARY_DIR} ${CLANG_TIDY_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running clang-tidy..."
|
||||||
|
)
|
||||||
|
add_custom_target(tidy-fix
|
||||||
|
COMMAND ${CLANG_TIDY_EXE} -p ${CMAKE_BINARY_DIR} --fix ${CLANG_TIDY_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running clang-tidy with fixes..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "clang-tidy no encontrado")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CLANG_FORMAT_EXE)
|
||||||
|
add_custom_target(format
|
||||||
|
COMMAND ${CLANG_FORMAT_EXE} -i ${ALL_SOURCE_FILES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running clang-format..."
|
||||||
|
)
|
||||||
|
add_custom_target(format-check
|
||||||
|
COMMAND ${CLANG_FORMAT_EXE} --dry-run --Werror ${ALL_SOURCE_FILES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Checking clang-format..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "clang-format no encontrado")
|
||||||
|
endif()
|
||||||
190
source/core/audio/audio.cpp
Normal file
190
source/core/audio/audio.cpp
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#include "audio.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // Para clamp
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
#include "core/audio/jail_audio.hpp" // Para JA_*
|
||||||
|
#include "core/options.hpp" // Para Options::audio
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
Audio* Audio::instance = nullptr;
|
||||||
|
|
||||||
|
void Audio::init() { Audio::instance = new Audio(); }
|
||||||
|
void Audio::destroy() { delete Audio::instance; }
|
||||||
|
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Audio::Audio() { initSDLAudio(); }
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Audio::~Audio() {
|
||||||
|
// Liberar recursos de música cargados
|
||||||
|
for (auto& [name, music] : music_cache_) {
|
||||||
|
if (music) {
|
||||||
|
JA_StopMusic();
|
||||||
|
delete music;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Liberar recursos de sonido cargados
|
||||||
|
for (auto& [name, sound] : sound_cache_) {
|
||||||
|
if (sound) {
|
||||||
|
SDL_free(sound->buffer);
|
||||||
|
delete sound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JA_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::update() {
|
||||||
|
JA_Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce una pista de música a partir de su ruta de fichero
|
||||||
|
void Audio::playMusic(const std::string& path, const int loop) {
|
||||||
|
bool new_loop = (loop != 0);
|
||||||
|
|
||||||
|
if (music_.state == MusicState::PLAYING && music_.name == path && music_.loop == new_loop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar si no está en caché
|
||||||
|
auto it = music_cache_.find(path);
|
||||||
|
JA_Music_t* resource = nullptr;
|
||||||
|
if (it != music_cache_.end()) {
|
||||||
|
resource = it->second;
|
||||||
|
} else {
|
||||||
|
resource = JA_LoadMusic(path.c_str());
|
||||||
|
if (resource != nullptr) {
|
||||||
|
music_cache_[path] = resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource == nullptr) {
|
||||||
|
std::cerr << "Audio: no se pudo cargar la música: " << path << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (music_.state == MusicState::PLAYING) {
|
||||||
|
JA_StopMusic();
|
||||||
|
}
|
||||||
|
|
||||||
|
JA_PlayMusic(resource, loop);
|
||||||
|
music_.name = path;
|
||||||
|
music_.loop = new_loop;
|
||||||
|
music_.state = MusicState::PLAYING;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::pauseMusic() {
|
||||||
|
if (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||||
|
JA_PauseMusic();
|
||||||
|
music_.state = MusicState::PAUSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::resumeMusic() {
|
||||||
|
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||||
|
JA_ResumeMusic();
|
||||||
|
music_.state = MusicState::PLAYING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::stopMusic() {
|
||||||
|
if (music_enabled_) {
|
||||||
|
JA_StopMusic();
|
||||||
|
music_.state = MusicState::STOPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce un efecto de sonido a partir de su ruta de fichero
|
||||||
|
void Audio::playSound(const std::string& path, Group group) const {
|
||||||
|
if (!sound_enabled_) return;
|
||||||
|
|
||||||
|
auto it = sound_cache_.find(path);
|
||||||
|
JA_Sound_t* resource = nullptr;
|
||||||
|
if (it != sound_cache_.end()) {
|
||||||
|
resource = it->second;
|
||||||
|
} else {
|
||||||
|
resource = JA_LoadSound(path.c_str());
|
||||||
|
if (resource != nullptr) {
|
||||||
|
sound_cache_[path] = resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource == nullptr) {
|
||||||
|
std::cerr << "Audio: no se pudo cargar el sonido: " << path << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JA_PlaySound(resource, 0, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce un sonido por puntero directo (para reutilizar recursos ya cargados)
|
||||||
|
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||||
|
if (sound_enabled_) {
|
||||||
|
JA_PlaySound(sound, 0, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::stopAllSounds() const {
|
||||||
|
if (sound_enabled_) {
|
||||||
|
JA_StopChannel(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::fadeOutMusic(int milliseconds) const {
|
||||||
|
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
||||||
|
JA_FadeOutMusic(milliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Audio::getRealMusicState() -> MusicState {
|
||||||
|
JA_Music_state ja_state = JA_GetMusicState();
|
||||||
|
switch (ja_state) {
|
||||||
|
case JA_MUSIC_PLAYING: return MusicState::PLAYING;
|
||||||
|
case JA_MUSIC_PAUSED: return MusicState::PAUSED;
|
||||||
|
default: return MusicState::STOPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::setSoundVolume(float sound_volume, Group group) const {
|
||||||
|
if (sound_enabled_) {
|
||||||
|
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
JA_SetSoundVolume(sound_volume * Options::audio.volume, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::setMusicVolume(float music_volume) const {
|
||||||
|
if (music_enabled_) {
|
||||||
|
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
JA_SetMusicVolume(music_volume * Options::audio.volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::applySettings() {
|
||||||
|
enable(Options::audio.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::enable(bool value) {
|
||||||
|
enabled_ = value;
|
||||||
|
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
||||||
|
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::initSDLAudio() {
|
||||||
|
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
|
||||||
|
} else {
|
||||||
|
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||||
|
enable(Options::audio.enabled);
|
||||||
|
std::cout << "\n** AUDIO SYSTEM **\n";
|
||||||
|
std::cout << "Audio system initialized successfully\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
104
source/core/audio/audio.hpp
Normal file
104
source/core/audio/audio.hpp
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
struct JA_Sound_t;
|
||||||
|
struct JA_Music_t;
|
||||||
|
|
||||||
|
// --- Clase Audio: gestor de audio (singleton) ---
|
||||||
|
class Audio {
|
||||||
|
public:
|
||||||
|
// --- Enums ---
|
||||||
|
enum class Group : int {
|
||||||
|
ALL = -1,
|
||||||
|
GAME = 0,
|
||||||
|
INTERFACE = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class MusicState {
|
||||||
|
PLAYING,
|
||||||
|
PAUSED,
|
||||||
|
STOPPED,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Constantes ---
|
||||||
|
static constexpr float MAX_VOLUME = 1.0F;
|
||||||
|
static constexpr float MIN_VOLUME = 0.0F;
|
||||||
|
static constexpr int FREQUENCY = 48000;
|
||||||
|
|
||||||
|
// --- Singleton ---
|
||||||
|
static void init();
|
||||||
|
static void destroy();
|
||||||
|
static auto get() -> Audio*;
|
||||||
|
Audio(const Audio&) = delete;
|
||||||
|
auto operator=(const Audio&) -> Audio& = delete;
|
||||||
|
|
||||||
|
static void update();
|
||||||
|
|
||||||
|
// --- Control de música ---
|
||||||
|
// 'path' es la ruta al fichero de música (.ogg). Se cachea automáticamente.
|
||||||
|
void playMusic(const std::string& path, int loop = -1);
|
||||||
|
void pauseMusic();
|
||||||
|
void resumeMusic();
|
||||||
|
void stopMusic();
|
||||||
|
void fadeOutMusic(int milliseconds) const;
|
||||||
|
|
||||||
|
// --- Control de sonidos ---
|
||||||
|
// 'path' es la ruta al fichero WAV. Se cachea automáticamente.
|
||||||
|
void playSound(const std::string& path, Group group = Group::GAME) const;
|
||||||
|
void playSound(JA_Sound_t* sound, Group group = Group::GAME) const;
|
||||||
|
void stopAllSounds() const;
|
||||||
|
|
||||||
|
// --- Control de volumen ---
|
||||||
|
void setSoundVolume(float volume, Group group = Group::ALL) const;
|
||||||
|
void setMusicVolume(float volume) const;
|
||||||
|
|
||||||
|
// --- Configuración general ---
|
||||||
|
void enable(bool value);
|
||||||
|
void toggleEnabled() { enabled_ = !enabled_; }
|
||||||
|
void applySettings();
|
||||||
|
|
||||||
|
// --- Configuración de sonidos ---
|
||||||
|
void enableSound() { sound_enabled_ = true; }
|
||||||
|
void disableSound() { sound_enabled_ = false; }
|
||||||
|
void enableSound(bool value) { sound_enabled_ = value; }
|
||||||
|
void toggleSound() { sound_enabled_ = !sound_enabled_; }
|
||||||
|
|
||||||
|
// --- Configuración de música ---
|
||||||
|
void enableMusic() { music_enabled_ = true; }
|
||||||
|
void disableMusic() { music_enabled_ = false; }
|
||||||
|
void enableMusic(bool value) { music_enabled_ = value; }
|
||||||
|
void toggleMusic() { music_enabled_ = !music_enabled_; }
|
||||||
|
|
||||||
|
// --- Consultas de estado ---
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||||
|
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||||
|
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||||
|
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
|
||||||
|
[[nodiscard]] static auto getRealMusicState() -> MusicState;
|
||||||
|
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Music {
|
||||||
|
MusicState state{MusicState::STOPPED};
|
||||||
|
std::string name;
|
||||||
|
bool loop{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
Audio();
|
||||||
|
~Audio();
|
||||||
|
void initSDLAudio();
|
||||||
|
|
||||||
|
static Audio* instance;
|
||||||
|
|
||||||
|
Music music_;
|
||||||
|
bool enabled_{true};
|
||||||
|
bool sound_enabled_{true};
|
||||||
|
bool music_enabled_{true};
|
||||||
|
|
||||||
|
// Caché de recursos cargados (ruta → recurso)
|
||||||
|
mutable std::unordered_map<std::string, JA_Music_t*> music_cache_;
|
||||||
|
mutable std::unordered_map<std::string, JA_Sound_t*> sound_cache_;
|
||||||
|
};
|
||||||
482
source/core/audio/jail_audio.hpp
Normal file
482
source/core/audio/jail_audio.hpp
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// --- 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 <stdlib.h> // Para free, malloc
|
||||||
|
#include <string.h> // Para strcpy, strlen
|
||||||
|
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
|
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory
|
||||||
|
|
||||||
|
// --- Public Enums ---
|
||||||
|
enum JA_Channel_state { JA_CHANNEL_INVALID,
|
||||||
|
JA_CHANNEL_FREE,
|
||||||
|
JA_CHANNEL_PLAYING,
|
||||||
|
JA_CHANNEL_PAUSED,
|
||||||
|
JA_SOUND_DISABLED };
|
||||||
|
enum JA_Music_state { JA_MUSIC_INVALID,
|
||||||
|
JA_MUSIC_PLAYING,
|
||||||
|
JA_MUSIC_PAUSED,
|
||||||
|
JA_MUSIC_STOPPED,
|
||||||
|
JA_MUSIC_DISABLED };
|
||||||
|
|
||||||
|
// --- Struct Definitions ---
|
||||||
|
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
|
||||||
|
#define JA_MAX_GROUPS 2
|
||||||
|
|
||||||
|
struct JA_Sound_t {
|
||||||
|
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
Uint32 length{0};
|
||||||
|
Uint8* buffer{NULL};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JA_Channel_t {
|
||||||
|
JA_Sound_t* sound{nullptr};
|
||||||
|
int pos{0};
|
||||||
|
int times{0};
|
||||||
|
int group{0};
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
JA_Channel_state state{JA_CHANNEL_FREE};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JA_Music_t {
|
||||||
|
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
Uint32 length{0};
|
||||||
|
Uint8* buffer{nullptr};
|
||||||
|
char* filename{nullptr};
|
||||||
|
|
||||||
|
int pos{0};
|
||||||
|
int times{0};
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
JA_Music_state state{JA_MUSIC_INVALID};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Internal Global State ---
|
||||||
|
// Marcado 'inline' (C++17) para asegurar una única instancia.
|
||||||
|
|
||||||
|
inline JA_Music_t* current_music{nullptr};
|
||||||
|
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
|
||||||
|
|
||||||
|
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
inline float JA_musicVolume{1.0f};
|
||||||
|
inline float JA_soundVolume[JA_MAX_GROUPS];
|
||||||
|
inline bool JA_musicEnabled{true};
|
||||||
|
inline bool JA_soundEnabled{true};
|
||||||
|
inline SDL_AudioDeviceID sdlAudioDevice{0};
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
|
||||||
|
// --- Core Functions ---
|
||||||
|
|
||||||
|
inline void JA_Update() {
|
||||||
|
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;
|
||||||
|
} 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_music->times != 0) {
|
||||||
|
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
|
||||||
|
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
|
||||||
|
}
|
||||||
|
if (current_music->times > 0) current_music->times--;
|
||||||
|
} else {
|
||||||
|
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (channels[i].times > 0) channels[i].times--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||||
|
#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
|
||||||
|
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||||
|
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||||
|
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_Quit() {
|
||||||
|
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
|
||||||
|
sdlAudioDevice = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Music Functions ---
|
||||||
|
|
||||||
|
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||||
|
JA_Music_t* music = new JA_Music_t();
|
||||||
|
|
||||||
|
int chan, samplerate;
|
||||||
|
short* output;
|
||||||
|
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
|
||||||
|
|
||||||
|
music->spec.channels = chan;
|
||||||
|
music->spec.freq = samplerate;
|
||||||
|
music->spec.format = SDL_AUDIO_S16;
|
||||||
|
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
|
||||||
|
SDL_memcpy(music->buffer, output, music->length);
|
||||||
|
free(output);
|
||||||
|
music->pos = 0;
|
||||||
|
music->state = JA_MUSIC_STOPPED;
|
||||||
|
|
||||||
|
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.
|
||||||
|
FILE* f = fopen(filename, "rb");
|
||||||
|
if (!f) return NULL; // Añadida comprobación de apertura
|
||||||
|
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
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fread(buffer, fsize, 1, f) != 1) {
|
||||||
|
fclose(f);
|
||||||
|
free(buffer);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(buffer);
|
||||||
|
|
||||||
|
return music;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||||
|
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music
|
||||||
|
|
||||||
|
JA_StopMusic();
|
||||||
|
|
||||||
|
current_music = music;
|
||||||
|
current_music->pos = 0;
|
||||||
|
current_music->state = JA_MUSIC_PLAYING;
|
||||||
|
current_music->times = loop;
|
||||||
|
|
||||||
|
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||||
|
if (!current_music->stream) { // Comprobar creación de stream
|
||||||
|
SDL_Log("Failed to create audio stream!");
|
||||||
|
current_music->state = JA_MUSIC_STOPPED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||||
|
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
inline 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_PauseMusic() {
|
||||||
|
if (!JA_musicEnabled) return;
|
||||||
|
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
|
||||||
|
|
||||||
|
current_music->state = JA_MUSIC_PAUSED;
|
||||||
|
SDL_UnbindAudioStream(current_music->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_ResumeMusic() {
|
||||||
|
if (!JA_musicEnabled) return;
|
||||||
|
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
|
||||||
|
|
||||||
|
current_music->state = JA_MUSIC_PLAYING;
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_StopMusic() {
|
||||||
|
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
||||||
|
|
||||||
|
current_music->pos = 0;
|
||||||
|
current_music->state = JA_MUSIC_STOPPED;
|
||||||
|
if (current_music->stream) {
|
||||||
|
SDL_DestroyAudioStream(current_music->stream);
|
||||||
|
current_music->stream = nullptr;
|
||||||
|
}
|
||||||
|
// No liberamos filename aquí, se debería liberar 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;
|
||||||
|
|
||||||
|
fading = true;
|
||||||
|
fade_start_time = SDL_GetTicks();
|
||||||
|
fade_duration = milliseconds;
|
||||||
|
fade_initial_volume = JA_musicVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline JA_Music_state JA_GetMusicState() {
|
||||||
|
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
||||||
|
if (!current_music) return JA_MUSIC_INVALID;
|
||||||
|
|
||||||
|
return current_music->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_DeleteMusic(JA_Music_t* music) {
|
||||||
|
if (!music) return;
|
||||||
|
if (current_music == music) {
|
||||||
|
JA_StopMusic();
|
||||||
|
current_music = nullptr;
|
||||||
|
}
|
||||||
|
SDL_free(music->buffer);
|
||||||
|
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
||||||
|
free(music->filename); // filename se libera aquí
|
||||||
|
delete music;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_SetMusicVolume(float volume) {
|
||||||
|
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
||||||
|
if (current_music && current_music->stream) {
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||||
|
}
|
||||||
|
return JA_musicVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_SetMusicPosition(float value) {
|
||||||
|
if (!current_music) return;
|
||||||
|
current_music->pos = value * current_music->spec.freq;
|
||||||
|
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
|
||||||
|
// el streaming. El streaming siempre parece empezar desde el principio.
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_GetMusicPosition() {
|
||||||
|
if (!current_music) return 0;
|
||||||
|
return float(current_music->pos) / float(current_music->spec.freq);
|
||||||
|
// Nota: Ver `JA_SetMusicPosition`
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
|
||||||
|
delete sound;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
|
||||||
|
delete sound;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
|
||||||
|
if (!JA_soundEnabled || !sound) return -1;
|
||||||
|
|
||||||
|
int channel = 0;
|
||||||
|
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
||||||
|
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
// No hay canal libre, reemplazamos el primero
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JA_PlaySoundOnChannel(sound, channel, loop, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
|
||||||
|
if (!JA_soundEnabled || !sound) return -1;
|
||||||
|
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
|
||||||
|
|
||||||
|
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
|
||||||
|
|
||||||
|
channels[channel].sound = sound;
|
||||||
|
channels[channel].times = loop;
|
||||||
|
channels[channel].pos = 0;
|
||||||
|
channels[channel].group = group; // Asignar grupo
|
||||||
|
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||||
|
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||||
|
|
||||||
|
if (!channels[channel].stream) {
|
||||||
|
SDL_Log("Failed to create audio stream for sound!");
|
||||||
|
channels[channel].state = JA_CHANNEL_FREE;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
||||||
|
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||||
|
if (!sound) return;
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||||
|
}
|
||||||
|
SDL_free(sound->buffer);
|
||||||
|
delete sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_PauseChannel(const int channel) {
|
||||||
|
if (!JA_soundEnabled) return;
|
||||||
|
|
||||||
|
if (channel == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||||
|
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||||
|
channels[i].state = JA_CHANNEL_PAUSED;
|
||||||
|
SDL_UnbindAudioStream(channels[i].stream);
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
||||||
|
channels[channel].state = JA_CHANNEL_PAUSED;
|
||||||
|
SDL_UnbindAudioStream(channels[channel].stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_ResumeChannel(const int channel) {
|
||||||
|
if (!JA_soundEnabled) return;
|
||||||
|
|
||||||
|
if (channel == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||||
|
if (channels[i].state == JA_CHANNEL_PAUSED) {
|
||||||
|
channels[i].state = JA_CHANNEL_PLAYING;
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
||||||
|
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_StopChannel(const int channel) {
|
||||||
|
if (channel == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if (channels[i].state != JA_CHANNEL_FREE) {
|
||||||
|
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
|
||||||
|
channels[i].stream = nullptr;
|
||||||
|
channels[i].state = JA_CHANNEL_FREE;
|
||||||
|
channels[i].pos = 0;
|
||||||
|
channels[i].sound = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state != JA_CHANNEL_FREE) {
|
||||||
|
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
|
||||||
|
channels[channel].stream = nullptr;
|
||||||
|
channels[channel].state = JA_CHANNEL_FREE;
|
||||||
|
channels[channel].pos = 0;
|
||||||
|
channels[channel].sound = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline JA_Channel_state JA_GetChannelState(const int channel) {
|
||||||
|
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
|
||||||
|
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
|
||||||
|
|
||||||
|
return channels[channel].state;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
|
||||||
|
{
|
||||||
|
const float v = SDL_clamp(volume, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
if (group == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
|
||||||
|
JA_soundVolume[i] = v;
|
||||||
|
}
|
||||||
|
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
||||||
|
JA_soundVolume[group] = v;
|
||||||
|
} else {
|
||||||
|
return v; // Grupo inválido
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar volumen a canales activos
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
||||||
|
if (group == -1 || channels[i].group == group) {
|
||||||
|
if (channels[i].stream) {
|
||||||
|
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_EnableSound(const bool value) {
|
||||||
|
if (!value) {
|
||||||
|
JA_StopChannel(-1); // Detener todos los canales
|
||||||
|
}
|
||||||
|
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
|
||||||
|
return v;
|
||||||
|
}
|
||||||
318
source/core/input/input.cpp
Normal file
318
source/core/input/input.cpp
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
#include "core/input/input.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <ranges>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
Input* Input::instance = nullptr;
|
||||||
|
|
||||||
|
void Input::init(const std::string& game_controller_db_path) {
|
||||||
|
Input::instance = new Input(game_controller_db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::destroy() { delete Input::instance; }
|
||||||
|
auto Input::get() -> Input* { return Input::instance; }
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
// Los bindings de sistema se mapean a F1-F12.
|
||||||
|
// Añade los bindings de tu juego (LEFT, RIGHT, etc.) llamando a bindKey() después de init().
|
||||||
|
Input::Input(std::string game_controller_db_path)
|
||||||
|
: gamepad_mappings_file_(std::move(game_controller_db_path)) {
|
||||||
|
keyboard_.bindings = {
|
||||||
|
// Controles de sistema
|
||||||
|
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
|
||||||
|
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
||||||
|
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
||||||
|
{Action::TOGGLE_POSTFX, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||||
|
{Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}},
|
||||||
|
{Action::PREVIOUS_PALETTE, KeyState{.scancode = SDL_SCANCODE_F6}},
|
||||||
|
{Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}},
|
||||||
|
{Action::TOGGLE_MUSIC, KeyState{.scancode = SDL_SCANCODE_F8}},
|
||||||
|
{Action::TOGGLE_BORDER, KeyState{.scancode = SDL_SCANCODE_F9}},
|
||||||
|
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F10}},
|
||||||
|
{Action::TOGGLE_DEBUG, KeyState{.scancode = SDL_SCANCODE_F12}}};
|
||||||
|
|
||||||
|
initSDLGamePad();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::bindKey(Action action, SDL_Scancode code) {
|
||||||
|
keyboard_.bindings[action].scancode = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (gamepad != nullptr) {
|
||||||
|
gamepad->bindings[action].button = button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (gamepad != nullptr) {
|
||||||
|
gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
bool success_keyboard = false;
|
||||||
|
bool success_controller = false;
|
||||||
|
|
||||||
|
if (check_keyboard) {
|
||||||
|
if (repeat) {
|
||||||
|
success_keyboard = keyboard_.bindings[action].is_held;
|
||||||
|
} else {
|
||||||
|
success_keyboard = keyboard_.bindings[action].just_pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||||
|
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||||
|
active_gamepad = gamepads_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_gamepad != nullptr) {
|
||||||
|
success_controller = checkAxisInput(action, active_gamepad, repeat);
|
||||||
|
|
||||||
|
if (!success_controller) {
|
||||||
|
success_controller = checkTriggerInput(action, active_gamepad, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success_controller) {
|
||||||
|
if (repeat) {
|
||||||
|
success_controller = active_gamepad->bindings[action].is_held;
|
||||||
|
} else {
|
||||||
|
success_controller = active_gamepad->bindings[action].just_pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (success_keyboard || success_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (check_keyboard) {
|
||||||
|
for (const auto& pair : keyboard_.bindings) {
|
||||||
|
if (pair.second.just_pressed) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||||
|
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||||
|
active_gamepad = gamepads_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_gamepad != nullptr) {
|
||||||
|
for (const auto& pair : active_gamepad->bindings) {
|
||||||
|
if (pair.second.just_pressed) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::checkAnyButton(bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
for (auto bi : BUTTON_INPUTS) {
|
||||||
|
if (checkAction(bi, repeat, CHECK_KEYBOARD)) return true;
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::gameControllerFound() const -> bool { return !gamepads_.empty(); }
|
||||||
|
|
||||||
|
auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string {
|
||||||
|
return gamepad == nullptr ? std::string() : gamepad->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::getControllerNames() const -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
names.push_back(gamepad->name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }
|
||||||
|
|
||||||
|
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad->instance_id == id) return gamepad;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad && gamepad->name == name) return gamepad;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto& binding = gamepad->bindings[action];
|
||||||
|
if (binding.button < 200) return false;
|
||||||
|
|
||||||
|
bool axis_active_now = false;
|
||||||
|
if (binding.button == 200) {
|
||||||
|
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
|
||||||
|
} else if (binding.button == 201) {
|
||||||
|
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repeat) return axis_active_now;
|
||||||
|
if (axis_active_now && !binding.axis_active) { binding.axis_active = true; return true; }
|
||||||
|
if (!axis_active_now && binding.axis_active) { binding.axis_active = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (gamepad->bindings[action].button == static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) return false;
|
||||||
|
|
||||||
|
int button = gamepad->bindings[action].button;
|
||||||
|
bool trigger_active_now = false;
|
||||||
|
|
||||||
|
if (button == TRIGGER_L2_AS_BUTTON) {
|
||||||
|
trigger_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||||
|
} else if (button == TRIGGER_R2_AS_BUTTON) {
|
||||||
|
trigger_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& binding = gamepad->bindings[action];
|
||||||
|
if (repeat) return trigger_active_now;
|
||||||
|
if (trigger_active_now && !binding.trigger_active) { binding.trigger_active = true; return true; }
|
||||||
|
if (!trigger_active_now && binding.trigger_active) { binding.trigger_active = false; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::addGamepadMappingsFromFile() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
|
||||||
|
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << ": " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::discoverGamepads() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
handleEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::initSDLGamePad() {
|
||||||
|
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
|
||||||
|
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
|
||||||
|
} else {
|
||||||
|
addGamepadMappingsFromFile();
|
||||||
|
discoverGamepads();
|
||||||
|
std::cout << "\n** INPUT SYSTEM **\n";
|
||||||
|
std::cout << "Input System initialized successfully\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::resetInputStates() {
|
||||||
|
for (auto& key : keyboard_.bindings) {
|
||||||
|
key.second.is_held = false;
|
||||||
|
key.second.just_pressed = false;
|
||||||
|
}
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
for (auto& binding : gamepad->bindings) {
|
||||||
|
binding.second.is_held = false;
|
||||||
|
binding.second.just_pressed = false;
|
||||||
|
binding.second.trigger_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::update() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
const bool* key_states = SDL_GetKeyboardState(nullptr);
|
||||||
|
|
||||||
|
for (auto& binding : keyboard_.bindings) {
|
||||||
|
bool key_is_down_now = key_states[binding.second.scancode];
|
||||||
|
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
|
||||||
|
binding.second.is_held = key_is_down_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
for (auto& binding : gamepad->bindings) {
|
||||||
|
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||||
|
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||||
|
binding.second.is_held = button_is_down_now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::handleEvent(const SDL_Event& event) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_EVENT_GAMEPAD_ADDED:
|
||||||
|
return addGamepad(event.gdevice.which);
|
||||||
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
|
return removeGamepad(event.gdevice.which);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
|
||||||
|
if (pad == nullptr) {
|
||||||
|
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto gamepad = std::make_shared<Gamepad>(pad);
|
||||||
|
auto name = gamepad->name;
|
||||||
|
std::cout << "Gamepad connected (" << name << ")\n";
|
||||||
|
gamepads_.push_back(std::move(gamepad));
|
||||||
|
return name + " CONNECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) -> bool {
|
||||||
|
return gamepad->instance_id == id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it != gamepads_.end()) {
|
||||||
|
std::string name = (*it)->name;
|
||||||
|
std::cout << "Gamepad disconnected (" << name << ")\n";
|
||||||
|
gamepads_.erase(it);
|
||||||
|
return name + " DISCONNECTED";
|
||||||
|
}
|
||||||
|
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::printConnectedGamepads() const { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (gamepads_.empty()) {
|
||||||
|
std::cout << "No hay gamepads conectados.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::cout << "Gamepads conectados:\n";
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
std::string name = gamepad->name.empty() ? "Desconocido" : gamepad->name;
|
||||||
|
std::cout << " - ID: " << gamepad->instance_id << ", Nombre: " << name << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (gamepads_.empty()) return nullptr;
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad && gamepad->name == gamepad_name) return gamepad;
|
||||||
|
}
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad) return gamepad;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
130
source/core/input/input.hpp
Normal file
130
source/core/input/input.hpp
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h> // Para SDL_Scancode, SDL_GamepadButton, SDL_JoystickID, SDL_CloseGamepad, SDL_Gamepad, etc.
|
||||||
|
|
||||||
|
#include <array> // Para array
|
||||||
|
#include <memory> // Para shared_ptr
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <unordered_map> // Para unordered_map
|
||||||
|
#include <utility> // Para pair
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
#include "core/input/input_types.hpp" // Para InputAction
|
||||||
|
|
||||||
|
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
|
||||||
|
class Input {
|
||||||
|
public:
|
||||||
|
// --- Constantes ---
|
||||||
|
static constexpr bool ALLOW_REPEAT = true;
|
||||||
|
static constexpr bool DO_NOT_ALLOW_REPEAT = false;
|
||||||
|
static constexpr bool CHECK_KEYBOARD = true;
|
||||||
|
static constexpr bool DO_NOT_CHECK_KEYBOARD = false;
|
||||||
|
static constexpr int TRIGGER_L2_AS_BUTTON = 100;
|
||||||
|
static constexpr int TRIGGER_R2_AS_BUTTON = 101;
|
||||||
|
|
||||||
|
// --- Tipos ---
|
||||||
|
using Action = InputAction;
|
||||||
|
|
||||||
|
// --- Estructuras ---
|
||||||
|
struct KeyState {
|
||||||
|
Uint8 scancode{0};
|
||||||
|
bool is_held{false};
|
||||||
|
bool just_pressed{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ButtonState {
|
||||||
|
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)};
|
||||||
|
bool is_held{false};
|
||||||
|
bool just_pressed{false};
|
||||||
|
bool axis_active{false};
|
||||||
|
bool trigger_active{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Keyboard {
|
||||||
|
std::unordered_map<Action, KeyState> bindings;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Gamepad {
|
||||||
|
SDL_Gamepad* pad{nullptr};
|
||||||
|
SDL_JoystickID instance_id{0};
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
std::unordered_map<Action, ButtonState> bindings;
|
||||||
|
|
||||||
|
explicit Gamepad(SDL_Gamepad* gamepad)
|
||||||
|
: pad(gamepad),
|
||||||
|
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
|
||||||
|
name(std::string(SDL_GetGamepadName(gamepad))),
|
||||||
|
path(std::string(SDL_GetGamepadPath(pad))),
|
||||||
|
bindings{} {} // Sin bindings por defecto — define los de tu juego en main
|
||||||
|
|
||||||
|
~Gamepad() {
|
||||||
|
if (pad != nullptr) {
|
||||||
|
SDL_CloseGamepad(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void rebindAction(Action action, SDL_GamepadButton new_button) {
|
||||||
|
bindings[action].button = static_cast<int>(new_button);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using Gamepads = std::vector<std::shared_ptr<Gamepad>>;
|
||||||
|
|
||||||
|
// --- Singleton ---
|
||||||
|
static void init(const std::string& game_controller_db_path);
|
||||||
|
static void destroy();
|
||||||
|
static auto get() -> Input*;
|
||||||
|
|
||||||
|
// --- Actualización ---
|
||||||
|
void update();
|
||||||
|
|
||||||
|
// --- Configuración de controles ---
|
||||||
|
void bindKey(Action action, SDL_Scancode code);
|
||||||
|
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
|
||||||
|
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
|
||||||
|
|
||||||
|
// --- Consulta de entrada ---
|
||||||
|
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||||
|
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||||
|
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||||
|
void resetInputStates();
|
||||||
|
|
||||||
|
// --- Gestión de gamepads ---
|
||||||
|
[[nodiscard]] auto gameControllerFound() const -> bool;
|
||||||
|
[[nodiscard]] auto getNumGamepads() const -> int;
|
||||||
|
[[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
|
||||||
|
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
||||||
|
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
||||||
|
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||||
|
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||||
|
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||||
|
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
|
||||||
|
void printConnectedGamepads() const;
|
||||||
|
|
||||||
|
// --- Eventos ---
|
||||||
|
auto handleEvent(const SDL_Event& event) -> std::string;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr Sint16 AXIS_THRESHOLD = 30000;
|
||||||
|
static constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||||
|
// Si tu juego tiene acciones con botones analógicos (triggers/ejes), añádelos aquí:
|
||||||
|
static constexpr std::array<Action, 0> BUTTON_INPUTS = {};
|
||||||
|
|
||||||
|
explicit Input(std::string game_controller_db_path);
|
||||||
|
~Input() = default;
|
||||||
|
|
||||||
|
void initSDLGamePad();
|
||||||
|
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||||
|
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||||
|
auto addGamepad(int device_index) -> std::string;
|
||||||
|
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||||
|
void addGamepadMappingsFromFile();
|
||||||
|
void discoverGamepads();
|
||||||
|
|
||||||
|
static Input* instance;
|
||||||
|
|
||||||
|
Gamepads gamepads_;
|
||||||
|
Keyboard keyboard_{};
|
||||||
|
std::string gamepad_mappings_file_;
|
||||||
|
};
|
||||||
75
source/core/input/input_types.cpp
Normal file
75
source/core/input/input_types.cpp
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#include "input_types.hpp"
|
||||||
|
|
||||||
|
#include <utility> // Para pair
|
||||||
|
|
||||||
|
// Definición de los mapas acción ↔ string
|
||||||
|
// Si añades acciones al enum, añádelas también aquí para que sean serializables
|
||||||
|
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||||
|
// Sistema
|
||||||
|
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||||
|
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||||
|
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||||
|
{InputAction::TOGGLE_VSYNC, "TOGGLE_VSYNC"},
|
||||||
|
{InputAction::TOGGLE_INTEGER_SCALE, "TOGGLE_INTEGER_SCALE"},
|
||||||
|
{InputAction::TOGGLE_POSTFX, "TOGGLE_POSTFX"},
|
||||||
|
{InputAction::NEXT_POSTFX_PRESET, "NEXT_POSTFX_PRESET"},
|
||||||
|
{InputAction::TOGGLE_SUPERSAMPLING, "TOGGLE_SUPERSAMPLING"},
|
||||||
|
{InputAction::TOGGLE_BORDER, "TOGGLE_BORDER"},
|
||||||
|
{InputAction::TOGGLE_MUSIC, "TOGGLE_MUSIC"},
|
||||||
|
{InputAction::NEXT_PALETTE, "NEXT_PALETTE"},
|
||||||
|
{InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"},
|
||||||
|
{InputAction::SHOW_DEBUG_INFO, "SHOW_DEBUG_INFO"},
|
||||||
|
{InputAction::TOGGLE_DEBUG, "TOGGLE_DEBUG"},
|
||||||
|
// Añade aquí los mapeos de tus acciones de juego
|
||||||
|
{InputAction::NONE, "NONE"}};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||||
|
// Sistema
|
||||||
|
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||||
|
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||||
|
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||||
|
{"TOGGLE_VSYNC", InputAction::TOGGLE_VSYNC},
|
||||||
|
{"TOGGLE_INTEGER_SCALE", InputAction::TOGGLE_INTEGER_SCALE},
|
||||||
|
{"TOGGLE_POSTFX", InputAction::TOGGLE_POSTFX},
|
||||||
|
{"NEXT_POSTFX_PRESET", InputAction::NEXT_POSTFX_PRESET},
|
||||||
|
{"TOGGLE_SUPERSAMPLING", InputAction::TOGGLE_SUPERSAMPLING},
|
||||||
|
{"TOGGLE_BORDER", InputAction::TOGGLE_BORDER},
|
||||||
|
{"TOGGLE_MUSIC", InputAction::TOGGLE_MUSIC},
|
||||||
|
{"NEXT_PALETTE", InputAction::NEXT_PALETTE},
|
||||||
|
{"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE},
|
||||||
|
{"SHOW_DEBUG_INFO", InputAction::SHOW_DEBUG_INFO},
|
||||||
|
{"TOGGLE_DEBUG", InputAction::TOGGLE_DEBUG},
|
||||||
|
// Añade aquí los mapeos de tus acciones de juego
|
||||||
|
{"NONE", InputAction::NONE}};
|
||||||
|
|
||||||
|
const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING = {
|
||||||
|
{SDL_GAMEPAD_BUTTON_WEST, "WEST"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_NORTH, "NORTH"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_EAST, "EAST"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_START, "START"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_BACK, "BACK"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"},
|
||||||
|
{static_cast<SDL_GamepadButton>(100), "L2_AS_BUTTON"},
|
||||||
|
{static_cast<SDL_GamepadButton>(101), "R2_AS_BUTTON"}};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON = {
|
||||||
|
{"WEST", SDL_GAMEPAD_BUTTON_WEST},
|
||||||
|
{"NORTH", SDL_GAMEPAD_BUTTON_NORTH},
|
||||||
|
{"EAST", SDL_GAMEPAD_BUTTON_EAST},
|
||||||
|
{"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||||
|
{"START", SDL_GAMEPAD_BUTTON_START},
|
||||||
|
{"BACK", SDL_GAMEPAD_BUTTON_BACK},
|
||||||
|
{"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||||
|
{"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||||
|
{"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||||
|
{"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||||
|
{"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||||
|
{"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||||
|
{"L2_AS_BUTTON", static_cast<SDL_GamepadButton>(100)},
|
||||||
|
{"R2_AS_BUTTON", static_cast<SDL_GamepadButton>(101)}};
|
||||||
48
source/core/input/input_types.hpp
Normal file
48
source/core/input/input_types.hpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
// --- Enum InputAction ---
|
||||||
|
// Acciones de entrada disponibles.
|
||||||
|
//
|
||||||
|
// Las acciones de sistema (F1-F12) están listas para usar.
|
||||||
|
// Añade tus acciones de juego debajo del bloque marcado.
|
||||||
|
enum class InputAction : int {
|
||||||
|
// [SISTEMA] Controles de ventana y video (mapeados por defecto a F1-F12)
|
||||||
|
WINDOW_DEC_ZOOM, // F1 - Reduce el zoom de la ventana
|
||||||
|
WINDOW_INC_ZOOM, // F2 - Aumenta el zoom de la ventana
|
||||||
|
TOGGLE_FULLSCREEN, // F3 - Pantalla completa / ventana
|
||||||
|
TOGGLE_POSTFX, // F4 - Activa/desactiva PostFX
|
||||||
|
NEXT_PALETTE, // F5 - Siguiente paleta
|
||||||
|
PREVIOUS_PALETTE, // F6 - Paleta anterior
|
||||||
|
TOGGLE_INTEGER_SCALE, // F7 - Escalado entero
|
||||||
|
TOGGLE_MUSIC, // F8 - Silencia/activa música
|
||||||
|
TOGGLE_BORDER, // F9 - Muestra/oculta borde
|
||||||
|
TOGGLE_VSYNC, // F10 - VSync
|
||||||
|
TOGGLE_SUPERSAMPLING, // Supersampling PostFX
|
||||||
|
NEXT_POSTFX_PRESET, // Siguiente preset PostFX
|
||||||
|
SHOW_DEBUG_INFO, // Muestra info de debug
|
||||||
|
TOGGLE_DEBUG, // F12 - Activa/desactiva debug
|
||||||
|
|
||||||
|
// [JUEGO] Añade aquí las acciones específicas de tu juego, por ejemplo:
|
||||||
|
// LEFT,
|
||||||
|
// RIGHT,
|
||||||
|
// JUMP,
|
||||||
|
// ATTACK,
|
||||||
|
// ACCEPT,
|
||||||
|
// CANCEL,
|
||||||
|
// PAUSE,
|
||||||
|
|
||||||
|
// Obligatorio - no eliminar
|
||||||
|
NONE,
|
||||||
|
SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Mapeos ---
|
||||||
|
extern const std::unordered_map<InputAction, std::string> ACTION_TO_STRING; // Acción → string
|
||||||
|
extern const std::unordered_map<std::string, InputAction> STRING_TO_ACTION; // String → acción
|
||||||
|
extern const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING; // Botón SDL → string
|
||||||
|
extern const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON; // String → botón SDL
|
||||||
25
source/core/input/mouse.cpp
Normal file
25
source/core/input/mouse.cpp
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#include "core/input/mouse.hpp"
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor
|
||||||
|
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
|
||||||
|
bool cursor_visible = true; // Estado del cursor
|
||||||
|
|
||||||
|
void handleEvent(const SDL_Event& event) {
|
||||||
|
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||||
|
last_mouse_move_time = SDL_GetTicks();
|
||||||
|
if (!cursor_visible) {
|
||||||
|
SDL_ShowCursor();
|
||||||
|
cursor_visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorVisibility() {
|
||||||
|
Uint32 current_time = SDL_GetTicks();
|
||||||
|
if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace Mouse
|
||||||
12
source/core/input/mouse.hpp
Normal file
12
source/core/input/mouse.hpp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor
|
||||||
|
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
|
||||||
|
extern bool cursor_visible; // Estado del cursor
|
||||||
|
|
||||||
|
void handleEvent(const SDL_Event& event);
|
||||||
|
void updateCursorVisibility();
|
||||||
|
} // namespace Mouse
|
||||||
94
source/core/options.hpp
Normal file
94
source/core/options.hpp
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
// --- Namespace Options: configuración del juego ---
|
||||||
|
// Rellena los valores antes de llamar a Screen::init() y Audio::init()
|
||||||
|
namespace Options {
|
||||||
|
|
||||||
|
// Borde alrededor del área de juego (en pixels de la resolución lógica)
|
||||||
|
struct Border {
|
||||||
|
bool enabled{false}; // Activa el borde
|
||||||
|
float width{0.0F}; // Ancho del borde (izquierda + derecha)
|
||||||
|
float height{0.0F}; // Alto del borde (arriba + abajo)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Opciones de vídeo
|
||||||
|
struct Video {
|
||||||
|
bool fullscreen{false}; // Pantalla completa al arrancar
|
||||||
|
int filter{0}; // Filtro de escalado: 0 = NEAREST, 1 = LINEAR
|
||||||
|
bool vertical_sync{false}; // VSync
|
||||||
|
bool postfx{false}; // Efectos PostFX (shaders)
|
||||||
|
bool supersampling{false}; // Supersampling 3× para PostFX
|
||||||
|
bool integer_scale{false}; // Escalado entero en fullscreen
|
||||||
|
Border border{}; // Borde de pantalla
|
||||||
|
std::string palette{}; // Nombre de la paleta activa (sin extensión)
|
||||||
|
std::string palettes_path{}; // Directorio donde buscar ficheros .pal
|
||||||
|
std::string info; // Info del modo de vídeo (rellenada por Screen)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Opciones de ventana
|
||||||
|
struct Window {
|
||||||
|
std::string caption{"Game"}; // Título de la ventana
|
||||||
|
int zoom{2}; // Factor de zoom inicial
|
||||||
|
int max_zoom{2}; // Máximo zoom (calculado por Screen)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolución lógica del juego (en pixels indexados)
|
||||||
|
struct Game {
|
||||||
|
float width{320.0F};
|
||||||
|
float height{180.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Opciones de música
|
||||||
|
struct Music {
|
||||||
|
bool enabled{true}; // Activa la música
|
||||||
|
float volume{1.0F}; // Volumen (0.0 – 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Opciones de efectos de sonido
|
||||||
|
struct Sound {
|
||||||
|
bool enabled{true}; // Activa los efectos
|
||||||
|
float volume{1.0F}; // Volumen (0.0 – 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Opciones de audio
|
||||||
|
struct Audio {
|
||||||
|
Music music{}; // Música
|
||||||
|
Sound sound{}; // Efectos
|
||||||
|
bool enabled{true}; // Activa el sistema de audio
|
||||||
|
float volume{1.0F}; // Volumen global (0.0 – 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preset de efectos PostFX
|
||||||
|
struct PostFXPreset {
|
||||||
|
std::string name;
|
||||||
|
float vignette{0.6F}; // Viñeta (0 = ninguna, 1 = máxima)
|
||||||
|
float scanlines{0.7F}; // Scanlines CRT
|
||||||
|
float chroma{0.15F}; // Aberración cromática
|
||||||
|
float mask{0.0F}; // Máscara de fósforo RGB
|
||||||
|
float gamma{0.0F}; // Corrección gamma
|
||||||
|
float curvature{0.0F}; // Distorsión barrel CRT
|
||||||
|
float bleeding{0.0F}; // Sangrado de color NTSC
|
||||||
|
float flicker{0.0F}; // Parpadeo de fósforo
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuración de fuente de texto para Screen
|
||||||
|
struct TextConfig {
|
||||||
|
std::string surface_path{}; // Ruta al GIF con el bitmap de la fuente
|
||||||
|
std::string fnt_path{}; // Ruta al fichero .fnt de definición de glifos
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Variables globales (inicializa antes de arrancar el juego) ---
|
||||||
|
inline Game game{};
|
||||||
|
inline Video video{};
|
||||||
|
inline Window window{};
|
||||||
|
inline Audio audio{};
|
||||||
|
inline TextConfig text_config{};
|
||||||
|
inline bool console{false}; // Imprime mensajes de debug por consola
|
||||||
|
|
||||||
|
inline std::vector<PostFXPreset> postfx_presets{};
|
||||||
|
inline int current_postfx_preset{0};
|
||||||
|
|
||||||
|
} // namespace Options
|
||||||
295
source/core/rendering/gif.cpp
Normal file
295
source/core/rendering/gif.cpp
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
#include "core/rendering/gif.hpp"
|
||||||
|
|
||||||
|
#include <cstring> // Para memcpy, size_t
|
||||||
|
#include <iostream> // Para std::cout
|
||||||
|
#include <stdexcept> // Para runtime_error
|
||||||
|
#include <string> // Para allocator, char_traits, operator==, basic_string
|
||||||
|
|
||||||
|
namespace GIF {
|
||||||
|
|
||||||
|
// Función inline para reemplazar el macro READ.
|
||||||
|
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'.
|
||||||
|
inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
|
||||||
|
std::memcpy(dst, buffer, size);
|
||||||
|
buffer += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa el diccionario LZW con los valores iniciales
|
||||||
|
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
|
||||||
|
int size = 1 << code_length;
|
||||||
|
dictionary.resize(1 << (code_length + 1));
|
||||||
|
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||||
|
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
|
||||||
|
dictionary[dictionary_ind].prev = -1;
|
||||||
|
dictionary[dictionary_ind].len = 1;
|
||||||
|
}
|
||||||
|
dictionary_ind += 2; // Reservamos espacio para clear y stop codes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lee los próximos bits del stream de entrada para formar un código
|
||||||
|
inline auto readNextCode(const uint8_t*& input, int& input_length, unsigned int& mask, int code_length) -> int {
|
||||||
|
int code = 0;
|
||||||
|
for (int i = 0; i < (code_length + 1); i++) {
|
||||||
|
if (input_length <= 0) {
|
||||||
|
throw std::runtime_error("Unexpected end of input in decompress");
|
||||||
|
}
|
||||||
|
int bit = ((*input & mask) != 0) ? 1 : 0;
|
||||||
|
mask <<= 1;
|
||||||
|
if (mask == 0x100) {
|
||||||
|
mask = 0x01;
|
||||||
|
input++;
|
||||||
|
input_length--;
|
||||||
|
}
|
||||||
|
code |= (bit << i);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encuentra el primer byte de una cadena del diccionario
|
||||||
|
inline auto findFirstByte(const std::vector<DictionaryEntry>& dictionary, int code) -> uint8_t {
|
||||||
|
int ptr = code;
|
||||||
|
while (dictionary[ptr].prev != -1) {
|
||||||
|
ptr = dictionary[ptr].prev;
|
||||||
|
}
|
||||||
|
return dictionary[ptr].byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agrega una nueva entrada al diccionario
|
||||||
|
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
|
||||||
|
uint8_t first_byte;
|
||||||
|
if (code == dictionary_ind) {
|
||||||
|
first_byte = findFirstByte(dictionary, prev);
|
||||||
|
} else {
|
||||||
|
first_byte = findFirstByte(dictionary, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary[dictionary_ind].byte = first_byte;
|
||||||
|
dictionary[dictionary_ind].prev = prev;
|
||||||
|
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
|
||||||
|
dictionary_ind++;
|
||||||
|
|
||||||
|
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
|
||||||
|
code_length++;
|
||||||
|
dictionary.resize(1 << (code_length + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe la cadena decodificada al buffer de salida
|
||||||
|
inline auto writeDecodedString(const std::vector<DictionaryEntry>& dictionary, int code, uint8_t*& out) -> int {
|
||||||
|
int cur_code = code;
|
||||||
|
int match_len = dictionary[cur_code].len;
|
||||||
|
while (cur_code != -1) {
|
||||||
|
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||||
|
if (dictionary[cur_code].prev == cur_code) {
|
||||||
|
std::cerr << "Internal error; self-reference detected." << '\n';
|
||||||
|
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||||
|
}
|
||||||
|
cur_code = dictionary[cur_code].prev;
|
||||||
|
}
|
||||||
|
out += match_len;
|
||||||
|
return match_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Verifica que el code_length tenga un rango razonable.
|
||||||
|
if (code_length < 2 || code_length > 12) {
|
||||||
|
throw std::runtime_error("Invalid LZW code length");
|
||||||
|
}
|
||||||
|
|
||||||
|
int prev = -1;
|
||||||
|
std::vector<DictionaryEntry> dictionary;
|
||||||
|
int dictionary_ind;
|
||||||
|
unsigned int mask = 0x01;
|
||||||
|
int reset_code_length = code_length;
|
||||||
|
int clear_code = 1 << code_length;
|
||||||
|
int stop_code = clear_code + 1;
|
||||||
|
|
||||||
|
// Inicializamos el diccionario con el tamaño correspondiente.
|
||||||
|
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||||
|
|
||||||
|
// Bucle principal: procesar el stream comprimido.
|
||||||
|
while (input_length > 0) {
|
||||||
|
int code = readNextCode(input, input_length, mask, code_length);
|
||||||
|
|
||||||
|
if (code == clear_code) {
|
||||||
|
// Reinicia el diccionario.
|
||||||
|
code_length = reset_code_length;
|
||||||
|
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||||
|
prev = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == stop_code) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev > -1 && code_length < 12) {
|
||||||
|
if (code > dictionary_ind) {
|
||||||
|
std::cerr << "code = " << std::hex << code
|
||||||
|
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
||||||
|
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
|
||||||
|
}
|
||||||
|
|
||||||
|
addDictionaryEntry(dictionary, dictionary_ind, code_length, prev, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = code;
|
||||||
|
|
||||||
|
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||||
|
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||||
|
std::cerr << "Invalid LZW code " << code
|
||||||
|
<< ", dictionary size " << dictionary.size() << '\n';
|
||||||
|
throw std::runtime_error("LZW error: invalid code encountered");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeDecodedString(dictionary, code, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
uint8_t block_size = *buffer;
|
||||||
|
buffer++;
|
||||||
|
while (block_size != 0) {
|
||||||
|
data.insert(data.end(), buffer, buffer + block_size);
|
||||||
|
buffer += block_size;
|
||||||
|
block_size = *buffer;
|
||||||
|
buffer++;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
ImageDescriptor image_descriptor;
|
||||||
|
// Lee 9 bytes para el image descriptor.
|
||||||
|
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
|
||||||
|
|
||||||
|
uint8_t lzw_code_size;
|
||||||
|
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
|
||||||
|
|
||||||
|
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
||||||
|
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
|
||||||
|
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
|
||||||
|
|
||||||
|
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
|
||||||
|
return uncompressed_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
uint8_t header[6];
|
||||||
|
std::memcpy(header, buffer, 6);
|
||||||
|
buffer += 6;
|
||||||
|
|
||||||
|
ScreenDescriptor screen_descriptor;
|
||||||
|
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
|
||||||
|
buffer += sizeof(ScreenDescriptor);
|
||||||
|
|
||||||
|
std::vector<uint32_t> global_color_table;
|
||||||
|
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||||
|
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||||
|
global_color_table.resize(global_color_table_size);
|
||||||
|
for (int i = 0; i < global_color_table_size; ++i) {
|
||||||
|
uint8_t r = buffer[0];
|
||||||
|
uint8_t g = buffer[1];
|
||||||
|
uint8_t b = buffer[2];
|
||||||
|
global_color_table[i] = (r << 16) | (g << 8) | b;
|
||||||
|
buffer += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return global_color_table;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
|
||||||
|
uint8_t header[6];
|
||||||
|
std::memcpy(header, buffer, 6);
|
||||||
|
buffer += 6;
|
||||||
|
|
||||||
|
// Opcional: Validar header
|
||||||
|
std::string header_str(reinterpret_cast<char*>(header), 6);
|
||||||
|
if (header_str != "GIF87a" && header_str != "GIF89a") {
|
||||||
|
throw std::runtime_error("Formato de archivo GIF inválido.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer el Screen Descriptor (7 bytes, empaquetado sin padding)
|
||||||
|
ScreenDescriptor screen_descriptor;
|
||||||
|
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
|
||||||
|
|
||||||
|
// Asigna ancho y alto
|
||||||
|
w = screen_descriptor.width;
|
||||||
|
h = screen_descriptor.height;
|
||||||
|
|
||||||
|
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
|
||||||
|
std::vector<RGB> global_color_table;
|
||||||
|
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||||
|
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||||
|
global_color_table.resize(global_color_table_size);
|
||||||
|
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
|
||||||
|
buffer += 3 * global_color_table_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
|
||||||
|
uint8_t block_type = *buffer++;
|
||||||
|
while (block_type != TRAILER) {
|
||||||
|
if (block_type == EXTENSION_INTRODUCER) // 0x21
|
||||||
|
{
|
||||||
|
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión.
|
||||||
|
uint8_t extension_label = *buffer++;
|
||||||
|
switch (extension_label) {
|
||||||
|
case GRAPHIC_CONTROL: // 0xF9
|
||||||
|
{
|
||||||
|
// Procesar Graphic Control Extension:
|
||||||
|
uint8_t block_size = *buffer++; // Normalmente, blockSize == 4
|
||||||
|
buffer += block_size; // Saltamos los 4 bytes del bloque fijo
|
||||||
|
// Saltar los sub-bloques
|
||||||
|
uint8_t sub_block_size = *buffer++;
|
||||||
|
while (sub_block_size != 0) {
|
||||||
|
buffer += sub_block_size;
|
||||||
|
sub_block_size = *buffer++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case APPLICATION_EXTENSION: // 0xFF
|
||||||
|
case COMMENT_EXTENSION: // 0xFE
|
||||||
|
case PLAINTEXT_EXTENSION: // 0x01
|
||||||
|
{
|
||||||
|
// Para estas extensiones, saltamos el bloque fijo y los sub-bloques.
|
||||||
|
uint8_t block_size = *buffer++;
|
||||||
|
buffer += block_size;
|
||||||
|
uint8_t sub_block_size = *buffer++;
|
||||||
|
while (sub_block_size != 0) {
|
||||||
|
buffer += sub_block_size;
|
||||||
|
sub_block_size = *buffer++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// Si la etiqueta de extensión es desconocida, saltarla también:
|
||||||
|
uint8_t block_size = *buffer++;
|
||||||
|
buffer += block_size;
|
||||||
|
uint8_t sub_block_size = *buffer++;
|
||||||
|
while (sub_block_size != 0) {
|
||||||
|
buffer += sub_block_size;
|
||||||
|
sub_block_size = *buffer++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||||
|
// Procesar el Image Descriptor y retornar los datos de imagen
|
||||||
|
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
|
||||||
|
return std::vector<uint8_t>{};
|
||||||
|
}
|
||||||
|
block_type = *buffer++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::vector<uint8_t>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Gif::loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
|
||||||
|
return processGifStream(buffer, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace GIF
|
||||||
92
source/core/rendering/gif.hpp
Normal file
92
source/core/rendering/gif.hpp
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
namespace GIF {
|
||||||
|
|
||||||
|
// Constantes definidas con constexpr, en lugar de macros
|
||||||
|
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
|
||||||
|
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
|
||||||
|
constexpr uint8_t TRAILER = 0x3B;
|
||||||
|
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
|
||||||
|
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
|
||||||
|
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
|
||||||
|
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct ScreenDescriptor {
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
uint8_t fields;
|
||||||
|
uint8_t background_color_index;
|
||||||
|
uint8_t pixel_aspect_ratio;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RGB {
|
||||||
|
uint8_t r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImageDescriptor {
|
||||||
|
uint16_t image_left_position;
|
||||||
|
uint16_t image_top_position;
|
||||||
|
uint16_t image_width;
|
||||||
|
uint16_t image_height;
|
||||||
|
uint8_t fields;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
struct DictionaryEntry {
|
||||||
|
uint8_t byte;
|
||||||
|
int prev;
|
||||||
|
int len;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Extension {
|
||||||
|
uint8_t extension_code;
|
||||||
|
uint8_t block_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GraphicControlExtension {
|
||||||
|
uint8_t fields;
|
||||||
|
uint16_t delay_time;
|
||||||
|
uint8_t transparent_color_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ApplicationExtension {
|
||||||
|
uint8_t application_id[8];
|
||||||
|
uint8_t version[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlaintextExtension {
|
||||||
|
uint16_t left, top, width, height;
|
||||||
|
uint8_t cell_width, cell_height;
|
||||||
|
uint8_t foreground_color, background_color;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Gif {
|
||||||
|
public:
|
||||||
|
// Descompone (uncompress) el bloque comprimido usando LZW.
|
||||||
|
// Este método puede lanzar std::runtime_error en caso de error.
|
||||||
|
static void decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out);
|
||||||
|
|
||||||
|
// Carga la paleta (global color table) a partir de un buffer,
|
||||||
|
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
|
||||||
|
static auto loadPalette(const uint8_t* buffer) -> std::vector<uint32_t>;
|
||||||
|
|
||||||
|
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
|
||||||
|
// asigna el ancho y alto mediante referencias.
|
||||||
|
static auto loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
|
||||||
|
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
|
||||||
|
|
||||||
|
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
|
||||||
|
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>;
|
||||||
|
|
||||||
|
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
|
||||||
|
static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace GIF
|
||||||
636
source/core/rendering/screen.cpp
Normal file
636
source/core/rendering/screen.cpp
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // Para max, min, transform
|
||||||
|
#include <cctype> // Para toupper
|
||||||
|
#include <filesystem> // Para directory_iterator
|
||||||
|
#include <fstream> // Para basic_ostream, operator<<, endl, basic_...
|
||||||
|
#include <iostream> // Para cerr
|
||||||
|
#include <iterator> // Para istreambuf_iterator, operator==
|
||||||
|
#include <string> // Para char_traits, string, operator+, operator==
|
||||||
|
|
||||||
|
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||||
|
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||||
|
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
|
||||||
|
#include "core/rendering/text.hpp" // Para Text
|
||||||
|
#include "core/options.hpp" // Para Options
|
||||||
|
#include "utils/utils.hpp" // Para loadFileBytes
|
||||||
|
|
||||||
|
|
||||||
|
// [SINGLETON]
|
||||||
|
Screen* Screen::screen = nullptr;
|
||||||
|
|
||||||
|
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||||
|
void Screen::init() {
|
||||||
|
Screen::screen = new Screen();
|
||||||
|
}
|
||||||
|
|
||||||
|
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||||
|
void Screen::destroy() {
|
||||||
|
delete Screen::screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||||
|
auto Screen::get() -> Screen* {
|
||||||
|
return Screen::screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Screen::Screen() {
|
||||||
|
// Escanear el directorio de paletas para encontrar ficheros .pal
|
||||||
|
if (!Options::video.palettes_path.empty()) {
|
||||||
|
try {
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(Options::video.palettes_path)) {
|
||||||
|
if (entry.is_regular_file() && entry.path().extension() == ".pal") {
|
||||||
|
palettes_.push_back(entry.path().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(palettes_.begin(), palettes_.end());
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// Si el directorio no existe o falla, palettes_ queda vacío
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arranca SDL VIDEO, crea la ventana y el renderizador
|
||||||
|
initSDLVideo();
|
||||||
|
if (Options::video.fullscreen) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajusta los tamaños
|
||||||
|
game_surface_dstrect_ = {.x = Options::video.border.width, .y = Options::video.border.height, .w = Options::game.width, .h = Options::game.height};
|
||||||
|
// adjustWindowSize();
|
||||||
|
current_palette_ = findPalette(Options::video.palette);
|
||||||
|
|
||||||
|
// Define el color del borde para el modo de pantalla completa
|
||||||
|
border_color_ = static_cast<Uint8>(PaletteColor::BLACK);
|
||||||
|
|
||||||
|
// Crea la textura donde se dibujan los graficos del juego
|
||||||
|
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width, Options::game.height);
|
||||||
|
if (game_texture_ == nullptr) {
|
||||||
|
// Registrar el error si está habilitado
|
||||||
|
if (Options::console) {
|
||||||
|
std::cerr << "Error: game_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_SetTextureScaleMode(game_texture_, SDL_SCALEMODE_NEAREST);
|
||||||
|
|
||||||
|
// Crea la textura donde se dibuja el borde que rodea el area de juego
|
||||||
|
border_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||||
|
if (border_texture_ == nullptr) {
|
||||||
|
// Registrar el error si está habilitado
|
||||||
|
if (Options::console) {
|
||||||
|
std::cerr << "Error: border_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_SetTextureScaleMode(border_texture_, SDL_SCALEMODE_NEAREST);
|
||||||
|
|
||||||
|
// Cargar la paleta una sola vez
|
||||||
|
Palette initial_palette{};
|
||||||
|
if (!palettes_.empty()) {
|
||||||
|
initial_palette = readPalFile(palettes_.at(current_palette_));
|
||||||
|
} else {
|
||||||
|
// Sin ficheros .pal: paleta de grises por defecto
|
||||||
|
for (int i = 0; i < 256; ++i) {
|
||||||
|
const auto v = static_cast<Uint32>(i);
|
||||||
|
initial_palette[static_cast<size_t>(i)] = (255U << 24U) | (v << 16U) | (v << 8U) | v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea la surface donde se dibujan los graficos del juego
|
||||||
|
game_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
|
||||||
|
game_surface_->setPalette(initial_palette);
|
||||||
|
game_surface_->clear(static_cast<Uint8>(PaletteColor::BLACK));
|
||||||
|
|
||||||
|
// Crea la surface para el borde de colores
|
||||||
|
border_surface_ = std::make_shared<Surface>(Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||||
|
border_surface_->setPalette(initial_palette);
|
||||||
|
border_surface_->clear(border_color_);
|
||||||
|
|
||||||
|
// Establece la surface que actuará como renderer para recibir las llamadas a render()
|
||||||
|
renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||||
|
|
||||||
|
// Crea el objeto de texto para la pantalla de carga
|
||||||
|
createText();
|
||||||
|
|
||||||
|
// Extrae el nombre de las paletas desde su ruta
|
||||||
|
processPaletteList();
|
||||||
|
|
||||||
|
// 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_texture_, nullptr, nullptr);
|
||||||
|
SDL_RenderTexture(renderer_, border_texture_, nullptr, nullptr);
|
||||||
|
|
||||||
|
// Ahora sí inicializar los shaders
|
||||||
|
initShaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Screen::~Screen() {
|
||||||
|
SDL_DestroyTexture(game_texture_);
|
||||||
|
SDL_DestroyTexture(border_texture_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpia el renderer
|
||||||
|
void Screen::clearRenderer(Color color) {
|
||||||
|
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepara para empezar a dibujar en la textura de juego
|
||||||
|
void Screen::start() { setRendererSurface(nullptr); }
|
||||||
|
|
||||||
|
// Vuelca el contenido del renderizador en pantalla
|
||||||
|
void Screen::render() {
|
||||||
|
fps_.increment();
|
||||||
|
|
||||||
|
// Renderiza todos los overlays (escribe en game_surface_ CPU-side)
|
||||||
|
renderOverlays();
|
||||||
|
|
||||||
|
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
|
||||||
|
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
|
||||||
|
if (!(shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||||
|
surfaceToTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia la textura al renderizador (o hace el present GPU)
|
||||||
|
textureToRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el modo de video
|
||||||
|
void Screen::setVideoMode(bool mode) {
|
||||||
|
// Actualiza las opciones
|
||||||
|
Options::video.fullscreen = mode;
|
||||||
|
|
||||||
|
// Configura el modo de pantalla y ajusta la ventana
|
||||||
|
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||||
|
adjustWindowSize();
|
||||||
|
adjustRenderLogicalSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camibia entre pantalla completa y ventana
|
||||||
|
void Screen::toggleVideoMode() {
|
||||||
|
Options::video.fullscreen = !Options::video.fullscreen;
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce el tamaño de la ventana
|
||||||
|
auto Screen::decWindowZoom() -> bool {
|
||||||
|
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||||
|
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||||
|
--Options::window.zoom;
|
||||||
|
Options::window.zoom = std::max(Options::window.zoom, 1);
|
||||||
|
|
||||||
|
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aumenta el tamaño de la ventana
|
||||||
|
auto Screen::incWindowZoom() -> bool {
|
||||||
|
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||||
|
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||||
|
++Options::window.zoom;
|
||||||
|
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||||
|
|
||||||
|
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el color del borde
|
||||||
|
void Screen::setBorderColor(Uint8 color) {
|
||||||
|
border_color_ = color;
|
||||||
|
border_surface_->clear(border_color_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia entre borde visible y no visible
|
||||||
|
void Screen::toggleBorder() {
|
||||||
|
Options::video.border.enabled = !Options::video.border.enabled;
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
initShaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuja las notificaciones
|
||||||
|
void Screen::renderNotifications() const {
|
||||||
|
// Las notificaciones se implementan en el juego concreto
|
||||||
|
(void)notifications_enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el estado del PostFX
|
||||||
|
void Screen::togglePostFX() {
|
||||||
|
Options::video.postfx = !Options::video.postfx;
|
||||||
|
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||||
|
if (Options::video.postfx) {
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
} else {
|
||||||
|
// Pass-through: efectos a 0, el shader copia la textura sin modificar
|
||||||
|
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Backend no inicializado aún — inicializarlo ahora
|
||||||
|
initShaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recarga el shader del preset actual sin toggle
|
||||||
|
void Screen::reloadPostFX() {
|
||||||
|
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||||
|
// El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
} else if (Options::video.postfx) {
|
||||||
|
initShaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
|
||||||
|
void Screen::update(float delta_time) {
|
||||||
|
(void)delta_time;
|
||||||
|
fps_.calculate(SDL_GetTicks());
|
||||||
|
Mouse::updateCursorVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula el tamaño de la ventana
|
||||||
|
void Screen::adjustWindowSize() {
|
||||||
|
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
|
||||||
|
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
|
||||||
|
|
||||||
|
// Establece el nuevo tamaño
|
||||||
|
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||||
|
int old_width;
|
||||||
|
int old_height;
|
||||||
|
SDL_GetWindowSize(window_, &old_width, &old_height);
|
||||||
|
|
||||||
|
int old_pos_x;
|
||||||
|
int old_pos_y;
|
||||||
|
SDL_GetWindowPosition(window_, &old_pos_x, &old_pos_y);
|
||||||
|
|
||||||
|
const int NEW_POS_X = old_pos_x + ((old_width - (window_width_ * Options::window.zoom)) / 2);
|
||||||
|
const int NEW_POS_Y = old_pos_y + ((old_height - (window_height_ * Options::window.zoom)) / 2);
|
||||||
|
|
||||||
|
SDL_SetWindowSize(window_, window_width_ * Options::window.zoom, window_height_ * Options::window.zoom);
|
||||||
|
SDL_SetWindowPosition(window_, std::max(NEW_POS_X, WINDOWS_DECORATIONS), std::max(NEW_POS_Y, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajusta el tamaño lógico del renderizador
|
||||||
|
void Screen::adjustRenderLogicalSize() {
|
||||||
|
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el renderizador para las surfaces
|
||||||
|
void Screen::setRendererSurface(const std::shared_ptr<Surface>& surface) {
|
||||||
|
(surface) ? renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(surface) : renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia la paleta
|
||||||
|
void Screen::nextPalette() {
|
||||||
|
++current_palette_;
|
||||||
|
if (current_palette_ == static_cast<int>(palettes_.size())) {
|
||||||
|
current_palette_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPalete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia la paleta
|
||||||
|
void Screen::previousPalette() {
|
||||||
|
if (current_palette_ > 0) {
|
||||||
|
--current_palette_;
|
||||||
|
} else {
|
||||||
|
current_palette_ = static_cast<Uint8>(palettes_.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPalete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la paleta
|
||||||
|
void Screen::setPalete() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto palette = readPalFile(palettes_.at(current_palette_));
|
||||||
|
game_surface_->loadPalette(palette);
|
||||||
|
border_surface_->loadPalette(palette);
|
||||||
|
|
||||||
|
Options::video.palette = palettes_.at(current_palette_);
|
||||||
|
|
||||||
|
// Eliminar ".gif"
|
||||||
|
size_t pos = Options::video.palette.find(".pal");
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
Options::video.palette.erase(pos, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir a mayúsculas
|
||||||
|
std::ranges::transform(Options::video.palette, Options::video.palette.begin(), ::toupper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrae los nombres de las paletas
|
||||||
|
void Screen::processPaletteList() {
|
||||||
|
for (auto& palette : palettes_) {
|
||||||
|
palette = getFileName(palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia la surface a la textura
|
||||||
|
void Screen::surfaceToTexture() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (Options::video.border.enabled) {
|
||||||
|
border_surface_->copyToTexture(renderer_, border_texture_);
|
||||||
|
game_surface_->copyToTexture(renderer_, border_texture_, nullptr, &game_surface_dstrect_);
|
||||||
|
} else {
|
||||||
|
game_surface_->copyToTexture(renderer_, game_texture_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia la textura al renderizador (o hace el present GPU)
|
||||||
|
void Screen::textureToRenderer() {
|
||||||
|
SDL_Texture* texture_to_render = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||||
|
|
||||||
|
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||||
|
// ---- SDL3 GPU path: convertir Surface → ARGB → upload → PostFX/pass-through → present ----
|
||||||
|
if (Options::video.border.enabled) {
|
||||||
|
// El border_surface_ solo tiene el color de borde; hay que componer encima el game_surface_
|
||||||
|
const int BORDER_W = static_cast<int>(border_surface_->getWidth());
|
||||||
|
const int BORDER_H = static_cast<int>(border_surface_->getHeight());
|
||||||
|
pixel_buffer_.resize(static_cast<size_t>(BORDER_W * BORDER_H));
|
||||||
|
border_surface_->toARGBBuffer(pixel_buffer_.data());
|
||||||
|
|
||||||
|
// Compositar game_surface_ en la posición correcta dentro del buffer
|
||||||
|
const int GAME_W = static_cast<int>(game_surface_->getWidth());
|
||||||
|
const int GAME_H = static_cast<int>(game_surface_->getHeight());
|
||||||
|
const int OFF_X = static_cast<int>(game_surface_dstrect_.x);
|
||||||
|
const int OFF_Y = static_cast<int>(game_surface_dstrect_.y);
|
||||||
|
std::vector<Uint32> game_pixels(static_cast<size_t>(GAME_W * GAME_H));
|
||||||
|
game_surface_->toARGBBuffer(game_pixels.data());
|
||||||
|
for (int y = 0; y < GAME_H; ++y) {
|
||||||
|
for (int x = 0; x < GAME_W; ++x) {
|
||||||
|
pixel_buffer_[static_cast<size_t>(((OFF_Y + y) * BORDER_W) + (OFF_X + x))] = game_pixels[static_cast<size_t>((y * GAME_W) + x)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shader_backend_->uploadPixels(pixel_buffer_.data(), BORDER_W, BORDER_H);
|
||||||
|
} else {
|
||||||
|
const int GAME_W = static_cast<int>(game_surface_->getWidth());
|
||||||
|
const int GAME_H = static_cast<int>(game_surface_->getHeight());
|
||||||
|
pixel_buffer_.resize(static_cast<size_t>(GAME_W * GAME_H));
|
||||||
|
game_surface_->toARGBBuffer(pixel_buffer_.data());
|
||||||
|
shader_backend_->uploadPixels(pixel_buffer_.data(), GAME_W, GAME_H);
|
||||||
|
}
|
||||||
|
shader_backend_->render();
|
||||||
|
} else {
|
||||||
|
// ---- SDL_Renderer path (fallback / no-shader) ----
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
SDL_RenderTexture(renderer_, texture_to_render, nullptr, nullptr);
|
||||||
|
SDL_RenderPresent(renderer_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderiza todos los overlays
|
||||||
|
void Screen::renderOverlays() {
|
||||||
|
renderNotifications();
|
||||||
|
#ifdef _DEBUG
|
||||||
|
renderInfo();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localiza la paleta dentro del vector de paletas
|
||||||
|
auto Screen::findPalette(const std::string& name) -> size_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
std::string upper_name = toUpper(name + ".pal");
|
||||||
|
|
||||||
|
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||||
|
if (toUpper(getFileName(palettes_[i])) == upper_name) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<size_t>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra información por pantalla
|
||||||
|
void Screen::renderInfo() const {
|
||||||
|
if (show_fps_ && text_) {
|
||||||
|
auto color = static_cast<Uint8>(PaletteColor::YELLOW);
|
||||||
|
auto shadow = static_cast<Uint8>(PaletteColor::BLACK);
|
||||||
|
|
||||||
|
// FPS con sombra
|
||||||
|
const std::string FPS_TEXT = std::to_string(fps_.last_value) + " FPS";
|
||||||
|
const int FPS_X = Options::game.width - text_->length(FPS_TEXT) - 1;
|
||||||
|
|
||||||
|
text_->writeColored(FPS_X + 1, 1, FPS_TEXT, shadow);
|
||||||
|
text_->writeColored(FPS_X, 0, FPS_TEXT, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpia la game_surface_
|
||||||
|
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
|
||||||
|
|
||||||
|
// Establece el tamaño del borde
|
||||||
|
void Screen::setBorderWidth(int width) { Options::video.border.width = width; }
|
||||||
|
|
||||||
|
// Establece el tamaño del borde
|
||||||
|
void Screen::setBorderHeight(int height) { Options::video.border.height = height; }
|
||||||
|
|
||||||
|
// Establece si se ha de ver el borde en el modo ventana
|
||||||
|
void Screen::setBorderEnabled(bool value) { Options::video.border.enabled = value; }
|
||||||
|
|
||||||
|
// Muestra la ventana
|
||||||
|
void Screen::show() { SDL_ShowWindow(window_); }
|
||||||
|
|
||||||
|
// Oculta la ventana
|
||||||
|
void Screen::hide() { SDL_HideWindow(window_); }
|
||||||
|
|
||||||
|
// Establece la visibilidad de las notificaciones
|
||||||
|
void Screen::setNotificationsEnabled(bool value) { notifications_enabled_ = value; }
|
||||||
|
|
||||||
|
// Activa / desactiva el contador de FPS
|
||||||
|
void Screen::toggleFPS() { show_fps_ = !show_fps_; }
|
||||||
|
|
||||||
|
// Alterna entre activar y desactivar el escalado entero
|
||||||
|
void Screen::toggleIntegerScale() {
|
||||||
|
Options::video.integer_scale = !Options::video.integer_scale;
|
||||||
|
SDL_SetRenderLogicalPresentation(renderer_, Options::game.width, Options::game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||||
|
if (shader_backend_) {
|
||||||
|
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alterna entre activar y desactivar el V-Sync
|
||||||
|
void Screen::toggleVSync() {
|
||||||
|
Options::video.vertical_sync = !Options::video.vertical_sync;
|
||||||
|
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||||
|
if (shader_backend_) {
|
||||||
|
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
auto Screen::getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||||
|
auto Screen::getRendererSurface() -> std::shared_ptr<Surface> { return (*renderer_surface_); }
|
||||||
|
auto Screen::getBorderSurface() -> std::shared_ptr<Surface> { return border_surface_; }
|
||||||
|
|
||||||
|
auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
|
||||||
|
return loadFileBytes(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activa/desactiva el supersampling global (Ctrl+F4)
|
||||||
|
void Screen::toggleSupersampling() {
|
||||||
|
Options::video.supersampling = !Options::video.supersampling;
|
||||||
|
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica los parámetros del preset actual al backend de shaders
|
||||||
|
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (shader_backend_ && !Options::postfx_presets.empty()) {
|
||||||
|
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)];
|
||||||
|
// Supersampling es un toggle global (Options::video.supersampling), no por preset.
|
||||||
|
// setOversample primero: puede recrear texturas antes de que setPostFXParams
|
||||||
|
// decida si hornear scanlines en CPU o aplicarlas en GPU.
|
||||||
|
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
||||||
|
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
|
||||||
|
shader_backend_->setPostFXParams(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa los shaders
|
||||||
|
// El device GPU se crea siempre (independientemente de postfx) para evitar
|
||||||
|
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
|
||||||
|
void Screen::initShaders() {
|
||||||
|
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||||
|
|
||||||
|
if (!shader_backend_) {
|
||||||
|
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||||
|
}
|
||||||
|
shader_backend_->init(window_, tex, "", "");
|
||||||
|
|
||||||
|
// Propagar flags de vsync e integer scale al backend GPU
|
||||||
|
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||||
|
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||||
|
|
||||||
|
if (Options::video.postfx) {
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
} else {
|
||||||
|
// Pass-through: todos los efectos a 0, el shader solo copia la textura
|
||||||
|
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene información sobre la pantalla
|
||||||
|
void Screen::getDisplayInfo() { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
std::cout << "\n** VIDEO SYSTEM **\n";
|
||||||
|
|
||||||
|
int num_displays = 0;
|
||||||
|
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
||||||
|
if (displays != nullptr) {
|
||||||
|
for (int i = 0; i < num_displays; ++i) {
|
||||||
|
SDL_DisplayID instance_id = displays[i];
|
||||||
|
const char* name = SDL_GetDisplayName(instance_id);
|
||||||
|
|
||||||
|
std::cout << "Display " << instance_id << ": " << ((name != nullptr) ? name : "Unknown") << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
||||||
|
|
||||||
|
// Guarda información del monitor en display_monitor_
|
||||||
|
const char* first_display_name = SDL_GetDisplayName(displays[0]);
|
||||||
|
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
|
||||||
|
display_monitor_.width = static_cast<int>(dm->w);
|
||||||
|
display_monitor_.height = static_cast<int>(dm->h);
|
||||||
|
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
|
||||||
|
|
||||||
|
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||||
|
Options::window.max_zoom = std::min(dm->w / Options::game.width, dm->h / Options::game.height);
|
||||||
|
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||||
|
|
||||||
|
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
|
||||||
|
std::cout << "Current display mode: " << static_cast<int>(dm->w) << "x" << static_cast<int>(dm->h) << " @ " << static_cast<int>(dm->refresh_rate) << "Hz\n";
|
||||||
|
|
||||||
|
std::cout << "Window resolution: " << static_cast<int>(Options::game.width) << "x" << static_cast<int>(Options::game.height) << " x" << Options::window.zoom << '\n';
|
||||||
|
|
||||||
|
Options::video.info = std::to_string(static_cast<int>(dm->w)) + "x" +
|
||||||
|
std::to_string(static_cast<int>(dm->h)) + " @ " +
|
||||||
|
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
|
||||||
|
|
||||||
|
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||||
|
const int MAX_ZOOM = std::min(dm->w / Options::game.width, (dm->h - WINDOWS_DECORATIONS) / Options::game.height);
|
||||||
|
|
||||||
|
// Normaliza los valores de zoom
|
||||||
|
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
|
||||||
|
|
||||||
|
SDL_free(displays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arranca SDL VIDEO y crea la ventana
|
||||||
|
auto Screen::initSDLVideo() -> bool {
|
||||||
|
// Inicializar SDL
|
||||||
|
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
|
std::cerr << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener información de la pantalla
|
||||||
|
getDisplayInfo();
|
||||||
|
|
||||||
|
// Configurar hint para renderizado
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
|
||||||
|
std::cout << "WARNING: Failed to set Metal hint!\n";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Crear ventana
|
||||||
|
const auto WINDOW_WIDTH = Options::video.border.enabled ? Options::game.width + (Options::video.border.width * 2) : Options::game.width;
|
||||||
|
const auto WINDOW_HEIGHT = Options::video.border.enabled ? Options::game.height + (Options::video.border.height * 2) : Options::game.height;
|
||||||
|
SDL_WindowFlags window_flags = 0;
|
||||||
|
if (Options::video.fullscreen) {
|
||||||
|
window_flags |= SDL_WINDOW_FULLSCREEN;
|
||||||
|
}
|
||||||
|
window_ = SDL_CreateWindow(Options::window.caption.c_str(), WINDOW_WIDTH * Options::window.zoom, WINDOW_HEIGHT * Options::window.zoom, window_flags);
|
||||||
|
|
||||||
|
if (window_ == nullptr) {
|
||||||
|
std::cerr << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
|
||||||
|
SDL_Quit();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear renderer
|
||||||
|
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||||
|
if (renderer_ == nullptr) {
|
||||||
|
std::cerr << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
|
||||||
|
SDL_DestroyWindow(window_);
|
||||||
|
window_ = nullptr;
|
||||||
|
SDL_Quit();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar renderer
|
||||||
|
const int EXTRA_WIDTH = Options::video.border.enabled ? Options::video.border.width * 2 : 0;
|
||||||
|
const int EXTRA_HEIGHT = Options::video.border.enabled ? Options::video.border.height * 2 : 0;
|
||||||
|
SDL_SetRenderLogicalPresentation(
|
||||||
|
renderer_,
|
||||||
|
Options::game.width + EXTRA_WIDTH,
|
||||||
|
Options::game.height + EXTRA_HEIGHT,
|
||||||
|
Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||||
|
|
||||||
|
std::cout << "Video system initialized successfully\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea el objeto de texto
|
||||||
|
void Screen::createText() {
|
||||||
|
if (!Options::text_config.surface_path.empty() && !Options::text_config.fnt_path.empty()) {
|
||||||
|
auto surface = std::make_shared<Surface>(Options::text_config.surface_path);
|
||||||
|
text_ = std::make_shared<Text>(surface, Options::text_config.fnt_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
166
source/core/rendering/screen.hpp
Normal file
166
source/core/rendering/screen.hpp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_pixels.h> // Para Uint32
|
||||||
|
|
||||||
|
#include <cstddef> // Para size_t
|
||||||
|
#include <memory> // Para shared_ptr, __shared_ptr_access
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
#include "utils/utils.hpp" // Para Color
|
||||||
|
class Surface;
|
||||||
|
class Text;
|
||||||
|
namespace Rendering {
|
||||||
|
class ShaderBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Screen {
|
||||||
|
public:
|
||||||
|
// Tipos de filtro
|
||||||
|
enum class Filter : Uint32 {
|
||||||
|
NEAREST = 0,
|
||||||
|
LINEAR = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
static void init(); // Crea el singleton
|
||||||
|
static void destroy(); // Destruye el singleton
|
||||||
|
static auto get() -> Screen*; // Obtiene el singleton
|
||||||
|
|
||||||
|
// Renderizado
|
||||||
|
void clearRenderer(Color color = {0x00, 0x00, 0x00}); // Limpia el renderer
|
||||||
|
void clearSurface(Uint8 index); // Limpia la game_surface_
|
||||||
|
void start(); // Prepara para empezar a dibujar en la textura de juego
|
||||||
|
void render(); // Vuelca el contenido del renderizador en pantalla
|
||||||
|
void update(float delta_time); // Actualiza la lógica de la clase
|
||||||
|
|
||||||
|
// Video y ventana
|
||||||
|
void setVideoMode(bool mode); // Establece el modo de video
|
||||||
|
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||||
|
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
|
||||||
|
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||||
|
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
|
||||||
|
auto incWindowZoom() -> bool; // Aumenta el tamaño de la ventana
|
||||||
|
void show(); // Muestra la ventana
|
||||||
|
void hide(); // Oculta la ventana
|
||||||
|
|
||||||
|
// Borde
|
||||||
|
void setBorderColor(Uint8 color); // Cambia el color del borde
|
||||||
|
static void setBorderWidth(int width); // Establece el ancho del borde
|
||||||
|
static void setBorderHeight(int height); // Establece el alto del borde
|
||||||
|
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
|
||||||
|
void toggleBorder(); // Cambia entre borde visible y no visible
|
||||||
|
|
||||||
|
// Paletas y PostFX
|
||||||
|
void nextPalette(); // Cambia a la siguiente paleta
|
||||||
|
void previousPalette(); // Cambia a la paleta anterior
|
||||||
|
void setPalete(); // Establece la paleta actual
|
||||||
|
void togglePostFX(); // Cambia el estado del PostFX
|
||||||
|
void toggleSupersampling(); // Activa/desactiva el supersampling global
|
||||||
|
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
|
||||||
|
|
||||||
|
// Surfaces y notificaciones
|
||||||
|
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
|
||||||
|
void setNotificationsEnabled(bool value); // Establece la visibilidad de las notificaciones
|
||||||
|
void toggleFPS(); // Activa o desactiva el contador de FPS
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
auto getRenderer() -> SDL_Renderer*;
|
||||||
|
auto getRendererSurface() -> std::shared_ptr<Surface>;
|
||||||
|
auto getBorderSurface() -> std::shared_ptr<Surface>;
|
||||||
|
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
|
||||||
|
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Estructuras
|
||||||
|
struct DisplayMonitor {
|
||||||
|
std::string name;
|
||||||
|
int width{0};
|
||||||
|
int height{0};
|
||||||
|
int refresh_rate{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FPS {
|
||||||
|
Uint32 ticks{0}; // Tiempo en milisegundos desde que se comenzó a contar
|
||||||
|
int frame_count{0}; // Número acumulado de frames en el intervalo
|
||||||
|
int last_value{0}; // Número de frames calculado en el último segundo
|
||||||
|
|
||||||
|
void increment() {
|
||||||
|
frame_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto calculate(Uint32 current_ticks) -> int {
|
||||||
|
if (current_ticks - ticks >= 1000) {
|
||||||
|
last_value = frame_count;
|
||||||
|
frame_count = 0;
|
||||||
|
ticks = current_ticks;
|
||||||
|
}
|
||||||
|
return last_value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Constantes
|
||||||
|
static constexpr int WINDOWS_DECORATIONS = 35; // Decoraciones de la ventana
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
static Screen* screen;
|
||||||
|
|
||||||
|
// Métodos privados
|
||||||
|
void renderNotifications() const; // Dibuja las notificaciones
|
||||||
|
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||||
|
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
|
||||||
|
void processPaletteList(); // Extrae los nombres de las paletas
|
||||||
|
void surfaceToTexture(); // Copia la surface a la textura
|
||||||
|
void textureToRenderer(); // Copia la textura al renderizador
|
||||||
|
void renderOverlays(); // Renderiza todos los overlays
|
||||||
|
auto findPalette(const std::string& name) -> size_t; // Localiza la paleta dentro del vector de paletas
|
||||||
|
void initShaders(); // Inicializa los shaders
|
||||||
|
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset actual al backend
|
||||||
|
void renderInfo() const; // Muestra información por pantalla
|
||||||
|
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||||
|
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||||
|
void createText(); // Crea el objeto de texto
|
||||||
|
|
||||||
|
// Constructor y destructor
|
||||||
|
Screen();
|
||||||
|
~Screen();
|
||||||
|
|
||||||
|
// Objetos SDL
|
||||||
|
SDL_Window* window_{nullptr}; // Ventana de la aplicación
|
||||||
|
SDL_Renderer* renderer_{nullptr}; // Renderizador de la ventana
|
||||||
|
SDL_Texture* game_texture_{nullptr}; // Textura donde se dibuja el juego
|
||||||
|
SDL_Texture* border_texture_{nullptr}; // Textura donde se dibuja el borde del juego
|
||||||
|
|
||||||
|
// Surfaces y renderizado
|
||||||
|
std::shared_ptr<Surface> game_surface_; // Surface principal del juego
|
||||||
|
std::shared_ptr<Surface> border_surface_; // Surface para el borde de la pantalla
|
||||||
|
std::shared_ptr<std::shared_ptr<Surface>> renderer_surface_; // Puntero a la Surface activa
|
||||||
|
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (OpenGL/Metal/Vulkan)
|
||||||
|
std::shared_ptr<Text> text_; // Objeto para escribir texto
|
||||||
|
|
||||||
|
// Configuración de ventana y pantalla
|
||||||
|
int window_width_{0}; // Ancho de la pantalla o ventana
|
||||||
|
int window_height_{0}; // Alto de la pantalla o ventana
|
||||||
|
SDL_FRect game_surface_dstrect_; // Coordenadas donde se dibuja la textura del juego
|
||||||
|
|
||||||
|
// Paletas y colores
|
||||||
|
Uint8 border_color_{0}; // Color del borde
|
||||||
|
std::vector<std::string> palettes_; // Listado de ficheros de paleta disponibles
|
||||||
|
Uint8 current_palette_{0}; // Índice para el vector de paletas
|
||||||
|
|
||||||
|
// Estado y configuración
|
||||||
|
bool notifications_enabled_{false}; // Indica si se muestran las notificaciones
|
||||||
|
FPS fps_; // Gestor de frames por segundo
|
||||||
|
DisplayMonitor display_monitor_; // Información de la pantalla
|
||||||
|
|
||||||
|
// Shaders
|
||||||
|
std::string info_resolution_; // Texto con la información de la pantalla
|
||||||
|
std::vector<Uint32> pixel_buffer_; // Buffer intermedio para SDL3GPU path (surface → ARGB)
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
bool show_fps_{true}; // Indica si ha de mostrar el contador de FPS
|
||||||
|
#else
|
||||||
|
bool show_fps_{false}; // Indica si ha de mostrar el contador de FPS
|
||||||
|
#endif
|
||||||
|
};
|
||||||
10341
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
10341
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
720
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
720
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <algorithm> // std::min, std::max, std::floor
|
||||||
|
#include <cmath> // std::floor
|
||||||
|
#include <cstring> // memcpy, strlen
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
|
||||||
|
#include "core/rendering/sdl3gpu/postfx_vert_spv.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// ============================================================================
|
||||||
|
// MSL shaders (Metal Shading Language) — macOS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
static const char* POSTFX_VERT_MSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct PostVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||||
|
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
|
||||||
|
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
|
||||||
|
PostVOut out;
|
||||||
|
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||||
|
out.uv = uvs[vid];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
static const char* POSTFX_FRAG_MSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct PostVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_strength;
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
float mask_strength;
|
||||||
|
float gamma_strength;
|
||||||
|
float curvature;
|
||||||
|
float bleeding;
|
||||||
|
float pixel_scale;
|
||||||
|
float time;
|
||||||
|
float oversample; // 1.0 = sin SS, 3.0 = 3× supersampling
|
||||||
|
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||||
|
};
|
||||||
|
|
||||||
|
// YCbCr helpers for NTSC bleeding
|
||||||
|
static float3 rgb_to_ycc(float3 rgb) {
|
||||||
|
return float3(
|
||||||
|
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
|
||||||
|
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
|
||||||
|
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static float3 ycc_to_rgb(float3 ycc) {
|
||||||
|
float y = ycc.x;
|
||||||
|
float cb = ycc.y - 0.5f;
|
||||||
|
float cr = ycc.z - 0.5f;
|
||||||
|
return clamp(float3(
|
||||||
|
y + 1.402f*cr,
|
||||||
|
y - 0.344f*cb - 0.714f*cr,
|
||||||
|
y + 1.772f*cb
|
||||||
|
), 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||||
|
texture2d<float> scene [[texture(0)]],
|
||||||
|
sampler samp [[sampler(0)]],
|
||||||
|
constant PostFXUniforms& u [[buffer(0)]]) {
|
||||||
|
float2 uv = in.uv;
|
||||||
|
|
||||||
|
// Curvatura barrel CRT
|
||||||
|
if (u.curvature > 0.0f) {
|
||||||
|
float2 c = uv - 0.5f;
|
||||||
|
float rsq = dot(c, c);
|
||||||
|
float2 dist = float2(0.05f, 0.1f) * u.curvature;
|
||||||
|
float2 barrelScale = 1.0f - 0.23f * dist;
|
||||||
|
c += c * (dist * rsq);
|
||||||
|
c *= barrelScale;
|
||||||
|
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
|
||||||
|
return float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
uv = c + 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra base
|
||||||
|
float3 base = scene.sample(samp, uv).rgb;
|
||||||
|
|
||||||
|
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||||
|
// step = 1 pixel de juego en espacio UV (corrige SS: scene.get_width() = game_w * oversample).
|
||||||
|
float3 colour;
|
||||||
|
if (u.bleeding > 0.0f) {
|
||||||
|
float tw = float(scene.get_width());
|
||||||
|
float step = u.oversample / tw; // 1 pixel lógico en UV
|
||||||
|
float3 ycc = rgb_to_ycc(base);
|
||||||
|
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
|
||||||
|
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
|
||||||
|
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
|
||||||
|
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
|
||||||
|
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
|
||||||
|
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||||
|
} else {
|
||||||
|
colour = base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aberración cromática (drift animado con time para efecto NTSC real)
|
||||||
|
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
|
||||||
|
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
|
||||||
|
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
|
||||||
|
|
||||||
|
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||||
|
if (u.gamma_strength > 0.0f) {
|
||||||
|
float3 lin = pow(colour, float3(2.4f));
|
||||||
|
colour = mix(colour, lin, u.gamma_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanlines — 1 pixel físico oscuro por fila lógica.
|
||||||
|
// Usa uv.y (independiente del offset de letterbox) con pixel_scale para
|
||||||
|
// calcular la posición dentro de la fila en coordenadas físicas.
|
||||||
|
// 3x: 1 dark + 2 bright. 4x: 1 dark + 3 bright.
|
||||||
|
// bright=3.5×, dark floor=0.42 (mantiene aspecto CRT original).
|
||||||
|
if (u.scanline_strength > 0.0f) {
|
||||||
|
float ps = max(1.0f, round(u.pixel_scale));
|
||||||
|
float frac_in_row = fract(uv.y * u.screen_height);
|
||||||
|
float row_pos = floor(frac_in_row * ps);
|
||||||
|
float is_dark = step(ps - 1.0f, row_pos);
|
||||||
|
float scan = mix(3.5f, 0.42f, is_dark);
|
||||||
|
colour *= mix(1.0f, scan, u.scanline_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u.gamma_strength > 0.0f) {
|
||||||
|
float3 enc = pow(colour, float3(1.0f/2.2f));
|
||||||
|
colour = mix(colour, enc, u.gamma_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viñeta
|
||||||
|
float2 d = uv - 0.5f;
|
||||||
|
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
|
||||||
|
colour *= clamp(vignette, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// Máscara de fósforo RGB — después de scanlines (orden original):
|
||||||
|
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
|
||||||
|
if (u.mask_strength > 0.0f) {
|
||||||
|
float whichMask = fract(in.pos.x * 0.3333333f);
|
||||||
|
float3 mask = float3(0.80f);
|
||||||
|
if (whichMask < 0.3333333f) mask.x = 1.0f;
|
||||||
|
else if (whichMask < 0.6666667f) mask.y = 1.0f;
|
||||||
|
else mask.z = 1.0f;
|
||||||
|
colour = mix(colour, colour * mask, u.mask_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parpadeo de fósforo CRT (~50 Hz)
|
||||||
|
if (u.flicker > 0.0f) {
|
||||||
|
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
|
||||||
|
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
|
||||||
|
}
|
||||||
|
|
||||||
|
return float4(colour, 1.0f);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
namespace Rendering {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Destructor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
SDL3GPUShader::~SDL3GPUShader() {
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// init
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::init(SDL_Window* window,
|
||||||
|
SDL_Texture* texture,
|
||||||
|
const std::string& /*vertex_source*/,
|
||||||
|
const std::string& /*fragment_source*/) -> bool {
|
||||||
|
// Si ya estaba inicializado (p.ej. al cambiar borde), liberar recursos
|
||||||
|
// de textura/pipeline pero mantener el device vivo para evitar conflictos
|
||||||
|
// con SDL_Renderer en Windows/Vulkan.
|
||||||
|
if (is_initialized_) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
window_ = window;
|
||||||
|
|
||||||
|
// Dimensions from the SDL_Texture placeholder
|
||||||
|
float fw = 0.0F;
|
||||||
|
float fh = 0.0F;
|
||||||
|
SDL_GetTextureSize(texture, &fw, &fh);
|
||||||
|
game_width_ = static_cast<int>(fw);
|
||||||
|
game_height_ = static_cast<int>(fh);
|
||||||
|
tex_width_ = game_width_ * oversample_;
|
||||||
|
tex_height_ = game_height_ * oversample_;
|
||||||
|
uniforms_.screen_height = static_cast<float>(tex_height_); // Altura de la textura GPU
|
||||||
|
uniforms_.oversample = static_cast<float>(oversample_);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 1. Create GPU device (solo si no existe ya)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (device_ == nullptr) {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
|
||||||
|
#else
|
||||||
|
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
#endif
|
||||||
|
device_ = SDL_CreateGPUDevice(PREFERRED, false, nullptr);
|
||||||
|
if (device_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_CreateGPUDevice failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SDL_Log("SDL3GPUShader: driver = %s", SDL_GetGPUDeviceDriver(device_));
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 2. Claim window (una sola vez — no liberar hasta destroy())
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: %s", SDL_GetError());
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, vsync_ ? SDL_GPU_PRESENTMODE_VSYNC : SDL_GPU_PRESENTMODE_IMMEDIATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 3. Create scene texture (upload target + sampler source)
|
||||||
|
// Format: B8G8R8A8_UNORM matches SDL ARGB8888 byte layout on LE
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
SDL_GPUTextureCreateInfo tex_info = {};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
tex_info.width = static_cast<Uint32>(tex_width_);
|
||||||
|
tex_info.height = static_cast<Uint32>(tex_height_);
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
|
||||||
|
if (scene_texture_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create scene texture: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 4. Create upload transfer buffer (CPU → GPU, size = w*h*4 bytes)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = static_cast<Uint32>(tex_width_ * tex_height_ * 4);
|
||||||
|
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
|
||||||
|
if (upload_buffer_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create upload buffer: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 5. Create samplers: NEAREST (pixel art) + LINEAR (supersampling)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
SDL_GPUSamplerCreateInfo samp_info = {};
|
||||||
|
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
|
||||||
|
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
|
||||||
|
samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||||
|
samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
||||||
|
if (sampler_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create sampler: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUSamplerCreateInfo lsamp_info = {};
|
||||||
|
lsamp_info.min_filter = SDL_GPU_FILTER_LINEAR;
|
||||||
|
lsamp_info.mag_filter = SDL_GPU_FILTER_LINEAR;
|
||||||
|
lsamp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||||
|
lsamp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
lsamp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
lsamp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
linear_sampler_ = SDL_CreateGPUSampler(device_, &lsamp_info);
|
||||||
|
if (linear_sampler_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create linear sampler: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 6. Create PostFX graphics pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (!createPipeline()) {
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_initialized_ = true;
|
||||||
|
SDL_Log("SDL3GPUShader: initialized OK (%dx%d)", tex_width_, tex_height_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createPipeline
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createPipeline() -> bool {
|
||||||
|
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* vert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* frag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if ((vert == nullptr) || (frag == nullptr)) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to compile PostFX shaders");
|
||||||
|
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
|
||||||
|
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUColorTargetBlendState no_blend = {};
|
||||||
|
no_blend.enable_blend = false;
|
||||||
|
no_blend.enable_color_write_mask = false;
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription color_target = {};
|
||||||
|
color_target.format = SWAPCHAIN_FMT;
|
||||||
|
color_target.blend_state = no_blend;
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState no_input = {};
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
|
||||||
|
pipe_info.vertex_shader = vert;
|
||||||
|
pipe_info.fragment_shader = frag;
|
||||||
|
pipe_info.vertex_input_state = no_input;
|
||||||
|
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
pipe_info.target_info.num_color_targets = 1;
|
||||||
|
pipe_info.target_info.color_target_descriptions = &color_target;
|
||||||
|
|
||||||
|
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device_, vert);
|
||||||
|
SDL_ReleaseGPUShader(device_, frag);
|
||||||
|
|
||||||
|
if (pipeline_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer.
|
||||||
|
// Con supersampling (oversample_ > 1) expande cada pixel del juego a un bloque
|
||||||
|
// oversample × oversample y hornea la scanline oscura en la última fila del bloque.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
|
||||||
|
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
|
||||||
|
|
||||||
|
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oversample_ <= 1) {
|
||||||
|
// Path sin supersampling: copia directa
|
||||||
|
std::memcpy(mapped, pixels, static_cast<size_t>(width * height * 4));
|
||||||
|
} else {
|
||||||
|
// Path con supersampling: expande cada pixel a OS×OS, oscurece última fila.
|
||||||
|
// Replica la fórmula del shader: mix(3.5, 0.42, scanline_strength).
|
||||||
|
auto* out = static_cast<Uint32*>(mapped);
|
||||||
|
const int OS = oversample_;
|
||||||
|
const float BRIGHT_MUL = 1.0F + (baked_scanline_strength_ * 2.5F); // rows 0..OS-2
|
||||||
|
const float DARK_MUL = 1.0F - (baked_scanline_strength_ * 0.58F); // row OS-1
|
||||||
|
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const Uint32 SRC = pixels[(y * width) + x];
|
||||||
|
const Uint32 ALPHA = (SRC >> 24) & 0xFFU;
|
||||||
|
const auto FR = static_cast<float>((SRC >> 16) & 0xFFU);
|
||||||
|
const auto FG = static_cast<float>((SRC >> 8) & 0xFFU);
|
||||||
|
const auto FB = static_cast<float>(SRC & 0xFFU);
|
||||||
|
|
||||||
|
auto make_px = [ALPHA](float rv, float gv, float bv) -> Uint32 {
|
||||||
|
auto cl = [](float v) -> Uint32 { return static_cast<Uint32>(std::min(255.0F, v)); };
|
||||||
|
return (ALPHA << 24) | (cl(rv) << 16) | (cl(gv) << 8) | cl(bv);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Uint32 BRIGHT = make_px(FR * BRIGHT_MUL, FG * BRIGHT_MUL, FB * BRIGHT_MUL);
|
||||||
|
const Uint32 DARK = make_px(FR * DARK_MUL, FG * DARK_MUL, FB * DARK_MUL);
|
||||||
|
|
||||||
|
for (int dy = 0; dy < OS; ++dy) {
|
||||||
|
const Uint32 OUT_PX = (dy == OS - 1) ? DARK : BRIGHT;
|
||||||
|
const int DST_Y = (y * OS) + dy;
|
||||||
|
for (int dx = 0; dx < OS; ++dx) {
|
||||||
|
out[(DST_Y * (width * OS)) + ((x * OS) + dx)] = OUT_PX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// render — upload scene texture + PostFX pass → swapchain
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::render() {
|
||||||
|
if (!is_initialized_) { return; }
|
||||||
|
|
||||||
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||||
|
if (cmd == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Copy pass: transfer buffer → scene texture ----
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||||
|
if (copy != nullptr) {
|
||||||
|
SDL_GPUTextureTransferInfo src = {};
|
||||||
|
src.transfer_buffer = upload_buffer_;
|
||||||
|
src.offset = 0;
|
||||||
|
src.pixels_per_row = static_cast<Uint32>(tex_width_);
|
||||||
|
src.rows_per_layer = static_cast<Uint32>(tex_height_);
|
||||||
|
|
||||||
|
SDL_GPUTextureRegion dst = {};
|
||||||
|
dst.texture = scene_texture_;
|
||||||
|
dst.w = static_cast<Uint32>(tex_width_);
|
||||||
|
dst.h = static_cast<Uint32>(tex_height_);
|
||||||
|
dst.d = 1;
|
||||||
|
|
||||||
|
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Acquire swapchain texture ----
|
||||||
|
SDL_GPUTexture* swapchain = nullptr;
|
||||||
|
Uint32 sw = 0;
|
||||||
|
Uint32 sh = 0;
|
||||||
|
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (swapchain == nullptr) {
|
||||||
|
// Window minimized — skip frame
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Render pass: PostFX → swapchain ----
|
||||||
|
SDL_GPUColorTargetInfo color_target = {};
|
||||||
|
color_target.texture = swapchain;
|
||||||
|
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||||
|
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||||
|
|
||||||
|
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
||||||
|
if (pass != nullptr) {
|
||||||
|
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||||
|
|
||||||
|
// Calcular viewport usando las dimensiones lógicas del canvas (game_width_/height_),
|
||||||
|
// no las de la textura GPU (que pueden ser game×3 con supersampling).
|
||||||
|
// El GPU escala la textura para cubrir el viewport independientemente de su resolución.
|
||||||
|
float vx = 0.0F;
|
||||||
|
float vy = 0.0F;
|
||||||
|
float vw = 0.0F;
|
||||||
|
float vh = 0.0F;
|
||||||
|
if (integer_scale_) {
|
||||||
|
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
|
||||||
|
vw = static_cast<float>(game_width_ * SCALE);
|
||||||
|
vh = static_cast<float>(game_height_ * SCALE);
|
||||||
|
} else {
|
||||||
|
const float SCALE = std::min(
|
||||||
|
static_cast<float>(sw) / static_cast<float>(game_width_),
|
||||||
|
static_cast<float>(sh) / static_cast<float>(game_height_));
|
||||||
|
vw = static_cast<float>(game_width_) * SCALE;
|
||||||
|
vh = static_cast<float>(game_height_) * SCALE;
|
||||||
|
}
|
||||||
|
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||||
|
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
|
||||||
|
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
|
SDL_SetGPUViewport(pass, &vp);
|
||||||
|
|
||||||
|
// pixel_scale: pixels físicos por pixel lógico de juego (para scanlines sin SS).
|
||||||
|
// Con SS las scanlines están horneadas en CPU → scanline_strength=0 → no se usa.
|
||||||
|
uniforms_.pixel_scale = (game_height_ > 0)
|
||||||
|
? (vh / static_cast<float>(game_height_))
|
||||||
|
: 1.0F;
|
||||||
|
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||||
|
uniforms_.oversample = static_cast<float>(oversample_);
|
||||||
|
|
||||||
|
// Con supersampling usamos LINEAR para que el escalado a zooms no-múltiplo-de-3
|
||||||
|
// promedia correctamente las filas de scanline horneadas en CPU.
|
||||||
|
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
|
||||||
|
? linear_sampler_
|
||||||
|
: sampler_;
|
||||||
|
|
||||||
|
SDL_GPUTextureSamplerBinding binding = {};
|
||||||
|
binding.texture = scene_texture_;
|
||||||
|
binding.sampler = active_sampler;
|
||||||
|
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||||
|
|
||||||
|
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||||
|
|
||||||
|
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||||
|
SDL_EndGPURenderPass(pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// cleanup — libera pipeline/texturas/buffer pero mantiene device + swapchain
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::cleanup() {
|
||||||
|
is_initialized_ = false;
|
||||||
|
|
||||||
|
if (device_ != nullptr) {
|
||||||
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
|
||||||
|
if (pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||||
|
pipeline_ = nullptr;
|
||||||
|
}
|
||||||
|
if (scene_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
|
scene_texture_ = nullptr;
|
||||||
|
}
|
||||||
|
if (upload_buffer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||||
|
upload_buffer_ = nullptr;
|
||||||
|
}
|
||||||
|
if (sampler_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUSampler(device_, sampler_);
|
||||||
|
sampler_ = nullptr;
|
||||||
|
}
|
||||||
|
if (linear_sampler_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUSampler(device_, linear_sampler_);
|
||||||
|
linear_sampler_ = nullptr;
|
||||||
|
}
|
||||||
|
// device_ y el claim de la ventana se mantienen vivos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// destroy — limpieza completa incluyendo device y swapchain (solo al cerrar)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::destroy() {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
if (device_ != nullptr) {
|
||||||
|
if (window_ != nullptr) {
|
||||||
|
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||||
|
}
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
}
|
||||||
|
window_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shader creation helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createShaderMSL(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||||
|
info.code_size = std::strlen(msl_source) + 1;
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: MSL shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device, // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = spv_code;
|
||||||
|
info.code_size = spv_size;
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
|
||||||
|
uniforms_.vignette_strength = p.vignette;
|
||||||
|
uniforms_.chroma_strength = p.chroma;
|
||||||
|
uniforms_.mask_strength = p.mask;
|
||||||
|
uniforms_.gamma_strength = p.gamma;
|
||||||
|
uniforms_.curvature = p.curvature;
|
||||||
|
uniforms_.bleeding = p.bleeding;
|
||||||
|
uniforms_.flicker = p.flicker;
|
||||||
|
|
||||||
|
// Con supersampling las scanlines se hornean en CPU (uploadPixels).
|
||||||
|
// El shader recibe strength=0 para no aplicarlas de nuevo en GPU.
|
||||||
|
baked_scanline_strength_ = p.scanlines;
|
||||||
|
uniforms_.scanline_strength = (oversample_ > 1) ? 0.0F : p.scanlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setVSync(bool vsync) {
|
||||||
|
vsync_ = vsync;
|
||||||
|
if (device_ != nullptr && window_ != nullptr) {
|
||||||
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, vsync_ ? SDL_GPU_PRESENTMODE_VSYNC : SDL_GPU_PRESENTMODE_IMMEDIATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setScaleMode(bool integer_scale) {
|
||||||
|
integer_scale_ = integer_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// setOversample — cambia el factor SS; recrea texturas si ya está inicializado
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::setOversample(int factor) {
|
||||||
|
const int NEW_FACTOR = std::max(1, factor);
|
||||||
|
if (NEW_FACTOR == oversample_) { return; }
|
||||||
|
oversample_ = NEW_FACTOR;
|
||||||
|
if (is_initialized_) {
|
||||||
|
reinitTexturesAndBuffer();
|
||||||
|
// scanline_strength se actualizará en el próximo setPostFXParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// reinitTexturesAndBuffer — recrea scene_texture_ y upload_buffer_ con el
|
||||||
|
// tamaño actual (game × oversample_). No toca pipeline ni samplers.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
|
||||||
|
if (device_ == nullptr) { return false; }
|
||||||
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
|
||||||
|
if (scene_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
|
scene_texture_ = nullptr;
|
||||||
|
}
|
||||||
|
if (upload_buffer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||||
|
upload_buffer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
tex_width_ = game_width_ * oversample_;
|
||||||
|
tex_height_ = game_height_ * oversample_;
|
||||||
|
uniforms_.screen_height = static_cast<float>(tex_height_);
|
||||||
|
uniforms_.oversample = static_cast<float>(oversample_);
|
||||||
|
|
||||||
|
SDL_GPUTextureCreateInfo tex_info = {};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
tex_info.width = static_cast<Uint32>(tex_width_);
|
||||||
|
tex_info.height = static_cast<Uint32>(tex_height_);
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
|
||||||
|
if (scene_texture_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = static_cast<Uint32>(tex_width_ * tex_height_ * 4);
|
||||||
|
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
|
||||||
|
if (upload_buffer_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
|
||||||
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
|
scene_texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("SDL3GPUShader: oversample %d → texture %dx%d", oversample_, tex_width_, tex_height_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
106
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
106
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include "core/rendering/shader_backend.hpp"
|
||||||
|
|
||||||
|
// PostFX uniforms pushed to fragment stage each frame.
|
||||||
|
// Must match the MSL struct and GLSL uniform block layout.
|
||||||
|
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||||
|
struct PostFXUniforms {
|
||||||
|
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||||
|
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||||
|
float scanline_strength; // 0 = off, 1 = full
|
||||||
|
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||||
|
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||||
|
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||||
|
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||||
|
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||||
|
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||||
|
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||||
|
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||||
|
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace Rendering {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
|
||||||
|
*
|
||||||
|
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
|
||||||
|
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||||
|
* → PostFX render pass → swapchain → present
|
||||||
|
*/
|
||||||
|
class SDL3GPUShader : public ShaderBackend {
|
||||||
|
public:
|
||||||
|
SDL3GPUShader() = default;
|
||||||
|
~SDL3GPUShader() override;
|
||||||
|
|
||||||
|
auto init(SDL_Window* window,
|
||||||
|
SDL_Texture* texture,
|
||||||
|
const std::string& vertex_source,
|
||||||
|
const std::string& fragment_source) -> bool override;
|
||||||
|
|
||||||
|
void render() override;
|
||||||
|
void setTextureSize(float width, float height) override {}
|
||||||
|
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
||||||
|
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||||
|
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||||
|
|
||||||
|
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
|
||||||
|
void uploadPixels(const Uint32* pixels, int width, int height) override;
|
||||||
|
|
||||||
|
// Actualiza los parámetros de intensidad de los efectos PostFX
|
||||||
|
void setPostFXParams(const PostFXParams& p) override;
|
||||||
|
|
||||||
|
// Activa/desactiva VSync en el swapchain
|
||||||
|
void setVSync(bool vsync) override;
|
||||||
|
|
||||||
|
// Activa/desactiva escalado entero (integer scale)
|
||||||
|
void setScaleMode(bool integer_scale) override;
|
||||||
|
|
||||||
|
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||||
|
void setOversample(int factor) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||||
|
|
||||||
|
static auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||||
|
|
||||||
|
auto createPipeline() -> bool;
|
||||||
|
auto reinitTexturesAndBuffer() -> bool; // Recrea textura y buffer con oversample actual
|
||||||
|
|
||||||
|
SDL_Window* window_ = nullptr;
|
||||||
|
SDL_GPUDevice* device_ = nullptr;
|
||||||
|
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
|
||||||
|
SDL_GPUTexture* scene_texture_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||||
|
SDL_GPUSampler* sampler_ = nullptr; // NEAREST — para path sin supersampling
|
||||||
|
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR — para path con supersampling
|
||||||
|
|
||||||
|
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
|
||||||
|
|
||||||
|
int game_width_ = 0; // Dimensiones originales del canvas (sin SS)
|
||||||
|
int game_height_ = 0;
|
||||||
|
int tex_width_ = 0; // Dimensiones de la textura GPU (game × oversample_)
|
||||||
|
int tex_height_ = 0;
|
||||||
|
int oversample_ = 1; // Factor SS actual (1 o 3)
|
||||||
|
float baked_scanline_strength_ = 0.0F; // Guardado para hornear en CPU
|
||||||
|
bool is_initialized_ = false;
|
||||||
|
bool vsync_ = true;
|
||||||
|
bool integer_scale_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
100
source/core/rendering/shader_backend.hpp
Normal file
100
source/core/rendering/shader_backend.hpp
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Rendering {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parámetros de intensidad de los efectos PostFX
|
||||||
|
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
|
||||||
|
*/
|
||||||
|
struct PostFXParams {
|
||||||
|
float vignette = 0.0F; // Intensidad de la viñeta
|
||||||
|
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||||
|
float chroma = 0.0F; // Aberración cromática
|
||||||
|
float mask = 0.0F; // Máscara de fósforo RGB
|
||||||
|
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||||
|
float curvature = 0.0F; // Curvatura barrel CRT
|
||||||
|
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||||
|
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Interfaz abstracta para backends de renderizado con shaders
|
||||||
|
*
|
||||||
|
* Esta interfaz define el contrato que todos los backends de shaders
|
||||||
|
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
|
||||||
|
*/
|
||||||
|
class ShaderBackend {
|
||||||
|
public:
|
||||||
|
virtual ~ShaderBackend() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inicializa el backend de shaders
|
||||||
|
* @param window Ventana SDL
|
||||||
|
* @param texture Textura de backbuffer a la que aplicar shaders
|
||||||
|
* @param vertex_source Código fuente del vertex shader
|
||||||
|
* @param fragment_source Código fuente del fragment shader
|
||||||
|
* @return true si la inicialización fue exitosa
|
||||||
|
*/
|
||||||
|
virtual auto init(SDL_Window* window,
|
||||||
|
SDL_Texture* texture,
|
||||||
|
const std::string& vertex_source,
|
||||||
|
const std::string& fragment_source) -> bool = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Renderiza la textura con los shaders aplicados
|
||||||
|
*/
|
||||||
|
virtual void render() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece el tamaño de la textura como parámetro del shader
|
||||||
|
* @param width Ancho de la textura
|
||||||
|
* @param height Alto de la textura
|
||||||
|
*/
|
||||||
|
virtual void setTextureSize(float width, float height) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Limpia y libera recursos del backend
|
||||||
|
*/
|
||||||
|
virtual void cleanup() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
|
||||||
|
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
|
||||||
|
*/
|
||||||
|
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece los parámetros de intensidad de los efectos PostFX
|
||||||
|
* @param p Struct con todos los parámetros PostFX
|
||||||
|
*/
|
||||||
|
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa o desactiva VSync en el swapchain del GPU device
|
||||||
|
*/
|
||||||
|
virtual void setVSync(bool /*vsync*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa o desactiva el escalado entero (integer scale)
|
||||||
|
*/
|
||||||
|
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||||
|
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||||
|
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||||
|
*/
|
||||||
|
virtual void setOversample(int /*factor*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verifica si el backend está usando aceleración por hardware
|
||||||
|
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
344
source/core/rendering/sprites/animated_sprite.cpp
Normal file
344
source/core/rendering/sprites/animated_sprite.cpp
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
#include "core/rendering/sprites/animated_sprite.hpp"
|
||||||
|
|
||||||
|
#include <cstddef> // Para size_t
|
||||||
|
#include <fstream> // Para basic_ostream, basic_istream, operator<<, basic...
|
||||||
|
#include <iostream> // Para cout, cerr
|
||||||
|
#include <sstream> // Para basic_stringstream
|
||||||
|
#include <stdexcept> // Para runtime_error
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
#include "utils/utils.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||||
|
|
||||||
|
// Helper: Convierte un nodo YAML de frames (array) a vector de SDL_FRect
|
||||||
|
auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
||||||
|
std::vector<SDL_FRect> frames;
|
||||||
|
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
||||||
|
|
||||||
|
for (const auto& frame_index_node : frames_node) {
|
||||||
|
const int NUM_TILE = frame_index_node.get_value<int>();
|
||||||
|
if (NUM_TILE <= max_tiles) {
|
||||||
|
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||||
|
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||||
|
frames.emplace_back(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga las animaciones desde un fichero YAML
|
||||||
|
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
std::vector<AnimationData> animations;
|
||||||
|
|
||||||
|
// Extract filename for logging
|
||||||
|
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load YAML file using ResourceHelper (supports both filesystem and pack)
|
||||||
|
auto file_data = loadFileBytes(file_path);
|
||||||
|
|
||||||
|
if (file_data.empty()) {
|
||||||
|
std::cerr << "Error: Unable to load animation file " << FILE_NAME << '\n';
|
||||||
|
throw std::runtime_error("Animation file not found: " + file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
printWithDots("Animation : ", FILE_NAME, "[ LOADED ]");
|
||||||
|
|
||||||
|
// Parse YAML from string
|
||||||
|
std::string yaml_content(file_data.begin(), file_data.end());
|
||||||
|
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||||
|
|
||||||
|
// --- Parse global configuration ---
|
||||||
|
if (yaml.contains("tileSetFile")) {
|
||||||
|
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||||
|
surface = std::make_shared<Surface>(tile_set_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("frameWidth")) {
|
||||||
|
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("frameHeight")) {
|
||||||
|
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate sprite sheet parameters
|
||||||
|
int frames_per_row = 1;
|
||||||
|
int max_tiles = 1;
|
||||||
|
if (surface) {
|
||||||
|
frames_per_row = surface->getWidth() / static_cast<int>(frame_width);
|
||||||
|
const int W = surface->getWidth() / static_cast<int>(frame_width);
|
||||||
|
const int H = surface->getHeight() / static_cast<int>(frame_height);
|
||||||
|
max_tiles = W * H;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Parse animations array ---
|
||||||
|
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||||
|
const auto& animations_node = yaml["animations"];
|
||||||
|
|
||||||
|
for (const auto& anim_node : animations_node) {
|
||||||
|
AnimationData animation;
|
||||||
|
|
||||||
|
// Parse animation name
|
||||||
|
if (anim_node.contains("name")) {
|
||||||
|
animation.name = anim_node["name"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse speed (seconds per frame)
|
||||||
|
if (anim_node.contains("speed")) {
|
||||||
|
animation.speed = anim_node["speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse loop frame index
|
||||||
|
if (anim_node.contains("loop")) {
|
||||||
|
animation.loop = anim_node["loop"].get_value<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse frames array
|
||||||
|
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||||
|
animation.frames = convertYAMLFramesToRects(
|
||||||
|
anim_node["frames"],
|
||||||
|
frame_width,
|
||||||
|
frame_height,
|
||||||
|
frames_per_row,
|
||||||
|
max_tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
animations.push_back(animation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (const fkyaml::exception& e) {
|
||||||
|
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error loading animation " << FILE_NAME << ": " << e.what() << '\n';
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return animations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor con bytes YAML del cache (parsing lazy)
|
||||||
|
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||||
|
// Parsear YAML desde los bytes cargados en cache
|
||||||
|
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||||
|
|
||||||
|
// Variables para almacenar configuración global
|
||||||
|
float frame_width = 0.0F;
|
||||||
|
float frame_height = 0.0F;
|
||||||
|
|
||||||
|
// --- Parse global configuration ---
|
||||||
|
if (yaml.contains("tileSetFile")) {
|
||||||
|
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||||
|
// Ahora SÍ podemos acceder al cache (ya está completamente cargado)
|
||||||
|
surface_ = std::make_shared<Surface>(tile_set_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("frameWidth")) {
|
||||||
|
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("frameHeight")) {
|
||||||
|
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate sprite sheet parameters
|
||||||
|
int frames_per_row = 1;
|
||||||
|
int max_tiles = 1;
|
||||||
|
if (surface_) {
|
||||||
|
frames_per_row = surface_->getWidth() / static_cast<int>(frame_width);
|
||||||
|
const int W = surface_->getWidth() / static_cast<int>(frame_width);
|
||||||
|
const int H = surface_->getHeight() / static_cast<int>(frame_height);
|
||||||
|
max_tiles = W * H;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Parse animations array ---
|
||||||
|
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||||
|
const auto& animations_node = yaml["animations"];
|
||||||
|
|
||||||
|
for (const auto& anim_node : animations_node) {
|
||||||
|
AnimationData animation;
|
||||||
|
|
||||||
|
// Parse animation name
|
||||||
|
if (anim_node.contains("name")) {
|
||||||
|
animation.name = anim_node["name"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse speed (seconds per frame)
|
||||||
|
if (anim_node.contains("speed")) {
|
||||||
|
animation.speed = anim_node["speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse loop frame index
|
||||||
|
if (anim_node.contains("loop")) {
|
||||||
|
animation.loop = anim_node["loop"].get_value<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse frames array
|
||||||
|
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||||
|
animation.frames = convertYAMLFramesToRects(
|
||||||
|
anim_node["frames"],
|
||||||
|
frame_width,
|
||||||
|
frame_height,
|
||||||
|
frames_per_row,
|
||||||
|
max_tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
animations_.push_back(animation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set dimensions
|
||||||
|
setWidth(frame_width);
|
||||||
|
setHeight(frame_height);
|
||||||
|
|
||||||
|
// Inicializar con la primera animación si existe
|
||||||
|
if (!animations_.empty() && !animations_[0].frames.empty()) {
|
||||||
|
setClip(animations_[0].frames[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (const fkyaml::exception& e) {
|
||||||
|
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor per a subclasses amb surface directa (sense YAML)
|
||||||
|
AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||||
|
: MovingSprite(std::move(surface), pos) {
|
||||||
|
// animations_ queda buit (protegit per el guard de animate())
|
||||||
|
if (surface_) {
|
||||||
|
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el indice de la animación a partir del nombre
|
||||||
|
auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto index = -1;
|
||||||
|
|
||||||
|
for (const auto& a : animations_) {
|
||||||
|
index++;
|
||||||
|
if (a.name == name) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula el frame correspondiente a la animación (time-based)
|
||||||
|
void AnimatedSprite::animate(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (animations_.empty()) { return; }
|
||||||
|
if (animations_[current_animation_].speed <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acumula el tiempo transcurrido
|
||||||
|
animations_[current_animation_].accumulated_time += delta_time;
|
||||||
|
|
||||||
|
// Calcula el frame actual a partir del tiempo acumulado
|
||||||
|
const int TARGET_FRAME = static_cast<int>(
|
||||||
|
animations_[current_animation_].accumulated_time /
|
||||||
|
animations_[current_animation_].speed);
|
||||||
|
|
||||||
|
// Si alcanza el final de la animación, maneja el loop
|
||||||
|
if (TARGET_FRAME >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||||
|
if (animations_[current_animation_].loop == -1) {
|
||||||
|
// Si no hay loop, congela en el último frame
|
||||||
|
animations_[current_animation_].current_frame =
|
||||||
|
static_cast<int>(animations_[current_animation_].frames.size()) - 1;
|
||||||
|
animations_[current_animation_].completed = true;
|
||||||
|
|
||||||
|
// Establece el clip del último frame
|
||||||
|
if (animations_[current_animation_].current_frame >= 0) {
|
||||||
|
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si hay loop, vuelve al frame indicado
|
||||||
|
animations_[current_animation_].accumulated_time =
|
||||||
|
static_cast<float>(animations_[current_animation_].loop) *
|
||||||
|
animations_[current_animation_].speed;
|
||||||
|
animations_[current_animation_].current_frame = animations_[current_animation_].loop;
|
||||||
|
|
||||||
|
// Establece el clip del frame de loop
|
||||||
|
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Actualiza el frame actual
|
||||||
|
animations_[current_animation_].current_frame = TARGET_FRAME;
|
||||||
|
|
||||||
|
// Establece el clip del frame actual
|
||||||
|
if (animations_[current_animation_].current_frame >= 0 &&
|
||||||
|
animations_[current_animation_].current_frame <
|
||||||
|
static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||||
|
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si ha terminado la animación
|
||||||
|
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||||
|
return animations_[current_animation_].completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animacion actual
|
||||||
|
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||||
|
const auto NEW_ANIMATION = getIndex(name);
|
||||||
|
if (current_animation_ != NEW_ANIMATION) {
|
||||||
|
current_animation_ = NEW_ANIMATION;
|
||||||
|
animations_[current_animation_].current_frame = 0;
|
||||||
|
animations_[current_animation_].accumulated_time = 0.0F;
|
||||||
|
animations_[current_animation_].completed = false;
|
||||||
|
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animacion actual
|
||||||
|
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||||
|
const auto NEW_ANIMATION = index;
|
||||||
|
if (current_animation_ != NEW_ANIMATION) {
|
||||||
|
current_animation_ = NEW_ANIMATION;
|
||||||
|
animations_[current_animation_].current_frame = 0;
|
||||||
|
animations_[current_animation_].accumulated_time = 0.0F;
|
||||||
|
animations_[current_animation_].completed = false;
|
||||||
|
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza las variables del objeto (time-based)
|
||||||
|
void AnimatedSprite::update(float delta_time) {
|
||||||
|
animate(delta_time);
|
||||||
|
MovingSprite::update(delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinicia la animación
|
||||||
|
void AnimatedSprite::resetAnimation() {
|
||||||
|
animations_[current_animation_].current_frame = 0;
|
||||||
|
animations_[current_animation_].accumulated_time = 0.0F;
|
||||||
|
animations_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el frame actual de la animación
|
||||||
|
void AnimatedSprite::setCurrentAnimationFrame(int num) {
|
||||||
|
// Descarta valores fuera de rango
|
||||||
|
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||||
|
num = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el valor de la variable
|
||||||
|
animations_[current_animation_].current_frame = num;
|
||||||
|
|
||||||
|
// Escoge el frame correspondiente de la animación
|
||||||
|
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
62
source/core/rendering/sprites/animated_sprite.hpp
Normal file
62
source/core/rendering/sprites/animated_sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // Para uint8_t
|
||||||
|
#include <memory> // Para shared_ptr
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <utility>
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
#include "core/rendering/sprites/moving_sprite.hpp" // Para MovingSprite
|
||||||
|
|
||||||
|
class Surface;
|
||||||
|
|
||||||
|
// Recurso de animación: bytes crudos de un fichero YAML (para carga lazy)
|
||||||
|
struct AnimationResource {
|
||||||
|
std::string name; // Nombre del fichero
|
||||||
|
std::vector<uint8_t> yaml_data; // Bytes del archivo YAML sin parsear
|
||||||
|
};
|
||||||
|
|
||||||
|
class AnimatedSprite : public MovingSprite {
|
||||||
|
public:
|
||||||
|
using Animations = std::vector<std::string>;
|
||||||
|
|
||||||
|
struct AnimationData {
|
||||||
|
std::string name;
|
||||||
|
std::vector<SDL_FRect> frames;
|
||||||
|
float speed{0.083F}; // Segundos por frame
|
||||||
|
int loop{0}; // Frame al que vuelve al terminar (-1 = sin loop)
|
||||||
|
bool completed{false};
|
||||||
|
int current_frame{0};
|
||||||
|
float accumulated_time{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carga las animaciones desde un fichero YAML en el sistema de ficheros
|
||||||
|
static auto loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData>;
|
||||||
|
|
||||||
|
// Constructor con datos pre-cargados (bytes YAML en memoria)
|
||||||
|
explicit AnimatedSprite(const AnimationResource& cached_data);
|
||||||
|
|
||||||
|
~AnimatedSprite() override = default;
|
||||||
|
|
||||||
|
void update(float delta_time) override;
|
||||||
|
|
||||||
|
auto animationIsCompleted() -> bool;
|
||||||
|
auto getIndex(const std::string& name) -> int;
|
||||||
|
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); }
|
||||||
|
|
||||||
|
void setCurrentAnimation(const std::string& name = "default");
|
||||||
|
void setCurrentAnimation(int index = 0);
|
||||||
|
void resetAnimation();
|
||||||
|
void setCurrentAnimationFrame(int num);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||||
|
|
||||||
|
void animate(float delta_time);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<AnimationData> animations_;
|
||||||
|
int current_animation_{0};
|
||||||
|
};
|
||||||
188
source/core/rendering/sprites/dissolve_sprite.cpp
Normal file
188
source/core/rendering/sprites/dissolve_sprite.cpp
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#include "core/rendering/sprites/dissolve_sprite.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // Para min
|
||||||
|
#include <cstdint> // Para uint32_t
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp" // Para Surface
|
||||||
|
|
||||||
|
// Hash 2D estable per a dithering (rank aleatori per posició de píxel)
|
||||||
|
static auto pixelRank(int col, int row) -> float {
|
||||||
|
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||||
|
h ^= (h >> 13);
|
||||||
|
h *= 1274126177U;
|
||||||
|
h ^= (h >> 16);
|
||||||
|
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rang per a un píxel tenint en compte direcció (70% direccional + 30% aleatori)
|
||||||
|
auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float {
|
||||||
|
const float RANDOM = pixelRank(col, row);
|
||||||
|
if (dir == DissolveDirection::NONE || frame_h <= 0) {
|
||||||
|
return RANDOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
float y_factor = 0.0F;
|
||||||
|
if (dir == DissolveDirection::DOWN) {
|
||||||
|
y_factor = static_cast<float>(row) / static_cast<float>(frame_h);
|
||||||
|
} else {
|
||||||
|
y_factor = static_cast<float>(frame_h - 1 - row) / static_cast<float>(frame_h);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (y_factor * 0.7F) + (RANDOM * 0.3F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor per a surface directa (sense AnimationResource)
|
||||||
|
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||||
|
: AnimatedSprite(std::move(surface), pos) {
|
||||||
|
if (surface_) {
|
||||||
|
const int W = static_cast<int>(surface_->getWidth());
|
||||||
|
const int H = static_cast<int>(surface_->getHeight());
|
||||||
|
surface_display_ = std::make_shared<Surface>(W, H);
|
||||||
|
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||||
|
surface_display_->clear(surface_->getTransparentColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
DissolveSprite::DissolveSprite(const AnimationResource& data)
|
||||||
|
: AnimatedSprite(data) {
|
||||||
|
if (surface_) {
|
||||||
|
const int W = static_cast<int>(surface_->getWidth());
|
||||||
|
const int H = static_cast<int>(surface_->getHeight());
|
||||||
|
surface_display_ = std::make_shared<Surface>(W, H);
|
||||||
|
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||||
|
// Inicialitza tots els píxels com a transparents
|
||||||
|
surface_display_->clear(surface_->getTransparentColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstrueix la surface_display_ filtrant píxels per progress_
|
||||||
|
void DissolveSprite::rebuildDisplaySurface() {
|
||||||
|
if (!surface_ || !surface_display_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SDL_FRect CLIP = clip_;
|
||||||
|
const int SX = static_cast<int>(CLIP.x);
|
||||||
|
const int SY = static_cast<int>(CLIP.y);
|
||||||
|
const int SW = static_cast<int>(CLIP.w);
|
||||||
|
const int SH = static_cast<int>(CLIP.h);
|
||||||
|
|
||||||
|
if (SW <= 0 || SH <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto src_data = surface_->getSurfaceData();
|
||||||
|
auto dst_data = surface_display_->getSurfaceData();
|
||||||
|
|
||||||
|
const int SRC_W = static_cast<int>(src_data->width);
|
||||||
|
const int DST_W = static_cast<int>(dst_data->width);
|
||||||
|
const Uint8 TRANSPARENT = surface_->getTransparentColor();
|
||||||
|
|
||||||
|
// Esborra frame anterior si ha canviat
|
||||||
|
if (prev_clip_.w > 0 && prev_clip_.h > 0 &&
|
||||||
|
(prev_clip_.x != CLIP.x || prev_clip_.y != CLIP.y ||
|
||||||
|
prev_clip_.w != CLIP.w || prev_clip_.h != CLIP.h)) {
|
||||||
|
surface_display_->fillRect(&prev_clip_, TRANSPARENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esborra la zona del frame actual (reconstrucció neta)
|
||||||
|
surface_display_->fillRect(&CLIP, TRANSPARENT);
|
||||||
|
|
||||||
|
// Copia píxels filtrats per progress_
|
||||||
|
for (int row = 0; row < SH; ++row) {
|
||||||
|
for (int col = 0; col < SW; ++col) {
|
||||||
|
const Uint8 COLOR = src_data->data[((SY + row) * SRC_W) + (SX + col)];
|
||||||
|
if (COLOR == TRANSPARENT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float RANK = computePixelRank(col, row, SH, direction_);
|
||||||
|
if (RANK >= progress_) {
|
||||||
|
const Uint8 OUT = (COLOR == source_color_) ? target_color_ : COLOR;
|
||||||
|
dst_data->data[((SY + row) * DST_W) + (SX + col)] = OUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_clip_ = CLIP;
|
||||||
|
needs_rebuild_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualitza animació, moviment i transició temporal
|
||||||
|
void DissolveSprite::update(float delta_time) {
|
||||||
|
const SDL_FRect OLD_CLIP = clip_;
|
||||||
|
AnimatedSprite::update(delta_time);
|
||||||
|
|
||||||
|
// Detecta canvi de frame d'animació
|
||||||
|
if (clip_.x != OLD_CLIP.x || clip_.y != OLD_CLIP.y ||
|
||||||
|
clip_.w != OLD_CLIP.w || clip_.h != OLD_CLIP.h) {
|
||||||
|
needs_rebuild_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualitza transició temporal si activa
|
||||||
|
if (transition_mode_ != TransitionMode::NONE) {
|
||||||
|
transition_elapsed_ += delta_time * 1000.0F;
|
||||||
|
const float T = std::min(transition_elapsed_ / transition_duration_, 1.0F);
|
||||||
|
progress_ = (transition_mode_ == TransitionMode::DISSOLVING) ? T : (1.0F - T);
|
||||||
|
needs_rebuild_ = true;
|
||||||
|
if (T >= 1.0F) {
|
||||||
|
transition_mode_ = TransitionMode::NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needs_rebuild_) {
|
||||||
|
rebuildDisplaySurface();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderitza: usa surface_display_ (amb color replace) si disponible
|
||||||
|
void DissolveSprite::render() {
|
||||||
|
if (!surface_display_) {
|
||||||
|
AnimatedSprite::render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
surface_display_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_, flip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estableix el progrés manualment
|
||||||
|
void DissolveSprite::setProgress(float progress) {
|
||||||
|
progress_ = std::min(std::max(progress, 0.0F), 1.0F);
|
||||||
|
needs_rebuild_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicia dissolució temporal (visible → invisible)
|
||||||
|
void DissolveSprite::startDissolve(float duration_ms, DissolveDirection dir) {
|
||||||
|
direction_ = dir;
|
||||||
|
transition_mode_ = TransitionMode::DISSOLVING;
|
||||||
|
transition_duration_ = duration_ms;
|
||||||
|
transition_elapsed_ = 0.0F;
|
||||||
|
progress_ = 0.0F;
|
||||||
|
needs_rebuild_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicia generació temporal (invisible → visible)
|
||||||
|
void DissolveSprite::startGenerate(float duration_ms, DissolveDirection dir) {
|
||||||
|
direction_ = dir;
|
||||||
|
transition_mode_ = TransitionMode::GENERATING;
|
||||||
|
transition_duration_ = duration_ms;
|
||||||
|
transition_elapsed_ = 0.0F;
|
||||||
|
progress_ = 1.0F;
|
||||||
|
needs_rebuild_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atura la transició temporal
|
||||||
|
void DissolveSprite::stopTransition() {
|
||||||
|
transition_mode_ = TransitionMode::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retorna si la transició ha acabat
|
||||||
|
auto DissolveSprite::isTransitionDone() const -> bool {
|
||||||
|
return transition_mode_ == TransitionMode::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configura substitució de color per a la reconstrucció
|
||||||
|
void DissolveSprite::setColorReplace(Uint8 source, Uint8 target) {
|
||||||
|
source_color_ = source;
|
||||||
|
target_color_ = target;
|
||||||
|
needs_rebuild_ = true;
|
||||||
|
}
|
||||||
62
source/core/rendering/sprites/dissolve_sprite.hpp
Normal file
62
source/core/rendering/sprites/dissolve_sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory> // Para shared_ptr
|
||||||
|
|
||||||
|
#include "core/rendering/sprites/animated_sprite.hpp" // Para AnimatedSprite
|
||||||
|
|
||||||
|
class Surface;
|
||||||
|
|
||||||
|
// Direcció de la dissolució
|
||||||
|
enum class DissolveDirection { NONE,
|
||||||
|
DOWN,
|
||||||
|
UP };
|
||||||
|
|
||||||
|
// Sprite que pot dissoldre's o generar-se de forma aleatòria en X mil·lisegons.
|
||||||
|
// progress_ va de 0.0 (totalment visible) a 1.0 (totalment invisible).
|
||||||
|
class DissolveSprite : public AnimatedSprite {
|
||||||
|
public:
|
||||||
|
explicit DissolveSprite(const AnimationResource& data);
|
||||||
|
DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||||
|
~DissolveSprite() override = default;
|
||||||
|
|
||||||
|
void update(float delta_time) override;
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
// Progrés manual [0.0 = totalment visible, 1.0 = totalment invisible]
|
||||||
|
void setProgress(float progress);
|
||||||
|
[[nodiscard]] auto getProgress() const -> float { return progress_; }
|
||||||
|
|
||||||
|
// Inicia una dissolució temporal (visible → invisible en duration_ms)
|
||||||
|
void startDissolve(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||||
|
|
||||||
|
// Inicia una generació temporal (invisible → visible en duration_ms)
|
||||||
|
void startGenerate(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||||
|
|
||||||
|
void stopTransition();
|
||||||
|
[[nodiscard]] auto isTransitionDone() const -> bool;
|
||||||
|
|
||||||
|
// Substitució de color: en reconstruir, substitueix source per target
|
||||||
|
void setColorReplace(Uint8 source, Uint8 target);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class TransitionMode { NONE,
|
||||||
|
DISSOLVING,
|
||||||
|
GENERATING };
|
||||||
|
|
||||||
|
std::shared_ptr<Surface> surface_display_; // Superfície amb els píxels filtrats
|
||||||
|
|
||||||
|
float progress_{0.0F}; // [0=visible, 1=invisible]
|
||||||
|
DissolveDirection direction_{DissolveDirection::NONE};
|
||||||
|
TransitionMode transition_mode_{TransitionMode::NONE};
|
||||||
|
float transition_duration_{0.0F};
|
||||||
|
float transition_elapsed_{0.0F};
|
||||||
|
SDL_FRect prev_clip_{.x = 0, .y = 0, .w = 0, .h = 0};
|
||||||
|
bool needs_rebuild_{false};
|
||||||
|
Uint8 source_color_{255}; // 255 = transparent = sense replace per defecte
|
||||||
|
Uint8 target_color_{0};
|
||||||
|
|
||||||
|
void rebuildDisplaySurface();
|
||||||
|
[[nodiscard]] static auto computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float;
|
||||||
|
};
|
||||||
103
source/core/rendering/sprites/moving_sprite.cpp
Normal file
103
source/core/rendering/sprites/moving_sprite.cpp
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#include "core/rendering/sprites/moving_sprite.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp" // Para Surface
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
|
||||||
|
: Sprite(std::move(surface), pos),
|
||||||
|
x_(pos.x),
|
||||||
|
y_(pos.y),
|
||||||
|
flip_(flip) { Sprite::pos_ = pos; }
|
||||||
|
|
||||||
|
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||||
|
: Sprite(std::move(surface), pos),
|
||||||
|
x_(pos.x),
|
||||||
|
y_(pos.y) { Sprite::pos_ = pos; }
|
||||||
|
|
||||||
|
MovingSprite::MovingSprite() { Sprite::clear(); }
|
||||||
|
|
||||||
|
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
|
||||||
|
: Sprite(std::move(surface)) { Sprite::clear(); }
|
||||||
|
|
||||||
|
// Reinicia todas las variables
|
||||||
|
void MovingSprite::clear() {
|
||||||
|
// Resetea posición
|
||||||
|
x_ = 0.0F;
|
||||||
|
y_ = 0.0F;
|
||||||
|
|
||||||
|
// Resetea velocidad
|
||||||
|
vx_ = 0.0F;
|
||||||
|
vy_ = 0.0F;
|
||||||
|
|
||||||
|
// Resetea aceleración
|
||||||
|
ax_ = 0.0F;
|
||||||
|
ay_ = 0.0F;
|
||||||
|
|
||||||
|
// Resetea flip
|
||||||
|
flip_ = SDL_FLIP_NONE;
|
||||||
|
|
||||||
|
Sprite::clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mueve el sprite (time-based)
|
||||||
|
// Nota: vx_, vy_ ahora se interpretan como pixels/segundo
|
||||||
|
// Nota: ax_, ay_ ahora se interpretan como pixels/segundo²
|
||||||
|
void MovingSprite::move(float delta_time) {
|
||||||
|
// Aplica aceleración a velocidad (time-based)
|
||||||
|
vx_ += ax_ * delta_time;
|
||||||
|
vy_ += ay_ * delta_time;
|
||||||
|
|
||||||
|
// Aplica velocidad a posición (time-based)
|
||||||
|
x_ += vx_ * delta_time;
|
||||||
|
y_ += vy_ * delta_time;
|
||||||
|
|
||||||
|
// Actualiza posición entera para renderizado
|
||||||
|
pos_.x = static_cast<int>(x_);
|
||||||
|
pos_.y = static_cast<int>(y_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza las variables internas del objeto (time-based)
|
||||||
|
void MovingSprite::update(float delta_time) {
|
||||||
|
move(delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra el sprite por pantalla
|
||||||
|
void MovingSprite::render() {
|
||||||
|
surface_->render(pos_.x, pos_.y, &clip_, flip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra el sprite por pantalla
|
||||||
|
void MovingSprite::render(Uint8 source_color, Uint8 target_color) {
|
||||||
|
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_, flip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la posición y_ el tamaño del objeto
|
||||||
|
void MovingSprite::setPos(SDL_FRect rect) {
|
||||||
|
x_ = rect.x;
|
||||||
|
y_ = rect.y;
|
||||||
|
|
||||||
|
pos_ = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de las variables
|
||||||
|
void MovingSprite::setPos(float x, float y) {
|
||||||
|
x_ = x;
|
||||||
|
y_ = y;
|
||||||
|
|
||||||
|
pos_.x = static_cast<int>(x_);
|
||||||
|
pos_.y = static_cast<int>(y_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setPosX(float value) {
|
||||||
|
x_ = value;
|
||||||
|
pos_.x = static_cast<int>(x_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setPosY(float value) {
|
||||||
|
y_ = value;
|
||||||
|
pos_.y = static_cast<int>(y_);
|
||||||
|
}
|
||||||
77
source/core/rendering/sprites/moving_sprite.hpp
Normal file
77
source/core/rendering/sprites/moving_sprite.hpp
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory> // Para shared_ptr
|
||||||
|
|
||||||
|
#include "core/rendering/sprites/sprite.hpp" // Para Sprite
|
||||||
|
class Surface; // lines 8-8
|
||||||
|
|
||||||
|
// Clase MovingSprite. Añade movimiento y flip al sprite
|
||||||
|
class MovingSprite : public Sprite {
|
||||||
|
public:
|
||||||
|
// Constructores
|
||||||
|
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip);
|
||||||
|
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||||
|
explicit MovingSprite();
|
||||||
|
explicit MovingSprite(std::shared_ptr<Surface> surface);
|
||||||
|
~MovingSprite() override = default;
|
||||||
|
|
||||||
|
// Actualización y renderizado
|
||||||
|
void update(float delta_time) override; // Actualiza variables internas (time-based)
|
||||||
|
void render() override; // Muestra el sprite por pantalla
|
||||||
|
void render(Uint8 source_color, Uint8 target_color) override; // Renderiza con reemplazo de color
|
||||||
|
|
||||||
|
// Gestión de estado
|
||||||
|
void clear() override; // Reinicia todas las variables a cero
|
||||||
|
|
||||||
|
// Getters de posición
|
||||||
|
[[nodiscard]] auto getPosX() const -> float { return x_; }
|
||||||
|
[[nodiscard]] auto getPosY() const -> float { return y_; }
|
||||||
|
|
||||||
|
// Getters de velocidad
|
||||||
|
[[nodiscard]] auto getVelX() const -> float { return vx_; }
|
||||||
|
[[nodiscard]] auto getVelY() const -> float { return vy_; }
|
||||||
|
|
||||||
|
// Getters de aceleración
|
||||||
|
[[nodiscard]] auto getAccelX() const -> float { return ax_; }
|
||||||
|
[[nodiscard]] auto getAccelY() const -> float { return ay_; }
|
||||||
|
|
||||||
|
// Setters de posición
|
||||||
|
void setPos(SDL_FRect rect); // Establece posición y tamaño del objeto
|
||||||
|
void setPos(float x, float y); // Establece posición x, y
|
||||||
|
void setPosX(float value); // Establece posición X
|
||||||
|
void setPosY(float value); // Establece posición Y
|
||||||
|
|
||||||
|
// Setters de velocidad
|
||||||
|
void setVelX(float value) { vx_ = value; }
|
||||||
|
void setVelY(float value) { vy_ = value; }
|
||||||
|
|
||||||
|
// Setters de aceleración
|
||||||
|
void setAccelX(float value) { ax_ = value; }
|
||||||
|
void setAccelY(float value) { ay_ = value; }
|
||||||
|
|
||||||
|
// Gestión de flip (volteo horizontal)
|
||||||
|
void setFlip(SDL_FlipMode flip) { flip_ = flip; } // Establece modo de flip
|
||||||
|
auto getFlip() -> SDL_FlipMode { return flip_; } // Obtiene modo de flip
|
||||||
|
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } // Alterna flip horizontal
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Métodos protegidos
|
||||||
|
void move(float delta_time); // Mueve el sprite (time-based)
|
||||||
|
|
||||||
|
// Variables miembro - Posición
|
||||||
|
float x_{0.0F}; // Posición en el eje X
|
||||||
|
float y_{0.0F}; // Posición en el eje Y
|
||||||
|
|
||||||
|
// Variables miembro - Velocidad (pixels/segundo)
|
||||||
|
float vx_{0.0F}; // Velocidad en el eje X
|
||||||
|
float vy_{0.0F}; // Velocidad en el eje Y
|
||||||
|
|
||||||
|
// Variables miembro - Aceleración (pixels/segundo²)
|
||||||
|
float ax_{0.0F}; // Aceleración en el eje X
|
||||||
|
float ay_{0.0F}; // Aceleración en el eje Y
|
||||||
|
|
||||||
|
// Variables miembro - Renderizado
|
||||||
|
SDL_FlipMode flip_{SDL_FLIP_NONE}; // Modo de volteo del sprite
|
||||||
|
};
|
||||||
76
source/core/rendering/sprites/sprite.cpp
Normal file
76
source/core/rendering/sprites/sprite.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "core/rendering/sprites/sprite.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp" // Para Surface
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Sprite::Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h)
|
||||||
|
: surface_(std::move(surface)),
|
||||||
|
pos_{.x = x, .y = y, .w = w, .h = h},
|
||||||
|
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||||
|
|
||||||
|
Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
|
||||||
|
: surface_(std::move(surface)),
|
||||||
|
pos_(rect),
|
||||||
|
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||||
|
|
||||||
|
Sprite::Sprite() = default;
|
||||||
|
|
||||||
|
Sprite::Sprite(std::shared_ptr<Surface> surface)
|
||||||
|
: surface_(std::move(surface)),
|
||||||
|
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
|
||||||
|
clip_(pos_) {}
|
||||||
|
|
||||||
|
// Muestra el sprite por pantalla
|
||||||
|
void Sprite::render() {
|
||||||
|
surface_->render(pos_.x, pos_.y, &clip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sprite::render(Uint8 source_color, Uint8 target_color) {
|
||||||
|
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height) {
|
||||||
|
surface_->renderWithVerticalFade(
|
||||||
|
static_cast<int>(pos_.x),
|
||||||
|
static_cast<int>(pos_.y),
|
||||||
|
fade_h,
|
||||||
|
canvas_height,
|
||||||
|
&clip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color) {
|
||||||
|
surface_->renderWithVerticalFade(
|
||||||
|
static_cast<int>(pos_.x),
|
||||||
|
static_cast<int>(pos_.y),
|
||||||
|
fade_h,
|
||||||
|
canvas_height,
|
||||||
|
source_color,
|
||||||
|
target_color,
|
||||||
|
&clip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la posición del objeto
|
||||||
|
void Sprite::setPosition(float x, float y) {
|
||||||
|
pos_.x = x;
|
||||||
|
pos_.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la posición del objeto
|
||||||
|
void Sprite::setPosition(SDL_FPoint p) {
|
||||||
|
pos_.x = p.x;
|
||||||
|
pos_.y = p.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinicia las variables a cero
|
||||||
|
void Sprite::clear() {
|
||||||
|
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||||
|
clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza el estado del sprite (time-based)
|
||||||
|
void Sprite::update(float delta_time) {
|
||||||
|
// Base implementation does nothing (static sprites)
|
||||||
|
(void)delta_time; // Evita warning de parámetro no usado
|
||||||
|
}
|
||||||
62
source/core/rendering/sprites/sprite.hpp
Normal file
62
source/core/rendering/sprites/sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory> // Para shared_ptr
|
||||||
|
#include <utility>
|
||||||
|
class Surface; // lines 5-5
|
||||||
|
|
||||||
|
// Clase Sprite
|
||||||
|
class Sprite {
|
||||||
|
public:
|
||||||
|
// Constructores
|
||||||
|
Sprite(std::shared_ptr<Surface>, float x, float y, float w, float h);
|
||||||
|
Sprite(std::shared_ptr<Surface>, SDL_FRect rect);
|
||||||
|
Sprite();
|
||||||
|
explicit Sprite(std::shared_ptr<Surface>);
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
virtual ~Sprite() = default;
|
||||||
|
|
||||||
|
// Actualización y renderizado
|
||||||
|
virtual void update(float delta_time); // Actualiza el estado del sprite (time-based)
|
||||||
|
virtual void render(); // Muestra el sprite por pantalla
|
||||||
|
virtual void render(Uint8 source_color, Uint8 target_color); // Renderiza con reemplazo de color
|
||||||
|
virtual void renderWithVerticalFade(int fade_h, int canvas_height); // Renderiza amb dissolució vertical (hash 2D, sense parpelleig)
|
||||||
|
virtual void renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color); // Idem amb reemplaç de color
|
||||||
|
|
||||||
|
// Gestión de estado
|
||||||
|
virtual void clear(); // Reinicia las variables a cero
|
||||||
|
|
||||||
|
// Obtención de propiedades
|
||||||
|
[[nodiscard]] auto getX() const -> float { return pos_.x; }
|
||||||
|
[[nodiscard]] auto getY() const -> float { return pos_.y; }
|
||||||
|
[[nodiscard]] auto getWidth() const -> float { return pos_.w; }
|
||||||
|
[[nodiscard]] auto getHeight() const -> float { return pos_.h; }
|
||||||
|
[[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; }
|
||||||
|
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
|
||||||
|
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
|
||||||
|
auto getRect() -> SDL_FRect& { return pos_; }
|
||||||
|
|
||||||
|
// Modificación de posición y tamaño
|
||||||
|
void setX(float x) { pos_.x = x; }
|
||||||
|
void setY(float y) { pos_.y = y; }
|
||||||
|
void setWidth(float w) { pos_.w = w; }
|
||||||
|
void setHeight(float h) { pos_.h = h; }
|
||||||
|
void setPosition(float x, float y);
|
||||||
|
void setPosition(SDL_FPoint p);
|
||||||
|
void setPosition(SDL_FRect r) { pos_ = r; }
|
||||||
|
void incX(float value) { pos_.x += value; }
|
||||||
|
void incY(float value) { pos_.y += value; }
|
||||||
|
|
||||||
|
// Modificación de clip y surface
|
||||||
|
void setClip(SDL_FRect rect) { clip_ = rect; }
|
||||||
|
void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
|
||||||
|
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Variables miembro
|
||||||
|
std::shared_ptr<Surface> surface_{nullptr}; // Surface donde estan todos los dibujos del sprite
|
||||||
|
SDL_FRect pos_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Posición y tamaño donde dibujar el sprite
|
||||||
|
SDL_FRect clip_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Rectangulo de origen de la surface que se dibujará en pantalla
|
||||||
|
};
|
||||||
679
source/core/rendering/surface.cpp
Normal file
679
source/core/rendering/surface.cpp
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
// IWYU pragma: no_include <bits/std_abs.h>
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // Para min, max, copy_n, fill
|
||||||
|
#include <cmath> // Para abs
|
||||||
|
#include <cstdint> // Para uint32_t
|
||||||
|
#include <cstring> // Para memcpy, size_t
|
||||||
|
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ist...
|
||||||
|
#include <iostream> // Para cerr
|
||||||
|
#include <memory> // Para shared_ptr, __shared_ptr_access, default...
|
||||||
|
#include <sstream> // Para basic_istringstream
|
||||||
|
#include <stdexcept> // Para runtime_error
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
#include "core/rendering/gif.hpp"
|
||||||
|
#include "utils/utils.hpp"
|
||||||
|
#include "core/rendering/screen.hpp" // Para Screen
|
||||||
|
|
||||||
|
|
||||||
|
// Carga una paleta desde un archivo .gif
|
||||||
|
auto loadPalette(const std::string& file_path) -> Palette {
|
||||||
|
// Carga el fichero desde el sistema de ficheros
|
||||||
|
auto buffer = loadFileBytes(file_path);
|
||||||
|
if (buffer.empty()) {
|
||||||
|
throw std::runtime_error("Error opening file: " + file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar la paleta usando los datos del buffer
|
||||||
|
std::vector<uint32_t> pal = GIF::Gif::loadPalette(buffer.data());
|
||||||
|
if (pal.empty()) {
|
||||||
|
throw std::runtime_error("No palette found in GIF file: " + file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear la paleta y copiar los datos desde 'pal'
|
||||||
|
Palette palette = {}; // Inicializa la paleta con ceros
|
||||||
|
std::copy_n(pal.begin(), std::min(pal.size(), palette.size()), palette.begin());
|
||||||
|
|
||||||
|
// Mensaje de depuración
|
||||||
|
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||||
|
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga una paleta desde un archivo .pal
|
||||||
|
auto readPalFile(const std::string& file_path) -> Palette {
|
||||||
|
Palette palette{};
|
||||||
|
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)
|
||||||
|
|
||||||
|
// Carga el fichero desde el sistema de ficheros
|
||||||
|
auto file_data = loadFileBytes(file_path);
|
||||||
|
if (file_data.empty()) {
|
||||||
|
throw std::runtime_error("No se pudo abrir el archivo .pal: " + file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert bytes to string for parsing
|
||||||
|
std::string content(file_data.begin(), file_data.end());
|
||||||
|
std::istringstream stream(content);
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
int line_number = 0;
|
||||||
|
int color_index = 0;
|
||||||
|
|
||||||
|
while (std::getline(stream, line)) {
|
||||||
|
++line_number;
|
||||||
|
|
||||||
|
// Ignorar las tres primeras líneas del archivo
|
||||||
|
if (line_number <= 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesar las líneas restantes con valores RGB
|
||||||
|
std::istringstream ss(line);
|
||||||
|
int r;
|
||||||
|
int g;
|
||||||
|
int b;
|
||||||
|
if (ss >> r >> g >> b) {
|
||||||
|
// Construir el color ARGB (A = 255 por defecto)
|
||||||
|
Uint32 color = (255 << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
palette[color_index++] = color;
|
||||||
|
|
||||||
|
// Limitar a un máximo de 256 colores (opcional)
|
||||||
|
if (color_index >= 256) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Surface::Surface(int w, int h)
|
||||||
|
: surface_data_(std::make_shared<SurfaceData>(w, h)),
|
||||||
|
transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) { initializeSubPalette(sub_palette_); }
|
||||||
|
|
||||||
|
Surface::Surface(const std::string& file_path)
|
||||||
|
: transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) {
|
||||||
|
SurfaceData loaded_data = loadSurface(file_path);
|
||||||
|
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded_data));
|
||||||
|
|
||||||
|
initializeSubPalette(sub_palette_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga una superficie desde un archivo
|
||||||
|
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Carga el fichero desde el sistema de ficheros
|
||||||
|
std::vector<Uint8> buffer = loadFileBytes(file_path);
|
||||||
|
if (buffer.empty()) {
|
||||||
|
std::cerr << "Error opening file: " << file_path << '\n';
|
||||||
|
throw std::runtime_error("Error opening file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear un objeto Gif y llamar a la función loadGif
|
||||||
|
Uint16 w = 0;
|
||||||
|
Uint16 h = 0;
|
||||||
|
std::vector<Uint8> raw_pixels = GIF::Gif::loadGif(buffer.data(), w, h);
|
||||||
|
if (raw_pixels.empty()) {
|
||||||
|
std::cerr << "Error loading GIF from file: " << file_path << '\n';
|
||||||
|
throw std::runtime_error("Error loading GIF");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>,
|
||||||
|
// reservamos un bloque dinámico y copiamos los datos del vector.
|
||||||
|
size_t pixel_count = raw_pixels.size();
|
||||||
|
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>());
|
||||||
|
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
|
||||||
|
|
||||||
|
// Crear y devolver directamente el objeto SurfaceData
|
||||||
|
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||||
|
return {static_cast<float>(w), static_cast<float>(h), pixels};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga una paleta desde un archivo
|
||||||
|
void Surface::loadPalette(const std::string& file_path) {
|
||||||
|
palette_ = ::loadPalette(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga una paleta desde otra paleta
|
||||||
|
void Surface::loadPalette(const Palette& palette) {
|
||||||
|
palette_ = palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece un color en la paleta
|
||||||
|
void Surface::setColor(int index, Uint32 color) {
|
||||||
|
palette_.at(index) = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rellena la superficie con un color
|
||||||
|
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
|
||||||
|
Uint8* data_ptr = surface_data_->data.get();
|
||||||
|
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pone un pixel en la SurfaceData
|
||||||
|
void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||||
|
return; // Coordenadas fuera de rango
|
||||||
|
}
|
||||||
|
|
||||||
|
const int INDEX = x + (y * surface_data_->width);
|
||||||
|
surface_data_->data.get()[INDEX] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el color de un pixel de la surface_data
|
||||||
|
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
|
||||||
|
|
||||||
|
// Dibuja un rectangulo relleno
|
||||||
|
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||||
|
float x_start = std::max(0.0F, rect->x);
|
||||||
|
float y_start = std::max(0.0F, rect->y);
|
||||||
|
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||||
|
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||||
|
|
||||||
|
// Recorrer cada píxel dentro del rectángulo directamente
|
||||||
|
for (int y = y_start; y < y_end; ++y) {
|
||||||
|
for (int x = x_start; x < x_end; ++x) {
|
||||||
|
const int INDEX = x + (y * surface_data_->width);
|
||||||
|
surface_data_->data.get()[INDEX] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuja el borde de un rectangulo
|
||||||
|
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||||
|
float x_start = std::max(0.0F, rect->x);
|
||||||
|
float y_start = std::max(0.0F, rect->y);
|
||||||
|
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||||
|
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||||
|
|
||||||
|
// Dibujar bordes horizontales
|
||||||
|
for (int x = x_start; x < x_end; ++x) {
|
||||||
|
// Borde superior
|
||||||
|
const int TOP_INDEX = x + (y_start * surface_data_->width);
|
||||||
|
surface_data_->data.get()[TOP_INDEX] = color;
|
||||||
|
|
||||||
|
// Borde inferior
|
||||||
|
const int BOTTOM_INDEX = x + ((y_end - 1) * surface_data_->width);
|
||||||
|
surface_data_->data.get()[BOTTOM_INDEX] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibujar bordes verticales
|
||||||
|
for (int y = y_start; y < y_end; ++y) {
|
||||||
|
// Borde izquierdo
|
||||||
|
const int LEFT_INDEX = x_start + (y * surface_data_->width);
|
||||||
|
surface_data_->data.get()[LEFT_INDEX] = color;
|
||||||
|
|
||||||
|
// Borde derecho
|
||||||
|
const int RIGHT_INDEX = (x_end - 1) + (y * surface_data_->width);
|
||||||
|
surface_data_->data.get()[RIGHT_INDEX] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuja una linea
|
||||||
|
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Calcula las diferencias
|
||||||
|
float dx = std::abs(x2 - x1);
|
||||||
|
float dy = std::abs(y2 - y1);
|
||||||
|
|
||||||
|
// Determina la dirección del incremento
|
||||||
|
float sx = (x1 < x2) ? 1 : -1;
|
||||||
|
float sy = (y1 < y2) ? 1 : -1;
|
||||||
|
|
||||||
|
float err = dx - dy;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Asegúrate de no dibujar fuera de los límites de la superficie
|
||||||
|
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
|
||||||
|
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si alcanzamos el punto final, salimos
|
||||||
|
if (x1 == x2 && y1 == y2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int e2 = 2 * err;
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy;
|
||||||
|
x1 += sx;
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
y1 += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
|
||||||
|
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
|
// Limitar la región para evitar accesos fuera de rango en origen
|
||||||
|
w = std::min(w, surface_data_->width - sx);
|
||||||
|
h = std::min(h, surface_data_->height - sy);
|
||||||
|
|
||||||
|
// Limitar la región para evitar accesos fuera de rango en destino
|
||||||
|
w = std::min(w, surface_data->width - dx);
|
||||||
|
h = std::min(h, surface_data->height - dy);
|
||||||
|
|
||||||
|
for (int iy = 0; iy < h; ++iy) {
|
||||||
|
for (int ix = 0; ix < w; ++ix) {
|
||||||
|
// Verificar que las coordenadas de destino están dentro de los límites
|
||||||
|
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
|
||||||
|
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
|
||||||
|
int src_x = sx + ix;
|
||||||
|
int src_y = sy + iy;
|
||||||
|
|
||||||
|
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||||
|
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||||
|
surface_data->data.get()[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
|
||||||
|
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
|
// Determina la región de origen (clip) a renderizar
|
||||||
|
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||||
|
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||||
|
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||||
|
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||||
|
|
||||||
|
// Limitar la región para evitar accesos fuera de rango en origen
|
||||||
|
w = std::min(w, surface_data_->width - sx);
|
||||||
|
h = std::min(h, surface_data_->height - sy);
|
||||||
|
w = std::min(w, surface_data_dest->width - x);
|
||||||
|
h = std::min(h, surface_data_dest->height - y);
|
||||||
|
|
||||||
|
// Limitar la región para evitar accesos fuera de rango en destino
|
||||||
|
w = std::min(w, surface_data_dest->width - x);
|
||||||
|
h = std::min(h, surface_data_dest->height - y);
|
||||||
|
|
||||||
|
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||||
|
for (int iy = 0; iy < h; ++iy) {
|
||||||
|
for (int ix = 0; ix < w; ++ix) {
|
||||||
|
// Coordenadas de origen
|
||||||
|
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||||
|
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||||
|
|
||||||
|
// Coordenadas de destino
|
||||||
|
int dest_x = x + ix;
|
||||||
|
int dest_y = y + iy;
|
||||||
|
|
||||||
|
// Verificar que las coordenadas de destino están dentro de los límites
|
||||||
|
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
|
||||||
|
// Copia el píxel si no es transparente
|
||||||
|
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||||
|
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||||
|
surface_data_dest->data[dest_x + (dest_y * surface_data_dest->width)] = sub_palette_[color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper para calcular coordenadas con flip
|
||||||
|
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
|
||||||
|
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||||
|
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper para copiar un pixel si no es transparente
|
||||||
|
void Surface::copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const {
|
||||||
|
if (dest_x < 0 || dest_y < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||||
|
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||||
|
dest_data[dest_x + (dest_y * dest_width)] = sub_palette_[color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia una región de la superficie de origen a la de destino
|
||||||
|
void Surface::render(SDL_FRect* src_rect, SDL_FRect* dst_rect, SDL_FlipMode flip) {
|
||||||
|
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
|
// Si srcRect es nullptr, tomar toda la superficie fuente
|
||||||
|
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||||
|
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||||
|
float sw = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||||
|
float sh = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||||
|
|
||||||
|
// Si dstRect es nullptr, asignar las mismas dimensiones que srcRect
|
||||||
|
float dx = (dst_rect != nullptr) ? dst_rect->x : 0;
|
||||||
|
float dy = (dst_rect != nullptr) ? dst_rect->y : 0;
|
||||||
|
float dw = (dst_rect != nullptr) ? dst_rect->w : sw;
|
||||||
|
float dh = (dst_rect != nullptr) ? dst_rect->h : sh;
|
||||||
|
|
||||||
|
// Asegurarse de que srcRect y dstRect tienen las mismas dimensiones
|
||||||
|
if (sw != dw || sh != dh) {
|
||||||
|
dw = sw; // Respetar las dimensiones de srcRect
|
||||||
|
dh = sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limitar la región para evitar accesos fuera de rango en src y dst
|
||||||
|
sw = std::min(sw, surface_data_->width - sx);
|
||||||
|
sh = std::min(sh, surface_data_->height - sy);
|
||||||
|
dw = std::min(dw, surface_data->width - dx);
|
||||||
|
dh = std::min(dh, surface_data->height - dy);
|
||||||
|
|
||||||
|
int final_width = std::min(sw, dw);
|
||||||
|
int final_height = std::min(sh, dh);
|
||||||
|
|
||||||
|
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||||
|
for (int iy = 0; iy < final_height; ++iy) {
|
||||||
|
for (int ix = 0; ix < final_width; ++ix) {
|
||||||
|
int src_x = 0;
|
||||||
|
int src_y = 0;
|
||||||
|
calculateFlippedCoords(ix, iy, sx, sy, final_width, final_height, flip, src_x, src_y);
|
||||||
|
|
||||||
|
int dest_x = dx + ix;
|
||||||
|
int dest_y = dy + iy;
|
||||||
|
|
||||||
|
// Verificar límites de destino antes de copiar
|
||||||
|
if (dest_x >= 0 && dest_x < surface_data->width && dest_y >= 0 && dest_y < surface_data->height) {
|
||||||
|
copyPixelIfNotTransparent(surface_data->data.get(), dest_x, dest_y, surface_data->width, src_x, src_y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||||
|
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect, SDL_FlipMode flip) const {
|
||||||
|
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
|
// Determina la región de origen (clip) a renderizar
|
||||||
|
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||||
|
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||||
|
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||||
|
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||||
|
|
||||||
|
// Limitar la región para evitar accesos fuera de rango
|
||||||
|
w = std::min(w, surface_data_->width - sx);
|
||||||
|
h = std::min(h, surface_data_->height - sy);
|
||||||
|
|
||||||
|
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||||
|
for (int iy = 0; iy < h; ++iy) {
|
||||||
|
for (int ix = 0; ix < w; ++ix) {
|
||||||
|
// Coordenadas de origen
|
||||||
|
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||||
|
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||||
|
|
||||||
|
// Coordenadas de destino
|
||||||
|
int dest_x = x + ix;
|
||||||
|
int dest_y = y + iy;
|
||||||
|
|
||||||
|
// Verifica que las coordenadas de destino estén dentro de los límites
|
||||||
|
if (dest_x < 0 || dest_y < 0 || dest_x >= surface_data->width || dest_y >= surface_data->height) {
|
||||||
|
continue; // Saltar píxeles fuera del rango del destino
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copia el píxel si no es transparente
|
||||||
|
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||||
|
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||||
|
surface_data->data[dest_x + (dest_y * surface_data->width)] =
|
||||||
|
(color == source_color) ? target_color : color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash 2D estable per a dithering sense flickering
|
||||||
|
static auto pixelThreshold(int col, int row) -> float {
|
||||||
|
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||||
|
h ^= (h >> 13);
|
||||||
|
h *= 1274126177U;
|
||||||
|
h ^= (h >> 16);
|
||||||
|
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula la densidad de fade para un pixel en posición screen_y
|
||||||
|
static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> float {
|
||||||
|
if (screen_y < fade_h) {
|
||||||
|
return static_cast<float>(fade_h - screen_y) / static_cast<float>(fade_h);
|
||||||
|
}
|
||||||
|
if (screen_y >= canvas_height - fade_h) {
|
||||||
|
return static_cast<float>(screen_y - (canvas_height - fade_h)) / static_cast<float>(fade_h);
|
||||||
|
}
|
||||||
|
return 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||||
|
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
|
||||||
|
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||||
|
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||||
|
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||||
|
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||||
|
|
||||||
|
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
|
for (int row = 0; row < SH; row++) {
|
||||||
|
const int SCREEN_Y = y + row;
|
||||||
|
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||||
|
|
||||||
|
for (int col = 0; col < SW; col++) {
|
||||||
|
const int SCREEN_X = x + col;
|
||||||
|
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||||
|
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixelThreshold(col, row) < DENSITY) {
|
||||||
|
continue; // Pixel tapat per la zona de fade
|
||||||
|
}
|
||||||
|
|
||||||
|
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idem però reemplaçant un color índex
|
||||||
|
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
|
||||||
|
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||||
|
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||||
|
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||||
|
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||||
|
|
||||||
|
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||||
|
|
||||||
|
for (int row = 0; row < SH; row++) {
|
||||||
|
const int SCREEN_Y = y + row;
|
||||||
|
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||||
|
|
||||||
|
for (int col = 0; col < SW; col++) {
|
||||||
|
const int SCREEN_X = x + col;
|
||||||
|
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||||
|
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixelThreshold(col, row) < DENSITY) {
|
||||||
|
continue; // Pixel tapat per la zona de fade
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
|
||||||
|
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture ni SDL_Renderer)
|
||||||
|
void Surface::toARGBBuffer(Uint32* buffer) const { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if (!surface_data_ || (surface_data_->data == nullptr)) { return; }
|
||||||
|
const int WIDTH = static_cast<int>(surface_data_->width);
|
||||||
|
const int HEIGHT = static_cast<int>(surface_data_->height);
|
||||||
|
const Uint8* src = surface_data_->data.get();
|
||||||
|
for (int y = 0; y < HEIGHT; ++y) {
|
||||||
|
for (int x = 0; x < WIDTH; ++x) {
|
||||||
|
buffer[(y * WIDTH) + x] = palette_[src[(y * WIDTH) + x]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vuelca la superficie a una textura
|
||||||
|
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||||
|
throw std::runtime_error("Renderer or texture is null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||||
|
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32* pixels = nullptr;
|
||||||
|
int pitch = 0;
|
||||||
|
|
||||||
|
// Bloquea la textura para modificar los píxeles directamente
|
||||||
|
if (!SDL_LockTexture(texture, nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||||
|
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware)
|
||||||
|
int row_stride = pitch / sizeof(Uint32);
|
||||||
|
|
||||||
|
for (int y = 0; y < surface_data_->height; ++y) {
|
||||||
|
for (int x = 0; x < surface_data_->width; ++x) {
|
||||||
|
// Calcular la posición correcta en la textura teniendo en cuenta el stride
|
||||||
|
int texture_index = (y * row_stride) + x;
|
||||||
|
int surface_index = (y * surface_data_->width) + x;
|
||||||
|
|
||||||
|
pixels[texture_index] = palette_[surface_data_->data.get()[surface_index]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UnlockTexture(texture); // Desbloquea la textura
|
||||||
|
|
||||||
|
// Renderiza la textura en la pantalla completa
|
||||||
|
if (!SDL_RenderTexture(renderer, texture, nullptr, nullptr)) {
|
||||||
|
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vuelca la superficie a una textura
|
||||||
|
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||||
|
throw std::runtime_error("Renderer or texture is null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||||
|
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32* pixels = nullptr;
|
||||||
|
int pitch = 0;
|
||||||
|
|
||||||
|
SDL_Rect lock_rect;
|
||||||
|
if (dest_rect != nullptr) {
|
||||||
|
lock_rect.x = static_cast<int>(dest_rect->x);
|
||||||
|
lock_rect.y = static_cast<int>(dest_rect->y);
|
||||||
|
lock_rect.w = static_cast<int>(dest_rect->w);
|
||||||
|
lock_rect.h = static_cast<int>(dest_rect->h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usa lockRect solo si destRect no es nulo
|
||||||
|
if (!SDL_LockTexture(texture, (dest_rect != nullptr) ? &lock_rect : nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||||
|
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int row_stride = pitch / sizeof(Uint32);
|
||||||
|
|
||||||
|
for (int y = 0; y < surface_data_->height; ++y) {
|
||||||
|
for (int x = 0; x < surface_data_->width; ++x) {
|
||||||
|
int texture_index = (y * row_stride) + x;
|
||||||
|
int surface_index = (y * surface_data_->width) + x;
|
||||||
|
|
||||||
|
pixels[texture_index] = palette_[surface_data_->data.get()[surface_index]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UnlockTexture(texture);
|
||||||
|
|
||||||
|
// Renderiza la textura con los rectángulos especificados
|
||||||
|
if (!SDL_RenderTexture(renderer, texture, src_rect, dest_rect)) {
|
||||||
|
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Realiza un efecto de fundido en la paleta principal
|
||||||
|
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Verificar que el tamaño mínimo de palette_ sea adecuado
|
||||||
|
static constexpr int PALETTE_SIZE = 19;
|
||||||
|
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
|
||||||
|
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desplazar colores (pares e impares)
|
||||||
|
for (int i = 18; i > 1; --i) {
|
||||||
|
palette_[i] = palette_[i - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajustar el primer color
|
||||||
|
palette_[1] = palette_[0];
|
||||||
|
|
||||||
|
// Devolver si el índice 15 coincide con el índice 0
|
||||||
|
return palette_[15] == palette_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Realiza un efecto de fundido en la paleta secundaria
|
||||||
|
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
// Variable estática para almacenar el último tick
|
||||||
|
static Uint32 last_tick_ = 0;
|
||||||
|
|
||||||
|
// Obtener el tiempo actual
|
||||||
|
Uint32 current_tick = SDL_GetTicks();
|
||||||
|
|
||||||
|
// Verificar si ha pasado el tiempo de retardo
|
||||||
|
if (current_tick - last_tick_ < delay) {
|
||||||
|
return false; // No se realiza el fade
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar el último tick
|
||||||
|
last_tick_ = current_tick;
|
||||||
|
|
||||||
|
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
|
||||||
|
static constexpr int SUB_PALETTE_SIZE = 19;
|
||||||
|
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
|
||||||
|
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desplazar colores (pares e impares)
|
||||||
|
for (int i = 18; i > 1; --i) {
|
||||||
|
sub_palette_[i] = sub_palette_[i - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajustar el primer color
|
||||||
|
sub_palette_[1] = sub_palette_[0];
|
||||||
|
|
||||||
|
// Devolver si el índice 15 coincide con el índice 0
|
||||||
|
return sub_palette_[15] == sub_palette_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restaura la sub paleta a su estado original
|
||||||
|
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
|
||||||
152
source/core/rendering/surface.hpp
Normal file
152
source/core/rendering/surface.hpp
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array> // Para array
|
||||||
|
#include <memory> // Para default_delete, shared_ptr, __shared_pt...
|
||||||
|
#include <numeric> // Para iota
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <utility> // Para move
|
||||||
|
|
||||||
|
#include "utils/utils.hpp" // Para PaletteColor
|
||||||
|
|
||||||
|
// Alias
|
||||||
|
using Palette = std::array<Uint32, 256>;
|
||||||
|
using SubPalette = std::array<Uint8, 256>;
|
||||||
|
|
||||||
|
// Carga una paleta desde un archivo .gif
|
||||||
|
auto loadPalette(const std::string& file_path) -> Palette;
|
||||||
|
|
||||||
|
// Carga una paleta desde un archivo .pal
|
||||||
|
auto readPalFile(const std::string& file_path) -> Palette;
|
||||||
|
|
||||||
|
struct SurfaceData {
|
||||||
|
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
|
||||||
|
float width; // Ancho de la imagen
|
||||||
|
float height; // Alto de la imagen
|
||||||
|
|
||||||
|
// Constructor por defecto
|
||||||
|
SurfaceData()
|
||||||
|
: data(nullptr),
|
||||||
|
width(0),
|
||||||
|
height(0) {}
|
||||||
|
|
||||||
|
// Constructor que inicializa dimensiones y asigna memoria
|
||||||
|
SurfaceData(float w, float h)
|
||||||
|
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
|
||||||
|
width(w),
|
||||||
|
height(h) {}
|
||||||
|
|
||||||
|
// Constructor para inicializar directamente con datos
|
||||||
|
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
|
||||||
|
: data(std::move(pixels)),
|
||||||
|
width(w),
|
||||||
|
height(h) {}
|
||||||
|
|
||||||
|
// Constructor de movimiento
|
||||||
|
SurfaceData(SurfaceData&& other) noexcept = default;
|
||||||
|
|
||||||
|
// Operador de movimiento
|
||||||
|
auto operator=(SurfaceData&& other) noexcept -> SurfaceData& = default;
|
||||||
|
|
||||||
|
// Evita copias accidentales
|
||||||
|
SurfaceData(const SurfaceData&) = delete;
|
||||||
|
auto operator=(const SurfaceData&) -> SurfaceData& = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Surface {
|
||||||
|
private:
|
||||||
|
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
|
||||||
|
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
|
||||||
|
SubPalette sub_palette_; // Paleta para reindexar colores
|
||||||
|
int transparent_color_; // Indice de la paleta que se omite en la copia de datos
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
Surface(int w, int h);
|
||||||
|
explicit Surface(const std::string& file_path);
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
~Surface() = default;
|
||||||
|
|
||||||
|
// Carga una SurfaceData desde un archivo
|
||||||
|
static auto loadSurface(const std::string& file_path) -> SurfaceData;
|
||||||
|
|
||||||
|
// Carga una paleta desde un archivo
|
||||||
|
void loadPalette(const std::string& file_path);
|
||||||
|
void loadPalette(const Palette& palette);
|
||||||
|
|
||||||
|
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
|
||||||
|
void render(float dx, float dy, float sx, float sy, float w, float h);
|
||||||
|
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||||
|
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||||
|
|
||||||
|
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||||
|
void renderWithColorReplace(int x, int y, Uint8 source_color = 0, Uint8 target_color = 0, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||||
|
|
||||||
|
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||||
|
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect = nullptr) const;
|
||||||
|
|
||||||
|
// Idem però reemplaçant un color índex (per a sprites sobre fons del mateix color)
|
||||||
|
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect = nullptr) const;
|
||||||
|
|
||||||
|
// Establece un color en la paleta
|
||||||
|
void setColor(int index, Uint32 color);
|
||||||
|
|
||||||
|
// Rellena la SurfaceData con un color
|
||||||
|
void clear(Uint8 color);
|
||||||
|
|
||||||
|
// Vuelca la SurfaceData a una textura
|
||||||
|
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture);
|
||||||
|
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect);
|
||||||
|
|
||||||
|
// Realiza un efecto de fundido en las paletas
|
||||||
|
auto fadePalette() -> bool;
|
||||||
|
auto fadeSubPalette(Uint32 delay = 0) -> bool;
|
||||||
|
|
||||||
|
// Restaura la sub paleta a su estado original
|
||||||
|
void resetSubPalette();
|
||||||
|
|
||||||
|
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture)
|
||||||
|
void toARGBBuffer(Uint32* buffer) const;
|
||||||
|
|
||||||
|
// Pone un pixel en la SurfaceData
|
||||||
|
void putPixel(int x, int y, Uint8 color);
|
||||||
|
|
||||||
|
// Obtiene el color de un pixel de la surface_data
|
||||||
|
auto getPixel(int x, int y) -> Uint8;
|
||||||
|
|
||||||
|
// Dibuja un rectangulo relleno
|
||||||
|
void fillRect(const SDL_FRect* rect, Uint8 color);
|
||||||
|
|
||||||
|
// Dibuja el borde de un rectangulo
|
||||||
|
void drawRectBorder(const SDL_FRect* rect, Uint8 color);
|
||||||
|
|
||||||
|
// Dibuja una linea
|
||||||
|
void drawLine(float x1, float y1, float x2, float y2, Uint8 color);
|
||||||
|
|
||||||
|
// Metodos para gestionar surface_data_
|
||||||
|
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
|
||||||
|
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
|
||||||
|
|
||||||
|
// Obtien ancho y alto
|
||||||
|
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
|
||||||
|
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
|
||||||
|
|
||||||
|
// Color transparente
|
||||||
|
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
|
||||||
|
void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; }
|
||||||
|
|
||||||
|
// Paleta
|
||||||
|
void setPalette(const std::array<Uint32, 256>& palette) { palette_ = palette; }
|
||||||
|
|
||||||
|
// Inicializa la sub paleta
|
||||||
|
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Helper para calcular coordenadas con flip
|
||||||
|
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
|
||||||
|
|
||||||
|
// Helper para copiar un pixel si no es transparente
|
||||||
|
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;
|
||||||
|
};
|
||||||
311
source/core/rendering/text.cpp
Normal file
311
source/core/rendering/text.cpp
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
#include "core/rendering/text.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstddef> // Para size_t
|
||||||
|
#include <iostream> // Para cerr
|
||||||
|
#include <sstream> // Para istringstream
|
||||||
|
#include <stdexcept> // Para runtime_error
|
||||||
|
|
||||||
|
#include "core/rendering/screen.hpp" // Para Screen
|
||||||
|
#include "core/rendering/surface.hpp" // Para Surface
|
||||||
|
#include "core/rendering/sprites/sprite.hpp" // Para Sprite
|
||||||
|
|
||||||
|
#include "utils/utils.hpp" // Para getFileName, stringToColor, printWithDots
|
||||||
|
|
||||||
|
// Extrae el siguiente codepoint UTF-8 de la cadena, avanzando 'pos' al byte siguiente
|
||||||
|
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto c = static_cast<unsigned char>(s[pos]);
|
||||||
|
uint32_t cp = 0;
|
||||||
|
size_t extra = 0;
|
||||||
|
|
||||||
|
if (c < 0x80) {
|
||||||
|
cp = c;
|
||||||
|
extra = 0;
|
||||||
|
} else if (c < 0xC0) {
|
||||||
|
pos++;
|
||||||
|
return 0xFFFD;
|
||||||
|
} // byte de continuación suelto
|
||||||
|
else if (c < 0xE0) {
|
||||||
|
cp = c & 0x1F;
|
||||||
|
extra = 1;
|
||||||
|
} else if (c < 0xF0) {
|
||||||
|
cp = c & 0x0F;
|
||||||
|
extra = 2;
|
||||||
|
} else if (c < 0xF8) {
|
||||||
|
cp = c & 0x07;
|
||||||
|
extra = 3;
|
||||||
|
} else {
|
||||||
|
pos++;
|
||||||
|
return 0xFFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++;
|
||||||
|
for (size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
||||||
|
auto cb = static_cast<unsigned char>(s[pos]);
|
||||||
|
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
||||||
|
cp = (cp << 6) | (cb & 0x3F);
|
||||||
|
}
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte un codepoint Unicode a una cadena UTF-8
|
||||||
|
auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
std::string result;
|
||||||
|
if (cp < 0x80) {
|
||||||
|
result += static_cast<char>(cp);
|
||||||
|
} else if (cp < 0x800) {
|
||||||
|
result += static_cast<char>(0xC0 | (cp >> 6));
|
||||||
|
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||||
|
} else if (cp < 0x10000) {
|
||||||
|
result += static_cast<char>(0xE0 | (cp >> 12));
|
||||||
|
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||||
|
} else {
|
||||||
|
result += static_cast<char>(0xF0 | (cp >> 18));
|
||||||
|
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga un fichero de definición de fuente .fnt
|
||||||
|
// Formato: líneas "clave valor", comentarios con #, gliphos como "codepoint ancho"
|
||||||
|
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto tf = std::make_shared<File>();
|
||||||
|
|
||||||
|
auto file_data = loadFileBytes(file_path);
|
||||||
|
if (file_data.empty()) {
|
||||||
|
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
|
||||||
|
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content(file_data.begin(), file_data.end());
|
||||||
|
std::istringstream stream(content);
|
||||||
|
std::string line;
|
||||||
|
int glyph_index = 0;
|
||||||
|
|
||||||
|
while (std::getline(stream, line)) {
|
||||||
|
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||||
|
if (line.empty() || line[0] == '#') { continue; }
|
||||||
|
|
||||||
|
std::istringstream ls(line);
|
||||||
|
std::string key;
|
||||||
|
ls >> key;
|
||||||
|
|
||||||
|
if (key == "box_width") {
|
||||||
|
ls >> tf->box_width;
|
||||||
|
} else if (key == "box_height") {
|
||||||
|
ls >> tf->box_height;
|
||||||
|
} else if (key == "columns") {
|
||||||
|
ls >> tf->columns;
|
||||||
|
} else if (key == "cell_spacing") {
|
||||||
|
ls >> tf->cell_spacing;
|
||||||
|
} else if (key == "row_spacing") {
|
||||||
|
ls >> tf->row_spacing;
|
||||||
|
} else {
|
||||||
|
// Línea de glifo: codepoint_decimal ancho_visual
|
||||||
|
uint32_t codepoint = 0;
|
||||||
|
int width = 0;
|
||||||
|
try {
|
||||||
|
codepoint = static_cast<uint32_t>(std::stoul(key));
|
||||||
|
ls >> width;
|
||||||
|
} catch (...) {
|
||||||
|
continue; // línea mal formateada, ignorar
|
||||||
|
}
|
||||||
|
Offset off{};
|
||||||
|
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
||||||
|
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
||||||
|
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
||||||
|
off.w = width;
|
||||||
|
tf->offset[codepoint] = off;
|
||||||
|
++glyph_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor desde fichero
|
||||||
|
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
||||||
|
auto tf = loadTextFile(text_file);
|
||||||
|
|
||||||
|
box_height_ = tf->box_height;
|
||||||
|
box_width_ = tf->box_width;
|
||||||
|
offset_ = tf->offset;
|
||||||
|
|
||||||
|
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor desde estructura precargada
|
||||||
|
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
||||||
|
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
||||||
|
box_width_(text_file->box_width),
|
||||||
|
box_height_(text_file->box_height),
|
||||||
|
offset_(text_file->offset) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe texto en pantalla
|
||||||
|
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
int shift = 0;
|
||||||
|
int glyphs_done = 0;
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
sprite_->setY(y);
|
||||||
|
while (pos < text.size()) {
|
||||||
|
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||||
|
uint32_t cp = nextCodepoint(text, pos);
|
||||||
|
auto it = offset_.find(cp);
|
||||||
|
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||||
|
if (it != offset_.end()) {
|
||||||
|
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||||
|
sprite_->setX(x + shift);
|
||||||
|
sprite_->render(1, 15);
|
||||||
|
shift += it->second.w + kerning;
|
||||||
|
}
|
||||||
|
++glyphs_done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto en una surface
|
||||||
|
auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||||
|
auto width = length(text, kerning) * zoom;
|
||||||
|
auto height = box_height_ * zoom;
|
||||||
|
auto surface = std::make_shared<Surface>(width, height);
|
||||||
|
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||||
|
Screen::get()->setRendererSurface(surface);
|
||||||
|
surface->clear(stringToColor("transparent"));
|
||||||
|
write(0, 0, text, kerning);
|
||||||
|
Screen::get()->setRendererSurface(previuos_renderer);
|
||||||
|
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto con extras en una surface
|
||||||
|
auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||||
|
auto width = Text::length(text, kerning) + shadow_distance;
|
||||||
|
auto height = box_height_ + shadow_distance;
|
||||||
|
auto surface = std::make_shared<Surface>(width, height);
|
||||||
|
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||||
|
Screen::get()->setRendererSurface(surface);
|
||||||
|
surface->clear(stringToColor("transparent"));
|
||||||
|
writeDX(flags, 0, 0, text, kerning, text_color, shadow_distance, shadow_color, lenght);
|
||||||
|
Screen::get()->setRendererSurface(previuos_renderer);
|
||||||
|
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto con colores
|
||||||
|
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
int shift = 0;
|
||||||
|
int glyphs_done = 0;
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
sprite_->setY(y);
|
||||||
|
while (pos < text.size()) {
|
||||||
|
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||||
|
uint32_t cp = nextCodepoint(text, pos);
|
||||||
|
auto it = offset_.find(cp);
|
||||||
|
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||||
|
if (it != offset_.end()) {
|
||||||
|
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||||
|
sprite_->setX(x + shift);
|
||||||
|
sprite_->render(1, color);
|
||||||
|
shift += it->second.w + kerning;
|
||||||
|
}
|
||||||
|
++glyphs_done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto con sombra
|
||||||
|
void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int lenght) {
|
||||||
|
writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, lenght);
|
||||||
|
write(x, y, text, kerning, lenght);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto centrado en un punto x
|
||||||
|
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int lenght) {
|
||||||
|
x -= (Text::length(text, kerning) / 2);
|
||||||
|
write(x, y, text, kerning, lenght);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe texto con extras
|
||||||
|
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
const auto CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
||||||
|
const auto SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||||
|
const auto COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||||
|
const auto STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||||
|
|
||||||
|
if (CENTERED) {
|
||||||
|
x -= (Text::length(text, kerning) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SHADOWED) {
|
||||||
|
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (STROKED) {
|
||||||
|
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||||
|
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||||
|
for (int dy = -dist; dy <= dist; ++dy) {
|
||||||
|
for (int dx = -dist; dx <= dist; ++dx) {
|
||||||
|
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (COLORED) {
|
||||||
|
writeColored(x, y, text, text_color, kerning, lenght);
|
||||||
|
} else {
|
||||||
|
writeColored(x, y, text, text_color, kerning, lenght);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la longitud en pixels de una cadena UTF-8
|
||||||
|
auto Text::length(const std::string& text, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
int shift = 0;
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
while (pos < text.size()) {
|
||||||
|
uint32_t cp = nextCodepoint(text, pos);
|
||||||
|
auto it = offset_.find(cp);
|
||||||
|
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||||
|
if (it != offset_.end()) {
|
||||||
|
shift += it->second.w + kerning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shift > 0 ? shift - kerning : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el ancho en pixels de un glifo dado su codepoint Unicode
|
||||||
|
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||||
|
auto it = offset_.find(codepoint);
|
||||||
|
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||||
|
if (it != offset_.end()) { return it->second.w + kerning; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el clip rect (región en el bitmap) de un glifo dado su codepoint
|
||||||
|
auto Text::getGlyphClip(uint32_t codepoint) const -> SDL_FRect {
|
||||||
|
auto it = offset_.find(codepoint);
|
||||||
|
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||||
|
if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; }
|
||||||
|
return {.x = static_cast<float>(it->second.x),
|
||||||
|
.y = static_cast<float>(it->second.y),
|
||||||
|
.w = static_cast<float>(box_width_),
|
||||||
|
.h = static_cast<float>(box_height_)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el tamaño de la caja de cada caracter
|
||||||
|
auto Text::getCharacterSize() const -> int {
|
||||||
|
return box_width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si se usa un tamaño fijo de letra
|
||||||
|
void Text::setFixedWidth(bool value) {
|
||||||
|
fixed_width_ = value;
|
||||||
|
}
|
||||||
72
source/core/rendering/text.hpp
Normal file
72
source/core/rendering/text.hpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory> // Para shared_ptr, unique_ptr
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <unordered_map> // Para unordered_map
|
||||||
|
|
||||||
|
#include "core/rendering/sprites/sprite.hpp" // Para Sprite
|
||||||
|
class Surface; // Forward declaration
|
||||||
|
|
||||||
|
// Clase texto. Pinta texto en pantalla a partir de un bitmap con soporte UTF-8
|
||||||
|
class Text {
|
||||||
|
public:
|
||||||
|
// Tipos anidados públicos
|
||||||
|
struct Offset {
|
||||||
|
int x{0}, y{0}, w{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct File {
|
||||||
|
int box_width{0}; // Anchura de la caja de cada caracter en el png
|
||||||
|
int box_height{0}; // Altura de la caja de cada caracter en el png
|
||||||
|
int columns{16}; // Número de columnas en el bitmap
|
||||||
|
int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior)
|
||||||
|
int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing)
|
||||||
|
std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
|
||||||
|
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
~Text() = default;
|
||||||
|
|
||||||
|
// Constantes de flags para writeDX
|
||||||
|
static constexpr int COLOR_FLAG = 1;
|
||||||
|
static constexpr int SHADOW_FLAG = 2;
|
||||||
|
static constexpr int CENTER_FLAG = 4;
|
||||||
|
static constexpr int STROKE_FLAG = 8;
|
||||||
|
|
||||||
|
void write(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
|
||||||
|
void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
|
||||||
|
void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
|
||||||
|
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
|
||||||
|
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1); // Escribe texto con extras
|
||||||
|
|
||||||
|
auto writeToSurface(const std::string& text, int zoom = 1, int kerning = 1) -> std::shared_ptr<Surface>; // Escribe el texto en una textura
|
||||||
|
auto writeDXToSurface(Uint8 flags, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1) -> std::shared_ptr<Surface>; // Escribe el texto con extras en una textura
|
||||||
|
|
||||||
|
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; // Obtiene la longitud en pixels de una cadena
|
||||||
|
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el tamaño del caracter
|
||||||
|
[[nodiscard]] auto glyphWidth(uint32_t codepoint, int kerning = 0) const -> int; // Devuelve el ancho en pixels de un glifo
|
||||||
|
[[nodiscard]] auto getGlyphClip(uint32_t codepoint) const -> SDL_FRect; // Devuelve el clip rect del glifo
|
||||||
|
[[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } // Acceso al sprite interno
|
||||||
|
|
||||||
|
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
|
||||||
|
|
||||||
|
static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>; // Carga un fichero de definición de fuente .fnt
|
||||||
|
static auto codepointToUtf8(uint32_t cp) -> std::string; // Convierte un codepoint Unicode a string UTF-8
|
||||||
|
static auto nextCodepoint(const std::string& s, size_t& pos) -> uint32_t; // Extrae el siguiente codepoint UTF-8
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Objetos y punteros
|
||||||
|
std::unique_ptr<Sprite> sprite_ = nullptr; // Objeto con los graficos para el texto
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
int box_width_ = 0; // Anchura de la caja de cada caracter en el png
|
||||||
|
int box_height_ = 0; // Altura de la caja de cada caracter en el png
|
||||||
|
bool fixed_width_ = false; // Indica si el texto se ha de escribir con longitud fija
|
||||||
|
std::unordered_map<uint32_t, Offset> offset_; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||||
|
};
|
||||||
14726
source/external/fkyaml_node.hpp
vendored
Normal file
14726
source/external/fkyaml_node.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5565
source/external/stb_vorbis.h
vendored
Normal file
5565
source/external/stb_vorbis.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
100
source/main.cpp
Normal file
100
source/main.cpp
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#define SDL_MAIN_USE_CALLBACKS
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_main.h>
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/input/input.hpp"
|
||||||
|
#include "core/options.hpp"
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "utils/delta_timer.hpp"
|
||||||
|
|
||||||
|
class App {
|
||||||
|
public:
|
||||||
|
App() = default;
|
||||||
|
~App() { cleanup(); }
|
||||||
|
|
||||||
|
SDL_AppResult init(int /*argc*/, char* /*argv*/[]) {
|
||||||
|
// --- Configuración ---
|
||||||
|
Options::game.width = 320.0F;
|
||||||
|
Options::game.height = 180.0F;
|
||||||
|
Options::window.caption = "Esqueleto";
|
||||||
|
Options::window.zoom = 3;
|
||||||
|
Options::window.max_zoom = 4;
|
||||||
|
Options::video.palettes_path = "data/palettes";
|
||||||
|
Options::text_config.surface_path = ""; // Ruta al .gif de la fuente
|
||||||
|
Options::text_config.fnt_path = ""; // Ruta al .fnt
|
||||||
|
|
||||||
|
// --- Inicialización de sistemas ---
|
||||||
|
Screen::init();
|
||||||
|
Audio::init();
|
||||||
|
Input::init("data/gamecontrollerdb.txt");
|
||||||
|
|
||||||
|
// Vincular teclas del juego (añadir acciones específicas aquí):
|
||||||
|
// Input::get()->bindKey(InputAction::LEFT, SDL_SCANCODE_LEFT);
|
||||||
|
// Input::get()->bindKey(InputAction::RIGHT, SDL_SCANCODE_RIGHT);
|
||||||
|
|
||||||
|
initialized_ = true;
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult handleEvent(const SDL_Event& event) {
|
||||||
|
if (event.type == SDL_EVENT_QUIT) {
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
Input::get()->handleEvent(event);
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult iterate() {
|
||||||
|
float delta_time = delta_timer_.tick();
|
||||||
|
|
||||||
|
Input::get()->update();
|
||||||
|
Audio::update();
|
||||||
|
Screen::get()->update(delta_time);
|
||||||
|
|
||||||
|
// --- Lógica del juego ---
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// --- Renderizado ---
|
||||||
|
Screen::get()->start();
|
||||||
|
Screen::get()->clearSurface(0);
|
||||||
|
|
||||||
|
// Dibuja aquí...
|
||||||
|
|
||||||
|
Screen::get()->render();
|
||||||
|
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void cleanup() {
|
||||||
|
if (!initialized_) return;
|
||||||
|
Input::destroy();
|
||||||
|
Audio::destroy();
|
||||||
|
Screen::destroy();
|
||||||
|
SDL_Quit();
|
||||||
|
initialized_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeltaTimer delta_timer_;
|
||||||
|
bool initialized_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
static App app;
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppInit(void** /*appstate*/, int argc, char* argv[]) {
|
||||||
|
return app.init(argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppEvent(void* /*appstate*/, SDL_Event* event) {
|
||||||
|
return app.handleEvent(*event);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppIterate(void* /*appstate*/) {
|
||||||
|
return app.iterate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) {
|
||||||
|
// ~App() se encarga de la limpieza
|
||||||
|
}
|
||||||
38
source/utils/delta_timer.cpp
Normal file
38
source/utils/delta_timer.cpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#include "utils/delta_timer.hpp"
|
||||||
|
|
||||||
|
DeltaTimer::DeltaTimer() noexcept
|
||||||
|
: last_counter_(SDL_GetPerformanceCounter()),
|
||||||
|
perf_freq_(static_cast<double>(SDL_GetPerformanceFrequency())),
|
||||||
|
time_scale_(1.0F) {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DeltaTimer::tick() noexcept -> float {
|
||||||
|
const Uint64 NOW = SDL_GetPerformanceCounter();
|
||||||
|
const Uint64 DIFF = (NOW > last_counter_) ? (NOW - last_counter_) : 0;
|
||||||
|
last_counter_ = NOW;
|
||||||
|
const double SECONDS = static_cast<double>(DIFF) / perf_freq_;
|
||||||
|
return static_cast<float>(SECONDS * static_cast<double>(time_scale_));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DeltaTimer::peek() const noexcept -> float {
|
||||||
|
const Uint64 NOW = SDL_GetPerformanceCounter();
|
||||||
|
const Uint64 DIFF = (NOW > last_counter_) ? (NOW - last_counter_) : 0;
|
||||||
|
const double SECONDS = static_cast<double>(DIFF) / perf_freq_;
|
||||||
|
return static_cast<float>(SECONDS * static_cast<double>(time_scale_));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeltaTimer::reset(Uint64 counter) noexcept {
|
||||||
|
if (counter == 0) {
|
||||||
|
last_counter_ = SDL_GetPerformanceCounter();
|
||||||
|
} else {
|
||||||
|
last_counter_ = counter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeltaTimer::setTimeScale(float scale) noexcept {
|
||||||
|
time_scale_ = std::max(scale, 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DeltaTimer::getTimeScale() const noexcept -> float {
|
||||||
|
return time_scale_;
|
||||||
|
}
|
||||||
28
source/utils/delta_timer.hpp
Normal file
28
source/utils/delta_timer.hpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
class DeltaTimer {
|
||||||
|
public:
|
||||||
|
DeltaTimer() noexcept;
|
||||||
|
|
||||||
|
// Calcula delta en segundos y actualiza el contador interno
|
||||||
|
auto tick() noexcept -> float;
|
||||||
|
|
||||||
|
// Devuelve el delta estimado desde el último tick sin actualizar el contador
|
||||||
|
[[nodiscard]] auto peek() const noexcept -> float;
|
||||||
|
|
||||||
|
// Reinicia el contador al valor actual o al valor pasado (en performance counter ticks)
|
||||||
|
void reset(Uint64 counter = 0) noexcept;
|
||||||
|
|
||||||
|
// Escala el tiempo retornado por tick/peek, por defecto 1.0f
|
||||||
|
void setTimeScale(float scale) noexcept;
|
||||||
|
[[nodiscard]] auto getTimeScale() const noexcept -> float;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Uint64 last_counter_;
|
||||||
|
double perf_freq_;
|
||||||
|
float time_scale_;
|
||||||
|
};
|
||||||
303
source/utils/utils.cpp
Normal file
303
source/utils/utils.cpp
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
#include "utils/utils.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // Para find, transform
|
||||||
|
#include <cctype> // Para tolower
|
||||||
|
#include <cmath> // Para round, abs
|
||||||
|
#include <cstdlib> // Para abs
|
||||||
|
#include <exception> // Para exception
|
||||||
|
#include <filesystem> // Para path
|
||||||
|
#include <fstream> // Para ifstream
|
||||||
|
#include <iostream> // Para basic_ostream, cout, basic_ios, ios, endl
|
||||||
|
#include <string> // Para basic_string, string, char_traits, allocator
|
||||||
|
#include <unordered_map> // Para unordered_map
|
||||||
|
|
||||||
|
// Calcula el cuadrado de la distancia entre dos puntos
|
||||||
|
auto distanceSquared(int x1, int y1, int x2, int y2) -> double {
|
||||||
|
const int DELTA_X = x2 - x1;
|
||||||
|
const int DELTA_Y = y2 - y1;
|
||||||
|
return (DELTA_X * DELTA_X) + (DELTA_Y * DELTA_Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre dos circulos
|
||||||
|
auto checkCollision(const Circle& a, const Circle& b) -> bool {
|
||||||
|
int total_radius_squared = a.r + b.r;
|
||||||
|
total_radius_squared = total_radius_squared * total_radius_squared;
|
||||||
|
return distanceSquared(a.x, a.y, b.x, b.y) < total_radius_squared;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre un circulo y un rectangulo
|
||||||
|
auto checkCollision(const Circle& a, const SDL_FRect& rect) -> bool {
|
||||||
|
SDL_Rect b = toSDLRect(rect);
|
||||||
|
int c_x;
|
||||||
|
int c_y;
|
||||||
|
|
||||||
|
if (a.x < b.x) {
|
||||||
|
c_x = b.x;
|
||||||
|
} else if (a.x > b.x + b.w) {
|
||||||
|
c_x = b.x + b.w;
|
||||||
|
} else {
|
||||||
|
c_x = a.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.y < b.y) {
|
||||||
|
c_y = b.y;
|
||||||
|
} else if (a.y > b.y + b.h) {
|
||||||
|
c_y = b.y + b.h;
|
||||||
|
} else {
|
||||||
|
c_y = a.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return distanceSquared(a.x, a.y, c_x, c_y) < a.r * a.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre dos rectangulos
|
||||||
|
auto checkCollision(const SDL_FRect& rect_a, const SDL_FRect& rect_b) -> bool {
|
||||||
|
SDL_Rect a = toSDLRect(rect_a);
|
||||||
|
SDL_Rect b = toSDLRect(rect_b);
|
||||||
|
const int LEFT_A = a.x;
|
||||||
|
const int RIGHT_A = a.x + a.w;
|
||||||
|
const int TOP_A = a.y;
|
||||||
|
const int BOTTOM_A = a.y + a.h;
|
||||||
|
const int LEFT_B = b.x;
|
||||||
|
const int RIGHT_B = b.x + b.w;
|
||||||
|
const int TOP_B = b.y;
|
||||||
|
const int BOTTOM_B = b.y + b.h;
|
||||||
|
|
||||||
|
if (BOTTOM_A <= TOP_B) return false;
|
||||||
|
if (TOP_A >= BOTTOM_B) return false;
|
||||||
|
if (RIGHT_A <= LEFT_B) return false;
|
||||||
|
if (LEFT_A >= RIGHT_B) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre un punto y un rectangulo
|
||||||
|
auto checkCollision(const SDL_FPoint& point, const SDL_FRect& rect) -> bool {
|
||||||
|
SDL_Rect r = toSDLRect(rect);
|
||||||
|
SDL_Point p = toSDLPoint(point);
|
||||||
|
if (p.x < r.x) return false;
|
||||||
|
if (p.x > r.x + r.w) return false;
|
||||||
|
if (p.y < r.y) return false;
|
||||||
|
if (p.y > r.y + r.h) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre una linea horizontal y un rectangulo
|
||||||
|
auto checkCollision(const LineHorizontal& l, const SDL_FRect& rect) -> bool {
|
||||||
|
SDL_Rect r = toSDLRect(rect);
|
||||||
|
if (l.y < r.y) return false;
|
||||||
|
if (l.y >= r.y + r.h) return false;
|
||||||
|
if (l.x1 >= r.x + r.w) return false;
|
||||||
|
if (l.x2 < r.x) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre una linea vertical y un rectangulo
|
||||||
|
auto checkCollision(const LineVertical& l, const SDL_FRect& rect) -> bool {
|
||||||
|
SDL_Rect r = toSDLRect(rect);
|
||||||
|
if (l.x < r.x) return false;
|
||||||
|
if (l.x >= r.x + r.w) return false;
|
||||||
|
if (l.y1 >= r.y + r.h) return false;
|
||||||
|
if (l.y2 < r.y) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre una linea horizontal y un punto
|
||||||
|
auto checkCollision(const LineHorizontal& l, const SDL_FPoint& point) -> bool {
|
||||||
|
SDL_Point p = toSDLPoint(point);
|
||||||
|
if (p.y > l.y) return false;
|
||||||
|
if (p.y < l.y) return false;
|
||||||
|
if (p.x < l.x1) return false;
|
||||||
|
if (p.x > l.x2) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre dos lineas
|
||||||
|
auto checkCollision(const Line& l1, const Line& l2) -> SDL_Point {
|
||||||
|
const float X1 = l1.x1;
|
||||||
|
const float Y1 = l1.y1;
|
||||||
|
const float X2 = l1.x2;
|
||||||
|
const float Y2 = l1.y2;
|
||||||
|
const float X3 = l2.x1;
|
||||||
|
const float Y3 = l2.y1;
|
||||||
|
const float X4 = l2.x2;
|
||||||
|
const float Y4 = l2.y2;
|
||||||
|
|
||||||
|
float u_a = (((X4 - X3) * (Y1 - Y3)) - ((Y4 - Y3) * (X1 - X3))) / (((Y4 - Y3) * (X2 - X1)) - ((X4 - X3) * (Y2 - Y1)));
|
||||||
|
float u_b = (((X2 - X1) * (Y1 - Y3)) - ((Y2 - Y1) * (X1 - X3))) / (((Y4 - Y3) * (X2 - X1)) - ((X4 - X3) * (Y2 - Y1)));
|
||||||
|
|
||||||
|
if (u_a >= 0 && u_a <= 1 && u_b >= 0 && u_b <= 1) {
|
||||||
|
const float X = X1 + (u_a * (X2 - X1));
|
||||||
|
const float Y = Y1 + (u_a * (Y2 - Y1));
|
||||||
|
return {.x = static_cast<int>(std::round(X)), .y = static_cast<int>(std::round(Y))};
|
||||||
|
}
|
||||||
|
return {.x = -1, .y = -1};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre dos lineas (diagonal-vertical)
|
||||||
|
auto checkCollision(const LineDiagonal& l1, const LineVertical& l2) -> SDL_Point {
|
||||||
|
const float X1 = l1.x1;
|
||||||
|
const float Y1 = l1.y1;
|
||||||
|
const float X2 = l1.x2;
|
||||||
|
const float Y2 = l1.y2;
|
||||||
|
const float X3 = l2.x;
|
||||||
|
const float Y3 = l2.y1;
|
||||||
|
const float X4 = l2.x;
|
||||||
|
const float Y4 = l2.y2;
|
||||||
|
|
||||||
|
float u_a = (((X4 - X3) * (Y1 - Y3)) - ((Y4 - Y3) * (X1 - X3))) / (((Y4 - Y3) * (X2 - X1)) - ((X4 - X3) * (Y2 - Y1)));
|
||||||
|
float u_b = (((X2 - X1) * (Y1 - Y3)) - ((Y2 - Y1) * (X1 - X3))) / (((Y4 - Y3) * (X2 - X1)) - ((X4 - X3) * (Y2 - Y1)));
|
||||||
|
|
||||||
|
if (u_a >= 0 && u_a <= 1 && u_b >= 0 && u_b <= 1) {
|
||||||
|
const float X = X1 + (u_a * (X2 - X1));
|
||||||
|
const float Y = Y1 + (u_a * (Y2 - Y1));
|
||||||
|
return {.x = static_cast<int>(X), .y = static_cast<int>(Y)};
|
||||||
|
}
|
||||||
|
return {.x = -1, .y = -1};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normaliza una linea diagonal
|
||||||
|
void normalizeLine(LineDiagonal& l) {
|
||||||
|
if (l.x2 < l.x1) {
|
||||||
|
const int X = l.x1;
|
||||||
|
const int Y = l.y1;
|
||||||
|
l.x1 = l.x2;
|
||||||
|
l.y1 = l.y2;
|
||||||
|
l.x2 = X;
|
||||||
|
l.y2 = Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte SDL_FRect a SDL_Rect
|
||||||
|
auto toSDLRect(const SDL_FRect& frect) -> SDL_Rect {
|
||||||
|
return SDL_Rect{
|
||||||
|
.x = static_cast<int>(frect.x),
|
||||||
|
.y = static_cast<int>(frect.y),
|
||||||
|
.w = static_cast<int>(frect.w),
|
||||||
|
.h = static_cast<int>(frect.h)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte SDL_FPoint a SDL_Point
|
||||||
|
auto toSDLPoint(const SDL_FPoint& fpoint) -> SDL_Point {
|
||||||
|
return SDL_Point{
|
||||||
|
.x = static_cast<int>(fpoint.x),
|
||||||
|
.y = static_cast<int>(fpoint.y)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detector de colisiones entre un punto y una linea diagonal
|
||||||
|
auto checkCollision(const SDL_FPoint& point, const LineDiagonal& l) -> bool {
|
||||||
|
SDL_Point p = toSDLPoint(point);
|
||||||
|
if (abs(p.x - l.x1) != abs(p.y - l.y1)) return false;
|
||||||
|
if (p.x > l.x1 && p.x > l.x2) return false;
|
||||||
|
if (p.x < l.x1 && p.x < l.x2) return false;
|
||||||
|
if (p.y > l.y1 && p.y > l.y2) return false;
|
||||||
|
if (p.y < l.y1 && p.y < l.y2) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte una cadena a un indice de la paleta
|
||||||
|
auto stringToColor(const std::string& str) -> Uint8 {
|
||||||
|
static const std::unordered_map<std::string, Uint8> PALETTE_MAP = {
|
||||||
|
{"black", 0}, {"bright_black", 1},
|
||||||
|
{"blue", 2}, {"bright_blue", 3},
|
||||||
|
{"red", 4}, {"bright_red", 5},
|
||||||
|
{"magenta", 6}, {"bright_magenta", 7},
|
||||||
|
{"green", 8}, {"bright_green", 9},
|
||||||
|
{"cyan", 10}, {"bright_cyan", 11},
|
||||||
|
{"yellow", 12}, {"bright_yellow", 13},
|
||||||
|
{"white", 14}, {"bright_white", 15},
|
||||||
|
{"transparent", 255}};
|
||||||
|
|
||||||
|
auto it = PALETTE_MAP.find(str);
|
||||||
|
return (it != PALETTE_MAP.end()) ? it->second : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte una cadena a un entero de forma segura
|
||||||
|
auto safeStoi(const std::string& value, int default_value) -> int {
|
||||||
|
try {
|
||||||
|
return std::stoi(value);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte una cadena a un booleano
|
||||||
|
auto stringToBool(const std::string& str) -> bool {
|
||||||
|
std::string lower_str = str;
|
||||||
|
std::ranges::transform(lower_str, lower_str.begin(), ::tolower);
|
||||||
|
return (lower_str == "true" || lower_str == "1" || lower_str == "yes" || lower_str == "on");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte un booleano a una cadena
|
||||||
|
auto boolToString(bool value) -> std::string {
|
||||||
|
return value ? "1" : "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compara dos colores
|
||||||
|
auto colorAreEqual(Color color1, Color color2) -> bool {
|
||||||
|
return color1.r == color2.r && color1.g == color2.g && color1.b == color2.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para convertir un string a minúsculas
|
||||||
|
auto toLower(const std::string& str) -> std::string {
|
||||||
|
std::string lower_str = str;
|
||||||
|
std::ranges::transform(lower_str, lower_str.begin(), ::tolower);
|
||||||
|
return lower_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para convertir un string a mayúsculas
|
||||||
|
auto toUpper(const std::string& str) -> std::string {
|
||||||
|
std::string upper_str = str;
|
||||||
|
std::ranges::transform(upper_str, upper_str.begin(), ::toupper);
|
||||||
|
return upper_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga un fichero como vector de bytes
|
||||||
|
auto loadFileBytes(const std::string& path) -> std::vector<uint8_t> {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) return {};
|
||||||
|
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el nombre de un fichero a partir de una ruta completa
|
||||||
|
auto getFileName(const std::string& path) -> std::string {
|
||||||
|
return std::filesystem::path(path).filename().string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la ruta eliminando el nombre del fichero
|
||||||
|
auto getPath(const std::string& full_path) -> std::string {
|
||||||
|
return std::filesystem::path(full_path).parent_path().string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imprime por pantalla una linea de texto de tamaño fijo rellena con puntos
|
||||||
|
void printWithDots(const std::string& text1, const std::string& text2, const std::string& text3) {
|
||||||
|
std::cout.setf(std::ios::left, std::ios::adjustfield);
|
||||||
|
std::cout << text1;
|
||||||
|
std::cout.width(50 - text1.length() - text3.length());
|
||||||
|
std::cout.fill('.');
|
||||||
|
std::cout << text2;
|
||||||
|
std::cout << text3 << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si una vector contiene una cadena
|
||||||
|
auto stringInVector(const std::vector<std::string>& vec, const std::string& str) -> bool {
|
||||||
|
return std::ranges::find(vec, str) != vec.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rellena una textura de un color
|
||||||
|
void fillTextureWithColor(SDL_Renderer* renderer, SDL_Texture* texture, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
|
||||||
|
SDL_Texture* previous_target = SDL_GetRenderTarget(renderer);
|
||||||
|
SDL_SetRenderTarget(renderer, texture);
|
||||||
|
SDL_SetRenderDrawColor(renderer, r, g, b, a);
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
SDL_SetRenderTarget(renderer, previous_target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añade espacios entre las letras de un string
|
||||||
|
auto spaceBetweenLetters(const std::string& input) -> std::string {
|
||||||
|
std::string result;
|
||||||
|
for (size_t i = 0; i < input.size(); ++i) {
|
||||||
|
result += input[i];
|
||||||
|
if (i != input.size() - 1) result += ' ';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
119
source/utils/utils.hpp
Normal file
119
source/utils/utils.hpp
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // Para uint8_t
|
||||||
|
#include <string> // Para string
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
enum class PaletteColor : Uint8 {
|
||||||
|
BLACK = 0,
|
||||||
|
BRIGHT_BLACK = 1,
|
||||||
|
|
||||||
|
BLUE = 2,
|
||||||
|
BRIGHT_BLUE = 3,
|
||||||
|
|
||||||
|
RED = 4,
|
||||||
|
BRIGHT_RED = 5,
|
||||||
|
|
||||||
|
MAGENTA = 6,
|
||||||
|
BRIGHT_MAGENTA = 7,
|
||||||
|
|
||||||
|
GREEN = 8,
|
||||||
|
BRIGHT_GREEN = 9,
|
||||||
|
|
||||||
|
CYAN = 10,
|
||||||
|
BRIGHT_CYAN = 11,
|
||||||
|
|
||||||
|
YELLOW = 12,
|
||||||
|
BRIGHT_YELLOW = 13,
|
||||||
|
|
||||||
|
WHITE = 14,
|
||||||
|
BRIGHT_WHITE = 15,
|
||||||
|
|
||||||
|
TRANSPARENT = 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir un circulo
|
||||||
|
struct Circle {
|
||||||
|
int x{0};
|
||||||
|
int y{0};
|
||||||
|
int r{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir una linea horizontal
|
||||||
|
struct LineHorizontal {
|
||||||
|
int x1{0}, x2{0}, y{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir una linea vertical
|
||||||
|
struct LineVertical {
|
||||||
|
int x{0}, y1{0}, y2{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir una linea diagonal
|
||||||
|
struct LineDiagonal {
|
||||||
|
int x1{0}, y1{0}, x2{0}, y2{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir una linea
|
||||||
|
struct Line {
|
||||||
|
int x1{0}, y1{0}, x2{0}, y2{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir un color
|
||||||
|
struct Color {
|
||||||
|
Uint8 r{0};
|
||||||
|
Uint8 g{0};
|
||||||
|
Uint8 b{0};
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Color(Uint8 red, Uint8 green, Uint8 blue)
|
||||||
|
: r(red),
|
||||||
|
g(green),
|
||||||
|
b(blue) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// COLISIONES Y GEOMETRÍA
|
||||||
|
auto distanceSquared(int x1, int y1, int x2, int y2) -> double; // Distancia² entre dos puntos
|
||||||
|
auto checkCollision(const Circle& a, const Circle& b) -> bool; // Colisión círculo-círculo
|
||||||
|
auto checkCollision(const Circle& a, const SDL_FRect& rect) -> bool; // Colisión círculo-rectángulo
|
||||||
|
auto checkCollision(const SDL_FRect& a, const SDL_FRect& b) -> bool; // Colisión rectángulo-rectángulo
|
||||||
|
auto checkCollision(const SDL_FPoint& p, const SDL_FRect& r) -> bool; // Colisión punto-rectángulo
|
||||||
|
auto checkCollision(const LineHorizontal& l, const SDL_FRect& r) -> bool; // Colisión línea horizontal-rectángulo
|
||||||
|
auto checkCollision(const LineVertical& l, const SDL_FRect& r) -> bool; // Colisión línea vertical-rectángulo
|
||||||
|
auto checkCollision(const LineHorizontal& l, const SDL_FPoint& p) -> bool; // Colisión línea horizontal-punto
|
||||||
|
auto checkCollision(const Line& l1, const Line& l2) -> SDL_Point; // Colisión línea-línea (intersección)
|
||||||
|
auto checkCollision(const LineDiagonal& l1, const LineVertical& l2) -> SDL_Point; // Colisión diagonal-vertical
|
||||||
|
auto checkCollision(const SDL_FPoint& p, const LineDiagonal& l) -> bool; // Colisión punto-diagonal
|
||||||
|
void normalizeLine(LineDiagonal& l); // Normaliza línea diagonal (x1 < x2)
|
||||||
|
|
||||||
|
// CONVERSIONES DE TIPOS SDL
|
||||||
|
auto toSDLRect(const SDL_FRect& frect) -> SDL_Rect; // Convierte SDL_FRect a SDL_Rect
|
||||||
|
auto toSDLPoint(const SDL_FPoint& fpoint) -> SDL_Point; // Convierte SDL_FPoint a SDL_Point
|
||||||
|
|
||||||
|
// CONVERSIONES DE STRING
|
||||||
|
auto stringToColor(const std::string& str) -> Uint8; // String a índice de paleta
|
||||||
|
auto safeStoi(const std::string& value, int default_value = 0) -> int; // String a int seguro (sin excepciones)
|
||||||
|
auto stringToBool(const std::string& str) -> bool; // String a bool (true/1/yes/on)
|
||||||
|
auto boolToString(bool value) -> std::string; // Bool a string (1/0)
|
||||||
|
auto toLower(const std::string& str) -> std::string; // String a minúsculas
|
||||||
|
auto toUpper(const std::string& str) -> std::string; // String a mayúsculas
|
||||||
|
|
||||||
|
// OPERACIONES CON STRINGS
|
||||||
|
auto stringInVector(const std::vector<std::string>& vec, const std::string& str) -> bool; // Busca string en vector
|
||||||
|
auto spaceBetweenLetters(const std::string& input) -> std::string; // Añade espacios entre letras
|
||||||
|
|
||||||
|
// OPERACIONES CON COLORES
|
||||||
|
auto colorAreEqual(Color color1, Color color2) -> bool; // Compara dos colores RGB
|
||||||
|
|
||||||
|
// OPERACIONES CON FICHEROS
|
||||||
|
auto loadFileBytes(const std::string& path) -> std::vector<uint8_t>; // Carga un fichero como bytes
|
||||||
|
auto getFileName(const std::string& path) -> std::string; // Extrae nombre de fichero de ruta
|
||||||
|
auto getPath(const std::string& full_path) -> std::string; // Extrae directorio de ruta completa
|
||||||
|
|
||||||
|
// RENDERIZADO
|
||||||
|
void fillTextureWithColor(SDL_Renderer* renderer, SDL_Texture* texture, Uint8 r, Uint8 g, Uint8 b, Uint8 a); // Rellena textura
|
||||||
|
|
||||||
|
// OUTPUT Y UTILIDADES DE CONSOLA
|
||||||
|
void printWithDots(const std::string& text1, const std::string& text2, const std::string& text3); // Imprime línea con puntos
|
||||||
Reference in New Issue
Block a user