tidy-fix automàtic (sense naming)
This commit is contained in:
@@ -62,10 +62,14 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
return;
|
||||
}
|
||||
|
||||
if (!music_enabled_) return;
|
||||
if (!music_enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) return;
|
||||
if (resource == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(resource, crossfade_ms, loop);
|
||||
@@ -83,7 +87,9 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
|
||||
// Reproduce la música por puntero (con crossfade opcional)
|
||||
void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) return;
|
||||
if (!music_enabled_ || music == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(music, crossfade_ms, loop);
|
||||
|
||||
@@ -62,7 +62,7 @@ class Audio {
|
||||
// --- Helpers de conversió per a la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
static constexpr auto toPercent(float volume) -> int {
|
||||
return static_cast<int>(volume * 100.0F + 0.5F);
|
||||
return static_cast<int>((volume * 100.0F) + 0.5F);
|
||||
}
|
||||
static constexpr auto fromPercent(int percent) -> float {
|
||||
return static_cast<float>(percent) / 100.0F;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
namespace AudioResource {
|
||||
|
||||
JA_Music_t* getMusic(const std::string& name) {
|
||||
auto getMusic(const std::string& name) -> JA_Music_t* {
|
||||
return Resource::Cache::get()->getMusic(name);
|
||||
}
|
||||
|
||||
JA_Sound_t* getSound(const std::string& name) {
|
||||
auto getSound(const std::string& name) -> JA_Sound_t* {
|
||||
return Resource::Cache::get()->getSound(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
namespace AudioResource {
|
||||
JA_Music_t* getMusic(const std::string& name);
|
||||
JA_Sound_t* getSound(const std::string& name);
|
||||
auto getMusic(const std::string& name) -> JA_Music_t*;
|
||||
auto getSound(const std::string& name) -> JA_Sound_t*;
|
||||
} // namespace AudioResource
|
||||
|
||||
+221
-120
@@ -2,10 +2,10 @@
|
||||
|
||||
// --- Includes ---
|
||||
#include <SDL3/SDL.h>
|
||||
#include <stdint.h> // Para uint32_t, uint8_t
|
||||
#include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
|
||||
#include <stdlib.h> // Para free, malloc
|
||||
|
||||
#include <cstdint> // Para uint32_t, uint8_t
|
||||
#include <cstdio> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
|
||||
#include <cstdlib> // Para free, malloc
|
||||
#include <iostream> // Para std::cout
|
||||
#include <memory> // Para std::unique_ptr
|
||||
#include <string> // Para std::string
|
||||
@@ -19,7 +19,9 @@
|
||||
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
|
||||
struct SDLFreeDeleter {
|
||||
void operator()(Uint8* p) const noexcept {
|
||||
if (p) SDL_free(p);
|
||||
if (p != nullptr) {
|
||||
SDL_free(p);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,8 +42,10 @@ enum JA_Music_state {
|
||||
};
|
||||
|
||||
// --- Struct Definitions ---
|
||||
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
|
||||
#define JA_MAX_GROUPS 2
|
||||
enum {
|
||||
JA_MAX_SIMULTANEOUS_CHANNELS = 20,
|
||||
JA_MAX_GROUPS = 2
|
||||
};
|
||||
|
||||
struct JA_Sound_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
@@ -84,7 +88,7 @@ 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_musicVolume{1.0F};
|
||||
inline float JA_soundVolume[JA_MAX_GROUPS];
|
||||
inline bool JA_musicEnabled{true};
|
||||
inline bool JA_soundEnabled{true};
|
||||
@@ -95,7 +99,7 @@ struct JA_FadeState {
|
||||
bool active{false};
|
||||
Uint64 start_time{0};
|
||||
int duration_ms{0};
|
||||
float initial_volume{0.0f};
|
||||
float initial_volume{0.0F};
|
||||
};
|
||||
|
||||
struct JA_OutgoingMusic {
|
||||
@@ -108,8 +112,8 @@ inline JA_FadeState incoming_fade;
|
||||
|
||||
// --- Forward Declarations ---
|
||||
inline void JA_StopMusic();
|
||||
inline void JA_StopChannel(const int channel);
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
|
||||
inline void JA_StopChannel(int channel);
|
||||
inline auto JA_PlaySoundOnChannel(JA_Sound_t* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1);
|
||||
|
||||
// --- Music streaming internals ---
|
||||
@@ -120,12 +124,14 @@ static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2;
|
||||
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;
|
||||
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;
|
||||
inline auto JA_FeedMusicChunk(JA_Music_t* music) -> int {
|
||||
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
short chunk[JA_MUSIC_CHUNK_SHORTS];
|
||||
const int num_channels = music->spec.channels;
|
||||
@@ -134,7 +140,9 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) {
|
||||
num_channels,
|
||||
chunk,
|
||||
JA_MUSIC_CHUNK_SHORTS);
|
||||
if (samples_per_channel <= 0) return 0;
|
||||
if (samples_per_channel <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, chunk, bytes);
|
||||
@@ -144,19 +152,25 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) {
|
||||
// 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;
|
||||
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) {
|
||||
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;
|
||||
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--;
|
||||
if (music->times > 0) {
|
||||
music->times--;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -168,14 +182,18 @@ inline void JA_PumpMusic(JA_Music_t* music) {
|
||||
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
|
||||
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
|
||||
inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
|
||||
if (!music || !music->vorbis || !music->stream) return;
|
||||
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
const int needed_bytes = static_cast<int>((static_cast<int64_t>(duration_ms) * bytes_per_second) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) {
|
||||
const int decoded = JA_FeedMusicChunk(music);
|
||||
if (decoded <= 0) break; // EOF: deixem drenar el que hi haja
|
||||
if (decoded <= 0) {
|
||||
break; // EOF: deixem drenar el que hi haja
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +201,7 @@ inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
|
||||
|
||||
inline void JA_Update() {
|
||||
// --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
|
||||
if (outgoing_music.stream && outgoing_music.fade.active) {
|
||||
if ((outgoing_music.stream != nullptr) && outgoing_music.fade.active) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
Uint64 elapsed = now - outgoing_music.fade.start_time;
|
||||
if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
|
||||
@@ -192,12 +210,12 @@ inline void JA_Update() {
|
||||
outgoing_music.fade.active = false;
|
||||
} else {
|
||||
float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms;
|
||||
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent));
|
||||
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - percent));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Current music ---
|
||||
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
|
||||
if (JA_musicEnabled && (current_music != nullptr) && current_music->state == JA_MUSIC_PLAYING) {
|
||||
// Fade-in (parte de un crossfade)
|
||||
if (incoming_fade.active) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
@@ -221,42 +239,59 @@ inline void JA_Update() {
|
||||
|
||||
// --- Sound channels ---
|
||||
if (JA_soundEnabled) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
if (channels[i].times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
|
||||
if (channels[i].times > 0) channels[i].times--;
|
||||
if (channels[i].times > 0) {
|
||||
channels[i].times--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
|
||||
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) {
|
||||
JA_StopChannel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||
JA_audioSpec = {format, num_channels, freq};
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
JA_audioSpec = {.format = format, .channels = num_channels, .freq = freq};
|
||||
if (sdlAudioDevice != 0u) {
|
||||
SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
}
|
||||
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
|
||||
if (sdlAudioDevice == 0) {
|
||||
std::cout << "Failed to initialize SDL audio!" << '\n';
|
||||
}
|
||||
for (auto& channel : channels) {
|
||||
channel.state = JA_CHANNEL_FREE;
|
||||
}
|
||||
for (float& i : JA_soundVolume) {
|
||||
i = 0.5F;
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_Quit() {
|
||||
if (outgoing_music.stream) {
|
||||
if (outgoing_music.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
}
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
if (sdlAudioDevice != 0u) {
|
||||
SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
}
|
||||
sdlAudioDevice = 0;
|
||||
}
|
||||
|
||||
// --- Music Functions ---
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
if (!buffer || length == 0) return nullptr;
|
||||
inline auto JA_LoadMusic(const Uint8* buffer, Uint32 length) -> JA_Music_t* {
|
||||
if ((buffer == nullptr) || length == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
|
||||
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||
@@ -270,7 +305,7 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
static_cast<int>(length),
|
||||
&vorbis_error,
|
||||
nullptr);
|
||||
if (!music->vorbis) {
|
||||
if (music->vorbis == nullptr) {
|
||||
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
|
||||
delete music;
|
||||
return nullptr;
|
||||
@@ -287,21 +322,25 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
|
||||
// Overload amb filename — els callers l'usen per poder comparar la música
|
||||
// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa.
|
||||
inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
|
||||
inline auto JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) -> JA_Music_t* {
|
||||
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), length);
|
||||
if (music && filename) music->filename = filename;
|
||||
if ((music != nullptr) && (filename != nullptr)) {
|
||||
music->filename = filename;
|
||||
}
|
||||
return music;
|
||||
}
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
inline auto JA_LoadMusic(const char* filename) -> JA_Music_t* {
|
||||
// Carreguem primer el arxiu en memòria i després el descomprimim.
|
||||
FILE* f = fopen(filename, "rb");
|
||||
if (!f) return nullptr;
|
||||
if (f == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
|
||||
if (!buffer) {
|
||||
if (buffer == nullptr) {
|
||||
fclose(f);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -313,7 +352,7 @@ inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
fclose(f);
|
||||
|
||||
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
|
||||
if (music) {
|
||||
if (music != nullptr) {
|
||||
music->filename = filename;
|
||||
}
|
||||
|
||||
@@ -323,7 +362,9 @@ inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
}
|
||||
|
||||
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
if (!JA_musicEnabled || !music || !music->vorbis) return;
|
||||
if (!JA_musicEnabled || (music == nullptr) || (music->vorbis == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
JA_StopMusic();
|
||||
|
||||
@@ -336,7 +377,7 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||
if (!current_music->stream) {
|
||||
if (current_music->stream == nullptr) {
|
||||
std::cout << "Failed to create audio stream!" << '\n';
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
return;
|
||||
@@ -351,23 +392,35 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
|
||||
if (!music) music = current_music;
|
||||
if (!music || music->filename.empty()) return nullptr;
|
||||
inline auto JA_GetMusicFilename(const JA_Music_t* music = nullptr) -> const char* {
|
||||
if (music == nullptr) {
|
||||
music = current_music;
|
||||
}
|
||||
if ((music == nullptr) || music->filename.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return music->filename.c_str();
|
||||
}
|
||||
|
||||
inline void JA_PauseMusic() {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
|
||||
if (!JA_musicEnabled) {
|
||||
return;
|
||||
}
|
||||
if ((current_music == nullptr) || 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;
|
||||
if (!JA_musicEnabled) {
|
||||
return;
|
||||
}
|
||||
if ((current_music == nullptr) || current_music->state != JA_MUSIC_PAUSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||
@@ -375,33 +428,39 @@ inline void JA_ResumeMusic() {
|
||||
|
||||
inline void JA_StopMusic() {
|
||||
// Limpiar outgoing crossfade si existe
|
||||
if (outgoing_music.stream) {
|
||||
if (outgoing_music.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
}
|
||||
incoming_fade.active = false;
|
||||
|
||||
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
||||
if ((current_music == nullptr) || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->stream) {
|
||||
if (current_music->stream != nullptr) {
|
||||
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) {
|
||||
if (current_music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_FadeOutMusic(const int milliseconds) {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
|
||||
if (!JA_musicEnabled) {
|
||||
return;
|
||||
}
|
||||
if ((current_music == nullptr) || current_music->state != JA_MUSIC_PLAYING) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destruir outgoing anterior si existe
|
||||
if (outgoing_music.stream) {
|
||||
if (outgoing_music.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
}
|
||||
@@ -412,20 +471,24 @@ inline void JA_FadeOutMusic(const int milliseconds) {
|
||||
|
||||
// Robar el stream del current_music al outgoing
|
||||
outgoing_music.stream = current_music->stream;
|
||||
outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume};
|
||||
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = milliseconds, .initial_volume = JA_musicVolume};
|
||||
|
||||
// Dejar current_music sin stream (ya lo tiene outgoing)
|
||||
current_music->stream = nullptr;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
|
||||
if (current_music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
}
|
||||
incoming_fade.active = false;
|
||||
}
|
||||
|
||||
inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) {
|
||||
if (!JA_musicEnabled || !music || !music->vorbis) return;
|
||||
if (!JA_musicEnabled || (music == nullptr) || (music->vorbis == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destruir outgoing anterior si existe (crossfade durante crossfade)
|
||||
if (outgoing_music.stream) {
|
||||
if (outgoing_music.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
@@ -434,13 +497,15 @@ inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const i
|
||||
// Robar el stream de la musica actual al outgoing para el fade-out.
|
||||
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
|
||||
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
|
||||
if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) {
|
||||
if ((current_music != nullptr) && current_music->state == JA_MUSIC_PLAYING && (current_music->stream != nullptr)) {
|
||||
JA_PreFillOutgoing(current_music, crossfade_ms);
|
||||
outgoing_music.stream = current_music->stream;
|
||||
outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume};
|
||||
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = JA_musicVolume};
|
||||
current_music->stream = nullptr;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
|
||||
if (current_music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
|
||||
@@ -450,42 +515,52 @@ inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const i
|
||||
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||
if (!current_music->stream) {
|
||||
if (current_music->stream == nullptr) {
|
||||
std::cout << "Failed to create audio stream for crossfade!" << '\n';
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
return;
|
||||
}
|
||||
SDL_SetAudioStreamGain(current_music->stream, 0.0f);
|
||||
SDL_SetAudioStreamGain(current_music->stream, 0.0F);
|
||||
JA_PumpMusic(current_music); // pre-carrega abans de bindejar
|
||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||
|
||||
// Configurar fade-in
|
||||
incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f};
|
||||
incoming_fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = 0.0F};
|
||||
}
|
||||
|
||||
inline JA_Music_state JA_GetMusicState() {
|
||||
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
||||
if (!current_music) return JA_MUSIC_INVALID;
|
||||
inline auto JA_GetMusicState() -> JA_Music_state {
|
||||
if (!JA_musicEnabled) {
|
||||
return JA_MUSIC_DISABLED;
|
||||
}
|
||||
if (current_music == nullptr) {
|
||||
return JA_MUSIC_INVALID;
|
||||
}
|
||||
|
||||
return current_music->state;
|
||||
}
|
||||
|
||||
inline void JA_DeleteMusic(JA_Music_t* music) {
|
||||
if (!music) return;
|
||||
if (music == nullptr) {
|
||||
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);
|
||||
if (music->stream != nullptr) {
|
||||
SDL_DestroyAudioStream(music->stream);
|
||||
}
|
||||
if (music->vorbis != nullptr) {
|
||||
stb_vorbis_close(music->vorbis);
|
||||
}
|
||||
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
|
||||
// al destructor de JA_Music_t.
|
||||
delete music;
|
||||
}
|
||||
|
||||
inline float JA_SetMusicVolume(float volume) {
|
||||
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
||||
if (current_music && current_music->stream) {
|
||||
inline auto JA_SetMusicVolume(float volume) -> float {
|
||||
JA_musicVolume = SDL_clamp(volume, 0.0F, 1.0F);
|
||||
if ((current_music != nullptr) && (current_music->stream != nullptr)) {
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||
}
|
||||
return JA_musicVolume;
|
||||
@@ -495,18 +570,20 @@ inline void JA_SetMusicPosition(float /*value*/) {
|
||||
// No implementat amb el backend de streaming.
|
||||
}
|
||||
|
||||
inline float JA_GetMusicPosition() {
|
||||
return 0.0f;
|
||||
inline auto JA_GetMusicPosition() -> float {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
inline void JA_EnableMusic(const bool value) {
|
||||
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
||||
if (!value && (current_music != nullptr) && (current_music->state == JA_MUSIC_PLAYING)) {
|
||||
JA_StopMusic();
|
||||
}
|
||||
JA_musicEnabled = value;
|
||||
}
|
||||
|
||||
// --- Sound Functions ---
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||
inline auto JA_LoadSound(uint8_t* buffer, uint32_t size) -> JA_Sound_t* {
|
||||
auto sound = std::make_unique<JA_Sound_t>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
|
||||
@@ -517,7 +594,7 @@ inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||
inline auto JA_LoadSound(const char* filename) -> JA_Sound_t* {
|
||||
auto sound = std::make_unique<JA_Sound_t>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
|
||||
@@ -528,8 +605,10 @@ inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
|
||||
if (!JA_soundEnabled || !sound) return -1;
|
||||
inline auto JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) -> int {
|
||||
if (!JA_soundEnabled || (sound == nullptr)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int channel = 0;
|
||||
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
||||
@@ -541,9 +620,13 @@ inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group =
|
||||
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;
|
||||
inline auto JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) -> int {
|
||||
if (!JA_soundEnabled || (sound == nullptr)) {
|
||||
return -1;
|
||||
}
|
||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
JA_StopChannel(channel);
|
||||
|
||||
@@ -554,7 +637,7 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||
|
||||
if (!channels[channel].stream) {
|
||||
if (channels[channel].stream == nullptr) {
|
||||
std::cout << "Failed to create audio stream for sound!" << '\n';
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
return -1;
|
||||
@@ -568,23 +651,30 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
|
||||
}
|
||||
|
||||
inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||
if (!sound) return;
|
||||
if (sound == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||
if (channels[i].sound == sound) {
|
||||
JA_StopChannel(i);
|
||||
}
|
||||
}
|
||||
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
|
||||
delete sound;
|
||||
}
|
||||
|
||||
inline void JA_PauseChannel(const int channel) {
|
||||
if (!JA_soundEnabled) return;
|
||||
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);
|
||||
for (auto& channel : channels) {
|
||||
if (channel.state == JA_CHANNEL_PLAYING) {
|
||||
channel.state = JA_CHANNEL_PAUSED;
|
||||
SDL_UnbindAudioStream(channel.stream);
|
||||
}
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
||||
channels[channel].state = JA_CHANNEL_PAUSED;
|
||||
@@ -594,14 +684,17 @@ inline void JA_PauseChannel(const int channel) {
|
||||
}
|
||||
|
||||
inline void JA_ResumeChannel(const int channel) {
|
||||
if (!JA_soundEnabled) return;
|
||||
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);
|
||||
for (auto& channel : channels) {
|
||||
if (channel.state == JA_CHANNEL_PAUSED) {
|
||||
channel.state = JA_CHANNEL_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, channel.stream);
|
||||
}
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
@@ -612,18 +705,22 @@ inline void JA_ResumeChannel(const int channel) {
|
||||
|
||||
inline void JA_StopChannel(const int channel) {
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].state != JA_CHANNEL_FREE) {
|
||||
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
|
||||
channels[i].stream = nullptr;
|
||||
channels[i].state = JA_CHANNEL_FREE;
|
||||
channels[i].pos = 0;
|
||||
channels[i].sound = nullptr;
|
||||
for (auto& channel : channels) {
|
||||
if (channel.state != JA_CHANNEL_FREE) {
|
||||
if (channel.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(channel.stream);
|
||||
}
|
||||
channel.stream = nullptr;
|
||||
channel.state = JA_CHANNEL_FREE;
|
||||
channel.pos = 0;
|
||||
channel.sound = nullptr;
|
||||
}
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state != JA_CHANNEL_FREE) {
|
||||
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
|
||||
if (channels[channel].stream != nullptr) {
|
||||
SDL_DestroyAudioStream(channels[channel].stream);
|
||||
}
|
||||
channels[channel].stream = nullptr;
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
channels[channel].pos = 0;
|
||||
@@ -632,19 +729,23 @@ inline void JA_StopChannel(const int channel) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
inline auto JA_GetChannelState(const int channel) -> JA_Channel_state {
|
||||
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);
|
||||
inline auto JA_SetSoundVolume(float volume, const int group = -1) -> float {
|
||||
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;
|
||||
for (float& i : JA_soundVolume) {
|
||||
i = v;
|
||||
}
|
||||
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
||||
JA_soundVolume[group] = v;
|
||||
@@ -653,11 +754,11 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) {
|
||||
}
|
||||
|
||||
// Aplicar volum als canals actius.
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
||||
if (group == -1 || channels[i].group == group) {
|
||||
if (channels[i].stream) {
|
||||
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
|
||||
for (auto& channel : channels) {
|
||||
if ((channel.state == JA_CHANNEL_PLAYING) || (channel.state == JA_CHANNEL_PAUSED)) {
|
||||
if (group == -1 || channel.group == group) {
|
||||
if (channel.stream != nullptr) {
|
||||
SDL_SetAudioStreamGain(channel.stream, JA_soundVolume[channel.group]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,7 +773,7 @@ inline void JA_EnableSound(const bool value) {
|
||||
JA_soundEnabled = value;
|
||||
}
|
||||
|
||||
inline float JA_SetVolume(float volume) {
|
||||
inline auto JA_SetVolume(float volume) -> float {
|
||||
float v = JA_SetMusicVolume(volume);
|
||||
JA_SetSoundVolume(v, -1);
|
||||
return v;
|
||||
|
||||
@@ -52,8 +52,8 @@ namespace Gamepad {
|
||||
// Recorta el nom visible del mando: trim des del primer '(' o '['
|
||||
// (per a evitar coses com "Retroid Controller (vendor: 1001) ..."),
|
||||
// elimina espais finals i talla a 25 caràcters.
|
||||
static std::string prettyName(const char* raw) {
|
||||
std::string name = (raw && *raw) ? raw : "Gamepad";
|
||||
static auto prettyName(const char* raw) -> std::string {
|
||||
std::string name = ((raw != nullptr) && (*raw != 0)) ? raw : "Gamepad";
|
||||
const auto pos = name.find_first_of("([");
|
||||
if (pos != std::string::npos) {
|
||||
name.erase(pos);
|
||||
@@ -64,7 +64,9 @@ namespace Gamepad {
|
||||
if (name.size() > 25) {
|
||||
name.resize(25);
|
||||
}
|
||||
if (name.empty()) name = "Gamepad";
|
||||
if (name.empty()) {
|
||||
name = "Gamepad";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -98,12 +100,14 @@ namespace Gamepad {
|
||||
static void openFirstGamepad() {
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||
if (ids) {
|
||||
if (ids != nullptr) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
installWebStandardMapping(ids[i]);
|
||||
if (!SDL_IsGamepad(ids[i])) continue;
|
||||
if (!SDL_IsGamepad(ids[i])) {
|
||||
continue;
|
||||
}
|
||||
pad_ = SDL_OpenGamepad(ids[i]);
|
||||
if (pad_) {
|
||||
if (pad_ != nullptr) {
|
||||
pad_id_ = ids[i];
|
||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
||||
break;
|
||||
@@ -128,7 +132,7 @@ namespace Gamepad {
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (pad_) {
|
||||
if (pad_ != nullptr) {
|
||||
SDL_CloseGamepad(pad_);
|
||||
pad_ = nullptr;
|
||||
pad_id_ = 0;
|
||||
@@ -145,12 +149,14 @@ namespace Gamepad {
|
||||
// GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
|
||||
// injectem el mapping estàndard abans d'obrir el mando.
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
|
||||
if (!pad_) {
|
||||
if (pad_ == nullptr) {
|
||||
SDL_JoystickID jid = event.jdevice.which;
|
||||
installWebStandardMapping(jid);
|
||||
if (!SDL_IsGamepad(jid)) return;
|
||||
if (!SDL_IsGamepad(jid)) {
|
||||
return;
|
||||
}
|
||||
pad_ = SDL_OpenGamepad(jid);
|
||||
if (pad_) {
|
||||
if (pad_ != nullptr) {
|
||||
pad_id_ = jid;
|
||||
std::string name = prettyName(SDL_GetGamepadName(pad_));
|
||||
SDL_Log("Gamepad connectat: %s", name.c_str());
|
||||
@@ -158,7 +164,7 @@ namespace Gamepad {
|
||||
}
|
||||
}
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
if (pad_ && event.jdevice.which == pad_id_) {
|
||||
if ((pad_ != nullptr) && event.jdevice.which == pad_id_) {
|
||||
std::string saved_name = prettyName(SDL_GetGamepadName(pad_));
|
||||
SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
|
||||
SDL_CloseGamepad(pad_);
|
||||
@@ -190,7 +196,9 @@ namespace Gamepad {
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (!pad_) return;
|
||||
if (pad_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// D-pad
|
||||
bool dup = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
@@ -220,19 +228,35 @@ namespace Gamepad {
|
||||
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
|
||||
|
||||
// Select (Back) → obre/tanca menú de servei (flanc)
|
||||
if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle"));
|
||||
if (back && !prev_back_) {
|
||||
pushKey(KeyConfig::scancode("menu_toggle"));
|
||||
}
|
||||
// Start → pausa (flanc)
|
||||
if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle"));
|
||||
if (start && !prev_start_) {
|
||||
pushKey(KeyConfig::scancode("pause_toggle"));
|
||||
}
|
||||
|
||||
if (Menu::isOpen()) {
|
||||
// Navegació del menú per flanc
|
||||
if (up && !prev_up_) pushKey(SDL_SCANCODE_UP);
|
||||
if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN);
|
||||
if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT);
|
||||
if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT);
|
||||
if (up && !prev_up_) {
|
||||
pushKey(SDL_SCANCODE_UP);
|
||||
}
|
||||
if (dn && !prev_down_) {
|
||||
pushKey(SDL_SCANCODE_DOWN);
|
||||
}
|
||||
if (lt && !prev_left_) {
|
||||
pushKey(SDL_SCANCODE_LEFT);
|
||||
}
|
||||
if (rt && !prev_right_) {
|
||||
pushKey(SDL_SCANCODE_RIGHT);
|
||||
}
|
||||
// EAST accepta, SOUTH cancela / endarrere
|
||||
if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN);
|
||||
if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE);
|
||||
if (east && !prev_east_) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
if (south && !prev_south_) {
|
||||
pushKey(SDL_SCANCODE_BACKSPACE);
|
||||
}
|
||||
|
||||
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
||||
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
||||
|
||||
@@ -34,7 +34,9 @@ namespace GlobalInputs {
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
if (dec_zoom) consumed = true;
|
||||
if (dec_zoom) {
|
||||
consumed = true;
|
||||
}
|
||||
dec_zoom_prev = dec_zoom;
|
||||
|
||||
// F2 — Augmentar zoom
|
||||
@@ -45,7 +47,9 @@ namespace GlobalInputs {
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
if (inc_zoom) consumed = true;
|
||||
if (inc_zoom) {
|
||||
consumed = true;
|
||||
}
|
||||
inc_zoom_prev = inc_zoom;
|
||||
|
||||
// F3 — Toggle pantalla completa
|
||||
@@ -54,7 +58,9 @@ namespace GlobalInputs {
|
||||
Screen::get()->toggleFullscreen();
|
||||
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
|
||||
}
|
||||
if (fullscreen) consumed = true;
|
||||
if (fullscreen) {
|
||||
consumed = true;
|
||||
}
|
||||
fullscreen_prev = fullscreen;
|
||||
|
||||
// F4 — Toggle shaders
|
||||
@@ -63,7 +69,9 @@ namespace GlobalInputs {
|
||||
Screen::get()->toggleShaders();
|
||||
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
|
||||
}
|
||||
if (shader) consumed = true;
|
||||
if (shader) {
|
||||
consumed = true;
|
||||
}
|
||||
shader_prev = shader;
|
||||
|
||||
// F5 — Toggle aspect ratio 4:3
|
||||
@@ -72,7 +80,9 @@ namespace GlobalInputs {
|
||||
Screen::get()->toggleAspectRatio();
|
||||
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
||||
}
|
||||
if (aspect) consumed = true;
|
||||
if (aspect) {
|
||||
consumed = true;
|
||||
}
|
||||
aspect_prev = aspect;
|
||||
|
||||
// F6 — Toggle supersampling
|
||||
@@ -82,7 +92,9 @@ namespace GlobalInputs {
|
||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||
}
|
||||
}
|
||||
if (ss) consumed = true;
|
||||
if (ss) {
|
||||
consumed = true;
|
||||
}
|
||||
ss_prev = ss;
|
||||
|
||||
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
|
||||
@@ -94,7 +106,9 @@ namespace GlobalInputs {
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_shader) consumed = true;
|
||||
if (next_shader) {
|
||||
consumed = true;
|
||||
}
|
||||
next_shader_prev = next_shader;
|
||||
|
||||
// F8 — Pròxim preset del shader actiu
|
||||
@@ -106,7 +120,9 @@ namespace GlobalInputs {
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_preset) consumed = true;
|
||||
if (next_preset) {
|
||||
consumed = true;
|
||||
}
|
||||
next_preset_prev = next_preset;
|
||||
|
||||
// F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
|
||||
@@ -117,7 +133,9 @@ namespace GlobalInputs {
|
||||
? Locale::get("notifications.filter_linear")
|
||||
: Locale::get("notifications.filter_nearest"));
|
||||
}
|
||||
if (texture_filter) consumed = true;
|
||||
if (texture_filter) {
|
||||
consumed = true;
|
||||
}
|
||||
texture_filter_prev = texture_filter;
|
||||
|
||||
// F10 — Toggle render info (FPS, driver, shader)
|
||||
@@ -125,7 +143,9 @@ namespace GlobalInputs {
|
||||
if (render_info && !render_info_prev) {
|
||||
Overlay::toggleRenderInfo();
|
||||
}
|
||||
if (render_info) consumed = true;
|
||||
if (render_info) {
|
||||
consumed = true;
|
||||
}
|
||||
render_info_prev = render_info;
|
||||
|
||||
return consumed;
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace KeyConfig {
|
||||
|
||||
auto findIndex(const std::string& id) -> size_t {
|
||||
auto it = index_.find(id);
|
||||
if (it == index_.end()) return SIZE_MAX;
|
||||
if (it == index_.end()) {
|
||||
return SIZE_MAX;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -30,7 +32,9 @@ namespace KeyConfig {
|
||||
std::string content(buf.begin(), buf.end());
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (!yaml.contains("keys")) return;
|
||||
if (!yaml.contains("keys")) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& node : yaml["keys"]) {
|
||||
KeyEntry entry;
|
||||
@@ -59,7 +63,9 @@ namespace KeyConfig {
|
||||
|
||||
void applyOverrides(const std::string& disk_path) {
|
||||
std::ifstream file(disk_path);
|
||||
if (!file.good()) return;
|
||||
if (!file.good()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
@@ -67,7 +73,9 @@ namespace KeyConfig {
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (!yaml.contains("overrides")) return;
|
||||
if (!yaml.contains("overrides")) {
|
||||
return;
|
||||
}
|
||||
|
||||
int applied = 0;
|
||||
for (const auto& kv : yaml["overrides"].as_map()) {
|
||||
@@ -118,28 +126,38 @@ namespace KeyConfig {
|
||||
|
||||
auto scancode(const std::string& id) -> SDL_Scancode {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return SDL_SCANCODE_UNKNOWN;
|
||||
if (idx == SIZE_MAX) {
|
||||
return SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
return entries_[idx].scancode;
|
||||
}
|
||||
|
||||
auto scancodePtr(const std::string& id) -> SDL_Scancode* {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return nullptr;
|
||||
if (idx == SIZE_MAX) {
|
||||
return nullptr;
|
||||
}
|
||||
return &entries_[idx].scancode;
|
||||
}
|
||||
|
||||
void setScancode(const std::string& id, SDL_Scancode sc) {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return;
|
||||
if (idx == SIZE_MAX) {
|
||||
return;
|
||||
}
|
||||
entries_[idx].scancode = sc;
|
||||
const char* name = SDL_GetScancodeName(sc);
|
||||
entries_[idx].code = (name != nullptr) ? name : "";
|
||||
}
|
||||
|
||||
auto isGuiKey(SDL_Scancode sc) -> bool {
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) return false;
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& e : entries_) {
|
||||
if (e.scancode == sc) return true;
|
||||
if (e.scancode == sc) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -149,12 +167,16 @@ namespace KeyConfig {
|
||||
}
|
||||
|
||||
auto saveOverrides() -> bool {
|
||||
if (overrides_path_.empty()) return false;
|
||||
if (overrides_path_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recull només les entrades remapeades.
|
||||
std::vector<const KeyEntry*> changed;
|
||||
for (const auto& e : entries_) {
|
||||
if (e.scancode != e.default_scancode) changed.push_back(&e);
|
||||
if (e.scancode != e.default_scancode) {
|
||||
changed.push_back(&e);
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(overrides_path_);
|
||||
|
||||
@@ -17,7 +17,9 @@ namespace KeyRemap {
|
||||
|
||||
void update() {
|
||||
const bool* ks = SDL_GetKeyboardState(nullptr);
|
||||
if (!ks) return;
|
||||
if (ks == nullptr) {
|
||||
return;
|
||||
}
|
||||
mirror(Options::keys_game.up, SDL_SCANCODE_UP, ks);
|
||||
mirror(Options::keys_game.down, SDL_SCANCODE_DOWN, ks);
|
||||
mirror(Options::keys_game.left, SDL_SCANCODE_LEFT, ks);
|
||||
|
||||
+43
-24
@@ -42,18 +42,18 @@ void JD8_ClearScreen(Uint8 color) {
|
||||
memset(screen, color, 64000);
|
||||
}
|
||||
|
||||
JD8_Surface JD8_NewSurface() {
|
||||
auto JD8_NewSurface() -> JD8_Surface {
|
||||
return new Uint8[64000]{};
|
||||
}
|
||||
|
||||
// Helper intern: deriva el basename d'una ruta per a buscar al Cache.
|
||||
static std::string jd8_basename(const char* file) {
|
||||
static auto jd8_basename(const char* file) -> std::string {
|
||||
std::string s = file;
|
||||
auto pos = s.find_last_of("/\\");
|
||||
return pos == std::string::npos ? s : s.substr(pos + 1);
|
||||
}
|
||||
|
||||
JD8_Surface JD8_LoadSurface(const char* file) {
|
||||
auto JD8_LoadSurface(const char* file) -> JD8_Surface {
|
||||
// Prova primer el Resource::Cache. Si l'asset és precarregat, copiem
|
||||
// els 64KB des del cache (microsegons) i ens estalviem la decodificació
|
||||
// GIF. Mantenim el contracte de la funció: el caller rep un buffer
|
||||
@@ -70,7 +70,8 @@ JD8_Surface JD8_LoadSurface(const char* file) {
|
||||
}
|
||||
|
||||
auto buffer = ResourceHelper::loadFile(file);
|
||||
unsigned short w, h;
|
||||
unsigned short w;
|
||||
unsigned short h;
|
||||
Uint8* pixels = LoadGif(buffer.data(), &w, &h);
|
||||
if (pixels == nullptr) {
|
||||
printf("Unable to load bitmap: %s\n", SDL_GetError());
|
||||
@@ -82,11 +83,11 @@ JD8_Surface JD8_LoadSurface(const char* file) {
|
||||
return image;
|
||||
}
|
||||
|
||||
JD8_Palette JD8_LoadPalette(const char* file) {
|
||||
auto JD8_LoadPalette(const char* file) -> JD8_Palette {
|
||||
// Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
|
||||
// — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
|
||||
// l'ownership a `JD8_SetScreenPalette`).
|
||||
JD8_Palette palette = new Color[256];
|
||||
auto palette = new Color[256];
|
||||
|
||||
if (Resource::Cache::get() != nullptr) {
|
||||
try {
|
||||
@@ -106,7 +107,9 @@ JD8_Palette JD8_LoadPalette(const char* file) {
|
||||
}
|
||||
|
||||
void JD8_SetScreenPalette(JD8_Palette palette) {
|
||||
if (main_palette == palette) return;
|
||||
if (main_palette == palette) {
|
||||
return;
|
||||
}
|
||||
delete[] main_palette;
|
||||
main_palette = palette;
|
||||
}
|
||||
@@ -126,9 +129,15 @@ void JD8_FillRect(int x, int y, int w, int h, Uint8 color) {
|
||||
h += y;
|
||||
y = 0;
|
||||
}
|
||||
if (x + w > 320) w = 320 - x;
|
||||
if (y + h > 200) h = 200 - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
if (x + w > 320) {
|
||||
w = 320 - x;
|
||||
}
|
||||
if (y + h > 200) {
|
||||
h = 200 - y;
|
||||
}
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
for (int row = y; row < y + h; ++row) {
|
||||
memset(&screen[x + (row * 320)], color, w);
|
||||
}
|
||||
@@ -158,47 +167,55 @@ void JD8_BlitToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCK(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
void JD8_BlitCK(int x, int y, const JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int j = 0; j < sh; j++) {
|
||||
for (int i = 0; i < sw; i++) {
|
||||
if (surface[src_pointer + i] != colorkey) screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
if (surface[src_pointer + i] != colorkey) {
|
||||
screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
}
|
||||
}
|
||||
src_pointer += 320;
|
||||
dst_pointer += 320;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCKCut(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
void JD8_BlitCKCut(int x, int y, const JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int j = 0; j < sh; j++) {
|
||||
for (int i = 0; i < sw; i++) {
|
||||
if (surface[src_pointer + i] != colorkey && (x + i >= 0) && (y + j >= 0) && (x + i < 320) && (y + j < 200)) screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
if (surface[src_pointer + i] != colorkey && (x + i >= 0) && (y + j >= 0) && (x + i < 320) && (y + j < 200)) {
|
||||
screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
}
|
||||
}
|
||||
src_pointer += 320;
|
||||
dst_pointer += 320;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey) {
|
||||
void JD8_BlitCKScroll(int y, const JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey) {
|
||||
int dst_pointer = y * 320;
|
||||
for (int j = sy; j < sy + sh; j++) {
|
||||
for (int i = 0; i < 320; i++) {
|
||||
int x = (i + sx) % 320;
|
||||
if (surface[x + j * 320] != colorkey) screen[dst_pointer] = surface[x + j * 320];
|
||||
if (surface[x + (j * 320)] != colorkey) {
|
||||
screen[dst_pointer] = surface[x + (j * 320)];
|
||||
}
|
||||
dst_pointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey) {
|
||||
void JD8_BlitCKToSurface(int x, int y, const JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int j = 0; j < sh; j++) {
|
||||
for (int i = 0; i < sw; i++) {
|
||||
if (surface[src_pointer + i] != colorkey) dest[dst_pointer + i] = surface[src_pointer + i];
|
||||
if (surface[src_pointer + i] != colorkey) {
|
||||
dest[dst_pointer + i] = surface[src_pointer + i];
|
||||
}
|
||||
}
|
||||
src_pointer += 320;
|
||||
dst_pointer += 320;
|
||||
@@ -218,15 +235,15 @@ void JD8_Flip() {
|
||||
}
|
||||
}
|
||||
|
||||
Uint32* JD8_GetFramebuffer() {
|
||||
auto JD8_GetFramebuffer() -> Uint32* {
|
||||
return pixel_data;
|
||||
}
|
||||
|
||||
void JD8_FreeSurface(JD8_Surface surface) {
|
||||
void JD8_FreeSurface(const JD8_Surface surface) {
|
||||
delete[] surface;
|
||||
}
|
||||
|
||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) {
|
||||
auto JD8_GetPixel(JD8_Surface surface, int x, int y) -> Uint8 {
|
||||
return surface[x + (y * 320)];
|
||||
}
|
||||
|
||||
@@ -292,12 +309,14 @@ void JD8_FadeStartToPal(JD8_Palette pal) {
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
bool JD8_FadeIsActive() {
|
||||
auto JD8_FadeIsActive() -> bool {
|
||||
return fade_type != FadeType::None;
|
||||
}
|
||||
|
||||
bool JD8_FadeTickStep() {
|
||||
if (fade_type == FadeType::None) return true;
|
||||
auto JD8_FadeTickStep() -> bool {
|
||||
if (fade_type == FadeType::None) {
|
||||
return true;
|
||||
}
|
||||
|
||||
apply_fade_step();
|
||||
fade_step++;
|
||||
|
||||
+12
-12
@@ -16,11 +16,11 @@ void JD8_Quit();
|
||||
|
||||
void JD8_ClearScreen(Uint8 color);
|
||||
|
||||
JD8_Surface JD8_NewSurface();
|
||||
auto JD8_NewSurface() -> JD8_Surface;
|
||||
|
||||
JD8_Surface JD8_LoadSurface(const char* file);
|
||||
auto JD8_LoadSurface(const char* file) -> JD8_Surface;
|
||||
|
||||
JD8_Palette JD8_LoadPalette(const char* file);
|
||||
auto JD8_LoadPalette(const char* file) -> JD8_Palette;
|
||||
|
||||
void JD8_SetScreenPalette(JD8_Palette palette);
|
||||
|
||||
@@ -36,13 +36,13 @@ void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh)
|
||||
|
||||
void JD8_BlitToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest);
|
||||
|
||||
void JD8_BlitCK(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
void JD8_BlitCK(int x, int y, const JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
|
||||
void JD8_BlitCKCut(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
void JD8_BlitCKCut(int x, int y, const JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
|
||||
void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey);
|
||||
void JD8_BlitCKScroll(int y, const JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey);
|
||||
|
||||
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey);
|
||||
void JD8_BlitCKToSurface(int x, int y, const JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey);
|
||||
|
||||
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
|
||||
// funció al final de cada tick i després llegeix el framebuffer via
|
||||
@@ -51,11 +51,11 @@ void JD8_Flip();
|
||||
|
||||
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
|
||||
// JD8_Flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
|
||||
Uint32* JD8_GetFramebuffer();
|
||||
auto JD8_GetFramebuffer() -> Uint32*;
|
||||
|
||||
void JD8_FreeSurface(JD8_Surface surface);
|
||||
void JD8_FreeSurface(const JD8_Surface surface);
|
||||
|
||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
||||
auto JD8_GetPixel(JD8_Surface surface, int x, int y) -> Uint8;
|
||||
|
||||
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
|
||||
|
||||
@@ -70,8 +70,8 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
||||
// L'embolcall `scenes::PaletteFade` ho fa més idiomàtic per a escenes.
|
||||
void JD8_FadeStartOut();
|
||||
void JD8_FadeStartToPal(JD8_Palette pal);
|
||||
bool JD8_FadeTickStep();
|
||||
bool JD8_FadeIsActive();
|
||||
auto JD8_FadeTickStep() -> bool;
|
||||
auto JD8_FadeIsActive() -> bool;
|
||||
|
||||
// JD_Font JD_LoadFont( char *file, int width, int height);
|
||||
|
||||
|
||||
+25
-11
@@ -27,12 +27,16 @@ namespace {
|
||||
config.clear();
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ifstream fi(config_file);
|
||||
if (!fi.is_open()) return;
|
||||
if (!fi.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(fi, line)) {
|
||||
const auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
if (eq == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
config.push_back({line.substr(0, eq), line.substr(eq + 1)});
|
||||
}
|
||||
}
|
||||
@@ -40,7 +44,9 @@ namespace {
|
||||
void save_config_values() {
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ofstream fo(config_file);
|
||||
if (!fo.is_open()) return;
|
||||
if (!fo.is_open()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& pair : config) {
|
||||
fo << pair.key << '=' << pair.value << '\n';
|
||||
}
|
||||
@@ -52,7 +58,7 @@ void file_setresourcefolder(const char* str) {
|
||||
resource_folder = str;
|
||||
}
|
||||
|
||||
const char* file_getresourcefolder() {
|
||||
auto file_getresourcefolder() -> const char* {
|
||||
return resource_folder.c_str();
|
||||
}
|
||||
|
||||
@@ -76,9 +82,13 @@ void file_setconfigfolder(const char* foldername) {
|
||||
// arranque dins del navegador. La config no persistirà entre recàrregues
|
||||
// (MEMFS és volàtil); caldria IDBFS si volguéssem persistència a web.
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
const char* homedir = (pw && pw->pw_dir && pw->pw_dir[0]) ? pw->pw_dir : nullptr;
|
||||
if (!homedir || !homedir[0]) homedir = getenv("HOME");
|
||||
if (!homedir || !homedir[0]) homedir = "/tmp";
|
||||
const char* homedir = ((pw != nullptr) && (pw->pw_dir != nullptr) && (pw->pw_dir[0] != 0)) ? pw->pw_dir : nullptr;
|
||||
if ((homedir == nullptr) || (homedir[0] == 0)) {
|
||||
homedir = getenv("HOME");
|
||||
}
|
||||
if ((homedir == nullptr) || (homedir[0] == 0)) {
|
||||
homedir = "/tmp";
|
||||
}
|
||||
config_folder = std::string(homedir) + "/.config/" + foldername;
|
||||
#endif
|
||||
|
||||
@@ -92,14 +102,16 @@ void file_setconfigfolder(const char* foldername) {
|
||||
// volàtil al navegador de totes formes: ignorem l'error i continuem.
|
||||
}
|
||||
|
||||
const char* file_getconfigfolder() {
|
||||
auto file_getconfigfolder() -> const char* {
|
||||
thread_local std::string folder;
|
||||
folder = config_folder + "/";
|
||||
return folder.c_str();
|
||||
}
|
||||
|
||||
const char* file_getconfigvalue(const char* key) {
|
||||
if (config.empty()) load_config_values();
|
||||
auto file_getconfigvalue(const char* key) -> const char* {
|
||||
if (config.empty()) {
|
||||
load_config_values();
|
||||
}
|
||||
for (const auto& pair : config) {
|
||||
if (pair.key == key) {
|
||||
thread_local std::string value_cache;
|
||||
@@ -111,7 +123,9 @@ const char* file_getconfigvalue(const char* key) {
|
||||
}
|
||||
|
||||
void file_setconfigvalue(const char* key, const char* value) {
|
||||
if (config.empty()) load_config_values();
|
||||
if (config.empty()) {
|
||||
load_config_values();
|
||||
}
|
||||
for (auto& pair : config) {
|
||||
if (pair.key == key) {
|
||||
pair.value = value;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
void file_setconfigfolder(const char* foldername);
|
||||
const char* file_getconfigfolder();
|
||||
auto file_getconfigfolder() -> const char*;
|
||||
|
||||
void file_setresourcefolder(const char* str);
|
||||
const char* file_getresourcefolder();
|
||||
auto file_getresourcefolder() -> const char*;
|
||||
|
||||
const char* file_getconfigvalue(const char* key);
|
||||
auto file_getconfigvalue(const char* key) -> const char*;
|
||||
void file_setconfigvalue(const char* key, const char* value);
|
||||
|
||||
@@ -24,7 +24,7 @@ void JG_QuitSignal() {
|
||||
quitting = true;
|
||||
}
|
||||
|
||||
bool JG_Quitting() {
|
||||
auto JG_Quitting() -> bool {
|
||||
return quitting;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ void JG_SetUpdateTicks(Uint32 milliseconds) {
|
||||
update_ticks = milliseconds;
|
||||
}
|
||||
|
||||
bool JG_ShouldUpdate() {
|
||||
auto JG_ShouldUpdate() -> bool {
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
if (now - update_time > update_ticks) {
|
||||
update_time = now;
|
||||
@@ -45,11 +45,11 @@ bool JG_ShouldUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 JG_GetCycleCounter() {
|
||||
auto JG_GetCycleCounter() -> Uint32 {
|
||||
return cycle_counter;
|
||||
}
|
||||
|
||||
Uint32 JG_GetDeltaMs() {
|
||||
auto JG_GetDeltaMs() -> Uint32 {
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
const Uint32 delta = now - last_delta_time;
|
||||
last_delta_time = now;
|
||||
|
||||
@@ -7,14 +7,14 @@ void JG_Finalize();
|
||||
|
||||
void JG_QuitSignal();
|
||||
|
||||
bool JG_Quitting();
|
||||
auto JG_Quitting() -> bool;
|
||||
|
||||
void JG_SetUpdateTicks(Uint32 milliseconds);
|
||||
|
||||
bool JG_ShouldUpdate();
|
||||
auto JG_ShouldUpdate() -> bool;
|
||||
|
||||
Uint32 JG_GetCycleCounter();
|
||||
auto JG_GetCycleCounter() -> Uint32;
|
||||
|
||||
// Temps transcorregut (en ms) des de l'última crida a JG_GetDeltaMs.
|
||||
// Helper per a la migració progressiva a time-based (Fase 4+).
|
||||
Uint32 JG_GetDeltaMs();
|
||||
auto JG_GetDeltaMs() -> Uint32;
|
||||
|
||||
+45
-22
@@ -1,5 +1,6 @@
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "core/system/director.hpp"
|
||||
@@ -19,7 +20,7 @@ namespace {
|
||||
// Temps restant en mil·lisegons durant el qual JI_KeyPressed/JI_AnyKey
|
||||
// retornen false. Utilitzat per a evitar que pulsacions fortuïtes
|
||||
// saltin cinemàtiques al començament.
|
||||
float wait_ms = 0.0f;
|
||||
float wait_ms = 0.0F;
|
||||
|
||||
// Per a calcular el delta entre crides a JI_Update sense que els callers
|
||||
// hagen de passar-lo explícitament. Es reinicia a la primera crida.
|
||||
@@ -29,7 +30,7 @@ namespace {
|
||||
|
||||
Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
|
||||
|
||||
Uint8 scancode_to_ascii(Uint8 scancode) {
|
||||
auto scancode_to_ascii(Uint8 scancode) -> Uint8 {
|
||||
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
|
||||
}
|
||||
@@ -47,8 +48,12 @@ void JI_SetInputBlocked(bool blocked) {
|
||||
}
|
||||
|
||||
void JI_SetVirtualKey(int scancode, int source, bool pressed) {
|
||||
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return;
|
||||
if (source < 0 || source >= JI_VSRC_COUNT) return;
|
||||
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) {
|
||||
return;
|
||||
}
|
||||
if (source < 0 || source >= JI_VSRC_COUNT) {
|
||||
return;
|
||||
}
|
||||
virtual_keystates[source][scancode] = pressed ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -64,49 +69,67 @@ void JI_Update() {
|
||||
// El director ha processat tots els events. Ací només refresquem
|
||||
// el snapshot del teclat i consumim el flag de tecla polsada.
|
||||
if (keystates == nullptr) {
|
||||
keystates = SDL_GetKeyboardState(NULL);
|
||||
keystates = SDL_GetKeyboardState(nullptr);
|
||||
}
|
||||
|
||||
const Uint64 now = SDL_GetTicks();
|
||||
if (last_update_tick == 0) last_update_tick = now;
|
||||
const float delta_ms = static_cast<float>(now - last_update_tick);
|
||||
if (last_update_tick == 0) {
|
||||
last_update_tick = now;
|
||||
}
|
||||
const auto delta_ms = static_cast<float>(now - last_update_tick);
|
||||
last_update_tick = now;
|
||||
|
||||
if (wait_ms > 0.0f) {
|
||||
if (wait_ms > 0.0F) {
|
||||
wait_ms -= delta_ms;
|
||||
if (wait_ms < 0.0f) wait_ms = 0.0f;
|
||||
wait_ms = std::max(wait_ms, 0.0f);
|
||||
}
|
||||
|
||||
// Consumim el flag de "alguna tecla no-GUI polsada" del director
|
||||
key_pressed = Director::get()->consumeKeyPressed();
|
||||
}
|
||||
|
||||
bool JI_KeyPressed(int key) {
|
||||
if (wait_ms > 0.0f || keystates == nullptr) return false;
|
||||
auto JI_KeyPressed(int key) -> bool {
|
||||
if (wait_ms > 0.0F || keystates == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// Input bloquejat (p.ex. menú flotant obert)
|
||||
if (input_blocked) return false;
|
||||
if (input_blocked) {
|
||||
return false;
|
||||
}
|
||||
// ESC bloquejada pel Director (primera pulsació mostra notificació)
|
||||
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false;
|
||||
if (key < 0 || key >= SDL_SCANCODE_COUNT) return false;
|
||||
if (keystates[key] != 0) return true;
|
||||
for (int src = 0; src < JI_VSRC_COUNT; src++) {
|
||||
if (virtual_keystates[src][key] != 0) return true;
|
||||
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) {
|
||||
return false;
|
||||
}
|
||||
if (key < 0 || key >= SDL_SCANCODE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
if (static_cast<int>(keystates[key]) != 0) {
|
||||
return true;
|
||||
}
|
||||
for (auto& virtual_keystate : virtual_keystates) {
|
||||
if (virtual_keystate[key] != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool JI_CheatActivated(const char* cheat_code) {
|
||||
auto JI_CheatActivated(const char* cheat_code) -> bool {
|
||||
const size_t len = std::strlen(cheat_code);
|
||||
if (len > sizeof(cheat)) return false;
|
||||
if (len > sizeof(cheat)) {
|
||||
return false;
|
||||
}
|
||||
// Compara contra els últims `len` caràcters del buffer. El buffer té
|
||||
// mida fixa 5 i acumula sempre el darrer tecle a la posició 4.
|
||||
const size_t offset = sizeof(cheat) - len;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (cheat[offset + i] != static_cast<Uint8>(cheat_code[i])) return false;
|
||||
if (cheat[offset + i] != static_cast<Uint8>(cheat_code[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JI_AnyKey() {
|
||||
return wait_ms > 0.0f ? false : key_pressed;
|
||||
auto JI_AnyKey() -> bool {
|
||||
return wait_ms > 0.0F ? false : key_pressed;
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ void JI_SetInputBlocked(bool blocked);
|
||||
enum JI_VirtualSource {
|
||||
JI_VSRC_GAMEPAD = 0,
|
||||
JI_VSRC_REMAP = 1,
|
||||
JI_VSRC_COUNT
|
||||
JI_VSRC_COUNT = 2
|
||||
};
|
||||
void JI_SetVirtualKey(int scancode, int source, bool pressed);
|
||||
|
||||
void JI_Update();
|
||||
|
||||
bool JI_KeyPressed(int key);
|
||||
auto JI_KeyPressed(int key) -> bool;
|
||||
|
||||
bool JI_CheatActivated(const char* cheat_code);
|
||||
auto JI_CheatActivated(const char* cheat_code) -> bool;
|
||||
|
||||
bool JI_AnyKey();
|
||||
auto JI_AnyKey() -> bool;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Locale {
|
||||
static void traverse(const fkyaml::node& node, const std::string& prefix) {
|
||||
if (node.is_mapping()) {
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
std::string key = it.key().get_value<std::string>();
|
||||
auto key = it.key().get_value<std::string>();
|
||||
std::string full = prefix.empty() ? key : prefix + "." + key;
|
||||
traverse(it.value(), full);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ namespace Locale {
|
||||
}
|
||||
}
|
||||
|
||||
bool load(const char* filename) {
|
||||
auto load(const char* filename) -> bool {
|
||||
auto buffer = ResourceHelper::loadFile(filename);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Locale: unable to load " << filename << '\n';
|
||||
@@ -48,7 +48,9 @@ namespace Locale {
|
||||
|
||||
auto get(const char* key) -> const char* {
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) return it->second.c_str();
|
||||
if (it != strings_.end()) {
|
||||
return it->second.c_str();
|
||||
}
|
||||
return key; // fallback: retorna la clau mateixa
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Les claus són nested amb notació punt ("menu.items.zoom").
|
||||
// Si una clau no existeix, Locale::get torna la clau mateixa (útil per debug).
|
||||
namespace Locale {
|
||||
bool load(const char* filename);
|
||||
auto load(const char* filename) -> bool;
|
||||
|
||||
// Retorna la cadena associada a la clau. El punter és estable durant tota la
|
||||
// sessió (no canvia), per tant es pot guardar en const char*.
|
||||
|
||||
+114
-72
@@ -1,5 +1,6 @@
|
||||
#include "core/rendering/menu.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
@@ -72,16 +73,20 @@ namespace Menu {
|
||||
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
|
||||
};
|
||||
|
||||
static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
|
||||
static auto isVisible(const Item& it) -> bool { return !it.visible || it.visible(); }
|
||||
|
||||
// Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
|
||||
// Si cap és visible retorna `from`.
|
||||
static int nextVisibleCursor(const Page& p, int from, int dir) {
|
||||
static auto nextVisibleCursor(const Page& p, int from, int dir) -> int {
|
||||
const int n = static_cast<int>(p.items.size());
|
||||
if (n <= 0) return from;
|
||||
if (n <= 0) {
|
||||
return from;
|
||||
}
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
int idx = ((from + dir * i) % n + n) % n;
|
||||
if (isVisible(p.items[idx])) return idx;
|
||||
if (isVisible(p.items[idx])) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return from;
|
||||
}
|
||||
@@ -97,7 +102,7 @@ namespace Menu {
|
||||
|
||||
// --- Transició entre pàgines ---
|
||||
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
|
||||
static Page transition_outgoing_{"", {}, 0};
|
||||
static Page transition_outgoing_{.title = "", .items = {}, .cursor = 0};
|
||||
static bool transition_active_{false};
|
||||
static float transition_progress_{1.0F};
|
||||
static int transition_dir_{+1}; // +1 endavant, -1 enrere
|
||||
@@ -120,19 +125,19 @@ namespace Menu {
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
static std::string yesNo(bool b) { return b ? Locale::get("menu.values.yes") : Locale::get("menu.values.no"); }
|
||||
static std::string onOff(bool b) { return b ? Locale::get("menu.values.on") : Locale::get("menu.values.off"); }
|
||||
static auto yesNo(bool b) -> std::string { return b ? Locale::get("menu.values.yes") : Locale::get("menu.values.no"); }
|
||||
static auto onOff(bool b) -> std::string { return b ? Locale::get("menu.values.on") : Locale::get("menu.values.off"); }
|
||||
|
||||
// --- Builders de pàgines ---
|
||||
|
||||
static Page buildVideo();
|
||||
static Page buildAudio();
|
||||
static Page buildControls();
|
||||
static Page buildGame();
|
||||
static Page buildSystem();
|
||||
static auto buildVideo() -> Page;
|
||||
static auto buildAudio() -> Page;
|
||||
static auto buildControls() -> Page;
|
||||
static auto buildGame() -> Page;
|
||||
static auto buildSystem() -> Page;
|
||||
|
||||
static Page buildRoot() {
|
||||
Page p{Locale::get("menu.titles.root"), {}, 0};
|
||||
static auto buildRoot() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.root"), .items = {}, .cursor = 0};
|
||||
p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
|
||||
@@ -141,8 +146,8 @@ namespace Menu {
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildVideo() {
|
||||
Page p{Locale::get("menu.titles.video"), {}, 0};
|
||||
static auto buildVideo() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.video"), .items = {}, .cursor = 0};
|
||||
|
||||
// Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
|
||||
#ifndef __EMSCRIPTEN__
|
||||
@@ -150,8 +155,9 @@ namespace Menu {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
||||
return std::string(buf); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->decZoom();
|
||||
else if (dir > 0) Screen::get()->incZoom(); }, nullptr, nullptr});
|
||||
if (dir < 0) { Screen::get()->decZoom();
|
||||
} else if (dir > 0) { Screen::get()->incZoom();
|
||||
} }, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
|
||||
#endif
|
||||
@@ -185,15 +191,18 @@ namespace Menu {
|
||||
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->prevShaderType();
|
||||
else Screen::get()->nextShaderType(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
if (dir < 0) { Screen::get()->prevShaderType();
|
||||
} else { Screen::get()->nextShaderType();
|
||||
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->prevPreset();
|
||||
else Screen::get()->nextPreset(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
if (dir < 0) { Screen::get()->prevPreset();
|
||||
} else { Screen::get()->nextPreset();
|
||||
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!Options::video.shader_enabled) { return false;
|
||||
}
|
||||
const char* name = Screen::get()->getActiveShaderName();
|
||||
return name && std::string(name) == "POSTFX"; }});
|
||||
#endif
|
||||
@@ -213,10 +222,10 @@ namespace Menu {
|
||||
}
|
||||
|
||||
// Converteix volum 0..1 a percentatge i ho formata com "50%"
|
||||
static std::string volPct(float v) {
|
||||
int pct = static_cast<int>(v * 100.0F + 0.5F);
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
static auto volPct(float v) -> std::string {
|
||||
int pct = static_cast<int>((v * 100.0F) + 0.5F);
|
||||
pct = std::max(pct, 0);
|
||||
pct = std::min(pct, 100);
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "%d%%", pct);
|
||||
return std::string(buf);
|
||||
@@ -225,13 +234,13 @@ namespace Menu {
|
||||
// Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping
|
||||
static void stepVolume(float& v, int dir) {
|
||||
v += (dir >= 0 ? 0.05F : -0.05F);
|
||||
if (v < 0.0F) v = 0.0F;
|
||||
if (v > 1.0F) v = 1.0F;
|
||||
v = std::max(v, 0.0F);
|
||||
v = std::min(v, 1.0F);
|
||||
Options::applyAudio();
|
||||
}
|
||||
|
||||
static Page buildControls() {
|
||||
Page p{Locale::get("menu.titles.controls"), {}, 0};
|
||||
static auto buildControls() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.controls"), .items = {}, .cursor = 0};
|
||||
p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.up});
|
||||
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down});
|
||||
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left});
|
||||
@@ -240,8 +249,8 @@ namespace Menu {
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildAudio() {
|
||||
Page p{Locale::get("menu.titles.audio"), {}, 0};
|
||||
static auto buildAudio() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.audio"), .items = {}, .cursor = 0};
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) {
|
||||
Options::audio.enabled = !Options::audio.enabled;
|
||||
@@ -264,8 +273,8 @@ namespace Menu {
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildGame() {
|
||||
Page p{Locale::get("menu.titles.game"), {}, 0};
|
||||
static auto buildGame() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.game"), .items = {}, .cursor = 0};
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::Toggle, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});
|
||||
|
||||
@@ -276,19 +285,23 @@ namespace Menu {
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildSystem() {
|
||||
Page p{Locale::get("menu.titles.system"), {}, 0};
|
||||
static auto buildSystem() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.system"), .items = {}, .cursor = 0};
|
||||
p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] {
|
||||
if (Director::get()) Director::get()->requestRestart();
|
||||
if (Director::get()) {
|
||||
Director::get()->requestRestart();
|
||||
}
|
||||
},
|
||||
nullptr,
|
||||
nullptr});
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] {
|
||||
if (Director::get()) Director::get()->requestQuit();
|
||||
if (Director::get()) {
|
||||
Director::get()->requestQuit();
|
||||
}
|
||||
},
|
||||
nullptr,
|
||||
nullptr});
|
||||
@@ -307,10 +320,14 @@ namespace Menu {
|
||||
const Uint8 sb = (src_argb >> 16) & 0xFF;
|
||||
const Uint8 inv = 255 - sa;
|
||||
for (int row = y; row < y + h; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
if (row < 0 || row >= SCREEN_H) {
|
||||
continue;
|
||||
}
|
||||
for (int col = x; col < x + w; col++) {
|
||||
if (col < 0 || col >= SCREEN_W) continue;
|
||||
Uint32* p = &buf[col + row * SCREEN_W];
|
||||
if (col < 0 || col >= SCREEN_W) {
|
||||
continue;
|
||||
}
|
||||
Uint32* p = &buf[col + (row * SCREEN_W)];
|
||||
Uint32 dst = *p;
|
||||
Uint8 dr = dst & 0xFF;
|
||||
Uint8 dg = (dst >> 8) & 0xFF;
|
||||
@@ -318,17 +335,21 @@ namespace Menu {
|
||||
Uint8 r = (sr * sa + dr * inv) / 255;
|
||||
Uint8 g = (sg * sa + dg * inv) / 255;
|
||||
Uint8 b = (sb * sa + db * inv) / 255;
|
||||
*p = 0xFF000000u | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
|
||||
*p = 0xFF000000U | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fillRect(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
|
||||
for (int row = y; row < y + h; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
if (row < 0 || row >= SCREEN_H) {
|
||||
continue;
|
||||
}
|
||||
for (int col = x; col < x + w; col++) {
|
||||
if (col < 0 || col >= SCREEN_W) continue;
|
||||
buf[col + row * SCREEN_W] = color;
|
||||
if (col < 0 || col >= SCREEN_W) {
|
||||
continue;
|
||||
}
|
||||
buf[col + (row * SCREEN_W)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,12 +364,14 @@ namespace Menu {
|
||||
// Mida final de la caixa segons el nombre d'items *visibles*.
|
||||
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
|
||||
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
|
||||
static int boxHeight(const Page& page) {
|
||||
static auto boxHeight(const Page& page) -> int {
|
||||
int n = 0;
|
||||
for (const auto& it : page.items) {
|
||||
if (isVisible(it)) ++n;
|
||||
if (isVisible(it)) {
|
||||
++n;
|
||||
}
|
||||
}
|
||||
int body = (n == 0) ? 8 : (n - 1) * ITEM_SPACING + 8;
|
||||
int body = (n == 0) ? 8 : ((n - 1) * ITEM_SPACING) + 8;
|
||||
int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
|
||||
return header + body + BOTTOM_PAD;
|
||||
}
|
||||
@@ -403,7 +426,9 @@ namespace Menu {
|
||||
// render() faça decréixer open_anim_ fins a 0. En aquell moment es neteja
|
||||
// l'estat. Si es crida estant ja tancat o tancant-se, no-op.
|
||||
void close() {
|
||||
if (stack_.empty() || closing_) return;
|
||||
if (stack_.empty() || closing_) {
|
||||
return;
|
||||
}
|
||||
closing_ = true;
|
||||
capturing_ = nullptr;
|
||||
transition_active_ = false;
|
||||
@@ -416,7 +441,9 @@ namespace Menu {
|
||||
}
|
||||
|
||||
void captureKey(SDL_Scancode sc) {
|
||||
if (!capturing_) return;
|
||||
if (capturing_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||
// Cancel·la
|
||||
capturing_ = nullptr;
|
||||
@@ -427,15 +454,18 @@ namespace Menu {
|
||||
}
|
||||
|
||||
void handleKey(SDL_Scancode sc) {
|
||||
if (!isOpen()) return;
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
Page& page = stack_.back();
|
||||
if (page.items.empty()) {
|
||||
// Pàgina buida — només backspace surt
|
||||
if (sc == SDL_SCANCODE_BACKSPACE) {
|
||||
if (stack_.size() > 1)
|
||||
if (stack_.size() > 1) {
|
||||
popPage();
|
||||
else
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -467,7 +497,9 @@ namespace Menu {
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
if (page.items[page.cursor].kind == ItemKind::Submenu ||
|
||||
page.items[page.cursor].kind == ItemKind::Action) {
|
||||
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
|
||||
if (page.items[page.cursor].enter) {
|
||||
page.items[page.cursor].enter();
|
||||
}
|
||||
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
|
||||
capturing_ = page.items[page.cursor].scancode;
|
||||
} else if (page.items[page.cursor].change) {
|
||||
@@ -475,10 +507,11 @@ namespace Menu {
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
if (stack_.size() > 1)
|
||||
if (stack_.size() > 1) {
|
||||
popPage();
|
||||
else
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -500,7 +533,7 @@ namespace Menu {
|
||||
static void renderPageContent(Uint32* pixel_data, const Page& page, int box_x, int box_y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
// Títol
|
||||
int title_w = font_->width(page.title);
|
||||
int title_x = box_x + (BOX_W - title_w) / 2 + x_offset;
|
||||
int title_x = box_x + ((BOX_W - title_w) / 2) + x_offset;
|
||||
font_->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
|
||||
// Línia sota el títol (també lliscada) — clippada manualment
|
||||
@@ -519,26 +552,31 @@ namespace Menu {
|
||||
int items_y = title_line_y + 4;
|
||||
if (!page.subtitle.empty()) {
|
||||
int sub_w = font_->width(page.subtitle.c_str());
|
||||
int sub_x = box_x + (BOX_W - sub_w) / 2 + x_offset;
|
||||
int sub_x = box_x + ((BOX_W - sub_w) / 2) + x_offset;
|
||||
font_->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
items_y += SUBTITLE_H;
|
||||
}
|
||||
// Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
|
||||
int visible_count = 0;
|
||||
for (const auto& it : page.items)
|
||||
if (isVisible(it)) ++visible_count;
|
||||
for (const auto& it : page.items) {
|
||||
if (isVisible(it)) {
|
||||
++visible_count;
|
||||
}
|
||||
}
|
||||
if (visible_count == 0) {
|
||||
const char* empty_text = Locale::get("menu.values.empty");
|
||||
int ew = font_->width(empty_text);
|
||||
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
font_->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
|
||||
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
|
||||
for (size_t i = 0; i < page.items.size(); i++) {
|
||||
const Item& item = page.items[i];
|
||||
if (!isVisible(item)) continue;
|
||||
int y = items_y + y_slot * ITEM_SPACING;
|
||||
if (!isVisible(item)) {
|
||||
continue;
|
||||
}
|
||||
int y = items_y + (y_slot * ITEM_SPACING);
|
||||
++y_slot;
|
||||
bool selected = (static_cast<int>(i) == page.cursor);
|
||||
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
@@ -546,7 +584,7 @@ namespace Menu {
|
||||
// Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
|
||||
if (item.kind == ItemKind::Action) {
|
||||
int lw = font_->width(item.label);
|
||||
int lx = box_x + (BOX_W - lw) / 2 + x_offset;
|
||||
int lx = box_x + ((BOX_W - lw) / 2) + x_offset;
|
||||
if (selected) {
|
||||
font_->drawClipped(pixel_data, lx - font_->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
@@ -567,8 +605,10 @@ namespace Menu {
|
||||
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
} else if (item.kind == ItemKind::KeyBind) {
|
||||
bool this_capturing = (capturing_ == item.scancode);
|
||||
const char* text = this_capturing ? Locale::get("menu.values.press_key") : (item.scancode ? SDL_GetScancodeName(*item.scancode) : "");
|
||||
if (!text || !*text) text = Locale::get("menu.values.unknown");
|
||||
const char* text = this_capturing ? Locale::get("menu.values.press_key") : ((item.scancode != nullptr) ? SDL_GetScancodeName(*item.scancode) : "");
|
||||
if ((text == nullptr) || (*text == 0)) {
|
||||
text = Locale::get("menu.values.unknown");
|
||||
}
|
||||
int tw = font_->width(text);
|
||||
Uint32 tc = this_capturing ? 0xFF00FFFF : (selected ? CURSOR_COLOR : VALUE_COLOR);
|
||||
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
@@ -582,7 +622,9 @@ namespace Menu {
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!isVisible() || !font_ || !pixel_data) return;
|
||||
if (!isVisible() || !font_ || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delta time
|
||||
Uint32 now = SDL_GetTicks();
|
||||
@@ -600,7 +642,7 @@ namespace Menu {
|
||||
}
|
||||
} else if (open_anim_ < 1.0F) {
|
||||
open_anim_ += OPEN_SPEED * dt;
|
||||
if (open_anim_ > 1.0F) open_anim_ = 1.0F;
|
||||
open_anim_ = std::min(open_anim_, 1.0F);
|
||||
}
|
||||
|
||||
// Avança transició
|
||||
@@ -626,7 +668,7 @@ namespace Menu {
|
||||
animated_h_ = static_cast<float>(current_h);
|
||||
} else {
|
||||
float t = HEIGHT_RATE * dt;
|
||||
if (t > 1.0F) t = 1.0F;
|
||||
t = std::min(t, 1.0F);
|
||||
animated_h_ += diff * t;
|
||||
}
|
||||
}
|
||||
@@ -643,12 +685,12 @@ namespace Menu {
|
||||
|
||||
// Caixa creix verticalment durant l'obertura
|
||||
int box_h = static_cast<int>(target_h * eased);
|
||||
if (box_h < 2) box_h = 2;
|
||||
box_h = std::max(box_h, 2);
|
||||
int box_x = (SCREEN_W - BOX_W) / 2;
|
||||
int box_y = (SCREEN_H - box_h) / 2;
|
||||
|
||||
// Fons semi-transparent (alpha escalat per l'animació d'obertura)
|
||||
Uint8 alpha = static_cast<Uint8>(BG_ALPHA * eased);
|
||||
auto alpha = static_cast<Uint8>(BG_ALPHA * eased);
|
||||
blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha);
|
||||
|
||||
// El contingut només apareix quan la caixa és prou gran
|
||||
|
||||
@@ -102,16 +102,22 @@ namespace Overlay {
|
||||
// Pinta un rectangle sòlid dins els límits de la pantalla
|
||||
static void drawRect(Uint32* pixel_data, int rx, int ry, int rw, int rh, Uint32 color) {
|
||||
for (int row = ry; row < ry + rh; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
if (row < 0 || row >= SCREEN_H) {
|
||||
continue;
|
||||
}
|
||||
for (int col = rx; col < rx + rw; col++) {
|
||||
if (col < 0 || col >= SCREEN_W) continue;
|
||||
pixel_data[col + row * SCREEN_W] = color;
|
||||
if (col < 0 || col >= SCREEN_W) {
|
||||
continue;
|
||||
}
|
||||
pixel_data[col + (row * SCREEN_W)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!font_ || !pixel_data) return;
|
||||
if (!font_ || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcula delta time
|
||||
Uint32 now = SDL_GetTicks();
|
||||
@@ -148,7 +154,9 @@ namespace Overlay {
|
||||
break;
|
||||
}
|
||||
|
||||
if (notif.status == Status::FINISHED) continue;
|
||||
if (notif.status == Status::FINISHED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Posició segons el tipus
|
||||
int box_x = 0;
|
||||
@@ -180,7 +188,7 @@ namespace Overlay {
|
||||
int line_y = box_y + NOTIF_PADDING_V;
|
||||
for (const auto& line : notif.lines) {
|
||||
int line_w = font_->width(line.c_str());
|
||||
int line_x = box_x + (notif.box_w - line_w) / 2; // centrat dins la caixa
|
||||
int line_x = box_x + ((notif.box_w - line_w) / 2); // centrat dins la caixa
|
||||
if (notif.style == NotifStyle::SHADOW) {
|
||||
font_->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
|
||||
} else if (notif.style == NotifStyle::OUTLINE) {
|
||||
@@ -203,7 +211,7 @@ namespace Overlay {
|
||||
// Mateix lloc: entra fins a 1
|
||||
if (info_anim_ < 1.0F) {
|
||||
info_anim_ += INFO_SLIDE_SPEED * dt;
|
||||
if (info_anim_ > 1.0F) info_anim_ = 1.0F;
|
||||
info_anim_ = std::min(info_anim_, 1.0F);
|
||||
}
|
||||
} else {
|
||||
// Canvi: si visible_pos està OFF, commuta directament
|
||||
@@ -225,10 +233,10 @@ namespace Overlay {
|
||||
float target = seg.visible ? 1.0F : 0.0F;
|
||||
if (seg.anim < target) {
|
||||
seg.anim += SEG_SPEED * dt;
|
||||
if (seg.anim > target) seg.anim = target;
|
||||
seg.anim = std::min(seg.anim, target);
|
||||
} else if (seg.anim > target) {
|
||||
seg.anim -= SEG_SPEED * dt;
|
||||
if (seg.anim < target) seg.anim = target;
|
||||
seg.anim = std::max(seg.anim, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,9 +291,7 @@ namespace Overlay {
|
||||
}
|
||||
|
||||
// Elimina les acabades
|
||||
notifications_.erase(
|
||||
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
||||
notifications_.end());
|
||||
std::erase_if(notifications_, [](const Notification& n) { return n.status == Status::FINISHED; });
|
||||
|
||||
// Si la notificació d'ESC ha desaparegut, reseteja l'estat
|
||||
if (esc_waiting_ && notifications_.empty()) {
|
||||
@@ -293,7 +299,7 @@ namespace Overlay {
|
||||
}
|
||||
|
||||
// Indicador de pausa persistent (cantó superior dret)
|
||||
if (Director::get() && Director::get()->isPaused()) {
|
||||
if ((Director::get() != nullptr) && Director::get()->isPaused()) {
|
||||
const char* pause_text = Locale::get("notifications.pause");
|
||||
int w = font_->width(pause_text);
|
||||
int x = SCREEN_W - w - 4;
|
||||
@@ -357,9 +363,7 @@ namespace Overlay {
|
||||
}
|
||||
|
||||
// Neteja notificacions finalitzades
|
||||
notifications_.erase(
|
||||
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
||||
notifications_.end());
|
||||
std::erase_if(notifications_, [](const Notification& n) { return n.status == Status::FINISHED; });
|
||||
|
||||
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
|
||||
if (Menu::isVisible()) {
|
||||
@@ -392,7 +396,7 @@ namespace Overlay {
|
||||
int max_w = 0;
|
||||
for (const auto& line : lines) {
|
||||
int w = font_->width(line.c_str());
|
||||
if (w > max_w) max_w = w;
|
||||
max_w = std::max(w, max_w);
|
||||
}
|
||||
notif.box_w = max_w + NOTIF_PADDING_H * 2;
|
||||
int line_h = font_->charHeight();
|
||||
@@ -414,7 +418,7 @@ namespace Overlay {
|
||||
void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask) {
|
||||
const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3};
|
||||
for (int i = 0; i < INFO_SEGMENT_COUNT; i++) {
|
||||
info_segments_[i].mono_digits = (mono_mask >> i) & 1u;
|
||||
info_segments_[i].mono_digits = (((mono_mask >> i) & 1U) != 0u);
|
||||
if (segs[i] != nullptr && *segs[i] != '\0') {
|
||||
info_segments_[i].text = segs[i];
|
||||
info_segments_[i].visible = true;
|
||||
@@ -425,7 +429,9 @@ namespace Overlay {
|
||||
}
|
||||
|
||||
void startCredits() {
|
||||
if (credits_phase_ != CreditsPhase::IDLE) return;
|
||||
if (credits_phase_ != CreditsPhase::IDLE) {
|
||||
return;
|
||||
}
|
||||
credits_phase_ = CreditsPhase::DELAY;
|
||||
credits_timer_ = 0.0F;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
@@ -76,15 +77,15 @@ Screen::Screen() {
|
||||
|
||||
calculateMaxZoom();
|
||||
|
||||
if (zoom_ < 1) zoom_ = 1;
|
||||
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
|
||||
zoom_ = std::max(zoom_, 1);
|
||||
zoom_ = std::min(zoom_, max_zoom_);
|
||||
|
||||
// Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i
|
||||
// ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada
|
||||
// canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi
|
||||
// de monitor), baixem al màxim suportat.
|
||||
if (Options::video.internal_resolution < 1) Options::video.internal_resolution = 1;
|
||||
if (Options::video.internal_resolution > max_zoom_) Options::video.internal_resolution = max_zoom_;
|
||||
Options::video.internal_resolution = std::max(Options::video.internal_resolution, 1);
|
||||
Options::video.internal_resolution = std::min(Options::video.internal_resolution, max_zoom_);
|
||||
|
||||
int w = GAME_WIDTH * zoom_;
|
||||
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
|
||||
@@ -124,15 +125,25 @@ Screen::~Screen() {
|
||||
if (shader_backend_) {
|
||||
#ifndef NO_SHADERS
|
||||
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
|
||||
if (gpu) gpu->destroy();
|
||||
if (gpu != nullptr) {
|
||||
gpu->destroy();
|
||||
}
|
||||
#endif
|
||||
shader_backend_.reset();
|
||||
}
|
||||
|
||||
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
|
||||
if (texture_) SDL_DestroyTexture(texture_);
|
||||
if (renderer_) SDL_DestroyRenderer(renderer_);
|
||||
if (window_) SDL_DestroyWindow(window_);
|
||||
if (internal_texture_sdl_ != nullptr) {
|
||||
SDL_DestroyTexture(internal_texture_sdl_);
|
||||
}
|
||||
if (texture_ != nullptr) {
|
||||
SDL_DestroyTexture(texture_);
|
||||
}
|
||||
if (renderer_ != nullptr) {
|
||||
SDL_DestroyRenderer(renderer_);
|
||||
}
|
||||
if (window_ != nullptr) {
|
||||
SDL_DestroyWindow(window_);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::initShaders() {
|
||||
@@ -143,7 +154,9 @@ void Screen::initShaders() {
|
||||
// curtcircuiten cap al fallback SDL_Renderer.
|
||||
return;
|
||||
#else
|
||||
if (!Options::video.gpu_acceleration) return;
|
||||
if (!Options::video.gpu_acceleration) {
|
||||
return;
|
||||
}
|
||||
|
||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
|
||||
@@ -282,19 +295,25 @@ void Screen::toggleFullscreen() {
|
||||
}
|
||||
|
||||
void Screen::incZoom() {
|
||||
if (fullscreen_ || zoom_ >= max_zoom_) return;
|
||||
if (fullscreen_ || zoom_ >= max_zoom_) {
|
||||
return;
|
||||
}
|
||||
zoom_++;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::decZoom() {
|
||||
if (fullscreen_ || zoom_ <= 1) return;
|
||||
if (fullscreen_ || zoom_ <= 1) {
|
||||
return;
|
||||
}
|
||||
zoom_--;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::setZoom(int zoom) {
|
||||
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) return;
|
||||
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) {
|
||||
return;
|
||||
}
|
||||
zoom_ = zoom;
|
||||
adjustWindowSize();
|
||||
}
|
||||
@@ -310,9 +329,15 @@ auto Screen::toggleSupersampling() -> bool {
|
||||
// SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
|
||||
// i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
|
||||
// submostreig intern i no usa aquesta via).
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) {
|
||||
return false;
|
||||
}
|
||||
Options::video.supersampling = !Options::video.supersampling;
|
||||
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
||||
return true;
|
||||
@@ -366,9 +391,11 @@ void Screen::cycleTextureFilter(int dir) {
|
||||
|
||||
void Screen::changeInternalResolution(int dir) {
|
||||
int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
|
||||
if (next < 1) next = 1;
|
||||
if (next > max_zoom_) next = max_zoom_;
|
||||
if (next == Options::video.internal_resolution) return;
|
||||
next = std::max(next, 1);
|
||||
next = std::min(next, max_zoom_);
|
||||
if (next == Options::video.internal_resolution) {
|
||||
return;
|
||||
}
|
||||
Options::video.internal_resolution = next;
|
||||
|
||||
// Propaga al backend actiu. Al fallback path, la textura es recrea al
|
||||
@@ -381,8 +408,12 @@ void Screen::changeInternalResolution(int dir) {
|
||||
}
|
||||
|
||||
auto Screen::nextShaderType() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
@@ -397,16 +428,24 @@ auto Screen::nextShaderType() -> bool {
|
||||
}
|
||||
|
||||
auto Screen::nextPreset() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::postfx_presets.empty()) return false;
|
||||
if (Options::postfx_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
if (Options::crtpi_presets.empty()) return false;
|
||||
if (Options::crtpi_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||
applyCurrentCrtPiPreset();
|
||||
@@ -420,17 +459,25 @@ auto Screen::prevShaderType() -> bool {
|
||||
}
|
||||
|
||||
auto Screen::prevPreset() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::postfx_presets.empty()) return false;
|
||||
if (Options::postfx_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
int n = static_cast<int>(Options::postfx_presets.size());
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
|
||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
if (Options::crtpi_presets.empty()) return false;
|
||||
if (Options::crtpi_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
int n = static_cast<int>(Options::crtpi_presets.size());
|
||||
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
|
||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||
@@ -440,13 +487,17 @@ auto Screen::prevPreset() -> bool {
|
||||
}
|
||||
|
||||
auto Screen::getCurrentPresetName() const -> const char* {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "---";
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return "---";
|
||||
}
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size()))
|
||||
if (Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size())) {
|
||||
return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
|
||||
}
|
||||
} else {
|
||||
if (Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size()))
|
||||
if (Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size())) {
|
||||
return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
|
||||
}
|
||||
}
|
||||
return "---";
|
||||
}
|
||||
@@ -458,7 +509,9 @@ void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||
}
|
||||
|
||||
void Screen::applyCurrentPostFXPreset() {
|
||||
if (!shader_backend_ || Options::postfx_presets.empty()) return;
|
||||
if (!shader_backend_ || Options::postfx_presets.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& preset = Options::postfx_presets[Options::current_postfx_preset];
|
||||
Rendering::PostFXParams p;
|
||||
p.vignette = preset.vignette;
|
||||
@@ -473,7 +526,9 @@ void Screen::applyCurrentPostFXPreset() {
|
||||
}
|
||||
|
||||
void Screen::applyCurrentCrtPiPreset() {
|
||||
if (!shader_backend_ || Options::crtpi_presets.empty()) return;
|
||||
if (!shader_backend_ || Options::crtpi_presets.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& preset = Options::crtpi_presets[Options::current_crtpi_preset];
|
||||
Rendering::CrtPiParams p;
|
||||
p.scanline_weight = preset.scanline_weight;
|
||||
@@ -498,7 +553,9 @@ auto Screen::isHardwareAccelerated() const -> bool {
|
||||
}
|
||||
|
||||
auto Screen::getActiveShaderName() const -> const char* {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "SENSE GPU";
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return "SENSE GPU";
|
||||
}
|
||||
return shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX ? "POSTFX" : "CRT-PI";
|
||||
}
|
||||
|
||||
@@ -534,7 +591,7 @@ void Screen::updateRenderInfo() {
|
||||
fps_driver.c_str(),
|
||||
shader_seg.empty() ? nullptr : shader_seg.c_str(),
|
||||
ss_seg,
|
||||
time_buf[0] ? time_buf : nullptr,
|
||||
(time_buf[0] != 0) ? time_buf : nullptr,
|
||||
0b1001);
|
||||
}
|
||||
|
||||
@@ -544,7 +601,9 @@ void Screen::applyFallbackPresentation() {
|
||||
SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||
? SDL_SCALEMODE_LINEAR
|
||||
: SDL_SCALEMODE_NEAREST;
|
||||
if (texture_) SDL_SetTextureScaleMode(texture_, scale);
|
||||
if (texture_ != nullptr) {
|
||||
SDL_SetTextureScaleMode(texture_, scale);
|
||||
}
|
||||
|
||||
// Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és
|
||||
// l'única opció viable al path fallback (el GPU path fa l'upscale 4:3 abans
|
||||
@@ -578,7 +637,9 @@ void Screen::applyFallbackPresentation() {
|
||||
}
|
||||
|
||||
void Screen::ensureFallbackInternalTexture() {
|
||||
if (renderer_ == nullptr) return;
|
||||
if (renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int mult = Options::video.internal_resolution;
|
||||
if (mult <= 1) {
|
||||
// No cal textura intermèdia — recicla si la teníem.
|
||||
@@ -589,7 +650,9 @@ void Screen::ensureFallbackInternalTexture() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) return;
|
||||
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (internal_texture_sdl_ != nullptr) {
|
||||
SDL_DestroyTexture(internal_texture_sdl_);
|
||||
@@ -620,11 +683,11 @@ void Screen::adjustWindowSize() {
|
||||
void Screen::calculateMaxZoom() {
|
||||
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
|
||||
if (mode) {
|
||||
if (mode != nullptr) {
|
||||
int max_w = mode->w / GAME_WIDTH;
|
||||
int max_h = mode->h / GAME_HEIGHT;
|
||||
max_zoom_ = (max_w < max_h) ? max_w : max_h;
|
||||
if (max_zoom_ < 1) max_zoom_ = 1;
|
||||
max_zoom_ = std::max(max_zoom_, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -906,8 +906,8 @@ namespace Rendering {
|
||||
|
||||
// ---- Calcular viewport (dimensions lògiques del canvas) ----
|
||||
// Si 4:3 actiu, effective_height ja és 240 (la textura estirada)
|
||||
const float logical_w = static_cast<float>(game_width_);
|
||||
const float logical_h = static_cast<float>(effective_height);
|
||||
const auto logical_w = static_cast<float>(game_width_);
|
||||
const auto logical_h = static_cast<float>(effective_height);
|
||||
|
||||
float vx = 0.0F;
|
||||
float vy = 0.0F;
|
||||
@@ -1276,7 +1276,9 @@ namespace Rendering {
|
||||
// Recrea la textura intermèdia amb les noves dimensions (320·N × 200·N).
|
||||
void SDL3GPUShader::setInternalResolution(int multiplier) {
|
||||
const int NEW = std::max(1, multiplier);
|
||||
if (NEW == internal_res_) return;
|
||||
if (NEW == internal_res_) {
|
||||
return;
|
||||
}
|
||||
internal_res_ = NEW;
|
||||
if (is_initialized_ && device_ != nullptr) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
@@ -1286,7 +1288,9 @@ namespace Rendering {
|
||||
|
||||
void SDL3GPUShader::setStretch4_3(bool enabled) {
|
||||
stretch_4_3_ = enabled;
|
||||
if (!is_initialized_ || device_ == nullptr) return;
|
||||
if (!is_initialized_ || device_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recrear scaled_texture_ perquè tinga les dimensions correctes (amb o sense 4:3)
|
||||
if (oversample_ > 1 && ss_factor_ > 0) {
|
||||
@@ -1464,7 +1468,9 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||
internal_texture_ = nullptr;
|
||||
}
|
||||
if (internal_res_ <= 1 || device_ == nullptr) return true;
|
||||
if (internal_res_ <= 1 || device_ == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const int W = game_width_ * internal_res_;
|
||||
const int H = game_height_ * internal_res_;
|
||||
|
||||
+127
-57
@@ -12,7 +12,7 @@
|
||||
|
||||
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
|
||||
struct rgb;
|
||||
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
||||
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
|
||||
|
||||
Text::Text(const char* fnt_file, const char* gif_file) {
|
||||
loadBitmap(gif_file);
|
||||
@@ -23,7 +23,9 @@ Text::Text(const char* fnt_file, const char* gif_file) {
|
||||
|
||||
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
auto byte = static_cast<uint8_t>(*ptr);
|
||||
if (byte == 0) return 0;
|
||||
if (byte == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t cp = 0;
|
||||
int extra = 0;
|
||||
@@ -47,7 +49,9 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
ptr++;
|
||||
for (int i = 0; i < extra; i++) {
|
||||
auto cont = static_cast<uint8_t>(*ptr);
|
||||
if ((cont & 0xC0) != 0x80) return 0xFFFD;
|
||||
if ((cont & 0xC0) != 0x80) {
|
||||
return 0xFFFD;
|
||||
}
|
||||
cp = (cp << 6) | (cont & 0x3F);
|
||||
ptr++;
|
||||
}
|
||||
@@ -71,7 +75,9 @@ void Text::loadFont(const char* fnt_file) {
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
// Ignora comentaris i línies buides
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Elimina comentaris inline
|
||||
auto comment_pos = line.find('#');
|
||||
@@ -133,9 +139,10 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
int w = raw[6] | (raw[7] << 8);
|
||||
int h = raw[8] | (raw[9] << 8);
|
||||
|
||||
unsigned short gw = 0, gh = 0;
|
||||
unsigned short gw = 0;
|
||||
unsigned short gh = 0;
|
||||
Uint8* pixels = LoadGif(raw, &gw, &gh);
|
||||
if (!pixels) {
|
||||
if (pixels == nullptr) {
|
||||
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
|
||||
return;
|
||||
}
|
||||
@@ -151,14 +158,18 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
// --- Renderitzat ---
|
||||
|
||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
@@ -174,21 +185,27 @@ void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color
|
||||
// Pinta glifo pixel a pixel
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = cursor_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
// Píxel no transparent (índex 0 és fons típicament)
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,17 +221,23 @@ void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 colo
|
||||
}
|
||||
|
||||
void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Descart ràpid si el glifo sencer cau fora verticalment
|
||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
|
||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
@@ -235,21 +258,31 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
if (dst_y < clip_y_min || dst_y >= clip_y_max) continue;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
if (dst_y < clip_y_min || dst_y >= clip_y_max) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = cursor_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
if (dst_x < clip_x_min || dst_x >= clip_x_max) continue;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
if (dst_x < clip_x_min || dst_x >= clip_x_max) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,14 +292,18 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
|
||||
}
|
||||
|
||||
void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
@@ -279,23 +316,29 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
|
||||
|
||||
const auto& glyph = it->second;
|
||||
// Centra el glif dins la cel·la
|
||||
int glyph_x = cursor_x + (cell_w - glyph.w) / 2;
|
||||
int glyph_x = cursor_x + ((cell_w - glyph.w) / 2);
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = glyph_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,21 +348,27 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
|
||||
}
|
||||
|
||||
void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
bool first = true;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
if (!first) cursor_x += 1;
|
||||
if (!first) {
|
||||
cursor_x += 1;
|
||||
}
|
||||
cursor_x += box_width_;
|
||||
first = false;
|
||||
continue;
|
||||
@@ -329,22 +378,30 @@ void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Ui
|
||||
const auto& glyph = it->second;
|
||||
bool is_digit = (cp >= '0' && cp <= '9');
|
||||
|
||||
if (!first) cursor_x += 1; // kerning
|
||||
if (!first) {
|
||||
cursor_x += 1; // kerning
|
||||
}
|
||||
|
||||
int glyph_x = is_digit ? cursor_x + (digit_cell_w - glyph.w) / 2 : cursor_x;
|
||||
int glyph_x = is_digit ? cursor_x + ((digit_cell_w - glyph.w) / 2) : cursor_x;
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = glyph_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,32 +415,41 @@ auto Text::widthMonoDigits(const char* text, int digit_cell_w) const -> int {
|
||||
const char* ptr = text;
|
||||
int w = 0;
|
||||
bool first = true;
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (!first) w += 1; // kerning
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
if (!first) {
|
||||
w += 1; // kerning
|
||||
}
|
||||
first = false;
|
||||
bool is_digit = (cp >= '0' && cp <= '9');
|
||||
if (is_digit) {
|
||||
w += digit_cell_w;
|
||||
} else {
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) it = glyphs_.find('?');
|
||||
if (it != glyphs_.end())
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
}
|
||||
if (it != glyphs_.end()) {
|
||||
w += it->second.w;
|
||||
else
|
||||
} else {
|
||||
w += box_width_;
|
||||
}
|
||||
}
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
auto Text::widthMono(const char* text, int cell_w) const -> int {
|
||||
auto Text::widthMono(const char* text, int cell_w) -> int {
|
||||
const char* ptr = text;
|
||||
int count = 0;
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count * cell_w;
|
||||
@@ -394,16 +460,20 @@ auto Text::width(const char* text) const -> int {
|
||||
int w = 0;
|
||||
bool first = true;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
}
|
||||
|
||||
if (!first) w += 1; // kerning
|
||||
if (!first) {
|
||||
w += 1; // kerning
|
||||
}
|
||||
first = false;
|
||||
|
||||
if (it != glyphs_.end()) {
|
||||
|
||||
@@ -28,7 +28,7 @@ class Text {
|
||||
// Calcula ancho en píxeles d'un text
|
||||
[[nodiscard]] auto width(const char* text) const -> int;
|
||||
// Amplada mono: nombre de codepoints × cell_w
|
||||
[[nodiscard]] auto widthMono(const char* text, int cell_w) const -> int;
|
||||
[[nodiscard]] static auto widthMono(const char* text, int cell_w) -> int;
|
||||
// Amplada mono-dígits: amplada natural, però substituint els dígits per digit_cell_w
|
||||
[[nodiscard]] auto widthMonoDigits(const char* text, int digit_cell_w) const -> int;
|
||||
[[nodiscard]] auto charHeight() const -> int { return box_height_; }
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// ni inline, així que no podem tornar-lo a incloure aquí. Ens fiem de les
|
||||
// declaracions extern dels símbols que ens calen (linkatge C++ normal,
|
||||
// igual que fa text.cpp).
|
||||
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
||||
extern unsigned char* LoadPalette(unsigned char* data);
|
||||
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
|
||||
extern auto LoadPalette(unsigned char* data) -> unsigned char*;
|
||||
|
||||
namespace Resource {
|
||||
|
||||
@@ -90,7 +90,9 @@ namespace Resource {
|
||||
}
|
||||
|
||||
auto Cache::getProgress() const -> float {
|
||||
if (total_count_ == 0) return 1.0F;
|
||||
if (total_count_ == 0) {
|
||||
return 1.0F;
|
||||
}
|
||||
return static_cast<float>(loaded_count_) / static_cast<float>(total_count_);
|
||||
}
|
||||
|
||||
@@ -102,7 +104,9 @@ namespace Resource {
|
||||
}
|
||||
|
||||
auto Cache::loadStep(int budget_ms) -> bool {
|
||||
if (stage_ == LoadStage::DONE) return true;
|
||||
if (stage_ == LoadStage::DONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Uint64 start_ns = SDL_GetTicksNS();
|
||||
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
|
||||
@@ -112,7 +116,9 @@ namespace Resource {
|
||||
switch (stage_) {
|
||||
case LoadStage::MUSICS: {
|
||||
auto items = list->getListByType(List::Type::MUSIC);
|
||||
if (stage_index_ == 0) musics_.clear();
|
||||
if (stage_index_ == 0) {
|
||||
musics_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::SOUNDS;
|
||||
stage_index_ = 0;
|
||||
@@ -123,7 +129,9 @@ namespace Resource {
|
||||
}
|
||||
case LoadStage::SOUNDS: {
|
||||
auto items = list->getListByType(List::Type::SOUND);
|
||||
if (stage_index_ == 0) sounds_.clear();
|
||||
if (stage_index_ == 0) {
|
||||
sounds_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::BITMAPS;
|
||||
stage_index_ = 0;
|
||||
@@ -134,7 +142,9 @@ namespace Resource {
|
||||
}
|
||||
case LoadStage::BITMAPS: {
|
||||
auto items = list->getListByType(List::Type::BITMAP);
|
||||
if (stage_index_ == 0) surfaces_.clear();
|
||||
if (stage_index_ == 0) {
|
||||
surfaces_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::TEXT_FILES;
|
||||
stage_index_ = 0;
|
||||
@@ -148,7 +158,9 @@ namespace Resource {
|
||||
auto font_items = list->getListByType(List::Type::FONT);
|
||||
auto items = data_items;
|
||||
items.insert(items.end(), font_items.begin(), font_items.end());
|
||||
if (stage_index_ == 0) text_files_.clear();
|
||||
if (stage_index_ == 0) {
|
||||
text_files_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::DONE;
|
||||
stage_index_ = 0;
|
||||
@@ -161,7 +173,9 @@ namespace Resource {
|
||||
case LoadStage::DONE:
|
||||
break;
|
||||
}
|
||||
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
|
||||
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return stage_ == LoadStage::DONE;
|
||||
|
||||
@@ -16,13 +16,17 @@ namespace ResourceHelper {
|
||||
auto readFromDisk(const std::string& relative_path) -> std::vector<uint8_t> {
|
||||
const std::string full = std::string(file_getresourcefolder()) + relative_path;
|
||||
std::ifstream file(full, std::ios::binary | std::ios::ate);
|
||||
if (!file) return {};
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::streamsize size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> data(size);
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), size)) return {};
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
||||
return {};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -95,11 +95,21 @@ namespace Resource {
|
||||
}
|
||||
|
||||
auto List::parseAssetType(const std::string& type_str) -> Type {
|
||||
if (type_str == "DATA") return Type::DATA;
|
||||
if (type_str == "BITMAP") return Type::BITMAP;
|
||||
if (type_str == "MUSIC") return Type::MUSIC;
|
||||
if (type_str == "SOUND") return Type::SOUND;
|
||||
if (type_str == "FONT") return Type::FONT;
|
||||
if (type_str == "DATA") {
|
||||
return Type::DATA;
|
||||
}
|
||||
if (type_str == "BITMAP") {
|
||||
return Type::BITMAP;
|
||||
}
|
||||
if (type_str == "MUSIC") {
|
||||
return Type::MUSIC;
|
||||
}
|
||||
if (type_str == "SOUND") {
|
||||
return Type::SOUND;
|
||||
}
|
||||
if (type_str == "FONT") {
|
||||
return Type::FONT;
|
||||
}
|
||||
throw std::runtime_error("Unknown asset type: " + type_str);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32
|
||||
}
|
||||
|
||||
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) return;
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= static_cast<uint8_t>(key[i % key.length()]);
|
||||
}
|
||||
@@ -162,7 +164,9 @@ auto ResourcePack::addDirectory(const std::string& directory) -> bool {
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string filepath = entry.path().string();
|
||||
std::string filename = std::filesystem::relative(entry.path(), directory).string();
|
||||
@@ -177,7 +181,9 @@ auto ResourcePack::addDirectory(const std::string& directory) -> bool {
|
||||
|
||||
auto ResourcePack::getResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
auto it = resources_.find(filename);
|
||||
if (it == resources_.end()) return {};
|
||||
if (it == resources_.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ResourceEntry& entry = it->second;
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
|
||||
@@ -56,7 +56,7 @@ void Director::initGameContext() {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<scenes::Scene> Director::createNextScene() {
|
||||
auto Director::createNextScene() const -> std::unique_ptr<scenes::Scene> {
|
||||
// Mentre el Resource::Cache no haja acabat de precarregar, executem
|
||||
// el BootLoaderScene — pinta una barra de progrés i avança la
|
||||
// càrrega per pressupost de temps. Quan acaba, retorna i tornem ací
|
||||
@@ -139,7 +139,7 @@ void Director::setup() {
|
||||
has_frame_ = false;
|
||||
}
|
||||
|
||||
bool Director::iterate() {
|
||||
auto Director::iterate() -> bool {
|
||||
if (quit_requested_) {
|
||||
JG_QuitSignal();
|
||||
current_scene_.reset(); // destrueix l'escena actual ordenadament
|
||||
@@ -216,9 +216,13 @@ bool Director::iterate() {
|
||||
// game_state_ i info::ctx. Si és impossible (game_state_ == -1,
|
||||
// quit, o state no registrat), eixim del loop.
|
||||
if (!current_scene_) {
|
||||
if (game_state_ == -1 || JG_Quitting()) return false;
|
||||
if (game_state_ == -1 || JG_Quitting()) {
|
||||
return false;
|
||||
}
|
||||
current_scene_ = createNextScene();
|
||||
if (!current_scene_) return false;
|
||||
if (!current_scene_) {
|
||||
return false;
|
||||
}
|
||||
current_scene_->onEnter();
|
||||
last_tick_ms_ = SDL_GetTicks();
|
||||
}
|
||||
@@ -270,7 +274,9 @@ void Director::run() {
|
||||
setup();
|
||||
while (true) {
|
||||
pollAllEvents();
|
||||
if (!iterate()) break;
|
||||
if (!iterate()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
teardown();
|
||||
}
|
||||
@@ -379,16 +385,14 @@ void Director::handleEvent(const SDL_Event& event) {
|
||||
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
||||
// quan l'overlay faça timeout
|
||||
return;
|
||||
} else {
|
||||
// Comprova si és una tecla d'UI registrada (no passa al joc).
|
||||
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
|
||||
// incloent pause_toggle i menu_toggle (defensa en profunditat:
|
||||
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
|
||||
const auto sc = event.key.scancode;
|
||||
if (!KeyConfig::isGuiKey(sc)) {
|
||||
key_pressed_ = true;
|
||||
JI_moveCheats(sc);
|
||||
}
|
||||
} // Comprova si és una tecla d'UI registrada (no passa al joc).
|
||||
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
|
||||
// incloent pause_toggle i menu_toggle (defensa en profunditat:
|
||||
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
|
||||
const auto sc = event.key.scancode;
|
||||
if (!KeyConfig::isGuiKey(sc)) {
|
||||
key_pressed_ = true;
|
||||
JI_moveCheats(sc);
|
||||
}
|
||||
}
|
||||
Mouse::handleEvent(event);
|
||||
|
||||
@@ -28,13 +28,13 @@ class Director {
|
||||
// per l'event loop de SDL3 en lloc d'un bucle propi — imprescindible
|
||||
// per al port a emscripten, on el runtime posseïx el main loop.
|
||||
void setup();
|
||||
bool iterate(); // torna false quan el joc vol eixir
|
||||
auto iterate() -> bool; // torna false quan el joc vol eixir
|
||||
void teardown();
|
||||
void handleEvent(const SDL_Event& event);
|
||||
|
||||
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
|
||||
void requestQuit();
|
||||
auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||
[[nodiscard]] auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||
|
||||
// Demana un reinici "suau": para música i sons, reseteja info::ctx i
|
||||
// torna a l'intro (state 255). Es processa al començament del pròxim
|
||||
@@ -45,14 +45,13 @@ class Director {
|
||||
auto consumeKeyPressed() -> bool;
|
||||
|
||||
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
||||
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
||||
[[nodiscard]] auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
||||
|
||||
// Pausa: mentre està activa, iterate() no avança l'escena — es
|
||||
// continua presentant el darrer frame amb overlay fresc.
|
||||
void togglePause();
|
||||
auto isPaused() const -> bool { return paused_; }
|
||||
[[nodiscard]] auto isPaused() const -> bool { return paused_; }
|
||||
|
||||
public:
|
||||
~Director();
|
||||
|
||||
private:
|
||||
@@ -64,10 +63,10 @@ class Director {
|
||||
|
||||
// Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
|
||||
// Es crida una sola vegada des d'iterate() a la primera invocació.
|
||||
void initGameContext();
|
||||
static void initGameContext();
|
||||
// Construeix l'escena apropiada segons game_state_ i info::ctx.
|
||||
// Retorna nullptr si l'state actual no té escena registrada (bug).
|
||||
std::unique_ptr<scenes::Scene> createNextScene();
|
||||
auto createNextScene() const -> std::unique_ptr<scenes::Scene>;
|
||||
|
||||
// Buffers persistents entre iteracions. Abans eren locals a run(),
|
||||
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
|
||||
|
||||
Reference in New Issue
Block a user