reestructuració

This commit is contained in:
2026-04-17 17:15:38 +02:00
parent 55caef3210
commit 5fec0110b3
66 changed files with 221 additions and 217 deletions

View File

@@ -0,0 +1,549 @@
#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.c"
// --- 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};
// OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una
// sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming.
Uint8* ogg_data{nullptr};
Uint32 ogg_length{0};
stb_vorbis* vorbis{nullptr}; // Handle del decoder, viu tot el cicle del JA_Music_t
char* filename{nullptr};
int times{0}; // Loops restants (-1 = infinit, 0 = un sol play)
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
// --- Internal Global State ---
// Marcado 'inline' (C++17) para asegurar una única instancia.
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};
// --- 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);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192;
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
// decodificats per canal (0 = EOF de l'stream vorbis).
inline int JA_FeedMusicChunk(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return 0;
short chunk[JA_MUSIC_CHUNK_SHORTS];
const int channels = music->spec.channels;
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
channels,
chunk,
JA_MUSIC_CHUNK_SHORTS);
if (samples_per_channel <= 0) return 0;
const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, chunk, bytes);
return samples_per_channel;
}
// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
inline void JA_PumpMusic(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return;
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int low_water_bytes = static_cast<int>(JA_MUSIC_LOW_WATER_SECONDS * static_cast<float>(bytes_per_second));
while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded > 0) continue;
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) music->times--;
} else {
break;
}
}
}
// --- 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));
}
}
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
// vorbis s'ha esgotat i no queden loops.
JA_PumpMusic(current_music);
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
JA_StopMusic();
}
}
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);
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);
sdlAudioDevice = 0;
}
// --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
if (!buffer || length == 0) return nullptr;
// Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta
// memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres.
Uint8* ogg_copy = static_cast<Uint8*>(SDL_malloc(length));
if (!ogg_copy) return nullptr;
SDL_memcpy(ogg_copy, buffer, length);
int error = 0;
stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast<int>(length), &error, nullptr);
if (!vorbis) {
SDL_free(ogg_copy);
SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error);
return nullptr;
}
auto* music = new JA_Music_t();
music->ogg_data = ogg_copy;
music->ogg_length = length;
music->vorbis = vorbis;
const stb_vorbis_info info = stb_vorbis_get_info(vorbis);
music->spec.channels = info.channels;
music->spec.freq = static_cast<int>(info.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->state = JA_MUSIC_STOPPED;
return music;
}
inline JA_Music_t* JA_LoadMusic(const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) {
fclose(f);
return 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) {
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 || !music->vorbis) return;
JA_StopMusic();
current_music = music;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
SDL_Log("Failed to create audio stream!");
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
JA_PumpMusic(current_music);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) 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;
return music->filename;
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return;
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic.
// Rebobinem perquè un futur JA_PlayMusic comence des del principi.
if (current_music->vorbis) {
stb_vorbis_seek_start(current_music->vorbis);
}
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == 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;
}
if (music->stream) SDL_DestroyAudioStream(music->stream);
if (music->vorbis) stb_vorbis_close(music->vorbis);
SDL_free(music->ogg_data);
free(music->filename);
delete music;
}
inline float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
if (current_music && current_music->stream) {
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
}
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float /*value*/) {
// No implementat amb el backend de streaming. Mai va arribar a usar-se
// en el codi existent, així que es manté com a stub.
}
inline float JA_GetMusicPosition() {
// Veure nota a JA_SetMusicPosition.
return 0.0f;
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
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) {
channel = 0;
}
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
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) {
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;
}
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
if (channels[i].stream) {
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
}
}
}
}
return v;
}
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1);
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1);
return v;
}

389
source/core/input/input.cpp Normal file
View File

@@ -0,0 +1,389 @@
#include "core/input/input.h"
#include <SDL3/SDL.h>
#include <iostream> // for basic_ostream, operator<<, cout, basi...
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char *name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Constructor
Input::Input(std::string file) {
// Fichero gamecontrollerdb.txt
dbPath = file;
// Inicializa las variables
keyBindings_t kb;
kb.scancode = 0;
kb.active = false;
keyBindings.resize(input_number_of_inputs, kb);
GameControllerBindings_t gcb;
gcb.button = SDL_GAMEPAD_BUTTON_INVALID;
gcb.active = false;
gameControllerBindings.resize(input_number_of_inputs, gcb);
numGamepads = 0;
verbose = true;
enabled = true;
}
// Destructor
Input::~Input() {
for (auto *pad : connectedControllers) {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
connectedControllers.clear();
connectedControllerIds.clear();
controllerNames.clear();
numGamepads = 0;
}
// Actualiza el estado del objeto
void Input::update() {
if (disabledUntil == d_keyPressed && !checkAnyInput()) {
enable();
}
}
// Asigna inputs a teclas
void Input::bindKey(Uint8 input, SDL_Scancode code) {
keyBindings[input].scancode = code;
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(Uint8 input, SDL_GamepadButton button) {
gameControllerBindings[input].button = button;
}
// Comprueba si un input esta activo
bool Input::checkInput(Uint8 input, bool repeat, int device, int index) {
if (!enabled) {
return false;
}
bool successKeyboard = false;
bool successGameController = false;
if (device == INPUT_USE_ANY) {
index = 0;
}
if (device == INPUT_USE_KEYBOARD || device == INPUT_USE_ANY) {
const bool *keyStates = SDL_GetKeyboardState(nullptr);
if (repeat) {
if (keyStates[keyBindings[input].scancode]) {
successKeyboard = true;
} else {
successKeyboard = false;
}
} else {
if (!keyBindings[input].active) {
if (keyStates[keyBindings[input].scancode]) {
keyBindings[input].active = true;
successKeyboard = true;
} else {
successKeyboard = false;
}
} else {
if (!keyStates[keyBindings[input].scancode]) {
keyBindings[input].active = false;
successKeyboard = false;
} else {
successKeyboard = false;
}
}
}
}
if (gameControllerFound() && index >= 0 && index < (int)connectedControllers.size())
if ((device == INPUT_USE_GAMECONTROLLER) || (device == INPUT_USE_ANY)) {
if (repeat) {
if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[input].button)) {
successGameController = true;
} else {
successGameController = false;
}
} else {
if (!gameControllerBindings[input].active) {
if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[input].button)) {
gameControllerBindings[input].active = true;
successGameController = true;
} else {
successGameController = false;
}
} else {
if (!SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[input].button)) {
gameControllerBindings[input].active = false;
successGameController = false;
} else {
successGameController = false;
}
}
}
}
return (successKeyboard || successGameController);
}
// Comprueba si hay almenos un input activo
bool Input::checkAnyInput(int device, int index) {
if (device == INPUT_USE_ANY) {
index = 0;
}
if (device == INPUT_USE_KEYBOARD || device == INPUT_USE_ANY) {
const bool *mKeystates = SDL_GetKeyboardState(nullptr);
for (int i = 0; i < (int)keyBindings.size(); ++i) {
if (mKeystates[keyBindings[i].scancode]) {
return true;
}
}
}
if (gameControllerFound() && index >= 0 && index < (int)connectedControllers.size()) {
if (device == INPUT_USE_GAMECONTROLLER || device == INPUT_USE_ANY) {
for (int i = 0; i < (int)gameControllerBindings.size(); ++i) {
if (SDL_GetGamepadButton(connectedControllers[index], gameControllerBindings[i].button)) {
return true;
}
}
}
}
return false;
}
// Construye el nombre visible de un mando.
// Recorta des del primer '(' o '[' (per a evitar coses tipus
// "Retroid Controller (vendor: 1001) ...") i talla a 25 caràcters.
std::string Input::buildControllerName(SDL_Gamepad *pad, int padIndex) {
(void)padIndex;
const char *padName = SDL_GetGamepadName(pad);
std::string name = padName ? padName : "Unknown";
const auto pos = name.find_first_of("([");
if (pos != std::string::npos) {
name.erase(pos);
}
while (!name.empty() && name.back() == ' ') {
name.pop_back();
}
if (name.size() > 25) {
name.resize(25);
}
return name;
}
// Busca si hay un mando conectado. Cierra y limpia el estado previo para
// que la función sea idempotente si se invoca más de una vez.
bool Input::discoverGameController() {
// Cierra los mandos ya abiertos y limpia los vectores paralelos
for (auto *pad : connectedControllers) {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
connectedControllers.clear();
connectedControllerIds.clear();
controllerNames.clear();
numGamepads = 0;
bool found = false;
if (SDL_WasInit(SDL_INIT_GAMEPAD) != SDL_INIT_GAMEPAD) {
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
}
if (SDL_AddGamepadMappingsFromFile(dbPath.c_str()) < 0) {
if (verbose) {
std::cout << "Error, could not load " << dbPath.c_str() << " file: " << SDL_GetError() << std::endl;
}
}
int nJoysticks = 0;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&nJoysticks);
if (joysticks) {
int gamepadCount = 0;
for (int i = 0; i < nJoysticks; ++i) {
if (SDL_IsGamepad(joysticks[i])) {
gamepadCount++;
}
}
if (verbose) {
std::cout << "\nChecking for game controllers...\n";
std::cout << nJoysticks << " joysticks found, " << gamepadCount << " are gamepads\n";
}
if (gamepadCount > 0) {
found = true;
int padIndex = 0;
for (int i = 0; i < nJoysticks; i++) {
if (!SDL_IsGamepad(joysticks[i])) continue;
installWebStandardMapping(joysticks[i]);
SDL_Gamepad *pad = SDL_OpenGamepad(joysticks[i]);
if (pad != nullptr) {
const std::string name = buildControllerName(pad, padIndex);
connectedControllers.push_back(pad);
connectedControllerIds.push_back(joysticks[i]);
controllerNames.push_back(name);
numGamepads++;
padIndex++;
if (verbose) {
std::cout << name << std::endl;
}
} else {
if (verbose) {
std::cout << "SDL_GetError() = " << SDL_GetError() << std::endl;
}
}
}
SDL_SetGamepadEventsEnabled(true);
}
SDL_free(joysticks);
}
return found;
}
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED
bool Input::handleGamepadAdded(SDL_JoystickID jid, std::string &outName) {
if (!SDL_IsGamepad(jid)) {
return false;
}
// Si el mando ya está registrado no hace nada (ej. evento retroactivo tras el scan inicial)
for (SDL_JoystickID existing : connectedControllerIds) {
if (existing == jid) {
return false;
}
}
installWebStandardMapping(jid);
SDL_Gamepad *pad = SDL_OpenGamepad(jid);
if (pad == nullptr) {
if (verbose) {
std::cout << "Failed to open gamepad " << jid << ": " << SDL_GetError() << std::endl;
}
return false;
}
const int padIndex = (int)connectedControllers.size();
const std::string name = buildControllerName(pad, padIndex);
connectedControllers.push_back(pad);
connectedControllerIds.push_back(jid);
controllerNames.push_back(name);
numGamepads++;
if (verbose) {
std::cout << "Gamepad connected: " << name << std::endl;
}
outName = name;
return true;
}
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED
bool Input::handleGamepadRemoved(SDL_JoystickID jid, std::string &outName) {
for (size_t i = 0; i < connectedControllerIds.size(); ++i) {
if (connectedControllerIds[i] != jid) continue;
outName = controllerNames[i];
if (connectedControllers[i] != nullptr) {
SDL_CloseGamepad(connectedControllers[i]);
}
connectedControllers.erase(connectedControllers.begin() + i);
connectedControllerIds.erase(connectedControllerIds.begin() + i);
controllerNames.erase(controllerNames.begin() + i);
numGamepads--;
if (numGamepads < 0) numGamepads = 0;
if (verbose) {
std::cout << "Gamepad disconnected: " << outName << std::endl;
}
return true;
}
return false;
}
// Comprueba si hay algun mando conectado
bool Input::gameControllerFound() {
if (numGamepads > 0) {
return true;
} else {
return false;
}
}
// Obten el nombre de un mando de juego
std::string Input::getControllerName(int index) {
if (numGamepads > 0) {
return controllerNames[index];
} else {
return "";
}
}
// Obten el numero de mandos conectados
int Input::getNumControllers() {
return numGamepads;
}
// Establece si ha de mostrar mensajes
void Input::setVerbose(bool value) {
verbose = value;
}
// Deshabilita las entradas durante un periodo de tiempo
void Input::disableUntil(i_disable_e value) {
disabledUntil = value;
enabled = false;
}
// Hablita las entradas
void Input::enable() {
enabled = true;
disabledUntil = d_notDisabled;
}

126
source/core/input/input.h Normal file
View File

@@ -0,0 +1,126 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string, basic_string
#include <vector> // for vector
// Valores de repetición
constexpr bool REPEAT_TRUE = true;
constexpr bool REPEAT_FALSE = false;
// Métodos de entrada
constexpr int INPUT_USE_KEYBOARD = 0;
constexpr int INPUT_USE_GAMECONTROLLER = 1;
constexpr int INPUT_USE_ANY = 2;
enum inputs_e {
// Inputs obligatorios
input_null,
input_up,
input_down,
input_left,
input_right,
input_pause,
input_exit,
input_accept,
input_cancel,
// Inputs personalizados
input_fire_left,
input_fire_center,
input_fire_right,
input_window_fullscreen,
input_window_inc_size,
input_window_dec_size,
// Input obligatorio
input_number_of_inputs
};
enum i_disable_e {
d_notDisabled,
d_forever,
d_keyPressed
};
class Input {
private:
struct keyBindings_t {
Uint8 scancode; // Scancode asociado
bool active; // Indica si está activo
};
struct GameControllerBindings_t {
SDL_GamepadButton button; // GameControllerButton asociado
bool active; // Indica si está activo
};
// Objetos y punteros
std::vector<SDL_Gamepad *> connectedControllers; // Vector con todos los mandos conectados
std::vector<SDL_JoystickID> connectedControllerIds; // Instance IDs paralelos para mapear eventos
// Variables
std::vector<keyBindings_t> keyBindings; // Vector con las teclas asociadas a los inputs predefinidos
std::vector<GameControllerBindings_t> gameControllerBindings; // Vector con las teclas asociadas a los inputs predefinidos
std::vector<std::string> controllerNames; // Vector con los nombres de los mandos
int numGamepads; // Numero de mandos conectados
std::string dbPath; // Ruta al archivo gamecontrollerdb.txt
bool verbose; // Indica si ha de mostrar mensajes
i_disable_e disabledUntil; // Tiempo que esta deshabilitado
bool enabled; // Indica si está habilitado
// Construye el nombre visible de un mando (name truncado + sufijo #N)
std::string buildControllerName(SDL_Gamepad *pad, int padIndex);
public:
// Constructor
Input(std::string file);
// Destructor
~Input();
// Actualiza el estado del objeto
void update();
// Asigna inputs a teclas
void bindKey(Uint8 input, SDL_Scancode code);
// Asigna inputs a botones del mando
void bindGameControllerButton(Uint8 input, SDL_GamepadButton button);
// Comprueba si un input esta activo
bool checkInput(Uint8 input, bool repeat = true, int device = INPUT_USE_ANY, int index = 0);
// Comprueba si hay almenos un input activo
bool checkAnyInput(int device = INPUT_USE_ANY, int index = 0);
// Busca si hay un mando conectado
bool discoverGameController();
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED. Devuelve true si el mando se ha añadido
// (no estaba ya registrado) y escribe el nombre visible en outName.
bool handleGamepadAdded(SDL_JoystickID jid, std::string &outName);
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED. Devuelve true si se ha encontrado y
// eliminado, y escribe el nombre visible en outName.
bool handleGamepadRemoved(SDL_JoystickID jid, std::string &outName);
// Comprueba si hay algun mando conectado
bool gameControllerFound();
// Obten el numero de mandos conectados
int getNumControllers();
// Obten el nombre de un mando de juego
std::string getControllerName(int index);
// Establece si ha de mostrar mensajes
void setVerbose(bool value);
// Deshabilita las entradas durante un periodo de tiempo
void disableUntil(i_disable_e value);
// Hablita las entradas
void enable();
};

View File

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

View File

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

View File

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

35
source/core/locale/lang.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string, basic_string
class Asset;
// Códigos de idioma
constexpr int es_ES = 0;
constexpr int ba_BA = 1;
constexpr int en_UK = 2;
constexpr int MAX_LANGUAGES = 3;
// Textos
constexpr int MAX_TEXT_STRINGS = 100;
// Clase Lang
class Lang {
private:
Asset *mAsset; // Objeto que gestiona todos los ficheros de recursos
std::string mTextStrings[MAX_TEXT_STRINGS]; // Vector con los textos
public:
// Constructor
Lang(Asset *mAsset);
// Destructor
~Lang();
// Inicializa los textos del juego en el idioma seleccionado
bool setLang(Uint8 lang);
// Obtiene la cadena de texto del indice
std::string getText(int index);
};

View File

@@ -0,0 +1,438 @@
#include "core/rendering/animatedsprite.h"
#include <fstream> // for basic_ostream, operator<<, basic_istream, basic...
#include <iostream> // for cout
#include <sstream> // for basic_stringstream
#include "core/rendering/texture.h" // for Texture
// Parser compartido: lee un istream con el formato .ani
static animatedSprite_t parseAnimationStream(std::istream &file, Texture *texture, const std::string &filename, bool verbose) {
animatedSprite_t as;
as.texture = texture;
int framesPerRow = 0;
int frameWidth = 0;
int frameHeight = 0;
int maxTiles = 0;
std::string line;
if (verbose) {
std::cout << "Animation loaded: " << filename << std::endl;
}
while (std::getline(file, line)) {
if (line == "[animation]") {
animation_t buffer;
buffer.counter = 0;
buffer.currentFrame = 0;
buffer.completed = false;
do {
std::getline(file, line);
int pos = line.find("=");
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "name") {
buffer.name = line.substr(pos + 1, line.length());
} else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
} else if (line.substr(0, pos) == "loop") {
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
} else if (line.substr(0, pos) == "frames") {
std::stringstream ss(line.substr(pos + 1, line.length()));
std::string tmp;
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
while (getline(ss, tmp, ',')) {
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
}
} while (line != "[/animation]");
as.animations.push_back(buffer);
} else {
int pos = line.find("=");
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
} else if (line.substr(0, pos) == "frameWidth") {
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
} else if (line.substr(0, pos) == "frameHeight") {
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
if (framesPerRow == 0 && frameWidth > 0) {
framesPerRow = texture->getWidth() / frameWidth;
}
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
const int w = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight;
maxTiles = w * h;
}
}
}
}
return as;
}
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose) {
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
std::ifstream file(filePath);
if (!file.good()) {
if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
animatedSprite_t as;
as.texture = texture;
return as;
}
return parseAnimationStream(file, texture, filename, verbose);
}
// Carga la animación desde bytes en memoria
animatedSprite_t loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &nameForLogs, bool verbose) {
if (bytes.empty()) {
animatedSprite_t as;
as.texture = texture;
return as;
}
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
return parseAnimationStream(ss, texture, nameForLogs, verbose);
}
// Constructor
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, std::string file, std::vector<std::string> *buffer) {
// Copia los punteros
setTexture(texture);
setRenderer(renderer);
// Carga las animaciones
if (file != "") {
animatedSprite_t as = loadAnimationFromFile(texture, file);
// Copia los datos de las animaciones
for (auto animation : as.animations) {
this->animation.push_back(animation);
}
}
else if (buffer) {
loadFromVector(buffer);
}
// Inicializa variables
currentAnimation = 0;
}
// Constructor
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation) {
// Copia los punteros
setTexture(animation->texture);
setRenderer(renderer);
// Inicializa variables
currentAnimation = 0;
// Copia los datos de las animaciones
for (auto a : animation->animations) {
this->animation.push_back(a);
}
}
// Destructor
AnimatedSprite::~AnimatedSprite() {
for (auto &a : animation) {
a.frames.clear();
}
animation.clear();
}
// Obtiene el indice de la animación a partir del nombre
int AnimatedSprite::getIndex(std::string name) {
int index = -1;
for (auto a : animation) {
index++;
if (a.name == name) {
return index;
}
}
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << std::endl;
return -1;
}
// Calcula el frame correspondiente a la animación
void AnimatedSprite::animate() {
if (!enabled || animation[currentAnimation].speed == 0) {
return;
}
// Calcula el frame actual a partir del contador
animation[currentAnimation].currentFrame = animation[currentAnimation].counter / animation[currentAnimation].speed;
// Si alcanza el final de la animación, reinicia el contador de la animación
// en función de la variable loop y coloca el nuevo frame
if (animation[currentAnimation].currentFrame >= (int)animation[currentAnimation].frames.size()) {
if (animation[currentAnimation].loop == -1) { // Si no hay loop, deja el último frame
animation[currentAnimation].currentFrame = animation[currentAnimation].frames.size();
animation[currentAnimation].completed = true;
} else { // Si hay loop, vuelve al frame indicado
animation[currentAnimation].counter = 0;
animation[currentAnimation].currentFrame = animation[currentAnimation].loop;
}
}
// En caso contrario
else {
// Escoge el frame correspondiente de la animación
setSpriteClip(animation[currentAnimation].frames[animation[currentAnimation].currentFrame]);
// Incrementa el contador de la animacion
animation[currentAnimation].counter++;
}
}
// Obtiene el numero de frames de la animación actual
int AnimatedSprite::getNumFrames() {
return (int)animation[currentAnimation].frames.size();
}
// Establece el frame actual de la animación
void AnimatedSprite::setCurrentFrame(int num) {
// Descarta valores fuera de rango
if (num >= (int)animation[currentAnimation].frames.size()) {
num = 0;
}
// Cambia el valor de la variable
animation[currentAnimation].currentFrame = num;
animation[currentAnimation].counter = 0;
// Escoge el frame correspondiente de la animación
setSpriteClip(animation[currentAnimation].frames[animation[currentAnimation].currentFrame]);
}
// Establece el valor del contador
void AnimatedSprite::setAnimationCounter(std::string name, int num) {
animation[getIndex(name)].counter = num;
}
// Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(std::string name, int speed) {
animation[getIndex(name)].counter = speed;
}
// Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(int index, int speed) {
animation[index].counter = speed;
}
// Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(std::string name, int loop) {
animation[getIndex(name)].loop = loop;
}
// Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(int index, int loop) {
animation[index].loop = loop;
}
// Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(std::string name, bool value) {
animation[getIndex(name)].completed = value;
}
// OLD - Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(int index, bool value) {
animation[index].completed = value;
}
// Comprueba si ha terminado la animación
bool AnimatedSprite::animationIsCompleted() {
return animation[currentAnimation].completed;
}
// Devuelve el rectangulo de una animación y frame concreto
SDL_Rect AnimatedSprite::getAnimationClip(std::string name, Uint8 index) {
return animation[getIndex(name)].frames[index];
}
// Devuelve el rectangulo de una animación y frame concreto
SDL_Rect AnimatedSprite::getAnimationClip(int indexA, Uint8 indexF) {
return animation[indexA].frames[indexF];
}
// Carga la animación desde un vector
bool AnimatedSprite::loadFromVector(std::vector<std::string> *source) {
// Inicializa variables
int framesPerRow = 0;
int frameWidth = 0;
int frameHeight = 0;
int maxTiles = 0;
// Indicador de éxito en el proceso
bool success = true;
std::string line;
// Recorre todo el vector
int index = 0;
while (index < (int)source->size()) {
// Lee desde el vector
line = source->at(index);
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
if (line == "[animation]") {
animation_t buffer;
buffer.counter = 0;
buffer.currentFrame = 0;
buffer.completed = false;
do {
// Aumenta el indice para leer la siguiente linea
index++;
line = source->at(index);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "name") {
buffer.name = line.substr(pos + 1, line.length());
}
else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "loop") {
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frames") {
// Se introducen los valores separados por comas en un vector
std::stringstream ss(line.substr(pos + 1, line.length()));
std::string tmp;
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
while (getline(ss, tmp, ',')) {
// Comprueba que el tile no sea mayor que el maximo indice permitido
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
}
else {
std::cout << "Warning: unknown parameter " << line.substr(0, pos).c_str() << std::endl;
success = false;
}
}
} while (line != "[/animation]");
// Añade la animación al vector de animaciones
animation.push_back(buffer);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameWidth") {
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameHeight") {
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
}
else {
std::cout << "Warning: unknown parameter " << line.substr(0, pos).c_str() << std::endl;
success = false;
}
// Normaliza valores
if (framesPerRow == 0 && frameWidth > 0) {
framesPerRow = texture->getWidth() / frameWidth;
}
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
const int w = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight;
maxTiles = w * h;
}
}
}
// Una vez procesada la linea, aumenta el indice para pasar a la siguiente
index++;
}
// Pone un valor por defecto
setRect({0, 0, frameWidth, frameHeight});
return success;
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(std::string name) {
const int newAnimation = getIndex(name);
if (currentAnimation != newAnimation) {
currentAnimation = newAnimation;
animation[currentAnimation].currentFrame = 0;
animation[currentAnimation].counter = 0;
animation[currentAnimation].completed = false;
}
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(int index) {
const int newAnimation = index;
if (currentAnimation != newAnimation) {
currentAnimation = newAnimation;
animation[currentAnimation].currentFrame = 0;
animation[currentAnimation].counter = 0;
animation[currentAnimation].completed = false;
}
}
// Actualiza las variables del objeto
void AnimatedSprite::update() {
animate();
MovingSprite::update();
}
// Establece el rectangulo para un frame de una animación
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h) {
animation[index_animation].frames.push_back({x, y, w, h});
}
// OLD - Establece el contador para todas las animaciones
void AnimatedSprite::setAnimationCounter(int value) {
for (auto &a : animation) {
a.counter = value;
}
}
// Reinicia la animación
void AnimatedSprite::resetAnimation() {
animation[currentAnimation].currentFrame = 0;
animation[currentAnimation].counter = 0;
animation[currentAnimation].completed = false;
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string, basic_string
#include <vector> // for vector
#include "core/rendering/movingsprite.h" // for MovingSprite
class Texture;
struct animation_t {
std::string name; // Nombre de la animacion
std::vector<SDL_Rect> frames; // Cada uno de los frames que componen la animación
int speed; // Velocidad de la animación
int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
bool completed; // Indica si ha finalizado la animación
int currentFrame; // Frame actual
int counter; // Contador para las animaciones
};
struct animatedSprite_t {
std::vector<animation_t> animations; // Vector con las diferentes animaciones
Texture *texture; // Textura con los graficos para el sprite
};
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose = false);
// Carga la animación desde bytes en memoria
animatedSprite_t loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &nameForLogs = "", bool verbose = false);
class AnimatedSprite : public MovingSprite {
private:
// Variables
std::vector<animation_t> animation; // Vector con las diferentes animaciones
int currentAnimation; // Animacion activa
public:
// Constructor
AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, std::string file = "", std::vector<std::string> *buffer = nullptr);
AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation);
// Destructor
~AnimatedSprite();
// Calcula el frame correspondiente a la animación actual
void animate();
// Obtiene el numero de frames de la animación actual
int getNumFrames();
// Establece el frame actual de la animación
void setCurrentFrame(int num);
// Establece el valor del contador
void setAnimationCounter(std::string name, int num);
// Establece la velocidad de una animación
void setAnimationSpeed(std::string name, int speed);
void setAnimationSpeed(int index, int speed);
// Establece el frame al que vuelve la animación al finalizar
void setAnimationLoop(std::string name, int loop);
void setAnimationLoop(int index, int loop);
// Establece el valor de la variable
void setAnimationCompleted(std::string name, bool value);
void setAnimationCompleted(int index, bool value);
// Comprueba si ha terminado la animación
bool animationIsCompleted();
// Devuelve el rectangulo de una animación y frame concreto
SDL_Rect getAnimationClip(std::string name = "default", Uint8 index = 0);
SDL_Rect getAnimationClip(int indexA = 0, Uint8 indexF = 0);
// Obtiene el indice de la animación a partir del nombre
int getIndex(std::string name);
// Carga la animación desde un vector
bool loadFromVector(std::vector<std::string> *source);
// Establece la animacion actual
void setCurrentAnimation(std::string name = "default");
void setCurrentAnimation(int index = 0);
// Actualiza las variables del objeto
void update();
// OLD - Establece el rectangulo para un frame de una animación
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h);
// OLD - Establece el contador para todas las animaciones
void setAnimationCounter(int value);
// Reinicia la animación
void resetAnimation();
};

View File

@@ -0,0 +1,180 @@
#include "core/rendering/fade.h"
#include <SDL3/SDL.h>
#include <stdlib.h> // for rand
#include <iostream> // for char_traits, basic_ostream, operator<<
#include "game/defaults.hpp" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
// Constructor
Fade::Fade(SDL_Renderer *renderer) {
mRenderer = renderer;
mBackbuffer = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
if (mBackbuffer != nullptr) {
SDL_SetTextureScaleMode(mBackbuffer, SDL_SCALEMODE_NEAREST);
}
if (mBackbuffer == nullptr) {
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
}
// Destructor
Fade::~Fade() {
SDL_DestroyTexture(mBackbuffer);
mBackbuffer = nullptr;
}
// Inicializa las variables
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
mFadeType = FADE_CENTER;
mEnabled = false;
mFinished = false;
mCounter = 0;
mR = r;
mG = g;
mB = b;
mROriginal = r;
mGOriginal = g;
mBOriginal = b;
mLastSquareTicks = 0;
mSquaresDrawn = 0;
mFullscreenDone = false;
}
// Pinta una transición en pantalla
void Fade::render() {
if (mEnabled && !mFinished) {
switch (mFadeType) {
case FADE_FULLSCREEN: {
if (!mFullscreenDone) {
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
int alpha = mCounter * 4;
if (alpha >= 255) {
alpha = 255;
mFullscreenDone = true;
// Deja todos los buffers del mismo color
SDL_SetRenderTarget(mRenderer, mBackbuffer);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
SDL_SetRenderTarget(mRenderer, nullptr);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
mFinished = true;
} else {
// Dibujamos sobre el renderizador
SDL_SetRenderTarget(mRenderer, nullptr);
// Copia el backbuffer con la imagen que había al renderizador
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, alpha);
SDL_RenderFillRect(mRenderer, &fRect1);
}
}
break;
}
case FADE_CENTER: {
SDL_FRect fR1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
SDL_FRect fR2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
for (int i = 0; i < mCounter; i++) {
fR1.h = fR2.h = (float)(i * 4);
fR2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
SDL_RenderFillRect(mRenderer, &fR1);
SDL_RenderFillRect(mRenderer, &fR2);
}
if ((mCounter * 4) > GAMECANVAS_HEIGHT)
mFinished = true;
break;
}
case FADE_RANDOM_SQUARE: {
Uint32 now = SDL_GetTicks();
if (mSquaresDrawn < 50 && now - mLastSquareTicks >= 100) {
mLastSquareTicks = now;
SDL_FRect fRs = {0, 0, 32, 32};
// Crea un color al azar
Uint8 r = 255 * (rand() % 2);
Uint8 g = 255 * (rand() % 2);
Uint8 b = 255 * (rand() % 2);
SDL_SetRenderDrawColor(mRenderer, r, g, b, 64);
// Dibujamos sobre el backbuffer
SDL_SetRenderTarget(mRenderer, mBackbuffer);
fRs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
fRs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
SDL_RenderFillRect(mRenderer, &fRs);
// Volvemos a usar el renderizador de forma normal
SDL_SetRenderTarget(mRenderer, nullptr);
mSquaresDrawn++;
}
// Copiamos el backbuffer al renderizador
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
if (mSquaresDrawn >= 50) {
mFinished = true;
}
break;
}
default:
break;
}
}
if (mFinished) {
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
}
}
// Actualiza las variables internas
void Fade::update() {
if (mEnabled)
mCounter++;
}
// Activa el fade
void Fade::activateFade() {
mEnabled = true;
mFinished = false;
mCounter = 0;
mSquaresDrawn = 0;
mLastSquareTicks = 0;
mFullscreenDone = false;
mR = mROriginal;
mG = mGOriginal;
mB = mBOriginal;
}
// Comprueba si está activo
bool Fade::isEnabled() {
return mEnabled;
}
// Comprueba si ha terminado la transicion
bool Fade::hasEnded() {
return mFinished;
}
// Establece el tipo de fade
void Fade::setFadeType(Uint8 fadeType) {
mFadeType = fadeType;
}

View File

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

View File

@@ -0,0 +1,312 @@
#include "core/rendering/movingsprite.h"
#include "core/rendering/texture.h" // for Texture
// Constructor
MovingSprite::MovingSprite(float x, float y, int w, int h, float velx, float vely, float accelx, float accely, Texture *texture, SDL_Renderer *renderer) {
// Copia los punteros
this->texture = texture;
this->renderer = renderer;
// Establece el alto y el ancho del sprite
this->w = w;
this->h = h;
// Establece la posición X,Y del sprite
this->x = x;
this->y = y;
xPrev = x;
yPrev = y;
// Establece la velocidad X,Y del sprite
vx = velx;
vy = vely;
// Establece la aceleración X,Y del sprite
ax = accelx;
ay = accely;
// Establece el zoom W,H del sprite
zoomW = 1;
zoomH = 1;
// Establece el angulo con el que se dibujará
angle = (double)0;
// Establece los valores de rotacion
rotateEnabled = false;
rotateSpeed = 0;
rotateAmount = (double)0;
// Contador interno
counter = 0;
// Establece el rectangulo de donde coger la imagen
spriteClip = {0, 0, w, h};
// Establece el centro de rotación
center = nullptr;
// Establece el tipo de volteado
currentFlip = SDL_FLIP_NONE;
};
// Reinicia todas las variables
void MovingSprite::clear() {
x = 0.0f; // Posición en el eje X
y = 0.0f; // Posición en el eje Y
vx = 0.0f; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
vy = 0.0f; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
ax = 0.0f; // Aceleración en el eje X. Variación de la velocidad
ay = 0.0f; // Aceleración en el eje Y. Variación de la velocidad
zoomW = 1.0f; // Zoom aplicado a la anchura
zoomH = 1.0f; // Zoom aplicado a la altura
angle = 0.0; // Angulo para dibujarlo
rotateEnabled = false; // Indica si ha de rotar
center = nullptr; // Centro de rotación
rotateSpeed = 0; // Velocidad de giro
rotateAmount = 0.0; // Cantidad de grados a girar en cada iteración
counter = 0; // Contador interno
currentFlip = SDL_FLIP_NONE; // Establece como se ha de voltear el sprite
}
// Mueve el sprite
void MovingSprite::move() {
if (enabled) {
xPrev = x;
yPrev = y;
x += vx;
y += vy;
vx += ax;
vy += ay;
}
}
// Muestra el sprite por pantalla
void MovingSprite::render() {
if (enabled) {
texture->render(renderer, (int)x, (int)y, &spriteClip, zoomW, zoomH, angle, center, currentFlip);
}
}
// Obtiene el valor de la variable
float MovingSprite::getPosX() {
return x;
}
// Obtiene el valor de la variable
float MovingSprite::getPosY() {
return y;
}
// Obtiene el valor de la variable
float MovingSprite::getVelX() {
return vx;
}
// Obtiene el valor de la variable
float MovingSprite::getVelY() {
return vy;
}
// Obtiene el valor de la variable
float MovingSprite::getAccelX() {
return ax;
}
// Obtiene el valor de la variable
float MovingSprite::getAccelY() {
return ay;
}
// Obtiene el valor de la variable
float MovingSprite::getZoomW() {
return zoomW;
}
// Obtiene el valor de la variable
float MovingSprite::getZoomH() {
return zoomH;
}
// Obtiene el valor de la variable
double MovingSprite::getAngle() {
return angle;
}
// Establece la posición y el tamaño del objeto
void MovingSprite::setRect(SDL_Rect rect) {
x = (float)rect.x;
y = (float)rect.y;
w = rect.w;
h = rect.h;
}
// Establece el valor de la variable
void MovingSprite::setPosX(float value) {
x = value;
}
// Establece el valor de la variable
void MovingSprite::setPosY(float value) {
y = value;
}
// Establece el valor de la variable
void MovingSprite::setVelX(float value) {
vx = value;
}
// Establece el valor de la variable
void MovingSprite::setVelY(float value) {
vy = value;
}
// Establece el valor de la variable
void MovingSprite::setAccelX(float value) {
ax = value;
}
// Establece el valor de la variable
void MovingSprite::setAccelY(float value) {
ay = value;
}
// Establece el valor de la variable
void MovingSprite::setZoomW(float value) {
zoomW = value;
}
// Establece el valor de la variable
void MovingSprite::setZoomH(float value) {
zoomH = value;
}
// Establece el valor de la variable
void MovingSprite::setAngle(double value) {
angle = value;
}
// Incrementa el valor de la variable
void MovingSprite::incAngle(double value) {
angle += value;
}
// Decrementa el valor de la variable
void MovingSprite::decAngle(double value) {
angle -= value;
}
// Obtiene el valor de la variable
bool MovingSprite::getRotate() {
return rotateEnabled;
}
// Obtiene el valor de la variable
Uint16 MovingSprite::getRotateSpeed() {
return rotateSpeed;
}
// Establece la rotacion
void MovingSprite::rotate() {
if (enabled)
if (rotateEnabled) {
if (counter % rotateSpeed == 0) {
incAngle(rotateAmount);
}
}
}
// Establece el valor de la variable
void MovingSprite::setRotate(bool value) {
rotateEnabled = value;
}
// Establece el valor de la variable
void MovingSprite::setRotateSpeed(int value) {
if (value < 1) {
rotateSpeed = 1;
} else {
rotateSpeed = value;
}
}
// Establece el valor de la variable
void MovingSprite::setRotateAmount(double value) {
rotateAmount = value;
}
// Establece el valor de la variable
void MovingSprite::disableRotate() {
rotateEnabled = false;
angle = (double)0;
}
// Actualiza las variables internas del objeto
void MovingSprite::update() {
move();
rotate();
if (enabled) {
++counter %= 60000;
}
}
// Cambia el sentido de la rotación
void MovingSprite::switchRotate() {
rotateAmount *= -1;
}
// Establece el valor de la variable
void MovingSprite::setFlip(SDL_FlipMode flip) {
currentFlip = flip;
}
// Gira el sprite horizontalmente
void MovingSprite::flip() {
currentFlip = (currentFlip == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL;
}
// Obtiene el valor de la variable
SDL_FlipMode MovingSprite::getFlip() {
return currentFlip;
}
// Devuelve el rectangulo donde está el sprite
SDL_Rect MovingSprite::getRect() {
const SDL_Rect rect = {(int)x, (int)y, w, h};
return rect;
}
// Deshace el último movimiento
void MovingSprite::undoMove() {
x = xPrev;
y = yPrev;
}
// Deshace el último movimiento en el eje X
void MovingSprite::undoMoveX() {
x = xPrev;
}
// Deshace el último movimiento en el eje Y
void MovingSprite::undoMoveY() {
y = yPrev;
}
// Pone a cero las velocidades de desplacamiento
void MovingSprite::clearVel() {
vx = vy = 0.0f;
}
// Devuelve el incremento en el eje X en pixels
int MovingSprite::getIncX() {
return (int)x - (int)xPrev;
}

View File

@@ -0,0 +1,163 @@
#pragma once
#include <SDL3/SDL.h>
#include "core/rendering/sprite.h" // for Sprite
class Texture;
// Clase MovingSprite. Añade posicion y velocidad en punto flotante
class MovingSprite : public Sprite {
protected:
float x; // Posición en el eje X
float y; // Posición en el eje Y
float xPrev; // Posición anterior en el eje X
float yPrev; // Posición anterior en el eje Y
float vx; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
float vy; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
float ax; // Aceleración en el eje X. Variación de la velocidad
float ay; // Aceleración en el eje Y. Variación de la velocidad
float zoomW; // Zoom aplicado a la anchura
float zoomH; // Zoom aplicado a la altura
double angle; // Angulo para dibujarlo
bool rotateEnabled; // Indica si ha de rotar
int rotateSpeed; // Velocidad de giro
double rotateAmount; // Cantidad de grados a girar en cada iteración
int counter; // Contador interno
SDL_Point *center; // Centro de rotación
SDL_FlipMode currentFlip; // Indica como se voltea el sprite
public:
// Constructor
MovingSprite(float x = 0, float y = 0, int w = 0, int h = 0, float velx = 0, float vely = 0, float accelx = 0, float accely = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr);
// Mueve el sprite
void move();
// Rota el sprite
void rotate();
// Actualiza las variables internas del objeto
void update();
// Reinicia todas las variables
void clear();
// Muestra el sprite por pantalla
void render();
// Obten el valor de la variable
float getPosX();
// Obten el valor de la variable
float getPosY();
// Obten el valor de la variable
float getVelX();
// Obten el valor de la variable
float getVelY();
// Obten el valor de la variable
float getAccelX();
// Obten el valor de la variable
float getAccelY();
// Obten el valor de la variable
float getZoomW();
// Obten el valor de la variable
float getZoomH();
// Obten el valor de la variable
double getAngle();
// Obtiene el valor de la variable
bool getRotate();
// Obtiene el valor de la variable
Uint16 getRotateSpeed();
// Establece la posición y el tamaño del objeto
void setRect(SDL_Rect rect);
// Establece el valor de la variable
void setPosX(float value);
// Establece el valor de la variable
void setPosY(float value);
// Establece el valor de la variable
void setVelX(float value);
// Establece el valor de la variable
void setVelY(float value);
// Establece el valor de la variable
void setAccelX(float value);
// Establece el valor de la variable
void setAccelY(float value);
// Establece el valor de la variable
void setZoomW(float value);
// Establece el valor de la variable
void setZoomH(float value);
// Establece el valor de la variable
void setAngle(double vaue);
// Incrementa el valor de la variable
void incAngle(double value);
// Decrementa el valor de la variable
void decAngle(double value);
// Establece el valor de la variable
void setRotate(bool value);
// Establece el valor de la variable
void setRotateSpeed(int value);
// Establece el valor de la variable
void setRotateAmount(double value);
// Quita el efecto de rotación y deja el sprite en su angulo inicial.
void disableRotate();
// Cambia el sentido de la rotación
void switchRotate();
// Establece el valor de la variable
void setFlip(SDL_FlipMode flip);
// Gira el sprite horizontalmente
void flip();
// Obtiene el valor de la variable
SDL_FlipMode getFlip();
// Devuelve el rectangulo donde está el sprite
SDL_Rect getRect();
// Deshace el último movimiento
void undoMove();
// Deshace el último movimiento en el eje X
void undoMoveX();
// Deshace el último movimiento en el eje Y
void undoMoveY();
// Pone a cero las velocidades de desplacamiento
void clearVel();
// Devuelve el incremento en el eje X en pixels
int getIncX();
};

View File

@@ -0,0 +1,369 @@
#include "core/rendering/screen.h"
#include <SDL3/SDL.h>
#include <algorithm> // for max, min
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include <string> // for basic_string, char_traits, string
#include "core/input/mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime
#include "core/rendering/text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_STROKE
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
// --- Fix per a fullscreen/resize en Emscripten ---
//
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
// anterior i el viewport queda desencuadrat.
//
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
// del event loop (el canvas encara no està estable en el moment del callback)
// i cridar setVideoMode() amb el flag de fullscreen actualitzat. La crida
// interna a SDL_SetWindowFullscreen(false) és la peça que realment fa eixir
// SDL del seu estat intern de fullscreen — sense això res més funciona.
namespace {
Screen *g_screen_instance = nullptr;
void deferredCanvasResize(void * /*userData*/) {
if (g_screen_instance) {
g_screen_instance->handleCanvasResized();
}
}
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent *event, void * /*userData*/) {
if (g_screen_instance && event) {
g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
}
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent * /*event*/, void * /*userData*/) {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif // __EMSCRIPTEN__
// Constructor
Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options_t *options) {
// Inicializa variables
this->window = window;
this->renderer = renderer;
this->options = options;
this->asset = asset;
gameCanvasWidth = options->gameWidth;
gameCanvasHeight = options->gameHeight;
borderWidth = options->borderWidth * 2;
borderHeight = options->borderHeight * 2;
// Define el color del borde para el modo de pantalla completa
borderColor = {0x00, 0x00, 0x00};
// Crea la textura donde se dibujan los graficos del juego
gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, gameCanvasWidth, gameCanvasHeight);
if (gameCanvas != nullptr) {
SDL_SetTextureScaleMode(gameCanvas, options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
}
if (gameCanvas == nullptr) {
if (options->console) {
std::cout << "gameCanvas could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
}
// Establece el modo de video
setVideoMode(options->videoMode != 0);
// Inicializa el sistema de notificaciones (Text compartido de Resource)
notificationText = Resource::get()->getText("8bithud");
notificationMessage = "";
notificationTextColor = {0xFF, 0xFF, 0xFF};
notificationOutlineColor = {0x00, 0x00, 0x00};
notificationEndTime = 0;
notificationY = 2;
// Registra callbacks natius d'Emscripten per a fullscreen/orientation
registerEmscriptenEventCallbacks();
}
// Destructor
Screen::~Screen() {
// notificationText es propiedad de Resource — no liberar.
SDL_DestroyTexture(gameCanvas);
}
// Limpia la pantalla
void Screen::clean(color_t color) {
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 0xFF);
SDL_RenderClear(renderer);
}
// Prepara para empezar a dibujar en la textura de juego
void Screen::start() {
SDL_SetRenderTarget(renderer, gameCanvas);
}
// Vuelca el contenido del renderizador en pantalla
void Screen::blit() {
// Dibuja la notificación activa sobre el gameCanvas antes de presentar
SDL_SetRenderTarget(renderer, gameCanvas);
renderNotification();
// Vuelve a dejar el renderizador en modo normal
SDL_SetRenderTarget(renderer, nullptr);
// Borra el contenido previo
SDL_SetRenderDrawColor(renderer, borderColor.r, borderColor.g, borderColor.b, 0xFF);
SDL_RenderClear(renderer);
// Copia la textura de juego en el renderizador en la posición adecuada
SDL_FRect fdest = {(float)dest.x, (float)dest.y, (float)dest.w, (float)dest.h};
SDL_RenderTexture(renderer, gameCanvas, nullptr, &fdest);
// Muestra por pantalla el renderizador
SDL_RenderPresent(renderer);
}
// ============================================================================
// Video y ventana
// ============================================================================
// Establece el modo de video
void Screen::setVideoMode(bool fullscreen) {
applyFullscreen(fullscreen);
if (fullscreen) {
applyFullscreenLayout();
} else {
applyWindowedLayout();
}
applyLogicalPresentation(fullscreen);
}
// Cambia entre pantalla completa y ventana
void Screen::toggleVideoMode() {
setVideoMode(options->videoMode == 0);
}
// Reduce el zoom de la ventana
auto Screen::decWindowZoom() -> bool {
if (options->videoMode != 0) { return false; }
const int PREV = options->windowSize;
options->windowSize = std::max(options->windowSize - 1, WINDOW_ZOOM_MIN);
if (options->windowSize == PREV) { return false; }
setVideoMode(false);
return true;
}
// Aumenta el zoom de la ventana
auto Screen::incWindowZoom() -> bool {
if (options->videoMode != 0) { return false; }
const int PREV = options->windowSize;
options->windowSize = std::min(options->windowSize + 1, WINDOW_ZOOM_MAX);
if (options->windowSize == PREV) { return false; }
setVideoMode(false);
return true;
}
// Establece el zoom de la ventana directamente
auto Screen::setWindowZoom(int zoom) -> bool {
if (options->videoMode != 0) { return false; }
if (zoom < WINDOW_ZOOM_MIN || zoom > WINDOW_ZOOM_MAX) { return false; }
if (zoom == options->windowSize) { return false; }
options->windowSize = zoom;
setVideoMode(false);
return true;
}
// Establece el escalado entero
void Screen::setIntegerScale(bool enabled) {
if (options->integerScale == enabled) { return; }
options->integerScale = enabled;
setVideoMode(options->videoMode != 0);
}
// Alterna el escalado entero
void Screen::toggleIntegerScale() {
setIntegerScale(!options->integerScale);
}
// Establece el V-Sync
void Screen::setVSync(bool enabled) {
options->vSync = enabled;
SDL_SetRenderVSync(renderer, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}
// Alterna el V-Sync
void Screen::toggleVSync() {
setVSync(!options->vSync);
}
// Cambia el color del borde
void Screen::setBorderColor(color_t color) {
borderColor = color;
}
// ============================================================================
// Helpers privados de setVideoMode
// ============================================================================
// SDL_SetWindowFullscreen + visibilidad del cursor
void Screen::applyFullscreen(bool fullscreen) {
SDL_SetWindowFullscreen(window, fullscreen);
if (fullscreen) {
SDL_HideCursor();
Mouse::cursorVisible = false;
} else {
SDL_ShowCursor();
Mouse::cursorVisible = true;
Mouse::lastMouseMoveTime = SDL_GetTicks();
}
}
// Calcula windowWidth/Height/dest para el modo ventana y aplica SDL_SetWindowSize
void Screen::applyWindowedLayout() {
if (options->borderEnabled) {
windowWidth = gameCanvasWidth + borderWidth;
windowHeight = gameCanvasHeight + borderHeight;
dest = {0 + (borderWidth / 2), 0 + (borderHeight / 2), gameCanvasWidth, gameCanvasHeight};
} else {
windowWidth = gameCanvasWidth;
windowHeight = gameCanvasHeight;
dest = {0, 0, gameCanvasWidth, gameCanvasHeight};
}
#ifdef __EMSCRIPTEN__
windowWidth *= WASM_RENDER_SCALE;
windowHeight *= WASM_RENDER_SCALE;
dest.w *= WASM_RENDER_SCALE;
dest.h *= WASM_RENDER_SCALE;
#endif
// Modifica el tamaño de la ventana
SDL_SetWindowSize(window, windowWidth * options->windowSize, windowHeight * options->windowSize);
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Obtiene el tamaño de la ventana en fullscreen y calcula el rect del juego
void Screen::applyFullscreenLayout() {
SDL_GetWindowSize(window, &windowWidth, &windowHeight);
computeFullscreenGameRect();
}
// Calcula el rectángulo dest para fullscreen: integerScale / keepAspect / stretched
void Screen::computeFullscreenGameRect() {
if (options->integerScale) {
// Calcula el tamaño de la escala máxima
int scale = 0;
while (((gameCanvasWidth * (scale + 1)) <= windowWidth) && ((gameCanvasHeight * (scale + 1)) <= windowHeight)) {
scale++;
}
dest.w = gameCanvasWidth * scale;
dest.h = gameCanvasHeight * scale;
dest.x = (windowWidth - dest.w) / 2;
dest.y = (windowHeight - dest.h) / 2;
} else if (options->keepAspect) {
float ratio = (float)gameCanvasWidth / (float)gameCanvasHeight;
if ((windowWidth - gameCanvasWidth) >= (windowHeight - gameCanvasHeight)) {
dest.h = windowHeight;
dest.w = (int)((windowHeight * ratio) + 0.5f);
dest.x = (windowWidth - dest.w) / 2;
dest.y = (windowHeight - dest.h) / 2;
} else {
dest.w = windowWidth;
dest.h = (int)((windowWidth / ratio) + 0.5f);
dest.x = (windowWidth - dest.w) / 2;
dest.y = (windowHeight - dest.h) / 2;
}
} else {
dest.w = windowWidth;
dest.h = windowHeight;
dest.x = dest.y = 0;
}
}
// Aplica la logical presentation y persiste el estado en options
void Screen::applyLogicalPresentation(bool fullscreen) {
SDL_SetRenderLogicalPresentation(renderer, windowWidth, windowHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
// Actualiza las opciones
options->videoMode = fullscreen ? SDL_WINDOW_FULLSCREEN : 0;
options->screen.windowWidth = windowWidth;
options->screen.windowHeight = windowHeight;
}
// ============================================================================
// Notificaciones
// ============================================================================
// Muestra una notificación en la línea superior durante durationMs
void Screen::notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs) {
notificationMessage = text;
notificationTextColor = textColor;
notificationOutlineColor = outlineColor;
notificationEndTime = SDL_GetTicks() + durationMs;
}
// Limpia la notificación actual
void Screen::clearNotification() {
notificationEndTime = 0;
notificationMessage.clear();
}
// Dibuja la notificación activa (si la hay) sobre el gameCanvas
void Screen::renderNotification() {
if (SDL_GetTicks() >= notificationEndTime) {
return;
}
notificationText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE,
gameCanvasWidth / 2,
notificationY,
notificationMessage,
1,
notificationTextColor,
1,
notificationOutlineColor);
}
// ============================================================================
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
// ============================================================================
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
// La crida a SDL_SetWindowFullscreen + SDL_SetRenderLogicalPresentation
// que fa setVideoMode és l'única manera de resincronitzar l'estat intern
// de SDL amb el canvas HTML real.
setVideoMode(options->videoMode != 0);
#endif
}
void Screen::syncFullscreenFlagFromBrowser(bool isFullscreen) {
#ifdef __EMSCRIPTEN__
options->videoMode = isFullscreen ? SDL_WINDOW_FULLSCREEN : 0;
#else
(void)isFullscreen;
#endif
}
void Screen::registerEmscriptenEventCallbacks() {
#ifdef __EMSCRIPTEN__
// IMPORTANT: NO registrem resize callback. En mòbil, fer scroll fa que el
// navegador oculti/mostri la barra d'URL, disparant un resize del DOM per
// cada scroll. Això portava a cridar setVideoMode per cada scroll, que
// re-aplicava la logical presentation i corrompia el viewport intern de SDL.
g_screen_instance = this;
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string
#include "utils/utils.h" // for color_t
class Asset;
class Text;
// Tipos de filtro
constexpr int FILTER_NEAREST = 0;
constexpr int FILTER_LINEAL = 1;
class Screen {
public:
// Constantes
static constexpr int WINDOW_ZOOM_MIN = 1;
static constexpr int WINDOW_ZOOM_MAX = 4;
#ifdef __EMSCRIPTEN__
// En WASM el tamaño de ventana está fijado a 1x, así que escalamos el
// renderizado por 3 aprovechando el modo NEAREST de la textura del juego
// para que los píxeles salgan nítidos.
static constexpr int WASM_RENDER_SCALE = 3;
#endif
// Constructor y destructor
Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options_t *options);
~Screen();
// Render loop
void clean(color_t color = {0x00, 0x00, 0x00}); // Limpia la pantalla
void start(); // Prepara para empezar a dibujar en la textura de juego
void blit(); // Vuelca el contenido del renderizador en pantalla
// Video y ventana
void setVideoMode(bool fullscreen); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten
void syncFullscreenFlagFromBrowser(bool isFullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten
void toggleIntegerScale(); // Alterna el escalado entero
void setIntegerScale(bool enabled); // Establece el escalado entero
void toggleVSync(); // Alterna el V-Sync
void setVSync(bool enabled); // Establece el V-Sync
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió)
auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió)
// Borde
void setBorderColor(color_t color); // Cambia el color del borde
// Notificaciones
void notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
void clearNotification(); // Limpia la notificación actual
private:
// Helpers internos de setVideoMode
void applyFullscreen(bool fullscreen); // SDL_SetWindowFullscreen + visibilidad del cursor
void applyWindowedLayout(); // Calcula windowWidth/Height/dest + SDL_SetWindowSize + SDL_SetWindowPosition
void applyFullscreenLayout(); // SDL_GetWindowSize + delegación a computeFullscreenGameRect
void computeFullscreenGameRect(); // Calcula dest en fullscreen (integerScale / keepAspect / stretched)
void applyLogicalPresentation(bool fullscreen); // SDL_SetRenderLogicalPresentation + persistencia a options
// Emscripten
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos de Emscripten para fullscreenchange y orientationchange. No-op fuera de Emscripten
// Notificaciones
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
// Objetos y punteros
SDL_Window *window; // Ventana de la aplicación
SDL_Renderer *renderer; // El renderizador de la ventana
Asset *asset; // Objeto con el listado de recursos
SDL_Texture *gameCanvas; // Textura para completar la ventana de juego hasta la pantalla completa
options_t *options; // Variable con todas las opciones del programa
// Variables
int windowWidth; // Ancho de la pantalla o ventana
int windowHeight; // Alto de la pantalla o ventana
int gameCanvasWidth; // Resolución interna del juego. Es el ancho de la textura donde se dibuja el juego
int gameCanvasHeight; // Resolución interna del juego. Es el alto de la textura donde se dibuja el juego
int borderWidth; // Anchura del borde
int borderHeight; // Altura del borde
SDL_Rect dest; // Coordenadas donde se va a dibujar la textura del juego sobre la pantalla o ventana
color_t borderColor; // Color del borde añadido a la textura de juego para rellenar la pantalla
// Notificaciones - una sola activa, sin apilación ni animaciones
Text *notificationText; // Fuente 8bithud dedicada a las notificaciones
std::string notificationMessage; // Texto a mostrar
color_t notificationTextColor; // Color del texto
color_t notificationOutlineColor; // Color del outline
Uint32 notificationEndTime; // SDL_GetTicks() hasta el cual se muestra
int notificationY; // Fila vertical en el canvas virtual
};

View File

@@ -0,0 +1,164 @@
#include "core/rendering/smartsprite.h"
#include "core/rendering/movingsprite.h" // for MovingSprite
class Texture;
// Constructor
SmartSprite::SmartSprite(Texture *texture, SDL_Renderer *renderer) {
// Copia punteros
setTexture(texture);
setRenderer(renderer);
// Inicializa el objeto
init();
}
// Inicializa el objeto
void SmartSprite::init() {
enabled = false;
enabledCounter = 0;
onDestination = false;
destX = 0;
destY = 0;
counter = 0;
finished = false;
}
// Actualiza la posición y comprueba si ha llegado a su destino
void SmartSprite::update() {
if (enabled) {
// Actualiza las variables internas del objeto
MovingSprite::update();
// Comprueba el movimiento
checkMove();
// Comprueba si ha terminado
checkFinished();
}
}
// Pinta el objeto en pantalla
void SmartSprite::render() {
if (enabled) {
// Muestra el sprite por pantalla
MovingSprite::render();
}
}
// Obtiene el valor de la variable
bool SmartSprite::isEnabled() {
return enabled;
}
// Establece el valor de la variable
void SmartSprite::setEnabled(bool enabled) {
this->enabled = enabled;
}
// Obtiene el valor de la variable
int SmartSprite::getEnabledCounter() {
return enabledCounter;
}
// Establece el valor de la variable
void SmartSprite::setEnabledCounter(int value) {
enabledCounter = value;
}
// Establece el valor de la variable
void SmartSprite::setDestX(int x) {
destX = x;
}
// Establece el valor de la variable
void SmartSprite::setDestY(int y) {
destY = y;
}
// Obtiene el valor de la variable
int SmartSprite::getDestX() {
return destX;
}
// Obtiene el valor de la variable
int SmartSprite::getDestY() {
return destY;
}
// Comprueba el movimiento
void SmartSprite::checkMove() {
// Comprueba si se desplaza en el eje X hacia la derecha
if (getAccelX() > 0 || getVelX() > 0) {
// Comprueba si ha llegado al destino
if (getPosX() > destX) {
// Lo coloca en posición
setPosX(destX);
// Lo detiene
setVelX(0.0f);
setAccelX(0.0f);
}
}
// Comprueba si se desplaza en el eje X hacia la izquierda
else if (getAccelX() < 0 || getVelX() < 0) {
// Comprueba si ha llegado al destino
if (getPosX() < destX) {
// Lo coloca en posición
setPosX(destX);
// Lo detiene
setVelX(0.0f);
setAccelX(0.0f);
}
}
// Comprueba si se desplaza en el eje Y hacia abajo
if (getAccelY() > 0 || getVelY() > 0) {
// Comprueba si ha llegado al destino
if (getPosY() > destY) {
// Lo coloca en posición
setPosY(destY);
// Lo detiene
setVelY(0.0f);
setAccelY(0.0f);
}
}
// Comprueba si se desplaza en el eje Y hacia arriba
else if (getAccelY() < 0 || getVelY() < 0) {
// Comprueba si ha llegado al destino
if (getPosY() < destY) {
// Lo coloca en posición
setPosY(destY);
// Lo detiene
setVelY(0.0f);
setAccelY(0.0f);
}
}
}
// Comprueba si ha terminado
void SmartSprite::checkFinished() {
// Comprueba si ha llegado a su destino
onDestination = (getPosX() == destX && getPosY() == destY) ? true : false;
if (onDestination) { // Si esta en el destino comprueba su contador
if (enabledCounter == 0) { // Si ha llegado a cero, deshabilita el objeto y lo marca como finalizado
finished = true;
} else { // Si no ha llegado a cero, decrementa el contador
enabledCounter--;
}
}
}
// Obtiene el valor de la variable
bool SmartSprite::isOnDestination() {
return onDestination;
}
// Obtiene el valor de la variable
bool SmartSprite::hasFinished() {
return finished;
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <SDL3/SDL.h>
#include "core/rendering/animatedsprite.h" // for AnimatedSprite
class Texture;
// Clase SmartSprite
class SmartSprite : public AnimatedSprite {
private:
// Variables
bool enabled; // Indica si esta habilitado
bool onDestination; // Indica si está en el destino
int destX; // Posicion de destino en el eje X
int destY; // Posicion de destino en el eje Y
int enabledCounter; // Contador para deshabilitarlo
bool finished; // Indica si ya ha terminado
// Comprueba el movimiento
void checkMove();
// Comprueba si ha terminado
void checkFinished();
public:
// Constructor
SmartSprite(Texture *texture, SDL_Renderer *renderer);
// Inicializa el objeto
void init();
// Actualiza la posición y comprueba si ha llegado a su destino
void update();
// Pinta el objeto en pantalla
void render();
// Obtiene el valor de la variable
bool isEnabled();
// Establece el valor de la variable
void setEnabled(bool enabled);
// Obtiene el valor de la variable
int getEnabledCounter();
// Establece el valor de la variable
void setEnabledCounter(int value);
// Establece el valor de la variable
void setDestX(int x);
// Establece el valor de la variable
void setDestY(int y);
// Obtiene el valor de la variable
int getDestX();
// Obtiene el valor de la variable
int getDestY();
// Obtiene el valor de la variable
bool isOnDestination();
// Obtiene el valor de la variable
bool hasFinished();
};

View File

@@ -0,0 +1,166 @@
#include "core/rendering/sprite.h"
#include "core/rendering/texture.h" // for Texture
// Constructor
Sprite::Sprite(int x, int y, int w, int h, Texture *texture, SDL_Renderer *renderer) {
// Establece la posición X,Y del sprite
this->x = x;
this->y = y;
// Establece el alto y el ancho del sprite
this->w = w;
this->h = h;
// Establece el puntero al renderizador de la ventana
this->renderer = renderer;
// Establece la textura donde están los gráficos para el sprite
this->texture = texture;
// Establece el rectangulo de donde coger la imagen
spriteClip = {0, 0, w, h};
// Inicializa variables
enabled = true;
}
Sprite::Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer) {
// Establece la posición X,Y del sprite
x = rect.x;
y = rect.y;
// Establece el alto y el ancho del sprite
w = rect.w;
h = rect.h;
// Establece el puntero al renderizador de la ventana
this->renderer = renderer;
// Establece la textura donde están los gráficos para el sprite
this->texture = texture;
// Establece el rectangulo de donde coger la imagen
spriteClip = {0, 0, w, h};
// Inicializa variables
enabled = true;
}
// Destructor
Sprite::~Sprite() {
texture = nullptr;
renderer = nullptr;
}
// Muestra el sprite por pantalla
void Sprite::render() {
if (enabled) {
texture->render(renderer, x, y, &spriteClip);
}
}
// Obten el valor de la variable
int Sprite::getPosX() {
return x;
}
// Obten el valor de la variable
int Sprite::getPosY() {
return y;
}
// Obten el valor de la variable
int Sprite::getWidth() {
return w;
}
// Obten el valor de la variable
int Sprite::getHeight() {
return h;
}
// Establece la posición del objeto
void Sprite::setPos(SDL_Rect rect) {
this->x = rect.x;
this->y = rect.y;
}
// Establece el valor de la variable
void Sprite::setPosX(int x) {
this->x = x;
}
// Establece el valor de la variable
void Sprite::setPosY(int y) {
this->y = y;
}
// Establece el valor de la variable
void Sprite::setWidth(int w) {
this->w = w;
}
// Establece el valor de la variable
void Sprite::setHeight(int h) {
this->h = h;
}
// Obten el valor de la variable
SDL_Rect Sprite::getSpriteClip() {
return spriteClip;
}
// Establece el valor de la variable
void Sprite::setSpriteClip(SDL_Rect rect) {
spriteClip = rect;
}
// Establece el valor de la variable
void Sprite::setSpriteClip(int x, int y, int w, int h) {
spriteClip = {x, y, w, h};
}
// Obten el valor de la variable
Texture *Sprite::getTexture() {
return texture;
}
// Establece el valor de la variable
void Sprite::setTexture(Texture *texture) {
this->texture = texture;
}
// Obten el valor de la variable
SDL_Renderer *Sprite::getRenderer() {
return renderer;
}
// Establece el valor de la variable
void Sprite::setRenderer(SDL_Renderer *renderer) {
this->renderer = renderer;
}
// Establece el valor de la variable
void Sprite::setEnabled(bool value) {
enabled = value;
}
// Comprueba si el objeto está habilitado
bool Sprite::isEnabled() {
return enabled;
}
// Devuelve el rectangulo donde está el sprite
SDL_Rect Sprite::getRect() {
SDL_Rect rect = {x, y, w, h};
return rect;
}
// Establece los valores de posición y tamaño del sprite
void Sprite::setRect(SDL_Rect rect) {
x = rect.x;
y = rect.y;
w = rect.w;
h = rect.h;
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include <SDL3/SDL.h>
class Texture;
// Clase sprite
class Sprite {
protected:
int x; // Posición en el eje X donde dibujar el sprite
int y; // Posición en el eje Y donde dibujar el sprite
int w; // Ancho del sprite
int h; // Alto del sprite
SDL_Renderer *renderer; // Puntero al renderizador de la ventana
Texture *texture; // Textura donde estan todos los dibujos del sprite
SDL_Rect spriteClip; // Rectangulo de origen de la textura que se dibujará en pantalla
bool enabled; // Indica si el sprite esta habilitado
public:
// Constructor
Sprite(int x = 0, int y = 0, int w = 0, int h = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr);
Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer);
// Destructor
~Sprite();
// Muestra el sprite por pantalla
void render();
// Obten el valor de la variable
int getPosX();
// Obten el valor de la variable
int getPosY();
// Obten el valor de la variable
int getWidth();
// Obten el valor de la variable
int getHeight();
// Establece la posición del objeto
void setPos(SDL_Rect rect);
// Establece el valor de la variable
void setPosX(int x);
// Establece el valor de la variable
void setPosY(int y);
// Establece el valor de la variable
void setWidth(int w);
// Establece el valor de la variable
void setHeight(int h);
// Obten el valor de la variable
SDL_Rect getSpriteClip();
// Establece el valor de la variable
void setSpriteClip(SDL_Rect rect);
// Establece el valor de la variable
void setSpriteClip(int x, int y, int w, int h);
// Obten el valor de la variable
Texture *getTexture();
// Establece el valor de la variable
void setTexture(Texture *texture);
// Obten el valor de la variable
SDL_Renderer *getRenderer();
// Establece el valor de la variable
void setRenderer(SDL_Renderer *renderer);
// Establece el valor de la variable
void setEnabled(bool value);
// Comprueba si el objeto está habilitado
bool isEnabled();
// Devuelve el rectangulo donde está el sprite
SDL_Rect getRect();
// Establece los valores de posición y tamaño del sprite
void setRect(SDL_Rect rect);
};

View File

@@ -0,0 +1,271 @@
#include "core/rendering/text.h"
#include <fstream> // for char_traits, basic_ostream, basic_ifstream, ope...
#include <iostream> // for cout
#include <sstream>
#include "core/rendering/sprite.h" // for Sprite
#include "core/rendering/texture.h" // for Texture
#include "utils/utils.h" // for color_t
// Parser compartido: rellena un textFile_t desde cualquier istream
static void parseTextFileStream(std::istream &rfile, textFile_t &tf) {
std::string buffer;
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxWidth = std::stoi(buffer);
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxHeight = std::stoi(buffer);
int index = 32;
int line_read = 0;
while (std::getline(rfile, buffer)) {
if (line_read % 2 == 1) {
tf.offset[index++].w = std::stoi(buffer);
}
buffer.clear();
line_read++;
}
}
static void computeTextFileOffsets(textFile_t &tf) {
for (int i = 32; i < 128; ++i) {
tf.offset[i].x = ((i - 32) % 15) * tf.boxWidth;
tf.offset[i].y = ((i - 32) / 15) * tf.boxHeight;
}
}
// Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose) {
textFile_t tf;
for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0;
tf.offset[i].y = 0;
tf.offset[i].w = 0;
}
const std::string filename = file.substr(file.find_last_of("\\/") + 1).c_str();
std::ifstream rfile(file);
if (rfile.is_open() && rfile.good()) {
parseTextFileStream(rfile, tf);
if (verbose) {
std::cout << "Text loaded: " << filename.c_str() << std::endl;
}
} else if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
computeTextFileOffsets(tf);
return tf;
}
// Llena una estructura textFile_t desde bytes en memoria
textFile_t LoadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose) {
textFile_t tf;
for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0;
tf.offset[i].y = 0;
tf.offset[i].w = 0;
}
if (!bytes.empty()) {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
parseTextFileStream(ss, tf);
if (verbose) {
std::cout << "Text loaded from memory" << std::endl;
}
}
computeTextFileOffsets(tf);
return tf;
}
// Constructor
Text::Text(std::string bitmapFile, std::string textFile, SDL_Renderer *renderer) {
// Carga los offsets desde el fichero
textFile_t tf = LoadTextFile(textFile);
// Inicializa variables desde la estructura
boxHeight = tf.boxHeight;
boxWidth = tf.boxWidth;
for (int i = 0; i < 128; ++i) {
offset[i].x = tf.offset[i].x;
offset[i].y = tf.offset[i].y;
offset[i].w = tf.offset[i].w;
}
// Crea los objetos
texture = new Texture(renderer, bitmapFile);
sprite = new Sprite({0, 0, boxWidth, boxHeight}, texture, renderer);
// Inicializa variables
fixedWidth = false;
}
// Constructor
Text::Text(std::string textFile, Texture *texture, SDL_Renderer *renderer) {
// Carga los offsets desde el fichero
textFile_t tf = LoadTextFile(textFile);
// Inicializa variables desde la estructura
boxHeight = tf.boxHeight;
boxWidth = tf.boxWidth;
for (int i = 0; i < 128; ++i) {
offset[i].x = tf.offset[i].x;
offset[i].y = tf.offset[i].y;
offset[i].w = tf.offset[i].w;
}
// Crea los objetos
this->texture = nullptr;
sprite = new Sprite({0, 0, boxWidth, boxHeight}, texture, renderer);
// Inicializa variables
fixedWidth = false;
}
// Constructor
Text::Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer) {
// Inicializa variables desde la estructura
boxHeight = textFile->boxHeight;
boxWidth = textFile->boxWidth;
for (int i = 0; i < 128; ++i) {
offset[i].x = textFile->offset[i].x;
offset[i].y = textFile->offset[i].y;
offset[i].w = textFile->offset[i].w;
}
// Crea los objetos
this->texture = nullptr;
sprite = new Sprite({0, 0, boxWidth, boxHeight}, texture, renderer);
// Inicializa variables
fixedWidth = false;
}
// Constructor desde bytes
Text::Text(const std::vector<uint8_t> &pngBytes, const std::vector<uint8_t> &txtBytes, SDL_Renderer *renderer) {
textFile_t tf = LoadTextFileFromMemory(txtBytes);
boxHeight = tf.boxHeight;
boxWidth = tf.boxWidth;
for (int i = 0; i < 128; ++i) {
offset[i].x = tf.offset[i].x;
offset[i].y = tf.offset[i].y;
offset[i].w = tf.offset[i].w;
}
// Crea la textura desde bytes (Text es dueño en este overload)
texture = new Texture(renderer, pngBytes);
sprite = new Sprite({0, 0, boxWidth, boxHeight}, texture, renderer);
fixedWidth = false;
}
// Destructor
Text::~Text() {
delete sprite;
if (texture != nullptr) {
delete texture;
}
}
// Escribe texto en pantalla
void Text::write(int x, int y, std::string text, int kerning, int lenght) {
int shift = 0;
if (lenght == -1) {
lenght = text.length();
}
sprite->setPosY(y);
const int width = sprite->getWidth();
const int height = sprite->getHeight();
for (int i = 0; i < lenght; ++i) {
const int index = text[i];
sprite->setSpriteClip(offset[index].x, offset[index].y, width, height);
sprite->setPosX(x + shift);
sprite->render();
shift += fixedWidth ? boxWidth : (offset[int(text[i])].w + kerning);
}
}
// Escribe el texto con colores
void Text::writeColored(int x, int y, std::string text, color_t color, int kerning, int lenght) {
sprite->getTexture()->setColor(color.r, color.g, color.b);
write(x, y, text, kerning, lenght);
sprite->getTexture()->setColor(255, 255, 255);
}
// Escribe el texto con sombra
void Text::writeShadowed(int x, int y, std::string text, color_t color, Uint8 shadowDistance, int kerning, int lenght) {
sprite->getTexture()->setColor(color.r, color.g, color.b);
write(x + shadowDistance, y + shadowDistance, text, kerning, lenght);
sprite->getTexture()->setColor(255, 255, 255);
write(x, y, text, kerning, lenght);
}
// Escribe el texto centrado en un punto x
void Text::writeCentered(int x, int y, std::string text, int kerning, int lenght) {
x -= (Text::lenght(text, kerning) / 2);
write(x, y, text, kerning, lenght);
}
// Escribe texto con extras
void Text::writeDX(Uint8 flags, int x, int y, std::string text, int kerning, color_t textColor, Uint8 shadowDistance, color_t shadowColor, int lenght) {
const bool centered = ((flags & TXT_CENTER) == TXT_CENTER);
const bool shadowed = ((flags & TXT_SHADOW) == TXT_SHADOW);
const bool colored = ((flags & TXT_COLOR) == TXT_COLOR);
const bool stroked = ((flags & TXT_STROKE) == TXT_STROKE);
if (centered) {
x -= (Text::lenght(text, kerning) / 2);
}
if (shadowed) {
writeColored(x + shadowDistance, y + shadowDistance, text, shadowColor, kerning, lenght);
}
if (stroked) {
for (int dist = 1; dist <= shadowDistance; ++dist) {
for (int dy = -dist; dy <= dist; ++dy) {
for (int dx = -dist; dx <= dist; ++dx) {
writeColored(x + dx, y + dy, text, shadowColor, kerning, lenght);
}
}
}
}
if (colored) {
writeColored(x, y, text, textColor, kerning, lenght);
} else {
write(x, y, text, kerning, lenght);
}
}
// Obtiene la longitud en pixels de una cadena
int Text::lenght(std::string text, int kerning) {
int shift = 0;
for (int i = 0; i < (int)text.length(); ++i)
shift += (offset[int(text[i])].w + kerning);
// Descuenta el kerning del último caracter
return shift - kerning;
}
// Devuelve el valor de la variable
int Text::getCharacterSize() {
return boxWidth;
}
// Recarga la textura
void Text::reLoadTexture() {
sprite->getTexture()->reLoad();
}
// Establece si se usa un tamaño fijo de letra
void Text::setFixedWidth(bool value) {
fixedWidth = value;
}

View File

@@ -0,0 +1,87 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string
#include <vector>
class Sprite;
class Texture;
#include "utils/utils.h"
// Opciones de texto
constexpr int TXT_COLOR = 1;
constexpr int TXT_SHADOW = 2;
constexpr int TXT_CENTER = 4;
constexpr int TXT_STROKE = 8;
struct offset_t {
int x;
int y;
int w;
};
struct textFile_t {
int boxWidth; // Anchura de la caja de cada caracter en el png
int boxHeight; // Altura de la caja de cada caracter en el png
offset_t offset[128]; // Vector con las posiciones y ancho de cada letra
};
// Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose = false);
// Llena una estructura textFile_t desde bytes en memoria
textFile_t LoadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose = false);
// Clase texto. Pinta texto en pantalla a partir de un bitmap
class Text {
private:
// Objetos y punteros
Sprite *sprite; // Objeto con los graficos para el texto
Texture *texture; // Textura con los bitmaps del texto
// Variables
int boxWidth; // Anchura de la caja de cada caracter en el png
int boxHeight; // Altura de la caja de cada caracter en el png
bool fixedWidth; // Indica si el texto se ha de escribir con longitud fija en todas las letras
offset_t offset[128]; // Vector con las posiciones y ancho de cada letra
public:
// Constructor
Text(std::string bitmapFile, std::string textFile, SDL_Renderer *renderer);
Text(std::string textFile, Texture *texture, SDL_Renderer *renderer);
Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer);
// Constructor desde bytes en memoria: comparte ownership del texture (no lo libera)
Text(const std::vector<uint8_t> &pngBytes, const std::vector<uint8_t> &txtBytes, SDL_Renderer *renderer);
// Destructor
~Text();
// Escribe el texto en pantalla
void write(int x, int y, std::string text, int kerning = 1, int lenght = -1);
// Escribe el texto con colores
void writeColored(int x, int y, std::string text, color_t color, int kerning = 1, int lenght = -1);
// Escribe el texto con sombra
void writeShadowed(int x, int y, std::string text, color_t color, Uint8 shadowDistance = 1, int kerning = 1, int lenght = -1);
// Escribe el texto centrado en un punto x
void writeCentered(int x, int y, std::string text, int kerning = 1, int lenght = -1);
// Escribe texto con extras
void writeDX(Uint8 flags, int x, int y, std::string text, int kerning = 1, color_t textColor = color_t(255, 255, 255), Uint8 shadowDistance = 1, color_t shadowColor = color_t(0, 0, 0), int lenght = -1);
// Obtiene la longitud en pixels de una cadena
int lenght(std::string text, int kerning = 1);
// Devuelve el valor de la variable
int getCharacterSize();
// Recarga la textura
void reLoadTexture();
// Establece si se usa un tamaño fijo de letra
void setFixedWidth(bool value);
};

View File

@@ -0,0 +1,212 @@
#include "core/rendering/texture.h"
#include <SDL3/SDL.h>
#include <stdlib.h> // for exit
#include <iostream> // for basic_ostream, operator<<, cout, endl
#define STB_IMAGE_IMPLEMENTATION
#include "external/stb_image.h" // for stbi_failure_reason, stbi_image_free
SDL_ScaleMode Texture::currentScaleMode = SDL_SCALEMODE_NEAREST;
void Texture::setGlobalScaleMode(SDL_ScaleMode mode) {
currentScaleMode = mode;
}
// Constructor
Texture::Texture(SDL_Renderer *renderer, std::string path, bool verbose) {
// Copia punteros
this->renderer = renderer;
this->path = path;
// Inicializa
texture = nullptr;
width = 0;
height = 0;
// Carga el fichero en la textura
if (path != "") {
loadFromFile(path, renderer, verbose);
}
}
// Constructor desde bytes
Texture::Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose) {
this->renderer = renderer;
this->path = "";
texture = nullptr;
width = 0;
height = 0;
if (!bytes.empty()) {
loadFromMemory(bytes.data(), bytes.size(), renderer, verbose);
}
}
// Destructor
Texture::~Texture() {
// Libera memoria
unload();
}
// Helper: convierte píxeles RGBA decodificados por stbi en SDL_Texture
static SDL_Texture *createTextureFromPixels(SDL_Renderer *renderer, unsigned char *data, int w, int h, int *out_w, int *out_h) {
const int pitch = 4 * w;
SDL_Surface *loadedSurface = SDL_CreateSurfaceFrom(w, h, SDL_PIXELFORMAT_RGBA32, (void *)data, pitch);
if (loadedSurface == nullptr) {
return nullptr;
}
SDL_Texture *newTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
if (newTexture != nullptr) {
*out_w = loadedSurface->w;
*out_h = loadedSurface->h;
SDL_SetTextureScaleMode(newTexture, Texture::currentScaleMode);
}
SDL_DestroySurface(loadedSurface);
return newTexture;
}
// Carga una imagen desde un fichero
bool Texture::loadFromFile(std::string path, SDL_Renderer *renderer, bool verbose) {
const std::string filename = path.substr(path.find_last_of("\\/") + 1);
int req_format = STBI_rgb_alpha;
int w, h, orig_format;
unsigned char *data = stbi_load(path.c_str(), &w, &h, &orig_format, req_format);
if (data == nullptr) {
SDL_Log("Loading image failed: %s", stbi_failure_reason());
exit(1);
} else if (verbose) {
std::cout << "Image loaded: " << filename.c_str() << std::endl;
}
unload();
SDL_Texture *newTexture = createTextureFromPixels(renderer, data, w, h, &this->width, &this->height);
if (newTexture == nullptr && verbose) {
std::cout << "Unable to load image " << path.c_str() << std::endl;
}
stbi_image_free(data);
texture = newTexture;
return texture != nullptr;
}
// Carga una imagen desde bytes en memoria
bool Texture::loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose) {
int w, h, orig_format;
unsigned char *pixels = stbi_load_from_memory(data, (int)size, &w, &h, &orig_format, STBI_rgb_alpha);
if (pixels == nullptr) {
SDL_Log("Loading image from memory failed: %s", stbi_failure_reason());
return false;
}
unload();
SDL_Texture *newTexture = createTextureFromPixels(renderer, pixels, w, h, &this->width, &this->height);
if (newTexture == nullptr && verbose) {
std::cout << "Unable to create texture from memory" << std::endl;
}
stbi_image_free(pixels);
texture = newTexture;
return texture != nullptr;
}
// Crea una textura en blanco
bool Texture::createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess access) {
// Crea una textura sin inicializar
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, access, width, height);
if (texture == nullptr) {
std::cout << "Unable to create blank texture! SDL Error: " << SDL_GetError() << std::endl;
} else {
this->width = width;
this->height = height;
SDL_SetTextureScaleMode(texture, currentScaleMode);
}
return texture != nullptr;
}
// Libera la memoria de la textura
void Texture::unload() {
// Libera la textura si existe
if (texture != nullptr) {
SDL_DestroyTexture(texture);
texture = nullptr;
width = 0;
height = 0;
}
}
// Establece el color para la modulacion
void Texture::setColor(Uint8 red, Uint8 green, Uint8 blue) {
SDL_SetTextureColorMod(texture, red, green, blue);
}
// Establece el blending
void Texture::setBlendMode(SDL_BlendMode blending) {
SDL_SetTextureBlendMode(texture, blending);
}
// Establece el alpha para la modulación
void Texture::setAlpha(Uint8 alpha) {
SDL_SetTextureAlphaMod(texture, alpha);
}
// Renderiza la textura en un punto específico
void Texture::render(SDL_Renderer *renderer, int x, int y, SDL_Rect *clip, float zoomW, float zoomH, double angle, SDL_Point *center, SDL_FlipMode flip) {
// Establece el destino de renderizado en la pantalla
SDL_FRect renderQuad = {(float)x, (float)y, (float)width, (float)height};
// Obtiene las dimesiones del clip de renderizado
if (clip != nullptr) {
renderQuad.w = (float)clip->w;
renderQuad.h = (float)clip->h;
}
renderQuad.w = renderQuad.w * zoomW;
renderQuad.h = renderQuad.h * zoomH;
// Convierte el clip a SDL_FRect
SDL_FRect srcRect;
SDL_FRect *srcRectPtr = nullptr;
if (clip != nullptr) {
srcRect = {(float)clip->x, (float)clip->y, (float)clip->w, (float)clip->h};
srcRectPtr = &srcRect;
}
// Convierte el centro a SDL_FPoint
SDL_FPoint fCenter;
SDL_FPoint *fCenterPtr = nullptr;
if (center != nullptr) {
fCenter = {(float)center->x, (float)center->y};
fCenterPtr = &fCenter;
}
// Renderiza a pantalla
SDL_RenderTextureRotated(renderer, texture, srcRectPtr, &renderQuad, angle, fCenterPtr, flip);
}
// Establece la textura como objetivo de renderizado
void Texture::setAsRenderTarget(SDL_Renderer *renderer) {
SDL_SetRenderTarget(renderer, texture);
}
// Obtiene el ancho de la imagen
int Texture::getWidth() {
return width;
}
// Obtiene el alto de la imagen
int Texture::getHeight() {
return height;
}
// Recarga la textura
bool Texture::reLoad() {
return loadFromFile(path, renderer);
}
// Obtiene la textura
SDL_Texture *Texture::getSDLTexture() {
return texture;
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for basic_string, string
#include <vector>
class Texture {
private:
// Objetos y punteros
SDL_Texture *texture; // La textura
SDL_Renderer *renderer; // Renderizador donde dibujar la textura
// Variables
int width; // Ancho de la imagen
int height; // Alto de la imagen
std::string path; // Ruta de la imagen de la textura
public:
static SDL_ScaleMode currentScaleMode; // Modo de escalado global para nuevas texturas
// Establece el modo de escalado global para nuevas texturas
static void setGlobalScaleMode(SDL_ScaleMode mode);
// Constructor
Texture(SDL_Renderer *renderer, std::string path = "", bool verbose = false);
// Constructor desde bytes (PNG en memoria)
Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose = false);
// Destructor
~Texture();
// Carga una imagen desde un fichero
bool loadFromFile(std::string path, SDL_Renderer *renderer, bool verbose = false);
// Carga una imagen desde bytes en memoria
bool loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose = false);
// Crea una textura en blanco
bool createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess = SDL_TEXTUREACCESS_STREAMING);
// Libera la memoria de la textura
void unload();
// Establece el color para la modulacion
void setColor(Uint8 red, Uint8 green, Uint8 blue);
// Establece el blending
void setBlendMode(SDL_BlendMode blending);
// Establece el alpha para la modulación
void setAlpha(Uint8 alpha);
// Renderiza la textura en un punto específico
void render(SDL_Renderer *renderer, int x, int y, SDL_Rect *clip = nullptr, float zoomW = 1, float zoomH = 1, double angle = 0.0, SDL_Point *center = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
// Establece la textura como objetivo de renderizado
void setAsRenderTarget(SDL_Renderer *renderer);
// Obtiene el ancho de la imagen
int getWidth();
// Obtiene el alto de la imagen
int getHeight();
// Recarga la textura
bool reLoad();
// Obtiene la textura
SDL_Texture *getSDLTexture();
};

View File

@@ -0,0 +1,115 @@
#include "core/rendering/writer.h"
#include "core/rendering/text.h" // for Text
// Constructor
Writer::Writer(Text *text) {
// Copia los punteros
this->text = text;
// Inicializa variables
posX = 0;
posY = 0;
kerning = 0;
caption = "";
speed = 0;
writingCounter = 0;
index = 0;
lenght = 0;
completed = false;
enabled = false;
enabledCounter = 0;
finished = false;
}
// Actualiza el objeto
void Writer::update() {
if (enabled) {
if (!completed) { // No completado
if (writingCounter > 0) {
writingCounter--;
}
else if (writingCounter == 0) {
index++;
writingCounter = speed;
}
if (index == lenght) {
completed = true;
}
}
if (completed) { // Completado
if (enabledCounter > 0) {
enabledCounter--;
} else if (enabledCounter == 0) {
finished = true;
}
}
}
}
// Dibuja el objeto en pantalla
void Writer::render() {
if (enabled) {
text->write(posX, posY, caption, kerning, index);
}
}
// Establece el valor de la variable
void Writer::setPosX(int value) {
posX = value;
}
// Establece el valor de la variable
void Writer::setPosY(int value) {
posY = value;
}
// Establece el valor de la variable
void Writer::setKerning(int value) {
kerning = value;
}
// Establece el valor de la variable
void Writer::setCaption(std::string text) {
caption = text;
lenght = text.length();
}
// Establece el valor de la variable
void Writer::setSpeed(int value) {
speed = value;
writingCounter = value;
}
// Establece el valor de la variable
void Writer::setEnabled(bool value) {
enabled = value;
}
// Obtiene el valor de la variable
bool Writer::IsEnabled() {
return enabled;
}
// Establece el valor de la variable
void Writer::setEnabledCounter(int time) {
enabledCounter = time;
}
// Obtiene el valor de la variable
int Writer::getEnabledCounter() {
return enabledCounter;
}
// Centra la cadena de texto a un punto X
void Writer::center(int x) {
setPosX(x - (text->lenght(caption, kerning) / 2));
}
// Obtiene el valor de la variable
bool Writer::hasFinished() {
return finished;
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include <string> // for string, basic_string
class Text;
// Clase Writer. Pinta texto en pantalla letra a letra a partir de una cadena y un bitmap
class Writer {
private:
// Objetos y punteros
Text *text; // Objeto encargado de escribir el texto
// Variables
int posX; // Posicion en el eje X donde empezar a escribir el texto
int posY; // Posicion en el eje Y donde empezar a escribir el texto
int kerning; // Kerning del texto, es decir, espaciado entre caracteres
std::string caption; // El texto para escribir
int speed; // Velocidad de escritura
int writingCounter; // Temporizador de escritura para cada caracter
int index; // Posición del texto que se está escribiendo
int lenght; // Longitud de la cadena a escribir
bool completed; // Indica si se ha escrito todo el texto
bool enabled; // Indica si el objeto está habilitado
int enabledCounter; // Temporizador para deshabilitar el objeto
bool finished; // Indica si ya ha terminado
public:
// Constructor
Writer(Text *text);
// Actualiza el objeto
void update();
// Dibuja el objeto en pantalla
void render();
// Establece el valor de la variable
void setPosX(int value);
// Establece el valor de la variable
void setPosY(int value);
// Establece el valor de la variable
void setKerning(int value);
// Establece el valor de la variable
void setCaption(std::string text);
// Establece el valor de la variable
void setSpeed(int value);
// Establece el valor de la variable
void setEnabled(bool value);
// Obtiene el valor de la variable
bool IsEnabled();
// Establece el valor de la variable
void setEnabledCounter(int time);
// Obtiene el valor de la variable
int getEnabledCounter();
// Centra la cadena de texto a un punto X
void center(int x);
// Obtiene el valor de la variable
bool hasFinished();
};

View File

@@ -0,0 +1,178 @@
#include "core/resources/asset.h"
#include <SDL3/SDL.h>
#include <stddef.h> // for size_t
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include "core/resources/resource_helper.h"
// Constructor
Asset::Asset(std::string executablePath) {
this->executablePath = executablePath.substr(0, executablePath.find_last_of("\\/"));
longestName = 0;
verbose = true;
}
// Añade un elemento a la lista
void Asset::add(std::string file, enum assetType type, bool required, bool absolute) {
item_t temp;
temp.file = absolute ? file : executablePath + file;
temp.type = type;
temp.required = required;
fileList.push_back(temp);
const std::string filename = file.substr(file.find_last_of("\\/") + 1);
longestName = SDL_max(longestName, filename.size());
}
// Devuelve el fichero de un elemento de la lista a partir de una cadena
std::string Asset::get(std::string text) {
for (auto f : fileList) {
const size_t lastIndex = f.file.find_last_of("/") + 1;
const std::string file = f.file.substr(lastIndex, std::string::npos);
if (file == text) {
return f.file;
}
}
if (verbose) {
std::cout << "Warning: file " << text.c_str() << " not found" << std::endl;
}
return "";
}
// Comprueba que existen todos los elementos
bool Asset::check() {
bool success = true;
if (verbose) {
std::cout << "\n** Checking files" << std::endl;
std::cout << "Executable path is: " << executablePath << std::endl;
std::cout << "Sample filepath: " << fileList.back().file << std::endl;
}
// Comprueba la lista de ficheros clasificandolos por tipo
for (int type = 0; type < t_maxAssetType; ++type) {
// Comprueba si hay ficheros de ese tipo
bool any = false;
for (auto f : fileList) {
if ((f.required) && (f.type == type)) {
any = true;
}
}
// Si hay ficheros de ese tipo, comprueba si existen
if (any) {
if (verbose) {
std::cout << "\n>> " << getTypeName(type).c_str() << " FILES" << std::endl;
}
for (auto f : fileList) {
if ((f.required) && (f.type == type)) {
success &= checkFile(f.file);
}
}
}
}
// Resultado
if (verbose) {
if (success) {
std::cout << "\n** All files OK.\n"
<< std::endl;
} else {
std::cout << "\n** A file is missing. Exiting.\n"
<< std::endl;
}
}
return success;
}
// Comprueba que existe un fichero
bool Asset::checkFile(std::string path) {
bool success = false;
std::string result = "ERROR";
// Comprueba si existe el fichero (pack o filesystem)
const std::string filename = path.substr(path.find_last_of("\\/") + 1);
if (ResourceHelper::shouldUseResourcePack(path)) {
auto bytes = ResourceHelper::loadFile(path);
if (!bytes.empty()) {
result = "OK";
success = true;
}
} else {
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
if (file != nullptr) {
result = "OK";
success = true;
SDL_CloseIO(file);
}
}
if (verbose) {
std::cout.setf(std::ios::left, std::ios::adjustfield);
std::cout << "Checking file: ";
std::cout.width(longestName + 2);
std::cout.fill('.');
std::cout << filename + " ";
std::cout << " [" + result + "]" << std::endl;
}
return success;
}
// Devuelve el nombre del tipo de recurso
std::string Asset::getTypeName(int type) {
switch (type) {
case t_bitmap:
return "BITMAP";
break;
case t_music:
return "MUSIC";
break;
case t_sound:
return "SOUND";
break;
case t_font:
return "FONT";
break;
case t_lang:
return "LANG";
break;
case t_data:
return "DATA";
break;
case t_room:
return "ROOM";
break;
case t_enemy:
return "ENEMY";
break;
case t_item:
return "ITEM";
break;
default:
return "ERROR";
break;
}
}
// Establece si ha de mostrar texto por pantalla
void Asset::setVerbose(bool value) {
verbose = value;
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <string> // for string, basic_string
#include <vector> // for vector
enum assetType {
t_bitmap,
t_music,
t_sound,
t_font,
t_lang,
t_data,
t_room,
t_enemy,
t_item,
t_maxAssetType
};
// Clase Asset
class Asset {
public:
// Estructura para definir un item
struct item_t {
std::string file; // Ruta del fichero desde la raiz del directorio
enum assetType type; // Indica el tipo de recurso
bool required; // Indica si es un fichero que debe de existir
};
private:
// Variables
int longestName; // Contiene la longitud del nombre de fichero mas largo
std::vector<item_t> fileList; // Listado con todas las rutas a los ficheros
std::string executablePath; // Ruta al ejecutable
bool verbose; // Indica si ha de mostrar información por pantalla
// Comprueba que existe un fichero
bool checkFile(std::string executablePath);
// Devuelve el nombre del tipo de recurso
std::string getTypeName(int type);
public:
// Constructor
Asset(std::string path);
// Añade un elemento a la lista
void add(std::string file, enum assetType type, bool required = true, bool absolute = false);
// Devuelve un elemento de la lista a partir de una cadena
std::string get(std::string text);
// Devuelve toda la lista de items registrados
const std::vector<item_t> &getAll() const { return fileList; }
// Comprueba que existen todos los elementos
bool check();
// Establece si ha de mostrar texto por pantalla
void setVerbose(bool value);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,864 @@
#include "core/system/director.h"
#include <SDL3/SDL.h>
#include <errno.h> // for errno, EEXIST, EACCES, ENAMETOO...
#include <stdio.h> // for printf, perror
#include <string.h> // for strcmp
#ifndef __EMSCRIPTEN__
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
#include <unistd.h> // for getuid
#endif
#include <cstdlib> // for exit, EXIT_FAILURE, srand
#include <filesystem>
#include <fstream> // for basic_ostream, operator<<, basi...
#include <iostream> // for cout
#include <memory>
#include <string> // for basic_string, operator+, char_t...
#include <vector> // for vector
#include "core/audio/jail_audio.hpp" // for JA_Init
#include "core/input/input.h" // for Input, inputs_e, INPUT_USE_GAME...
#include "core/input/mouse.hpp" // for Mouse::handleEvent, Mouse::upda...
#include "core/locale/lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
#include "core/rendering/screen.h" // for FILTER_NEAREST, Screen, FILTER_...
#include "core/rendering/texture.h" // for Texture
#include "core/resources/asset.h" // for Asset, assetType
#include "core/resources/resource.h"
#include "core/resources/resource_helper.h"
#include "game/defaults.hpp" // for SECTION_PROG_LOGO, GAMECANVAS_H...
#include "game/game.h" // for Game
#include "game/scenes/intro.h" // for Intro
#include "game/scenes/logo.h" // for Logo
#include "game/scenes/title.h" // for Title
#include "utils/utils.h" // for options_t, input_t, boolToString
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
#include <pwd.h>
#endif
// Constructor
Director::Director(int argc, const char *argv[]) {
std::cout << "Game start" << std::endl;
// Inicializa variables
section = new section_t();
section->name = SECTION_PROG_LOGO;
// Inicializa las opciones del programa
initOptions();
// Comprueba los parametros del programa
checkProgramArguments(argc, argv);
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
#ifndef DEBUG
createSystemFolder("jailgames/coffee_crisis");
#else
createSystemFolder("jailgames/coffee_crisis_debug");
#endif
// Inicializa el sistema de recursos (pack + fallback).
// En wasm siempre se usa filesystem (MEMFS) porque el propio --preload-file
// de emscripten ya empaqueta data/ — no hay resources.pack.
{
#if defined(__EMSCRIPTEN__)
const bool enable_fallback = true;
#elif defined(RELEASE_BUILD)
const bool enable_fallback = false;
#else
const bool enable_fallback = true;
#endif
if (!ResourceHelper::initializeResourceSystem("resources.pack", enable_fallback)) {
std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << std::endl;
exit(EXIT_FAILURE);
}
}
// Crea el objeto que controla los ficheros de recursos
asset = new Asset(executablePath);
asset->setVerbose(options->console);
// Si falta algún fichero no inicia el programa
if (!setFileList()) {
exit(EXIT_FAILURE);
}
// Carga el fichero de configuración
loadConfigFile();
// Inicializa SDL
initSDL();
// Inicializa JailAudio
initJailAudio();
// Establece el modo de escalado de texturas
Texture::setGlobalScaleMode(options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
// Crea los objetos
lang = new Lang(asset);
lang->setLang(options->language);
#ifdef __EMSCRIPTEN__
input = new Input("/gamecontrollerdb.txt");
#else
{
const std::string binDir = std::filesystem::path(executablePath).parent_path().string();
#ifdef MACOS_BUNDLE
input = new Input(binDir + "/../Resources/gamecontrollerdb.txt");
#else
input = new Input(binDir + "/gamecontrollerdb.txt");
#endif
}
#endif
initInput();
// Precarga todos los recursos en memoria (texturas, sonidos, música, ...).
// Vivirán durante toda la vida de la app; las escenas leen handles compartidos.
// Debe ir antes de crear Screen porque Screen usa Text (propiedad de Resource).
Resource::init(renderer, asset, input);
screen = new Screen(window, renderer, asset, options);
activeSection = ActiveSection::None;
}
Director::~Director() {
saveConfigFile();
// Libera las secciones primero: sus destructores tocan audio/render SDL
// (p.ej. Intro::~Intro llama a JA_DeleteMusic) y deben ejecutarse antes
// de SDL_Quit().
logo.reset();
intro.reset();
title.reset();
game.reset();
// Screen puede tener referencias a Text propiedad de Resource: destruir
// Screen antes que Resource.
delete screen;
// Libera todos los recursos precargados antes de cerrar SDL.
Resource::destroy();
delete asset;
delete input;
delete lang;
delete options;
delete section;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
ResourceHelper::shutdownResourceSystem();
std::cout << "\nBye!" << std::endl;
}
// Inicializa el objeto input
void Director::initInput() {
// Establece si ha de mostrar mensajes
input->setVerbose(options->console);
// Busca si hay un mando conectado
input->discoverGameController();
// Teclado - Movimiento del jugador
input->bindKey(input_up, SDL_SCANCODE_UP);
input->bindKey(input_down, SDL_SCANCODE_DOWN);
input->bindKey(input_left, SDL_SCANCODE_LEFT);
input->bindKey(input_right, SDL_SCANCODE_RIGHT);
input->bindKey(input_fire_left, SDL_SCANCODE_Q);
input->bindKey(input_fire_center, SDL_SCANCODE_W);
input->bindKey(input_fire_right, SDL_SCANCODE_E);
// Teclado - Otros
input->bindKey(input_accept, SDL_SCANCODE_RETURN);
input->bindKey(input_cancel, SDL_SCANCODE_ESCAPE);
input->bindKey(input_pause, SDL_SCANCODE_ESCAPE);
input->bindKey(input_exit, SDL_SCANCODE_ESCAPE);
input->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
input->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
// Mando - Movimiento del jugador
input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
input->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
input->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
input->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
input->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST);
input->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH);
input->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST);
// Mando - Otros
// SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK.
input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
#ifdef GAME_CONSOLE
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
#else
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START);
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK);
#endif
}
// Inicializa JailAudio
void Director::initJailAudio() {
JA_Init(48000, SDL_AUDIO_S16, 2);
}
// Arranca SDL y crea la ventana
bool Director::initSDL() {
// Indicador de éxito
bool success = true;
// Inicializa SDL
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
if (options->console) {
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Inicia el generador de numeros aleatorios
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
// Crea la ventana
int incW = 0;
int incH = 0;
if (options->borderEnabled) {
incW = options->borderWidth * 2;
incH = options->borderHeight * 2;
}
window = SDL_CreateWindow(WINDOW_CAPTION, (options->gameWidth + incW) * options->windowSize, (options->gameHeight + incH) * options->windowSize, 0);
if (window == nullptr) {
if (options->console) {
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Crea un renderizador para la ventana
renderer = SDL_CreateRenderer(window, NULL);
if (renderer == nullptr) {
if (options->console) {
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Activa vsync si es necesario
if (options->vSync) {
SDL_SetRenderVSync(renderer, 1);
}
// Inicializa el color de renderizado
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
// Establece el tamaño del buffer de renderizado
SDL_SetRenderLogicalPresentation(renderer, options->gameWidth, options->gameHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
// Establece el modo de mezcla
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}
}
}
if (options->console) {
std::cout << std::endl;
}
return success;
}
// Crea el indice de ficheros
bool Director::setFileList() {
#ifdef MACOS_BUNDLE
const std::string prefix = "/../Resources";
#else
const std::string prefix = "";
#endif
// Ficheros de configuración
asset->add(systemFolder + "/config.txt", t_data, false, true);
asset->add(systemFolder + "/score.bin", t_data, false, true);
asset->add(prefix + "/data/demo/demo.bin", t_data);
// Musicas
asset->add(prefix + "/data/music/intro.ogg", t_music);
asset->add(prefix + "/data/music/playing.ogg", t_music);
asset->add(prefix + "/data/music/title.ogg", t_music);
// Sonidos
asset->add(prefix + "/data/sound/balloon.wav", t_sound);
asset->add(prefix + "/data/sound/bubble1.wav", t_sound);
asset->add(prefix + "/data/sound/bubble2.wav", t_sound);
asset->add(prefix + "/data/sound/bubble3.wav", t_sound);
asset->add(prefix + "/data/sound/bubble4.wav", t_sound);
asset->add(prefix + "/data/sound/bullet.wav", t_sound);
asset->add(prefix + "/data/sound/coffeeout.wav", t_sound);
asset->add(prefix + "/data/sound/hiscore.wav", t_sound);
asset->add(prefix + "/data/sound/itemdrop.wav", t_sound);
asset->add(prefix + "/data/sound/itempickup.wav", t_sound);
asset->add(prefix + "/data/sound/menu_move.wav", t_sound);
asset->add(prefix + "/data/sound/menu_select.wav", t_sound);
asset->add(prefix + "/data/sound/player_collision.wav", t_sound);
asset->add(prefix + "/data/sound/stage_change.wav", t_sound);
asset->add(prefix + "/data/sound/title.wav", t_sound);
asset->add(prefix + "/data/sound/clock.wav", t_sound);
asset->add(prefix + "/data/sound/powerball.wav", t_sound);
// Texturas
asset->add(prefix + "/data/gfx/balloon1.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon1.ani", t_data);
asset->add(prefix + "/data/gfx/balloon2.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon2.ani", t_data);
asset->add(prefix + "/data/gfx/balloon3.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon3.ani", t_data);
asset->add(prefix + "/data/gfx/balloon4.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon4.ani", t_data);
asset->add(prefix + "/data/gfx/bullet.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_buildings.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_clouds.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_grass.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_text.png", t_bitmap);
asset->add(prefix + "/data/gfx/intro.png", t_bitmap);
asset->add(prefix + "/data/gfx/logo.png", t_bitmap);
asset->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap);
asset->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points1_disk.ani", t_data);
asset->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data);
asset->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data);
asset->add(prefix + "/data/gfx/item_clock.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_clock.ani", t_data);
asset->add(prefix + "/data/gfx/item_coffee.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_coffee.ani", t_data);
asset->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data);
asset->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_coffee.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_crisis.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_dust.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_dust.ani", t_data);
asset->add(prefix + "/data/gfx/title_gradient.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_head.ani", t_data);
asset->add(prefix + "/data/gfx/player_body.ani", t_data);
asset->add(prefix + "/data/gfx/player_legs.ani", t_data);
asset->add(prefix + "/data/gfx/player_death.ani", t_data);
asset->add(prefix + "/data/gfx/player_fire.ani", t_data);
asset->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap);
// Fuentes
asset->add(prefix + "/data/font/8bithud.png", t_font);
asset->add(prefix + "/data/font/8bithud.txt", t_font);
asset->add(prefix + "/data/font/nokia.png", t_font);
asset->add(prefix + "/data/font/nokia_big2.png", t_font);
asset->add(prefix + "/data/font/nokia.txt", t_font);
asset->add(prefix + "/data/font/nokia2.png", t_font);
asset->add(prefix + "/data/font/nokia2.txt", t_font);
asset->add(prefix + "/data/font/nokia_big2.txt", t_font);
asset->add(prefix + "/data/font/smb2_big.png", t_font);
asset->add(prefix + "/data/font/smb2_big.txt", t_font);
asset->add(prefix + "/data/font/smb2.png", t_font);
asset->add(prefix + "/data/font/smb2.txt", t_font);
// Textos
asset->add(prefix + "/data/lang/es_ES.txt", t_lang);
asset->add(prefix + "/data/lang/en_UK.txt", t_lang);
asset->add(prefix + "/data/lang/ba_BA.txt", t_lang);
// Menus
asset->add(prefix + "/data/menu/title.men", t_data);
asset->add(prefix + "/data/menu/title_gc.men", t_data);
asset->add(prefix + "/data/menu/options.men", t_data);
asset->add(prefix + "/data/menu/options_gc.men", t_data);
asset->add(prefix + "/data/menu/pause.men", t_data);
asset->add(prefix + "/data/menu/gameover.men", t_data);
asset->add(prefix + "/data/menu/player_select.men", t_data);
return asset->check();
}
// Inicializa las opciones del programa
void Director::initOptions() {
// Crea el puntero a la estructura de opciones
options = new options_t;
// Pone unos valores por defecto para las opciones de control
options->input.clear();
input_t inp;
inp.id = 0;
inp.name = "KEYBOARD";
inp.deviceType = INPUT_USE_KEYBOARD;
options->input.push_back(inp);
inp.id = 0;
inp.name = "GAME CONTROLLER";
inp.deviceType = INPUT_USE_GAMECONTROLLER;
options->input.push_back(inp);
// Opciones de video
options->gameWidth = GAMECANVAS_WIDTH;
options->gameHeight = GAMECANVAS_HEIGHT;
options->videoMode = 0;
options->windowSize = 3;
options->filter = FILTER_NEAREST;
options->vSync = true;
options->integerScale = true;
options->keepAspect = true;
options->borderWidth = 0;
options->borderHeight = 0;
options->borderEnabled = false;
// Opciones varios
options->playerSelected = 0;
options->difficulty = DIFFICULTY_NORMAL;
options->language = ba_BA;
options->console = false;
#ifdef __EMSCRIPTEN__
// En Emscripten la ventana la gestiona el navegador
options->windowSize = 4;
options->videoMode = 0;
options->integerScale = true;
#endif
}
// Comprueba los parametros del programa
void Director::checkProgramArguments(int argc, const char *argv[]) {
// Establece la ruta del programa
executablePath = argv[0];
// Comprueba el resto de parametros
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--console") == 0) {
options->console = true;
}
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string &folder) {
#ifdef __EMSCRIPTEN__
// En Emscripten usamos una carpeta en MEMFS (no persistente)
systemFolder = "/config/" + folder;
#elif _WIN32
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
#elif __linux__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/.config/" + folder;
{
// Intenta crear ".config", per si no existeix
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR CREATING CONFIG BASE FOLDER.");
exit(EXIT_FAILURE);
}
}
#endif
#ifdef __EMSCRIPTEN__
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
(void)folder;
#else
struct stat st = {0};
if (stat(systemFolder.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(systemFolder.c_str());
#else
int ret = mkdir(systemFolder.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("the parent directory does not allow write");
exit(EXIT_FAILURE);
case EEXIST:
printf("pathname already exists");
exit(EXIT_FAILURE);
case ENAMETOOLONG:
printf("pathname is too long");
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
#endif
}
// Carga el fichero de configuración
bool Director::loadConfigFile() {
// Indicador de éxito en la carga
bool success = true;
// Variables para manejar el fichero
const std::string filePath = "config.txt";
std::string line;
std::ifstream file(asset->get(filePath));
// Si el fichero se puede abrir
if (file.good()) {
// Procesa el fichero linea a linea
if (options->console) {
std::cout << "Reading file " << filePath << std::endl;
}
while (std::getline(file, line)) {
// Comprueba que la linea no sea un comentario
if (line.substr(0, 1) != "#") {
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (!setOptions(options, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
if (options->console) {
std::cout << "Warning: file " << filePath << std::endl;
std::cout << "Unknown parameter " << line.substr(0, pos).c_str() << std::endl;
}
success = false;
}
}
}
// Cierra el fichero
if (options->console) {
std::cout << "Closing file " << filePath << std::endl;
}
file.close();
}
// El fichero no existe
else { // Crea el fichero con los valores por defecto
saveConfigFile();
}
// Normaliza los valores
if (options->videoMode != 0 && options->videoMode != SDL_WINDOW_FULLSCREEN) {
options->videoMode = 0;
}
if (options->windowSize < 1 || options->windowSize > 4) {
options->windowSize = 3;
}
if (options->language < 0 || options->language > MAX_LANGUAGES) {
options->language = en_UK;
}
return success;
}
// Guarda el fichero de configuración
bool Director::saveConfigFile() {
bool success = true;
// Crea y abre el fichero de texto
std::ofstream file(asset->get("config.txt"));
if (file.good()) {
if (options->console) {
std::cout << asset->get("config.txt") << " open for writing" << std::endl;
}
} else {
if (options->console) {
std::cout << asset->get("config.txt") << " can't be opened" << std::endl;
}
}
// Opciones g´raficas
file << "## VISUAL OPTIONS\n";
if (options->videoMode == 0) {
file << "videoMode=0\n";
}
else if (options->videoMode == SDL_WINDOW_FULLSCREEN) {
file << "videoMode=SDL_WINDOW_FULLSCREEN\n";
}
file << "windowSize=" + std::to_string(options->windowSize) + "\n";
if (options->filter == FILTER_NEAREST) {
file << "filter=FILTER_NEAREST\n";
} else {
file << "filter=FILTER_LINEAL\n";
}
file << "vSync=" + boolToString(options->vSync) + "\n";
file << "integerScale=" + boolToString(options->integerScale) + "\n";
file << "keepAspect=" + boolToString(options->keepAspect) + "\n";
file << "borderEnabled=" + boolToString(options->borderEnabled) + "\n";
file << "borderWidth=" + std::to_string(options->borderWidth) + "\n";
file << "borderHeight=" + std::to_string(options->borderHeight) + "\n";
// Otras opciones del programa
file << "\n## OTHER OPTIONS\n";
file << "language=" + std::to_string(options->language) + "\n";
file << "difficulty=" + std::to_string(options->difficulty) + "\n";
file << "input0=" + std::to_string(options->input[0].deviceType) + "\n";
file << "input1=" + std::to_string(options->input[1].deviceType) + "\n";
// Cierra el fichero
file.close();
return success;
}
// Gestiona las transiciones entre secciones
void Director::handleSectionTransition() {
// Determina qué sección debería estar activa
ActiveSection targetSection = ActiveSection::None;
switch (section->name) {
case SECTION_PROG_LOGO:
targetSection = ActiveSection::Logo;
break;
case SECTION_PROG_INTRO:
targetSection = ActiveSection::Intro;
break;
case SECTION_PROG_TITLE:
targetSection = ActiveSection::Title;
break;
case SECTION_PROG_GAME:
targetSection = ActiveSection::Game;
break;
}
// Si no ha cambiado, no hay nada que hacer
if (targetSection == activeSection) return;
// Destruye la sección anterior
logo.reset();
intro.reset();
title.reset();
game.reset();
// Crea la nueva sección
activeSection = targetSection;
switch (activeSection) {
case ActiveSection::Logo:
logo = std::make_unique<Logo>(renderer, screen, asset, input, section);
break;
case ActiveSection::Intro:
intro = std::make_unique<Intro>(renderer, screen, asset, input, lang, section);
break;
case ActiveSection::Title:
title = std::make_unique<Title>(renderer, screen, input, asset, options, lang, section);
break;
case ActiveSection::Game: {
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, options, section);
break;
}
case ActiveSection::None:
break;
}
}
// Ejecuta un frame del juego
SDL_AppResult Director::iterate() {
#ifdef __EMSCRIPTEN__
// En WASM no se puede salir: reinicia al logo
if (section->name == SECTION_PROG_QUIT) {
section->name = SECTION_PROG_LOGO;
}
#else
if (section->name == SECTION_PROG_QUIT) {
return SDL_APP_SUCCESS;
}
#endif
// Actualiza la visibilidad del cursor del ratón
Mouse::updateCursorVisibility(options->videoMode != 0);
// Gestiona las transiciones entre secciones
handleSectionTransition();
// Ejecuta un frame de la sección activa
switch (activeSection) {
case ActiveSection::Logo:
logo->iterate();
break;
case ActiveSection::Intro:
intro->iterate();
break;
case ActiveSection::Title:
title->iterate();
break;
case ActiveSection::Game:
game->iterate();
break;
case ActiveSection::None:
break;
}
return SDL_APP_CONTINUE;
}
// Procesa un evento
SDL_AppResult Director::handleEvent(SDL_Event *event) {
#ifndef __EMSCRIPTEN__
// Evento de salida de la aplicación
if (event->type == SDL_EVENT_QUIT) {
section->name = SECTION_PROG_QUIT;
return SDL_APP_SUCCESS;
}
#endif
// Hot-plug de mandos
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
std::string name;
if (input->handleGamepadAdded(event->gdevice.which, name)) {
screen->notify(name + " " + lang->getText(94),
color_t{0x40, 0xFF, 0x40},
color_t{0, 0, 0},
2500);
}
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
std::string name;
if (input->handleGamepadRemoved(event->gdevice.which, name)) {
screen->notify(name + " " + lang->getText(95),
color_t{0xFF, 0x50, 0x50},
color_t{0, 0, 0},
2500);
}
}
// Gestiona la visibilidad del cursor según el movimiento del ratón
Mouse::handleEvent(*event, options->videoMode != 0);
// Reenvía el evento a la sección activa
switch (activeSection) {
case ActiveSection::Logo:
logo->handleEvent(event);
break;
case ActiveSection::Intro:
intro->handleEvent(event);
break;
case ActiveSection::Title:
title->handleEvent(event);
break;
case ActiveSection::Game:
game->handleEvent(event);
break;
case ActiveSection::None:
break;
}
return SDL_APP_CONTINUE;
}
// Asigna variables a partir de dos cadenas
bool Director::setOptions(options_t *options, std::string var, std::string value) {
// Indicador de éxito en la asignación
bool success = true;
// Opciones de video
if (var == "videoMode") {
if (value == "SDL_WINDOW_FULLSCREEN" || value == "SDL_WINDOW_FULLSCREEN_DESKTOP") {
options->videoMode = SDL_WINDOW_FULLSCREEN;
} else {
options->videoMode = 0;
}
}
else if (var == "windowSize") {
options->windowSize = std::stoi(value);
if ((options->windowSize < 1) || (options->windowSize > 4)) {
options->windowSize = 3;
}
}
else if (var == "filter") {
if (value == "FILTER_LINEAL") {
options->filter = FILTER_LINEAL;
} else {
options->filter = FILTER_NEAREST;
}
}
else if (var == "vSync") {
options->vSync = stringToBool(value);
}
else if (var == "integerScale") {
options->integerScale = stringToBool(value);
}
else if (var == "keepAspect") {
options->keepAspect = stringToBool(value);
}
else if (var == "borderEnabled") {
options->borderEnabled = stringToBool(value);
}
else if (var == "borderWidth") {
options->borderWidth = std::stoi(value);
}
else if (var == "borderHeight") {
options->borderHeight = std::stoi(value);
}
// Opciones varias
else if (var == "language") {
options->language = std::stoi(value);
}
else if (var == "difficulty") {
options->difficulty = std::stoi(value);
}
else if (var == "input0") {
options->input[0].deviceType = std::stoi(value);
}
else if (var == "input1") {
options->input[1].deviceType = std::stoi(value);
}
// Lineas vacias o que empiezan por comentario
else if (var == "" || var.substr(0, 1) == "#") {
}
else {
success = false;
}
return success;
}

View File

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