86 Commits

Author SHA1 Message Date
JailDesigner d89141e014 activa -Wextra -Wpedantic i neteja warnings 2026-05-19 15:42:50 +02:00
JailDesigner b65a615be2 merge audita-nolint: 11→7 NOLINT (neteja obsolets) 2026-05-18 06:33:16 +02:00
JailDesigner da56a81bc3 neteja NOLINT obsolets (11 a 7, sdl3gpu_shader + bulk segur) 2026-05-18 06:32:59 +02:00
JailDesigner a95b4bd1b6 merge: migració PostFX a versió analítica sense supersampling 2026-05-17 15:08:00 +02:00
JailDesigner b0c95111a2 elimina supersampling (Lanczos downscale, keybinding, menu, locale) 2026-05-17 15:07:28 +02:00
JailDesigner 96763847fb PostFXParams/Preset amb chroma_min/max + scan_*; presets AEE migrats 2026-05-17 15:06:42 +02:00
JailDesigner dcb004b5a7 shader postfx nou + spv regenerat + msl extret a headers 2026-05-17 15:06:24 +02:00
JailDesigner 70aa58ec46 merge: neteja tidy AEE (404→0) 2026-05-16 16:16:36 +02:00
JailDesigner e1bc1b597f refactor: extreure helpers per reduir complexitat cognitiva (tidy net) 2026-05-16 16:13:57 +02:00
JailDesigner b984e6041e fix: tidy statics, instance, stretch43, fill/find_if ranges, NOLINT externs 2026-05-16 15:17:38 +02:00
JailDesigner ae359f4a1e fix: tidy namespace Scenes::/Info:: PascalCase i locals UPPER_CASE 2026-05-16 15:06:16 +02:00
JailDesigner ae89b252e2 fix: tidy director/jdraw8/jinput/jfile (locals UPPER_CASE, file_*→Jf::) 2026-05-16 14:57:07 +02:00
JailDesigner 35cdd88cbb fix: tidy scenes enums Phase UPPER_CASE (intro/banner/mort/menu/credits) 2026-05-16 14:54:21 +02:00
JailDesigner 4cac807ce2 fix: tidy scenes (slides/secreta enums UPPER_CASE) i mapa membres _ 2026-05-16 14:46:22 +02:00
JailDesigner bbcc10da81 refactor: JI_* a Ji:: i JG_* a Jg:: 2026-05-16 14:43:16 +02:00
JailDesigner 9d30dd538c fix: tidy modulegame (enums UPPER_CASE, draw/update camelBack, membres _) 2026-05-16 14:40:18 +02:00
JailDesigner 1e00f5c3a4 fix: tidy gamepad/overlay/jfile (statics sense sufix, locals UPPER_CASE) 2026-05-16 14:37:56 +02:00
JailDesigner 7789c1c217 fix: menu.cpp enums UPPER_CASE i statics sense sufix 2026-05-16 14:35:28 +02:00
JailDesigner ec3cb78f6b fix: intro_sprites_scene snake_case → camelBack / UPPER_CASE 2026-05-16 14:32:25 +02:00
JailDesigner f37308a5f0 refactor: JD8_* a namespace Jd8:: 2026-05-16 14:24:22 +02:00
JailDesigner 1ce0d9c56c refactor: JA_* a namespace Ja:: (estil aee_arcade) 2026-05-16 14:15:25 +02:00
JailDesigner 08f587ffe4 merge: neteja cppcheck AEE 2026-05-16 13:52:34 +02:00
JailDesigner bf7be3a7f1 fix: cppcheck (21 troballes) 2026-05-16 13:52:31 +02:00
JailDesigner a48fe51f73 manuals tidy tier 3a: rondes, ternaris, anyofallof, padding, etc. 2026-05-14 19:24:02 +02:00
JailDesigner 0b82be193f manuals tidy tier 2: empty-catch, enum-size, trivially-destructible 2026-05-14 19:07:43 +02:00
JailDesigner 8676c0e773 manuals tidy tier 1: suffixes, params, switches, equals-default 2026-05-14 18:58:29 +02:00
JailDesigner b7a551c158 tidy-fix automàtic (sense naming) 2026-05-14 18:28:23 +02:00
JailDesigner 358e91ea30 moure source/scenes a source/game/scenes 2026-05-14 18:00:18 +02:00
JailDesigner 1aa0e96a91 afegir git hooks per format, tidy i cppcheck 2026-05-14 17:49:29 +02:00
JailDesigner 6ac16ebfeb binari i recursos a build/, targets en kebab 2026-05-14 17:26:15 +02:00
JailDesigner 5dcda36553 estandaritzat .clang-tidy amb el d'AEEA mantenint scenes/ al filtre 2026-05-14 16:35:44 +02:00
JailDesigner 41d429fc10 detecta Ninja com a generador de CMake si està al PATH 2026-05-14 16:23:47 +02:00
JailDesigner 4435bc4942 arreglos en makefile de macos 2026-05-03 18:07:13 +02:00
JailDesigner 4a4485c6f8 bugfixes 2026-04-18 18:16:41 +02:00
JailDesigner d09bb1cf6b actualitzat changelog 2026-04-18 17:57:05 +02:00
JailDesigner b1f9e57f36 fix: color de fonde dels sliders de 050505 a 000000 2026-04-18 15:20:25 +02:00
JailDesigner f7875baa2d refactor: fase 6 — Rule of 5 a Mapa i ModuleGame (no-copiables, no-movibles)
- Mapa té un JD8_Surface fondo propi que s'allibera al destructor: una
  còpia accidental provocaria double-free. Ara els 4 copy/move ops estan
  = delete.
- ModuleGame ja era no-copiable implícitament per tindre unique_ptr
  members, però els = delete expliciten la intenció i protegeixen
  davant refactors futurs que afegeixquen tipus copiables.

Fi de la modernització RAII per fases (1–6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:03:51 +02:00
JailDesigner c6e37af7d1 refactor: fase 5 — singletons a std::unique_ptr (elimina new/delete manual)
5 singletons afectats: Audio, Screen, Director, Resource::Cache, Resource::List.

- static T* instance → static std::unique_ptr<T> instance
- init(): new T() adoptat immediatament per unique_ptr (ownership RAII)
- destroy(): instance.reset() (sense delete manual)
- get(): retorna instance.get()
- Destructors moguts a public perquè std::default_delete hi pugui accedir
  (ctors privats + copy/move deleted → encapsulació efectiva mantinguda)

Ordre de destrucció preservat: SDL_AppQuit segueix cridant destroy() en
l'ordre invers a init() — la RAII automàtica no s'activa fins al final
del programa (LIFO de variables static).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:02:01 +02:00
JailDesigner 5e57034a38 refactor: fase 4 — llista enllaçada de Momia a std::vector<unique_ptr>
Eliminada completament la recursivitat per next-pointer:
- Momia::next, clear(), insertar() desapareixen
- update()/draw() no recursen: operen només sobre la instància pròpia
- ModuleGame::momies: Momia* (head de llista) → std::vector<std::unique_ptr<Momia>>
  - Destructor simplificat (vector s'autodestrueix)
  - Draw: range-for sobre el vector
  - Update: std::erase_if + decrement sincronitzat de info::ctx.momies
  - Cheat "alone": momies.clear()
  - iniciarMomies i nova_momia: emplace_back(std::make_unique<Momia>(...))

Zero new/delete manuals al cicle de vida de les momies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:56:05 +02:00
JailDesigner 2a8fbbb095 refactor: fase 3 — Text::bitmap_ a std::vector<Uint8>
- bitmap_: Uint8* (owning, free'd al destructor) → std::vector<Uint8>
- loadBitmap copia des del buffer de LoadGif i fa free(pixels) de
  l'intermedi (gif.h usa malloc internament)
- ~Text() eliminat: regla 0 aplicada (vector es destrueix sol)
- Les 4 comprovacions !bitmap_ → bitmap_.empty()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:44:07 +02:00
JailDesigner 53e93ef697 refactor: fase 2 — elimina malloc/free a jdraw8 i paletes d'escenes
- JD8_Init/Quit: new[]/delete[] per a screen, main_palette, pixel_data
- JD8_NewSurface/FreeSurface: new Uint8[64000]{}/delete[]
- JD8_LoadPalette: uniforme — sempre retorna `new Color[256]`, copiant del
  LoadPalette extern al path no-cached (l'intermedi raw es frees amb free()
  perquè gif.h el malloca)
- JD8_SetScreenPalette: delete[] la paleta reemplaçada
- slides/secreta/menu/banner/mort scenes: std::free/std::malloc → delete[]/new Color[256]

Ownership uniforme: tot el cicle de vida de surface/palette usa new[]/delete[].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:42:31 +02:00
JailDesigner e7aa2463b4 refactor: fase 1 — cleanup mecànic de baix risc (NULL→nullptr, typedef→using, explicit, enum class local)
- jdraw8.hpp: typedef → using (JD8_Surface, JD8_Palette)
- jdraw8.cpp: NULL → nullptr, C-casts → static_cast/reinterpret_cast, anon enum FadeType → enum class
- momia.cpp: NULL → nullptr
- bola/mapa/marcador/momia/engendro: explicit als constructors

Zero canvis de lògica ni ownership. Primera fase de la modernització RAII.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:37:48 +02:00
JailDesigner 27f8b0ae36 cppcheck 2026-04-18 13:22:13 +02:00
JailDesigner 2e1a82ff40 afegit suppress a cppcheck 2026-04-18 12:55:27 +02:00
JailDesigner 94aa69cffe afegit resource::cache
normalitzat Audio
2026-04-18 11:41:34 +02:00
JailDesigner 7409c799c3 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
JailDesigner 417699d276 renombrats els fitxers de musica 2026-04-17 13:29:07 +02:00
JailDesigner 9d86137203 arreglos en make i cmake per estandaritzar amb la resta de projectes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:59:31 +02:00
JailDesigner 52369be7ae el logo nou de la intro es tornava a descentrar 2026-04-16 22:15:37 +02:00
JailDesigner 1c11a3057b afegits events de canvas d'emscripten 2026-04-16 22:12:30 +02:00
JailDesigner e8b0b12f98 internal resolution 2026-04-16 21:40:14 +02:00
JailDesigner 16a3f5b470 treballant en internal resolution 2026-04-16 20:53:13 +02:00
JailDesigner 5cda8fc3f9 centrat correctament el logo de jailgames (el nou) 2026-04-16 20:18:28 +02:00
JailDesigner 5956d874c3 animacio de tancar el menu 2026-04-16 20:14:35 +02:00
JailDesigner e0f9b60f22 menu de sistema amb versió i opció per a tancar i reiniciar 2026-04-16 20:01:58 +02:00
JailDesigner d3bdd9b783 afegit fix de mandos en emscripten android 2026-04-16 19:35:48 +02:00
JailDesigner a36662ac6e fix: shaders on i off no afectaven a crtpi 2026-04-16 19:26:45 +02:00
JailDesigner 52431adb0e afegits tots els valors d'escala que dona sdl3 2026-04-16 19:15:35 +02:00
JailDesigner a3fc1119ae menu ara permet amagar items en funció d'altres items 2026-04-16 19:01:35 +02:00
JailDesigner 6394e9afab varies coses i detallets 2026-04-16 18:46:58 +02:00
JailDesigner fe41919e1e clang-format
mogudes coses de config.yaml a debug.yaml
2026-04-16 16:46:18 +02:00
JailDesigner 0cd09f6d28 idem 2026-04-16 16:37:38 +02:00
JailDesigner 083a57dab5 ordenada la carpeta data 2026-04-16 16:37:30 +02:00
JailDesigner 4244bcaea3 acabat amb resource.pack 2026-04-16 16:21:44 +02:00
JailDesigner b2d5f5af61 feat: resource.pack estil coffee_crisis — Fase 1 (pack + helper + eina pack_resources) 2026-04-16 13:58:39 +02:00
JailDesigner 7f26b8dbd0 opcions per defecte d'emscripten 2026-04-16 13:40:21 +02:00
JailDesigner 550e3e0e12 refactor: JA_Sound_t RAII — buffer amb unique_ptr + SDLFreeDeleter, elimina JA_NewSound 2026-04-16 13:28:31 +02:00
JailDesigner 96a3cf9ebc step B.2: elimina fiber — Director posseeix l'escena, JD8_Flip sense yield, fiber.hpp/cpp esborrats 2026-04-16 11:14:48 +02:00
JailDesigner 4e18f83ec5 step B.1: fades de ModuleGame tick-based amb scenes::PaletteFade (fases FadingIn/FadingOut sense redibuixar, per no perdre el frame final) 2026-04-16 10:27:04 +02:00
JailDesigner f9346add79 refactor: jail_audio RAII polish — JA_Music_t amb vector<Uint8>/string + elimina overload i camp morts 2026-04-16 10:02:55 +02:00
JailDesigner b3ff620c81 refactor: file_getfilebuffer → file_readfile (std::vector<char>) — elimina 3 leaks (paleta + música gameplay + música cinemàtica) 2026-04-16 09:43:27 +02:00
JailDesigner d343e719ca step 9: intro_sprites_scene com a sub-escena (elimina doIntroSprites + 3 variants aleatòries) 2026-04-16 08:38:47 +02:00
JailDesigner e18b7321eb step 8: intro_scene substituix doIntro() (revelat JAILGAMES lletra a lletra + cicle de paleta) 2026-04-16 08:00:22 +02:00
JailDesigner 6125277d70 docs: plan de migració scenes:: al repo (per a continuar des d'altres equips) 2026-04-16 00:18:09 +02:00
JailDesigner 6063b1c606 step 7: secreta_scene amb swap de tomba1→tomba2 i red pulse animat 2026-04-16 00:13:02 +02:00
JailDesigner 829d7431c1 step 6: credits_scene substituix doCredits() (scroll vertical + parallax condicional) 2026-04-16 00:03:25 +02:00
JailDesigner 605c273173 step 5: slides_scene amb wipe suau per easing (substituix doSlides) 2026-04-15 23:50:59 +02:00
JailDesigner ad38fc09cf step 4: intro_new_logo_scene substituix doIntroNewLogo(); doIntroSprites exposat temporalment 2026-04-15 23:28:22 +02:00
JailDesigner 8720e775a0 step 3: menu_scene substituix doMenu() + fix JI_Update al fiber loop 2026-04-15 23:19:58 +02:00
JailDesigner 2cb38ffb49 step 2: banner_scene substituix doBanner() (piràmides 2-5) + helper playMusic compartit 2026-04-15 23:13:05 +02:00
JailDesigner d86cb21efa step 1: mort_scene substituix doMort() amb la capa scenes:: 2026-04-15 23:05:45 +02:00
JailDesigner 4436f7f569 scenes: infraestructura de la capa scenes:: (scene, timeline, sprite mover, frame animator, palette fade, surface handle, registry) 2026-04-15 19:40:39 +02:00
JailDesigner 1507a1c740 fase 4+5: fibers cooperatius substitueixen el game thread, sense mutex ni cv 2026-04-15 18:50:43 +02:00
JailDesigner 801a8ad1bd fase 3: port de jail_audio header-only amb streaming i sense SDL_AddTimer 2026-04-15 18:23:34 +02:00
JailDesigner 80fa7b46e7 fase 2: fades de jd8 a màquina d'estats i helper wait_frame_or_skip a les cinemàtiques 2026-04-15 18:12:03 +02:00
JailDesigner 7f85b50c63 fase 1: jail i game a c++ idiomàtic (raii, info::ctx, cheats arreglats) 2026-04-15 18:03:46 +02:00
164 changed files with 21485 additions and 18065 deletions
+107
View File
@@ -0,0 +1,107 @@
Checks:
- readability-*
- modernize-*
- performance-*
- bugprone-*
- -readability-identifier-length
- -readability-magic-numbers
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays
WarningsAsErrors: '*'
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
HeaderFilterRegex: 'source/(core|game|utils)/'
FormatStyle: file
CheckOptions:
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
# =====================================================================
# CONSTANTES → UPPER_CASE (compile-time y runtime, en cualquier scope)
# =====================================================================
# Todo lo que sea const o constexpr se identifica visualmente en UPPER_CASE,
# sin importar si es global, local, miembro o static.
# constexpr en cualquier scope (globales y locales)
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
# Constantes globales (const no-constexpr)
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Constantes locales (const en función)
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
# Static const a nivel de archivo/namespace
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
# Miembros static const/constexpr de clase (p.ej. static constexpr int MAX = 100;)
- { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE }
# Miembros const no-static de clase (p.ej. const int limit;)
- { key: readability-identifier-naming.ConstantMemberCase, value: UPPER_CASE }
# Valores de enums
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# NOTA: Los parámetros const NO se tratan como constantes aquí.
# Un parámetro sigue siendo un parámetro aunque sea const → hereda ParameterCase.
# =====================================================================
# VARIABLES NO-CONST
# =====================================================================
# Variables locales
- { key: readability-identifier-naming.VariableCase, value: lower_case }
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
# Parámetros de función
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
# Variables estáticas no-const (static locales, static file-scope,
# y static members no-const de clase como el instance_ de un Singleton).
# Sufijo _ para marcar que tienen storage estático.
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
# =====================================================================
# MIEMBROS DE CLASE NO-CONST
# =====================================================================
# Privados: snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Protegidos: snake_case con sufijo _
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Públicos: snake_case sin sufijo
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# =====================================================================
# TIPOS
# =====================================================================
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
- { key: readability-identifier-naming.StructCase, value: CamelCase }
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
# Namespaces
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# =====================================================================
# FUNCIONES Y MÉTODOS (incluyendo constexpr)
# =====================================================================
# Un método/función constexpr es un invocable, no una constante → camelBack.
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
- { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack }
- { key: readability-identifier-naming.MethodCase, value: camelBack }
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
+1
View File
@@ -0,0 +1 @@
{"sessionId":"7b0c9c32-3dd4-48a3-ba06-c2303dc08243","pid":123890,"acquiredAt":1776510185734}
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Pre-commit hook: aplica clang-format als fitxers C++ staged abans del commit.
# - Només toca fitxers staged dins source/ (exclou source/external/).
# - Avorta el commit si hi ha canvis NO staged en aquests fitxers (per no incloure'ls sense voler).
set -euo pipefail
if ! command -v clang-format >/dev/null 2>&1; then
echo "pre-commit: clang-format no trobat — saltant format check" >&2
exit 0
fi
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=ACMR \
| grep -E '^source/.*\.(cpp|hpp|h)$' \
| grep -vE '^source/external/' || true)
if [ ${#STAGED[@]} -eq 0 ]; then
exit 0
fi
UNSTAGED_DIRTY=()
for f in "${STAGED[@]}"; do
if ! git diff --quiet -- "$f"; then
UNSTAGED_DIRTY+=("$f")
fi
done
if [ ${#UNSTAGED_DIRTY[@]} -gt 0 ]; then
echo "pre-commit: aquests fitxers tenen canvis NO staged i estan al commit." >&2
echo " Fes 'git add' o 'git stash' abans de continuar:" >&2
printf ' %s\n' "${UNSTAGED_DIRTY[@]}" >&2
exit 1
fi
clang-format -i "${STAGED[@]}"
git add -- "${STAGED[@]}"
# --- clang-tidy només sobre els fitxers staged ---
if ! command -v clang-tidy >/dev/null 2>&1; then
echo "pre-commit: clang-tidy no trobat — saltant tidy" >&2
exit 0
fi
REPO_ROOT="$(git rev-parse --show-toplevel)"
BUILD_DIR="$REPO_ROOT/build"
if [ ! -f "$BUILD_DIR/compile_commands.json" ]; then
echo "pre-commit: generant compile_commands.json (build dir buit)..." >&2
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" >/dev/null
fi
echo "pre-commit: clang-tidy sobre ${#STAGED[@]} fitxer(s)..." >&2
if ! clang-tidy -p "$BUILD_DIR" --quiet "${STAGED[@]}"; then
echo "pre-commit: clang-tidy ha trobat errors — commit avortat" >&2
exit 1
fi
# --- cppcheck només sobre els .cpp staged ---
if ! command -v cppcheck >/dev/null 2>&1; then
echo "pre-commit: cppcheck no trobat — saltant cppcheck" >&2
exit 0
fi
CPP_STAGED=()
for f in "${STAGED[@]}"; do
[[ "$f" == *.cpp ]] && CPP_STAGED+=("$f")
done
if [ ${#CPP_STAGED[@]} -eq 0 ]; then
exit 0
fi
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
if ! cppcheck \
--enable=warning,style,performance,portability \
--std=c++20 \
--language=c++ \
--inline-suppr \
--suppress=missingIncludeSystem \
--suppress=toomanyconfigs \
--suppress='*:*source/external/*' \
--suppress='*:*source/core/rendering/sdl3gpu/spv/*' \
--suppress=normalCheckLevelMaxBranches \
-D_DEBUG \
-DLINUX_BUILD \
--quiet \
--error-exitcode=1 \
-I "$REPO_ROOT/source" \
"${CPP_STAGED[@]}"; then
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
exit 1
fi
+54 -5
View File
@@ -1,8 +1,57 @@
# --- Build outputs ---
build/
dist/
aee aee
aee.exe aee.exe
.DS_Store *.o
trick.ini *.obj
.vscode/ *.exe
*.app
# --- Generated assets ---
resources.pack
data.jrf data.jrf
build/
dist/ # --- Runtime / debug junk ---
trick.ini
*.log
*.dmp
# --- Editor / IDE ---
.vscode/
.idea/
*.swp
*.swo
*~
.cache/
compile_commands.json
# --- macOS ---
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
.fseventsd
.DocumentRevisions-V100
.TemporaryItems
.VolumeIcon.icns
Icon?
# --- Windows ---
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
desktop.ini
$RECYCLE.BIN/
*.lnk
# --- Linux ---
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
+85 -2
View File
@@ -1,6 +1,88 @@
# Changelog # Changelog
Tots els canvis de la reconstrucció moderna (C++/SDL3) d'**Aventures en Egipte**, des de l'inici del port fins a la v1.1. Tots els canvis de la reconstrucció moderna (C++/SDL3) d'**Aventures en Egipte**.
## [1.2] — 2026-04-18
Versió de modernització profunda: desapareix el model *threads estil emulador* i tot el runtime passa a un sol fil tick-based compatible amb emscripten. Zero regressions de gameplay.
### Afegit
#### Arquitectura: capa `scenes::` tick-based
- Infraestructura `scenes::` ([source/scenes/](source/scenes/)): `Scene`, `SceneRegistry`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, helper `playMusic` (`4436f7f`)
- **MortScene** substitueix `doMort()` (`d86cb21`)
- **BannerScene** substitueix `doBanner()` per piràmides 25 (`2cb38ff`)
- **MenuScene** substitueix `doMenu()` + fix `JI_Update` al loop (`8720e77`)
- **IntroNewLogoScene** substitueix `doIntroNewLogo()` (`ad38fc0`)
- **SlidesScene** amb wipe suau per easing (`605c273`)
- **CreditsScene** amb scroll vertical + parallax condicional (`829d743`)
- **SecretaScene** amb swap `tomba1→tomba2` i red pulse animat (`6063b1c`)
- **IntroScene** amb revelat *JAILGAMES* lletra a lletra + cicle de paleta (`e18b732`)
- **IntroSpritesScene** com a sub-escena amb 3 variants aleatòries (`d343e71`)
- **ModuleGame** migrat a `scenes::Scene` amb fases `FadingIn`/`FadingOut` (`4e18f83`)
- Pla de migració documentat a [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) (`6125277`)
#### Resource pack
- Sistema d'empaquetat d'assets `resources.pack` (format **AEE1**, XOR-xifrat) estil *coffee_crisis* (`b2d5f5a`, `4244bca`)
- Classe `ResourcePack` + namespace `ResourceHelper` + eina CLI standalone `pack_resources` (target `make pack`)
- Cablejat a tots els callsites de recursos via `ResourceHelper::loadFile`
- Scaffold `.jrf` llegat eliminat completament de `jfile.cpp`
- Releases natius depenen del pack i l'usen obligatòriament (sense fallback); WASM i Debug mantenen fallback
- Normalització de `resource::cache` per a `Audio` (`94aa69c`)
#### Build WebAssembly
- Build WASM via Docker (`emscripten/emsdk:latest`) amb desplegament a maverick (`make wasm`)
- SDL3 compilat des de font via `FetchContent`; shaders omesos; `sdl3gpu_shader.cpp` exclòs
- Events de canvas d'emscripten (`1c11a30`)
- Fix de mandos en emscripten Android (`d3bdd9b`)
- Defaults específics d'emscripten (`7f26b8d`)
- Internal resolution configurable (`e8b0b12`, `16a3f5b`)
#### Menú i UI
- **Menú de sistema** amb versió i opció de tancar/reiniciar (`e0f9b60`)
- Animació de tancar el menú (`5956d87`)
- Items ocultables condicionalment en funció d'altres items (`a3fc111`)
- Tots els valors d'escala que exposa SDL3 (`52431ad`)
- `debug.yaml` separat de `config.yaml` (`fe41919`)
### Canviat
#### Runtime: sense fibers, sense threads, sense mutex
- **Fase 1** — jail i game a C++ idiomàtic: RAII, `info::ctx` com a singleton `inline`, cheats arreglats (`scancode→ASCII`) (`7f85b50`)
- **Fase 2** — fades de `jd8` a màquina d'estats + helper `wait_frame_or_skip` a les cinemàtiques (`80fa7b4`)
- **Fase 3** — `jail_audio` header-only amb streaming real (`stb_vorbis_open_memory` + `JA_PumpMusic`), sense `SDL_AddTimer` (`801a8ad`)
- **Fase 4+5** — fibers cooperatius substitueixen el game thread, sense mutex ni `cv` (`1507a1c`)
- **Step B.1** — fades de `ModuleGame` tick-based amb `scenes::PaletteFade` (`4e18f83`)
- **Step B.2** — **eliminació total del fiber**: `Director` posseeix l'escena (`current_scene_`, `game_state_`), `JD8_Flip` sense yield, `fiber.{hpp,cpp}` esborrats (`96a3cf9`)
- **Step 10** — `ModuleSequence` eliminat; dispatch via `SceneRegistry::tryCreate()` i `game_state_ == 0/1` directe des del `Director`
- Main loop via **SDL3 Callback API** (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit`/`Iterate`/`Event`/`Quit`, compatible amb emscripten
#### RAII i neteja de memòria
- **Fase 1** — cleanup mecànic: `NULL→nullptr`, `typedef→using`, `explicit`, `enum class` local (`e7aa246`)
- **Fase 2** — elimina `malloc`/`free` a `jdraw8` i paletes d'escenes (`53e93ef`)
- **Fase 3** — `Text::bitmap_` a `std::vector<Uint8>` (`2a8fbbb`)
- **Fase 4** — llista enllaçada de Momia a `std::vector<std::unique_ptr>` (`5e57034`)
- **Fase 5** — singletons a `std::unique_ptr` (elimina `new`/`delete` manual) (`c6e37af`)
- **Fase 6** — Rule of 5 a `Mapa` i `ModuleGame` (no-copiables, no-movibles) (`f7875ba`)
- `file_getfilebuffer``file_readfile` retornant `std::vector<char>` — elimina 3 leaks silenciosos (paleta + música gameplay + música cinemàtica) (`b3ff620`)
- `JA_Music_t` RAII amb `vector<Uint8>`/`string`, elimina overload i camps morts (`f9346ad`)
- `JA_Sound_t` RAII amb `unique_ptr + SDLFreeDeleter`, elimina `JA_NewSound` (`550e3e0`)
#### Build i tooling
- Unificats `.clang-format` i `.clang-tidy`, amb exclusió de `external/` i `spv/` via dummies (`7409c79`)
- `cppcheck` integrat amb suppress list (`27f8b0a`, `2e1a82f`)
- `make`/`cmake` estandarditzats amb la resta de projectes JailGames (`9d86137`)
- Fitxers de música renombrats a noms temàtics (`417699d`)
- Carpeta `data/` reordenada (`083a57d`)
### Arreglat
- Shaders ON/OFF no afectaven a CRT-Pi (`a36662a`)
- Logo nou de la intro tornava a descentrar-se (`52369be`, `5cda8fc`)
- Color de fons dels sliders de `0x050505` a `0x000000` (`b1f9e57`)
- Diversos detalls menors (`6394e9a`, `0cd09f6`)
---
## [1.1] — 2026-04-05 ## [1.1] — 2026-04-05
@@ -64,4 +146,5 @@ Versió que fa coincidir la numeració amb la del joc original del 2000.
--- ---
[1.1]: https://gitea/aee/compare/9e0ab87...HEAD [1.2]: https://gitea/aee/compare/486f00b...HEAD
[1.1]: https://gitea/aee/compare/9e0ab87...486f00b
+158 -39
View File
@@ -24,26 +24,74 @@ The executable is output to the project root. The `data/` folder must be in the
## Architecture ## Architecture
### Golden Rule: Do Not Touch Gameplay ### New Rules (Modernization Phase)
The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only. Any new feature must be implemented as an overlay on top of the existing game, never by modifying original gameplay code. The old "Golden Rule: Do Not Touch Gameplay" has been **revoked**. The original C-style code (jail engine + gameplay modules) is now a **modernization target**, not a sacred zone. The parallel-overlay approach has reached its ceiling: fades and cinematics are still blocking loops, audio relies on an async `SDL_AddTimer`, and the emulator-style game thread blocking at `publishFrame` is incompatible with an emscripten port.
The five current objectives are:
1. **Idiomatic C++**: RAII, `std::vector`/`std::string`/`std::optional`, classes with real constructors/destructors. No more raw `malloc/free` in structs.
2. **Zero blocking events**: no `while (...) { poll; }`, no `SDL_Delay` inside gameplay, no `cv.wait()` in `publishFrame`. Every subsystem must be able to advance in a single tick call.
3. **Time-based**: animations, cinematics and fades measured in milliseconds, not frames. `JG_ShouldUpdate()` as gameplay gate is on its way out.
4. **Overlay integrated**: overlay stops being a post-game layer painted by Director — it becomes part of the same render pass the game tick produces.
5. **SDL3 callbacks**: main loop handed over to `SDL_AppInit` / `SDL_AppIterate` / `SDL_AppEvent` / `SDL_AppQuit`, single-threaded, compatible with emscripten.
**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan.
### Migration Status (2026-04-16)
**Completat.** Totes les fases del pla original (07) i la migració `scenes::` (Steps 010) estan fetes, ModuleGame és una `scenes::Scene` tick-based, el cooperative fiber s'ha eliminat, i el build emscripten/WASM arrenca i es publica a maverick.
**Arquitectura actual**:
- Un sol thread (Director). Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit/Iterate/Event/Quit` a [main.cpp](source/main.cpp).
- `Director::iterate()` posseeix l'estat d'escena (`current_scene_`, `game_state_`) i fa input → tick de l'escena → `JD8_Flip` (sense yield, només converteix `screen``pixel_data`) → overlay → present. Tot en línia recta, zero fibers, zero mutex.
- Totes les escenes (inclòs `ModuleGame`) implementen `scenes::Scene` amb `onEnter/tick(delta_ms)/done/nextState`.
- `ModuleSequence` (el vell dispatcher) eliminat. Despatxa via `game_state_ == 0` (gameplay → `ModuleGame`) o `game_state_ == 1` (cinemàtica → `SceneRegistry::tryCreate(num_piramide)`).
**Escenes migrades** (totes registrades a `Director::init` via `SceneRegistry`):
- `MortScene` (state 100) · `BannerScene` (2..5) · `MenuScene` (0) · `SlidesScene` (1, 7)
- `CreditsScene` (8) · `SecretaScene` (6) · `IntroNewLogoScene` (255, `use_new_logo=true`)
- `IntroScene` (255, `use_new_logo=false`) · `IntroSpritesScene` (sub-escena de les dues intros)
**Files d'`Options::game` exposats per a tests ràpids** (persistits a `config.yaml`):
`piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits`.
**La capa `scenes::`** ([source/scenes/](source/scenes/)): `scene.hpp` (interfície), `scene_registry.hpp/.cpp`, `timeline`, `sprite_mover`, `frame_animator`, `palette_fade`, `surface_handle`, `scene_utils` (`playMusic`). Pures tick-based, zero while, zero `JG_ShouldUpdate`.
### Modernization Targets
**Invariants to preserve** (touch these and you broke the game):
- Gameplay feel, movement speed, enemy AI behavior
- Collision detection, scoring, lives, level progression
- Visible animation cadence (once translated to ms, must look identical)
- Difficulty curves and cinematic timings
- Cheat codes (`reviu`, `alone`, `obert`)
- Original palettes, fades, music cues
**Free to change** (internal representation):
- Data structures (structs → classes with RAII)
- Ownership (raw pointers → `std::unique_ptr`/`std::vector`/`std::string`)
- Timing representation (frame counters → ms accumulators)
- Threading model (game thread → single-threaded state machine)
- Global state (the old `info::` namespace is now an `inline` singleton `info::ctx` of type `GameContext`; access is `info::ctx.X` instead of `info::X`. Can be reset with `info::ctx.reset()`)
- API shapes of jail subsystems (as long as callers are updated consistently)
### Boundary: Original vs New Code ### Boundary: Original vs New Code
| Path | Owner | Rule | | Path | Owner | Rule |
|------|-------|------| |------|-------|------|
| `source/core/jail/` | Original engine | **Do not modify** gameplay behavior | | `source/core/jail/` | Legacy engine, modernization target | Free to modify with care — preserve external behavior |
| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** | | `source/game/*.cpp/hpp` | Legacy gameplay, modernization target | Free to modify with care — preserve gameplay invariants |
| `source/core/rendering/` | New presentation layer | Free to modify | | `source/core/rendering/` | New presentation layer | Free to modify |
| `source/core/input/` | New input layer | Free to modify | | `source/core/input/` | New input layer | Free to modify |
| `source/utils/` | New utilities | Free to modify | | `source/utils/` | New utilities | Free to modify |
| `source/game/options,defines,defaults` | New config system | Free to modify | | `source/game/options,defines,defaults` | New config system | Free to modify |
| `data/*.gif, *.ogg` | Original assets | **Do not modify** | | `data/gfx/, data/music/` | Original assets | **Do not modify** — assets remain untouchable |
| `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify | | `data/fonts/, data/shaders/, data/locale/` | New assets | Free to modify |
### Original "Jail" Engine (`source/core/jail/`) ### Legacy "Jail" Engine (`source/core/jail/`) — modernization target
Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay logic.** Flat C-style APIs (no classes), prefixed by subsystem. Being progressively converted to idiomatic C++ (see Phase 1 of the plan). External API names are kept stable during the transition to avoid churning call sites.
- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()` - **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()`
- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()` - **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()`
@@ -53,7 +101,7 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
### System Layer (`source/core/system/`) ### System Layer (`source/core/system/`)
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()` `Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top - **Director** (`director.hpp/cpp`) — **Orchestrator singleton**, únic thread del runtime. Posseeix l'estat d'escena (`current_scene_: unique_ptr<Scene>`, `game_state_`, `last_tick_ms_`) directament com a members. `iterate()` fa: poll events (via `SDL_AppEvent`) → input (Gamepad/KeyRemap/GlobalInputs/Mouse) → `JA_Update` → transició d'escena si `done()``scene->tick(delta_ms)` `JD8_Flip` (converteix `screen``pixel_data`) → overlay → present → `SDL_Delay` al frame target. Dispatcher: `game_state_ == 0``new ModuleGame`, `game_state_ == 1``SceneRegistry::tryCreate(info::ctx.num_piramide)` (amb redirect `num_piramide == 6 && diners < 200 → 7` replicant el vell `ModuleSequence::Go`).
### Presentation Layer (`source/core/rendering/`) ### Presentation Layer (`source/core/rendering/`)
@@ -65,9 +113,10 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
### Input Layer (`source/core/input/`) ### Input Layer (`source/core/input/`)
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer) - **KeyConfig** (`key_config.hpp/cpp`) — **Font única de veritat per a les tecles d'UI/sistema**. Carrega `data/input/keys.yaml` al boot (12 entrades: F1-F10 GlobalInputs + F11 pausa + F12 menú de servei) i opcionalment aplica overrides des de `~/.config/jailgames/aee/keys.yaml`. Exposa `KeyConfig::scancode("id")`, `scancodePtr("id")` (per a Menu KeyBind), `setScancode(...)`, `isGuiKey(sc)` (filtre del Director per a no propagar tecles d'UI a `JI_AnyKey`). `saveOverrides()` només persistix les entrades que difereixen del default. Les tecles de moviment del jugador NO viuen ací — es queden a `Options::keys_game`
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps function keys (via `KeyConfig::scancode("dec_zoom")`, etc.) to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity - **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement; A/B buttons, Start, Back translate to synthetic SDL key events (F12/ESC/Enter/Backspace) when menu is open, so Director handles them exactly like keyboard. Loads extra mappings from `gamecontrollerdb.txt` (next to the executable) at init via `SDL_AddGamepadMappingsFromFile`, extending SDL's built-in controller database - **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug + overlay notification with controller name. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement. Mapeig: SOUTH/EAST/WEST/NORTH (4 botons frontals) → Enter sintètic per avançar escenes; al menú EAST=accept, SOUTH=cancel/back. SELECT → menu_toggle (servei), START → pause_toggle (via `KeyConfig::scancode(...)`). Loads extra mappings from `gamecontrollerdb.txt` at init via `SDL_AddGamepadMappingsFromFile`
- **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp` - **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp`
### Locale Layer (`source/core/locale/`) ### Locale Layer (`source/core/locale/`)
@@ -79,8 +128,8 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
Follows the pattern from `jaildoctors_dilemma`, persists to YAML: Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
- **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT` - **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
- **defaults.hpp** — Default values: `Defaults::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game` - **defaults.hpp** — Default values: `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`. (Les tecles d'UI viuen a `data/input/keys.yaml` via `KeyConfig`)
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset` - **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
### Utilities (`source/utils/`) ### Utilities (`source/utils/`)
@@ -99,40 +148,58 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
| F6 | Toggle supersampling | | F6 | Toggle supersampling |
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) | | F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
| F8 | Cycle shader presets | | F8 | Cycle shader presets |
| F9 | Toggle stretch filter (nearest ↔ linear) | | F9 | Cycle texture filter (nearest ↔ linear) — sempre aplicat, independent de 4:3 |
| F10 | Cycle render info (off → top → bottom → off) | | F10 | Cycle render info (off → top → bottom → off) |
| F11 | Toggle pause (blocks game thread at publishFrame + `JA_PauseMusic`/`JA_ResumeMusic`) | | F11 | Toggle pause (Director skips `scene->tick()` + `JA_PauseMusic`/`JA_ResumeMusic`) |
| F12 | Toggle floating options menu | | F12 | Toggle floating options menu |
| ESC | Double-press to quit (with overlay notification) / close menu if open | | ESC | Double-press to quit (with overlay notification) / close menu if open |
| Backspace | Go up one menu level / close menu if at root | | Backspace | Go up one menu level / close menu if at root |
| ↑↓←→ / Enter | Menu navigation | | ↑↓←→ / Enter | Menu navigation |
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working. UI/system key bindings are loaded from [data/input/keys.yaml](data/input/keys.yaml) via `KeyConfig`. Overrides fets des del menú es persistixen a `~/.config/jailgames/aee/keys.yaml` (només les que difereixen del default). Game movement keys (`Options::keys_game.up/down/left/right`) viuen separadament a `config.yaml` (secció `controls:`) i es remapejen via la CONTROLS submenu — el `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.
### Threading Model (Emulator Architecture) ### Execution Model (Single-threaded, Scene-based)
Zero threads, zero fibers, zero mutex. Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`) a [main.cpp](source/main.cpp). Cada frame entra pel `SDL_AppIterate``Director::iterate()`:
``` ```
Main thread (Director) Game thread (ModuleGame/Sequence::Go()) SDL_AppIterate → Director::iterate() {
──────────────────── ──────────────────────────────────── if (quit_requested_) { scene.reset(); return false; }
loop at ~60 FPS { loop { if (!context_initialized_) initGameContext();
SDL_PollEvent() ... game logic ...
GlobalInputs, Mouse JD8_Flip(): Gamepad/KeyRemap/GlobalInputs/Mouse::update
if new_frame_available: palette→ARGB in pixel_data JA_Update() ← audio pump
copy to game_frame publishFrame(pixel_data) ⏸
signal → ────────────────────→ (blocks until Director consumes) if (!paused_) {
copy game_frame → present_buffer ←──── signal_consumed if (scene && (scene->done() || JG_Quitting()))
Overlay::render(present_buffer) continue game loop game_state_ = scene->nextState(); scene.reset();
Screen::present(present_buffer) } if (!scene) {
SDL_Delay to hit 60fps if (game_state_ == -1 || JG_Quitting()) return false;
scene = createNextScene(); ← ModuleGame o registry.tryCreate()
scene->onEnter();
}
JI_Update()
scene->tick(now - last_tick_ms_)
JD8_Flip() ← converteix screen indexat → pixel_data
memcpy pixel_data → game_frame
}
memcpy game_frame → presentation_buffer
Overlay::render(presentation_buffer)
Screen::present(presentation_buffer)
SDL_Delay(frame_target - elapsed)
} }
SDL_AppEvent → Director::handleEvent() ← events lliurats un a un per SDL
SDL_AppQuit → Director::teardown() ← Options::save + descàrrega ordenada
``` ```
**Key points:** **Key points:**
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay - `Director` posseeix `current_scene_`, `game_state_`, `last_tick_ms_`, `context_initialized_` com a members — abans vivien al stack del fiber.
- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame) - `JD8_Flip()` només converteix paleta + `screen` a ARGB (`pixel_data`). Ja **no fa yield** — tot corre lineal.
- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point - `JG_ShouldUpdate()` encara existeix a `jgame.cpp` com a timing-gate per a `ModuleGame::Update()` (10 ms fix), però ja no fa yield. Cap caller fa spin-wait.
- SDL events processed ONLY on main thread (SDL requirement) - Pausa (F11) simplement salta el bloc de tick; overlay i present continuen, es re-presenta l'últim frame congelat.
- `JI_Update()` no longer polls events — reads Director's state - Doble buffer (`game_frame` + `presentation_buffer`) es manté perquè el Director pot presentar múltiples frames per cada tick durant pausa; el cost (256 KB memcpy) és trivial a 320×200.
- SDL3 Callback API compatible amb emscripten: el navegador posseeix el main loop i ens crida via `requestAnimationFrame`. Zero canvis de codi per a portabilitat.
### Rendering Pipeline (inside Screen::present) ### Rendering Pipeline (inside Screen::present)
@@ -158,10 +225,40 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
| File | Content | | File | Content |
|------|---------| |------|---------|
| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (movement keys + menu_toggle + pause_toggle) | | `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `scaling_mode`, `texture_filter`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (només moviment del jugador) |
| `~/.config/jailgames/aee/keys.yaml` | UI key overrides (només entrades que difereixen del default de [data/input/keys.yaml](data/input/keys.yaml)). Generat per `KeyConfig::saveOverrides()` |
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) | | `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) | | `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |
### Resource Pack (`source/core/resources/`)
Sistema d'empaquetat d'assets a l'estil `coffee_crisis_arcade_edition`. Genera un sol fitxer binari opac `resources.pack` que substitueix la carpeta `data/` als releases natius.
**Format AEE1** (fidel a CCAE amb clau pròpia):
```
Header: "AEE1" (4B) + version uint32 + resource_count uint32
Index: per recurs → filename_len uint32 + filename + offset uint64 + size uint64 + checksum uint32
Payload: data_size uint64 + bytes XOR-xifrats amb "AEE_RESOURCES__2026"
```
Checksum: djb2-like amb seed `0x12345678`. Càrrega full-to-RAM (sense mmap).
**Fitxers**:
- [source/core/resources/resource_pack.hpp/cpp](source/core/resources/) — classe `ResourcePack`: `loadPack`, `savePack`, `addFile`, `addDirectory`, `getResource(name) → std::vector<uint8_t>`, `hasResource`
- [source/core/resources/resource_helper.hpp/cpp](source/core/resources/) — namespace `ResourceHelper`: `initializeResourceSystem(pack, enable_fallback)`, `loadFile(relative_path)`, `shutdownResourceSystem`. Prova el pack primer, cau a `file_getresourcefolder()+path` si el fallback està actiu.
- [tools/pack_resources/pack_resources.cpp](tools/pack_resources/pack_resources.cpp) — eina standalone CLI: `pack_resources [input_dir=data] [output=resources.pack]` + `--list pack`.
**Build**:
- `make pack` compila l'eina (target `pack_resources` a `EXCLUDE_FROM_ALL` de [CMakeLists.txt](CMakeLists.txt)) i genera `resources.pack` a la rel. 33 entrades ≈ 4 MB.
- `./build/pack_resources --list resources.pack` inspecciona el pack.
**Estat actual (Fases 1-6 completades, 2026-04-16)**:
- `ResourcePack` + `ResourceHelper` + eina `pack_resources` compilen i funcionen. El pack genera 33 entrades ≈ 4 MB.
- Cablejat al joc via `ResourceHelper::initializeResourceSystem` a [main.cpp](source/main.cpp) (amb `return SDL_APP_FAILURE` si falla), i `shutdownResourceSystem` a `SDL_AppQuit`.
- Tots els callsites de recursos usen `ResourceHelper::loadFile` (`std::vector<uint8_t>`): [locale.cpp](source/core/locale/locale.cpp), [text.cpp](source/core/rendering/text.cpp), [scene_utils.cpp](source/scenes/scene_utils.cpp), [modulegame.cpp](source/game/modulegame.cpp), [jdraw8.cpp](source/core/jail/jdraw8.cpp).
- Scaffold `.jrf` eliminat de [jfile.cpp](source/core/jail/jfile.cpp): `file_setresourcefilename`, `file_setsource`, `SOURCE_FILE`/`SOURCE_FOLDER`, `dictionary_loaded`, `file_getfilepointer`, `file_readfile`. Només queden config-folder i resource-folder getters/setters.
- Targets release a [Makefile](Makefile) (`_linux_release`/`_windows_release`/`_macos_release`) depenen de `pack` i copien `resources.pack` en lloc de `data/`. WASM intacte (`--preload-file data@/data`).
- `enable_fallback = false` a Release natiu (`NDEBUG && !__EMSCRIPTEN__`): el pack és obligatori. Debug i WASM mantenen el fallback actiu.
### External Libraries (`source/external/`) ### External Libraries (`source/external/`)
- `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()` - `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
@@ -170,20 +267,41 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
### Data Assets (`data/`) ### Data Assets (`data/`)
- `*.gif`, `*.ogg` — Original game assets (**do not modify**) - `gfx/` — Original game GIFs (**do not modify content**): `frames.gif`/`frames2.gif` (sprite sheet del joc), `logo.gif`/`logo_new.gif` (intros), `menu.gif`/`menu2.gif`, `intro.gif`/`intro2.gif`/`intro3.gif` (slides), `ffase.gif` (banner nivells), `final.gif`/`finals.gif` (crèdits), `gameover.gif`, `tomba1.gif`/`tomba2.gif` (escena secreta)
- `music/` — 8 pistes OGG originals amb noms temàtics: `mort.ogg` (game over), `secreta.ogg` (escena secreta + piràmide 6), `menu.ogg` (menú + intros), `banner.ogg` (banner de fase), `final.ogg` (slides finals + crèdits), `piramide_1_4_5.ogg` (gameplay default), `piramide_2.ogg`, `piramide_3.ogg`
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents) - `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
- `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl` - `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
- `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart - `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart
- `ui/` — Reserved for future UI graphics
### Known Issues & Technical Debt ### Known Issues & Technical Debt
1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround 1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
2. **Cheats are broken (`reviu`, `alone`, `obert`)**: `JI_CheatActivated` in [jinput.cpp:46](source/core/jail/jinput.cpp#L46) compares `SDL_Scancode` values (e.g. `SDL_SCANCODE_R`=21) against ASCII chars (`'r'`=114). They never match. Regression from SDL3 migration. Fix requires either scancode→char conversion in `JI_moveCheats` or storing chars directly. 2. ~~**Cheats are broken (`reviu`, `alone`, `obert`)**~~: Fixed. `JI_moveCheats` tradueix `SDL_Scancode` → ASCII via `scancode_to_ascii` abans de ficar-los al buffer ([jinput.cpp:32-37, 55-61](source/core/jail/jinput.cpp#L32-L61)), i `JI_CheatActivated` compara ASCII amb ASCII.
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX. 3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
4. ~~**Raw `malloc`/`free` in gameplay structs**~~: Majoritàriament arreglat. `Sprite`/`Entitat` usen `std::vector<Frame>` i `std::vector<Animacio>` ([sprite.hpp](source/game/sprite.hpp)). `jfile.cpp` ja no té el global `scratch[255]` (substituït per `thread_local std::string`). L'API `file_getfilebuffer` (que tornava raw `char*` amb `malloc`) s'ha substituït per `file_readfile` que retorna `std::vector<char>` RAII — elimina els leaks silenciosos que hi havia a `JD8_LoadPalette`, `ModuleGame::Go()` i `scenes::playMusic`. Queda `jail_audio.hpp` barrejant `new`/`malloc`/`SDL_malloc` de forma pairada i correcta (no leak), pendent de polir amb `std::unique_ptr` quan toque.
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. Migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 110) + `ModuleGame` també tick-based (Phase A). Tot `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()` eliminat. Els fades bloquejants `JD8_FadeOut`/`JD8_FadeToPal` també eliminats (Phase B.2): només queda l'API tick-step `JD8_FadeStart*` + `JD8_FadeTickStep`, encapsulada pel wrapper `scenes::PaletteFade`. ModuleGame té fases `FadingIn`/`FadingOut` pròpies.
6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::iterate()`. Ported from the `jaildoctors_dilemma` codebase.
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5 via cooperative `GameFiber`; **eliminated entirely in Phase B.2**. `JD8_Flip()` ja no fa yield — només converteix `screen``pixel_data`. Director posseeix l'estat d'escena (`current_scene_`, `game_state_`) i crida `scene->tick()` directament des d'`iterate()`. Fitxers `source/core/system/fiber.{hpp,cpp}` esborrats. Zero threads, zero mutex, zero fibers.
8. ~~**`ModuleSequence` legacy dispatcher**~~: Eliminated in Step 10. Era el vell switch per `num_piramide`, ara substituït per `SceneRegistry::tryCreate()` i dispatch directe des de `Director::iterate()`. `modulesequence.{hpp,cpp}` esborrats.
### WebAssembly Build
`make wasm` genera el build WASM via Docker (`emscripten/emsdk:latest`) i copia els 3 fitxers (`.js`/`.wasm`/`.data`) a `maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/`, amb un `ssh maverick './deploy.sh'` final. Output local a `dist/wasm/`.
**Diferències respecte build natiu** (a [CMakeLists.txt](CMakeLists.txt) dins `if(EMSCRIPTEN)`):
- SDL3 compilat des de font via `FetchContent` (no hi ha paquet de sistema).
- Shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2).
- `sdl3gpu_shader.cpp` exclòs dels sources — el fallback `SDL_Renderer` fa tota la presentació.
- [screen.cpp](source/core/rendering/screen.cpp) guarda `#ifndef NO_SHADERS` al voltant de l'include i les crides a `SDL3GPUShader` directes. La resta del codi va via interfície base `ShaderBackend`.
- Link flags: `--preload-file data@/data`, `-fexceptions`, `-sALLOW_MEMORY_GROWTH=1`, `-sMAX_WEBGL_VERSION=2`, `-sINITIAL_MEMORY=67108864`, `-sASSERTIONS=1`, `-sASYNCIFY=1`.
- Defines: `EMSCRIPTEN_BUILD`, `NO_SHADERS`.
**Filesystem**: MEMFS default — no persistent entre recàrregues. `file_setconfigfolder` té fallbacks robustos (`getpwuid``getenv("HOME")``/tmp`) perquè no pete quan emscripten no té `/etc/passwd`. La config es carrega per defecte cada vegada. IDBFS pendent si mai volguéssem persistència a web.
### Pending / Ideas for Later ### Pending / Ideas for Later
- **Sound effects**: infraestructura `JA_PlaySound*`/`JA_LoadSound` ja preparada. Cablejar events de gameplay (col·lisió momia, mort, recollir objecte, trencar tomba, cheat activat). Menus SONS/VOL SONS ja controlen el volum del pool.
- **IDBFS persistence a WASM**: montar `/home/web_user/.config` com a IDBFS a l'init i `FS.syncfs` després de cada save. Opcional — ara per ara la config no persistix entre recàrregues de pàgina.
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping). - **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed). - **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files. - **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
@@ -191,6 +309,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead. - **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits. - **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded. - **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
- **jail_audio `JA_Sound_t` RAII**: `JA_Music_t` ja està net (vector + string), però `JA_Sound_t` encara usa `Uint8*` via `SDL_LoadWAV` out-param. Petit polish per a completar la coherència RAII.
### Previously Fixed (kept for reference) ### Previously Fixed (kept for reference)
@@ -212,4 +331,4 @@ Director loop uses `FRAME_MS_VSYNC = 16` (60 FPS) or `FRAME_MS_NO_VSYNC = 4` (~2
Init order: `file_setconfigfolder``Options::load``Locale::load("locale/ca.yaml")``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Options::applyAudio()``Overlay::init``Menu::init``Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save``Director::destroy``Menu::destroy``Overlay::destroy``JA_Quit``JD8_Quit``Screen::destroy``JG_Finalize`. Init order: `file_setconfigfolder``Options::load``Locale::load("locale/ca.yaml")``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Options::applyAudio()``Overlay::init``Menu::init``Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save``Director::destroy``Menu::destroy``Overlay::destroy``JA_Quit``JD8_Quit``Screen::destroy``JG_Finalize`.
The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) now lives inside `Director::gameThreadFunc()`, running on the game thread. The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) lives inside `gameFiberEntry()` in an anonymous namespace in [director.cpp](source/core/system/director.cpp), invoked as the entry of the cooperative fiber (single-threaded).
+224 -16
View File
@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
project(aee VERSION 1.00) project(aee VERSION 1.00)
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
endif()
# Estándar de C++ # Estándar de C++
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -10,18 +15,50 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
# Exportar comandos de compilación para herramientas de análisis # Exportar comandos de compilación para herramientas de análisis
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓ AUTOMÀTICA DE VERSIÓ ---
# Si GIT_HASH ve de fora (p. ex. el Makefile via -DGIT_HASH=xxx), l'usem tal
# qual. Això evita problemes amb Docker/emscripten on git avorta per
# "dubious ownership" al volum muntat. En builds locals sense -DGIT_HASH
# resolem ací executant git directament.
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif()
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
# --- LISTA EXPLÍCITA DE FUENTES --- # --- LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES set(APP_SOURCES
# Core - Motor original "Jail" (no tocar gameplay) # Core - Motor original "Jail" (no tocar gameplay)
source/core/jail/jail_audio.cpp
source/core/jail/jdraw8.cpp source/core/jail/jdraw8.cpp
source/core/jail/jfile.cpp source/core/jail/jfile.cpp
source/core/jail/jgame.cpp source/core/jail/jgame.cpp
source/core/jail/jinput.cpp source/core/jail/jinput.cpp
# Core - Audio (wrapper canònic compartit amb la resta de projectes)
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# Core - Locale (nova capa) # Core - Locale (nova capa)
source/core/locale/locale.cpp source/core/locale/locale.cpp
# Core - Resources (pack binari AEE1 + cache d'assets precarregats)
source/core/resources/resource_pack.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_list.cpp
source/core/resources/resource_cache.cpp
# Core - Capa de presentación (nueva) # Core - Capa de presentación (nueva)
source/core/rendering/menu.cpp source/core/rendering/menu.cpp
source/core/rendering/overlay.cpp source/core/rendering/overlay.cpp
@@ -34,12 +71,32 @@ set(APP_SOURCES
# Core - Input (nova capa) # Core - Input (nova capa)
source/core/input/gamepad.cpp source/core/input/gamepad.cpp
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/input/key_config.cpp
source/core/input/key_remap.cpp source/core/input/key_remap.cpp
source/core/input/mouse.cpp source/core/input/mouse.cpp
# Core - System (nova capa) # Core - System (nova capa)
source/core/system/director.cpp source/core/system/director.cpp
# Scenes (cinemàtiques i menús reescrits)
source/game/scenes/timeline.cpp
source/game/scenes/sprite_mover.cpp
source/game/scenes/frame_animator.cpp
source/game/scenes/palette_fade.cpp
source/game/scenes/surface_handle.cpp
source/game/scenes/scene_registry.cpp
source/game/scenes/scene_utils.cpp
source/game/scenes/boot_loader_scene.cpp
source/game/scenes/mort_scene.cpp
source/game/scenes/banner_scene.cpp
source/game/scenes/menu_scene.cpp
source/game/scenes/intro_new_logo_scene.cpp
source/game/scenes/intro_scene.cpp
source/game/scenes/intro_sprites_scene.cpp
source/game/scenes/slides_scene.cpp
source/game/scenes/credits_scene.cpp
source/game/scenes/secreta_scene.cpp
# Game # Game
source/game/options.cpp source/game/options.cpp
source/game/bola.cpp source/game/bola.cpp
@@ -48,7 +105,6 @@ set(APP_SOURCES
source/game/mapa.cpp source/game/mapa.cpp
source/game/marcador.cpp source/game/marcador.cpp
source/game/modulegame.cpp source/game/modulegame.cpp
source/game/modulesequence.cpp
source/game/momia.cpp source/game/momia.cpp
source/game/prota.cpp source/game/prota.cpp
source/game/sprite.cpp source/game/sprite.cpp
@@ -63,8 +119,22 @@ set(APP_SOURCES
# Configuración de SDL3 # Configuración de SDL3
# En macOS bundle mode usamos el xcframework (universal arm64+x86_64). # En macOS bundle mode usamos el xcframework (universal arm64+x86_64).
# En el resto de casos, o en macOS sin bundle, usamos SDL3 del sistema via find_package. # En emscripten compilamos SDL3 desde source con FetchContent (no hi ha paquet de sistema).
if(APPLE AND MACOS_BUNDLE) # En el resto de casos, usamos SDL3 del sistema via find_package.
if(EMSCRIPTEN)
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3: compilat des de source per a Emscripten (FetchContent)")
elseif(APPLE AND MACOS_BUNDLE)
set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64") set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64")
message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})") message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})")
else() else()
@@ -72,25 +142,23 @@ else()
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}") message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif() endif()
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal) --- # --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal, Emscripten no suporta SDL3 GPU) ---
if(NOT APPLE) if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc) find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders") set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu") set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
set(ALL_SHADER_HEADERS set(ALL_SHADER_HEADERS
"${HEADERS_DIR}/postfx_vert_spv.h" "${HEADERS_DIR}/postfx_vert_spv.h"
"${HEADERS_DIR}/postfx_frag_spv.h" "${HEADERS_DIR}/postfx_frag_spv.h"
"${HEADERS_DIR}/upscale_frag_spv.h" "${HEADERS_DIR}/upscale_frag_spv.h"
"${HEADERS_DIR}/downscale_frag_spv.h"
"${HEADERS_DIR}/crtpi_frag_spv.h" "${HEADERS_DIR}/crtpi_frag_spv.h"
) )
set(ALL_SHADER_SOURCES set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/postfx.vert" "${SHADERS_DIR}/postfx.vert"
"${SHADERS_DIR}/postfx.frag" "${SHADERS_DIR}/postfx.frag"
"${SHADERS_DIR}/upscale.frag" "${SHADERS_DIR}/upscale.frag"
"${SHADERS_DIR}/downscale.frag"
"${SHADERS_DIR}/crtpi_frag.glsl" "${SHADERS_DIR}/crtpi_frag.glsl"
) )
@@ -120,21 +188,38 @@ if(NOT APPLE)
endforeach() endforeach()
message(STATUS "glslc no trobat — usant headers SPIR-V precompilats") message(STATUS "glslc no trobat — usant headers SPIR-V precompilats")
endif() endif()
elseif(EMSCRIPTEN)
message(STATUS "Emscripten: shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2)")
else() else()
message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)") message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)")
endif() endif()
# --- EJECUTABLE --- # --- EJECUTABLE ---
add_executable(${PROJECT_NAME} ${APP_SOURCES}) # A emscripten excloïm sdl3gpu_shader.cpp — SDL3 GPU no suporta WebGL2, i el
# fallback SDL_Renderer de Screen (amb NO_SHADERS) fa tota la presentació.
if(EMSCRIPTEN)
set(APP_SOURCES_WASM ${APP_SOURCES})
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
endif()
# Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc) # Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc)
if(NOT APPLE AND GLSLC_EXE) if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders) add_dependencies(${PROJECT_NAME} shaders)
endif() endif()
# --- DIRECTORIOS DE INCLUSIÓN --- # --- DIRECTORIOS DE INCLUSIÓN ---
target_include_directories(${PROJECT_NAME} PUBLIC target_include_directories(${PROJECT_NAME} PUBLIC
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
# Capçaleres de tercers a source/external/ — tractades com a sistema per
# silenciar warnings (gif.h, etc.) que no controlem.
target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC
"${CMAKE_SOURCE_DIR}/source/external"
) )
# Enlazar SDL3 # Enlazar SDL3
@@ -153,21 +238,93 @@ else()
endif() endif()
# --- FLAGS DE COMPILACIÓN --- # --- FLAGS DE COMPILACIÓN ---
target_compile_options(${PROJECT_NAME} PRIVATE -Wall) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>) target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
# --- CONFIGURACIÓN POR PLATAFORMA --- # --- CONFIGURACIÓN POR PLATAFORMA ---
if(WIN32) if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32) target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# -fexceptions: SDL3 i fkyaml llancen std::exception; sense això, `throw`
# acaba en `abort()`. També requerit al link per congruència ABI.
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY permet que Emscripten gestione yields durant la precarga
# d'assets. El main loop del joc ja usa SDL3 Callback API i no depén
# d'Asyncify — però el preloader del `.data` sí.
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
endif() endif()
# Ejecutable en la raíz del proyecto # --- EINA STANDALONE: pack_resources ---
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}) # Executable auxiliar que empaqueta `data/` a `resources.pack` (format AEE1).
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
# cmake --build build --target pack_resources
# Després executar: ./build/pack_resources data resources.pack
if(NOT EMSCRIPTEN)
add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- CLANG-FORMAT TARGETS --- # --- Regeneració automàtica de resources.pack ---
# Cada `cmake --build build` torna a empaquetar `data/` si algun fitxer ha
# canviat. Evita debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake
# a re-globbar a la pròxima invocació (recull fitxers nous afegits a data/).
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- CÒPIA DE gamecontrollerdb.txt AL COSTAT DEL BINARI ---
# SDL_AddGamepadMappingsFromFile només llegeix del filesystem real (no del
# pack), així que el fitxer ha de viure al directori del binari. Es copia
# només si existeix per no fallar la build d'algú que encara no ha fet
# `make controllerdb`.
if(EXISTS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt")
set(CONTROLLER_DB "${CMAKE_BINARY_DIR}/gamecontrollerdb.txt")
add_custom_command(
OUTPUT ${CONTROLLER_DB}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
"${CONTROLLER_DB}"
DEPENDS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
COMMENT "Copiant gamecontrollerdb.txt → build/"
VERBATIM
)
add_custom_target(controller_db ALL DEPENDS ${CONTROLLER_DB})
add_dependencies(${PROJECT_NAME} controller_db)
endif()
endif()
# --- STATIC ANALYSIS TARGETS ---
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format) find_program(CLANG_FORMAT_EXE NAMES clang-format)
find_program(CPPCHECK_EXE NAMES cppcheck)
# Recopilar todos los archivos fuente para formateo (excluir external/) # Recopilar todos los archivos fuente (excluir external/)
file(GLOB_RECURSE ALL_SOURCE_FILES file(GLOB_RECURSE ALL_SOURCE_FILES
"${CMAKE_SOURCE_DIR}/source/*.cpp" "${CMAKE_SOURCE_DIR}/source/*.cpp"
"${CMAKE_SOURCE_DIR}/source/*.hpp" "${CMAKE_SOURCE_DIR}/source/*.hpp"
@@ -175,6 +332,35 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
) )
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*") list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
# Targets de clang-tidy
if(CLANG_TIDY_EXE)
add_custom_target(tidy
COMMAND ${CLANG_TIDY_EXE}
-p ${CMAKE_BINARY_DIR}
${CLANG_TIDY_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-tidy..."
)
add_custom_target(tidy-fix
COMMAND ${CLANG_TIDY_EXE}
-p ${CMAKE_BINARY_DIR}
--fix
${CLANG_TIDY_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-tidy with fixes..."
)
else()
message(STATUS "clang-tidy no encontrado - targets 'tidy' y 'tidy-fix' no disponibles")
endif()
# Targets de clang-format
if(CLANG_FORMAT_EXE) if(CLANG_FORMAT_EXE)
add_custom_target(format add_custom_target(format
COMMAND ${CLANG_FORMAT_EXE} COMMAND ${CLANG_FORMAT_EXE}
@@ -195,3 +381,25 @@ if(CLANG_FORMAT_EXE)
else() else()
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles") message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
endif() endif()
# Target de cppcheck
if(CPPCHECK_EXE)
add_custom_target(cppcheck
COMMAND ${CPPCHECK_EXE}
--enable=warning,style,performance,portability
--std=c++20
--language=c++
--inline-suppr
--suppress=missingIncludeSystem
--suppress=toomanyconfigs
--suppress=*:*/source/external/*
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
--quiet
-I ${CMAKE_SOURCE_DIR}/source
${CPPCHECK_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running cppcheck..."
)
else()
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
endif()
+293 -52
View File
@@ -2,13 +2,25 @@
# DIRECTORIES # DIRECTORIES
# ============================================================================== # ==============================================================================
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST))) DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_BIN := $(addsuffix /, $(DIR_ROOT)) BUILDDIR := build
# ==============================================================================
# TOOLS
# ==============================================================================
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu/spv
ifeq ($(OS),Windows_NT)
GLSLC := $(shell where glslc 2>NUL)
else
GLSLC := $(shell command -v glslc 2>/dev/null)
endif
# ============================================================================== # ==============================================================================
# TARGET NAMES # TARGET NAMES
# ============================================================================== # ==============================================================================
TARGET_NAME := aee TARGET_NAME := aee
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME) TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
APP_NAME := Aventures en Egipte APP_NAME := Aventures en Egipte
DIST_DIR := dist DIST_DIR := dist
RELEASE_FOLDER := dist/_tmp RELEASE_FOLDER := dist/_tmp
@@ -18,9 +30,23 @@ RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
# VERSION (extracted from defines.hpp) # VERSION (extracted from defines.hpp)
# ============================================================================== # ==============================================================================
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
VERSION := v$(shell powershell -Command "(Select-String -Path 'source/game/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value") VERSION := $(shell powershell -Command "(Select-String -Path 'source/game/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/') VERSION := $(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif endif
# ============================================================================== # ==============================================================================
@@ -35,11 +61,15 @@ endif
# WINDOWS-SPECIFIC VARIABLES # WINDOWS-SPECIFIC VARIABLES
# ============================================================================== # ==============================================================================
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME) WIN_TARGET_FILE := $(BUILDDIR)/$(APP_NAME)
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME) WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
else else
WIN_TARGET_FILE := $(TARGET_FILE) WIN_TARGET_FILE := $(TARGET_FILE)
WIN_RELEASE_FILE := $(RELEASE_FILE) WIN_RELEASE_FILE := $(RELEASE_FILE)
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
endif endif
# ============================================================================== # ==============================================================================
@@ -65,40 +95,95 @@ else
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
endif endif
# ==============================================================================
# CMAKE GENERATOR (usa Ninja si está disponible; si no, MinGW Makefiles en
# Windows / generador por defecto en Linux/macOS). Ninja paraleliza mejor.
# ==============================================================================
ifeq ($(OS),Windows_NT)
# Dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i "NUL" NO és
# dispositiu — un redirect "2>NUL" crearia un fitxer literal anomenat
# NUL al cwd. Detectem MSYSTEM per usar /dev/null en aquests entorns.
ifneq ($(MSYSTEM),)
NULDEV := /dev/null
else
NULDEV := NUL
endif
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN := -G "MinGW Makefiles"
endif
else
HAS_NINJA := $(shell ninja --version 2>/dev/null)
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN :=
endif
endif
# ============================================================================== # ==============================================================================
# COMPILACIÓN CON CMAKE # COMPILACIÓN CON CMAKE
# ============================================================================== # ==============================================================================
all: all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
debug: debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
run: all
@./$(TARGET_FILE)
run-debug: debug
@./$(TARGET_FILE)
clean:
@rm -rf $(BUILDDIR)
rebuild: clean all
# ==============================================================================
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
# ==============================================================================
compile-shaders:
ifdef GLSLC
@cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
else
@echo "glslc no encontrado - asegurate de que los headers SPIR-V precompilados existen"
endif
# Empaqueta data/ a resources.pack (format AEE1). Build previ de l'eina + execució.
pack:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data build/resources.pack
# ============================================================================== # ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO) # RELEASE AUTOMÁTICO (detecta SO)
# ============================================================================== # ==============================================================================
release: release:
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
@"$(MAKE)" _windows_release @"$(MAKE)" _windows-release
else else
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
@$(MAKE) _macos_release @$(MAKE) _macos-release
else else
@$(MAKE) _linux_release @$(MAKE) _linux-release
endif endif
endif endif
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE) # COMPILACIÓN PARA WINDOWS (RELEASE)
# ============================================================================== # ==============================================================================
_windows_release: _windows-release: pack
@echo off @echo off
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER' # Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@@ -106,13 +191,13 @@ _windows_release:
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}" @powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}" @powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros # Copia ficheros (resources.pack substitueix la carpeta data/)
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse" @powershell -Command "Copy-Item 'build/resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE).exe'" @powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip # Crea el fichero .zip
@@ -126,15 +211,31 @@ _windows_release:
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE) # COMPILACIÓN PARA MACOS (RELEASE)
# ============================================================================== # ==============================================================================
_macos_release: _macos-release: pack
@echo "Creando release para macOS - Version: $(VERSION)" @echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario # Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg) # con brew; si brew tampoco está, indica el comando exacto al usuario.
@command -v create-dmg >/dev/null 2>&1 || { \
# Compila la versión para procesadores Intel con cmake echo ""; \
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON echo "============================================"; \
@cmake --build build/intel echo " Falta la dependencia: create-dmg"; \
echo "============================================"; \
if command -v brew >/dev/null 2>&1; then \
echo " Instalando con: brew install create-dmg"; \
brew install create-dmg || { \
echo ""; \
echo " ERROR: 'brew install create-dmg' ha fallado."; \
echo " Ejecuta el comando manualmente y vuelve a probar."; \
exit 1; \
}; \
else \
echo " Homebrew no está instalado."; \
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
echo " brew install create-dmg"; \
exit 1; \
fi; \
}
# Elimina datos de compilaciones anteriores # Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
@@ -148,8 +249,8 @@ _macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros # Copia carpetas y ficheros (resources.pack substitueix la carpeta data/)
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp build/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks" cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
@@ -163,31 +264,50 @@ _macos_release:
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \ sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist" sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Copia el ejecutable Intel al bundle # Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" # DMG Intel y continúa con la build de Apple Silicon).
@echo ""
# Firma la aplicación @echo "============================================"
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app" @echo " Compilando version Intel (x86_64)"
@echo "============================================"
# Empaqueta el .dmg de la versión Intel con create-dmg @if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
@echo "Creando DMG Intel con iconos de 96x96..." -DCMAKE_OSX_ARCHITECTURES=x86_64 \
create-dmg \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
--volname "$(APP_NAME)" \ -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
--window-pos 200 120 \ && cmake --build build/intel; then \
--window-size 720 300 \ cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
--icon-size 96 \ codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
--text-size 12 \ echo "Creando DMG Intel con iconos de 96x96..."; \
--icon "$(APP_NAME).app" 278 102 \ create-dmg \
--icon "LICENSE" 441 102 \ --volname "$(APP_NAME)" \
--icon "README.md" 604 102 \ --window-pos 200 120 \
--app-drop-link 115 102 \ --window-size 720 300 \
--hide-extension "$(APP_NAME).app" \ --icon-size 96 \
"$(MACOS_INTEL_RELEASE)" \ --text-size 12 \
"$(RELEASE_FOLDER)" || true --icon "$(APP_NAME).app" 278 102 \
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" --icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true; \
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
else \
echo ""; \
echo "============================================"; \
echo " WARNING: la build Intel ha fallado."; \
echo " Se omite el DMG Intel y se continúa con"; \
echo " la build de Apple Silicon."; \
echo "============================================"; \
echo ""; \
fi
# Compila la versión para procesadores Apple Silicon con cmake # Compila la versión para procesadores Apple Silicon con cmake
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON @echo ""
@echo "============================================"
@echo " Compilando version Apple Silicon (arm64)"
@echo "============================================"
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/arm @cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
@@ -217,22 +337,64 @@ _macos_release:
$(RMDIR) build/arm $(RMDIR) build/arm
$(RMFILE) "$(DIST_DIR)"/rw.* $(RMFILE) "$(DIST_DIR)"/rw.*
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker amb emscripten/emsdk)
# ==============================================================================
# Genera aee.{html,js,wasm,data} a dist/wasm/. Es pot provar servint amb un
# servidor HTTP local (els navegadors no carreguen `file://` WASM):
# cd dist/wasm && python3 -m http.server 8000
# # després obrir http://localhost:8000/aee.html
wasm:
@echo "Creando release para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
@$(MKDIR) "$(DIST_DIR)/wasm"
@cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/$(TARGET_NAME).html"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm-debug/.
wasm-debug:
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm-debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm-debug"
@$(MKDIR) "$(DIST_DIR)/wasm-debug"
@cp build/wasm-debug/$(TARGET_NAME).html $(DIST_DIR)/wasm-debug/
@cp build/wasm-debug/$(TARGET_NAME).js $(DIST_DIR)/wasm-debug/
@cp build/wasm-debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm-debug/
@cp build/wasm-debug/$(TARGET_NAME).data $(DIST_DIR)/wasm-debug/
@echo "Output: $(DIST_DIR)/wasm-debug/$(TARGET_NAME).html"
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE) # COMPILACIÓN PARA LINUX (RELEASE)
# ============================================================================== # ==============================================================================
_linux_release: _linux-release: pack
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe) # Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
$(MKDIR) "$(RELEASE_FOLDER)" $(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros # Copia ficheros (resources.pack substitueix la carpeta data/)
cp -r data "$(RELEASE_FOLDER)" cp build/resources.pack "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)" cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)" cp README.md "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)" cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
@@ -247,4 +409,83 @@ _linux_release:
# Elimina la carpeta temporal # Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
.PHONY: all debug release _windows_release _linux_release _macos_release # ==============================================================================
# ==============================================================================
# CODE QUALITY (delegados a cmake)
# ==============================================================================
format:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format
format-check:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format-check
tidy:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy
tidy-fix:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy-fix
cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# ==============================================================================
# GIT HOOKS
# ==============================================================================
hooks-install:
@git config core.hooksPath .githooks
@echo "Git hooks activats: $(shell pwd)/.githooks"
# DESCÀRREGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@echo "Descarregant gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualitzat"
# ==============================================================================
# AJUDA
# ==============================================================================
help:
@echo "Makefile per a Aventures en Egipte"
@echo "Comandes disponibles:"
@echo ""
@echo " Compilacio:"
@echo " make - Compilar amb cmake (Release)"
@echo " make debug - Compilar amb cmake (Debug)"
@echo ""
@echo " Execucio:"
@echo " make run - Compilar (Release) i executar"
@echo " make run-debug - Compilar (Debug) i executar"
@echo ""
@echo " Release:"
@echo " make release - Crear release (detecta SO automaticament)"
@echo " make wasm - Build WebAssembly (requereix Docker) + deploy a maverick"
@echo " make wasm-debug - Build WebAssembly Debug local (sense deploy)"
@echo ""
@echo " Eines:"
@echo " make compile-shaders - Compilar shaders SPIR-V"
@echo " make pack - Empaquetar data/ a $(BUILDDIR)/resources.pack (format AEE1)"
@echo " make controllerdb - Actualitzar gamecontrollerdb.txt des de SDL_GameControllerDB"
@echo ""
@echo " Qualitat de codi:"
@echo " make format - Formatar codi amb clang-format"
@echo " make format-check - Verificar format sense modificar"
@echo " make tidy - Anàlisi estàtic amb clang-tidy"
@echo " make tidy-fix - Anàlisi estàtic amb auto-fix"
@echo " make cppcheck - Anàlisi estàtic amb cppcheck"
@echo ""
@echo " Altres:"
@echo " make clean - Esborrar carpeta $(BUILDDIR)/"
@echo " make rebuild - clean + all"
@echo " make hooks-install - Activar git hooks del projecte"
@echo " make help - Mostrar esta ajuda"
@echo ""
@echo " Versio actual: $(VERSION) ($(GIT_HASH))"
.PHONY: all debug run run-debug clean rebuild pack release wasm wasm-debug _windows-release _linux-release _macos-release compile-shaders controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
+52
View File
@@ -0,0 +1,52 @@
# Aventures En Egipte - Asset Configuration
# Loaded at boot by Resource::List, decoded incrementally by Resource::Cache.
# Paths are relative to the resource pack root (i.e. relative to ./data/ in dev).
assets:
# FONTS - bitmap font for the overlay (8bithud)
fonts:
BITMAP:
- fonts/8bithud.gif
FONT:
- fonts/8bithud.fnt
# LOCALE - UI strings
locale:
DATA:
- locale/ca.yaml
# INPUT - UI key bindings defaults
input:
DATA:
- input/keys.yaml
# MUSIC - 8 OGG tracks
music:
MUSIC:
- music/banner.ogg
- music/final.ogg
- music/menu.ogg
- music/mort.ogg
- music/piramide_1_4_5.ogg
- music/piramide_2.ogg
- music/piramide_3.ogg
- music/secreta.ogg
# GFX - 14 GIFs (sprites + cinematic backgrounds)
gfx:
BITMAP:
- gfx/ffase.gif
- gfx/final.gif
- gfx/finals.gif
- gfx/frames.gif
- gfx/frames2.gif
- gfx/gameover.gif
- gfx/intro.gif
- gfx/intro2.gif
- gfx/intro3.gif
- gfx/logo.gif
- gfx/logo_new.gif
- gfx/menu.gif
- gfx/menu2.gif
- gfx/tomba1.gif
- gfx/tomba2.gif
-234
View File
@@ -1,234 +0,0 @@
/*
crt-pi - A Raspberry Pi friendly CRT shader.
Copyright (C) 2015-2016 davej
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
Notes:
This shader is designed to work well on Raspberry Pi GPUs (i.e. 1080P @ 60Hz on a game with a 4:3 aspect ratio). It pushes the Pi's GPU hard and enabling some features will slow it down so that it is no longer able to match 1080P @ 60Hz. You will need to overclock your Pi to the fastest setting in raspi-config to get the best results from this shader: 'Pi2' for Pi2 and 'Turbo' for original Pi and Pi Zero. Note: Pi2s are slower at running the shader than other Pis, this seems to be down to Pi2s lower maximum memory speed. Pi2s don't quite manage 1080P @ 60Hz - they drop about 1 in 1000 frames. You probably won't notice this, but if you do, try enabling FAKE_GAMMA.
SCANLINES enables scanlines. You'll almost certainly want to use it with MULTISAMPLE to reduce moire effects. SCANLINE_WEIGHT defines how wide scanlines are (it is an inverse value so a higher number = thinner lines). SCANLINE_GAP_BRIGHTNESS defines how dark the gaps between the scan lines are. Darker gaps between scan lines make moire effects more likely.
GAMMA enables gamma correction using the values in INPUT_GAMMA and OUTPUT_GAMMA. FAKE_GAMMA causes it to ignore the values in INPUT_GAMMA and OUTPUT_GAMMA and approximate gamma correction in a way which is faster than true gamma whilst still looking better than having none. You must have GAMMA defined to enable FAKE_GAMMA.
CURVATURE distorts the screen by CURVATURE_X and CURVATURE_Y. Curvature slows things down a lot.
By default the shader uses linear blending horizontally. If you find this too blury, enable SHARPER.
BLOOM_FACTOR controls the increase in width for bright scanlines.
MASK_TYPE defines what, if any, shadow mask to use. MASK_BRIGHTNESS defines how much the mask type darkens the screen.
*/
#pragma parameter CURVATURE_X "Screen curvature - horizontal" 0.10 0.0 1.0 0.01
#pragma parameter CURVATURE_Y "Screen curvature - vertical" 0.15 0.0 1.0 0.01
#pragma parameter MASK_BRIGHTNESS "Mask brightness" 0.70 0.0 1.0 0.01
#pragma parameter SCANLINE_WEIGHT "Scanline weight" 6.0 0.0 15.0 0.1
#pragma parameter SCANLINE_GAP_BRIGHTNESS "Scanline gap brightness" 0.12 0.0 1.0 0.01
#pragma parameter BLOOM_FACTOR "Bloom factor" 1.5 0.0 5.0 0.01
#pragma parameter INPUT_GAMMA "Input gamma" 2.4 0.0 5.0 0.01
#pragma parameter OUTPUT_GAMMA "Output gamma" 2.2 0.0 5.0 0.01
// Haven't put these as parameters as it would slow the code down.
#define SCANLINES
#define MULTISAMPLE
#define GAMMA
//#define FAKE_GAMMA
#define CURVATURE
//#define SHARPER
// MASK_TYPE: 0 = none, 1 = green/magenta, 2 = trinitron(ish)
#define MASK_TYPE 1
#ifdef GL_ES
#define COMPAT_PRECISION mediump
precision mediump float;
#else
#define COMPAT_PRECISION
#endif
#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float CURVATURE_X;
uniform COMPAT_PRECISION float CURVATURE_Y;
uniform COMPAT_PRECISION float MASK_BRIGHTNESS;
uniform COMPAT_PRECISION float SCANLINE_WEIGHT;
uniform COMPAT_PRECISION float SCANLINE_GAP_BRIGHTNESS;
uniform COMPAT_PRECISION float BLOOM_FACTOR;
uniform COMPAT_PRECISION float INPUT_GAMMA;
uniform COMPAT_PRECISION float OUTPUT_GAMMA;
#else
#define CURVATURE_X 0.25
#define CURVATURE_Y 0.45
#define MASK_BRIGHTNESS 0.70
#define SCANLINE_WEIGHT 6.0
#define SCANLINE_GAP_BRIGHTNESS 0.12
#define BLOOM_FACTOR 1.5
#define INPUT_GAMMA 2.4
#define OUTPUT_GAMMA 2.2
#endif
/* COMPATIBILITY
- GLSL compilers
*/
//uniform vec2 TextureSize;
#if defined(CURVATURE)
varying vec2 screenScale;
#endif
varying vec2 TEX0;
varying float filterWidth;
#if defined(VERTEX)
//uniform mat4 MVPMatrix;
//attribute vec4 VertexCoord;
//attribute vec2 TexCoord;
//uniform vec2 InputSize;
//uniform vec2 OutputSize;
void main()
{
#if defined(CURVATURE)
screenScale = vec2(1.0, 1.0); //TextureSize / InputSize;
#endif
filterWidth = (768.0 / 240.0) / 3.0;
TEX0 = vec2(gl_MultiTexCoord0.x, 1.0-gl_MultiTexCoord0.y)*1.0001;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#elif defined(FRAGMENT)
uniform sampler2D Texture;
#if defined(CURVATURE)
vec2 Distort(vec2 coord)
{
vec2 CURVATURE_DISTORTION = vec2(CURVATURE_X, CURVATURE_Y);
// Barrel distortion shrinks the display area a bit, this will allow us to counteract that.
vec2 barrelScale = 1.0 - (0.23 * CURVATURE_DISTORTION);
coord *= screenScale;
coord -= vec2(0.5);
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (CURVATURE_DISTORTION * rsq);
coord *= barrelScale;
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5)
coord = vec2(-1.0); // If out of bounds, return an invalid value.
else
{
coord += vec2(0.5);
coord /= screenScale;
}
return coord;
}
#endif
float CalcScanLineWeight(float dist)
{
return max(1.0-dist*dist*SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS);
}
float CalcScanLine(float dy)
{
float scanLineWeight = CalcScanLineWeight(dy);
#if defined(MULTISAMPLE)
scanLineWeight += CalcScanLineWeight(dy-filterWidth);
scanLineWeight += CalcScanLineWeight(dy+filterWidth);
scanLineWeight *= 0.3333333;
#endif
return scanLineWeight;
}
void main()
{
vec2 TextureSize = vec2(320.0, 240.0);
#if defined(CURVATURE)
vec2 texcoord = Distort(TEX0);
if (texcoord.x < 0.0)
gl_FragColor = vec4(0.0);
else
#else
vec2 texcoord = TEX0;
#endif
{
vec2 texcoordInPixels = texcoord * TextureSize;
#if defined(SHARPER)
vec2 tempCoord = floor(texcoordInPixels) + 0.5;
vec2 coord = tempCoord / TextureSize;
vec2 deltas = texcoordInPixels - tempCoord;
float scanLineWeight = CalcScanLine(deltas.y);
vec2 signs = sign(deltas);
deltas.x *= 2.0;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5;
deltas.y *= 8.0;
deltas /= TextureSize;
deltas *= signs;
vec2 tc = coord + deltas;
#else
float tempY = floor(texcoordInPixels.y) + 0.5;
float yCoord = tempY / TextureSize.y;
float dy = texcoordInPixels.y - tempY;
float scanLineWeight = CalcScanLine(dy);
float signY = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0;
dy /= TextureSize.y;
dy *= signY;
vec2 tc = vec2(texcoord.x, yCoord + dy);
#endif
vec3 colour = texture2D(Texture, tc).rgb;
#if defined(SCANLINES)
#if defined(GAMMA)
#if defined(FAKE_GAMMA)
colour = colour * colour;
#else
colour = pow(colour, vec3(INPUT_GAMMA));
#endif
#endif
scanLineWeight *= BLOOM_FACTOR;
colour *= scanLineWeight;
#if defined(GAMMA)
#if defined(FAKE_GAMMA)
colour = sqrt(colour);
#else
colour = pow(colour, vec3(1.0/OUTPUT_GAMMA));
#endif
#endif
#endif
#if MASK_TYPE == 0
gl_FragColor = vec4(colour, 1.0);
#else
#if MASK_TYPE == 1
float whichMask = fract((gl_FragCoord.x*1.0001) * 0.5);
vec3 mask;
if (whichMask < 0.5)
mask = vec3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS);
else
mask = vec3(1.0, MASK_BRIGHTNESS, 1.0);
#elif MASK_TYPE == 2
float whichMask = fract((gl_FragCoord.x*1.0001) * 0.3333333);
vec3 mask = vec3(MASK_BRIGHTNESS, MASK_BRIGHTNESS, MASK_BRIGHTNESS);
if (whichMask < 0.3333333)
mask.x = 1.0;
else if (whichMask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
#endif
gl_FragColor = vec4(colour * mask, 1.0);
#endif
}
}
#endif
View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+47
View File
@@ -0,0 +1,47 @@
# Aventures En Egipte — Configuració de tecles d'UI
#
# Font única de veritat per a les tecles de funció / sistema.
# Les tecles de moviment del jugador viuen separades a config.yaml (secció `controls:`).
#
# Si l'usuari remapeja alguna tecla des del menú de servei, la diferència respecte
# aquests valors per defecte es persistix a ~/.config/jailgames/aee/keys.yaml.
#
# Camps:
# id - Identificador usat des del codi via KeyConfig::scancode("id")
# code - Nom SDL del scancode (per SDL_GetScancodeFromName), p.ex. "F1", "Escape"
# desc - Descripció curta (per a HELP / overlays futurs)
keys:
- id: dec_zoom
code: "F1"
desc: "Redueix el zoom de la finestra"
- id: inc_zoom
code: "F2"
desc: "Augmenta el zoom de la finestra"
- id: fullscreen
code: "F3"
desc: "Pantalla completa"
- id: toggle_shader
code: "F4"
desc: "Activa/desactiva shaders"
- id: toggle_aspect_ratio
code: "F5"
desc: "Aspecte 4:3 / pixels quadrats"
- id: next_shader
code: "F7"
desc: "Tipus de shader (PostFX / CRT-Pi)"
- id: next_shader_preset
code: "F8"
desc: "Pròxim preset del shader"
- id: cycle_texture_filter
code: "F9"
desc: "Filtre de textura (nearest / linear)"
- id: toggle_render_info
code: "F10"
desc: "Mostra info de renderitzat"
- id: pause_toggle
code: "F11"
desc: "Pausa el joc"
- id: menu_toggle
code: "F12"
desc: "Menú de servei"
+69 -60
View File
@@ -4,77 +4,86 @@
menu: menu:
titles: titles:
root: "OPCIONS" root: "Opcions"
video: "VIDEO" video: "Vídeo"
audio: "AUDIO" audio: "Àudio"
controls: "CONTROLS" controls: "Controls"
game: "JOC" game: "Joc"
system: "Sistema"
items: items:
video: "VIDEO" video: "Vídeo"
audio: "AUDIO" audio: "Àudio"
controls: "CONTROLS" controls: "Controls"
game: "JOC" game: "Joc"
use_new_logo: "LOGO NOU" system: "Sistema"
show_title_credits: "CREDITS DEL PORT" restart: "Reinicia"
zoom: "ZOOM" exit_game: "Eixir del joc"
screen: "PANTALLA" use_new_logo: "Logo nou"
shader: "SHADER" show_title_credits: "Crèdits del port"
aspect_4_3: "ASPECTE 4:3" show_preload: "Barra de precàrrega"
supersampling: "SUPERSAMPLING" zoom: "Zoom"
vsync: "VSYNC" screen: "Pantalla"
integer_scale: "ESCALA ENTERA" shader: "Shader"
shader_type: "TIPUS SHADER" aspect_4_3: "Aspecte 4:3"
preset: "PRESET" vsync: "Vsync"
stretch_filter: "FILTRE 4:3" scaling_mode: "Escala"
render_info: "RENDER INFO" shader_type: "Tipus shader"
uptime: "TEMPS DE JOC" preset: "Preset"
master_enable: "AUDIO" texture_filter: "Filtre textura"
master_volume: "MASTER" render_info: "Render info"
music: "MUSICA" uptime: "Temps de joc"
music_volume: "VOL MUSICA" internal_resolution: "Resolució interna"
sounds: "SONS" master_enable: "Àudio"
sounds_volume: "VOL SONS" master_volume: "Màster"
move_up: "MOU AMUNT" music: "Música"
move_down: "MOU AVALL" music_volume: "Vol música"
move_left: "MOU ESQUERRA" sounds: "Sons"
move_right: "MOU DRETA" sounds_volume: "Vol sons"
menu_key: "TECLA MENU" move_up: "Mou amunt"
move_down: "Mou avall"
move_left: "Mou esquerra"
move_right: "Mou dreta"
menu_key: "Tecla menú"
values: values:
"yes": "SI" "yes": "Sí"
"no": "NO" "no": "No"
"on": "ON" "on": "On"
"off": "OFF" "off": "Off"
fullscreen: "COMPLETA" fullscreen: "Completa"
windowed: "FINESTRA" windowed: "Finestra"
linear: "LINEAR" linear: "Linear"
nearest: "NEAREST" nearest: "Nearest"
top: "TOP" top: "Top"
bottom: "BOTTOM" bottom: "Bottom"
press_key: "<PREM TECLA>" press_key: "<Prem tecla>"
empty: "(BUIT)" empty: "(Buit)"
unknown: "---" unknown: "---"
scaling_disabled: "Sense escala"
scaling_stretch: "Estirada"
scaling_letterbox: "Letterbox"
scaling_overscan: "Overscan"
scaling_integer: "Entera"
window: window:
title: "© 2000 Aventures en Egipte — JailDesigner" title: "© 2000 Aventures en Egipte — JailDesigner"
notifications: notifications:
exit_double_esc: "TORNA A PULSAR ESC PER EIXIR" exit_double_esc: "Torna a pulsar ESC per a eixir"
zoom_fmt: "ZOOM %dX" zoom_fmt: "Zoom %dX"
fullscreen: "PANTALLA COMPLETA" fullscreen: "Pantalla completa"
windowed: "FINESTRA" windowed: "Finestra"
shader_on: "SHADER ON" shader_on: "Shader on"
shader_off: "SHADER OFF" shader_off: "Shader off"
aspect_43: "4:3 CRT" aspect_43: "4:3 CRT"
aspect_square: "PIXELS QUADRATS" aspect_square: "Píxels quadrats"
ss_on: "SUPERSAMPLING ON" preset_fmt: "Preset: %s"
ss_off: "SUPERSAMPLING OFF" filter_linear: "Filtre: linear"
preset_fmt: "PRESET: %s" filter_nearest: "Filtre: nearest"
filter_linear: "FILTRE: LINEAR" pause: "Pausa"
filter_nearest: "FILTRE: NEAREST" gamepad_connected: "connectat"
pause: "PAUSA" gamepad_disconnected: "desconnectat"
resume: "REPRES"
credits: credits:
port_role: "Conversio a C++ i SDL3" port_role: "Conversio a C++ i SDL3"
-48
View File
@@ -1,48 +0,0 @@
#version 450
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D source;
layout(set = 3, binding = 0) uniform DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2, ±2 taps), 1 = Lanczos3 (ventana 3, ±3 taps)
float pad0;
float pad1;
float pad2;
} u;
// Kernel Lanczos normalizado: sinc(t) * sinc(t/a) para |t| < a, 0 fuera.
float lanczos(float t, float a) {
t = abs(t);
if (t < 0.0001) { return 1.0; }
if (t >= a) { return 0.0; }
const float PI = 3.14159265358979;
float pt = PI * t;
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
}
void main() {
vec2 src_size = vec2(textureSize(source, 0));
// Posición en coordenadas de texel (centros de texel en N+0.5)
vec2 p = v_uv * src_size;
vec2 p_floor = floor(p);
float a = (u.algorithm == 0) ? 2.0 : 3.0;
int win = int(a);
vec4 color = vec4(0.0);
float weight_sum = 0.0;
for (int j = -win; j <= win; j++) {
for (int i = -win; i <= win; i++) {
// Centro del texel (i,j) relativo a p_floor
vec2 tap_center = p_floor + vec2(float(i), float(j)) + 0.5;
vec2 offset = tap_center - p;
float w = lanczos(offset.x, a) * lanczos(offset.y, a);
color += texture(source, tap_center / src_size) * w;
weight_sum += w;
}
}
out_color = (weight_sum > 0.0) ? (color / weight_sum) : vec4(0.0, 0.0, 0.0, 1.0);
}
+47 -25
View File
@@ -6,7 +6,9 @@
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h // xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
// //
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp // PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
// (8 floats, 32 bytes, std140/scalar layout). // (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
layout(location = 0) in vec2 v_uv; layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
@@ -15,7 +17,7 @@ layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform PostFXUniforms { layout(set = 3, binding = 0) uniform PostFXUniforms {
float vignette_strength; float vignette_strength;
float chroma_strength; float chroma_min; // intensitat mínima de l'aberració cromàtica
float scanline_strength; float scanline_strength;
float screen_height; float screen_height;
float mask_strength; float mask_strength;
@@ -24,10 +26,28 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float bleeding; float bleeding;
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_) float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init float time; // seconds since SDL init
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS) float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16) float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
float pad3; // padding per tancar a 64 bytes (4 × vec4)
} u; } u;
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
float sampleBilinearX(vec2 uv_target, int channel) {
vec2 tex_size = vec2(textureSize(scene, 0));
float px = uv_target.x * tex_size.x - 0.5;
float p_floor = floor(px);
float f = px - p_floor;
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
// YCbCr helpers for NTSC bleeding // YCbCr helpers for NTSC bleeding
vec3 rgb_to_ycc(vec3 rgb) { vec3 rgb_to_ycc(vec3 rgb) {
return vec3( return vec3(
@@ -69,11 +89,11 @@ void main() {
vec3 base = texture(scene, uv).rgb; vec3 base = texture(scene, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia. // Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample). // step = 1 pixel lógico de juego en UV.
vec3 colour; vec3 colour;
if (u.bleeding > 0.0) { if (u.bleeding > 0.0) {
float tw = float(textureSize(scene, 0).x); float tw = float(textureSize(scene, 0).x);
float step = u.oversample / tw; // 1 pixel lógico en UV float step = 1.0 / tw; // 1 pixel lógico en UV
vec3 ycc = rgb_to_ycc(base); vec3 ycc = rgb_to_ycc(base);
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb); vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb); vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
@@ -85,10 +105,14 @@ void main() {
colour = base; colour = base;
} }
// Aberración cromática (drift animado con time para efecto NTSC real) // Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3)); // una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
colour.r = texture(scene, uv + vec2(ca, 0.0)).r; // per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
colour.b = texture(scene, uv - vec2(ca, 0.0)).b; if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
}
// Corrección gamma (linealizar antes de scanlines, codificar después) // Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0) { if (u.gamma_strength > 0.0) {
@@ -96,22 +120,20 @@ void main() {
colour = mix(colour, lin, u.gamma_strength); colour = mix(colour, lin, u.gamma_strength);
} }
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica. // Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50). // franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
// Constantes ajustables: // suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3) // continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
if (u.scanline_strength > 0.0) { if (u.scanline_strength > 0.0) {
float ps = max(1.0, round(u.pixel_scale)); float ps = max(u.pixel_scale, 1.0);
float frac_in_row = fract(uv.y * u.screen_height); float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
float row_pos = floor(frac_in_row * ps); float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
// bright_rows: cuántas subfilas son brillantes float d = abs(sub - dark_center);
// ps==1 → ps (todo brillante → is_dark nunca se activa) d = min(d, 1.0 - d); // wrap a la fila següent
// ps==2 → 1 brillante + 1 oscura float half_width = u.scan_dark_ratio * 0.5;
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO))); float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
float is_dark = step(bright_rows, row_pos); float scan = mix(1.0, u.scan_dark_floor, band);
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
colour *= mix(1.0, scan, u.scanline_strength); colour *= mix(1.0, scan, u.scanline_strength);
} }
View File
+463
View File
@@ -0,0 +1,463 @@
# Reescritura de cinemáticas: capa `scenes::` + migración escena a escena
## Current Status (actualitzat 2026-04-16)
**Steps completats** — capa `scenes::` estable i 7 de 9 escenes migrades:
-**Step 0** — Infraestructura: `Scene`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, `SceneRegistry`, `scene_utils`, dispatch al `gameFiberEntry`.
-**Step 1**`MortScene` (state 100). Pantalla game over + fade-in/out + música "00000001.ogg" → "00000003.ogg".
-**Step 2**`BannerScene` (states 2..5). Banner pre-piràmide amb les 4 variants consolidades a `(idx%2)*160, (idx/2)*75`.
-**Step 3**`MenuScene` (state 0). Primera ús real de `FrameAnimator` (camell 8×160ms). Scrollers manuals amb acumulador ms per palmeres/horitzó. Parpalleig "polsa tecla" time-based.
-**Step 4**`IntroNewLogoScene` (state 255, condicional a `use_new_logo`). Revelat lletra a lletra + cicle de paleta 256 passos. **Delega temporalment a `ModuleSequence::doIntroSprites()`** via `SurfaceHandle::release()` perquè el legacy allibera `gfx` internament. La delegació desapareixerà al Step 9.
-**Step 5**`SlidesScene` (states 1 i 7). Wipe suau amb `Easing::outCubic` (el "rasca" del vell s'ha evaporat). Redirect `6→7` replicat al `gameFiberEntry` abans del `tryCreate` perquè el flux "no tens prou diners" caiga a slides de fracàs.
-**Step 6**`CreditsScene` (state 8). Scroll vertical + parallax condicional si `diamants == 16`. Música heretable (només arranca si no en sona cap ja). Escriu `trick.ini` al final.
-**Step 7**`SecretaScene` (state 6). 11 fases amb swap de `tomba1.gif→tomba2.gif` via `SurfaceHandle::reset()` i efecte "red pulse" sobre els índexs 254/253 de la paleta. Primera ús d'`InitialFadeOut` (fade-out sobre la paleta prèvia abans de muntar la nova).
**Steps pendents** — ataquen el cor de la intro:
- 📋 **Step 8**`IntroScene` (state 255 quan `use_new_logo == false`). 11 passos lineals del wordmark "JAILGAMES" llegat + cicle de paleta. Delegaria a `doIntroSprites` legacy igual que `IntroNewLogoScene`. Estimació: ~150 línies. Complexitat Media-Alta, però lineal.
- 📋 **Step 9**`IntroSpritesScene`. **El hueso**. `switch (rand() % 3)` amb 3 variants completament diferents (~9001100 frames cada una), 68 loops anidats per variant, frames subsamplejats amb màscares diferents. Mateix arxiu `gfx` que la intro que la crida. Si l'API escala mal, s'acceptarà un `tick()` manual sense Timeline. En migrar aquest step s'elimina la delegació temporal `IntroNewLogoScene → doIntroSprites` i `doIntroSprites` pot passar de `public` a privat/eliminat. Complexitat Alta.
- 📋 **Step 10** — Neteja final. `ModuleSequence::doIntro()` legacy també desapareix quan `IntroScene` + `IntroSpritesScene` estan fetes. `wait_frame_or_skip()` helper s'elimina. `ModuleSequence::Go()` queda reduït a ~5 línies o desapareix del tot si es pot treure del `gameFiberEntry`. Pot ser també aquí on s'elimine el `fiber` per fi quan `ModuleGame` siga tick-based, però això és un altre capítol.
**Configuració per a proves ràpides** — afegits al `Options::game` (persistents a `config.yaml`):
- `piramide_inicial` (ja existia) — state d'entrada. Valors útils: `255` = intro normal, `0` = menú, `5` = banner piràmide 5, `6` = SecretaScene, `8` = Credits, `100` = Mort.
- `habitacio_inicial` (ja existia) — sala d'entrada dins la piràmide (1..5).
- `vides` (ja existia).
- `diamants_inicial` — per al final "bo" dels crèdits amb parallax + cotxe, posar a `16`.
- `diners_inicial` — necessari posar `200` per entrar a `SecretaScene` sense el redirect a slides-fracàs (si entres directament en state 6 o hi arribes des del gameplay).
- `show_title_credits` (ja existia) — desactivar-ho accelera els tests.
**Bugs notables resolts al llarg del camí** (mantenir present — poden reaparèixer si es toca codi similar):
1. `JI_Update()` no es cridava dins del mini-while del fiber → `JI_AnyKey()` no es refrescava → les escenes ignoraven les tecles de skip. Fix a [director.cpp:gameFiberEntry](source/core/system/director.cpp) al Step 3.
2. `IntroNewLogoScene::~` doble-free de `gfx_` perquè `doIntroSprites` sempre allibera el `gfx` que rep (tant al final normal com als paths de skip). Fix: `SurfaceHandle::release()` abans de delegar. Step 4.
3. `IntroNewLogoScene` no mutava `info::ctx.num_piramide = 0` al terminar, el fiber tornava a crear la mateixa escena — loop infinit. El `Go()` vell ho feia post-switch. Step 4.
4. Skip per tecla durant el revelat del logo nou saltava només les lletres i executava igualment `doIntroSprites`. El vell retornava abans de cridar doIntroSprites. Fix al Step 4: `Phase::Done` direct en skip, `Phase::Delegate` només per terminació natural.
---
## Context
Las fases 07b del plan anterior están completas. El runtime de AEE ya es moderno: fibers cooperativos, audio streaming sin `SDL_AddTimer`, callbacks `SDL_AppInit/Iterate/Event/Quit`, C++ idiomático en la capa jail. Lo que queda de *legacy pesado* es [source/game/modulesequence.cpp](source/game/modulesequence.cpp): **1309 líneas** con 9 funciones de cinemáticas lineales, 38+ `wait_frame_or_skip()` calls, constantes mágicas esparcidas, tres sub-variantes aleatorias en `doIntroSprites`, y código procedural difícil de editar.
Un refactor mecánico de eso no tiene sentido — las escenas son contenido, no plumbing. Cada una tiene su propia lógica específica y no se benefician de una state machine genérica ni de sed. Lo que sí tiene sentido es **reescribirlas de arriba a abajo** sobre una capa fina `scenes::` reutilizable (Timeline, SpriteMover, FrameAnimator, PaletteFade, surface handle RAII), convirtiendo cada función en una clase `Scene`. Cada escena migrada elimina su código legacy del modulesequence, hasta que la función Go() sólo quede como un delegador hacia el registry.
**Objetivos**:
1. Capa `scenes::` **pequeña y reutilizable** — helpers obvios, sin sobreingeniería, reusando [easing.hpp](source/utils/easing.hpp) y los `JD8_*` existentes.
2. Cada escena nueva: **~2080 líneas** de código declarativo (vs los cientos actuales).
3. **Fácil de añadir escenas nuevas** — derivar de `scenes::Scene`, llenar un Timeline o un `tick()` directo, registrar en el `SceneRegistry`.
4. **Time-based**: todo `delta_ms` explícito. Las escenas no tocan fibers, no tienen whiles, no llaman `JG_ShouldUpdate`.
5. **Migración gradual**: el fiber existente sigue corriendo por debajo. Las escenas nuevas se ejecutan *dentro* del fiber (por debajo del capó) pero su código es puro tick-based. Cuando las 9 estén migradas + ModuleGame también, el fiber se elimina de una pieza.
6. **Zero regresiones visuales** — cada escena nueva debe verse/sonar indistinguible de la vieja antes de eliminar el código legacy asociado.
---
## Capa `scenes::` — API
Namespace `scenes::` (plano, consistente con `Overlay::`, `Screen::`, `Menu::`).
### `scenes::Scene` — interfaz base [source/scenes/scene.hpp]
```cpp
class Scene {
public:
virtual ~Scene() = default;
// Llamado una vez cuando el Director la activa. Buen sitio para arrancar
// música o disparar un fade-in. Los assets pueden cargarse aquí o en el
// constructor (ambos válidos).
virtual void onEnter() {}
// Un paso de la escena. No debe bloquear, no debe llamar a JD8_Flip
// (lo hace el caller). delta_ms = tiempo real transcurrido desde el
// tick anterior.
virtual void tick(int delta_ms) = 0;
// True cuando la escena ha acabado y el Director debe pasar a la siguiente.
virtual bool done() const = 0;
// Valor de retorno equivalente al int que devolvía Go(). El caller lo
// usa para decidir el siguiente módulo. Consultado sólo cuando done().
virtual int nextState() const { return 1; }
};
```
### `scenes::Timeline` — secuencia de steps [source/scenes/timeline.hpp]
```cpp
class Timeline {
public:
using StepFn = std::function<void(float progress_0_1)>;
// Step con duración y callback que recibe el progreso [0..1] cada tick.
// Si fn es nullptr, el step es una espera pura.
Timeline& step(int duration_ms, StepFn fn = nullptr);
// Step que se ejecuta una sola vez al entrar (pinta algo estático y listo).
Timeline& once(std::function<void()> fn);
void tick(int delta_ms);
void skip(); // marca todos los steps restantes como done inmediatamente
void reset();
bool done() const;
int currentStepIndex() const;
float currentProgress() const; // 0..1 dentro del step actual
};
```
### `scenes::SpriteMover` — movimiento 2D con easing [source/scenes/sprite_mover.hpp]
```cpp
class SpriteMover {
public:
using EaseFn = float(*)(float);
void moveTo(int x0, int y0, int x1, int y1, int duration_ms,
EaseFn ease = Easing::linear);
void tick(int delta_ms);
int x() const;
int y() const;
bool done() const;
};
```
No gestiona surfaces — sólo posición. La escena hace `JD8_BlitCK(mover.x(), mover.y(), gfx, ...)` ella misma. Reutilizable para el coche de créditos, slides, Sam caminando, etc.
### `scenes::FrameAnimator` — iteración de frames subsampleados [source/scenes/frame_animator.hpp]
```cpp
class FrameAnimator {
public:
FrameAnimator(int num_frames, int frame_ms, bool loop = true);
void tick(int delta_ms);
int frame() const; // índice [0, num_frames)
bool done() const; // sólo relevante si loop=false
void reset();
};
```
Cubre camello (8 frames × 4 ticks), palmeras (4 × 8 ticks), Sam caminando con `(i/5) % fr`.
### `scenes::PaletteFade` — wrapper time-based de `JD8_Fade*` [source/scenes/palette_fade.hpp]
```cpp
class PaletteFade {
public:
void startFadeOut();
void startFadeTo(JD8_Palette target);
void tick(int delta_ms); // avanza un step de fade por tick
bool active() const;
bool done() const;
};
```
Wrapper sobre `JD8_FadeStartOut` / `JD8_FadeStartToPal` / `JD8_FadeTickStep` que ya existen.
### `scenes::SurfaceHandle` — RAII para `JD8_Surface` [source/scenes/surface_handle.hpp]
```cpp
class SurfaceHandle {
public:
SurfaceHandle() = default;
explicit SurfaceHandle(const char* file);
~SurfaceHandle();
SurfaceHandle(const SurfaceHandle&) = delete;
SurfaceHandle& operator=(const SurfaceHandle&) = delete;
SurfaceHandle(SurfaceHandle&&) noexcept;
SurfaceHandle& operator=(SurfaceHandle&&) noexcept;
operator JD8_Surface() const; // conversión implícita → pasable a JD8_Blit*
JD8_Surface get() const;
bool valid() const;
void reset(const char* file); // libera + recarga (doSecreta lo necesita)
};
```
### `scenes::SceneRegistry` — factory [source/scenes/scene_registry.hpp/cpp]
```cpp
class SceneRegistry {
public:
using Factory = std::function<std::unique_ptr<Scene>()>;
// Llamado al boot para registrar cada escena migrada.
void registerScene(int state_key, Factory f);
// Intenta crear la escena para un state dado. nullptr si no registrada.
// El caller (gameFiberEntry) cae al viejo Go() legacy si devuelve null.
std::unique_ptr<Scene> tryCreate(int state_key) const;
// Singleton accedido desde el Director al boot.
static SceneRegistry& instance();
};
```
El `state_key` es un valor sintético que combina `info::ctx.num_piramide` con el módulo objetivo (sequence vs game). Los valores exactos los resolvemos al implementar — podría ser el propio `num_piramide` si es suficiente para distinguir (255=intro, 0=menu, 1/7=slides, 2-5=banner, 6=secreta, 8=credits, 100=mort).
---
## Organización de archivos
```
source/scenes/
├── scene.hpp
├── scene_registry.hpp/.cpp
├── timeline.hpp/.cpp
├── sprite_mover.hpp/.cpp
├── frame_animator.hpp/.cpp
├── palette_fade.hpp/.cpp
├── surface_handle.hpp/.cpp
├── mort_scene.hpp/.cpp # orden de migración
├── banner_scene.hpp/.cpp
├── menu_scene.hpp/.cpp
├── intro_new_logo_scene.hpp/.cpp
├── slides_scene.hpp/.cpp
├── credits_scene.hpp/.cpp
├── secreta_scene.hpp/.cpp
├── intro_scene.hpp/.cpp
└── intro_sprites_scene.hpp/.cpp
```
Estructura plana — sin subdirectorios `helpers/` o `concrete/`. Añadir archivo nuevo = una línea al `CMakeLists.txt`.
---
## Integración con el Director existente
**No creo un Director nuevo**. Modifico [source/core/system/director.cpp](source/core/system/director.cpp) — concretamente `gameFiberEntry()` en el namespace anónimo — para que consulte el `SceneRegistry` antes de caer al viejo `ModuleSequence::Go()`:
```cpp
// pseudocodigo dentro de gameFiberEntry()
int state = 1;
while (state != -1 && !JG_Quitting()) {
// Intentamos resolver la escena por el state actual.
if (auto scene = SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
scene->onEnter();
Uint32 last = SDL_GetTicks();
while (!scene->done() && !JG_Quitting()) {
Uint32 now = SDL_GetTicks();
scene->tick(static_cast<int>(now - last));
last = now;
JD8_Flip(); // yields al Director (presenta con overlay encima)
}
state = scene->nextState();
continue;
}
// Fallback: todavía no migrada, usa el Go() legacy
if (state == 1) {
auto* ms = new ModuleSequence();
state = ms->Go();
delete ms;
} else if (state == 0) {
auto* mg = new ModuleGame();
state = mg->Go();
delete mg;
}
}
```
**Claves**:
- Las escenas nuevas son puras tick-based. `tick(delta_ms)` no sabe del fiber.
- El mini-while que las ejecuta vive en `gameFiberEntry`, que sí corre dentro del fiber. `JD8_Flip()` es el que hace el yield al Director — igual que ahora.
- Cuando todas las escenas + `ModuleGame` estén migradas, este mini-while migra al `Director::iterate()` directo y se elimina `gameFiberEntry` + `GameFiber::*`. Pero eso no es para esta tanda.
- Registro de escenas: se hace en `Director::init()` llamando a `SceneRegistry::instance().registerScene(state_key, []{ return std::make_unique<scenes::MortScene>(); })` para cada escena ya migrada.
---
## Orden de migración (simple → complejo)
Cada paso = una PR / commit / validación visual antes de seguir. Al migrar una escena, **se elimina la función legacy correspondiente** de modulesequence.cpp.
### Step 0 — Infraestructura
Crear los archivos de la capa `scenes::` (scene, timeline, sprite_mover, frame_animator, palette_fade, surface_handle, scene_registry) sin ninguna escena concreta todavía. Compilar para confirmar que la capa es sólida.
### Step 1 — `MortScene` (complejidad **Baja**)
Reemplaza `ModuleSequence::doMort()`. ~15 líneas originales: blit fullscreen `gameover.gif` + `JD8_FadeToPal` + música `00000001.ogg` + espera 1000ms o tecla + `info::ctx.vida = 5`. Es la primera víctima: valida la API mínima (`Scene` + `PaletteFade` + `SurfaceHandle`).
### Step 2 — `BannerScene` (complejidad **Baja**)
Reemplaza `ModuleSequence::doBanner()`. Blits estáticos "PIRÀMIDE X" + número + fade entrada + espera 5000ms + `JA_FadeOutMusic(250)` + fade salida. Primera prueba de `Timeline::step()` con `once()`.
### Step 3 — `MenuScene` (complejidad **Media-Baja**)
Reemplaza `ModuleSequence::doMenu()`. Primera prueba de `FrameAnimator` (palmeras, camello, horizonte). Bucle infinito hasta input. Lee/escribe `info::ctx.pepe_activat` y `info::ctx.nou_personatge`. Texto condicional con `Locale::get`.
### Step 4 — `IntroNewLogoScene` (complejidad **Media**)
Reemplaza `ModuleSequence::doIntroNewLogo()`. Revelado letra a letra (9 letras × 150ms) + cursor parpadeando + logo completo + ciclo de paleta 256 pasos. Timeline con 20+ steps. Mantiene la llamada final a `doIntroSprites` (que aún no está migrada — delegación legacy temporal).
### Step 5 — `SlidesScene` (complejidad **Media**)
Reemplaza `ModuleSequence::doSlides()`. 3 slides con scroll entrada-derecha + espera + scroll salida-izquierda. Primera prueba seria de `SpriteMover` con `Easing::outCubic`. Elige asset según `info::ctx.num_piramide` + `info::ctx.diners`. Fade de música al final.
### Step 6 — `CreditsScene` (complejidad **Media**)
Reemplaza `ModuleSequence::doCredits()`. Scroll vertical largo (~3100 frames = ~62s a 20ms) + scroll parallax condicional si `info::ctx.diamants == 16` con animación de coche. Escribe `info::ctx.nou_personatge = true` y crea `trick.ini`.
### Step 7 — `SecretaScene` (complejidad **Media-Alta**)
Reemplaza `ModuleSequence::doSecreta()`. 11 estados originales: scroll + recarga de asset a mitad (`SurfaceHandle::reset`) + animación RGB dinámica del rojo (`JD8_SetPaletteColor`). Primera escena que usa `SurfaceHandle::reset()`.
### Step 8 — `IntroScene` (complejidad **Media-Alta**)
Reemplaza `ModuleSequence::doIntro()` (el logo JAILGAMES legacy). 11 pasos lineales de construcción del wordmark + ciclo de paleta + delegación a `IntroSpritesScene`. Timeline con muchos `once()` + `step()`.
### Step 9 — `IntroSpritesScene` (complejidad **Alta**)
Reemplaza `ModuleSequence::doIntroSprites()`. La bestia: `switch(rand() % 3)` con 3 variantes completamente distintas (~900-1100 frames cada una). Cada variante tiene 6-8 loops anidados. Aquí probablemente hace falta combinar `Timeline` + `SpriteMover` + `FrameAnimator` + lógica ad-hoc. Si la API no escala limpia, se acepta que esta escena tenga `tick()` manual sin Timeline.
### Step 10 — Limpieza final
En este punto `ModuleSequence` ya no tiene ninguna función `doX()` — sólo el `Go()` que delega al registry. Se puede:
- Eliminar `ModuleSequence` completo y mover el dispatch al `gameFiberEntry` directo.
- Eliminar el helper `wait_frame_or_skip()`.
- Eliminar el include de `fiber.hpp` desde `jgame.cpp` si `ModuleGame` también es tick-based (fuera de scope de este plan, pero queda preparado).
---
## Invariantes por escena
Cada paso **debe cumplir**:
1. Visualmente indistinguible de la vieja versión (mismo timing, mismas transiciones, mismo feel). Validar jugándolo.
2. Skip por tecla funciona idéntico (misma tecla, mismo momento).
3. Build nativo compila limpio, sin warnings nuevos.
4. Audio sigue: música arranca, fades suaves, no hay cortes.
5. Overlay sigue animándose encima (pause, notificaciones, render info) — lo hace el Director sin tocar la escena.
6. La función legacy `doX()` se elimina en el mismo commit que su `XScene`, no se deja código muerto.
---
## Fuera de scope (explícito)
- **`ModuleGame`** (gameplay puro). Sigue con Go() + fiber. Se migrará más tarde con otra estructura (probablemente no Scene — es interactivo y no lineal).
- **Emscripten fiber backend** + build WASM (fases 7c/7d del plan anterior). Cuando estén migradas las escenas + ModuleGame, los fibers se eliminan y este punto se vuelve trivial.
- **Fase 6** (time-based total con accumulator pattern). La saltamos — no aporta valor real con el framerate actual.
- **Multi-language** de textos en escenas. Se usa `Locale::get` directamente donde haga falta, sin envoltorio nuevo.
---
## Critical files
| Archivo | Step | Tipo |
|---|---|---|
| [source/scenes/scene.hpp](source/scenes/scene.hpp) | 0 | nuevo, interfaz base |
| [source/scenes/timeline.hpp](source/scenes/timeline.hpp) + .cpp | 0 | nuevo, helper central |
| [source/scenes/sprite_mover.hpp](source/scenes/sprite_mover.hpp) + .cpp | 0 | nuevo |
| [source/scenes/frame_animator.hpp](source/scenes/frame_animator.hpp) + .cpp | 0 | nuevo |
| [source/scenes/palette_fade.hpp](source/scenes/palette_fade.hpp) + .cpp | 0 | nuevo |
| [source/scenes/surface_handle.hpp](source/scenes/surface_handle.hpp) + .cpp | 0 | nuevo, RAII |
| [source/scenes/scene_registry.hpp](source/scenes/scene_registry.hpp) + .cpp | 0 | nuevo, factory |
| [source/scenes/*_scene.hpp](source/scenes/) + .cpp | 19 | una por paso |
| [source/core/system/director.cpp](source/core/system/director.cpp) | 0 | modificar `gameFiberEntry` |
| [source/game/modulesequence.cpp](source/game/modulesequence.cpp) | 19 | borrar funciones `doX()` una a una |
| [CMakeLists.txt](CMakeLists.txt) | 09 | añadir archivos nuevos |
## Reusables existentes
- [source/utils/easing.hpp](source/utils/easing.hpp) — `Easing::linear`, `outQuad`, `outCubic`, `inOutQuad`, `lerp`, `lerpInt`. Usados por `SpriteMover` y cualquier step de `Timeline` que reciba progress.
- [source/core/jail/jdraw8.hpp](source/core/jail/jdraw8.hpp) — `JD8_FadeStartOut`, `JD8_FadeStartToPal`, `JD8_FadeTickStep`, `JD8_FadeIsActive`. Usados por `PaletteFade`.
- [source/core/jail/jail_audio.hpp](source/core/jail/jail_audio.hpp) — `JA_PlayMusic`, `JA_FadeOutMusic`, `JA_PauseMusic`, `JA_ResumeMusic`.
- [source/core/locale/locale.hpp](source/core/locale/locale.hpp) — `Locale::get("key")` para strings de UI en las escenas.
- [source/core/rendering/overlay.hpp](source/core/rendering/overlay.hpp) — sigue siendo responsabilidad del Director; las escenas no tocan overlay.
- [source/core/jail/jinput.hpp](source/core/jail/jinput.hpp) — `JI_AnyKey`, `JI_KeyPressed` para detectar skip y navegación de menú.
---
## Ejemplos concretos
### `MortScene` (Step 1) — ~20 líneas de lógica
```cpp
// mort_scene.hpp
namespace scenes {
class MortScene : public Scene {
public:
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return done_; }
int nextState() const override { return 1; } // igual que doMort → vuelve a seq
private:
SurfaceHandle gfx_;
PaletteFade fade_;
int remaining_ms_ = 1000;
bool done_ = false;
};
}
// mort_scene.cpp
void MortScene::onEnter() {
// Lo que hacía ModuleSequence::doMort() linealmente, declarativo.
int size = 0;
char* buf = file_getfilebuffer("00000001.ogg", size);
JA_PlayMusic(JA_LoadMusic((Uint8*)buf, size, "00000001.ogg"));
JI_DisableKeyboard(60);
info::ctx.vida = 5;
gfx_ = SurfaceHandle("gameover.gif");
JD8_Palette pal = JD8_LoadPalette("gameover.gif");
JD8_ClearScreen(0);
JD8_Blit(gfx_);
fade_.startFadeTo(pal);
}
void MortScene::tick(int delta_ms) {
fade_.tick(delta_ms);
if (JI_AnyKey()) { done_ = true; return; }
remaining_ms_ -= delta_ms;
if (remaining_ms_ <= 0) done_ = true;
}
```
### `BannerScene` (Step 2) — Timeline + fades
```cpp
void BannerScene::onEnter() {
play_music("00000004.ogg");
gfx_ = SurfaceHandle("ffase.gif");
JD8_Palette pal = JD8_LoadPalette("ffase.gif");
timeline_
.once([this]{
JD8_ClearScreen(0);
// blits del banner + número según info::ctx.num_piramide
fade_in_.startFadeTo(pal);
})
.step(5000); // espera. Cualquier tecla hace timeline_.skip().
}
void BannerScene::tick(int delta_ms) {
fade_in_.tick(delta_ms);
if (!timeline_.done()) {
if (JI_AnyKey()) timeline_.skip();
timeline_.tick(delta_ms);
if (timeline_.done() && !fade_out_started_) {
JA_FadeOutMusic(250);
fade_out_.startFadeOut();
fade_out_started_ = true;
}
} else {
fade_out_.tick(delta_ms);
}
}
bool BannerScene::done() const { return timeline_.done() && fade_out_.done(); }
```
---
## Verification
Tras **cada step**:
1. `cmake --build build` limpio, sin warnings nuevos.
2. Ejecutar el juego entero desde intro hasta muerte, con atención específica a la escena migrada. Comparar con un git stash temporal del viejo código si hace falta.
3. **Skip por tecla** en la escena migrada — debe saltar a la siguiente igual que antes.
4. **Pausa F11** durante la escena — el juego se congela, el overlay sigue animándose.
5. **Menú F12** durante la escena — debe abrir encima.
6. **Cerrar ventana** durante la escena — responde al instante (sin el viejo bug de congelamiento).
7. **Audio** — la música debe arrancar cuando toca, los fades suaves, sin cortes.
8. **ESC doble-press** — sale limpiamente.
Tras el **step 10** (limpieza final):
- `modulesequence.cpp` tiene ~50 líneas (solo `Go()` mínimo) o desaparece del todo.
- El juego entero es jugable de principio a fin.
- FPS estable ≥60 con vsync.
- Cero referencias a `wait_frame_or_skip` en el código.
---
## Cadencia
Igual que antes: **paso a paso con pausa**. Cada step (09) se cierra con build verde + validación visual antes de arrancar el siguiente. No encadeno pasos automáticamente.
+251 -153
View File
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.c" // NOLINT(bugprone-suspicious-include): stb header-only library
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para JA_*
#include "game/options.hpp" // Para Options::audio
// Singleton
std::unique_ptr<Audio> Audio::instance;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = std::unique_ptr<Audio>(new Audio()); }
// Libera la instancia
void Audio::destroy() { Audio::instance.reset(); }
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance.get(); }
// Constructor
Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
Ja::quit();
}
// Método principal
void Audio::update() {
Ja::update();
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
if (instance && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
// Reproduce la música por nombre (con crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
return;
}
if (!music_enabled_) {
return;
}
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) {
return;
}
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
Ja::crossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
Ja::stopMusic();
}
Ja::playMusic(resource, loop);
}
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Reproduce la música por puntero (con crossfade opcional)
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) {
return;
}
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
Ja::crossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
Ja::stopMusic();
}
Ja::playMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
music_.loop = (loop != 0);
music_.state = MusicState::PLAYING;
}
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
Ja::pauseMusic();
music_.state = MusicState::PAUSED;
}
}
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
Ja::resumeMusic();
music_.state = MusicState::PLAYING;
}
}
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
Ja::stopMusic();
music_.state = MusicState::STOPPED;
}
}
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(Ja::Sound* sound, Group group) const {
if (sound_enabled_ && sound != nullptr) {
Ja::playSound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
Ja::stopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
Ja::fadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
Ja::MusicState ja_state = Ja::getMusicState();
switch (ja_state) {
case Ja::MusicState::PLAYING:
return MusicState::PLAYING;
case Ja::MusicState::PAUSED:
return MusicState::PAUSED;
case Ja::MusicState::STOPPED:
case Ja::MusicState::INVALID:
case Ja::MusicState::DISABLED:
default:
return MusicState::STOPPED;
}
}
// Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const bool ACTIVE = enabled_ && sound_enabled_;
const float CONVERTED_VOLUME = ACTIVE ? sound_volume * Options::audio.volume : 0.0F;
Ja::setSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
// Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const bool ACTIVE = enabled_ && music_enabled_;
const float CONVERTED_VOLUME = ACTIVE ? music_volume * Options::audio.volume : 0.0F;
Ja::setMusicVolume(CONVERTED_VOLUME);
}
// Aplica la configuración
void Audio::applySettings() {
enable(Options::audio.enabled);
}
// Establecer estado general
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
}
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
}
}
+121
View File
@@ -0,0 +1,121 @@
#pragma once
#include <cmath> // Para std::lround
#include <cstdint> // Para int8_t, uint8_t
#include <memory> // Para std::unique_ptr
#include <string> // Para string
#include <utility> // Para move
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
// --- Clase Audio: gestor de audio (singleton) ---
// Implementació canònica, byte-idèntica entre projectes.
// Els volums es manegen internament com a float 0.01.0; la capa de
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio {
public:
// --- Enums ---
enum class Group : std::int8_t {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState : std::uint8_t {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%)
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms)
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
static void destroy(); // Libera el objeto Audio
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
~Audio(); // Destructor (públic per a std::unique_ptr)
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
static void update(); // Actualización del sistema de audio
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Helpers de conversió per a la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
static constexpr auto toPercent(float volume) -> int {
return static_cast<int>(std::lround(volume * 100.0F));
}
static constexpr auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
}
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
void applySettings(); // Aplica la configuración
// --- Configuración de sonidos ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
// --- Configuración de música ---
void enableMusic() { music_enabled_ = true; } // Habilitar música
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Consultas de estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState;
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private:
// --- Tipos anidados ---
struct Music {
MusicState state{MusicState::STOPPED}; // Estado actual de la música
std::string name; // Última pista de música reproducida
bool loop{false}; // Indica si se reproduce en bucle
};
// --- Métodos ---
Audio(); // Constructor privado
void initSDLAudio(); // Inicializa SDL Audio
// --- Variables miembro ---
static std::unique_ptr<Audio> instance; // Instancia única de Audio
Music music_; // Estado de la música
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};
+15
View File
@@ -0,0 +1,15 @@
#include "core/audio/audio_adapter.hpp"
#include "core/resources/resource_cache.hpp"
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music* {
return Resource::Cache::get()->getMusic(name);
}
auto getSound(const std::string& name) -> Ja::Sound* {
return Resource::Cache::get()->getSound(name);
}
} // namespace AudioResource
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// --- Audio Resource Adapter ---
// Aquest fitxer exposa una interfície comuna a Audio per obtenir Ja::Music* /
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
#include <string> // Para string
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
} // namespace AudioResource
+699
View File
@@ -0,0 +1,699 @@
#pragma once
// --- Includes ---
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#define STB_VORBIS_HEADER_ONLY
// NOLINTNEXTLINE(bugprone-suspicious-include) — stb_vorbis és single-file: la macro de dalt limita a només-declaracions.
#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
struct SdlFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p != nullptr) { SDL_free(p); }
}
};
namespace Ja {
// --- Public Enums ---
enum class ChannelState : std::uint8_t {
INVALID,
FREE,
PLAYING,
PAUSED,
DISABLED,
};
enum class MusicState : std::uint8_t {
INVALID,
PLAYING,
PAUSED,
STOPPED,
DISABLED,
};
// --- Constants ---
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
inline constexpr int MAX_GROUPS = 2;
inline constexpr SDL_AudioSpec DEFAULT_SPEC{.format = SDL_AUDIO_S16, .channels = 2, .freq = 48000};
// --- Struct Definitions ---
struct Sound {
SDL_AudioSpec spec{DEFAULT_SPEC};
Uint32 length{0};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SdlFreeDeleter` allibera amb SDL_free.
std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
};
// L'ordre (punters primer, ints després, enum de 8 bits al final) minimitza
// el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
struct Channel {
Sound* sound{nullptr};
SDL_AudioStream* stream{nullptr};
int pos{0};
int times{0};
int group{0};
ChannelState state{ChannelState::FREE};
};
struct Music {
SDL_AudioSpec spec{DEFAULT_SPEC};
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
// d'entrada una sola vegada en loadMusic i es descomprimix en chunks
// per streaming. Com que stb_vorbis guarda un punter persistent al
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
// (una reallocation invalidaria el punter que el decoder conserva).
std::vector<Uint8> ogg_data;
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music
std::string filename;
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
SDL_AudioStream* stream{nullptr};
MusicState state{MusicState::INVALID};
};
struct FadeState {
bool active{false};
Uint64 start_time{0};
int duration_ms{0};
float initial_volume{0.0F};
};
struct OutgoingMusic {
SDL_AudioStream* stream{nullptr};
FadeState fade;
};
// --- Internal Global State (inline, C++17) ---
inline Music* current_music{nullptr};
inline Channel channels[MAX_SIMULTANEOUS_CHANNELS];
inline SDL_AudioSpec audio_spec{DEFAULT_SPEC};
inline float music_volume{1.0F};
inline float sound_volume[MAX_GROUPS];
inline bool music_enabled{true};
inline bool sound_enabled{true};
inline SDL_AudioDeviceID sdl_audio_device{0};
inline OutgoingMusic outgoing_music;
inline FadeState incoming_fade;
// --- Forward Declarations ---
inline void stopMusic();
inline void stopChannel(int channel);
inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
inline constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
inline constexpr int 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.
inline constexpr float 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 auto feedMusicChunk(Music* music) -> int {
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return 0; }
short chunk[MUSIC_CHUNK_SHORTS];
const int NUM_CHANNELS = music->spec.channels;
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
NUM_CHANNELS,
chunk,
MUSIC_CHUNK_SHORTS);
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, chunk, BYTES);
return SAMPLES_PER_CHANNEL;
}
// Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
inline void pumpMusic(Music* music) {
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; }
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
const int DECODED = feedMusicChunk(music);
if (DECODED > 0) { continue; }
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) { music->times--; }
} else {
break;
}
}
}
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
inline void preFillOutgoing(Music* music, int duration_ms) {
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; }
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
const int DECODED = feedMusicChunk(music);
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja
}
}
// --- update() helpers ---
inline void updateOutgoingFade() {
if ((outgoing_music.stream == nullptr) || !outgoing_music.fade.active) { return; }
const Uint64 NOW = SDL_GetTicks();
const Uint64 ELAPSED = NOW - outgoing_music.fade.start_time;
if (ELAPSED >= static_cast<Uint64>(outgoing_music.fade.duration_ms)) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
} else {
const float PERCENT = static_cast<float>(ELAPSED) / static_cast<float>(outgoing_music.fade.duration_ms);
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PERCENT));
}
}
inline void updateIncomingFade() {
if (!incoming_fade.active) { return; }
const Uint64 NOW = SDL_GetTicks();
const Uint64 ELAPSED = NOW - incoming_fade.start_time;
if (ELAPSED >= static_cast<Uint64>(incoming_fade.duration_ms)) {
incoming_fade.active = false;
SDL_SetAudioStreamGain(current_music->stream, music_volume);
} else {
const float PERCENT = static_cast<float>(ELAPSED) / static_cast<float>(incoming_fade.duration_ms);
SDL_SetAudioStreamGain(current_music->stream, music_volume * PERCENT);
}
}
inline void updateCurrentMusic() {
if (!music_enabled || (current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
updateIncomingFade();
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
// vorbis s'ha esgotat i no queden loops.
pumpMusic(current_music);
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
stopMusic();
}
}
inline void updateSoundChannels() {
if (!sound_enabled) { return; }
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
auto& ch = channels[i];
if (ch.state != ChannelState::PLAYING) { continue; }
if (ch.times != 0) {
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(ch.stream)) < (ch.sound->length / 2)) {
SDL_PutAudioStreamData(ch.stream, ch.sound->buffer.get(), ch.sound->length);
if (ch.times > 0) { ch.times--; }
}
} else {
if (SDL_GetAudioStreamAvailable(ch.stream) == 0) { stopChannel(i); }
}
}
}
inline void update() {
updateOutgoingFade();
updateCurrentMusic();
updateSoundChannels();
}
inline void init(int freq, SDL_AudioFormat format, int num_channels) {
audio_spec = {.format = format, .channels = num_channels, .freq = freq};
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); }
sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec);
if (sdl_audio_device == 0) { std::cout << "Failed to initialize SDL audio!" << '\n'; }
for (auto& ch : channels) { ch.state = ChannelState::FREE; }
std::ranges::fill(sound_volume, 0.5F);
}
inline void quit() {
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); }
sdl_audio_device = 0;
}
// --- Music Functions ---
inline auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
if ((buffer == nullptr) || length == 0) { return nullptr; }
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
// persistent al buffer; com que ací no el resize'jem, el .data() és
// estable durant tot el cicle de vida del music.
auto* music = new Music();
music->ogg_data.assign(buffer, buffer + length);
int vorbis_error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&vorbis_error,
nullptr);
if (music->vorbis == nullptr) {
std::cout << "loadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
delete music;
return nullptr;
}
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
music->spec.channels = INFO.channels;
music->spec.freq = static_cast<int>(INFO.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->state = MusicState::STOPPED;
return music;
}
// Overload amb filename — els callers l'usen per poder comparar la música
// en curs amb getMusicFilename() i no rearrancar-la si ja és la mateixa.
inline auto loadMusic(Uint8* buffer, Uint32 length, const char* filename) -> Music* {
Music* music = loadMusic(static_cast<const Uint8*>(buffer), length);
if ((music != nullptr) && (filename != nullptr)) { music->filename = filename; }
return music;
}
inline auto loadMusic(const char* filename) -> Music* {
// Carreguem primer el arxiu en memòria i després el descomprimim.
FILE* f = std::fopen(filename, "rb");
if (f == nullptr) { return nullptr; }
std::fseek(f, 0, SEEK_END);
const long FSIZE = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
if (FSIZE <= 0) {
std::fclose(f);
return nullptr;
}
auto* buffer = static_cast<Uint8*>(std::malloc(static_cast<size_t>(FSIZE) + 1));
if (buffer == nullptr) {
std::fclose(f);
return nullptr;
}
if (std::fread(buffer, FSIZE, 1, f) != 1) {
std::fclose(f);
std::free(buffer);
return nullptr;
}
std::fclose(f);
Music* music = loadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(FSIZE));
if (music != nullptr) { music->filename = filename; }
std::free(buffer);
return music;
}
inline void playMusic(Music* music, int loop = -1) {
if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; }
stopMusic();
current_music = music;
current_music->state = MusicState::PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &audio_spec);
if (current_music->stream == nullptr) {
std::cout << "Failed to create audio stream!" << '\n';
current_music->state = MusicState::STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, music_volume);
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
pumpMusic(current_music);
if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) {
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
}
}
inline auto getMusicFilename(const Music* 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 pauseMusic() {
if (!music_enabled) { return; }
if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
current_music->state = MusicState::PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void resumeMusic() {
if (!music_enabled) { return; }
if ((current_music == nullptr) || current_music->state != MusicState::PAUSED) { return; }
current_music->state = MusicState::PLAYING;
SDL_BindAudioStream(sdl_audio_device, current_music->stream);
}
inline void stopMusic() {
// Limpiar outgoing crossfade si existe
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 == nullptr) || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; }
current_music->state = MusicState::STOPPED;
if (current_music->stream != nullptr) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// Deixem el handle de vorbis viu — es tanca en deleteMusic.
// Rebobinem perquè un futur playMusic comence des del principi.
if (current_music->vorbis != nullptr) {
stb_vorbis_seek_start(current_music->vorbis);
}
}
inline void fadeOutMusic(int milliseconds) {
if (!music_enabled) { return; }
if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
// Destruir outgoing anterior si existe
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
preFillOutgoing(current_music, milliseconds);
// Robar el stream del current_music al outgoing
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = milliseconds, .initial_volume = music_volume};
// Dejar current_music sin stream (ya lo tiene outgoing)
current_music->stream = nullptr;
current_music->state = MusicState::STOPPED;
if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); }
incoming_fade.active = false;
}
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop) {
if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; }
// Destruir outgoing anterior si existe (crossfade durante crossfade)
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
// Robar el stream de la musica actual al outgoing para el fade-out.
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
if ((current_music != nullptr) && current_music->state == MusicState::PLAYING && (current_music->stream != nullptr)) {
preFillOutgoing(current_music, crossfade_ms);
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = music_volume};
current_music->stream = nullptr;
current_music->state = MusicState::STOPPED;
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)
current_music = music;
current_music->state = MusicState::PLAYING;
current_music->times = loop;
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &audio_spec);
if (current_music->stream == nullptr) {
std::cout << "Failed to create audio stream for crossfade!" << '\n';
current_music->state = MusicState::STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, 0.0F);
pumpMusic(current_music); // pre-carrega abans de bindejar
SDL_BindAudioStream(sdl_audio_device, current_music->stream);
// Configurar fade-in
incoming_fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = 0.0F};
}
inline auto getMusicState() -> MusicState {
if (!music_enabled) { return MusicState::DISABLED; }
if (current_music == nullptr) { return MusicState::INVALID; }
return current_music->state;
}
inline void deleteMusic(Music* music) {
if (music == nullptr) { return; }
if (current_music == music) {
stopMusic();
current_music = nullptr;
}
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 Music.
delete music;
}
inline auto setMusicVolume(float volume) -> float {
music_volume = SDL_clamp(volume, 0.0F, 1.0F);
if ((current_music != nullptr) && (current_music->stream != nullptr)) {
SDL_SetAudioStreamGain(current_music->stream, music_volume);
}
return music_volume;
}
inline void setMusicPosition(float /*value*/) {
// No implementat amb el backend de streaming.
}
inline auto getMusicPosition() -> float {
return 0.0F;
}
inline void enableMusic(bool value) {
if (!value && (current_music != nullptr) && (current_music->state == MusicState::PLAYING)) { stopMusic(); }
music_enabled = value;
}
// --- Sound Functions ---
inline auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
auto sound = std::make_unique<Sound>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline auto loadSound(const char* filename) -> Sound* {
auto sound = std::make_unique<Sound>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline auto playSound(Sound* sound, int loop = 0, int group = 0) -> int {
if (!sound_enabled || (sound == nullptr)) { return -1; }
int channel = 0;
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; }
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
// No hi ha canal lliure, reemplacem el primer
channel = 0;
}
return playSoundOnChannel(sound, channel, loop, group);
}
inline auto playSoundOnChannel(Sound* sound, int channel, int loop, int group) -> int {
if (!sound_enabled || (sound == nullptr)) { return -1; }
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
stopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group;
channels[channel].state = ChannelState::PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec);
if (channels[channel].stream == nullptr) {
std::cout << "Failed to create audio stream for sound!" << '\n';
channels[channel].state = ChannelState::FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]);
SDL_BindAudioStream(sdl_audio_device, channels[channel].stream);
return channel;
}
inline void deleteSound(Sound* sound) {
if (sound == nullptr) { return; }
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) { stopChannel(i); }
}
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
delete sound;
}
inline void pauseChannel(int channel) {
if (!sound_enabled) { return; }
if (channel == -1) {
for (auto& ch : channels) {
if (ch.state == ChannelState::PLAYING) {
ch.state = ChannelState::PAUSED;
SDL_UnbindAudioStream(ch.stream);
}
}
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == ChannelState::PLAYING) {
channels[channel].state = ChannelState::PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void resumeChannel(int channel) {
if (!sound_enabled) { return; }
if (channel == -1) {
for (auto& ch : channels) {
if (ch.state == ChannelState::PAUSED) {
ch.state = ChannelState::PLAYING;
SDL_BindAudioStream(sdl_audio_device, ch.stream);
}
}
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == ChannelState::PAUSED) {
channels[channel].state = ChannelState::PLAYING;
SDL_BindAudioStream(sdl_audio_device, channels[channel].stream);
}
}
}
inline void stopChannel(int channel) {
if (channel == -1) {
for (auto& ch : channels) {
if (ch.state != ChannelState::FREE) {
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
ch.stream = nullptr;
ch.state = ChannelState::FREE;
ch.pos = 0;
ch.sound = nullptr;
}
}
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != ChannelState::FREE) {
if (channels[channel].stream != nullptr) { SDL_DestroyAudioStream(channels[channel].stream); }
channels[channel].stream = nullptr;
channels[channel].state = ChannelState::FREE;
channels[channel].pos = 0;
channels[channel].sound = nullptr;
}
}
}
inline auto getChannelState(int channel) -> ChannelState {
if (!sound_enabled) { return ChannelState::DISABLED; }
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return ChannelState::INVALID; }
return channels[channel].state;
}
inline auto setSoundVolume(float volume, int group = -1) -> float {
const float V = SDL_clamp(volume, 0.0F, 1.0F);
if (group == -1) {
std::ranges::fill(sound_volume, V);
} else if (group >= 0 && group < MAX_GROUPS) {
sound_volume[group] = V;
} else {
return V;
}
// Aplicar volum als canals actius.
for (auto& ch : channels) {
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
if (group == -1 || ch.group == group) {
if (ch.stream != nullptr) {
SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]);
}
}
}
}
return V;
}
inline void enableSound(bool value) {
if (!value) {
stopChannel(-1);
}
sound_enabled = value;
}
inline auto setVolume(float volume) -> float {
const float V = setMusicVolume(volume);
setSoundVolume(V, -1);
return V;
}
} // namespace Ja
+206 -99
View File
@@ -1,40 +1,120 @@
#include "core/input/gamepad.hpp" #include "core/input/gamepad.hpp"
#include <cstdio> #include <cstdio>
#include <string>
#include "core/input/key_config.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/menu.hpp" #include "core/rendering/menu.hpp"
#include "game/options.hpp" #include "core/rendering/overlay.hpp"
namespace Gamepad { namespace Gamepad {
static SDL_Gamepad* pad_ = nullptr; static SDL_Gamepad* pad = nullptr;
static SDL_JoystickID pad_id_ = 0; static SDL_JoystickID pad_id = 0;
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id
// de Chrome/Android no porta Vendor/Product, el parser extreu valors
// escombraries, el GUID no està a gamecontrollerdb i el gamepad queda
// obert amb un mapping incorrecte). Com el W3C Gamepad API garanteix
// layout estàndard quan mapping=="standard", injectem un mapping SDL
// amb eixe layout per al GUID del joystick abans d'obrir-lo com gamepad.
// Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// 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 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);
}
while (!name.empty() && name.back() == ' ') {
name.pop_back();
}
if (name.size() > 25) {
name.resize(25);
}
if (name.empty()) {
name = "Gamepad";
}
return name;
}
// Dead-zone del stick esquerre (rang Sint16: -32768..32767) // Dead-zone del stick esquerre (rang Sint16: -32768..32767)
static constexpr Sint16 STICK_DEADZONE = 12000; static constexpr Sint16 STICK_DEADZONE = 12000;
// Estat previ per a detecció de flanc (edge-triggered) // Estat previ per a detecció de flanc (edge-triggered)
static bool prev_up_ = false; static bool prev_up = false;
static bool prev_down_ = false; static bool prev_down = false;
static bool prev_left_ = false; static bool prev_left = false;
static bool prev_right_ = false; static bool prev_right = false;
static bool prev_a_ = false; static bool prev_south = false;
static bool prev_b_ = false; static bool prev_east = false;
static bool prev_start_ = false; static bool prev_west = false;
static bool prev_back_ = false; static bool prev_north = false;
static bool prev_start = false;
static bool prev_back = false;
static void notify(const std::string& name, const char* status_key) {
std::string msg = name.empty() ? "Gamepad" : name;
msg += ' ';
msg += Locale::get(status_key);
Overlay::showNotification(msg.c_str(), 2.5F);
}
static void notifyConnected(const std::string& name) { notify(name, "notifications.gamepad_connected"); }
static void notifyDisconnected(const std::string& name) { notify(name, "notifications.gamepad_disconnected"); }
// Obri el primer joystick disponible que siga reconegut com a gamepad
// (o que ho esdevinga després d'injectar el mapping web estàndard).
static void openFirstGamepad() { static void openFirstGamepad() {
int count = 0; int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count); SDL_JoystickID* ids = SDL_GetJoysticks(&count);
if (ids && count > 0) { if (ids != nullptr) {
pad_ = SDL_OpenGamepad(ids[0]); for (int i = 0; i < count; ++i) {
if (pad_) { installWebStandardMapping(ids[i]);
pad_id_ = ids[0]; if (!SDL_IsGamepad(ids[i])) {
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_)); continue;
}
pad = SDL_OpenGamepad(ids[i]);
if (pad != nullptr) {
pad_id = ids[i];
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad));
break;
}
} }
SDL_free(ids);
} }
if (ids) SDL_free(ids);
} }
void init() { void init() {
@@ -52,38 +132,50 @@ namespace Gamepad {
} }
void destroy() { void destroy() {
if (pad_) { if (pad != nullptr) {
SDL_CloseGamepad(pad_); SDL_CloseGamepad(pad);
pad_ = nullptr; pad = nullptr;
pad_id_ = 0; pad_id = 0;
} }
SDL_QuitSubSystem(SDL_INIT_GAMEPAD); SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
} }
auto isConnected() -> bool { auto isConnected() -> bool {
return pad_ != nullptr; return pad != nullptr;
} }
void handleEvent(const SDL_Event& event) { void handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) { // A Emscripten els dispositius web entren com a JOYSTICK_ADDED (no
if (!pad_) { // GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
pad_ = SDL_OpenGamepad(event.gdevice.which); // injectem el mapping estàndard abans d'obrir el mando.
if (pad_) { if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
pad_id_ = event.gdevice.which; if (pad == nullptr) {
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_)); SDL_JoystickID jid = event.jdevice.which;
installWebStandardMapping(jid);
if (!SDL_IsGamepad(jid)) {
return;
}
pad = SDL_OpenGamepad(jid);
if (pad != nullptr) {
pad_id = jid;
std::string name = prettyName(SDL_GetGamepadName(pad));
SDL_Log("Gamepad connectat: %s", name.c_str());
notifyConnected(name);
} }
} }
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) { } else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
if (pad_ && event.gdevice.which == pad_id_) { if ((pad != nullptr) && event.jdevice.which == pad_id) {
SDL_Log("Gamepad desconnectat"); std::string saved_name = prettyName(SDL_GetGamepadName(pad));
SDL_CloseGamepad(pad_); SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
pad_ = nullptr; SDL_CloseGamepad(pad);
pad_id_ = 0; pad = nullptr;
pad_id = 0;
// Neteja qualsevol tecla virtual que poguera estar premuda // Neteja qualsevol tecla virtual que poguera estar premuda
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false); Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
notifyDisconnected(saved_name);
} }
} }
} }
@@ -103,71 +195,86 @@ namespace Gamepad {
SDL_PushEvent(&e); SDL_PushEvent(&e);
} }
void update() { // Estat agregat d'un frame: D-pad i stick combinats, més botons frontals.
if (!pad_) return; struct PadState {
bool up;
bool down;
bool left;
bool right;
bool south;
bool east;
bool west;
bool north;
bool start;
bool back;
};
// D-pad static auto readPadState() -> PadState {
bool dup = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_UP); const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
bool ddn = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_DOWN); const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
bool dlt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_LEFT); return PadState{
bool drt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); .up = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP) || LY < -STICK_DEADZONE,
.down = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) || LY > STICK_DEADZONE,
.left = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) || LX < -STICK_DEADZONE,
.right = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) || LX > STICK_DEADZONE,
.south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH),
.east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST),
.west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST),
.north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH),
.start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START),
.back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK),
};
}
// Stick esquerre amb dead-zone static void handleMenuNavigation(const PadState& s) {
Sint16 lx = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTX); if (s.up && !prev_up) { pushKey(SDL_SCANCODE_UP); }
Sint16 ly = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTY); if (s.down && !prev_down) { pushKey(SDL_SCANCODE_DOWN); }
bool sup = ly < -STICK_DEADZONE; if (s.left && !prev_left) { pushKey(SDL_SCANCODE_LEFT); }
bool sdn = ly > STICK_DEADZONE; if (s.right && !prev_right) { pushKey(SDL_SCANCODE_RIGHT); }
bool slt = lx < -STICK_DEADZONE; if (s.east && !prev_east) { pushKey(SDL_SCANCODE_RETURN); }
bool srt = lx > STICK_DEADZONE; if (s.south && !prev_south) { pushKey(SDL_SCANCODE_BACKSPACE); }
// Mentre el menú està obert, el joc no ha de rebre moviment.
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
}
bool up = dup || sup; static void handleGameInput(const PadState& s) {
bool dn = ddn || sdn; Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, s.up);
bool lt = dlt || slt; Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, s.down);
bool rt = drt || srt; Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, s.left);
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, s.right);
// Botons const bool ANY_FRONT_EDGE = (s.south && !prev_south) || (s.east && !prev_east) ||
bool a = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH); // A/Cross (s.west && !prev_west) || (s.north && !prev_north);
bool b = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST); // B/Circle if (ANY_FRONT_EDGE) {
bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START); pushKey(SDL_SCANCODE_RETURN);
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
// Start → obre/tanca menú (flanc)
if (start && !prev_start_) pushKey(Options::keys_gui.menu_toggle);
// Back → ESC (flanc)
if (back && !prev_back_) pushKey(SDL_SCANCODE_ESCAPE);
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 (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN);
if (b && !prev_b_) 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);
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
} else {
// Moviment al joc — level-triggered (polling)
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, up);
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
// Botó A al joc: emet Enter per avançar seqüències (JI_AnyKey)
if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN);
} }
}
prev_up_ = up; void update() {
prev_down_ = dn; if (pad == nullptr) {
prev_left_ = lt; return;
prev_right_ = rt; }
prev_a_ = a; const PadState S = readPadState();
prev_b_ = b; // Flancs globals: Select i Start sempre operen.
prev_start_ = start; if (S.back && !prev_back) { pushKey(KeyConfig::scancode("menu_toggle")); }
prev_back_ = back; if (S.start && !prev_start) { pushKey(KeyConfig::scancode("pause_toggle")); }
if (Menu::isOpen()) {
handleMenuNavigation(S);
} else {
handleGameInput(S);
}
prev_up = S.up;
prev_down = S.down;
prev_left = S.left;
prev_right = S.right;
prev_south = S.south;
prev_east = S.east;
prev_west = S.west;
prev_north = S.north;
prev_start = S.start;
prev_back = S.back;
} }
} // namespace Gamepad } // namespace Gamepad
+46 -85
View File
@@ -1,8 +1,10 @@
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include <cstdio> #include <cstdio>
#include <functional>
#include <string> #include <string>
#include "core/input/key_config.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
@@ -16,112 +18,71 @@ namespace GlobalInputs {
static bool fullscreen_prev = false; static bool fullscreen_prev = false;
static bool shader_prev = false; static bool shader_prev = false;
static bool aspect_prev = false; static bool aspect_prev = false;
static bool ss_prev = false;
static bool next_shader_prev = false; static bool next_shader_prev = false;
static bool next_preset_prev = false; static bool next_preset_prev = false;
static bool stretch_filter_prev = false; static bool texture_filter_prev = false;
static bool render_info_prev = false; static bool render_info_prev = false;
// Patró comú: lectura amb detecció de flanc + acumulació al flag "consumed".
// `on_press` només s'executa al flanc puja; `prev` es manté actualitzat.
static auto edgeTrigger(const char* key_id, bool& prev, const std::function<void()>& on_press) -> bool {
const bool PRESSED = Ji::keyPressed(KeyConfig::scancode(key_id));
if (PRESSED && !prev) {
on_press();
}
prev = PRESSED;
return PRESSED;
}
auto handle() -> bool { auto handle() -> bool {
bool consumed = false; bool consumed = false;
consumed |= edgeTrigger("dec_zoom", dec_zoom_prev, [] {
// F1 — Reduir zoom
bool dec_zoom = JI_KeyPressed(Options::keys_gui.dec_zoom);
if (dec_zoom && !dec_zoom_prev) {
Screen::get()->decZoom(); Screen::get()->decZoom();
char msg[32]; char msg[32];
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom()); snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
Overlay::showNotification(msg); Overlay::showNotification(msg);
} });
if (dec_zoom) consumed = true; consumed |= edgeTrigger("inc_zoom", inc_zoom_prev, [] {
dec_zoom_prev = dec_zoom;
// F2 — Augmentar zoom
bool inc_zoom = JI_KeyPressed(Options::keys_gui.inc_zoom);
if (inc_zoom && !inc_zoom_prev) {
Screen::get()->incZoom(); Screen::get()->incZoom();
char msg[32]; char msg[32];
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom()); snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
Overlay::showNotification(msg); Overlay::showNotification(msg);
} });
if (inc_zoom) consumed = true; consumed |= edgeTrigger("fullscreen", fullscreen_prev, [] {
inc_zoom_prev = inc_zoom;
// F3 — Toggle pantalla completa
bool fullscreen = JI_KeyPressed(Options::keys_gui.fullscreen);
if (fullscreen && !fullscreen_prev) {
Screen::get()->toggleFullscreen(); Screen::get()->toggleFullscreen();
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed")); Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
} });
if (fullscreen) consumed = true; consumed |= edgeTrigger("toggle_shader", shader_prev, [] {
fullscreen_prev = fullscreen;
// F4 — Toggle shaders
bool shader = JI_KeyPressed(Options::keys_gui.toggle_shader);
if (shader && !shader_prev) {
Screen::get()->toggleShaders(); Screen::get()->toggleShaders();
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off")); Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
} });
if (shader) consumed = true; consumed |= edgeTrigger("toggle_aspect_ratio", aspect_prev, [] {
shader_prev = shader;
// F5 — Toggle aspect ratio 4:3
bool aspect = JI_KeyPressed(Options::keys_gui.toggle_aspect_ratio);
if (aspect && !aspect_prev) {
Screen::get()->toggleAspectRatio(); Screen::get()->toggleAspectRatio();
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square")); Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
} });
if (aspect) consumed = true; consumed |= edgeTrigger("next_shader", next_shader_prev, [] {
aspect_prev = aspect; if (Screen::get()->nextShaderType()) {
char msg[64];
// F6 — Toggle supersampling snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
bool ss = JI_KeyPressed(Options::keys_gui.toggle_supersampling); Overlay::showNotification(msg);
if (ss && !ss_prev) { }
Screen::get()->toggleSupersampling(); });
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off")); consumed |= edgeTrigger("next_shader_preset", next_preset_prev, [] {
} if (Screen::get()->nextPreset()) {
if (ss) consumed = true; char msg[64];
ss_prev = ss; snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
Overlay::showNotification(msg);
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi) }
bool next_shader = JI_KeyPressed(Options::keys_gui.next_shader); });
if (next_shader && !next_shader_prev) { consumed |= edgeTrigger("cycle_texture_filter", texture_filter_prev, [] {
Screen::get()->nextShaderType(); Screen::get()->cycleTextureFilter(+1);
char msg[64]; Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName()); ? Locale::get("notifications.filter_linear")
Overlay::showNotification(msg); : Locale::get("notifications.filter_nearest"));
} });
if (next_shader) consumed = true; consumed |= edgeTrigger("toggle_render_info", render_info_prev, [] {
next_shader_prev = next_shader;
// F8 — Pròxim preset del shader actiu
bool next_preset = JI_KeyPressed(Options::keys_gui.next_shader_preset);
if (next_preset && !next_preset_prev) {
Screen::get()->nextPreset();
char msg[64];
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
Overlay::showNotification(msg);
}
if (next_preset) consumed = true;
next_preset_prev = next_preset;
// F9 — Toggle filtre d'estirament 4:3 (NEAREST ↔ LINEAR)
bool stretch_filter = JI_KeyPressed(Options::keys_gui.toggle_stretch_filter);
if (stretch_filter && !stretch_filter_prev) {
Screen::get()->toggleStretchFilter();
Overlay::showNotification(Options::video.stretch_filter_linear ? Locale::get("notifications.filter_linear") : Locale::get("notifications.filter_nearest"));
}
if (stretch_filter) consumed = true;
stretch_filter_prev = stretch_filter;
// F10 — Toggle render info (FPS, driver, shader)
bool render_info = JI_KeyPressed(Options::keys_gui.toggle_render_info);
if (render_info && !render_info_prev) {
Overlay::toggleRenderInfo(); Overlay::toggleRenderInfo();
} });
if (render_info) consumed = true;
render_info_prev = render_info;
return consumed; return consumed;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
namespace GlobalInputs { namespace GlobalInputs {
// Comprovar una vegada per frame, després de JI_Update() // Comprovar una vegada per frame, després de Ji::update()
// Retorna true si ha consumit alguna tecla (per suprimir-la de la capa de joc) // Retorna true si ha consumit alguna tecla (per suprimir-la de la capa de joc)
auto handle() -> bool; auto handle() -> bool;
} // namespace GlobalInputs } // namespace GlobalInputs
+200
View File
@@ -0,0 +1,200 @@
#include "core/input/key_config.hpp"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <utility>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace KeyConfig {
namespace {
std::vector<KeyEntry> key_entries;
std::unordered_map<std::string, size_t> index_table;
std::string overrides_path;
auto findIndex(const std::string& id) -> size_t {
auto it = index_table.find(id);
if (it == index_table.end()) {
return SIZE_MAX;
}
return it->second;
}
void loadDefaults(const std::string& defaults_resource_path) {
auto buf = ResourceHelper::loadFile(defaults_resource_path);
if (buf.empty()) {
std::cerr << "KeyConfig: no s'ha pogut llegir " << defaults_resource_path << '\n';
return;
}
std::string content(buf.begin(), buf.end());
try {
auto yaml = fkyaml::node::deserialize(content);
if (!yaml.contains("keys")) {
return;
}
for (const auto& node : yaml["keys"]) {
KeyEntry entry;
entry.id = node["id"].get_value<std::string>();
entry.code = node["code"].get_value<std::string>();
if (node.contains("desc")) {
entry.desc = node["desc"].get_value<std::string>();
}
SDL_Scancode sc = SDL_GetScancodeFromName(entry.code.c_str());
if (sc == SDL_SCANCODE_UNKNOWN) {
std::cerr << "KeyConfig: scancode desconegut '" << entry.code
<< "' per '" << entry.id << "'\n";
}
entry.scancode = sc;
entry.default_scancode = sc;
index_table[entry.id] = key_entries.size();
key_entries.push_back(std::move(entry));
}
std::cout << "KeyConfig: " << key_entries.size() << " tecles carregades de "
<< defaults_resource_path << '\n';
} catch (const fkyaml::exception& e) {
std::cerr << "KeyConfig: error parsejant YAML: " << e.what() << '\n';
}
}
void applyOverrides(const std::string& disk_path) {
std::ifstream file(disk_path);
if (!file.good()) {
return;
}
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (!yaml.contains("overrides")) {
return;
}
int applied = 0;
for (const auto& kv : yaml["overrides"].as_map()) {
auto id = kv.first.get_value<std::string>();
auto code = kv.second.get_value<std::string>();
auto idx = findIndex(id);
if (idx == SIZE_MAX) {
std::cerr << "KeyConfig: override per id desconegut '" << id << "'\n";
continue;
}
SDL_Scancode sc = SDL_GetScancodeFromName(code.c_str());
if (sc == SDL_SCANCODE_UNKNOWN) {
std::cerr << "KeyConfig: override amb scancode invàlid '" << code
<< "' per '" << id << "'\n";
continue;
}
key_entries[idx].scancode = sc;
key_entries[idx].code = code;
applied++;
}
if (applied > 0) {
std::cout << "KeyConfig: aplicats " << applied
<< " overrides de " << disk_path << '\n';
}
} catch (const fkyaml::exception& e) {
std::cerr << "KeyConfig: error parsejant overrides: " << e.what() << '\n';
}
}
} // namespace
void init(const std::string& defaults_resource_path,
const std::string& user_overrides_disk_path) {
key_entries.clear();
index_table.clear();
overrides_path = user_overrides_disk_path;
loadDefaults(defaults_resource_path);
if (!overrides_path.empty()) {
applyOverrides(overrides_path);
}
}
void destroy() {
key_entries.clear();
index_table.clear();
overrides_path.clear();
}
auto scancode(const std::string& id) -> SDL_Scancode {
auto idx = findIndex(id);
if (idx == SIZE_MAX) {
return SDL_SCANCODE_UNKNOWN;
}
return key_entries[idx].scancode;
}
auto scancodePtr(const std::string& id) -> SDL_Scancode* {
auto idx = findIndex(id);
if (idx == SIZE_MAX) {
return nullptr;
}
return &key_entries[idx].scancode;
}
void setScancode(const std::string& id, SDL_Scancode sc) {
auto idx = findIndex(id);
if (idx == SIZE_MAX) {
return;
}
key_entries[idx].scancode = sc;
const char* name = SDL_GetScancodeName(sc);
key_entries[idx].code = (name != nullptr) ? name : "";
}
auto isGuiKey(SDL_Scancode sc) -> bool {
if (sc == SDL_SCANCODE_UNKNOWN) {
return false;
}
return std::ranges::any_of(key_entries, [sc](const auto& e) { return e.scancode == sc; });
}
auto entries() -> const std::vector<KeyEntry>& {
return key_entries;
}
auto saveOverrides() -> bool {
if (overrides_path.empty()) {
return false;
}
// Recull només les entrades remapeades.
std::vector<const KeyEntry*> changed;
for (const auto& e : key_entries) {
if (e.scancode != e.default_scancode) {
changed.push_back(&e);
}
}
std::ofstream file(overrides_path);
if (!file.is_open()) {
std::cerr << "KeyConfig: no es pot escriure " << overrides_path << '\n';
return false;
}
file << "# AEE - Overrides de tecles d'UI\n";
file << "# Auto-generat. Només llista les tecles modificades respecte\n";
file << "# els valors per defecte de data/input/keys.yaml.\n";
file << "\n";
if (changed.empty()) {
file << "overrides: {}\n";
} else {
file << "overrides:\n";
for (const auto* e : changed) {
file << " " << e->id << ": \"" << e->code << "\"\n";
}
}
return true;
}
} // namespace KeyConfig
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <unordered_map>
#include <vector>
// KeyConfig: font única de veritat per a les tecles d'UI/sistema.
//
// Llegeix els valors per defecte des de `data/input/keys.yaml` (recurs read-only)
// i opcionalment aplica overrides des d'un fitxer de l'usuari (per a remapejos
// fets des del menú de servei). Els callers consulten per `id` (ex. "menu_toggle").
//
// Les tecles de moviment del jugador NO viuen ací — es queden a Options::keys_game.
struct KeyEntry {
std::string id;
std::string code; // nom SDL del scancode tal com apareix al YAML
std::string desc;
SDL_Scancode scancode{SDL_SCANCODE_UNKNOWN};
SDL_Scancode default_scancode{SDL_SCANCODE_UNKNOWN};
};
namespace KeyConfig {
// Inicialitza KeyConfig llegint defaults des d'un recurs (via ResourceHelper)
// i opcionalment sobreposant overrides des d'un fitxer de disc.
void init(const std::string& defaults_resource_path,
const std::string& user_overrides_disk_path);
void destroy();
// Consulta el scancode actual associat a un id. Torna SDL_SCANCODE_UNKNOWN si no existix.
[[nodiscard]] auto scancode(const std::string& id) -> SDL_Scancode;
// Punter estable al scancode d'un id — útil per a Menu::ItemKind::KeyBind.
// Torna nullptr si l'id no existix.
[[nodiscard]] auto scancodePtr(const std::string& id) -> SDL_Scancode*;
// Estableix el scancode d'un id. No persistix per si sol — cal cridar saveOverrides().
void setScancode(const std::string& id, SDL_Scancode sc);
// True si el scancode coincidix amb alguna tecla d'UI registrada.
// Usat pel Director per a evitar que tecles d'UI activen `key_pressed_` al joc.
[[nodiscard]] auto isGuiKey(SDL_Scancode sc) -> bool;
// Llistat complet de les entrades (per a HELP / debug / iteració).
[[nodiscard]] auto entries() -> const std::vector<KeyEntry>&;
// Persistix al fitxer d'overrides les entrades que difereixen del default.
// Si no s'ha proporcionat user_overrides_disk_path al init, és no-op.
auto saveOverrides() -> bool;
} // namespace KeyConfig
+5 -3
View File
@@ -9,15 +9,17 @@ namespace KeyRemap {
static void mirror(SDL_Scancode custom, SDL_Scancode standard, const bool* ks) { static void mirror(SDL_Scancode custom, SDL_Scancode standard, const bool* ks) {
if (custom == standard || custom == SDL_SCANCODE_UNKNOWN) { if (custom == standard || custom == SDL_SCANCODE_UNKNOWN) {
JI_SetVirtualKey(standard, JI_VSRC_REMAP, false); Ji::setVirtualKey(standard, Ji::VirtualSource::REMAP, false);
return; return;
} }
JI_SetVirtualKey(standard, JI_VSRC_REMAP, ks[custom]); Ji::setVirtualKey(standard, Ji::VirtualSource::REMAP, ks[custom]);
} }
void update() { void update() {
const bool* ks = SDL_GetKeyboardState(nullptr); 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.up, SDL_SCANCODE_UP, ks);
mirror(Options::keys_game.down, SDL_SCANCODE_DOWN, ks); mirror(Options::keys_game.down, SDL_SCANCODE_DOWN, ks);
mirror(Options::keys_game.left, SDL_SCANCODE_LEFT, ks); mirror(Options::keys_game.left, SDL_SCANCODE_LEFT, ks);
-450
View File
@@ -1,450 +0,0 @@
#ifndef JA_USESDLMIXER
#include "core/jail/jail_audio.hpp"
#include <SDL3/SDL.h>
#include <stdio.h>
#include "external/stb_vorbis.h"
#define JA_MAX_SIMULTANEOUS_CHANNELS 5
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{NULL};
};
struct JA_Channel_t {
JA_Sound_t* sound{nullptr};
int pos{0};
int times{0};
SDL_AudioStream* stream{nullptr};
JA_Channel_state state{JA_CHANNEL_FREE};
};
struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
char* filename{nullptr};
int pos{0};
int times{0};
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
JA_Music_t* current_music{nullptr};
JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
float JA_musicVolume{1.0f};
float JA_soundVolume{0.5f};
bool JA_musicEnabled{true};
bool JA_soundEnabled{true};
SDL_AudioDeviceID sdlAudioDevice{0};
SDL_TimerID JA_timerID{0};
bool fading = false;
int fade_start_time;
int fade_duration;
int fade_initial_volume;
/*
void audioCallback(void * userdata, uint8_t * stream, int len) {
SDL_memset(stream, 0, len);
if (current_music != NULL && current_music->state == JA_MUSIC_PLAYING) {
const int size = SDL_min(len, current_music->samples*2-current_music->pos);
SDL_MixAudioFormat(stream, (Uint8*)(current_music->output+current_music->pos), AUDIO_S16, size, JA_musicVolume);
current_music->pos += size/2;
if (size < len) {
if (current_music->times != 0) {
SDL_MixAudioFormat(stream+size, (Uint8*)current_music->output, AUDIO_S16, len-size, JA_musicVolume);
current_music->pos = (len-size)/2;
if (current_music->times > 0) current_music->times--;
} else {
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
}
}
}
// Mixar els channels mi amol
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state == JA_CHANNEL_PLAYING) {
const int size = SDL_min(len, channels[i].sound->length - channels[i].pos);
SDL_MixAudioFormat(stream, channels[i].sound->buffer + channels[i].pos, AUDIO_S16, size, JA_soundVolume);
channels[i].pos += size;
if (size < len) {
if (channels[i].times != 0) {
SDL_MixAudioFormat(stream + size, channels[i].sound->buffer, AUDIO_S16, len-size, JA_soundVolume);
channels[i].pos = len-size;
if (channels[i].times > 0) channels[i].times--;
} else {
JA_StopChannel(i);
}
}
}
}
}
*/
Uint32 JA_UpdateCallback(void* userdata, SDL_TimerID timerID, Uint32 interval) {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) {
fading = false;
JA_StopMusic();
return 30;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
}
}
if (current_music->times != 0) {
if (SDL_GetAudioStreamAvailable(current_music->stream) < int(current_music->length / 2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times > 0) current_music->times--;
} else {
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
}
}
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if (SDL_GetAudioStreamAvailable(channels[i].stream) < int(channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
return 30;
}
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
SDL_Log("Iniciant JailAudio...");
JA_audioSpec = {format, num_channels, freq};
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
SDL_Log((sdlAudioDevice == 0) ? "Failed to initialize SDL audio!\n" : "OK!\n");
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
// SDL_PauseAudioDevice(sdlAudioDevice);
JA_timerID = SDL_AddTimer(30, JA_UpdateCallback, nullptr);
}
void JA_Quit() {
if (JA_timerID) SDL_RemoveTimer(JA_timerID);
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
JA_Music_t* music = new JA_Music_t();
int chan, samplerate;
short* output;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
music->spec.channels = chan;
music->spec.freq = samplerate;
music->spec.format = SDL_AUDIO_S16;
music->buffer = (Uint8*)SDL_malloc(music->length);
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
if (filename) {
music->filename = (char*)malloc(strlen(filename) + 1);
strcpy(music->filename, filename);
}
return music;
}
JA_Music_t* JA_LoadMusic(const char* filename) {
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
FILE* f = fopen(filename, "rb");
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
Uint8* buffer = (Uint8*)malloc(fsize + 1);
if (fread(buffer, fsize, 1, f) != 1) return NULL;
fclose(f);
JA_Music_t* music = JA_LoadMusic(buffer, fsize, filename);
free(buffer);
return music;
}
void JA_PlayMusic(JA_Music_t* music, const int loop) {
if (!JA_musicEnabled) return;
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
// SDL_ResumeAudioStreamDevice(current_music->stream);
}
char* JA_GetMusicFilename(JA_Music_t* music) {
if (!music) music = current_music;
return music->filename;
}
void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->state = JA_MUSIC_PAUSED;
// SDL_PauseAudioStreamDevice(current_music->stream);
SDL_UnbindAudioStream(current_music->stream);
}
void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->state = JA_MUSIC_PLAYING;
// SDL_ResumeAudioStreamDevice(current_music->stream);
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
void JA_StopMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
// SDL_PauseAudioStreamDevice(current_music->stream);
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
free(current_music->filename);
current_music->filename = nullptr;
}
void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
void JA_DeleteMusic(JA_Music_t* music) {
if (current_music == music) current_music = nullptr;
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
delete music;
}
float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
if (current_music) SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
return JA_musicVolume;
}
void JA_SetMusicPosition(float value) {
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
}
float JA_GetMusicPosition() {
if (!current_music) return 0;
return float(current_music->pos) / float(current_music->spec.freq);
}
void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
return sound;
}
JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length);
return sound;
}
JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length);
return sound;
}
int JA_PlaySound(JA_Sound_t* sound, const int loop) {
if (!JA_soundEnabled) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop) {
if (!JA_soundEnabled) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
void JA_DeleteSound(JA_Sound_t* sound) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
delete sound;
}
void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
// SDL_PauseAudioStreamDevice(channels[i].stream);
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
// SDL_PauseAudioStreamDevice(channels[channel].stream);
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
// SDL_ResumeAudioStreamDevice(channels[i].stream);
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
// SDL_ResumeAudioStreamDevice(channels[channel].stream);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
void JA_StopChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
}
}
JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
float JA_SetSoundVolume(float volume) {
JA_soundVolume = SDL_clamp(volume, 0.0f, 1.0f);
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED))
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume);
return JA_soundVolume;
}
void JA_EnableSound(const bool value) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state == JA_CHANNEL_PLAYING) JA_StopChannel(i);
}
JA_soundEnabled = value;
}
float JA_SetVolume(float volume) {
JA_SetSoundVolume(JA_SetMusicVolume(volume) / 2.0f);
return JA_musicVolume;
}
#endif
-49
View File
@@ -1,49 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
enum JA_Channel_state { JA_CHANNEL_INVALID,
JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED };
enum JA_Music_state { JA_MUSIC_INVALID,
JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED };
struct JA_Sound_t;
struct JA_Music_t;
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels);
void JA_Quit();
JA_Music_t* JA_LoadMusic(const char* filename);
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename = nullptr);
void JA_PlayMusic(JA_Music_t* music, const int loop = -1);
char* JA_GetMusicFilename(JA_Music_t* music = nullptr);
void JA_PauseMusic();
void JA_ResumeMusic();
void JA_StopMusic();
void JA_FadeOutMusic(const int milliseconds);
JA_Music_state JA_GetMusicState();
void JA_DeleteMusic(JA_Music_t* music);
float JA_SetMusicVolume(float volume);
void JA_SetMusicPosition(float value);
float JA_GetMusicPosition();
void JA_EnableMusic(const bool value);
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length);
JA_Sound_t* JA_LoadSound(Uint8* buffer, Uint32 length);
JA_Sound_t* JA_LoadSound(const char* filename);
int JA_PlaySound(JA_Sound_t* sound, const int loop = 0);
int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0);
void JA_PauseChannel(const int channel);
void JA_ResumeChannel(const int channel);
void JA_StopChannel(const int channel);
JA_Channel_state JA_GetChannelState(const int channel);
void JA_DeleteSound(JA_Sound_t* sound);
float JA_SetSoundVolume(float volume);
void JA_EnableSound(const bool value);
float JA_SetVolume(float volume);
+205 -96
View File
@@ -1,9 +1,10 @@
#include "core/jail/jdraw8.hpp" #include "core/jail/jdraw8.hpp"
#include <fstream> #include <fstream>
#include <string>
#include "core/jail/jfile.hpp" #include "core/resources/resource_cache.hpp"
#include "core/system/director.hpp" #include "core/resources/resource_helper.hpp"
#if defined(__clang__) #if defined(__clang__)
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-but-set-variable" #pragma clang diagnostic ignored "-Wunused-but-set-variable"
@@ -11,87 +12,144 @@
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-but-set-variable" #pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#endif #endif
#include "external/gif.h" // NOLINTBEGIN(clang-analyzer-unix.Malloc): codi extern de tercers, no l'auditem.
#include <gif.h> // tercer-part: resolt via SYSTEM include path (source/external/)
// NOLINTEND(clang-analyzer-unix.Malloc)
#if defined(__clang__) #if defined(__clang__)
#pragma clang diagnostic pop #pragma clang diagnostic pop
#elif defined(__GNUC__) #elif defined(__GNUC__)
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
#endif #endif
JD8_Surface screen = NULL; Jd8::Surface screen = nullptr;
JD8_Palette main_palette = NULL; Jd8::Palette main_palette = nullptr;
Uint32* pixel_data = NULL; Uint32* pixel_data = nullptr;
void JD8_Init() { void Jd8::init() {
screen = (JD8_Surface)calloc(1, 64000); screen = new Uint8[64000]{};
main_palette = (JD8_Palette)calloc(1, 768); main_palette = new Color[256]{};
pixel_data = (Uint32*)calloc(1, 320 * 200 * 4); pixel_data = new Uint32[std::size_t{320} * 200]{};
} }
void JD8_Quit() { void Jd8::quit() {
if (screen != NULL) free(screen); delete[] screen;
if (main_palette != NULL) free(main_palette); delete[] main_palette;
if (pixel_data != NULL) free(pixel_data); delete[] pixel_data;
screen = nullptr;
main_palette = nullptr;
pixel_data = nullptr;
} }
void JD8_ClearScreen(Uint8 color) { void Jd8::clearScreen(Uint8 color) {
memset(screen, color, 64000); memset(screen, color, 64000);
} }
JD8_Surface JD8_NewSurface() { auto Jd8::newSurface() -> Jd8::Surface {
JD8_Surface surface = (JD8_Surface)malloc(64000); return new Uint8[64000]{};
memset(surface, 0, 64000);
return surface;
} }
JD8_Surface JD8_LoadSurface(const char* file) { // Helper intern: deriva el basename d'una ruta per a buscar al Cache.
int filesize = 0; static auto pathBasename(const char* file) -> std::string {
char* buffer = file_getfilebuffer(file, filesize); std::string s = file;
auto pos = s.find_last_of("/\\");
return pos == std::string::npos ? s : s.substr(pos + 1);
}
unsigned short w, h; auto Jd8::loadSurface(const char* file) -> Jd8::Surface {
Uint8* pixels = LoadGif((unsigned char*)buffer, &w, &h); // 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
// fresc que ha d'alliberar amb Jd8::freeSurface.
if (Resource::Cache::get() != nullptr) {
try {
const auto& cached = Resource::Cache::get()->getSurfacePixels(pathBasename(file));
Jd8::Surface image = Jd8::newSurface();
memcpy(image, cached.data(), 64000);
return image;
} catch (const std::exception&) {
// @INTENTIONAL: no està al cache (asset no llistat al manifest), fallback al loader.
}
}
free(buffer); auto buffer = ResourceHelper::loadFile(file);
unsigned short w;
if (pixels == NULL) { unsigned short h;
Uint8* pixels = LoadGif(buffer.data(), &w, &h);
if (pixels == nullptr) {
printf("Unable to load bitmap: %s\n", SDL_GetError()); printf("Unable to load bitmap: %s\n", SDL_GetError());
exit(1); exit(1);
} }
Jd8::Surface image = Jd8::newSurface();
JD8_Surface image = JD8_NewSurface();
memcpy(image, pixels, 64000); memcpy(image, pixels, 64000);
free(pixels); free(pixels);
return image; return image;
} }
JD8_Palette JD8_LoadPalette(const char* file) { auto Jd8::loadPalette(const char* file) -> Jd8::Palette {
int filesize = 0; // Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
char* buffer = NULL; // — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
buffer = file_getfilebuffer(file, filesize); // l'ownership a `Jd8::setScreenPalette`).
auto* palette = new Color[256];
JD8_Palette palette = (JD8_Palette)LoadPalette((unsigned char*)buffer); if (Resource::Cache::get() != nullptr) {
try {
const auto& cached = Resource::Cache::get()->getPaletteBytes(pathBasename(file));
memcpy(palette, cached.data(), 768);
return palette;
} catch (const std::exception&) {
// @INTENTIONAL: no està al cache, fallback a lectura + LoadPalette.
}
}
auto buffer = ResourceHelper::loadFile(file);
Uint8* raw = LoadPalette(buffer.data()); // external malloc
memcpy(palette, raw, 768);
free(raw);
return palette; return palette;
} }
void JD8_SetScreenPalette(JD8_Palette palette) { void Jd8::setScreenPalette(Jd8::Palette palette) {
if (main_palette == palette) return; if (main_palette == palette) {
if (main_palette != NULL) free(main_palette); return;
}
delete[] main_palette;
main_palette = palette; main_palette = palette;
} }
void JD8_FillSquare(int ini, int height, Uint8 color) { void Jd8::fillSquare(int ini, int height, Uint8 color) {
const int offset = ini * 320; const int OFFSET = ini * 320;
const int size = height * 320; const int SIZE = height * 320;
memset(&screen[offset], color, size); memset(&screen[OFFSET], color, SIZE);
} }
void JD8_Blit(JD8_Surface surface) { void Jd8::fillRect(int x, int y, int w, int h, Uint8 color) {
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
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);
}
}
void Jd8::blit(const Uint8* surface) {
memcpy(screen, surface, 64000); memcpy(screen, surface, 64000);
} }
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh) { void Jd8::blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh) {
int src_pointer = sx + (sy * 320); int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320); int dst_pointer = x + (y * 320);
for (int i = 0; i < sh; i++) { for (int i = 0; i < sh; i++) {
@@ -101,7 +159,7 @@ 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::blitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Jd8::Surface dest) {
int src_pointer = sx + (sy * 320); int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320); int dst_pointer = x + (y * 320);
for (int i = 0; i < sh; i++) { for (int i = 0; i < sh; i++) {
@@ -111,119 +169,170 @@ 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 Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
int src_pointer = sx + (sy * 320); int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320); int dst_pointer = x + (y * 320);
for (int j = 0; j < sh; j++) { for (int j = 0; j < sh; j++) {
for (int i = 0; i < sw; i++) { 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; src_pointer += 320;
dst_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 Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
int src_pointer = sx + (sy * 320); int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320); int dst_pointer = x + (y * 320);
for (int j = 0; j < sh; j++) { for (int j = 0; j < sh; j++) {
for (int i = 0; i < sw; i++) { 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; src_pointer += 320;
dst_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 Uint8* surface, int sx, int sy, int sh, Uint8 colorkey) {
int dst_pointer = y * 320; int dst_pointer = y * 320;
for (int j = sy; j < sy + sh; j++) { for (int j = sy; j < sy + sh; j++) {
for (int i = 0; i < 320; i++) { for (int i = 0; i < 320; i++) {
int x = (i + sx) % 320; 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++; 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 Uint8* surface, int sx, int sy, int sw, int sh, Jd8::Surface dest, Uint8 colorkey) {
int src_pointer = sx + (sy * 320); int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320); int dst_pointer = x + (y * 320);
for (int j = 0; j < sh; j++) { for (int j = 0; j < sh; j++) {
for (int i = 0; i < sw; i++) { 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; src_pointer += 320;
dst_pointer += 320; dst_pointer += 320;
} }
} }
void JD8_Flip() { void Jd8::flip() {
// Converteix el framebuffer indexat (paletted) a ARGB (pixel_data).
// El Director crida aquesta funció després del tick de cada escena
// per preparar el frame abans de presentar-lo. Ja no fa yield —
// tot corre en un sol thread sense fibers des de Phase B.2.
for (int x = 0; x < 320; x++) { for (int x = 0; x < 320; x++) {
for (int y = 0; y < 200; y++) { for (int y = 0; y < 200; y++) {
Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16); Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16);
pixel_data[x + (y * 320)] = color; pixel_data[x + (y * 320)] = color;
} }
} }
Director::get()->publishFrame(pixel_data);
} }
void JD8_FreeSurface(JD8_Surface surface) { auto Jd8::getFramebuffer() -> Uint32* {
free(surface); return pixel_data;
} }
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) { void Jd8::freeSurface(Jd8::Surface surface) { // NOLINT(readability-non-const-parameter): allibera memòria, no pot ser const
delete[] surface;
}
auto Jd8::getPixel(const Uint8* surface, int x, int y) -> Uint8 {
return surface[x + (y * 320)]; return surface[x + (y * 320)];
} }
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel) { void Jd8::putPixel(Jd8::Surface surface, int x, int y, Uint8 pixel) {
surface[x + (y * 320)] = pixel; surface[x + (y * 320)] = pixel;
} }
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) { void Jd8::setPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
main_palette[index].r = r << 2; main_palette[index].r = r << 2;
main_palette[index].g = g << 2; main_palette[index].g = g << 2;
main_palette[index].b = b << 2; main_palette[index].b = b << 2;
} }
void JD8_FadeOut() { // Màquina d'estats del fade. Evita que JD8_FadeOut/JD8_FadeToPal hagen de
for (int j = 0; j < 32; j++) { // mantindre whiles interns. Cada pas aplica un delta a la paleta activa i
for (int i = 0; i < 256; i++) { // el caller decideix quan fer Flip.
if (main_palette[i].r >= 8) namespace {
main_palette[i].r -= 8;
else enum class FadeType : std::uint8_t {
main_palette[i].r = 0; NONE = 0,
if (main_palette[i].g >= 8) OUT,
main_palette[i].g -= 8; TO_PAL,
else };
main_palette[i].g = 0;
if (main_palette[i].b >= 8) constexpr int FADE_STEPS = 32;
main_palette[i].b -= 8;
else FadeType fade_type = FadeType::NONE;
main_palette[i].b = 0; Color fade_target[256];
int fade_step = 0;
void applyFadeStep() {
if (fade_type == FadeType::OUT) {
for (int i = 0; i < 256; i++) {
main_palette[i].r = main_palette[i].r >= 8 ? main_palette[i].r - 8 : 0;
main_palette[i].g = main_palette[i].g >= 8 ? main_palette[i].g - 8 : 0;
main_palette[i].b = main_palette[i].b >= 8 ? main_palette[i].b - 8 : 0;
}
} else if (fade_type == FadeType::TO_PAL) {
for (int i = 0; i < 256; i++) {
main_palette[i].r = main_palette[i].r <= int(fade_target[i].r) - 8
? main_palette[i].r + 8
: fade_target[i].r;
main_palette[i].g = main_palette[i].g <= int(fade_target[i].g) - 8
? main_palette[i].g + 8
: fade_target[i].g;
main_palette[i].b = main_palette[i].b <= int(fade_target[i].b) - 8
? main_palette[i].b + 8
: fade_target[i].b;
}
} }
JD8_Flip();
} }
} // namespace
void Jd8::fadeStartOut() {
fade_type = FadeType::OUT;
fade_step = 0;
} }
#define MAX(a, b) (a) > (b) ? (a) : (b) void Jd8::fadeStartToPal(const Color* pal) {
fade_type = FadeType::TO_PAL;
void JD8_FadeToPal(JD8_Palette pal) { memcpy(fade_target, pal, sizeof(Color) * 256);
for (int j = 0; j < 32; j++) { fade_step = 0;
for (int i = 0; i < 256; i++) {
if (main_palette[i].r <= int(pal[i].r) - 8)
main_palette[i].r += 8;
else
main_palette[i].r = pal[i].r;
if (main_palette[i].g <= int(pal[i].g) - 8)
main_palette[i].g += 8;
else
main_palette[i].g = pal[i].g;
if (main_palette[i].b <= int(pal[i].b) - 8)
main_palette[i].b += 8;
else
main_palette[i].b = pal[i].b;
}
JD8_Flip();
}
} }
auto Jd8::fadeIsActive() -> bool {
return fade_type != FadeType::NONE;
}
auto Jd8::fadeTickStep() -> bool {
if (fade_type == FadeType::NONE) {
return true;
}
applyFadeStep();
fade_step++;
if (fade_step >= FADE_STEPS) {
fade_type = FadeType::NONE;
return true;
}
return false;
}
// Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `Jd8::flip`
// entre cada una que només funcionava mentre l'entorn tenia fibers i
// `Jd8::flip` cedia el control al Director. Ara tot fade es fa tick a
// tick via `Scenes::PaletteFade` (que encapsula `Jd8::fadeStartOut` /
// `Jd8::fadeStartToPal` + `Jd8::fadeTickStep`).
+78 -61
View File
@@ -1,61 +1,78 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
struct Color { struct Color {
Uint8 r; Uint8 r;
Uint8 g; Uint8 g;
Uint8 b; Uint8 b;
}; };
typedef Uint8* JD8_Surface; namespace Jd8 {
typedef Color* JD8_Palette;
using Surface = Uint8*;
void JD8_Init(); using Palette = Color*;
void JD8_Quit(); void init();
void JD8_ClearScreen(Uint8 color); void quit();
JD8_Surface JD8_NewSurface(); void clearScreen(Uint8 color);
JD8_Surface JD8_LoadSurface(const char* file); auto newSurface() -> Surface;
JD8_Palette JD8_LoadPalette(const char* file); auto loadSurface(const char* file) -> Surface;
void JD8_SetScreenPalette(JD8_Palette palette); auto loadPalette(const char* file) -> Palette;
void JD8_FillSquare(int ini, int height, Uint8 color); void setScreenPalette(Palette palette);
void JD8_Blit(JD8_Surface surface); void fillSquare(int ini, int height, Uint8 color);
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh); // Omple un rectangle arbitrari de la pantalla amb un color paletat.
// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.).
void JD8_BlitToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest); void fillRect(int x, int y, int w, int h, Uint8 color);
void JD8_BlitCK(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey); void blit(const Uint8* surface);
void JD8_BlitCKCut(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey); void blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh);
void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey); void blitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Surface dest);
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey); void blitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
void JD8_Flip(); void blitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
void JD8_FreeSurface(JD8_Surface surface); void blitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey);
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y); void blitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Surface dest, Uint8 colorkey);
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel); // Converteix la pantalla indexada a ARGB. El Director crida aquesta
// funció al final de cada tick i després llegeix el framebuffer via
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b); // getFramebuffer() per presentar-lo.
void flip();
void JD8_FadeOut();
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
void JD8_FadeToPal(JD8_Palette pal); // flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
auto getFramebuffer() -> Uint32*;
// JD_Font JD_LoadFont( char *file, int width, int height);
void freeSurface(Surface surface);
// void JD_DrawText( int x, int y, JD_Font *source, char *text);
auto getPixel(const Uint8* surface, int x, int y) -> Uint8;
// char *JD_GetFPS();
void putPixel(Surface surface, int x, int y, Uint8 pixel);
void setPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
// API de fade no bloquejant (màquina d'estats). `fadeStart*` inicia el
// fade; `fadeTickStep` aplica un pas i retorna `true` quan el fade ha
// acabat. Un pas correspon visualment a una iteració del fade original
// (32 passos en total). El caller és responsable de fer el Flip entre
// passos si el vol veure animat. `fadeIsActive` permet saber si hi ha
// un fade en curs per a enllaçar-lo amb un altre subsistema.
// L'embolcall `Scenes::PaletteFade` ho fa més idiomàtic per a escenes.
void fadeStartOut();
void fadeStartToPal(const Color* pal);
auto fadeTickStep() -> bool;
auto fadeIsActive() -> bool;
} // namespace Jd8
+115 -197
View File
@@ -1,15 +1,11 @@
#include "core/jail/jfile.hpp" #include "core/jail/jfile.hpp"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <algorithm>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -17,202 +13,124 @@
#include <pwd.h> #include <pwd.h>
#endif #endif
#define DEFAULT_FILENAME "data.jf2" namespace {
#define DEFAULT_FOLDER "data/"
#define CONFIG_FILENAME "config.txt"
struct file_t { struct Keyvalue {
std::string path; std::string key;
uint32_t size; std::string value;
uint32_t offset; };
};
std::vector<file_t> toc; std::vector<Keyvalue> config;
std::string resource_folder;
std::string config_folder;
/* El std::map me fa coses rares, vaig a usar un good old std::vector amb una estructura key,value propia i au, que sempre funciona */ void loadConfigValues() {
struct keyvalue_t { config.clear();
std::string key, value; const std::string CONFIG_FILE = config_folder + "/config.txt";
}; std::ifstream fi(CONFIG_FILE);
if (!fi.is_open()) {
char* resource_filename = NULL;
char* resource_folder = NULL;
int file_source = SOURCE_FILE;
char scratch[255];
static std::string config_folder;
std::vector<keyvalue_t> config;
void file_setresourcefilename(const char* str) {
if (resource_filename != NULL) free(resource_filename);
resource_filename = (char*)malloc(strlen(str) + 1);
strcpy(resource_filename, str);
}
void file_setresourcefolder(const char* str) {
if (resource_folder != NULL) free(resource_folder);
resource_folder = (char*)malloc(strlen(str) + 1);
strcpy(resource_folder, str);
}
void file_setsource(const int src) {
file_source = src % 2; // mod 2 so it always is a valid value, 0 (file) or 1 (folder)
if (src == SOURCE_FOLDER && resource_folder == NULL) file_setresourcefolder(DEFAULT_FOLDER);
}
bool file_getdictionary() {
if (resource_filename == NULL) file_setresourcefilename(DEFAULT_FILENAME);
std::ifstream fi(resource_filename, std::ios::binary);
if (!fi.is_open()) return false;
char header[4];
fi.read(header, 4);
uint32_t num_files, toc_offset;
fi.read((char*)&num_files, 4);
fi.read((char*)&toc_offset, 4);
fi.seekg(toc_offset);
for (uint32_t i = 0; i < num_files; ++i) {
uint32_t file_offset, file_size;
fi.read((char*)&file_offset, 4);
fi.read((char*)&file_size, 4);
uint8_t path_size;
fi.read((char*)&path_size, 1);
char file_name[256];
fi.read(file_name, path_size);
file_name[path_size] = 0;
std::string filename = file_name;
toc.push_back({filename, file_size, file_offset});
}
fi.close();
return true;
}
char* file_getfilenamewithfolder(const char* filename) {
strcpy(scratch, resource_folder);
strcat(scratch, filename);
return scratch;
}
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary) {
if (file_source == SOURCE_FILE and toc.size() == 0) {
if (not file_getdictionary()) file_setsource(SOURCE_FOLDER);
}
FILE* f;
if (file_source == SOURCE_FILE) {
bool found = false;
uint32_t count = 0;
while (!found && count < toc.size()) {
found = (std::string(resourcename) == toc[count].path);
if (!found) count++;
}
if (!found) {
perror("El recurs no s'ha trobat en l'arxiu de recursos");
exit(1);
}
filesize = toc[count].size;
f = fopen(resource_filename, binary ? "rb" : "r");
if (not f) {
perror("No s'ha pogut obrir l'arxiu de recursos");
exit(1);
}
fseek(f, toc[count].offset, SEEK_SET);
} else {
f = fopen(file_getfilenamewithfolder(resourcename), binary ? "rb" : "r");
fseek(f, 0, SEEK_END);
filesize = ftell(f);
fseek(f, 0, SEEK_SET);
}
return f;
}
char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate) {
FILE* f = file_getfilepointer(resourcename, filesize, true);
char* buffer = (char*)malloc(zero_terminate ? filesize : filesize + 1);
fread(buffer, filesize, 1, f);
if (zero_terminate) buffer[filesize] = 0;
fclose(f);
return buffer;
}
// Crea la carpeta del sistema donde guardar datos.
// Acepta rutas con subdirectorios (ej: "jailgames/aee") y crea toda la jerarquía.
void file_setconfigfolder(const char* foldername) {
#ifdef _WIN32
config_folder = std::string(getenv("APPDATA")) + "/" + foldername;
#elif __APPLE__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
#elif __linux__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
config_folder = std::string(homedir) + "/.config/" + foldername;
#endif
std::filesystem::create_directories(config_folder);
}
const char* file_getconfigfolder() {
static std::string folder;
folder = config_folder + "/";
return folder.c_str();
}
void file_loadconfigvalues() {
config.clear();
std::string config_file = config_folder + "/config.txt";
FILE* f = fopen(config_file.c_str(), "r");
if (!f) return;
char line[1024];
while (fgets(line, sizeof(line), f)) {
char* value = strchr(line, '=');
if (value) {
*value = '\0';
value++;
value[strlen(value) - 1] = '\0';
config.push_back({line, value});
}
}
fclose(f);
}
void file_saveconfigvalues() {
std::string config_file = config_folder + "/config.txt";
FILE* f = fopen(config_file.c_str(), "w");
if (f) {
for (auto pair : config) {
fprintf(f, "%s=%s\n", pair.key.c_str(), pair.value.c_str());
}
fclose(f);
}
}
const char* file_getconfigvalue(const char* key) {
if (config.empty()) file_loadconfigvalues();
for (auto pair : config) {
if (pair.key == std::string(key)) {
strcpy(scratch, pair.value.c_str());
return scratch;
}
}
return NULL;
}
void file_setconfigvalue(const char* key, const char* value) {
if (config.empty()) file_loadconfigvalues();
for (auto& pair : config) {
if (pair.key == std::string(key)) {
pair.value = value;
file_saveconfigvalues();
return; return;
} }
std::string line;
while (std::getline(fi, line)) {
const auto EQ = line.find('=');
if (EQ == std::string::npos) {
continue;
}
config.push_back({line.substr(0, EQ), line.substr(EQ + 1)});
}
} }
config.push_back({key, value});
file_saveconfigvalues(); void saveConfigValues() {
return; const std::string CONFIG_FILE = config_folder + "/config.txt";
std::ofstream fo(CONFIG_FILE);
if (!fo.is_open()) {
return;
}
for (const auto& pair : config) {
fo << pair.key << '=' << pair.value << '\n';
}
}
} // namespace
void Jf::setResourceFolder(const char* str) {
resource_folder = str;
}
auto Jf::getResourceFolder() -> const char* {
return resource_folder.c_str();
}
// Crea la carpeta del sistema on guardar les dades.
// Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
void Jf::setConfigFolder(const char* foldername) {
#ifdef _WIN32
const char* base = getenv("APPDATA");
if (!base) base = "C:/";
config_folder = std::string(base) + "/" + foldername;
#elif __APPLE__
struct passwd* pw = getpwuid(getuid());
const char* homedir = (pw && pw->pw_dir) ? pw->pw_dir : nullptr;
if (!homedir) homedir = getenv("HOME");
if (!homedir) homedir = "/tmp";
config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
#elif __linux__
// Nota emscripten: `__linux__` també està definit, però `getpwuid` pot
// retornar nullptr (sense /etc/passwd al MEMFS) o un passwd amb pw_dir
// buit. Amb els fallbacks HOME → /tmp evitem crashejar al primer
// 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 != 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;
#else
config_folder = std::string("/tmp/jailgames_config/") + foldername;
#endif
std::error_code ec;
std::filesystem::create_directories(config_folder, ec);
// A emscripten/MEMFS create_directories pot fallar (p.ex. parent
// read-only o libc++ amb path empty-check estricte). La config és
// volàtil al navegador de totes formes: ignorem l'error i continuem.
}
auto Jf::getConfigFolder() -> const char* {
thread_local std::string folder_;
folder_ = config_folder + "/";
return folder_.c_str();
}
auto Jf::getConfigValue(const char* key) -> const char* {
if (config.empty()) {
loadConfigValues();
}
const auto IT = std::ranges::find_if(config, [key](const Keyvalue& pair) { return pair.key == key; });
if (IT != config.end()) {
thread_local std::string value_cache_;
value_cache_ = IT->value;
return value_cache_.c_str();
}
return nullptr;
}
void Jf::setConfigValue(const char* key, const char* value) {
if (config.empty()) {
loadConfigValues();
}
const auto IT = std::ranges::find_if(config, [key](const Keyvalue& pair) { return pair.key == key; });
if (IT != config.end()) {
IT->value = value;
saveConfigValues();
return;
}
config.push_back({std::string(key), std::string(value)});
saveConfigValues();
} }
+8 -12
View File
@@ -1,18 +1,14 @@
#pragma once #pragma once
#include <stdio.h>
#define SOURCE_FILE 0 namespace Jf {
#define SOURCE_FOLDER 1
void file_setconfigfolder(const char* foldername); void setConfigFolder(const char* foldername);
const char* file_getconfigfolder(); auto getConfigFolder() -> const char*;
void file_setresourcefilename(const char* str); void setResourceFolder(const char* str);
void file_setresourcefolder(const char* str); auto getResourceFolder() -> const char*;
void file_setsource(const int src);
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary = false); auto getConfigValue(const char* key) -> const char*;
char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate = false); void setConfigValue(const char* key, const char* value);
const char* file_getconfigvalue(const char* key); } // namespace Jf
void file_setconfigvalue(const char* key, const char* value);
+57 -42
View File
@@ -1,42 +1,57 @@
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
bool eixir = false; namespace {
Uint32 updateTicks = 0;
Uint32 updateTime = 0; bool is_quitting = false;
Uint32 cycle_counter = 0; Uint32 update_ticks = 0;
Uint32 update_time = 0;
void JG_Init() { Uint32 cycle_counter = 0;
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); Uint32 last_delta_time = 0;
// SDL_WM_SetCaption( title, NULL );
updateTime = SDL_GetTicks(); } // namespace
}
void Jg::init() {
void JG_Finalize() { SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
SDL_Quit(); update_time = SDL_GetTicks();
} last_delta_time = update_time;
}
void JG_QuitSignal() {
eixir = true; void Jg::finalize() {
} SDL_Quit();
}
bool JG_Quitting() {
return eixir; void Jg::quitSignal() {
} is_quitting = true;
}
void JG_SetUpdateTicks(Uint32 milliseconds) {
updateTicks = milliseconds; auto Jg::quitting() -> bool {
} return is_quitting;
}
bool JG_ShouldUpdate() {
if (SDL_GetTicks() - updateTime > updateTicks) { void Jg::setUpdateTicks(Uint32 milliseconds) {
updateTime = SDL_GetTicks(); update_ticks = milliseconds;
cycle_counter++; }
return true;
} else { auto Jg::shouldUpdate() -> bool {
return false; const Uint32 NOW = SDL_GetTicks();
} if (NOW - update_time > update_ticks) {
} update_time = NOW;
cycle_counter++;
Uint32 JG_GetCycleCounter() { return true;
return cycle_counter; }
} // No toca update — retornem false sense més. Des de Phase B.2 ja no
// hi ha fibers: cap caller fa spin-waits (`while (!Jg::shouldUpdate())`)
// i el Director pren el control del main loop frame a frame.
return false;
}
auto Jg::getCycleCounter() -> Uint32 {
return cycle_counter;
}
auto Jg::getDeltaMs() -> Uint32 {
const Uint32 NOW = SDL_GetTicks();
const Uint32 DELTA = NOW - last_delta_time;
last_delta_time = NOW;
return DELTA;
}
+24 -16
View File
@@ -1,16 +1,24 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
void JG_Init(); namespace Jg {
void JG_Finalize(); void init();
void JG_QuitSignal(); void finalize();
bool JG_Quitting(); void quitSignal();
void JG_SetUpdateTicks(Uint32 milliseconds); auto quitting() -> bool;
bool JG_ShouldUpdate(); void setUpdateTicks(Uint32 milliseconds);
Uint32 JG_GetCycleCounter(); auto shouldUpdate() -> bool;
auto 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+).
auto getDeltaMs() -> Uint32;
} // namespace Jg
+93 -38
View File
@@ -1,76 +1,131 @@
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include <string> #include <algorithm>
#include <cstring>
#include "core/system/director.hpp" #include "core/system/director.hpp"
// keystates és actualitzat per SDL internament. Des del joc només fem lectures. namespace {
const bool* keystates = nullptr;
Uint8 cheat[5];
bool key_pressed = false;
int waitTime = 0;
void JI_DisableKeyboard(Uint32 time) { // keystates és actualitzat per SDL internament. Des del joc només fem lectures.
waitTime = time; const bool* keystates = nullptr;
// Buffer dels últims 5 caràcters tecle. Emmagatzemem caràcters ASCII
// lowercase (traduïts des de SDL_Scancode) per a poder comparar directament
// amb les cadenes dels cheats ("reviu", "alone", "obert").
Uint8 cheat[5] = {0, 0, 0, 0, 0};
bool key_pressed = false;
// 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;
// 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.
Uint64 last_update_tick = 0;
bool input_blocked = false;
Uint8 virtual_keystates[static_cast<size_t>(Ji::VirtualSource::COUNT)][SDL_SCANCODE_COUNT] = {{0}};
auto scancodeToAscii(Uint8 scancode) -> Uint8 {
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
}
return 0;
}
} // namespace
void Ji::disableKeyboard(Uint32 time) {
wait_ms = static_cast<float>(time);
} }
static bool input_blocked = false; void Ji::setInputBlocked(bool blocked) {
void JI_SetInputBlocked(bool blocked) {
input_blocked = blocked; input_blocked = blocked;
} }
static Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}}; void Ji::setVirtualKey(int scancode, VirtualSource source, bool pressed) {
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) {
void JI_SetVirtualKey(int scancode, int source, bool pressed) { return;
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return; }
if (source < 0 || source >= JI_VSRC_COUNT) return; const auto SRC_IDX = static_cast<size_t>(source);
virtual_keystates[source][scancode] = pressed ? 1 : 0; if (SRC_IDX >= static_cast<size_t>(VirtualSource::COUNT)) {
return;
}
virtual_keystates[SRC_IDX][scancode] = pressed ? 1 : 0;
} }
void JI_moveCheats(Uint8 new_key) { void Ji::moveCheats(Uint8 scancode) {
cheat[0] = cheat[1]; cheat[0] = cheat[1];
cheat[1] = cheat[2]; cheat[1] = cheat[2];
cheat[2] = cheat[3]; cheat[2] = cheat[3];
cheat[3] = cheat[4]; cheat[3] = cheat[4];
cheat[4] = new_key; cheat[4] = scancodeToAscii(scancode);
} }
void JI_Update() { void Ji::update() {
// El director ha processat tots els events. Ací només refresquem // El director ha processat tots els events. Ací només refresquem
// el snapshot del teclat i consumim el flag de tecla polsada. // el snapshot del teclat i consumim el flag de tecla polsada.
if (keystates == nullptr) { if (keystates == nullptr) {
keystates = SDL_GetKeyboardState(NULL); keystates = SDL_GetKeyboardState(nullptr);
} }
if (waitTime > 0) waitTime--; const Uint64 NOW = SDL_GetTicks();
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) {
wait_ms -= DELTA_MS;
wait_ms = std::max(wait_ms, 0.0F);
}
// Consumim el flag de "alguna tecla no-GUI polsada" del director // Consumim el flag de "alguna tecla no-GUI polsada" del director
key_pressed = Director::get()->consumeKeyPressed(); key_pressed = Director::get()->consumeKeyPressed();
} }
bool JI_KeyPressed(int key) { auto Ji::keyPressed(int key) -> bool {
if (waitTime > 0 || keystates == nullptr) return false; if (wait_ms > 0.0F || keystates == nullptr) {
return false;
}
// Input bloquejat (p.ex. menú flotant obert) // 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ó) // ESC bloquejada pel Director (primera pulsació mostra notificació)
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false; if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) {
if (key < 0 || key >= SDL_SCANCODE_COUNT) return false; 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;
} }
return false; if (key < 0 || key >= SDL_SCANCODE_COUNT) {
return false;
}
if (static_cast<int>(keystates[key]) != 0) {
return true;
}
return std::ranges::any_of(virtual_keystates, [key](const auto& vk) { return vk[key] != 0; });
} }
bool JI_CheatActivated(const char* cheat_code) { auto Ji::cheatActivated(const char* cheat_code) -> bool {
bool found = true; const size_t LEN = std::strlen(cheat_code);
for (size_t i = 0; i < strlen(cheat_code); i++) { if (LEN > sizeof(cheat)) {
if (cheat[i] != cheat_code[i]) found = false; return false;
} }
return found; // 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;
}
}
return true;
} }
bool JI_AnyKey() { auto Ji::anyKey() -> bool {
return waitTime > 0 ? false : key_pressed; return wait_ms > 0.0F ? false : key_pressed;
} }
+36 -25
View File
@@ -1,25 +1,36 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
void JI_DisableKeyboard(Uint32 time); #include <cstdint>
// Bloqueja tot l'input cap al joc (JI_KeyPressed retorna false per a tot) namespace Ji {
void JI_SetInputBlocked(bool blocked);
void disableKeyboard(Uint32 time);
// Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap)
// s'agrupen per OR. JI_KeyPressed retorna true si el teclat real O qualsevol // Bloqueja tot l'input cap al joc (Ji::keyPressed retorna false per a tot)
// font virtual està premuda. void setInputBlocked(bool blocked);
enum JI_VirtualSource {
JI_VSRC_GAMEPAD = 0, // Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap)
JI_VSRC_REMAP = 1, // s'agrupen per OR. Ji::keyPressed retorna true si el teclat real O qualsevol
JI_VSRC_COUNT // font virtual està premuda.
}; enum class VirtualSource : std::uint8_t {
void JI_SetVirtualKey(int scancode, int source, bool pressed); GAMEPAD = 0,
REMAP = 1,
void JI_Update(); COUNT = 2
};
bool JI_KeyPressed(int key); void setVirtualKey(int scancode, VirtualSource source, bool pressed);
bool JI_CheatActivated(const char* cheat_code); void update();
bool JI_AnyKey(); // Avança el buffer rotatori de cheats afegint `scancode` per detectar
// seqüències com "reviu", "alone", "obert". Usat pel Director quan rep
// un KEY_DOWN; el joc no l'ha de cridar directament.
void moveCheats(Uint8 scancode);
auto keyPressed(int key) -> bool;
auto cheatActivated(const char* cheat_code) -> bool;
auto anyKey() -> bool;
} // namespace Ji
+22 -16
View File
@@ -4,43 +4,47 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "core/jail/jfile.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
namespace Locale { namespace Locale {
static std::unordered_map<std::string, std::string> strings_; static std::unordered_map<std::string, std::string> strings_table;
// Aplana un node YAML en claus amb notació punt // Aplana un node YAML en claus amb notació punt
static void traverse(const fkyaml::node& node, const std::string& prefix) { static void traverse(const fkyaml::node& node, const std::string& prefix) {
if (node.is_mapping()) { if (node.is_mapping()) {
for (auto it = node.begin(); it != node.end(); ++it) { 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; std::string full = prefix;
if (!full.empty()) {
full += ".";
}
full += key;
traverse(it.value(), full); traverse(it.value(), full);
} }
} else if (node.is_scalar()) { } else if (node.is_scalar()) {
try { try {
strings_[prefix] = node.get_value<std::string>(); strings_table[prefix] = node.get_value<std::string>();
} catch (...) {} } catch (...) {
// @INTENTIONAL: si el valor no és string vàlid, l'ignorem i continuem.
}
} }
} }
bool load(const char* filename) { auto load(const char* filename) -> bool {
int size = 0; auto buffer = ResourceHelper::loadFile(filename);
char* buffer = file_getfilebuffer(filename, size, true); if (buffer.empty()) {
if (!buffer || size <= 0) {
std::cerr << "Locale: unable to load " << filename << '\n'; std::cerr << "Locale: unable to load " << filename << '\n';
return false; return false;
} }
std::string content(buffer, size); std::string content(reinterpret_cast<const char*>(buffer.data()), buffer.size());
free(buffer);
try { try {
auto yaml = fkyaml::node::deserialize(content); auto yaml = fkyaml::node::deserialize(content);
strings_.clear(); strings_table.clear();
traverse(yaml, ""); traverse(yaml, "");
std::cout << "Locale loaded: " << strings_.size() << " string(s) from " << filename << '\n'; std::cout << "Locale loaded: " << strings_table.size() << " string(s) from " << filename << '\n';
return true; return true;
} catch (const fkyaml::exception& e) { } catch (const fkyaml::exception& e) {
std::cerr << "Locale: error parsing " << filename << ": " << e.what() << '\n'; std::cerr << "Locale: error parsing " << filename << ": " << e.what() << '\n';
@@ -49,8 +53,10 @@ namespace Locale {
} }
auto get(const char* key) -> const char* { auto get(const char* key) -> const char* {
auto it = strings_.find(key); auto it = strings_table.find(key);
if (it != strings_.end()) return it->second.c_str(); if (it != strings_table.end()) {
return it->second.c_str();
}
return key; // fallback: retorna la clau mateixa return key; // fallback: retorna la clau mateixa
} }
+1 -1
View File
@@ -4,7 +4,7 @@
// Les claus són nested amb notació punt ("menu.items.zoom"). // 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). // Si una clau no existeix, Locale::get torna la clau mateixa (útil per debug).
namespace Locale { 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 // 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*. // sessió (no canvia), per tant es pot guardar en const char*.
+460 -225
View File
@@ -1,17 +1,23 @@
#include "core/rendering/menu.hpp" #include "core/rendering/menu.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio> #include <cstdio>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/director.hpp"
#include "game/defines.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
#include "version.h"
namespace Menu { namespace Menu {
@@ -35,186 +41,266 @@ namespace Menu {
static constexpr int ITEM_SPACING = 11; static constexpr int ITEM_SPACING = 11;
static constexpr int BOTTOM_PAD = 6; static constexpr int BOTTOM_PAD = 6;
static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap
static constexpr int SUBTITLE_H = 8 + 3; // línia de subtítol + gap
// --- Animació --- // --- Animació ---
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
static constexpr float CLOSE_SPEED = 10.0F; // 1.0 / 0.1s (una mica més ràpida que l'obertura)
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
// --- Items --- // --- Items ---
enum class ItemKind { Toggle, enum class ItemKind : std::uint8_t { TOGGLE,
Cycle, CYCLE,
IntRange, INT_RANGE,
Submenu, SUBMENU,
KeyBind }; KEY_BIND,
ACTION };
struct Item { struct Item {
const char* label; const char* label;
ItemKind kind; ItemKind kind;
std::function<std::string()> getValue; // opcional std::function<std::string()> get_value; // opcional
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
std::function<void()> enter; // per Submenu std::function<void()> enter; // per SUBMENU i ACTION
SDL_Scancode* scancode{nullptr}; // per KeyBind SDL_Scancode* scancode{nullptr}; // per KEY_BIND
std::function<bool()> visible{nullptr}; // nullptr ⇒ sempre visible
}; };
struct Page { struct Page {
const char* title; const char* title;
std::vector<Item> items; std::vector<Item> items;
int cursor{0}; int cursor{0};
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
}; };
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 auto nextVisibleCursor(const Page& p, int from, int dir) -> int {
const int N = static_cast<int>(p.items.size());
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;
}
}
return from;
}
// --- Estat --- // --- Estat ---
static std::vector<Page> stack_; static std::vector<Page> stack;
static std::unique_ptr<Text> font_; static std::unique_ptr<Text> font;
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert static float open_anim{0.0F}; // 0 = tancat, 1 = obert
static Uint32 last_ticks_{0}; static float animated_h{0.0F}; // alçada actual animada (smoothing cap al target visible)
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar static Uint32 last_ticks{0};
static SDL_Scancode* capturing{nullptr}; // != null → esperant tecla per assignar
static bool closing{false}; // true mentre l'animació de tancament és en curs
// --- Transició entre pàgines --- // --- Transició entre pàgines ---
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
static Page transition_outgoing_{"", {}, 0}; static Page transition_outgoing{.title = "", .items = {}, .cursor = 0, .subtitle = ""};
static bool transition_active_{false}; static bool transition_active{false};
static float transition_progress_{1.0F}; static float transition_progress{1.0F};
static int transition_dir_{+1}; // +1 endavant, -1 enrere static int transition_dir{+1}; // +1 endavant, -1 enrere
// Helpers per triggerar transicions // Helpers per triggerar transicions
static void pushPage(Page newPage) { static void pushPage(Page new_page) {
transition_outgoing_ = stack_.back(); transition_outgoing = stack.back();
stack_.push_back(std::move(newPage)); stack.push_back(std::move(new_page));
transition_active_ = true; transition_active = true;
transition_progress_ = 0.0F; transition_progress = 0.0F;
transition_dir_ = +1; transition_dir = +1;
} }
static void popPage() { static void popPage() {
transition_outgoing_ = stack_.back(); transition_outgoing = stack.back();
stack_.pop_back(); stack.pop_back();
transition_active_ = true; transition_active = true;
transition_progress_ = 0.0F; transition_progress = 0.0F;
transition_dir_ = -1; transition_dir = -1;
} }
// --- Helpers --- // --- Helpers ---
static std::string yesNo(bool b) { return b ? Locale::get("menu.values.yes") : Locale::get("menu.values.no"); } static auto yesNo(bool b) -> std::string { 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 onOff(bool b) -> std::string { return b ? Locale::get("menu.values.on") : Locale::get("menu.values.off"); }
// --- Builders de pàgines --- // --- Builders de pàgines ---
static Page buildVideo(); static auto buildVideo() -> Page;
static Page buildAudio(); static auto buildAudio() -> Page;
static Page buildControls(); static auto buildControls() -> Page;
static Page buildGame(); static auto buildGame() -> Page;
static auto buildSystem() -> Page;
static Page buildRoot() { static auto buildRoot() -> Page {
Page p{Locale::get("menu.titles.root"), {}, 0}; Page p{.title = Locale::get("menu.titles.root"), .items = {}, .cursor = 0, .subtitle = ""};
p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr}); 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.audio"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr}); p.items.push_back({Locale::get("menu.items.controls"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr}); p.items.push_back({Locale::get("menu.items.game"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
p.items.push_back({Locale::get("menu.items.system"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
return p; return p;
} }
static Page buildVideo() { static auto buildVideo() -> Page {
Page p{Locale::get("menu.titles.video"), {}, 0}; Page p{.title = Locale::get("menu.titles.video"), .items = {}, .cursor = 0, .subtitle = ""};
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] { // Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::INT_RANGE, [] {
char buf[16]; char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom()); std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
return std::string(buf); }, [](int dir) { return std::string(buf); }, [](int dir) {
if (dir < 0) Screen::get()->decZoom(); if (dir < 0) { Screen::get()->decZoom();
else if (dir > 0) Screen::get()->incZoom(); }, nullptr}); } 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}); 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
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr}); // Opcions visuals generals (sempre visibles)
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::TOGGLE, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr}); p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::TOGGLE, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr}); p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::CYCLE, [] {
switch (Options::video.scaling_mode) {
case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled"));
case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch"));
case Options::ScalingMode::LETTERBOX: return std::string(Locale::get("menu.values.scaling_letterbox"));
case Options::ScalingMode::OVERSCAN: return std::string(Locale::get("menu.values.scaling_overscan"));
case Options::ScalingMode::INTEGER: return std::string(Locale::get("menu.values.scaling_integer"));
}
return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr}); p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::CYCLE, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
? Locale::get("menu.values.linear")
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.integer_scale"), ItemKind::Toggle, [] { return onOff(Options::video.integer_scale); }, [](int) { Screen::get()->toggleIntegerScale(); }, nullptr}); p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::INT_RANGE, [] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution);
return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) { // Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
if (dir < 0) Screen::get()->prevShaderType(); #ifndef __EMSCRIPTEN__
else Screen::get()->nextShaderType(); }, nullptr}); 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.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) { 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()->prevPreset(); if (dir < 0) { Screen::get()->prevShaderType();
else Screen::get()->nextPreset(); }, nullptr}); } else { Screen::get()->nextShaderType();
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
p.items.push_back({Locale::get("menu.items.stretch_filter"), ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? Locale::get("menu.values.linear") : Locale::get("menu.values.nearest")); }, [](int) { Screen::get()->toggleStretchFilter(); }, nullptr}); 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; }});
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] { #endif
// Informació de render
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::CYCLE, [] {
switch (Options::render_info.position) { switch (Options::render_info.position) {
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off")); case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top")); case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom")); case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
} }
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr}); return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr}); p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::TOGGLE, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
return p; return p;
} }
// Converteix volum 0..1 a percentatge i ho formata com "50%" // Converteix volum 0..1 a percentatge i ho formata com "50%"
static std::string volPct(float v) { static auto volPct(float v) -> std::string {
int pct = static_cast<int>(v * 100.0F + 0.5F); int pct = static_cast<int>(std::lround(v * 100.0F));
if (pct < 0) pct = 0; pct = std::max(pct, 0);
if (pct > 100) pct = 100; pct = std::min(pct, 100);
char buf[8]; char buf[8];
std::snprintf(buf, sizeof(buf), "%d%%", pct); std::snprintf(buf, sizeof(buf), "%d%%", pct);
return std::string(buf); return {buf};
} }
// Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping // Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping
static void stepVolume(float& v, int dir) { static void stepVolume(float& v, int dir) {
v += (dir >= 0 ? 0.05F : -0.05F); v += (dir >= 0 ? 0.05F : -0.05F);
if (v < 0.0F) v = 0.0F; v = std::max(v, 0.0F);
if (v > 1.0F) v = 1.0F; v = std::min(v, 1.0F);
Options::applyAudio(); Options::applyAudio();
} }
static Page buildControls() { static auto buildControls() -> Page {
Page p{Locale::get("menu.titles.controls"), {}, 0}; Page p{.title = Locale::get("menu.titles.controls"), .items = {}, .cursor = 0, .subtitle = ""};
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_up"), ItemKind::KEY_BIND, 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_down"), ItemKind::KEY_BIND, 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}); p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.left});
p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right}); p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.right});
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_gui.menu_toggle}); p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
return p; return p;
} }
static Page buildAudio() { static auto buildAudio() -> Page {
Page p{Locale::get("menu.titles.audio"), {}, 0}; Page p{.title = Locale::get("menu.titles.audio"), .items = {}, .cursor = 0, .subtitle = ""};
p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) { p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::TOGGLE, [] { return onOff(Options::audio.enabled); }, [](int) {
Options::audio.enabled = !Options::audio.enabled; Options::audio.enabled = !Options::audio.enabled;
Options::applyAudio(); }, nullptr}); Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr}); p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music_enabled); }, [](int) { p.items.push_back({Locale::get("menu.items.music"), ItemKind::TOGGLE, [] { return onOff(Options::audio.music.enabled); }, [](int) {
Options::audio.music_enabled = !Options::audio.music_enabled; Options::audio.music.enabled = !Options::audio.music.enabled;
Options::applyAudio(); }, nullptr}); Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music_volume); }, [](int dir) { stepVolume(Options::audio.music_volume, dir); }, nullptr}); p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound_enabled); }, [](int) { p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::TOGGLE, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
Options::audio.sound_enabled = !Options::audio.sound_enabled; Options::audio.sound.enabled = !Options::audio.sound.enabled;
Options::applyAudio(); }, nullptr}); Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound_volume); }, [](int dir) { stepVolume(Options::audio.sound_volume, dir); }, nullptr}); p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
return p; return p;
} }
static Page buildGame() { static auto buildGame() -> Page {
Page p{Locale::get("menu.titles.game"), {}, 0}; Page p{.title = Locale::get("menu.titles.game"), .items = {}, .cursor = 0, .subtitle = ""};
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}); 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});
p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::Toggle, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr}); p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});
p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});
return p;
}
static auto buildSystem() -> Page {
Page p{.title = Locale::get("menu.titles.system"), .items = {}, .cursor = 0, .subtitle = ""};
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();
}
},
nullptr,
nullptr});
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::ACTION, nullptr, nullptr, [] {
if (Director::get()) {
Director::get()->requestQuit();
}
},
nullptr,
nullptr});
#endif
return p; return p;
} }
@@ -223,34 +309,42 @@ namespace Menu {
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria) // Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) { static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) {
const Uint8 sa = src_alpha; const Uint8 SA = src_alpha;
const Uint8 sr = src_argb & 0xFF; const Uint8 SR = src_argb & 0xFF;
const Uint8 sg = (src_argb >> 8) & 0xFF; const Uint8 SG = (src_argb >> 8) & 0xFF;
const Uint8 sb = (src_argb >> 16) & 0xFF; const Uint8 SB = (src_argb >> 16) & 0xFF;
const Uint8 inv = 255 - sa; const Uint8 INV = 255 - SA;
for (int row = y; row < y + h; row++) { 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++) { for (int col = x; col < x + w; col++) {
if (col < 0 || col >= SCREEN_W) continue; if (col < 0 || col >= SCREEN_W) {
Uint32* p = &buf[col + row * SCREEN_W]; continue;
}
Uint32* p = &buf[col + (row * SCREEN_W)];
Uint32 dst = *p; Uint32 dst = *p;
Uint8 dr = dst & 0xFF; Uint8 dr = dst & 0xFF;
Uint8 dg = (dst >> 8) & 0xFF; Uint8 dg = (dst >> 8) & 0xFF;
Uint8 db = (dst >> 16) & 0xFF; Uint8 db = (dst >> 16) & 0xFF;
Uint8 r = (sr * sa + dr * inv) / 255; Uint8 r = (SR * SA + dr * INV) / 255;
Uint8 g = (sg * sa + dg * inv) / 255; Uint8 g = (SG * SA + dg * INV) / 255;
Uint8 b = (sb * sa + db * 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) { static void fillRect(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
for (int row = y; row < y + h; row++) { 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++) { for (int col = x; col < x + w; col++) {
if (col < 0 || col >= SCREEN_W) continue; if (col < 0 || col >= SCREEN_W) {
buf[col + row * SCREEN_W] = color; continue;
}
buf[col + (row * SCREEN_W)] = color;
} }
} }
} }
@@ -262,129 +356,185 @@ namespace Menu {
fillRect(buf, x + w - 1, y, 1, h, color); fillRect(buf, x + w - 1, y, 1, h, color);
} }
// Mida final de la caixa segons el nombre d'items // Mida final de la caixa segons el nombre d'items *visibles*.
static int boxHeight(const Page& page) { // body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
int n = static_cast<int>(page.items.size()); // sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING; static auto boxHeight(const Page& page) -> int {
return HEADER_H + body + BOTTOM_PAD; const int N = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
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;
} }
// --- API pública --- // --- API pública ---
void init() { void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif"); font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
stack_.clear(); stack.clear();
open_anim_ = 0.0F; open_anim = 0.0F;
last_ticks_ = SDL_GetTicks(); closing = false;
last_ticks = SDL_GetTicks();
} }
void destroy() { void destroy() {
font_.reset(); font.reset();
stack_.clear(); stack.clear();
closing = false;
} }
// "Actiu": accepta input. Durant l'animació de tancament la pila encara
// té pàgines però ja no ha de processar tecles.
auto isOpen() -> bool { auto isOpen() -> bool {
return !stack_.empty(); return !stack.empty() && !closing;
}
// "Visible": encara hi ha caixa per pintar (incloent close animation).
auto isVisible() -> bool {
return !stack.empty();
} }
void toggle() { void toggle() {
if (closing && !stack.empty()) {
// Cancel·la el tancament en curs — continua l'animació cap a "obert"
// des del valor actual d'open_anim.
closing = false;
last_ticks = SDL_GetTicks();
return;
}
if (isOpen()) { if (isOpen()) {
close(); close();
} else { } else {
stack_.push_back(buildRoot()); stack.push_back(buildRoot());
open_anim_ = 0.0F; open_anim = 0.0F;
last_ticks_ = SDL_GetTicks(); closing = false;
animated_h = static_cast<float>(boxHeight(stack.back()));
last_ticks = SDL_GetTicks();
} }
} }
// close() no buida la pila immediatament: marca closing i deixa que
// 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() { void close() {
stack_.clear(); if (stack.empty() || closing) {
open_anim_ = 0.0F; return;
capturing_ = nullptr; }
transition_active_ = false; closing = true;
transition_progress_ = 1.0F; capturing = nullptr;
transition_active = false;
transition_progress = 1.0F;
last_ticks = SDL_GetTicks();
} }
auto isCapturing() -> bool { auto isCapturing() -> bool {
return capturing_ != nullptr; return capturing != nullptr;
} }
void captureKey(SDL_Scancode sc) { void captureKey(SDL_Scancode sc) {
if (!capturing_) return; if (capturing == nullptr) {
return;
}
if (sc == SDL_SCANCODE_ESCAPE) { if (sc == SDL_SCANCODE_ESCAPE) {
// Cancel·la // Cancel·la
capturing_ = nullptr; capturing = nullptr;
return; return;
} }
*capturing_ = sc; *capturing = sc;
capturing_ = nullptr; capturing = nullptr;
} }
void handleKey(SDL_Scancode sc) { static void backOrClose() {
if (!isOpen()) return; if (stack.size() > 1) {
Page& page = stack_.back(); popPage();
if (page.items.empty()) { } else {
// Pàgina buida — només backspace surt close();
if (sc == SDL_SCANCODE_BACKSPACE) {
if (stack_.size() > 1)
popPage();
else
close();
}
return;
} }
const int n = static_cast<int>(page.items.size()); }
// Activació d'un ítem (RETURN/KP_ENTER): SUBMENU/ACTION criden enter,
// KEY_BIND inicia captura, la resta avança change(+1).
static void activateItem(Item& item) {
if (item.kind == ItemKind::SUBMENU || item.kind == ItemKind::ACTION) {
if (item.enter) {
item.enter();
}
} else if (item.kind == ItemKind::KEY_BIND) {
capturing = item.scancode;
} else if (item.change) {
item.change(+1);
}
}
static void applyKeyToItem(Page& page, SDL_Scancode sc) {
Item& item = page.items[page.cursor];
switch (sc) { switch (sc) {
case SDL_SCANCODE_UP: case SDL_SCANCODE_UP:
page.cursor = (page.cursor - 1 + n) % n; page.cursor = nextVisibleCursor(page, page.cursor, -1);
break; break;
case SDL_SCANCODE_DOWN: case SDL_SCANCODE_DOWN:
page.cursor = (page.cursor + 1) % n; page.cursor = nextVisibleCursor(page, page.cursor, +1);
break; break;
case SDL_SCANCODE_LEFT: case SDL_SCANCODE_LEFT:
if (page.items[page.cursor].kind != ItemKind::Submenu && if (item.kind != ItemKind::SUBMENU && item.change) {
page.items[page.cursor].change) { item.change(-1);
page.items[page.cursor].change(-1);
} }
break; break;
case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_RIGHT:
if (page.items[page.cursor].kind != ItemKind::Submenu && if (item.kind != ItemKind::SUBMENU && item.change) {
page.items[page.cursor].change) { item.change(+1);
page.items[page.cursor].change(+1);
} }
break; break;
case SDL_SCANCODE_RETURN: case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_KP_ENTER:
if (page.items[page.cursor].kind == ItemKind::Submenu) { activateItem(item);
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) {
page.items[page.cursor].change(+1);
}
break; break;
case SDL_SCANCODE_BACKSPACE: case SDL_SCANCODE_BACKSPACE:
if (stack_.size() > 1) backOrClose();
popPage();
else
close();
break; break;
default: default:
break; break;
} }
} }
void handleKey(SDL_Scancode sc) {
if (!isOpen()) {
return;
}
Page& page = stack.back();
if (page.items.empty()) {
if (sc == SDL_SCANCODE_BACKSPACE) {
backOrClose();
}
return;
}
if (!isVisible(page.items[page.cursor])) {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
}
applyKeyToItem(page, sc);
// Defensa: si una acció ha amagat l'ítem que tenim sota el cursor,
// saltem al pròxim visible.
if (!stack.empty()) {
Page& top = stack.back();
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
top.cursor = nextVisibleCursor(top, top.cursor, +1);
}
}
}
// Forward decl: renderOneItem viu sota renderPageContent però aquest l'ha de cridar.
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max);
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip. // Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
// box_x/box_y són les coordenades de la caixa (per calcular posicions relatives); // box_x/box_y són les coordenades de la caixa (per calcular posicions relatives);
// clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora. // clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora.
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) { 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 // Títol
int title_w = font_->width(page.title); 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); 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 // Línia sota el títol (també lliscada) — clippada manualment
int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2; int title_line_y = box_y + TITLE_PAD_Y + font->charHeight() + 2;
if (title_line_y >= clip_y_min && title_line_y < clip_y_max) { if (title_line_y >= clip_y_min && title_line_y < clip_y_max) {
int line_x = box_x + 4 + x_offset; int line_x = box_x + 4 + x_offset;
int line_w = BOX_W - 8; int line_w = BOX_W - 8;
@@ -395,104 +545,189 @@ namespace Menu {
} }
} }
// Items o placeholder buit // Subtítol opcional (sota la línia del títol, abans dels items)
int items_y = title_line_y + 4; int items_y = title_line_y + 4;
if (page.items.empty()) { 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;
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)
const int VISIBLE_COUNT = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
if (VISIBLE_COUNT == 0) {
const char* empty_text = Locale::get("menu.values.empty"); const char* empty_text = Locale::get("menu.values.empty");
int ew = font_->width(empty_text); 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; return;
} }
int y_slot = 0;
for (size_t i = 0; i < page.items.size(); i++) { for (size_t i = 0; i < page.items.size(); i++) {
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
bool selected = (static_cast<int>(i) == page.cursor);
const Item& item = page.items[i]; const Item& item = page.items[i];
if (!isVisible(item)) {
if (selected) { continue;
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
if (item.kind == ItemKind::Submenu) {
const char* arrow = ">>";
int aw = font_->width(arrow);
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR;
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");
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);
} else if (item.getValue) {
std::string value = item.getValue();
int value_w = font_->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} }
const int Y = items_y + (y_slot * ITEM_SPACING);
++y_slot;
const bool SELECTED = (static_cast<int>(i) == page.cursor);
renderOneItem(pixel_data, item, SELECTED, box_x, Y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} }
} }
static auto keybindText(const Item& item, bool this_capturing) -> const char* {
const char* text = nullptr;
if (this_capturing) {
text = Locale::get("menu.values.press_key");
} else if (item.scancode != nullptr) {
text = SDL_GetScancodeName(*item.scancode);
} else {
text = "";
}
if ((text == nullptr) || (*text == 0)) {
text = Locale::get("menu.values.unknown");
}
return text;
}
static void renderActionItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
const int LW = font->width(item.label);
const 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);
}
font->drawClipped(pixel_data, LX, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
static auto keybindColor(bool this_capturing, bool selected) -> Uint32 {
if (this_capturing) {
return 0xFF00FFFF;
}
return selected ? CURSOR_COLOR : VALUE_COLOR;
}
static void renderItemValue(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
if (item.kind == ItemKind::SUBMENU) {
const char* arrow = ">>";
const int AW = font->width(arrow);
const Uint32 AC = selected ? CURSOR_COLOR : VALUE_COLOR;
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);
return;
}
if (item.kind == ItemKind::KEY_BIND) {
const bool THIS_CAPTURING = (capturing == item.scancode);
const char* text = keybindText(item, THIS_CAPTURING);
const int TW = font->width(text);
const Uint32 TC = keybindColor(THIS_CAPTURING, selected);
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);
return;
}
if (item.get_value) {
const std::string VALUE = item.get_value();
const int VALUE_W = font->width(VALUE.c_str());
const Uint32 VALUE_COLOR_LOCAL = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - VALUE_W + x_offset, y, VALUE.c_str(), VALUE_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
}
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
if (item.kind == ItemKind::ACTION) {
renderActionItem(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
if (selected) {
font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
renderItemValue(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
void render(Uint32* pixel_data) { void render(Uint32* pixel_data) {
if (!isOpen() || !font_ || !pixel_data) return; if (!isVisible() || !font || (pixel_data == nullptr)) {
return;
}
// Delta time // Delta time
Uint32 now = SDL_GetTicks(); Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(now - last_ticks_) / 1000.0F; float dt = static_cast<float>(now - last_ticks) / 1000.0F;
last_ticks_ = now; last_ticks = now;
if (open_anim_ < 1.0F) { if (closing) {
open_anim_ += OPEN_SPEED * dt; open_anim -= CLOSE_SPEED * dt;
if (open_anim_ > 1.0F) open_anim_ = 1.0F; if (open_anim <= 0.0F) {
// Animació de tancament completada — buida l'estat de veritat.
open_anim = 0.0F;
stack.clear();
animated_h = 0.0F;
closing = false;
return;
}
} else if (open_anim < 1.0F) {
open_anim += OPEN_SPEED * dt;
open_anim = std::min(open_anim, 1.0F);
} }
// Avança transició // Avança transició
if (transition_active_) { if (transition_active) {
transition_progress_ += TRANSITION_SPEED * dt; transition_progress += TRANSITION_SPEED * dt;
if (transition_progress_ >= 1.0F) { if (transition_progress >= 1.0F) {
transition_progress_ = 1.0F; transition_progress = 1.0F;
transition_active_ = false; transition_active = false;
} }
} }
const Page& page = stack_.back(); const Page& page = stack.back();
const int current_h = boxHeight(page); const int CURRENT_H = boxHeight(page);
float eased = Easing::outQuad(open_anim_); // Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles).
// Permet que el menú reaccione amb animació quan una opció canvia la visibilitat
// d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling).
if (animated_h <= 0.0F) {
animated_h = static_cast<float>(CURRENT_H);
} else {
float diff = static_cast<float>(CURRENT_H) - animated_h;
if (std::fabs(diff) < 0.5F) {
animated_h = static_cast<float>(CURRENT_H);
} else {
float t = HEIGHT_RATE * dt;
t = std::min(t, 1.0F);
animated_h += diff * t;
}
}
float eased = Easing::outQuad(open_anim);
// Calcula alçada (amb transició si escau) // Calcula alçada (amb transició si escau)
int target_h = current_h; int target_h = static_cast<int>(animated_h);
if (transition_active_) { if (transition_active) {
int outgoing_h = boxHeight(transition_outgoing_); int outgoing_h = boxHeight(transition_outgoing);
float tp = Easing::outQuad(transition_progress_); float tp = Easing::outQuad(transition_progress);
target_h = Easing::lerpInt(outgoing_h, current_h, tp); target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h), tp);
} }
// Caixa creix verticalment durant l'obertura // Caixa creix verticalment durant l'obertura
int box_h = static_cast<int>(target_h * eased); 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_x = (SCREEN_W - BOX_W) / 2;
int box_y = (SCREEN_H - box_h) / 2; int box_y = (SCREEN_H - box_h) / 2;
// Fons semi-transparent (alpha escalat per l'animació d'obertura) // 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); 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 // El contingut només apareix quan la caixa és prou gran
if (open_anim_ >= 0.9F) { if (open_anim >= 0.9F) {
int clip_x_min = box_x + 1; int clip_x_min = box_x + 1;
int clip_x_max = box_x + BOX_W - 1; int clip_x_max = box_x + BOX_W - 1;
int clip_y_min = box_y + 1; int clip_y_min = box_y + 1;
int clip_y_max = box_y + box_h - 1; int clip_y_max = box_y + box_h - 1;
if (transition_active_) { if (transition_active) {
float tp = Easing::outQuad(transition_progress_); float tp = Easing::outQuad(transition_progress);
int out_offset = static_cast<int>(-transition_dir_ * BOX_W * tp); int out_offset = static_cast<int>(-transition_dir * BOX_W * tp);
int new_offset = static_cast<int>(transition_dir_ * BOX_W * (1.0F - tp)); int new_offset = static_cast<int>(transition_dir * BOX_W * (1.0F - tp));
renderPageContent(pixel_data, transition_outgoing_, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); renderPageContent(pixel_data, transition_outgoing, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} else { } else {
renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max); renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
+5 -1
View File
@@ -6,11 +6,15 @@ namespace Menu {
void init(); void init();
void destroy(); void destroy();
// "Actiu": el menú accepta input. Fals durant l'animació de tancament.
[[nodiscard]] auto isOpen() -> bool; [[nodiscard]] auto isOpen() -> bool;
// "Visible": hi ha una caixa pintada (incloent l'animació de tancament).
// Overlay la usa per a decidir si cridar render().
[[nodiscard]] auto isVisible() -> bool;
void toggle(); void toggle();
void close(); void close();
// Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està obert // Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està visible
void render(Uint32* pixel_data); void render(Uint32* pixel_data);
// Gestió d'input — cridat des del Director en KEY_DOWN // Gestió d'input — cridat des del Director en KEY_DOWN
+271 -282
View File
@@ -14,7 +14,7 @@
namespace Overlay { namespace Overlay {
static std::unique_ptr<Text> font_; static std::unique_ptr<Text> font;
// --- Aspecte de la notificació --- // --- Aspecte de la notificació ---
static constexpr Uint32 NOTIF_BG_COLOR = 0xFF2E1A1A; // Fons blau fosc (ABGR) static constexpr Uint32 NOTIF_BG_COLOR = 0xFF2E1A1A; // Fons blau fosc (ABGR)
@@ -33,7 +33,7 @@ namespace Overlay {
// --- Estat de les notificacions --- // --- Estat de les notificacions ---
enum class Status { RISING, enum class Status : std::uint8_t { RISING,
STAY, STAY,
VANISHING, VANISHING,
FINISHED }; FINISHED };
@@ -52,12 +52,12 @@ namespace Overlay {
int box_h{0}; // Alçada de la caixa (calculat al crear) int box_h{0}; // Alçada de la caixa (calculat al crear)
}; };
static std::vector<Notification> notifications_; static std::vector<Notification> notifications;
static Uint32 last_ticks_ = 0; static Uint32 last_ticks = 0;
// --- Render info --- // --- Render info ---
static Options::RenderInfoPosition info_visible_pos_ = Options::RenderInfoPosition::OFF; static Options::RenderInfoPosition info_visible_pos = Options::RenderInfoPosition::OFF;
static float info_anim_ = 0.0F; // 0 = fora de pantalla, 1 = posició final static float info_anim = 0.0F; // 0 = fora de pantalla, 1 = posició final
static constexpr float INFO_SLIDE_SPEED = 5.0F; static constexpr float INFO_SLIDE_SPEED = 5.0F;
// Segments del render info — cadascú amb la seva pròpia visibilitat animada // Segments del render info — cadascú amb la seva pròpia visibilitat animada
@@ -69,17 +69,17 @@ namespace Overlay {
bool visible{false}; bool visible{false};
bool mono_digits{false}; // si true, dígits amb amplada fixa (la resta natural) bool mono_digits{false}; // si true, dígits amb amplada fixa (la resta natural)
}; };
static InfoSegment info_segments_[INFO_SEGMENT_COUNT]; static InfoSegment info_segments[INFO_SEGMENT_COUNT];
// --- Crèdits cinematogràfics --- // --- Crèdits cinematogràfics ---
// Usen el sistema de notificacions en posició TOP_CENTER_DROP. // Usen el sistema de notificacions en posició TOP_CENTER_DROP.
enum class CreditsPhase { IDLE, enum class CreditsPhase : std::uint8_t { IDLE,
DELAY, DELAY,
PLAYING_1, PLAYING_1,
GAP, GAP,
PLAYING_2 }; PLAYING_2 };
static CreditsPhase credits_phase_ = CreditsPhase::IDLE; static CreditsPhase credits_phase = CreditsPhase::IDLE;
static float credits_timer_ = 0.0F; // segons dins la phase actual static float credits_timer = 0.0F; // segons dins la phase actual
static constexpr float CREDITS_DELAY = 2.0F; static constexpr float CREDITS_DELAY = 2.0F;
static constexpr float CREDITS_GAP = 0.4F; static constexpr float CREDITS_GAP = 0.4F;
static constexpr float CREDITS_HOLD = 7.5F; static constexpr float CREDITS_HOLD = 7.5F;
@@ -87,282 +87,269 @@ namespace Overlay {
static constexpr Uint32 CREDITS_FG = NOTIF_TEXT_COLOR; // mateix cian static constexpr Uint32 CREDITS_FG = NOTIF_TEXT_COLOR; // mateix cian
// --- Doble ESC per a eixir --- // --- Doble ESC per a eixir ---
static bool esc_waiting_ = false; static bool esc_waiting = false;
void init() { void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif"); font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
last_ticks_ = SDL_GetTicks(); last_ticks = SDL_GetTicks();
} }
void destroy() { void destroy() {
font_.reset(); font.reset();
notifications_.clear(); notifications.clear();
} }
// Pinta un rectangle sòlid dins els límits de la pantalla // 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) { static void drawRect(Uint32* pixel_data, int rx, int ry, int rw, int rh, Uint32 color) {
for (int row = ry; row < ry + rh; row++) { 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++) { for (int col = rx; col < rx + rw; col++) {
if (col < 0 || col >= SCREEN_W) continue; if (col < 0 || col >= SCREEN_W) {
pixel_data[col + row * SCREEN_W] = color; continue;
}
pixel_data[col + (row * SCREEN_W)] = color;
} }
} }
} }
static void updateNotifFsm(Notification& notif, float dt) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
}
static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) {
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE:
box_x = NOTIF_MARGIN_X - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = NOTIF_MARGIN_Y;
break;
case NotifPosition::TOP_CENTER_DROP:
box_x = (SCREEN_W - notif.box_w) / 2;
box_y = NOTIF_MARGIN_Y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) {
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) {
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
}
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
}
static void renderOneNotification(Uint32* pixel_data, const Notification& notif) {
int box_x = 0;
int box_y = 0;
computeNotifBoxPos(notif, box_x, box_y);
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
const int LINE_H = font->charHeight();
int line_y = box_y + NOTIF_PADDING_V;
for (const auto& line : notif.lines) {
const int LINE_W = font->width(line.c_str());
const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2);
drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif);
line_y += LINE_H + 1;
}
}
static void updateRenderInfoFsm(float dt) {
const auto DESIRED = Options::render_info.position;
if (DESIRED == info_visible_pos) {
if (info_anim < 1.0F) {
info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F);
}
} else if (info_visible_pos == Options::RenderInfoPosition::OFF) {
info_visible_pos = DESIRED;
info_anim = 0.0F;
} else {
info_anim -= INFO_SLIDE_SPEED * dt;
if (info_anim <= 0.0F) {
info_anim = 0.0F;
info_visible_pos = DESIRED;
}
}
for (auto& seg : info_segments) {
const float TARGET = seg.visible ? 1.0F : 0.0F;
if (seg.anim < TARGET) {
seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET);
} else if (seg.anim > TARGET) {
seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET);
}
}
}
static auto computeInfoTotalWidth(int digit_cell) -> float {
float total_w = 0.0F;
for (const auto& seg : info_segments) {
if (seg.anim > 0.0F && !seg.text.empty()) {
const int W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), digit_cell)
: font->width(seg.text.c_str());
total_w += static_cast<float>(W) * Easing::outQuad(seg.anim);
}
}
return total_w;
}
static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) {
if (seg.mono_digits) {
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, digit_cell);
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, digit_cell);
} else {
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
}
static void renderRenderInfo(Uint32* pixel_data) {
if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) {
return;
}
const int DIGIT_CELL = font->charBoxWidth() - 1;
const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL);
if (TOTAL_W <= 0.0F) {
return;
}
const float EASED_Y = Easing::outQuad(info_anim);
const int CH = font->charHeight();
const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1;
const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H;
const int INFO_Y = START_Y + static_cast<int>(static_cast<float>(FINAL_Y - START_Y) * EASED_Y);
float cur_x = (SCREEN_W - TOTAL_W) / 2.0F;
for (const auto& seg : info_segments) {
if (seg.anim <= 0.01F || seg.text.empty()) {
continue;
}
const int XI = static_cast<int>(cur_x);
const int SEG_W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font->width(seg.text.c_str());
drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL);
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
}
}
static void renderPauseIndicator(Uint32* pixel_data) {
if ((Director::get() == nullptr) || !Director::get()->isPaused()) {
return;
}
const char* pause_text = Locale::get("notifications.pause");
const int W = font->width(pause_text);
const int X = SCREEN_W - W - 4;
const int Y = 4;
font->draw(pixel_data, X, Y - 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y + 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X - 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X + 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF);
}
static void emitCreditsLines(const char* role_key, const char* name_key) {
showNotification(
{std::string(Locale::get(role_key)), std::string(Locale::get(name_key))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
}
static void advanceCredits(float dt) {
if (credits_phase == CreditsPhase::IDLE) {
return;
}
credits_timer += dt;
switch (credits_phase) {
case CreditsPhase::DELAY:
if (credits_timer >= CREDITS_DELAY) {
emitCreditsLines("credits.port_role", "credits.port_name");
credits_phase = CreditsPhase::PLAYING_1;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_1:
if (notifications.empty()) {
credits_phase = CreditsPhase::GAP;
credits_timer = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer >= CREDITS_GAP) {
emitCreditsLines("credits.modern_role", "credits.modern_name");
credits_phase = CreditsPhase::PLAYING_2;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications.empty()) {
credits_phase = CreditsPhase::IDLE;
credits_timer = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
void render(Uint32* pixel_data) { void render(Uint32* pixel_data) {
if (!font_ || !pixel_data) return; if (!font || (pixel_data == nullptr)) {
return;
}
const Uint32 NOW = SDL_GetTicks();
const float DT = static_cast<float>(NOW - last_ticks) / 1000.0F;
last_ticks = NOW;
// Calcula delta time for (auto& notif : notifications) {
Uint32 now = SDL_GetTicks(); updateNotifFsm(notif, DT);
float dt = static_cast<float>(now - last_ticks_) / 1000.0F; if (notif.status != Status::FINISHED) {
last_ticks_ = now; renderOneNotification(pixel_data, notif);
// Actualitza i pinta cada notificació
for (auto& notif : notifications_) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
if (notif.status == Status::FINISHED) continue;
// Posició segons el tipus
int box_x = 0;
int box_y = 0;
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE: {
int target_x = NOTIF_MARGIN_X;
int target_y = NOTIF_MARGIN_Y;
box_x = target_x - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = target_y;
break;
}
case NotifPosition::TOP_CENTER_DROP: {
int target_y = NOTIF_MARGIN_Y;
box_x = (SCREEN_W - notif.box_w) / 2;
// Baixa des de sobre de la pantalla fins a target_y
box_y = target_y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
// Pinta fons (si BOX)
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
// Pinta el text línia a línia (amb ombra o contorn segons style)
int line_h = font_->charHeight();
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
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) {
// Contorn 4-direccional (N, S, E, W)
font_->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font_->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font_->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font_->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
}
font_->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
line_y += line_h + 1;
} }
} }
// Render info (FPS, driver, shader) — animat amb slide vertical updateRenderInfoFsm(DT);
// State machine: visible_pos s'actualitza cap a desired quan anim arriba a 0 renderRenderInfo(pixel_data);
{
const auto desired = Options::render_info.position;
if (desired == info_visible_pos_) {
// 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;
}
} else {
// Canvi: si visible_pos està OFF, commuta directament
if (info_visible_pos_ == Options::RenderInfoPosition::OFF) {
info_visible_pos_ = desired;
info_anim_ = 0.0F;
} else {
// Ix del lloc actual
info_anim_ -= INFO_SLIDE_SPEED * dt;
if (info_anim_ <= 0.0F) {
info_anim_ = 0.0F;
info_visible_pos_ = desired;
}
}
}
// Actualitza animacions individuals dels segments std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
for (auto& seg : info_segments_) { if (esc_waiting && notifications.empty()) {
float target = seg.visible ? 1.0F : 0.0F; esc_waiting = false;
if (seg.anim < target) {
seg.anim += SEG_SPEED * dt;
if (seg.anim > target) seg.anim = target;
} else if (seg.anim > target) {
seg.anim -= SEG_SPEED * dt;
if (seg.anim < target) seg.anim = target;
}
}
// Render si hi ha alguna cosa visible
if (info_visible_pos_ != Options::RenderInfoPosition::OFF && info_anim_ > 0.0F) {
const int DIGIT_CELL = font_->charBoxWidth() - 1; // amplada uniforme per dígit
// Calcula amplada total interpolant cada segment per la seva anim
float total_w = 0.0F;
for (auto& seg : info_segments_) {
if (seg.anim > 0.0F && !seg.text.empty()) {
int w = seg.mono_digits
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font_->width(seg.text.c_str());
total_w += w * Easing::outQuad(seg.anim);
}
}
if (total_w > 0.0F) {
float eased_y = Easing::outQuad(info_anim_);
int ch = font_->charHeight();
int final_y;
int start_y;
if (info_visible_pos_ == Options::RenderInfoPosition::TOP) {
final_y = 1;
start_y = -ch - 1;
} else {
final_y = SCREEN_H - ch - 1;
start_y = SCREEN_H;
}
int info_y = start_y + static_cast<int>((final_y - start_y) * eased_y);
// Dibuixa cada segment en la seva posició x acumulada
float cur_x = (SCREEN_W - total_w) / 2.0F;
for (auto& seg : info_segments_) {
if (seg.anim > 0.01F && !seg.text.empty()) {
int xi = static_cast<int>(cur_x);
int seg_w = seg.mono_digits
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font_->width(seg.text.c_str());
if (seg.mono_digits) {
font_->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL);
font_->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL);
} else {
font_->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font_->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
cur_x += seg_w * Easing::outQuad(seg.anim);
}
}
}
}
} }
// Elimina les acabades renderPauseIndicator(pixel_data);
notifications_.erase( advanceCredits(DT);
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
notifications_.end());
// Si la notificació d'ESC ha desaparegut, reseteja l'estat std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
if (esc_waiting_ && notifications_.empty()) { if (Menu::isVisible()) {
esc_waiting_ = false;
}
// Indicador de pausa persistent (cantó superior dret)
if (Director::get() && Director::get()->isPaused()) {
const char* pause_text = Locale::get("notifications.pause");
int w = font_->width(pause_text);
int x = SCREEN_W - w - 4;
int y = 4;
// Contorn blanc 4-direccional
font_->draw(pixel_data, x, y - 1, pause_text, 0xFFFFFFFF);
font_->draw(pixel_data, x, y + 1, pause_text, 0xFFFFFFFF);
font_->draw(pixel_data, x - 1, y, pause_text, 0xFFFFFFFF);
font_->draw(pixel_data, x + 1, y, pause_text, 0xFFFFFFFF);
// Text en roig
font_->draw(pixel_data, x, y, pause_text, 0xFF0000FF);
}
// Crèdits seqüencials — dispara notificacions TOP_CENTER_DROP una darrere l'altra.
if (credits_phase_ != CreditsPhase::IDLE) {
credits_timer_ += dt;
switch (credits_phase_) {
case CreditsPhase::DELAY:
if (credits_timer_ >= CREDITS_DELAY) {
showNotification(
{std::string(Locale::get("credits.port_role")),
std::string(Locale::get("credits.port_name"))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
credits_phase_ = CreditsPhase::PLAYING_1;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::PLAYING_1:
if (notifications_.empty()) {
credits_phase_ = CreditsPhase::GAP;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer_ >= CREDITS_GAP) {
showNotification(
{std::string(Locale::get("credits.modern_role")),
std::string(Locale::get("credits.modern_name"))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
credits_phase_ = CreditsPhase::PLAYING_2;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications_.empty()) {
credits_phase_ = CreditsPhase::IDLE;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
// Neteja notificacions finalitzades
notifications_.erase(
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
notifications_.end());
// Menú flotant per damunt de tot
if (Menu::isOpen()) {
Menu::render(pixel_data); Menu::render(pixel_data);
} }
} }
@@ -378,7 +365,7 @@ namespace Overlay {
Uint32 accent_color, Uint32 accent_color,
Uint32 text_color) { Uint32 text_color) {
// Reemplaça la notificació anterior // Reemplaça la notificació anterior
notifications_.clear(); notifications.clear();
Notification notif; Notification notif;
notif.lines = lines; notif.lines = lines;
@@ -391,15 +378,15 @@ namespace Overlay {
// Calcula l'amplada màxima de les línies // Calcula l'amplada màxima de les línies
int max_w = 0; int max_w = 0;
for (const auto& line : lines) { for (const auto& line : lines) {
int w = font_->width(line.c_str()); 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; notif.box_w = max_w + NOTIF_PADDING_H * 2;
int line_h = font_->charHeight(); int line_h = font->charHeight();
int line_count = static_cast<int>(lines.size()); int line_count = static_cast<int>(lines.size());
notif.box_h = line_count * line_h + (line_count - 1) * 1 + NOTIF_PADDING_V * 2; notif.box_h = line_count * line_h + (line_count - 1) * 1 + NOTIF_PADDING_V * 2;
notifications_.push_back(notif); notifications.push_back(notif);
} }
void toggleRenderInfo() { cycleRenderInfo(+1); } void toggleRenderInfo() { cycleRenderInfo(+1); }
@@ -414,45 +401,47 @@ namespace Overlay {
void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask) { 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}; const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3};
for (int i = 0; i < INFO_SEGMENT_COUNT; i++) { 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') { if (segs[i] != nullptr && *segs[i] != '\0') {
info_segments_[i].text = segs[i]; info_segments[i].text = segs[i];
info_segments_[i].visible = true; info_segments[i].visible = true;
} else { } else {
info_segments_[i].visible = false; info_segments[i].visible = false;
} }
} }
} }
void startCredits() { void startCredits() {
if (credits_phase_ != CreditsPhase::IDLE) return; if (credits_phase != CreditsPhase::IDLE) {
credits_phase_ = CreditsPhase::DELAY; return;
credits_timer_ = 0.0F; }
credits_phase = CreditsPhase::DELAY;
credits_timer = 0.0F;
} }
void cancelCredits() { void cancelCredits() {
credits_phase_ = CreditsPhase::IDLE; credits_phase = CreditsPhase::IDLE;
credits_timer_ = 0.0F; credits_timer = 0.0F;
notifications_.clear(); notifications.clear();
} }
auto creditsActive() -> bool { auto creditsActive() -> bool {
return credits_phase_ != CreditsPhase::IDLE; return credits_phase != CreditsPhase::IDLE;
} }
auto isEscConsumed() -> bool { auto isEscConsumed() -> bool {
return esc_waiting_; return esc_waiting;
} }
auto handleEscape() -> bool { auto handleEscape() -> bool {
if (!esc_waiting_) { if (!esc_waiting) {
// Primera pulsació: mostra avís i consumeix // Primera pulsació: mostra avís i consumeix
esc_waiting_ = true; esc_waiting = true;
showNotification(Locale::get("notifications.exit_double_esc"), 2.0F); showNotification(Locale::get("notifications.exit_double_esc"), 2.0F);
return true; // Consumit return true; // Consumit
} }
// Segona pulsació: deixa passar // Segona pulsació: deixa passar
esc_waiting_ = false; esc_waiting = false;
return false; return false;
} }
+3 -2
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -13,13 +14,13 @@ namespace Overlay {
void render(Uint32* pixel_data); void render(Uint32* pixel_data);
// Posició + animació d'una notificació // Posició + animació d'una notificació
enum class NotifPosition { enum class NotifPosition : std::uint8_t {
TOP_LEFT_SLIDE, // Cantó superior esquerra, slide horizontal des de fora TOP_LEFT_SLIDE, // Cantó superior esquerra, slide horizontal des de fora
TOP_CENTER_DROP, // Centrat horitzontal, baixa des de sobre TOP_CENTER_DROP, // Centrat horitzontal, baixa des de sobre
}; };
// Estil de la notificació: caixa de fons, ombra o contorn del text // Estil de la notificació: caixa de fons, ombra o contorn del text
enum class NotifStyle { enum class NotifStyle : std::uint8_t {
BOX, // Rectangle de fons amb accent_color BOX, // Rectangle de fons amb accent_color
SHADOW, // Sense fons, text amb ombra (offset +1,+1) en accent_color SHADOW, // Sense fons, text amb ombra (offset +1,+1) en accent_color
OUTLINE, // Sense fons, text amb contorn 4-direccional en accent_color OUTLINE, // Sense fons, text amb contorn 4-direccional en accent_color
+373 -73
View File
@@ -1,28 +1,73 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <iostream> #include <iostream>
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#endif
#include "game/defines.hpp" #include "game/defines.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
Screen* Screen::instance_ = nullptr; #ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
// --- Fix per a fullscreen/resize en Emscripten ---
//
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
// anterior i el viewport queda desencuadrat.
//
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
// del event loop (el canvas encara no està estable en el moment del callback)
// i re-sincronitzar SDL cridant SDL_SetWindowFullscreen + applyFallbackPresentation.
// La crida interna a SDL_SetWindowFullscreen és la peça que realment fa
// resincronitzar l'estat intern de SDL — sense això la logical presentation
// no encaixa amb el canvas real.
namespace {
Screen* g_screen_instance = nullptr;
void deferredCanvasResize(void* /*userData*/) {
if (g_screen_instance != nullptr) {
g_screen_instance->handleCanvasResized();
}
}
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent* event, void* /*userData*/) {
if (g_screen_instance != nullptr && event != nullptr) {
g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
}
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*userData*/) {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif // __EMSCRIPTEN__
std::unique_ptr<Screen> Screen::instance;
void Screen::init() { void Screen::init() {
instance_ = new Screen(); instance = std::unique_ptr<Screen>(new Screen());
} }
void Screen::destroy() { void Screen::destroy() {
delete instance_; instance.reset();
instance_ = nullptr;
} }
auto Screen::get() -> Screen* { auto Screen::get() -> Screen* {
return instance_; return instance.get();
} }
Screen::Screen() { Screen::Screen() {
@@ -32,44 +77,86 @@ Screen::Screen() {
calculateMaxZoom(); calculateMaxZoom();
if (zoom_ < 1) zoom_ = 1; zoom_ = std::max(zoom_, 1);
if (zoom_ > max_zoom_) zoom_ = max_zoom_; 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.
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 w = GAME_WIDTH * zoom_;
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_; int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0); window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
renderer_ = SDL_CreateRenderer(window_, nullptr); renderer_ = SDL_CreateRenderer(window_, nullptr);
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT); texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST); applyFallbackPresentation();
// Inicialitza backend GPU si l'acceleració està activada // Inicialitza backend GPU si l'acceleració està activada
initShaders(); initShaders();
std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n"; std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";
#ifdef __EMSCRIPTEN__
// IMPORTANT: NO registrem resize callback genèric. En mòbil, fer scroll
// fa que el navegador oculti/mostri la barra d'URL, disparant un resize
// del DOM per cada scroll. Això portaria a re-aplicar logical presentation
// per cada scroll i corrompria el viewport intern de SDL.
g_screen_instance = this;
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
} }
Screen::~Screen() { Screen::~Screen() {
#ifdef __EMSCRIPTEN__
g_screen_instance = nullptr;
#endif
// Guarda opcions abans de destruir // Guarda opcions abans de destruir
Options::window.zoom = zoom_; Options::window.zoom = zoom_;
Options::window.fullscreen = fullscreen_; Options::window.fullscreen = fullscreen_;
// Destrueix el backend GPU // Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
if (shader_backend_) { if (shader_backend_) {
#ifndef NO_SHADERS
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get()); auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
if (gpu) gpu->destroy(); if (gpu != nullptr) {
gpu->destroy();
}
#endif
shader_backend_.reset(); shader_backend_.reset();
} }
if (texture_) SDL_DestroyTexture(texture_); if (internal_texture_sdl_ != nullptr) {
if (renderer_) SDL_DestroyRenderer(renderer_); SDL_DestroyTexture(internal_texture_sdl_);
if (window_) SDL_DestroyWindow(window_); }
if (texture_ != nullptr) {
SDL_DestroyTexture(texture_);
}
if (renderer_ != nullptr) {
SDL_DestroyRenderer(renderer_);
}
if (window_ != nullptr) {
SDL_DestroyWindow(window_);
}
} }
void Screen::initShaders() { void Screen::initShaders() {
if (!Options::video.gpu_acceleration) return; #ifdef NO_SHADERS
// Build sense shaders (p.ex. emscripten/WebGL2, on SDL3 GPU no està
// disponible). Es salta tota la inicialització — shader_backend_ es
// queda nul·lptr i tots els `if (shader_backend_)` del render path
// curtcircuiten cap al fallback SDL_Renderer.
return;
#else
if (!Options::video.gpu_acceleration) {
return;
}
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>(); shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
@@ -88,16 +175,11 @@ void Screen::initShaders() {
std::cout << "GPU driver: " << gpu_driver_ << '\n'; std::cout << "GPU driver: " << gpu_driver_ << '\n';
// Aplica opcions de vídeo // Aplica opcions de vídeo
shader_backend_->setScaleMode(Options::video.integer_scale); shader_backend_->setScalingMode(Options::video.scaling_mode);
shader_backend_->setVSync(Options::video.vsync); shader_backend_->setVSync(Options::video.vsync);
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear); shader_backend_->setTextureFilter(Options::video.texture_filter);
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3); shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
shader_backend_->setLinearUpscale(Options::video.linear_upscale); shader_backend_->setInternalResolution(Options::video.internal_resolution);
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
if (Options::video.supersampling) {
shader_backend_->setOversample(3);
}
// Resol el shader actiu des del config // Resol el shader actiu des del config
if (Options::video.current_shader == "crtpi") { if (Options::video.current_shader == "crtpi") {
@@ -122,6 +204,7 @@ void Screen::initShaders() {
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
#endif
} }
void Screen::present(Uint32* pixel_data) { void Screen::present(Uint32* pixel_data) {
@@ -135,14 +218,62 @@ void Screen::present(Uint32* pixel_data) {
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT); shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render(); shader_backend_->render();
} else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { } else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// GPU activa però shaders desactivats: renderitza net (sense efectes) // GPU activa però shaders desactivats: renderitza net (sense efectes).
// Força POSTFX amb params zerats — altrament, si l'actiu és CRTPI,
// els seus efectes (scanlines, curvatura) seguirien aplicant-se encara
// que shader_enabled sigui false. Restaurem l'actiu al final per a
// no trencar la selecció de l'usuari.
Rendering::PostFXParams clean{}; Rendering::PostFXParams clean{};
shader_backend_->setPostFXParams(clean); shader_backend_->setPostFXParams(clean);
const auto PREV_SHADER = shader_backend_->getActiveShader();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT); shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render(); shader_backend_->render();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(PREV_SHADER);
}
} else { } else {
// Fallback SDL_Renderer // Fallback SDL_Renderer. A MULT=1, flux directe original: logical
// presentation (setada per applyFallbackPresentation) + scale mode de
// texture_ segons l'opció. A MULT>1, la còpia intermèdia crea la
// font ampliada (NN via GPU), i es presenta via logical presentation
// a la mida de la font intermèdia.
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32)); SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
const int MULT = Options::video.internal_resolution;
if (MULT > 1) {
ensureFallbackInternalTexture();
if (internal_texture_sdl_ != nullptr) {
// Còpia NN a la textura intermèdia (MULT·game). Sampler NN
// per construcció: volem píxels grans i nets.
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_SetRenderTarget(renderer_, nullptr);
// Filtre global al pas final → finestra (via logical presentation
// que applyFallbackPresentation ja configura amb mida game·MULT).
SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
SDL_SetTextureScaleMode(internal_texture_sdl_, final_scale);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, internal_texture_sdl_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
return;
}
// Si la creació de la textura intermèdia ha fallat, caiem al path normal.
}
// MULT=1 (o fallback-del-fallback): texture_ directament. El scale mode
// el manté applyFallbackPresentation — però el re-apliquem per si la
// ruta MULT>1 el va sobreescriure anteriorment.
SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
SDL_SetTextureScaleMode(texture_, direct_scale);
SDL_RenderClear(renderer_); SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_RenderPresent(renderer_); SDL_RenderPresent(renderer_);
@@ -158,19 +289,25 @@ void Screen::toggleFullscreen() {
} }
void Screen::incZoom() { void Screen::incZoom() {
if (fullscreen_ || zoom_ >= max_zoom_) return; if (fullscreen_ || zoom_ >= max_zoom_) {
return;
}
zoom_++; zoom_++;
adjustWindowSize(); adjustWindowSize();
} }
void Screen::decZoom() { void Screen::decZoom() {
if (fullscreen_ || zoom_ <= 1) return; if (fullscreen_ || zoom_ <= 1) {
return;
}
zoom_--; zoom_--;
adjustWindowSize(); adjustWindowSize();
} }
void Screen::setZoom(int zoom) { void Screen::setZoom(int zoom) {
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) return; if (zoom < 1 || zoom > max_zoom_ || fullscreen_) {
return;
}
zoom_ = zoom; zoom_ = zoom;
adjustWindowSize(); adjustWindowSize();
} }
@@ -182,26 +319,28 @@ void Screen::toggleShaders() {
} }
} }
void Screen::toggleSupersampling() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
Options::video.supersampling = !Options::video.supersampling;
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
}
void Screen::toggleAspectRatio() { void Screen::toggleAspectRatio() {
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3; Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
if (shader_backend_) { if (shader_backend_) {
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3); shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
} else {
applyFallbackPresentation();
} }
if (!fullscreen_) { if (!fullscreen_) {
adjustWindowSize(); adjustWindowSize();
} }
} }
void Screen::toggleIntegerScale() { void Screen::cycleScalingMode(int dir) {
Options::video.integer_scale = !Options::video.integer_scale; constexpr int N = 5; // DISABLED, STRETCH, LETTERBOX, OVERSCAN, INTEGER
int cur = static_cast<int>(Options::video.scaling_mode);
int step = (dir >= 0) ? 1 : -1;
cur = ((cur + step) % N + N) % N;
Options::video.scaling_mode = static_cast<Options::ScalingMode>(cur);
if (shader_backend_) { if (shader_backend_) {
shader_backend_->setScaleMode(Options::video.integer_scale); shader_backend_->setScalingMode(Options::video.scaling_mode);
} else {
applyFallbackPresentation();
} }
} }
@@ -212,15 +351,45 @@ void Screen::toggleVSync() {
} }
} }
void Screen::toggleStretchFilter() { void Screen::cycleTextureFilter(int dir) {
Options::video.stretch_filter_linear = !Options::video.stretch_filter_linear; // NEAREST <-> LINEAR (només 2 valors, dir no importa més enllà de canviar)
(void)dir;
Options::video.texture_filter =
(Options::video.texture_filter == Options::TextureFilter::LINEAR)
? Options::TextureFilter::NEAREST
: Options::TextureFilter::LINEAR;
if (shader_backend_) { if (shader_backend_) {
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear); shader_backend_->setTextureFilter(Options::video.texture_filter);
} else {
applyFallbackPresentation();
} }
} }
void Screen::nextShaderType() { void Screen::changeInternalResolution(int dir) {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
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
// pròxim present via ensureFallbackInternalTexture.
if (shader_backend_) {
shader_backend_->setInternalResolution(next);
} else {
applyFallbackPresentation();
}
}
auto Screen::nextShaderType() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
return false;
}
if (!Options::video.shader_enabled) {
return false;
}
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI); shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
@@ -231,55 +400,80 @@ void Screen::nextShaderType() {
Options::video.current_shader = "postfx"; Options::video.current_shader = "postfx";
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} }
return true;
} }
void Screen::nextPreset() { auto Screen::nextPreset() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
return false;
}
if (!Options::video.shader_enabled) {
return false;
}
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return; if (Options::postfx_presets.empty()) {
return false;
}
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size()); 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; Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} else { } else {
if (Options::crtpi_presets.empty()) return; if (Options::crtpi_presets.empty()) {
return false;
}
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size()); 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; Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
} }
return true;
} }
void Screen::prevShaderType() { auto Screen::prevShaderType() -> bool {
// Només dues opcions — prev == next // Només dues opcions — prev == next
nextShaderType(); return nextShaderType();
} }
void Screen::prevPreset() { auto Screen::prevPreset() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
return false;
}
if (!Options::video.shader_enabled) {
return false;
}
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return; if (Options::postfx_presets.empty()) {
return false;
}
int n = static_cast<int>(Options::postfx_presets.size()); int n = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n; Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name; Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} else { } else {
if (Options::crtpi_presets.empty()) return; if (Options::crtpi_presets.empty()) {
return false;
}
int n = static_cast<int>(Options::crtpi_presets.size()); int n = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n; Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name; Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
} }
return true;
} }
auto Screen::getCurrentPresetName() const -> const char* { 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 (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(); return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
}
} else { } 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 Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
}
} }
return "---"; return "---";
} }
@@ -291,22 +485,30 @@ void Screen::setActiveShader(Rendering::ShaderType type) {
} }
void Screen::applyCurrentPostFXPreset() { 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]; const auto& preset = Options::postfx_presets[Options::current_postfx_preset];
Rendering::PostFXParams p; Rendering::PostFXParams p;
p.vignette = preset.vignette; p.vignette = preset.vignette;
p.scanlines = preset.scanlines; p.scanlines = preset.scanlines;
p.chroma = preset.chroma; p.chroma_min = preset.chroma_min;
p.chroma_max = preset.chroma_max;
p.mask = preset.mask; p.mask = preset.mask;
p.gamma = preset.gamma; p.gamma = preset.gamma;
p.curvature = preset.curvature; p.curvature = preset.curvature;
p.bleeding = preset.bleeding; p.bleeding = preset.bleeding;
p.flicker = preset.flicker; p.flicker = preset.flicker;
p.scan_dark_ratio = preset.scan_dark_ratio;
p.scan_dark_floor = preset.scan_dark_floor;
p.scan_edge_soft = preset.scan_edge_soft;
shader_backend_->setPostFXParams(p); shader_backend_->setPostFXParams(p);
} }
void Screen::applyCurrentCrtPiPreset() { 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]; const auto& preset = Options::crtpi_presets[Options::current_crtpi_preset];
Rendering::CrtPiParams p; Rendering::CrtPiParams p;
p.scanline_weight = preset.scanline_weight; p.scanline_weight = preset.scanline_weight;
@@ -331,12 +533,14 @@ auto Screen::isHardwareAccelerated() const -> bool {
} }
auto Screen::getActiveShaderName() const -> const char* { 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"; return shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX ? "POSTFX" : "CRT-PI";
} }
void Screen::updateRenderInfo() { void Screen::updateRenderInfo() {
static const Uint32 start_ticks = SDL_GetTicks(); static const Uint32 START_TICKS = SDL_GetTicks();
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_); std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
// Segment 0: FPS + driver (sempre visible) // Segment 0: FPS + driver (sempre visible)
@@ -348,27 +552,101 @@ void Screen::updateRenderInfo() {
shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName()); shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName());
} }
// Segment 2: supersampling indicator // Segment 2: hora (només si show_time)
const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr;
// Segment 3: hora (només si show_time)
char time_buf[32] = {0}; char time_buf[32] = {0};
if (Options::render_info.show_time) { if (Options::render_info.show_time) {
Uint32 elapsed = SDL_GetTicks() - start_ticks; Uint32 elapsed = SDL_GetTicks() - START_TICKS;
int minutes = elapsed / 60000; int minutes = elapsed / 60000;
int seconds = (elapsed / 1000) % 60; int seconds = (elapsed / 1000) % 60;
int centis = (elapsed / 10) % 100; int centis = (elapsed / 10) % 100;
snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis); snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis);
} }
// Dígits en mono a FPS (segment 0) i TEMPS (segment 3): els dígits canvien // Dígits en mono a FPS (segment 0) i TEMPS (segment 2): els dígits canvien
// contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no // contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no
Overlay::setRenderInfoSegments( Overlay::setRenderInfoSegments(
fps_driver.c_str(), fps_driver.c_str(),
shader_seg.empty() ? nullptr : shader_seg.c_str(), shader_seg.empty() ? nullptr : shader_seg.c_str(),
ss_seg, (time_buf[0] != 0) ? time_buf : nullptr,
time_buf[0] ? time_buf : nullptr, nullptr,
0b1001); 0b0101);
}
void Screen::applyFallbackPresentation() {
// Fallback SDL_Renderer (p.ex. emscripten/WebGL2 sense shaders GPU).
// Filtre global (texture_filter) s'aplica sempre, independent de 4:3.
SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
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
// d'escollir el mode de finestra; en fallback no tenim eixa capa intermèdia).
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
if (Options::video.aspect_ratio_4_3) {
mode = SDL_LOGICAL_PRESENTATION_STRETCH;
} else {
switch (Options::video.scaling_mode) {
case Options::ScalingMode::DISABLED:
mode = SDL_LOGICAL_PRESENTATION_DISABLED;
break;
case Options::ScalingMode::STRETCH:
mode = SDL_LOGICAL_PRESENTATION_STRETCH;
break;
case Options::ScalingMode::LETTERBOX:
mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
break;
case Options::ScalingMode::OVERSCAN:
mode = SDL_LOGICAL_PRESENTATION_OVERSCAN;
break;
case Options::ScalingMode::INTEGER:
mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
break;
}
}
// Amb resolució interna N > 1, la mida lògica creix proporcionalment
// perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear.
const int MULT = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * MULT, GAME_HEIGHT * MULT, mode);
}
void Screen::ensureFallbackInternalTexture() {
if (renderer_ == nullptr) {
return;
}
const int MULT = Options::video.internal_resolution;
if (MULT <= 1) {
// No cal textura intermèdia — recicla si la teníem.
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
internal_texture_sdl_ = nullptr;
internal_texture_mult_ = 0;
}
return;
}
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == MULT) {
return;
}
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
internal_texture_sdl_ = nullptr;
}
internal_texture_sdl_ = SDL_CreateTexture(renderer_,
SDL_PIXELFORMAT_ABGR8888,
SDL_TEXTUREACCESS_TARGET,
GAME_WIDTH * MULT,
GAME_HEIGHT * MULT);
if (internal_texture_sdl_ == nullptr) {
std::cerr << "Screen: failed to create fallback internal texture (×" << MULT << "): "
<< SDL_GetError() << '\n';
internal_texture_mult_ = 0;
return;
}
internal_texture_mult_ = MULT;
} }
void Screen::adjustWindowSize() { void Screen::adjustWindowSize() {
@@ -382,10 +660,32 @@ void Screen::adjustWindowSize() {
void Screen::calculateMaxZoom() { void Screen::calculateMaxZoom() {
SDL_DisplayID display = SDL_GetPrimaryDisplay(); SDL_DisplayID display = SDL_GetPrimaryDisplay();
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display); const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
if (mode) { if (mode != nullptr) {
int max_w = mode->w / GAME_WIDTH; int max_w = mode->w / GAME_WIDTH;
int max_h = mode->h / GAME_HEIGHT; int max_h = mode->h / GAME_HEIGHT;
max_zoom_ = (max_w < max_h) ? max_w : max_h; max_zoom_ = (max_w < max_h) ? max_w : max_h;
if (max_zoom_ < 1) max_zoom_ = 1; max_zoom_ = std::max(max_zoom_, 1);
} }
} }
#ifdef __EMSCRIPTEN__
// ============================================================================
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
// ============================================================================
void Screen::handleCanvasResized() {
if (window_ == nullptr) return;
// Re-sincronitza l'estat intern de SDL amb el canvas HTML real. La crida
// a SDL_SetWindowFullscreen és l'única manera de forçar SDL a reconèixer
// la mida actual del canvas; després re-apliquem la logical presentation
// (el path WASM sempre va pel fallback SDL_Renderer, sense shaders GPU).
SDL_SetWindowFullscreen(window_, fullscreen_);
applyFallbackPresentation();
}
void Screen::syncFullscreenFlagFromBrowser(bool is_fullscreen) {
fullscreen_ = is_fullscreen;
Options::window.fullscreen = is_fullscreen;
}
#endif
+28 -10
View File
@@ -13,6 +13,8 @@ class Screen {
static void destroy(); static void destroy();
static auto get() -> Screen*; static auto get() -> Screen*;
~Screen(); // públic per a std::unique_ptr
// Presentació — rep el buffer ARGB de 320x200 de JD8 // Presentació — rep el buffer ARGB de 320x200 de JD8
void present(Uint32* pixel_data); void present(Uint32* pixel_data);
@@ -23,16 +25,20 @@ class Screen {
void setZoom(int zoom); void setZoom(int zoom);
// Shaders i vídeo // Shaders i vídeo
// Mètodes que depenen d'una precondició (GPU present, shaders on, etc.)
// retornen `bool`: true si l'acció s'ha aplicat, false si la precondició
// no es complia. Els callers (F-keys, menú) poden suprimir notificacions
// o feedback quan la crida no ha tingut efecte.
void toggleShaders(); void toggleShaders();
void toggleSupersampling();
void toggleAspectRatio(); void toggleAspectRatio();
void toggleIntegerScale(); void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
void toggleVSync(); void toggleVSync();
void toggleStretchFilter(); void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
void nextShaderType(); // Cicla PostFX ↔ CrtPi (F7) void changeInternalResolution(int dir); // +/1, clampat a [1, max_zoom_]
void prevShaderType(); // Cicla al revés auto nextShaderType() -> bool; // false si GPU off / shaders off
void nextPreset(); // Cicla presets del shader actiu (F8) auto prevShaderType() -> bool; // idem
void prevPreset(); // Cicla presets al revés auto nextPreset() -> bool; // false si GPU off / shaders off
auto prevPreset() -> bool; // idem
[[nodiscard]] auto getCurrentPresetName() const -> const char*; [[nodiscard]] auto getCurrentPresetName() const -> const char*;
void setActiveShader(Rendering::ShaderType type); void setActiveShader(Rendering::ShaderType type);
void applyCurrentPostFXPreset(); void applyCurrentPostFXPreset();
@@ -41,24 +47,36 @@ class Screen {
// Getters // Getters
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; } [[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
[[nodiscard]] auto getZoom() const -> int { return zoom_; } [[nodiscard]] auto getZoom() const -> int { return zoom_; }
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
[[nodiscard]] auto isHardwareAccelerated() const -> bool; [[nodiscard]] auto isHardwareAccelerated() const -> bool;
[[nodiscard]] auto getActiveShaderName() const -> const char*; [[nodiscard]] auto getActiveShaderName() const -> const char*;
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; } [[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
[[nodiscard]] auto getRenderer() -> SDL_Renderer* { return renderer_; } [[nodiscard]] auto getRenderer() -> SDL_Renderer* { return renderer_; }
#ifdef __EMSCRIPTEN__
// Sincronització amb el canvas HTML quan el navegador canvia la mida
// (fullscreen entrant/eixint, rotació de mòbil). Cridat pels callbacks
// natius d'Emscripten registrats al constructor.
void handleCanvasResized();
void syncFullscreenFlagFromBrowser(bool is_fullscreen);
#endif
private: private:
Screen(); Screen();
~Screen();
void adjustWindowSize(); void adjustWindowSize();
void calculateMaxZoom(); void calculateMaxZoom();
void initShaders(); void initShaders();
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
static Screen* instance_; static std::unique_ptr<Screen> instance;
SDL_Window* window_{nullptr}; SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{nullptr}; SDL_Renderer* renderer_{nullptr};
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer) SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
SDL_Texture* internal_texture_sdl_{nullptr}; // 320·N x 200·N TARGET (fallback path, només si N>1)
int internal_texture_mult_{0}; // Multiplicador amb què es va crear internal_texture_sdl_
// Backend GPU (nullptr si no disponible o desactivat) // Backend GPU (nullptr si no disponible o desactivat)
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,144 @@
#pragma once
#ifdef __APPLE__
// Fragment shader del shader "crtpi" (algoritme CRT-Pi): scanlines amb
// pesos gaussians, multisample opcional, gamma i màscara de subpíxels.
namespace Rendering::Msl {
inline constexpr const char* kCrtpiFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct CrtPiUniforms {
float scanline_weight;
float scanline_gap_brightness;
float bloom_factor;
float input_gamma;
float output_gamma;
float mask_brightness;
float curvature_x;
float curvature_y;
int mask_type;
int enable_scanlines;
int enable_multisample;
int enable_gamma;
int enable_curvature;
int enable_sharper;
float texture_width;
float texture_height;
};
static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) {
float2 curvature = float2(cx, cy);
float2 barrel_scale = 1.0f - (0.23f * curvature);
coord *= screen_scale;
coord -= 0.5f;
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); }
coord += 0.5f;
coord /= screen_scale;
return coord;
}
static float crtpi_scan_weight(float dist, float sw, float gap) {
return max(1.0f - dist * dist * sw, gap);
}
static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) {
float w = crtpi_scan_weight(dy, sw, gap);
if (ms) {
w += crtpi_scan_weight(dy - filter_w, sw, gap);
w += crtpi_scan_weight(dy + filter_w, sw, gap);
w *= 0.3333333f;
}
return w;
}
fragment float4 crtpi_fs(PostVOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]],
constant CrtPiUniforms& u [[buffer(0)]]) {
float2 tex_size = float2(u.texture_width, u.texture_height);
// Amplada del filtre de scanline analític. 768 = alçada de referència
// CRT a la qual es va tarar l'algoritme original; 3 = divisió per
// subpíxel (R/G/B) del multisample. El resultat escala amb la textura
// d'entrada, de manera que més alçada → filtre més fi.
const float CRT_REFERENCE_HEIGHT = 768.0f;
const float SUBPIXEL_DIV = 3.0f;
float filter_width = (CRT_REFERENCE_HEIGHT / u.texture_height) / SUBPIXEL_DIV;
float2 texcoord = in.uv;
if (u.enable_curvature != 0) {
texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y);
if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); }
}
float2 coord_in_pixels = texcoord * tex_size;
float2 tc;
float scan_weight;
if (u.enable_sharper != 0) {
float2 temp = floor(coord_in_pixels) + 0.5f;
tc = temp / tex_size;
float2 deltas = coord_in_pixels - temp;
scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float2 signs = sign(deltas);
deltas.x *= 2.0f;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5f;
deltas.y *= 8.0f;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
float temp_y = floor(coord_in_pixels.y) + 0.5f;
float y_coord = temp_y / tex_size.y;
float dy = coord_in_pixels.y - temp_y;
scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0f;
dy /= tex_size.y;
dy *= sign_y;
tc = float2(texcoord.x, y_coord + dy);
}
float3 colour = tex.sample(samp, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); }
colour *= scan_weight * u.bloom_factor;
if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); }
}
if (u.mask_type == 1) {
float wm = fract(in.pos.x * 0.5f);
float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness)
: float3(1.0f, u.mask_brightness, 1.0f);
colour *= mask;
} else if (u.mask_type == 2) {
float wm = fract(in.pos.x * 0.3333333f);
float3 mask = float3(u.mask_brightness);
if (wm < 0.3333333f) mask.x = 1.0f;
else if (wm < 0.6666666f) mask.y = 1.0f;
else mask.z = 1.0f;
colour *= mask;
}
return float4(colour, 1.0f);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,168 @@
#pragma once
#ifdef __APPLE__
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
//
// IMPORTANT: mantenir sincronitzat a mà amb data/shaders/postfx.frag. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi a la struct d'uniforms o a la lògica del GLSL cal replicar-lo ací al
// mateix commit. Mida total = 64 bytes (4 × vec4).
namespace Rendering::Msl {
inline constexpr const char* kPostfxFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct PostFXUniforms {
float vignette_strength;
float chroma_min;
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale;
float time;
float flicker;
float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio;
float scan_dark_floor;
float scan_edge_soft;
float pad3;
};
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel.
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
float2 tex_size = float2(scene.get_width(), scene.get_height());
float px = uv_target.x * tex_size.x - 0.5f;
float p_floor = floor(px);
float f = px - p_floor;
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
static float3 rgb_to_ycc(float3 rgb) {
return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
);
}
static float3 ycc_to_rgb(float3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5f;
float cr = ycc.z - 0.5f;
return clamp(float3(
y + 1.402f*cr,
y - 0.344f*cb - 0.714f*cr,
y + 1.772f*cb
), 0.0f, 1.0f);
}
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFXUniforms& u [[buffer(0)]]) {
float2 uv = in.uv;
if (u.curvature > 0.0f) {
float2 c = uv - 0.5f;
float rsq = dot(c, c);
float2 dist = float2(0.05f, 0.1f) * u.curvature;
float2 barrelScale = 1.0f - 0.23f * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
return float4(0.0f, 0.0f, 0.0f, 1.0f);
}
uv = c + 0.5f;
}
float3 base = scene.sample(samp, uv).rgb;
float3 colour;
if (u.bleeding > 0.0f) {
float tw = float(scene.get_width());
float step = 1.0f / tw;
float3 ycc = rgb_to_ycc(base);
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else {
colour = base;
}
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
// NEAREST sampler amb offsets subpíxel.
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
}
if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
// suavitzada amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge
// analític continu). scan_edge_soft = 0 recupera el step dur de l'original.
if (u.scanline_strength > 0.0f) {
float ps = max(u.pixel_scale, 1.0f);
float sub = fract(uv.y * u.screen_height);
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
float d = abs(sub - dark_center);
d = min(d, 1.0f - d);
float half_width = u.scan_dark_ratio * 0.5f;
float softness = u.scan_edge_soft * 0.5f / ps;
float band = 1.0f - smoothstep(half_width - softness, half_width + softness, d);
float scan = mix(1.0f, u.scan_dark_floor, band);
colour *= mix(1.0f, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0f) {
float3 enc = pow(colour, float3(1.0f/2.2f));
colour = mix(colour, enc, u.gamma_strength);
}
float2 d = uv - 0.5f;
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0f, 1.0f);
if (u.mask_strength > 0.0f) {
float whichMask = fract(in.pos.x * 0.3333333f);
float3 mask = float3(0.80f);
if (whichMask < 0.3333333f) mask.x = 1.0f;
else if (whichMask < 0.6666667f) mask.y = 1.0f;
else mask.z = 1.0f;
colour = mix(colour, colour * mask, u.mask_strength);
}
if (u.flicker > 0.0f) {
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
}
return float4(colour, 1.0f);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,30 @@
#pragma once
#ifdef __APPLE__
// Vertex shader compartit per tots els pipelines de post-procés:
// fullscreen-triangle que cobreix tota l'àrea del swapchain amb UVs a [0,1].
namespace Rendering::Msl {
inline constexpr const char* kPostfxVert = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,23 @@
#pragma once
#ifdef __APPLE__
// Fragment shader d'upscale (mostreig directe). S'usa per al pas de resolució
// interna (scene_texture → internal_texture) quan internal_resolution > 1.
namespace Rendering::Msl {
inline constexpr const char* kUpscaleFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct VertOut { float4 pos [[position]]; float2 uv; };
fragment float4 upscale_fs(VertOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler smp [[sampler(0)]])
{
return scene.sample(smp, in.uv);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
File diff suppressed because it is too large Load Diff
@@ -7,20 +7,28 @@
// PostFX uniforms pushed to fragment stage each frame. // PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout. // Must match the MSL struct and GLSL uniform block layout.
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement. // 16 floats = 64 bytes (4 × vec4) — meets Metal/Vulkan 16-byte alignment.
struct PostFXUniforms { struct PostFXUniforms {
// vec4 #0
float vignette_strength; // 0 = none, ~0.8 = subtle float vignette_strength; // 0 = none, ~0.8 = subtle
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration float chroma_min; // aberració cromàtica mínima (sempre present)
float scanline_strength; // 0 = off, 1 = full float scanline_strength; // 0 = off, 1 = full
float screen_height; // logical height in pixels (used by bleeding effect) float screen_height; // logical height in pixels (used by bleeding effect)
float mask_strength; // 0 = off, 1 = full phosphor dot mask // vec4 #1
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction float mask_strength; // 0 = off, 1 = full phosphor dot mask
float curvature; // 0 = flat, 1 = max barrel distortion float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding float curvature; // 0 = flat, 1 = max barrel distortion
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_) float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f) // vec4 #2
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS) float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16) float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float chroma_max; // si == chroma_min queda estàtic; si != pulsa sinusoidalment
// vec4 #3 — paràmetres de forma de les scanlines (exposats per preset)
float scan_dark_ratio; // fracció de subfila fosca (1/3 = 0.333 per defecte)
float scan_dark_floor; // brillantor de la subfila fosca (0.42 per defecte)
float scan_edge_soft; // suavitzat de la transició (0 = step dur, 1 = 1px físic)
float pad3;
}; };
// CrtPi uniforms pushed to fragment stage each frame. // CrtPi uniforms pushed to fragment stage each frame.
@@ -49,15 +57,6 @@ struct CrtPiUniforms {
float texture_height; // Alto del canvas en píxeles (inyectado en render) float texture_height; // Alto del canvas en píxeles (inyectado en render)
}; };
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
struct DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
float pad0;
float pad1;
float pad2;
};
namespace Rendering { namespace Rendering {
/** /**
@@ -78,7 +77,7 @@ namespace Rendering {
const std::string& fragment_source) -> bool override; const std::string& fragment_source) -> bool override;
void render() override; void render() override;
void setTextureSize(float width, float height) override {} void setTextureSize(float /*width*/, float /*height*/) override {}
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; } [[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
@@ -96,20 +95,8 @@ namespace Rendering {
// Activa/desactiva VSync en el swapchain // Activa/desactiva VSync en el swapchain
void setVSync(bool vsync) override; void setVSync(bool vsync) override;
// Activa/desactiva escalado entero (integer scale) // Selecciona el mode de presentació lògica (DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER)
void setScaleMode(bool integer_scale) override; void setScalingMode(Options::ScalingMode mode) override;
// Establece factor de supersampling (1 = off, 3 = 3×SS)
void setOversample(int factor) override;
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
void setLinearUpscale(bool linear) override;
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
void setDownscaleAlgo(int algo) override;
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI) // Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
void setActiveShader(ShaderType type) override; void setActiveShader(ShaderType type) override;
@@ -121,9 +108,16 @@ namespace Rendering {
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; } [[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
// Estirament vertical 4:3 (fusionat amb l'upscale pass) // Estirament vertical 4:3 (fusionat amb l'upscale pass)
void setStretch4_3(bool enabled) override; void setStretch43(bool enabled) override;
[[nodiscard]] auto isStretch4_3() const -> bool override { return stretch_4_3_; } [[nodiscard]] auto isStretch43() const -> bool override { return stretch_4_3_; }
void setStretchFilter(bool linear) override { stretch_filter_linear_ = linear; }
// Filtre de textura global (sempre aplicat, independent de 4:3)
void setTextureFilter(Options::TextureFilter filter) override {
texture_filter_linear_ = (filter == Options::TextureFilter::LINEAR);
}
// Multiplicador de resolució interna (1 = off).
void setInternalResolution(int multiplier) override;
private: private:
static auto createShaderMSL(SDL_GPUDevice* device, static auto createShaderMSL(SDL_GPUDevice* device,
@@ -142,44 +136,69 @@ namespace Rendering {
Uint32 num_uniform_buffers) -> SDL_GPUShader*; Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool; auto createPipeline() -> bool;
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_ auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC // Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode; [[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
SDL_Window* window_ = nullptr; SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr; SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_) SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass → swapchain
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS) SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass → swapchain
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos) SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale per al pas de resolució interna
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS) SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0) SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor, amb 4:3 si actiu)
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
SDL_GPUTransferBuffer* upload_buffer_ = nullptr; SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; // NEAREST SDL_GPUSampler* sampler_ = nullptr; // NEAREST
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR (per texture_filter_linear_)
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 200.0F, .pixel_scale = 1.0F, .oversample = 1.0F}; PostFXUniforms uniforms_{
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1}; .vignette_strength = 0.6F,
.chroma_min = 0.15F,
.scanline_strength = 0.7F,
.screen_height = 200.0F,
.mask_strength = 0.0F,
.gamma_strength = 0.0F,
.curvature = 0.0F,
.bleeding = 0.0F,
.pixel_scale = 1.0F,
.time = 0.0F,
.flicker = 0.0F,
.chroma_max = 0.15F,
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
.scan_edge_soft = 1.0F,
.pad3 = 0.0F};
CrtPiUniforms crtpi_uniforms_{
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = 1,
.enable_multisample = 1,
.enable_gamma = 1,
.enable_curvature = 0,
.enable_sharper = 0,
.texture_width = 0.0F,
.texture_height = 0.0F};
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
int game_width_ = 0; // Dimensions originals del canvas int game_width_ = 0; // Dimensions originals del canvas
int game_height_ = 0; int game_height_ = 0;
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
std::string driver_name_; std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige) std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false; bool is_initialized_ = false;
bool vsync_ = true; bool vsync_ = true;
bool integer_scale_ = false; Options::ScalingMode scaling_mode_ = Options::ScalingMode::INTEGER;
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
bool stretch_4_3_ = false; // Estirament vertical 4:3 bool stretch_4_3_ = false; // Estirament vertical 4:3
bool stretch_filter_linear_ = false; // Filtre per a l'estirament 4:3 (false=NEAREST, true=LINEAR) bool texture_filter_linear_ = false; // Filtre global (false=NEAREST, true=LINEAR)
}; };
} // namespace Rendering } // namespace Rendering
@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never
@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
+25 -36
View File
@@ -2,13 +2,16 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
#include <string> #include <string>
#include <utility> #include <utility>
#include "game/options.hpp"
namespace Rendering { namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */ /** @brief Identificador del shader de post-procesado activo */
enum class ShaderType { POSTFX, enum class ShaderType : std::uint8_t { POSTFX,
CRTPI }; CRTPI };
/** /**
@@ -18,12 +21,19 @@ namespace Rendering {
struct PostFXParams { struct PostFXParams {
float vignette = 0.0F; // Intensidad de la viñeta float vignette = 0.0F; // Intensidad de la viñeta
float scanlines = 0.0F; // Intensidad de las scanlines float scanlines = 0.0F; // Intensidad de las scanlines
float chroma = 0.0F; // Aberración cromática // Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
// queda estàtica. min > 0 garanteix que la imatge mai sigui lliure de chroma.
float chroma_min = 0.0F;
float chroma_max = 0.0F;
float mask = 0.0F; // Máscara de fósforo RGB float mask = 0.0F; // Máscara de fósforo RGB
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full) float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
float curvature = 0.0F; // Curvatura barrel CRT float curvature = 0.0F; // Curvatura barrel CRT
float bleeding = 0.0F; // Sangrado de color NTSC float bleeding = 0.0F; // Sangrado de color NTSC
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
float scan_dark_ratio = 0.333F; // fracció obscura (1/3)
float scan_dark_floor = 0.42F; // brillantor subfila fosca
float scan_edge_soft = 1.0F; // 0 = step dur; 1 = suavitzat 1 px físic
}; };
/** /**
@@ -105,37 +115,9 @@ namespace Rendering {
virtual void setVSync(bool /*vsync*/) {} virtual void setVSync(bool /*vsync*/) {}
/** /**
* @brief Activa o desactiva el escalado entero (integer scale) * @brief Selecciona el mode d'escala de la finestra (mapeja SDL_RendererLogicalPresentation).
*/ */
virtual void setScaleMode(bool /*integer_scale*/) {} virtual void setScalingMode(Options::ScalingMode /*mode*/) {}
/**
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
* Con factor > 1, la textura GPU se crea a game×factor resolución y
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
*/
virtual void setOversample(int /*factor*/) {}
/**
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
*/
virtual void setLinearUpscale(bool /*linear*/) {}
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
/**
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
*/
virtual void setDownscaleAlgo(int /*algo*/) {}
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
/**
* @brief Devuelve las dimensiones de la textura de supersampling.
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
*/
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
/** /**
* @brief Verifica si el backend está usando aceleración por hardware * @brief Verifica si el backend está usando aceleración por hardware
@@ -175,13 +157,20 @@ namespace Rendering {
* @brief Activa/desactiva estirament vertical 4:3 (200→240 línies efectives). * @brief Activa/desactiva estirament vertical 4:3 (200→240 línies efectives).
* Només afecta el viewport, no les textures ni els shaders. * Només afecta el viewport, no les textures ni els shaders.
*/ */
virtual void setStretch4_3(bool /*enabled*/) {} virtual void setStretch43(bool /*enabled*/) {}
[[nodiscard]] virtual auto isStretch4_3() const -> bool { return false; } [[nodiscard]] virtual auto isStretch43() const -> bool { return false; }
/** /**
* @brief Filtre per a l'estirament 4:3 (false=NEAREST, true=LINEAR). * @brief Filtre de textura global per a l'upscale final (sempre aplicat).
*/ */
virtual void setStretchFilter(bool /*linear*/) {} virtual void setTextureFilter(Options::TextureFilter /*filter*/) {}
/**
* @brief Multiplicador enter de la "resolució interna": fa un NN upscale
* de scene (320×200) a 320·N × 200·N i la pipeline downstream
* parteix d'aquesta textura. 1 = off (sense còpia addicional).
*/
virtual void setInternalResolution(int /*multiplier*/) {}
}; };
} // namespace Rendering } // namespace Rendering
+149 -198
View File
@@ -1,5 +1,6 @@
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -8,26 +9,25 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "core/jail/jfile.hpp" #include "core/resources/resource_helper.hpp"
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades) // Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
struct rgb; struct rgb;
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h); // NOLINTNEXTLINE(readability-identifier-naming) — exportat per external/gif.h, no controlem el nom.
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
Text::Text(const char* fnt_file, const char* gif_file) { Text::Text(const char* fnt_file, const char* gif_file) {
loadBitmap(gif_file); loadBitmap(gif_file);
loadFont(fnt_file); loadFont(fnt_file);
} }
Text::~Text() {
if (bitmap_) free(bitmap_);
}
// --- UTF-8 --- // --- UTF-8 ---
auto Text::nextCodepoint(const char*& ptr) -> uint32_t { auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
auto byte = static_cast<uint8_t>(*ptr); auto byte = static_cast<uint8_t>(*ptr);
if (byte == 0) return 0; if (byte == 0) {
return 0;
}
uint32_t cp = 0; uint32_t cp = 0;
int extra = 0; int extra = 0;
@@ -51,7 +51,9 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
ptr++; ptr++;
for (int i = 0; i < extra; i++) { for (int i = 0; i < extra; i++) {
auto cont = static_cast<uint8_t>(*ptr); auto cont = static_cast<uint8_t>(*ptr);
if ((cont & 0xC0) != 0x80) return 0xFFFD; if ((cont & 0xC0) != 0x80) {
return 0xFFFD;
}
cp = (cp << 6) | (cont & 0x3F); cp = (cp << 6) | (cont & 0x3F);
ptr++; ptr++;
} }
@@ -62,47 +64,47 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
// --- Càrrega de font --- // --- Càrrega de font ---
void Text::loadFont(const char* fnt_file) { void Text::loadFont(const char* fnt_file) {
int filesize = 0; auto buffer = ResourceHelper::loadFile(fnt_file);
char* buffer = file_getfilebuffer(fnt_file, filesize, true); if (buffer.empty()) {
if (!buffer) {
std::cerr << "Text: unable to load font file: " << fnt_file << '\n'; std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
return; return;
} }
std::istringstream stream(std::string(buffer, filesize)); std::istringstream stream(std::string(reinterpret_cast<const char*>(buffer.data()), buffer.size()));
free(buffer);
std::string line; std::string line;
int glyph_index = 0; int glyph_index = 0;
while (std::getline(stream, line)) { while (std::getline(stream, line)) {
// Ignora comentaris i línies buides // Ignora comentaris i línies buides
if (line.empty() || line[0] == '#') continue; if (line.empty() || line[0] == '#') {
continue;
}
// Elimina comentaris inline // Elimina comentaris inline
auto comment_pos = line.find('#'); auto comment_pos = line.find('#');
if (comment_pos != std::string::npos) { if (comment_pos != std::string::npos) {
line = line.substr(0, comment_pos); line.resize(comment_pos);
} }
// Parseja directives // Parseja directives
if (line.find("box_width") == 0) { if (line.starts_with("box_width")) {
sscanf(line.c_str(), "box_width %d", &box_width_); sscanf(line.c_str(), "box_width %d", &box_width_);
continue; continue;
} }
if (line.find("box_height") == 0) { if (line.starts_with("box_height")) {
sscanf(line.c_str(), "box_height %d", &box_height_); sscanf(line.c_str(), "box_height %d", &box_height_);
continue; continue;
} }
if (line.find("columns") == 0) { if (line.starts_with("columns")) {
sscanf(line.c_str(), "columns %d", &columns_); sscanf(line.c_str(), "columns %d", &columns_);
continue; continue;
} }
if (line.find("cell_spacing") == 0) { if (line.starts_with("cell_spacing")) {
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_); sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
continue; continue;
} }
if (line.find("row_spacing") == 0) { if (line.starts_with("row_spacing")) {
sscanf(line.c_str(), "row_spacing %d", &row_spacing_); sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
continue; continue;
} }
@@ -128,80 +130,86 @@ void Text::loadFont(const char* fnt_file) {
} }
void Text::loadBitmap(const char* gif_file) { void Text::loadBitmap(const char* gif_file) {
int filesize = 0; auto buffer = ResourceHelper::loadFile(gif_file);
char* buffer = file_getfilebuffer(gif_file, filesize); if (buffer.empty()) {
if (!buffer) {
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n'; std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
return; return;
} }
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian) // Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
auto* raw = reinterpret_cast<unsigned char*>(buffer); auto* raw = buffer.data();
int w = raw[6] | (raw[7] << 8); int w = raw[6] | (raw[7] << 8);
int h = raw[8] | (raw[9] << 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); Uint8* pixels = LoadGif(raw, &gw, &gh);
if (!pixels) { if (pixels == nullptr) {
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n'; std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
free(buffer);
return; return;
} }
bitmap_width_ = w; bitmap_width_ = w;
bitmap_height_ = h; bitmap_height_ = h;
bitmap_ = pixels; bitmap_.assign(pixels, pixels + (static_cast<size_t>(w) * h));
free(pixels); // LoadGif usa malloc internament
free(buffer);
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n'; std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
} }
// --- Renderitzat --- // --- Renderitzat ---
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const { auto Text::resolveGlyph(uint32_t cp) const -> const GlyphInfo* {
if (!bitmap_ || !pixel_data) return; auto it = glyphs_.find(cp);
if (it != glyphs_.end()) {
return &it->second;
}
it = glyphs_.find('?');
return (it != glyphs_.end()) ? &it->second : nullptr;
}
const char* ptr = text; void Text::blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
int cursor_x = x; const int GY_START = std::max(0, clip_y_min - dst_y);
const int GY_END = std::min(box_height_, clip_y_max - dst_y);
while (*ptr) { const int GX_START = std::max(0, clip_x_min - dst_x);
uint32_t cp = nextCodepoint(ptr); const int GX_END = std::min(glyph.w, clip_x_max - dst_x);
if (cp == 0) break; for (int gy = GY_START; gy < GY_END; gy++) {
const int SRC_Y = glyph.y + gy;
auto it = glyphs_.find(cp); if (SRC_Y >= bitmap_height_) {
if (it == glyphs_.end()) { continue;
it = glyphs_.find('?'); }
if (it == glyphs_.end()) { const int DST_ROW = dst_y + gy;
cursor_x += box_width_; for (int gx = GX_START; gx < GX_END; gx++) {
const int SRC_X = glyph.x + gx;
if (SRC_X >= bitmap_width_) {
continue; continue;
} }
} const Uint8 PIXEL = bitmap_[SRC_X + (SRC_Y * bitmap_width_)];
if (PIXEL != 0) {
const auto& glyph = it->second; pixel_data[(dst_x + gx) + (DST_ROW * SCREEN_WIDTH)] = 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;
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = cursor_x + gx;
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_];
// Píxel no transparent (índex 0 és fons típicament)
if (pixel != 0) {
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
}
} }
} }
}
}
cursor_x += glyph.w + 1; // +1 kerning void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
continue;
}
blitGlyph(pixel_data, cursor_x, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += glyph->w + 1;
} }
} }
@@ -212,152 +220,82 @@ 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 { 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_ || !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 int X_MIN = std::max(0, clip_x_min);
const int X_MAX = std::min(SCREEN_WIDTH, clip_x_max);
const int Y_MIN = std::max(0, clip_y_min);
const int Y_MAX = std::min(SCREEN_HEIGHT, clip_y_max);
const char* ptr = text; const char* ptr = text;
int cursor_x = x; int cursor_x = x;
while (*ptr != 0) {
while (*ptr) {
uint32_t cp = nextCodepoint(ptr); 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()) {
cursor_x += box_width_;
continue;
}
} }
const GlyphInfo* glyph = resolveGlyph(cp);
const auto& glyph = it->second; if (glyph == nullptr) {
cursor_x += box_width_;
// Si el glifo està completament fora del clip horitzontal, salta
if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) {
cursor_x += glyph.w + 1;
continue; continue;
} }
if (cursor_x + glyph->w > X_MIN && cursor_x < X_MAX) {
for (int gy = 0; gy < box_height_; gy++) { blitGlyph(pixel_data, cursor_x, y, *glyph, color, X_MIN, X_MAX, Y_MIN, Y_MAX);
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;
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;
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 (pixel != 0) {
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
}
}
} }
cursor_x += glyph->w + 1;
cursor_x += glyph.w + 1;
} }
} }
void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const { void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const {
if (!bitmap_ || !pixel_data) return; if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text; const char* ptr = text;
int cursor_x = x; int cursor_x = x;
while (*ptr != 0) {
while (*ptr) {
uint32_t cp = nextCodepoint(ptr); 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()) {
cursor_x += cell_w;
continue;
}
} }
const GlyphInfo* glyph = resolveGlyph(cp);
const auto& glyph = it->second; if (glyph == nullptr) {
// Centra el glif dins la cel·la cursor_x += cell_w;
int glyph_x = cursor_x + (cell_w - glyph.w) / 2; continue;
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
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;
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 (pixel != 0) {
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
}
}
} }
const int GLYPH_X = cursor_x + ((cell_w - glyph->w) / 2);
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += cell_w; cursor_x += cell_w;
} }
} }
void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const { void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const {
if (!bitmap_ || !pixel_data) return; if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text; const char* ptr = text;
int cursor_x = x; int cursor_x = x;
bool first = true; bool first = true;
while (*ptr != 0) {
while (*ptr) {
uint32_t cp = nextCodepoint(ptr); 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;
cursor_x += box_width_;
first = false;
continue;
}
} }
if (!first) {
const auto& glyph = it->second; cursor_x += 1; // kerning
bool is_digit = (cp >= '0' && cp <= '9');
if (!first) cursor_x += 1; // kerning
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;
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = glyph_x + gx;
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 (pixel != 0) {
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
}
}
} }
const GlyphInfo* glyph = resolveGlyph(cp);
cursor_x += is_digit ? digit_cell_w : glyph.w; if (glyph == nullptr) {
cursor_x += box_width_;
first = false;
continue;
}
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
const int GLYPH_X = IS_DIGIT ? cursor_x + ((digit_cell_w - glyph->w) / 2) : cursor_x;
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += IS_DIGIT ? digit_cell_w : glyph->w;
first = false; first = false;
} }
} }
@@ -366,32 +304,41 @@ auto Text::widthMonoDigits(const char* text, int digit_cell_w) const -> int {
const char* ptr = text; const char* ptr = text;
int w = 0; int w = 0;
bool first = true; bool first = true;
while (*ptr) { while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr); uint32_t cp = nextCodepoint(ptr);
if (cp == 0) break; if (cp == 0) {
if (!first) w += 1; // kerning break;
}
if (!first) {
w += 1; // kerning
}
first = false; first = false;
bool is_digit = (cp >= '0' && cp <= '9'); bool is_digit = (cp >= '0' && cp <= '9');
if (is_digit) { if (is_digit) {
w += digit_cell_w; w += digit_cell_w;
} else { } else {
auto it = glyphs_.find(cp); 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; w += it->second.w;
else } else {
w += box_width_; w += box_width_;
}
} }
} }
return w; 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; const char* ptr = text;
int count = 0; int count = 0;
while (*ptr) { while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr); uint32_t cp = nextCodepoint(ptr);
if (cp == 0) break; if (cp == 0) {
break;
}
count++; count++;
} }
return count * cell_w; return count * cell_w;
@@ -402,16 +349,20 @@ auto Text::width(const char* text) const -> int {
int w = 0; int w = 0;
bool first = true; bool first = true;
while (*ptr) { while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr); uint32_t cp = nextCodepoint(ptr);
if (cp == 0) break; if (cp == 0) {
break;
}
auto it = glyphs_.find(cp); auto it = glyphs_.find(cp);
if (it == glyphs_.end()) { if (it == glyphs_.end()) {
it = glyphs_.find('?'); it = glyphs_.find('?');
} }
if (!first) w += 1; // kerning if (!first) {
w += 1; // kerning
}
first = false; first = false;
if (it != glyphs_.end()) { if (it != glyphs_.end()) {
+10 -3
View File
@@ -5,11 +5,11 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector>
class Text { class Text {
public: public:
Text(const char* fnt_file, const char* gif_file); Text(const char* fnt_file, const char* gif_file);
~Text();
// Pinta texto sobre un buffer ARGB de 320x200 // Pinta texto sobre un buffer ARGB de 320x200
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const; void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const;
@@ -28,7 +28,7 @@ class Text {
// Calcula ancho en píxeles d'un text // Calcula ancho en píxeles d'un text
[[nodiscard]] auto width(const char* text) const -> int; [[nodiscard]] auto width(const char* text) const -> int;
// Amplada mono: nombre de codepoints × cell_w // 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 // 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 widthMonoDigits(const char* text, int digit_cell_w) const -> int;
[[nodiscard]] auto charHeight() const -> int { return box_height_; } [[nodiscard]] auto charHeight() const -> int { return box_height_; }
@@ -46,7 +46,7 @@ class Text {
int cell_spacing_{0}; int cell_spacing_{0};
int row_spacing_{0}; int row_spacing_{0};
Uint8* bitmap_{nullptr}; // píxels 8-bit del GIF de la font std::vector<Uint8> bitmap_; // píxels 8-bit del GIF de la font
int bitmap_width_{0}; int bitmap_width_{0};
int bitmap_height_{0}; int bitmap_height_{0};
@@ -57,6 +57,13 @@ class Text {
void loadFont(const char* fnt_file); void loadFont(const char* fnt_file);
void loadBitmap(const char* gif_file); void loadBitmap(const char* gif_file);
// Resolt un codepoint al GlyphInfo corresponent o al fallback '?'.
// Retorna nullptr si ni el codepoint ni el fallback existeixen.
[[nodiscard]] auto resolveGlyph(uint32_t cp) const -> const GlyphInfo*;
// Pinta un glif a (dst_x, dst_y) amb clipping per finestra.
// Si la finestra és tota la pantalla, passar clip_x_min=0, clip_x_max=SCREEN_WIDTH, idem y.
void blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const;
static constexpr int SCREEN_WIDTH = 320; static constexpr int SCREEN_WIDTH = 320;
static constexpr int SCREEN_HEIGHT = 200; static constexpr int SCREEN_HEIGHT = 200;
}; };
+270
View File
@@ -0,0 +1,270 @@
#include "core/resources/resource_cache.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include "core/audio/jail_audio.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_list.hpp"
// gif.h ja s'inclou des de jdraw8.cpp i text.cpp; el seu codi no és static
// 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).
// NOLINTBEGIN(readability-identifier-naming) — símbols externs de gif.h.
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
extern auto LoadPalette(unsigned char* data) -> unsigned char*;
// NOLINTEND(readability-identifier-naming)
namespace Resource {
std::unique_ptr<Cache> Cache::instance;
void Cache::init() { instance = std::unique_ptr<Cache>(new Cache()); }
void Cache::destroy() { instance.reset(); }
auto Cache::get() -> Cache* { return instance.get(); }
namespace {
auto basename(const std::string& path) -> std::string {
auto pos = path.find_last_of("/\\");
return pos == std::string::npos ? path : path.substr(pos + 1);
}
} // namespace
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
auto it = std::ranges::find_if(musics_, [&](const auto& m) { return m.name == name; });
if (it != musics_.end()) {
return it->music.get();
}
std::cerr << "Resource::Cache: música no trobada: " << name << '\n';
throw std::runtime_error("Music not found: " + name);
}
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
auto it = std::ranges::find_if(sounds_, [&](const auto& s) { return s.name == name; });
if (it != sounds_.end()) {
return it->sound.get();
}
std::cerr << "Resource::Cache: so no trobat: " << name << '\n';
throw std::runtime_error("Sound not found: " + name);
}
auto Cache::getSurfacePixels(const std::string& name) -> const std::vector<Uint8>& {
auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; });
if (it != surfaces_.end()) {
return it->pixels;
}
std::cerr << "Resource::Cache: surface no trobada: " << name << '\n';
throw std::runtime_error("Surface not found: " + name);
}
auto Cache::getPaletteBytes(const std::string& name) -> const std::vector<Uint8>& {
auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; });
if (it != surfaces_.end()) {
return it->palette;
}
std::cerr << "Resource::Cache: paleta no trobada: " << name << '\n';
throw std::runtime_error("Palette not found: " + name);
}
auto Cache::getTextFile(const std::string& name) -> const std::vector<uint8_t>& {
auto it = std::ranges::find_if(text_files_, [&](const auto& t) { return t.name == name; });
if (it != text_files_.end()) {
return it->bytes;
}
std::cerr << "Resource::Cache: text file no trobat: " << name << '\n';
throw std::runtime_error("TextFile not found: " + name);
}
void Cache::calculateTotal() {
const auto* list = List::get();
total_count_ = static_cast<int>(
list->getListByType(List::Type::MUSIC).size() +
list->getListByType(List::Type::SOUND).size() +
list->getListByType(List::Type::BITMAP).size() +
list->getListByType(List::Type::DATA).size() +
list->getListByType(List::Type::FONT).size());
loaded_count_ = 0;
}
auto Cache::getProgress() const -> float {
if (total_count_ == 0) {
return 1.0F;
}
return static_cast<float>(loaded_count_) / static_cast<float>(total_count_);
}
void Cache::beginLoad() {
calculateTotal();
stage_ = LoadStage::MUSICS;
stage_index_ = 0;
std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n";
}
void Cache::stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn) {
auto items = List::get()->getListByType(type);
if (stage_index_ == 0) {
clear_fn();
}
if (stage_index_ >= items.size()) {
stage_ = next;
stage_index_ = 0;
return;
}
load_fn(stage_index_++);
}
void Cache::stepTextFiles() {
auto data_items = List::get()->getListByType(List::Type::DATA);
auto font_items = List::get()->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_ >= items.size()) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
return;
}
loadOneTextFile(stage_index_++);
}
auto Cache::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) {
return true;
}
const Uint64 START_NS = SDL_GetTicksNS();
const Uint64 BUDGET_NS = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::MUSICS:
stepEachInList(List::Type::MUSIC, [this] { musics_.clear(); }, LoadStage::SOUNDS, [this](size_t i) { loadOneMusic(i); });
break;
case LoadStage::SOUNDS:
stepEachInList(List::Type::SOUND, [this] { sounds_.clear(); }, LoadStage::BITMAPS, [this](size_t i) { loadOneSound(i); });
break;
case LoadStage::BITMAPS:
stepEachInList(List::Type::BITMAP, [this] { surfaces_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOneBitmap(i); });
break;
case LoadStage::TEXT_FILES:
stepTextFiles();
break;
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) {
break;
}
}
return stage_ == LoadStage::DONE;
}
void Cache::loadOneMusic(size_t index) {
auto items = List::get()->getListByType(List::Type::MUSIC);
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
Ja::Music* music = Ja::loadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
if (music == nullptr) {
std::cerr << "Resource::Cache: Ja::loadMusic ha fallat per " << path << '\n';
return;
}
musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(music)});
++loaded_count_;
std::cout << " [music ] " << name << '\n';
}
void Cache::loadOneSound(size_t index) {
auto items = List::get()->getListByType(List::Type::SOUND);
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
Ja::Sound* sound = Ja::loadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
if (sound == nullptr) {
std::cerr << "Resource::Cache: Ja::loadSound ha fallat per " << path << '\n';
return;
}
sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(sound)});
++loaded_count_;
std::cout << " [sound ] " << name << '\n';
}
void Cache::loadOneBitmap(size_t index) {
auto items = List::get()->getListByType(List::Type::BITMAP);
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
// Decodifica píxels.
unsigned short w = 0;
unsigned short h = 0;
unsigned char* pixels = LoadGif(bytes.data(), &w, &h);
if (pixels == nullptr) {
std::cerr << "Resource::Cache: LoadGif ha fallat per " << path << '\n';
return;
}
SurfaceResource res;
res.name = name;
res.pixels.assign(pixels, pixels + 64000);
std::free(pixels);
// Decodifica paleta des del mateix GIF (necessita una segona passada
// perquè LoadGif no exposa la paleta).
unsigned char* palette = LoadPalette(bytes.data());
if (palette != nullptr) {
res.palette.assign(palette, palette + 768);
std::free(palette);
}
surfaces_.push_back(std::move(res));
++loaded_count_;
std::cout << " [bitmap] " << name << '\n';
}
void Cache::loadOneTextFile(size_t index) {
auto data_items = List::get()->getListByType(List::Type::DATA);
auto font_items = List::get()->getListByType(List::Type::FONT);
auto items = data_items;
items.insert(items.end(), font_items.begin(), font_items.end());
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
text_files_.push_back(TextFileResource{.name = name, .bytes = std::move(bytes)});
++loaded_count_;
std::cout << " [text ] " << name << '\n';
}
} // namespace Resource
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "core/resources/resource_list.hpp"
#include "core/resources/resource_types.hpp"
namespace Resource {
// Cache singleton: precarga + decode dels assets llistats al
// `Resource::List`. Implementa carrega incremental amb pressupost
// de temps per frame (`loadStep`) per a poder mostrar una barra de
// progrés des de l'escena `BootLoader`.
class Cache {
public:
static void init();
static void destroy();
static auto get() -> Cache*;
~Cache() = default;
Cache(const Cache&) = delete;
auto operator=(const Cache&) -> Cache& = delete;
// Getters: throw runtime_error si el nom no existeix al cache.
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
auto getSurfacePixels(const std::string& name) -> const std::vector<Uint8>&;
auto getPaletteBytes(const std::string& name) -> const std::vector<Uint8>&;
auto getTextFile(const std::string& name) -> const std::vector<uint8_t>&;
// Loader incremental.
void beginLoad();
auto loadStep(int budget_ms) -> bool; // true → DONE
[[nodiscard]] auto isLoadDone() const -> bool { return stage_ == LoadStage::DONE; }
[[nodiscard]] auto getProgress() const -> float; // 0.0..1.0
[[nodiscard]] auto getCurrentLoadingName() const -> const std::string& { return current_loading_name_; }
private:
Cache() = default;
enum class LoadStage : std::uint8_t {
MUSICS,
SOUNDS,
BITMAPS,
TEXT_FILES,
DONE,
};
void calculateTotal();
void loadOneMusic(size_t index);
void loadOneSound(size_t index);
void loadOneBitmap(size_t index);
void loadOneTextFile(size_t index);
void stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn);
void stepTextFiles();
std::vector<MusicResource> musics_;
std::vector<SoundResource> sounds_;
std::vector<SurfaceResource> surfaces_;
std::vector<TextFileResource> text_files_;
LoadStage stage_{LoadStage::DONE};
size_t stage_index_{0};
int total_count_{0};
int loaded_count_{0};
std::string current_loading_name_;
static std::unique_ptr<Cache> instance;
};
} // namespace Resource
+71
View File
@@ -0,0 +1,71 @@
#include "core/resources/resource_helper.hpp"
#include <fstream>
#include <iostream>
#include "core/jail/jfile.hpp"
#include "core/resources/resource_pack.hpp"
namespace ResourceHelper {
namespace {
ResourcePack pack_obj;
bool pack_loaded = false;
bool fallback_enabled = true;
auto readFromDisk(const std::string& relative_path) -> std::vector<uint8_t> {
const std::string FULL = std::string(Jf::getResourceFolder()) + relative_path;
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
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 {};
}
return data;
}
} // namespace
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled = enable_fallback;
pack_loaded = pack_obj.loadPack(pack_file);
if (pack_loaded) {
std::cout << "ResourceHelper: pack loaded (" << pack_obj.getResourceCount()
<< " entries) from " << pack_file << '\n';
} else if (enable_fallback) {
std::cout << "ResourceHelper: no pack at " << pack_file
<< " — using filesystem fallback\n";
} else {
std::cerr << "ResourceHelper: FATAL — no pack at " << pack_file
<< " and fallback disabled\n";
return false;
}
return true;
}
void shutdownResourceSystem() {
pack_obj.clear();
pack_loaded = false;
}
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t> {
if (pack_loaded && pack_obj.hasResource(relative_path)) {
return pack_obj.getResource(relative_path);
}
if (fallback_enabled) {
return readFromDisk(relative_path);
}
return {};
}
auto hasPack() -> bool {
return pack_loaded;
}
} // namespace ResourceHelper
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
// API d'alt nivell per a llegir recursos. Prova primer el pack (si està
// carregat), després cau al fitxer solt dins `Jf::getResourceFolder()`
// si el fallback està activat.
namespace ResourceHelper {
// Inicialitza el sistema. `pack_file` és la ruta absoluta (o relativa al
// CWD) al fitxer de recursos. `enable_fallback` permet llegir de disc
// quan el pack no conté l'entrada (útil per a Debug i WASM).
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool;
// Allibera el pack carregat a memòria.
void shutdownResourceSystem();
// Llegeix un recurs per ruta relativa (p.ex. "gfx/logo.gif", "fonts/8bithud.fnt").
// Retorna un vector buit si no es troba.
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t>;
// True si el sistema es va inicialitzar amb un pack vàlid.
[[nodiscard]] auto hasPack() -> bool;
} // namespace ResourceHelper
+121
View File
@@ -0,0 +1,121 @@
#include "core/resources/resource_list.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace Resource {
std::unique_ptr<List> List::instance;
void List::init(const std::string& yaml_path) {
instance = std::unique_ptr<List>(new List());
instance->loadFromYaml(yaml_path);
}
void List::destroy() { instance.reset(); }
auto List::get() -> List* { return instance.get(); }
void List::loadFromYaml(const std::string& yaml_path) {
auto bytes = ResourceHelper::loadFile(yaml_path);
if (bytes.empty()) {
std::cout << "Resource::List: cannot load manifest " << yaml_path << '\n';
return;
}
std::string content(bytes.begin(), bytes.end());
loadFromString(content);
}
void List::loadFromString(const std::string& yaml_content) {
try {
auto yaml = fkyaml::node::deserialize(yaml_content);
if (!yaml.contains("assets")) {
std::cout << "Resource::List: missing 'assets' root key\n";
return;
}
const auto& assets = yaml["assets"];
for (auto cat_it = assets.begin(); cat_it != assets.end(); ++cat_it) {
const auto& category_node = cat_it.value();
if (!category_node.is_mapping()) {
continue;
}
for (auto type_it = category_node.begin(); type_it != category_node.end(); ++type_it) {
auto type_str = type_it.key().get_value<std::string>();
Type type = parseAssetType(type_str);
const auto& items = type_it.value();
if (!items.is_sequence()) {
continue;
}
for (const auto& item : items) {
if (item.is_string()) {
addToMap(item.get_value<std::string>(), type);
}
}
}
}
std::cout << "Resource::List: loaded " << file_list_.size() << " assets from manifest\n";
} catch (const std::exception& e) {
std::cout << "Resource::List: YAML parse error: " << e.what() << '\n';
}
}
void List::addToMap(const std::string& path, Type type) {
auto key = basename(path);
if (file_list_.contains(key)) {
std::cout << "Resource::List: duplicate asset key '" << key << "', overwriting\n";
}
file_list_.emplace(key, Item{path, type});
}
auto List::get(const std::string& filename) const -> std::string {
auto it = file_list_.find(filename);
if (it != file_list_.end()) {
return it->second.path;
}
return "";
}
auto List::getListByType(Type type) const -> std::vector<std::string> {
std::vector<std::string> list;
for (const auto& [filename, item] : file_list_) {
if (item.type == type) {
list.push_back(item.path);
}
}
std::ranges::sort(list);
return list;
}
auto List::exists(const std::string& filename) const -> bool {
return file_list_.contains(filename);
}
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;
}
throw std::runtime_error("Unknown asset type: " + type_str);
}
auto List::basename(const std::string& path) -> std::string {
auto pos = path.find_last_of("/\\");
return pos == std::string::npos ? path : path.substr(pos + 1);
}
} // namespace Resource
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Resource {
// Registre lleuger d'assets carregat des de `data/config/assets.yaml`.
// Map<basename → Item> per a lookup O(1). Cache l'utilitza per a
// iterar per categoria a l'hora de carregar.
class List {
public:
enum class Type : std::uint8_t {
DATA,
BITMAP,
MUSIC,
SOUND,
FONT,
SIZE,
};
static void init(const std::string& yaml_path);
static void destroy();
static auto get() -> List*;
~List() = default;
List(const List&) = delete;
auto operator=(const List&) -> List& = delete;
[[nodiscard]] auto get(const std::string& filename) const -> std::string;
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
[[nodiscard]] auto exists(const std::string& filename) const -> bool;
[[nodiscard]] auto totalCount() const -> int { return static_cast<int>(file_list_.size()); }
private:
struct Item {
std::string path; // ruta relativa al pack (ex: "music/menu.ogg")
Type type;
Item(std::string p, Type t)
: path(std::move(p)),
type(t) {}
};
List() = default;
void loadFromYaml(const std::string& yaml_path);
void loadFromString(const std::string& yaml_content);
void addToMap(const std::string& path, Type type);
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type;
[[nodiscard]] static auto basename(const std::string& path) -> std::string;
std::unordered_map<std::string, Item> file_list_;
static std::unique_ptr<List> instance;
};
} // namespace Resource
+221
View File
@@ -0,0 +1,221 @@
#include "core/resources/resource_pack.hpp"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <numeric>
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "AEE_RESOURCES__2026";
namespace {
constexpr const char* MAGIC = "AEE1";
constexpr uint32_t VERSION = 1;
} // namespace
ResourcePack::ResourcePack() = default;
ResourcePack::~ResourcePack() {
clear();
}
auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
// djb2-like hash, seed 0x12345678 (idèntic a CCAE).
return std::accumulate(data.begin(), data.end(), uint32_t{0x12345678}, [](uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; });
}
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= static_cast<uint8_t>(key[i % key.length()]);
}
}
void ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
encryptData(data, key); // XOR és simètric
}
auto ResourcePack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
return false; // No imprimim error: el caller decideix si cal fallback
}
std::array<char, 4> header{};
file.read(header.data(), 4);
if (std::string(header.data(), 4) != MAGIC) {
std::cerr << "ResourcePack: invalid pack file format (bad magic): " << pack_file << '\n';
return false;
}
uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "ResourcePack: unsupported pack version: " << version << '\n';
return false;
}
uint32_t resource_count = 0;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (uint32_t i = 0; i < resource_count; ++i) {
uint32_t filename_length = 0;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
uint64_t data_size = 0;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), static_cast<std::streamsize>(data_size));
decryptData(data_, DEFAULT_ENCRYPT_KEY);
loaded_ = true;
return true;
}
auto ResourcePack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "ResourcePack: could not create pack file: " << pack_file << '\n';
return false;
}
file.write(MAGIC, 4);
uint32_t version = VERSION;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
auto filename_length = static_cast<uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
std::vector<uint8_t> encrypted = data_;
encryptData(encrypted, DEFAULT_ENCRYPT_KEY);
uint64_t data_size = encrypted.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted.data()), static_cast<std::streamsize>(data_size));
return true;
}
auto ResourcePack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "ResourcePack: could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "ResourcePack: could not read file: " << filepath << '\n';
return false;
}
ResourceEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) {
std::cerr << "ResourcePack: directory does not exist: " << directory << '\n';
return false;
}
namespace fs = std::filesystem;
return std::all_of(fs::recursive_directory_iterator(directory),
fs::recursive_directory_iterator{},
[&](const fs::directory_entry& entry) {
if (!entry.is_regular_file()) {
return true;
}
std::string filepath = entry.path().string();
std::string filename = fs::relative(entry.path(), directory).string();
std::ranges::replace(filename, '\\', '/');
return addFile(filename, filepath);
});
}
auto ResourcePack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
return {};
}
const ResourceEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "ResourcePack: invalid resource data: " << filename << '\n';
return {};
}
std::vector<uint8_t> result(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "ResourcePack: checksum mismatch for: " << filename << '\n';
}
return result;
}
auto ResourcePack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
void ResourcePack::clear() {
resources_.clear();
data_.clear();
loaded_ = false;
}
auto ResourcePack::getResourceCount() const -> size_t {
return resources_.size();
}
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
// Entrada d'un recurs dins el pack (format AEE, equivalent a CCAE).
struct ResourceEntry {
std::string filename;
uint64_t offset{0};
uint64_t size{0};
uint32_t checksum{0};
};
// Pack binari de recursos carregat a memòria. Formato:
// Header: "AEE1" (4 bytes) + version uint32 + resource_count uint32
// Index: per cada recurs -> filename_len uint32 + filename + offset uint64
// + size uint64 + checksum uint32
// Payload: data_size uint64 + bytes xifrats amb XOR (DEFAULT_ENCRYPT_KEY)
class ResourcePack {
public:
ResourcePack();
~ResourcePack();
// I/O del fitxer
auto loadPack(const std::string& pack_file) -> bool;
auto savePack(const std::string& pack_file) -> bool;
// Builders usats per l'eina pack_resources
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
auto addDirectory(const std::string& directory) -> bool;
[[nodiscard]] auto getResource(const std::string& filename) -> std::vector<uint8_t>;
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
void clear();
[[nodiscard]] auto getResourceCount() const -> size_t;
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
static const std::string DEFAULT_ENCRYPT_KEY;
private:
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
bool loaded_{false};
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t;
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
};
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
// Forward declarations to keep this header light.
namespace Ja {
struct Music;
struct Sound;
void deleteMusic(Music* music);
void deleteSound(Sound* sound);
} // namespace Ja
namespace Resource {
struct MusicDeleter {
void operator()(Ja::Music* music) const noexcept {
if (music != nullptr) {
Ja::deleteMusic(music);
}
}
};
struct SoundDeleter {
void operator()(Ja::Sound* sound) const noexcept {
if (sound != nullptr) {
Ja::deleteSound(sound);
}
}
};
struct MusicResource {
std::string name;
std::unique_ptr<Ja::Music, MusicDeleter> music;
};
struct SoundResource {
std::string name;
std::unique_ptr<Ja::Sound, SoundDeleter> sound;
};
// Una entrada BITMAP descodifica un GIF i emmagatzema els seus
// 64000 bytes de píxels paletats + la paleta de 256 colors (768
// bytes RGB). Així `getSurface(name)` i `getPalette(name)` comparteixen
// el mateix decode.
struct SurfaceResource {
std::string name;
std::vector<Uint8> pixels; // 64000 bytes (320 * 200) paletats
std::vector<Uint8> palette; // 768 bytes (256 * R G B)
};
// Per a fitxers de text generals (locale.yaml, keys.yaml, *.fnt).
struct TextFileResource {
std::string name;
std::vector<uint8_t> bytes;
};
} // namespace Resource
+318 -257
View File
@@ -3,316 +3,377 @@
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include "core/audio/audio.hpp"
#include "core/input/gamepad.hpp" #include "core/input/gamepad.hpp"
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include "core/input/key_config.hpp"
#include "core/input/key_remap.hpp" #include "core/input/key_remap.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/jail/jail_audio.hpp" #include "core/jail/jdraw8.hpp"
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/menu.hpp" #include "core/rendering/menu.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/info.hpp" #include "game/info.hpp"
#include "game/modulegame.hpp" #include "game/modulegame.hpp"
#include "game/modulesequence.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/banner_scene.hpp"
#include "game/scenes/boot_loader_scene.hpp"
#include "game/scenes/credits_scene.hpp"
#include "game/scenes/intro_new_logo_scene.hpp"
#include "game/scenes/intro_scene.hpp"
#include "game/scenes/menu_scene.hpp"
#include "game/scenes/mort_scene.hpp"
#include "game/scenes/scene.hpp"
#include "game/scenes/scene_registry.hpp"
#include "game/scenes/secreta_scene.hpp"
#include "game/scenes/slides_scene.hpp"
// Cheats del joc original — declarats a jinput.cpp std::unique_ptr<Director> Director::instance;
extern void JI_moveCheats(Uint8 new_key);
Director* Director::instance_ = nullptr; Director::~Director() = default;
void Director::initGameContext() {
Info::ctx.num_habitacio = Options::game.habitacio_inicial;
Info::ctx.num_piramide = Options::game.piramide_inicial;
Info::ctx.diners = Options::game.diners_inicial;
Info::ctx.diamants = Options::game.diamants_inicial;
Info::ctx.vida = Options::game.vides;
Info::ctx.momies = 0;
Info::ctx.nou_personatge = false;
Info::ctx.pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != nullptr) {
Info::ctx.nou_personatge = true;
fclose(ini);
}
}
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í
// amb el cache plenament disponible per a la resta d'escenes.
if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) {
return std::make_unique<Scenes::BootLoaderScene>();
}
if (game_state_ == 0) {
// Gameplay. ModuleGame és una Scenes::Scene des de la Phase A.
return std::make_unique<ModuleGame>();
}
// game_state_ == 1: dispatch al registry per num_piramide. Replica
// del redirect que el vell ModuleSequence::Go() feia: si el jugador
// arriba a la Secreta (6) sense prou diners, salta als slides de
// fracàs (7) abans de buscar l'escena al registry.
if (Info::ctx.num_piramide == 6 && Info::ctx.diners < 200) {
Info::ctx.num_piramide = 7;
}
return Scenes::SceneRegistry::instance().tryCreate(Info::ctx.num_piramide);
}
void Director::init() { void Director::init() {
instance_ = new Director(); instance = std::unique_ptr<Director>(new Director());
Gamepad::init(); Gamepad::init();
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
// amb una factory de `Scenes::Scene`. iterate() consulta aquest
// registry per a tots els states de seqüència (game_state_ == 1); si
// una clau no apareix ací, Director surt ordenadament.
auto& registry = Scenes::SceneRegistry::instance();
registry.registerScene(0, [] { return std::make_unique<Scenes::MenuScene>(); });
registry.registerScene(100, [] { return std::make_unique<Scenes::MortScene>(); });
// BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix
// pel switch intern llegint Info::ctx.num_piramide).
for (int p = 2; p <= 5; ++p) {
registry.registerScene(p, [] { return std::make_unique<Scenes::BannerScene>(); });
}
// SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
// - num_piramide == 1: slides narratius inicials (entrada al joc)
// - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
// l'usuari no té prou diners per a la Secreta)
registry.registerScene(1, [] { return std::make_unique<Scenes::SlidesScene>(); });
registry.registerScene(7, [] { return std::make_unique<Scenes::SlidesScene>(); });
registry.registerScene(6, [] { return std::make_unique<Scenes::SecretaScene>(); });
registry.registerScene(8, [] { return std::make_unique<Scenes::CreditsScene>(); });
// State 255 (intro): dues variants segons `Options::game.use_new_logo`.
// La factory tria a runtime — així es pot togglar des del menú sense
// re-registrar. Les dues escenes construeixen una IntroSpritesScene
// com a sub-escena per a la part d'animacions de sprites.
registry.registerScene(255, []() -> std::unique_ptr<Scenes::Scene> {
if (Options::game.use_new_logo) {
return std::make_unique<Scenes::IntroNewLogoScene>();
}
return std::make_unique<Scenes::IntroScene>();
});
} }
void Director::destroy() { void Director::destroy() {
Gamepad::destroy(); Gamepad::destroy();
delete instance_; instance.reset();
instance_ = nullptr;
} }
auto Director::get() -> Director* { auto Director::get() -> Director* {
return instance_; return instance.get();
} }
void Director::togglePause() { void Director::togglePause() {
paused_ = !paused_; paused_ = !paused_;
if (paused_) { if (paused_) {
JA_PauseMusic(); Audio::get()->pauseMusic();
} else { } else {
JA_ResumeMusic(); Audio::get()->resumeMusic();
} }
} }
void Director::setup() {
// Els buffers són membres (director.hpp); només els inicialitzem.
std::memset(game_frame_, 0, sizeof(game_frame_));
std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_));
has_frame_ = false;
}
void Director::applyRestart() {
restart_requested_ = false;
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
initGameContext();
Info::ctx.num_piramide = 255;
current_scene_.reset();
game_state_ = 1;
has_frame_ = false;
Menu::close();
Ji::setInputBlocked(false);
}
void Director::maybeStartTitleCredits() {
static bool credits_triggered_ = false;
if (credits_triggered_ || Info::ctx.num_piramide != 0) {
return;
}
if (Options::game.show_title_credits) {
Overlay::startCredits();
}
credits_triggered_ = true;
}
auto Director::tickActiveScene() -> bool {
if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
game_state_ = current_scene_->nextState();
current_scene_.reset();
}
if (!current_scene_) {
if (game_state_ == -1 || Jg::quitting()) {
return false;
}
current_scene_ = createNextScene();
if (!current_scene_) {
return false;
}
current_scene_->onEnter();
last_tick_ms_ = SDL_GetTicks();
}
Ji::update();
const Uint32 NOW = SDL_GetTicks();
const int DELTA_MS = static_cast<int>(NOW - last_tick_ms_);
last_tick_ms_ = NOW;
current_scene_->tick(DELTA_MS);
Jd8::flip();
std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
has_frame_ = true;
return true;
}
auto Director::iterate() -> bool {
if (quit_requested_) {
Jg::quitSignal();
current_scene_.reset();
return false;
}
if (restart_requested_) {
applyRestart();
}
if (!context_initialized_) {
initGameContext();
context_initialized_ = true;
}
constexpr Uint32 FRAME_MS_VSYNC = 16;
constexpr Uint32 FRAME_MS_NO_VSYNC = 4;
const Uint32 FRAME_START = SDL_GetTicks();
Gamepad::update();
KeyRemap::update();
GlobalInputs::handle();
Mouse::updateCursorVisibility();
Audio::update();
maybeStartTitleCredits();
if (esc_blocked_ && !Overlay::isEscConsumed()) {
esc_blocked_ = false;
}
if (!paused_) {
if (!tickActiveScene()) {
return false;
}
}
if (has_frame_) {
std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
Screen::get()->present(presentation_buffer_);
}
const Uint32 TARGET_MS = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
const Uint32 ELAPSED = SDL_GetTicks() - FRAME_START;
if (ELAPSED < TARGET_MS) {
SDL_Delay(TARGET_MS - ELAPSED);
}
return true;
}
void Director::teardown() {
// Senyal de quit i descàrrega ordenada de l'escena en curs. Els
// destructors de cada escena són no-bloquejants — ja no fan fades
// bloquejants. La resta de cleanup la gestiona `destroy()`.
Jg::quitSignal();
current_scene_.reset();
}
void Director::run() { void Director::run() {
// Llança el game thread setup();
game_thread_ = std::thread(&Director::gameThreadFunc, this); while (true) {
pollAllEvents();
// Doble buffer: game_frame és el frame net del joc, presentation_buffer if (!iterate()) {
// és el frame + overlay (es regenera cada iteració des de game_frame) break;
Uint32 game_frame[320 * 200]{};
Uint32 presentation_buffer[320 * 200]{};
bool has_frame = false;
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
// Bucle principal del director (no-bloquejant)
while (!game_thread_done_ && !quit_requested_) {
Uint32 frame_start = SDL_GetTicks();
handleEvents();
Gamepad::update();
KeyRemap::update();
GlobalInputs::handle();
Mouse::updateCursorVisibility();
// Dispara els crèdits cinematogràfics la primera vegada que el joc
// arriba al menú del títol (info::num_piramide == 0). Lectura no
// atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.
static bool credits_triggered = false;
if (!credits_triggered && info::num_piramide == 0) {
if (Options::game.show_title_credits) {
Overlay::startCredits();
}
credits_triggered = true;
}
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
if (esc_blocked_ && !Overlay::isEscConsumed()) {
esc_blocked_ = false;
}
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja).
// Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame.
bool new_frame = false;
if (!paused_) {
std::lock_guard lock(mutex_);
if (frame_ready_ && latest_frame_ != nullptr) {
memcpy(game_frame, latest_frame_, sizeof(game_frame));
frame_ready_ = false;
frame_consumed_ = true;
has_frame = true;
new_frame = true;
}
}
if (new_frame) {
frame_consumed_cv_.notify_one(); // desbloqueja el joc
}
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
if (has_frame) {
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer));
Screen::get()->present(presentation_buffer);
}
// Límit de framerate segons VSync
Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
Uint32 elapsed = SDL_GetTicks() - frame_start;
if (elapsed < target_ms) {
SDL_Delay(target_ms - elapsed);
} }
} }
teardown();
// Assegura que el game thread ix (despertar-lo per si està esperant)
quit_requested_ = true;
JG_QuitSignal();
{
std::lock_guard lock(mutex_);
frame_consumed_ = true;
}
frame_consumed_cv_.notify_all();
if (game_thread_.joinable()) {
game_thread_.join();
}
} }
void Director::handleEvents() { void Director::pollAllEvents() {
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) { handleEvent(event);
JG_QuitSignal();
requestQuit();
}
// Hot-plug de gamepad
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
Gamepad::handleEvent(event);
continue;
}
// Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
continue;
}
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
menu_keys_held_[event.key.scancode] = false;
continue;
}
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
Menu::captureKey(event.key.scancode);
menu_keys_held_[event.key.scancode] = true;
continue;
}
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.pause_toggle) {
togglePause();
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
menu_keys_held_[event.key.scancode] = true;
continue;
}
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.menu_toggle) {
Menu::toggle();
JI_SetInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true;
continue;
}
// Si el menú està obert, consumeix tot l'input de teclat
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
Menu::close();
JI_SetInputBlocked(false);
// Empassa l'ESC fins al release perquè el joc no la veja per polling
esc_swallow_until_release_ = true;
} else {
Menu::handleKey(event.key.scancode);
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
if (!Menu::isOpen()) {
JI_SetInputBlocked(false);
}
}
menu_keys_held_[event.key.scancode] = true;
continue;
}
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
continue; // no deixem passar KEY_UP al joc tampoc
}
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
esc_swallow_until_release_ = false;
continue;
}
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
if (!Overlay::isEscConsumed()) {
// Primera pulsació: mostra notificació
Overlay::handleEscape();
} else {
// Segona pulsació: senyal d'eixida al joc
esc_blocked_ = false;
key_pressed_ = true;
JG_QuitSignal();
// Si estem en pausa, la desactivem (sense reprendre la música,
// estem eixint): el game thread està bloquejat a publishFrame
// i necessita que Director consumeixca frames per despertar-lo
// i poder veure la senyal de quit.
paused_ = false;
}
continue; // no processa més aquest event
}
if (event.type == SDL_EVENT_KEY_UP) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
// quan l'overlay faça timeout
continue;
} else {
// Comprova si és una tecla GUI (no passa al joc)
const auto sc = event.key.scancode;
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
sc == Options::keys_gui.inc_zoom ||
sc == Options::keys_gui.fullscreen ||
sc == Options::keys_gui.toggle_shader ||
sc == Options::keys_gui.toggle_aspect_ratio ||
sc == Options::keys_gui.toggle_supersampling ||
sc == Options::keys_gui.next_shader ||
sc == Options::keys_gui.next_shader_preset ||
sc == Options::keys_gui.toggle_stretch_filter ||
sc == Options::keys_gui.toggle_render_info);
if (!is_gui_key) {
key_pressed_ = true;
JI_moveCheats(sc);
}
}
}
Mouse::handleEvent(event);
} }
} }
void Director::publishFrame(Uint32* pixels) { auto Director::handleMenuEvent(const SDL_Event& event) -> bool {
{ // Empassar-se el KEY_UP d'una tecla que el menú va consumir en KEY_DOWN.
std::lock_guard lock(mutex_); if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
latest_frame_ = pixels; event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
frame_ready_ = true; menu_keys_held_[event.key.scancode] = false;
frame_consumed_ = false; return true;
} }
frame_produced_cv_.notify_one(); const bool KEY_DOWN = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot.
if (Menu::isCapturing() && KEY_DOWN) {
Menu::captureKey(event.key.scancode);
menu_keys_held_[event.key.scancode] = true;
return true;
}
// Pausa / menú toggle.
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("pause_toggle")) {
togglePause();
menu_keys_held_[event.key.scancode] = true;
return true;
}
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("menu_toggle")) {
Menu::toggle();
Ji::setInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true;
return true;
}
// Si el menú està obert, consumeix tot l'input de teclat.
if (Menu::isOpen() && KEY_DOWN) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
Menu::close();
Ji::setInputBlocked(false);
esc_swallow_until_release_ = true;
} else {
Menu::handleKey(event.key.scancode);
if (!Menu::isOpen()) {
Ji::setInputBlocked(false);
}
}
menu_keys_held_[event.key.scancode] = true;
return true;
}
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
return true;
}
return false;
}
// Espera que el director consumeixca el frame auto Director::handleEscapeEvent(const SDL_Event& event) -> bool {
{ // Salta els crèdits amb qualsevol tecla que arribe al joc.
std::unique_lock lock(mutex_); if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
frame_consumed_cv_.wait(lock, [this] { Overlay::cancelCredits();
return frame_consumed_ || quit_requested_; return true;
});
} }
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar.
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
esc_swallow_until_release_ = false;
return true;
}
// ESC KEY_DOWN: bloqueja per polling i decideix notificació vs eixida.
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
esc_blocked_ = true;
if (!Overlay::isEscConsumed()) {
Overlay::handleEscape();
} else {
esc_blocked_ = false;
key_pressed_ = true;
Jg::quitSignal();
paused_ = false;
}
return true;
}
return false;
}
void Director::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
Jg::quitSignal();
requestQuit();
}
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
Gamepad::handleEvent(event);
return;
}
if (handleMenuEvent(event)) {
return;
}
if (handleEscapeEvent(event)) {
return;
}
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode != SDL_SCANCODE_ESCAPE) {
const auto SC = event.key.scancode;
if (!KeyConfig::isGuiKey(SC)) {
key_pressed_ = true;
Ji::moveCheats(SC);
}
}
Mouse::handleEvent(event);
} }
void Director::requestQuit() { void Director::requestQuit() {
quit_requested_ = true; quit_requested_ = true;
JG_QuitSignal(); Jg::quitSignal();
frame_consumed_cv_.notify_all(); }
frame_produced_cv_.notify_all();
void Director::requestRestart() {
restart_requested_ = true;
} }
auto Director::consumeKeyPressed() -> bool { auto Director::consumeKeyPressed() -> bool {
return key_pressed_.exchange(false); return key_pressed_.exchange(false);
} }
void Director::gameThreadFunc() {
info::num_habitacio = Options::game.habitacio_inicial;
info::num_piramide = Options::game.piramide_inicial;
info::diners = 0;
info::diamants = 0;
info::vida = Options::game.vides;
info::momies = 0;
info::nou_personatge = false;
info::pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != nullptr) {
info::nou_personatge = true;
fclose(ini);
}
int gameState = 1;
while (gameState != -1 && !quit_requested_) {
switch (gameState) {
case 0: {
auto* moduleGame = new ModuleGame();
gameState = moduleGame->Go();
delete moduleGame;
break;
}
case 1: {
auto* moduleSequence = new ModuleSequence();
gameState = moduleSequence->Go();
delete moduleSequence;
break;
}
}
}
game_thread_done_ = true;
// Despertar el director per si esperava un frame
frame_produced_cv_.notify_all();
}
+63 -29
View File
@@ -3,68 +3,102 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <atomic> #include <atomic>
#include <condition_variable>
#include <cstdint> #include <cstdint>
#include <mutex> #include <memory>
#include <thread>
// El Director és el thread principal que controla la presentació i els inputs. #include "game/scenes/scene.hpp"
// Executa el joc en un thread secundari (game thread) com si fos una "fibra emulada":
// el joc produeix un frame, es bloqueja a JD8_Flip(), i el director el presenta // El Director és l'únic thread del runtime. Cada iterate() fa input →
// abans de donar-li via per produir el següent. // tick de l'escena actual → Jd8::flip → overlay → present → sleep al frame
// target. Totes les escenes (`Scenes::Scene` i `ModuleGame`) són
// tick-based i no bloquegen — no hi ha fibers, mutex ni condition_variable.
// Compatible amb SDL_AppIterate i amb el futur port a emscripten.
class Director { class Director {
public: public:
static void init(); static void init();
static void destroy(); static void destroy();
static auto get() -> Director*; static auto get() -> Director*;
// Bucle principal del director. Crida des de main(). // Bucle principal clàssic (build natiu sense SDL_MAIN_USE_CALLBACKS).
// Internament crida setup() + bucle d'iterate() + teardown(). Crida des de main().
void run(); void run();
// Invocat pel game thread des de JD8_Flip(). Bloqueja fins que el director // Punts d'entrada compatibles amb SDL_AppInit / SDL_AppIterate /
// consumeix el frame i dona via per produir el següent. // SDL_AppEvent / SDL_AppQuit. Permeten que el Director siga driven
void publishFrame(Uint32* pixels); // 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();
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) // Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
void requestQuit(); void requestQuit();
[[nodiscard]] auto isQuitRequested() const -> bool { return quit_requested_; }
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey) // 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
// iterate() per evitar manipular l'escena des d'una lambda del menú.
void requestRestart();
// Consumeix el flag de "tecla polsada" (com l'antic Ji::anyKey)
auto consumeKeyPressed() -> bool; auto consumeKeyPressed() -> bool;
// Indica si ESC està bloquejada (el joc no l'ha de veure) // 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: bloqueja el consum de frames del game thread + pausa la música // Pausa: mentre està activa, iterate() no avança l'escena — es
// continua presentant el darrer frame amb overlay fresc.
void togglePause(); void togglePause();
auto isPaused() const -> bool { return paused_; } [[nodiscard]] auto isPaused() const -> bool { return paused_; }
~Director();
private: private:
Director() = default; Director() = default;
~Director() = default;
static Director* instance_; static std::unique_ptr<Director> instance;
void gameThreadFunc(); void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
void handleEvents();
std::thread game_thread_; // Inicialitza Info::ctx a partir de Options::game.* i comprova trick.ini.
std::mutex mutex_; // Es crida una sola vegada des d'iterate() a la primera invocació.
std::condition_variable frame_produced_cv_; static void initGameContext();
std::condition_variable frame_consumed_cv_; // Construeix l'escena apropiada segons game_state_ i Info::ctx.
// Retorna nullptr si l'state actual no té escena registrada (bug).
[[nodiscard]] auto createNextScene() const -> std::unique_ptr<Scenes::Scene>;
// Helpers d'iterate() — extrets per reduir complexitat cognitiva.
void applyRestart();
static void maybeStartTitleCredits();
auto tickActiveScene() -> bool; // true = continuar; false = sortir del loop
Uint32* latest_frame_{nullptr}; // Helpers d'handleEvent() — cada un retorna true si l'event s'ha consumit.
bool frame_ready_{false}; auto handleMenuEvent(const SDL_Event& event) -> bool;
bool frame_consumed_{true}; auto handleEscapeEvent(const SDL_Event& event) -> bool;
// Buffers persistents entre iteracions. Abans eren locals a run(),
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
// a reservar en cada crida del callback.
Uint32 game_frame_[320 * 200]{};
Uint32 presentation_buffer_[320 * 200]{};
bool has_frame_{false};
// Estat de l'escena actual. Abans vivia al stack del GameFiber; des
// de la Phase B.2 de la migració viu directament al Director.
std::unique_ptr<Scenes::Scene> current_scene_;
int game_state_{1}; // 0 = gameplay (ModuleGame), 1 = via SceneRegistry, -1 = quit
Uint32 last_tick_ms_{0};
bool context_initialized_{false};
std::atomic<bool> quit_requested_{false}; std::atomic<bool> quit_requested_{false};
std::atomic<bool> game_thread_done_{false}; std::atomic<bool> restart_requested_{false};
std::atomic<bool> key_pressed_{false}; std::atomic<bool> key_pressed_{false};
std::atomic<bool> esc_blocked_{false}; std::atomic<bool> esc_blocked_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};
// Quan el menú tanca amb ESC, empassem-nos l'ESC fins que l'usuari la deixe anar, // Quan el menú tanca amb ESC, empassem-nos l'ESC fins que l'usuari la deixe anar,
// per no fer eixir el joc al proper poll de JI_KeyPressed. // per no fer eixir el joc al proper poll de Ji::keyPressed.
std::atomic<bool> esc_swallow_until_release_{false}; std::atomic<bool> esc_swallow_until_release_{false};
// Tecles consumides pel menú (KEY_DOWN): el KEY_UP associat cal empassar-lo // Tecles consumides pel menú (KEY_DOWN): el KEY_UP associat cal empassar-lo
// per evitar que el joc (JI_AnyKey / JI_moveCheats) les veja quan el menú tanca. // per evitar que el joc (Ji::anyKey / Ji::moveCheats) les veja quan el menú tanca.
bool menu_keys_held_[SDL_SCANCODE_COUNT]{}; bool menu_keys_held_[SDL_SCANCODE_COUNT]{};
}; };
+4
View File
@@ -0,0 +1,4 @@
# source/external/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
+2
View File
@@ -1,3 +1,4 @@
// NOLINTBEGIN(clang-analyzer-unix.Malloc) — codi extern de tercers, no l'auditem
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -510,3 +511,4 @@ unsigned char* LoadGif(unsigned char *buffer, unsigned short* w, unsigned short*
fclose( gif_file ); fclose( gif_file );
}*/ }*/
// NOLINTEND(clang-analyzer-unix.Malloc)

Some files were not shown because too many files have changed in this diff Show More