Compare commits
36 Commits
4e18f83ec5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4435bc4942 | |||
| 4a4485c6f8 | |||
| d09bb1cf6b | |||
| b1f9e57f36 | |||
| f7875baa2d | |||
| c6e37af7d1 | |||
| 5e57034a38 | |||
| 2a8fbbb095 | |||
| 53e93ef697 | |||
| e7aa2463b4 | |||
| 27f8b0ae36 | |||
| 2e1a82ff40 | |||
| 94aa69cffe | |||
| 7409c799c3 | |||
| 417699d276 | |||
| 9d86137203 | |||
| 52369be7ae | |||
| 1c11a3057b | |||
| e8b0b12f98 | |||
| 16a3f5b470 | |||
| 5cda8fc3f9 | |||
| 5956d874c3 | |||
| e0f9b60f22 | |||
| d3bdd9b783 | |||
| a36662ac6e | |||
| 52431adb0e | |||
| a3fc1119ae | |||
| 6394e9afab | |||
| fe41919e1e | |||
| 0cd09f6d28 | |||
| 083a57dab5 | |||
| 4244bcaea3 | |||
| b2d5f5af61 | |||
| 7f26b8dbd0 | |||
| 550e3e0e12 | |||
| 96a3cf9ebc |
79
.clang-tidy
Normal file
@@ -0,0 +1,79 @@
|
||||
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,-warnings-as-errors
|
||||
|
||||
WarningsAsErrors: '*'
|
||||
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
|
||||
HeaderFilterRegex: 'source/.*'
|
||||
FormatStyle: file
|
||||
|
||||
CheckOptions:
|
||||
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
|
||||
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
|
||||
|
||||
# Variables locales en snake_case
|
||||
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
||||
|
||||
# Miembros privados en snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||
|
||||
# Miembros protegidos en snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||
|
||||
# Miembros públicos en snake_case (sin sufijo)
|
||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||
|
||||
# Namespaces en CamelCase
|
||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||
|
||||
# Variables estáticas privadas como miembros privados
|
||||
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||
|
||||
# Constantes estáticas sin sufijo
|
||||
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Constantes globales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Variables constexpr globales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||
|
||||
# Constantes locales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Constexpr miembros en UPPER_CASE (sin sufijo)
|
||||
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
|
||||
|
||||
# Constexpr miembros privados/protegidos con sufijo _
|
||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
|
||||
|
||||
# Clases, structs y enums en CamelCase
|
||||
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
||||
|
||||
# Valores de enums en UPPER_CASE
|
||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Métodos en camelBack (sin sufijos)
|
||||
- { 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 }
|
||||
|
||||
# Funciones en camelBack
|
||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||
|
||||
# Parámetros en lower_case
|
||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"7b0c9c32-3dd4-48a3-ba06-c2303dc08243","pid":123890,"acquiredAt":1776510185734}
|
||||
59
.gitignore
vendored
@@ -1,8 +1,57 @@
|
||||
# --- Build outputs ---
|
||||
build/
|
||||
dist/
|
||||
aee
|
||||
aee.exe
|
||||
.DS_Store
|
||||
trick.ini
|
||||
.vscode/
|
||||
*.o
|
||||
*.obj
|
||||
*.exe
|
||||
*.app
|
||||
|
||||
# --- Generated assets ---
|
||||
resources.pack
|
||||
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*
|
||||
|
||||
87
CHANGELOG.md
@@ -1,6 +1,88 @@
|
||||
# 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 2–5 (`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
|
||||
|
||||
@@ -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
|
||||
|
||||
163
CLAUDE.md
@@ -38,18 +38,25 @@ The five current objectives are:
|
||||
|
||||
**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.
|
||||
|
||||
The current emulator-thread architecture (Director + game thread + `publishFrame` mutex/cv) is **transitional**. It will be dismantled in Phase 5 and replaced by a single-threaded `SDL_AppIterate` loop in Phase 7.
|
||||
|
||||
### Migration Status (2026-04-16)
|
||||
|
||||
Phases 0–7b of the original runtime plan are **done**. Current effort is the **scene-by-scene rewrite of `source/game/modulesequence.cpp`** over a `scenes::` layer in [source/scenes/](source/scenes/):
|
||||
**Completat.** Totes les fases del pla original (0–7) i la migració `scenes::` (Steps 0–10) 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.
|
||||
|
||||
- **Done**: `MortScene` (state 100), `BannerScene` (2..5), `MenuScene` (0), `IntroNewLogoScene` (255 when `use_new_logo`), `SlidesScene` (1, 7), `CreditsScene` (8), `SecretaScene` (6). Each registered in `Director::init` via `SceneRegistry`. Each removed from the legacy `ModuleSequence::Go()` switch and deleted from `modulesequence.cpp`.
|
||||
- **Pending**: `IntroScene` (state 255 when `!use_new_logo` — the old JAILGAMES letter-by-letter), `IntroSpritesScene` (the Sam + momies animation with 3 random variants, hardest of the lot, currently still called from `IntroNewLogoScene::Phase::Delegate` via a temporary `doIntroSprites` exposed as `public` in `ModuleSequence`). Final cleanup of `modulesequence.cpp` comes after those two.
|
||||
- `SceneRegistry` lookup happens inside `gameFiberEntry()` before falling back to legacy `ModuleSequence::Go()`, with a redirect `num_piramide == 6 && diners < 200 → 7` replicated ahead of the lookup to match the legacy flow.
|
||||
- For quick tests, `Options::game` exposes `piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits` — all persisted in `config.yaml`.
|
||||
**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)`).
|
||||
|
||||
The scenes layer itself lives in [source/scenes/](source/scenes/): `scene.hpp` (interface), `scene_registry.hpp/.cpp`, `timeline.hpp/.cpp`, `sprite_mover.hpp/.cpp`, `frame_animator.hpp/.cpp`, `palette_fade.hpp/.cpp`, `surface_handle.hpp/.cpp`, `scene_utils.hpp/.cpp` (`playMusic`). Scenes are pure tick-based (no fibers, no `while`, no `JG_ShouldUpdate`) — the cooperative fiber still runs underneath them but `JD8_Flip()` inside the mini-while in `gameFiberEntry` is what yields. Once `IntroScene` + `IntroSpritesScene` are migrated, the fiber can be dismantled along with `ModuleGame`.
|
||||
**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
|
||||
|
||||
@@ -79,8 +86,8 @@ The scenes layer itself lives in [source/scenes/](source/scenes/): `scene.hpp` (
|
||||
| `source/core/input/` | New input layer | Free to modify |
|
||||
| `source/utils/` | New utilities | Free to modify |
|
||||
| `source/game/options,defines,defaults` | New config system | Free to modify |
|
||||
| `data/*.gif, *.ogg` | Original assets | **Do not modify** — assets remain untouchable |
|
||||
| `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify |
|
||||
| `data/gfx/, data/music/` | Original assets | **Do not modify** — assets remain untouchable |
|
||||
| `data/fonts/, data/shaders/, data/locale/` | New assets | Free to modify |
|
||||
|
||||
### Legacy "Jail" Engine (`source/core/jail/`) — modernization target
|
||||
|
||||
@@ -94,7 +101,7 @@ Flat C-style APIs (no classes), prefixed by subsystem. Being progressively conve
|
||||
|
||||
### 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/`)
|
||||
|
||||
@@ -106,9 +113,10 @@ Flat C-style APIs (no classes), prefixed by subsystem. Being progressively conve
|
||||
|
||||
### 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
|
||||
- **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`
|
||||
|
||||
### Locale Layer (`source/core/locale/`)
|
||||
@@ -120,8 +128,8 @@ Flat C-style APIs (no classes), prefixed by subsystem. Being progressively conve
|
||||
Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
||||
|
||||
- **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`
|
||||
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
|
||||
- **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: `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
|
||||
|
||||
### Utilities (`source/utils/`)
|
||||
|
||||
@@ -140,55 +148,58 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
||||
| F6 | Toggle supersampling |
|
||||
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
|
||||
| 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) |
|
||||
| F11 | Toggle pause (Director stops resuming the game fiber + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
||||
| F11 | Toggle pause (Director skips `scene->tick()` + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
||||
| F12 | Toggle floating options menu |
|
||||
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
||||
| Backspace | Go up one menu level / close menu if at root |
|
||||
| ↑↓←→ / 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.
|
||||
|
||||
### Execution Model (Single-threaded Fibers)
|
||||
### Execution Model (Single-threaded, Scene-based)
|
||||
|
||||
Since Phase 4+5, the old game thread + `publishFrame` mutex/cv has been **removed**. The game code (`ModuleGame`, `ModuleSequence`, all their `Go()` methods with internal `while` loops) runs inside a **cooperative fiber** (see [fiber.hpp](source/core/system/fiber.hpp) / [fiber.cpp](source/core/system/fiber.cpp)). The whole process is single-threaded.
|
||||
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 (only thread)
|
||||
─────────────────────────
|
||||
Director::run() loop {
|
||||
SDL_PollEvent()
|
||||
GlobalInputs, Mouse, KeyRemap
|
||||
SDL_AppIterate → Director::iterate() {
|
||||
if (quit_requested_) { scene.reset(); return false; }
|
||||
if (!context_initialized_) initGameContext();
|
||||
|
||||
Gamepad/KeyRemap/GlobalInputs/Mouse::update
|
||||
JA_Update() ← audio pump
|
||||
if !paused:
|
||||
GameFiber::resume() ← hands control to game code
|
||||
↓ (runs until next JD8_Flip)
|
||||
... game code runs ...
|
||||
JD8_Flip():
|
||||
palette → ARGB → pixel_data
|
||||
GameFiber::yield() ← returns control to Director
|
||||
↓
|
||||
copy JD8_GetFramebuffer() → game_frame
|
||||
|
||||
if (!paused_) {
|
||||
if (scene && (scene->done() || JG_Quitting()))
|
||||
game_state_ = scene->nextState(); scene.reset();
|
||||
if (!scene) {
|
||||
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 to hit 60fps
|
||||
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
|
||||
```
|
||||
|
||||
**Fiber backend** ([fiber.cpp](source/core/system/fiber.cpp)):
|
||||
- **Linux / macOS**: `ucontext_t` + `makecontext`/`swapcontext` (deprecated in POSIX.1-2008 but still functional in glibc and macOS libc; warning silenced with `#pragma`).
|
||||
- **Windows**: `ConvertThreadToFiber` / `CreateFiber` / `SwitchToFiber` (native Fibers API).
|
||||
- **Emscripten**: not yet. Phase 7 will add an `emscripten_fiber_*` or Asyncify backend.
|
||||
|
||||
**Key points:**
|
||||
- Single-threaded: zero `std::thread`, zero `std::mutex`, zero `std::condition_variable`.
|
||||
- `JD8_Flip()` is the natural sync point: it calls `GameFiber::yield()` instead of the old blocking `publishFrame`.
|
||||
- Pause (F11) works by Director skipping `resume()`: the fiber stays frozen at its last yield, and Director keeps repainting the last frame with fresh overlay.
|
||||
- Double buffer still exists (`game_frame` + `presentation_buffer`) because Director can present multiple frames per game frame during pause or slow sections. Eliminating it is marginal work and the extra 256 KB copy is cheap at 320×200.
|
||||
- The state machine alternating `ModuleSequence` (state=1) and `ModuleGame` (state=0) now lives in `gameFiberEntry()` inside an anonymous namespace in [director.cpp](source/core/system/director.cpp), called once as the fiber entry point.
|
||||
- SDL events still processed only on the main thread (which is now the only thread anyway).
|
||||
- `Director` posseeix `current_scene_`, `game_state_`, `last_tick_ms_`, `context_initialized_` com a members — abans vivien al stack del fiber.
|
||||
- `JD8_Flip()` només converteix paleta + `screen` a ARGB (`pixel_data`). Ja **no fa yield** — tot corre lineal.
|
||||
- `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.
|
||||
- Pausa (F11) simplement salta el bloc de tick; overlay i present continuen, es re-presenta l'últim frame congelat.
|
||||
- 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)
|
||||
|
||||
@@ -214,10 +225,40 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
||||
|
||||
| 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/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/`)
|
||||
|
||||
- `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()`
|
||||
@@ -226,11 +267,11 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
||||
|
||||
### 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)
|
||||
- `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
|
||||
- `ui/` — Reserved for future UI graphics
|
||||
|
||||
### Known Issues & Technical Debt
|
||||
|
||||
@@ -238,12 +279,29 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
||||
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.
|
||||
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. La migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 1–9) ha eliminat tots els `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()`. Les cinemàtiques ara són tick-based amb acumuladors ms. `JD8_FadeOut`/`JD8_FadeToPal` encara tenen el seu bucle intern de 32 passos (usat per a transicions fora d'escena com al final de `ModuleGame`); el wrapper tick-based `scenes::PaletteFade` el consumeix un pas per tick quan es crida des d'una escena.
|
||||
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::run()`. Ported from the `jaildoctors_dilemma` codebase.
|
||||
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5. Replaced by a cooperative `GameFiber` (ucontext on POSIX, Fibers API on Windows). `JD8_Flip()` calls `GameFiber::yield()`, Director calls `GameFiber::resume()` once per frame. Zero threads, zero mutexes. Emscripten fiber backend still pending for Phase 7.
|
||||
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. Migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 1–10) + `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
|
||||
|
||||
- **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).
|
||||
- **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.
|
||||
@@ -251,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.
|
||||
- **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.
|
||||
- **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)
|
||||
|
||||
|
||||
197
CMakeLists.txt
@@ -3,6 +3,11 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
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++
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
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
|
||||
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 ---
|
||||
set(APP_SOURCES
|
||||
# Core - Motor original "Jail" (no tocar gameplay)
|
||||
source/core/jail/jail_audio.cpp
|
||||
source/core/jail/jdraw8.cpp
|
||||
source/core/jail/jfile.cpp
|
||||
source/core/jail/jgame.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)
|
||||
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)
|
||||
source/core/rendering/menu.cpp
|
||||
source/core/rendering/overlay.cpp
|
||||
@@ -34,12 +71,12 @@ set(APP_SOURCES
|
||||
# Core - Input (nova capa)
|
||||
source/core/input/gamepad.cpp
|
||||
source/core/input/global_inputs.cpp
|
||||
source/core/input/key_config.cpp
|
||||
source/core/input/key_remap.cpp
|
||||
source/core/input/mouse.cpp
|
||||
|
||||
# Core - System (nova capa)
|
||||
source/core/system/director.cpp
|
||||
source/core/system/fiber.cpp
|
||||
|
||||
# Scenes (cinemàtiques i menús reescrits)
|
||||
source/scenes/timeline.cpp
|
||||
@@ -49,6 +86,7 @@ set(APP_SOURCES
|
||||
source/scenes/surface_handle.cpp
|
||||
source/scenes/scene_registry.cpp
|
||||
source/scenes/scene_utils.cpp
|
||||
source/scenes/boot_loader_scene.cpp
|
||||
source/scenes/mort_scene.cpp
|
||||
source/scenes/banner_scene.cpp
|
||||
source/scenes/menu_scene.cpp
|
||||
@@ -81,8 +119,22 @@ set(APP_SOURCES
|
||||
|
||||
# Configuración de SDL3
|
||||
# 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.
|
||||
if(APPLE AND MACOS_BUNDLE)
|
||||
# En emscripten compilamos SDL3 desde source con FetchContent (no hi ha paquet de sistema).
|
||||
# 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")
|
||||
message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})")
|
||||
else()
|
||||
@@ -90,12 +142,12 @@ else()
|
||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||
endif()
|
||||
|
||||
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal) ---
|
||||
if(NOT APPLE)
|
||||
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal, Emscripten no suporta SDL3 GPU) ---
|
||||
if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||
find_program(GLSLC_EXE NAMES glslc)
|
||||
|
||||
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
|
||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
@@ -138,21 +190,32 @@ if(NOT APPLE)
|
||||
endforeach()
|
||||
message(STATUS "glslc no trobat — usant headers SPIR-V precompilats")
|
||||
endif()
|
||||
elseif(EMSCRIPTEN)
|
||||
message(STATUS "Emscripten: shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2)")
|
||||
else()
|
||||
message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)")
|
||||
endif()
|
||||
|
||||
# --- 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)
|
||||
if(NOT APPLE AND GLSLC_EXE)
|
||||
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
endif()
|
||||
|
||||
# --- DIRECTORIOS DE INCLUSIÓN ---
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
|
||||
# Enlazar SDL3
|
||||
@@ -177,15 +240,72 @@ target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunctio
|
||||
# --- CONFIGURACIÓN POR PLATAFORMA ---
|
||||
if(WIN32)
|
||||
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()
|
||||
|
||||
# Ejecutable en la raíz del proyecto
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
# Ejecutable en la raíz del proyecto (solo nativos). A Emscripten queda dins build/.
|
||||
if(NOT EMSCRIPTEN)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
# --- CLANG-FORMAT TARGETS ---
|
||||
# --- EINA STANDALONE: pack_resources ---
|
||||
# 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)
|
||||
|
||||
# --- 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_SOURCE_DIR}/resources.pack")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${RESOURCE_PACK}
|
||||
COMMAND $<TARGET_FILE:pack_resources>
|
||||
"${CMAKE_SOURCE_DIR}/data"
|
||||
"${RESOURCE_PACK}"
|
||||
DEPENDS pack_resources ${DATA_FILES}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Empaquetant data/ → resources.pack"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||
endif()
|
||||
|
||||
# --- STATIC ANALYSIS TARGETS ---
|
||||
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
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
|
||||
"${CMAKE_SOURCE_DIR}/source/*.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/source/*.hpp"
|
||||
@@ -193,6 +313,35 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||
)
|
||||
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)
|
||||
add_custom_target(format
|
||||
COMMAND ${CLANG_FORMAT_EXE}
|
||||
@@ -213,3 +362,25 @@ if(CLANG_FORMAT_EXE)
|
||||
else()
|
||||
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
||||
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()
|
||||
|
||||
289
Makefile
@@ -4,6 +4,18 @@
|
||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
||||
|
||||
# ==============================================================================
|
||||
# TOOLS
|
||||
# ==============================================================================
|
||||
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
|
||||
SHADERS_DIR := $(DIR_ROOT)data/shaders
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GLSLC := $(shell where glslc 2>NUL)
|
||||
else
|
||||
GLSLC := $(shell command -v glslc 2>/dev/null)
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# TARGET NAMES
|
||||
# ==============================================================================
|
||||
@@ -18,9 +30,23 @@ RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||
# VERSION (extracted from defines.hpp)
|
||||
# ==============================================================================
|
||||
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
|
||||
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
|
||||
|
||||
# ==============================================================================
|
||||
@@ -37,9 +63,13 @@ endif
|
||||
ifeq ($(OS),Windows_NT)
|
||||
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
||||
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
|
||||
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
|
||||
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
|
||||
else
|
||||
WIN_TARGET_FILE := $(TARGET_FILE)
|
||||
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
||||
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
@@ -65,17 +95,42 @@ else
|
||||
UNAME_S := $(shell uname -s)
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
|
||||
# ==============================================================================
|
||||
ifeq ($(OS),Windows_NT)
|
||||
CMAKE_GEN := -G "MinGW Makefiles"
|
||||
else
|
||||
CMAKE_GEN :=
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN CON CMAKE
|
||||
# ==============================================================================
|
||||
all:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build
|
||||
|
||||
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
|
||||
|
||||
# ==============================================================================
|
||||
# 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 resources.pack
|
||||
|
||||
# ==============================================================================
|
||||
# RELEASE AUTOMÁTICO (detecta SO)
|
||||
# ==============================================================================
|
||||
@@ -93,12 +148,12 @@ endif
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||
# ==============================================================================
|
||||
_windows_release:
|
||||
_windows_release: pack
|
||||
@echo off
|
||||
@echo Creando release para Windows - Version: $(VERSION)
|
||||
|
||||
# Compila con cmake
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build
|
||||
|
||||
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
|
||||
@@ -106,13 +161,13 @@ _windows_release:
|
||||
@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}"
|
||||
|
||||
# Copia ficheros
|
||||
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse"
|
||||
# Copia ficheros (resources.pack substitueix la carpeta data/)
|
||||
@powershell -Command "Copy-Item 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'LICENSE' -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 '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
|
||||
|
||||
# Crea el fichero .zip
|
||||
@@ -126,15 +181,31 @@ _windows_release:
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||
# ==============================================================================
|
||||
_macos_release:
|
||||
_macos_release: pack
|
||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||
|
||||
# Verificar e instalar create-dmg si es necesario
|
||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
||||
|
||||
# Compila la versión para procesadores Intel con cmake
|
||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
|
||||
@cmake --build build/intel
|
||||
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " Falta la dependencia: create-dmg"; \
|
||||
echo "============================================"; \
|
||||
if command -v brew >/dev/null 2>&1; then \
|
||||
echo " Instalando con: brew install create-dmg"; \
|
||||
brew install create-dmg || { \
|
||||
echo ""; \
|
||||
echo " ERROR: 'brew install create-dmg' ha fallado."; \
|
||||
echo " Ejecuta el comando manualmente y vuelve a probar."; \
|
||||
exit 1; \
|
||||
}; \
|
||||
else \
|
||||
echo " Homebrew no está instalado."; \
|
||||
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
|
||||
echo " brew install create-dmg"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
}
|
||||
|
||||
# Elimina datos de compilaciones anteriores
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
@@ -148,8 +219,8 @@ _macos_release:
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
|
||||
# Copia carpetas y ficheros
|
||||
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
# Copia carpetas y ficheros (resources.pack substitueix la carpeta data/)
|
||||
cp resources.pack "$(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 release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
@@ -163,31 +234,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>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||
|
||||
# Copia el ejecutable Intel al bundle
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
|
||||
# Firma la aplicación
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
||||
|
||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
||||
@echo "Creando DMG Intel con iconos de 96x96..."
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--icon "$(APP_NAME).app" 278 102 \
|
||||
--icon "LICENSE" 441 102 \
|
||||
--icon "README.md" 604 102 \
|
||||
--app-drop-link 115 102 \
|
||||
--hide-extension "$(APP_NAME).app" \
|
||||
"$(MACOS_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)" || true
|
||||
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
|
||||
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||
# DMG Intel y continúa con la build de Apple Silicon).
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Intel (x86_64)"
|
||||
@echo "============================================"
|
||||
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
|
||||
&& cmake --build build/intel; then \
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||
echo "Creando DMG Intel con iconos de 96x96..."; \
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--icon "$(APP_NAME).app" 278 102 \
|
||||
--icon "LICENSE" 441 102 \
|
||||
--icon "README.md" 604 102 \
|
||||
--app-drop-link 115 102 \
|
||||
--hide-extension "$(APP_NAME).app" \
|
||||
"$(MACOS_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)" || true; \
|
||||
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " WARNING: la build Intel ha fallado."; \
|
||||
echo " Se omite el DMG Intel y se continúa con"; \
|
||||
echo " la build de Apple Silicon."; \
|
||||
echo "============================================"; \
|
||||
echo ""; \
|
||||
fi
|
||||
|
||||
# Compila la versión para procesadores Apple Silicon con cmake
|
||||
@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
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
|
||||
@@ -217,22 +307,64 @@ _macos_release:
|
||||
$(RMDIR) build/arm
|
||||
$(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)
|
||||
# ==============================================================================
|
||||
_linux_release:
|
||||
_linux_release: pack
|
||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||
|
||||
# Compila con cmake
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build
|
||||
|
||||
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# Copia ficheros
|
||||
cp -r data "$(RELEASE_FOLDER)"
|
||||
# Copia ficheros (resources.pack substitueix la carpeta data/)
|
||||
cp resources.pack "$(RELEASE_FOLDER)"
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||
@@ -247,4 +379,69 @@ _linux_release:
|
||||
# Elimina la carpeta temporal
|
||||
$(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
|
||||
|
||||
# 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 " 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 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 help - Mostrar esta ajuda"
|
||||
@echo ""
|
||||
@echo " Versio actual: $(VERSION) ($(GIT_HASH))"
|
||||
|
||||
.PHONY: all debug pack release wasm wasm_debug _windows_release _linux_release _macos_release compile_shaders controllerdb format format-check tidy tidy-fix cppcheck help
|
||||
|
||||
52
data/config/assets.yaml
Normal 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
data/crtpi.glsl
@@ -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
|
||||
|
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 |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
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 |
|
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 |
50
data/input/keys.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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: toggle_supersampling
|
||||
code: "F6"
|
||||
desc: "Activa/desactiva supersampling"
|
||||
- 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"
|
||||
@@ -4,77 +4,89 @@
|
||||
|
||||
menu:
|
||||
titles:
|
||||
root: "OPCIONS"
|
||||
video: "VIDEO"
|
||||
audio: "AUDIO"
|
||||
controls: "CONTROLS"
|
||||
game: "JOC"
|
||||
root: "Opcions"
|
||||
video: "Vídeo"
|
||||
audio: "Àudio"
|
||||
controls: "Controls"
|
||||
game: "Joc"
|
||||
system: "Sistema"
|
||||
|
||||
items:
|
||||
video: "VIDEO"
|
||||
audio: "AUDIO"
|
||||
controls: "CONTROLS"
|
||||
game: "JOC"
|
||||
use_new_logo: "LOGO NOU"
|
||||
show_title_credits: "CREDITS DEL PORT"
|
||||
zoom: "ZOOM"
|
||||
screen: "PANTALLA"
|
||||
shader: "SHADER"
|
||||
aspect_4_3: "ASPECTE 4:3"
|
||||
supersampling: "SUPERSAMPLING"
|
||||
vsync: "VSYNC"
|
||||
integer_scale: "ESCALA ENTERA"
|
||||
shader_type: "TIPUS SHADER"
|
||||
preset: "PRESET"
|
||||
stretch_filter: "FILTRE 4:3"
|
||||
render_info: "RENDER INFO"
|
||||
uptime: "TEMPS DE JOC"
|
||||
master_enable: "AUDIO"
|
||||
master_volume: "MASTER"
|
||||
music: "MUSICA"
|
||||
music_volume: "VOL MUSICA"
|
||||
sounds: "SONS"
|
||||
sounds_volume: "VOL SONS"
|
||||
move_up: "MOU AMUNT"
|
||||
move_down: "MOU AVALL"
|
||||
move_left: "MOU ESQUERRA"
|
||||
move_right: "MOU DRETA"
|
||||
menu_key: "TECLA MENU"
|
||||
video: "Vídeo"
|
||||
audio: "Àudio"
|
||||
controls: "Controls"
|
||||
game: "Joc"
|
||||
system: "Sistema"
|
||||
restart: "Reinicia"
|
||||
exit_game: "Eixir del joc"
|
||||
use_new_logo: "Logo nou"
|
||||
show_title_credits: "Crèdits del port"
|
||||
show_preload: "Barra de precàrrega"
|
||||
zoom: "Zoom"
|
||||
screen: "Pantalla"
|
||||
shader: "Shader"
|
||||
aspect_4_3: "Aspecte 4:3"
|
||||
supersampling: "Supersampling"
|
||||
vsync: "Vsync"
|
||||
scaling_mode: "Escala"
|
||||
shader_type: "Tipus shader"
|
||||
preset: "Preset"
|
||||
texture_filter: "Filtre textura"
|
||||
render_info: "Render info"
|
||||
uptime: "Temps de joc"
|
||||
internal_resolution: "Resolució interna"
|
||||
master_enable: "Àudio"
|
||||
master_volume: "Màster"
|
||||
music: "Música"
|
||||
music_volume: "Vol música"
|
||||
sounds: "Sons"
|
||||
sounds_volume: "Vol sons"
|
||||
move_up: "Mou amunt"
|
||||
move_down: "Mou avall"
|
||||
move_left: "Mou esquerra"
|
||||
move_right: "Mou dreta"
|
||||
menu_key: "Tecla menú"
|
||||
|
||||
values:
|
||||
"yes": "SI"
|
||||
"no": "NO"
|
||||
"on": "ON"
|
||||
"off": "OFF"
|
||||
fullscreen: "COMPLETA"
|
||||
windowed: "FINESTRA"
|
||||
linear: "LINEAR"
|
||||
nearest: "NEAREST"
|
||||
top: "TOP"
|
||||
bottom: "BOTTOM"
|
||||
press_key: "<PREM TECLA>"
|
||||
empty: "(BUIT)"
|
||||
"yes": "Sí"
|
||||
"no": "No"
|
||||
"on": "On"
|
||||
"off": "Off"
|
||||
fullscreen: "Completa"
|
||||
windowed: "Finestra"
|
||||
linear: "Linear"
|
||||
nearest: "Nearest"
|
||||
top: "Top"
|
||||
bottom: "Bottom"
|
||||
press_key: "<Prem tecla>"
|
||||
empty: "(Buit)"
|
||||
unknown: "---"
|
||||
scaling_disabled: "Sense escala"
|
||||
scaling_stretch: "Estirada"
|
||||
scaling_letterbox: "Letterbox"
|
||||
scaling_overscan: "Overscan"
|
||||
scaling_integer: "Entera"
|
||||
|
||||
window:
|
||||
title: "© 2000 Aventures en Egipte — JailDesigner"
|
||||
|
||||
notifications:
|
||||
exit_double_esc: "TORNA A PULSAR ESC PER EIXIR"
|
||||
zoom_fmt: "ZOOM %dX"
|
||||
fullscreen: "PANTALLA COMPLETA"
|
||||
windowed: "FINESTRA"
|
||||
shader_on: "SHADER ON"
|
||||
shader_off: "SHADER OFF"
|
||||
exit_double_esc: "Torna a pulsar ESC per a eixir"
|
||||
zoom_fmt: "Zoom %dX"
|
||||
fullscreen: "Pantalla completa"
|
||||
windowed: "Finestra"
|
||||
shader_on: "Shader on"
|
||||
shader_off: "Shader off"
|
||||
aspect_43: "4:3 CRT"
|
||||
aspect_square: "PIXELS QUADRATS"
|
||||
ss_on: "SUPERSAMPLING ON"
|
||||
ss_off: "SUPERSAMPLING OFF"
|
||||
preset_fmt: "PRESET: %s"
|
||||
filter_linear: "FILTRE: LINEAR"
|
||||
filter_nearest: "FILTRE: NEAREST"
|
||||
pause: "PAUSA"
|
||||
resume: "REPRES"
|
||||
aspect_square: "Píxels quadrats"
|
||||
ss_on: "Supersampling on"
|
||||
ss_off: "Supersampling off"
|
||||
preset_fmt: "Preset: %s"
|
||||
filter_linear: "Filtre: linear"
|
||||
filter_nearest: "Filtre: nearest"
|
||||
pause: "Pausa"
|
||||
gamepad_connected: "connectat"
|
||||
gamepad_disconnected: "desconnectat"
|
||||
|
||||
credits:
|
||||
port_role: "Conversio a C++ i SDL3"
|
||||
|
||||
207
source/core/audio/audio.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#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"
|
||||
// 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_MUSIC_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_t* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) return;
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(music, crossfade_ms, loop);
|
||||
} else {
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic();
|
||||
}
|
||||
JA_PlayMusic(music, loop);
|
||||
}
|
||||
|
||||
music_.name.clear(); // nom desconegut quan es passa per punter
|
||||
music_.loop = (loop != 0);
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Pausa la música
|
||||
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_t* 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_Music_state ja_state = JA_GetMusicState();
|
||||
switch (ja_state) {
|
||||
case JA_MUSIC_PLAYING:
|
||||
return MusicState::PLAYING;
|
||||
case JA_MUSIC_PAUSED:
|
||||
return MusicState::PAUSED;
|
||||
case JA_MUSIC_STOPPED:
|
||||
case JA_MUSIC_INVALID:
|
||||
case JA_MUSIC_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);
|
||||
}
|
||||
}
|
||||
115
source/core/audio/audio.hpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para int8_t, uint8_t
|
||||
#include <memory> // Para std::unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
// --- Clase Audio: gestor de audio (singleton) ---
|
||||
// Implementació canònica, byte-idèntica entre projectes.
|
||||
// Els volums es manegen internament com a float 0.0–1.0; la capa de
|
||||
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
|
||||
// per mostrar 0–100 a l'usuari.
|
||||
class Audio {
|
||||
public:
|
||||
// --- Enums ---
|
||||
enum class Group : 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(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
|
||||
// --- Control de sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
|
||||
// --- Control de volumen (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
void setMusicVolume(float volume) const; // Ajustar volumen de música
|
||||
|
||||
// --- Helpers de conversió per a la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
static constexpr auto toPercent(float volume) -> int {
|
||||
return static_cast<int>(volume * 100.0F + 0.5F);
|
||||
}
|
||||
static constexpr auto fromPercent(int percent) -> float {
|
||||
return static_cast<float>(percent) / 100.0F;
|
||||
}
|
||||
|
||||
// --- Configuración general ---
|
||||
void enable(bool value); // Establecer estado general
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
|
||||
void applySettings(); // Aplica la configuración
|
||||
|
||||
// --- Configuración de sonidos ---
|
||||
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
source/core/audio/audio_adapter.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "core/audio/audio_adapter.hpp"
|
||||
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
|
||||
namespace AudioResource {
|
||||
|
||||
JA_Music_t* getMusic(const std::string& name) {
|
||||
return Resource::Cache::get()->getMusic(name);
|
||||
}
|
||||
|
||||
JA_Sound_t* getSound(const std::string& name) {
|
||||
return Resource::Cache::get()->getSound(name);
|
||||
}
|
||||
|
||||
} // namespace AudioResource
|
||||
17
source/core/audio/audio_adapter.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
// --- Audio Resource Adapter ---
|
||||
// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* /
|
||||
// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp
|
||||
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
|
||||
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
|
||||
|
||||
#include <string> // Para string
|
||||
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
namespace AudioResource {
|
||||
JA_Music_t* getMusic(const std::string& name);
|
||||
JA_Sound_t* getSound(const std::string& name);
|
||||
} // namespace AudioResource
|
||||
@@ -2,16 +2,26 @@
|
||||
|
||||
// --- Includes ---
|
||||
#include <SDL3/SDL.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h> // Para uint32_t, uint8_t
|
||||
#include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
|
||||
#include <stdlib.h> // Para free, malloc
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream> // Para std::cout
|
||||
#include <memory> // Para std::unique_ptr
|
||||
#include <string> // Para std::string
|
||||
#include <vector> // Para std::vector
|
||||
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming
|
||||
|
||||
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
|
||||
// Compatible amb `std::unique_ptr<Uint8[], SDLFreeDeleter>` — zero size
|
||||
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
|
||||
struct SDLFreeDeleter {
|
||||
void operator()(Uint8* p) const noexcept {
|
||||
if (p) SDL_free(p);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Public Enums ---
|
||||
enum JA_Channel_state {
|
||||
@@ -36,7 +46,9 @@ enum JA_Music_state {
|
||||
struct JA_Sound_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{nullptr};
|
||||
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
|
||||
// via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free.
|
||||
std::unique_ptr<Uint8[], SDLFreeDeleter> buffer;
|
||||
};
|
||||
|
||||
struct JA_Channel_t {
|
||||
@@ -78,15 +90,27 @@ inline bool JA_musicEnabled{true};
|
||||
inline bool JA_soundEnabled{true};
|
||||
inline SDL_AudioDeviceID sdlAudioDevice{0};
|
||||
|
||||
inline bool fading{false};
|
||||
inline int fade_start_time{0};
|
||||
inline int fade_duration{0};
|
||||
inline float fade_initial_volume{0.0f};
|
||||
// --- Crossfade / Fade State ---
|
||||
struct JA_FadeState {
|
||||
bool active{false};
|
||||
Uint64 start_time{0};
|
||||
int duration_ms{0};
|
||||
float initial_volume{0.0f};
|
||||
};
|
||||
|
||||
struct JA_OutgoingMusic {
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_FadeState fade;
|
||||
};
|
||||
|
||||
inline JA_OutgoingMusic outgoing_music;
|
||||
inline JA_FadeState incoming_fade;
|
||||
|
||||
// --- Forward Declarations ---
|
||||
inline void JA_StopMusic();
|
||||
inline void JA_StopChannel(const int channel);
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
|
||||
inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1);
|
||||
|
||||
// --- Music streaming internals ---
|
||||
// Bytes-per-sample per canal (sempre s16)
|
||||
@@ -94,7 +118,7 @@ static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2;
|
||||
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||
static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192;
|
||||
// Umbral d'àudio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
|
||||
|
||||
@@ -104,15 +128,15 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) {
|
||||
if (!music || !music->vorbis || !music->stream) return 0;
|
||||
|
||||
short chunk[JA_MUSIC_CHUNK_SHORTS];
|
||||
const int channels = music->spec.channels;
|
||||
const int num_channels = music->spec.channels;
|
||||
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
|
||||
music->vorbis,
|
||||
channels,
|
||||
num_channels,
|
||||
chunk,
|
||||
JA_MUSIC_CHUNK_SHORTS);
|
||||
if (samples_per_channel <= 0) return 0;
|
||||
|
||||
const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, chunk, bytes);
|
||||
return samples_per_channel;
|
||||
}
|
||||
@@ -139,23 +163,51 @@ inline void JA_PumpMusic(JA_Music_t* music) {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
|
||||
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
|
||||
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
|
||||
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
|
||||
inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
|
||||
if (!music || !music->vorbis || !music->stream) return;
|
||||
|
||||
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||
const int needed_bytes = static_cast<int>((static_cast<int64_t>(duration_ms) * bytes_per_second) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) {
|
||||
const int decoded = JA_FeedMusicChunk(music);
|
||||
if (decoded <= 0) break; // EOF: deixem drenar el que hi haja
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Functions ---
|
||||
|
||||
// Crida-la una vegada per frame des del main loop (Director). Substituïx
|
||||
// el callback asíncron SDL_AddTimer de la versió antiga — imprescindible
|
||||
// per al port a emscripten/SDL_AppIterate.
|
||||
inline void JA_Update() {
|
||||
// --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
|
||||
if (outgoing_music.stream && outgoing_music.fade.active) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
Uint64 elapsed = now - outgoing_music.fade.start_time;
|
||||
if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
} else {
|
||||
float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms;
|
||||
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Current music ---
|
||||
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
|
||||
if (fading) {
|
||||
int time = SDL_GetTicks();
|
||||
if (time > (fade_start_time + fade_duration)) {
|
||||
fading = false;
|
||||
JA_StopMusic();
|
||||
return;
|
||||
// Fade-in (parte de un crossfade)
|
||||
if (incoming_fade.active) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
Uint64 elapsed = now - incoming_fade.start_time;
|
||||
if (elapsed >= (Uint64)incoming_fade.duration_ms) {
|
||||
incoming_fade.active = false;
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||
} else {
|
||||
const int time_passed = time - fade_start_time;
|
||||
const float percent = (float)time_passed / (float)fade_duration;
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0f - percent));
|
||||
float percent = (float)elapsed / (float)incoming_fade.duration_ms;
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,12 +219,13 @@ inline void JA_Update() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sound channels ---
|
||||
if (JA_soundEnabled) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
if (channels[i].times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
|
||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
|
||||
if (channels[i].times > 0) channels[i].times--;
|
||||
}
|
||||
} else {
|
||||
@@ -183,19 +236,19 @@ inline void JA_Update() {
|
||||
}
|
||||
|
||||
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||
#ifdef _DEBUG
|
||||
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
|
||||
#endif
|
||||
|
||||
JA_audioSpec = {format, num_channels, freq};
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
|
||||
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
|
||||
}
|
||||
|
||||
inline void JA_Quit() {
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
}
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||
sdlAudioDevice = 0;
|
||||
}
|
||||
@@ -212,11 +265,13 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
auto* music = new JA_Music_t();
|
||||
music->ogg_data.assign(buffer, buffer + length);
|
||||
|
||||
int error = 0;
|
||||
int vorbis_error = 0;
|
||||
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||
static_cast<int>(length), &error, nullptr);
|
||||
static_cast<int>(length),
|
||||
&vorbis_error,
|
||||
nullptr);
|
||||
if (!music->vorbis) {
|
||||
SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error);
|
||||
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
|
||||
delete music;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -238,6 +293,35 @@ inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filena
|
||||
return music;
|
||||
}
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
// Carreguem primer el arxiu en memòria i després el descomprimim.
|
||||
FILE* f = fopen(filename, "rb");
|
||||
if (!f) return nullptr;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
|
||||
if (!buffer) {
|
||||
fclose(f);
|
||||
return nullptr;
|
||||
}
|
||||
if (fread(buffer, fsize, 1, f) != 1) {
|
||||
fclose(f);
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
|
||||
if (music) {
|
||||
music->filename = filename;
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
|
||||
return music;
|
||||
}
|
||||
|
||||
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
if (!JA_musicEnabled || !music || !music->vorbis) return;
|
||||
|
||||
@@ -253,7 +337,7 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||
if (!current_music->stream) {
|
||||
SDL_Log("Failed to create audio stream!");
|
||||
std::cout << "Failed to create audio stream!" << '\n';
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
return;
|
||||
}
|
||||
@@ -262,10 +346,12 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
|
||||
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
|
||||
JA_PumpMusic(current_music);
|
||||
|
||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) {
|
||||
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* JA_GetMusicFilename(JA_Music_t* music = nullptr) {
|
||||
inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
|
||||
if (!music) music = current_music;
|
||||
if (!music || music->filename.empty()) return nullptr;
|
||||
return music->filename.c_str();
|
||||
@@ -288,6 +374,14 @@ inline void JA_ResumeMusic() {
|
||||
}
|
||||
|
||||
inline void JA_StopMusic() {
|
||||
// Limpiar outgoing crossfade si existe
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
}
|
||||
incoming_fade.active = false;
|
||||
|
||||
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
||||
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
@@ -304,12 +398,69 @@ inline void JA_StopMusic() {
|
||||
|
||||
inline void JA_FadeOutMusic(const int milliseconds) {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
|
||||
|
||||
fading = true;
|
||||
fade_start_time = SDL_GetTicks();
|
||||
fade_duration = milliseconds;
|
||||
fade_initial_volume = JA_musicVolume;
|
||||
// Destruir outgoing anterior si existe
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
}
|
||||
|
||||
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
|
||||
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
|
||||
JA_PreFillOutgoing(current_music, milliseconds);
|
||||
|
||||
// Robar el stream del current_music al outgoing
|
||||
outgoing_music.stream = current_music->stream;
|
||||
outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume};
|
||||
|
||||
// Dejar current_music sin stream (ya lo tiene outgoing)
|
||||
current_music->stream = nullptr;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
|
||||
incoming_fade.active = false;
|
||||
}
|
||||
|
||||
inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) {
|
||||
if (!JA_musicEnabled || !music || !music->vorbis) return;
|
||||
|
||||
// Destruir outgoing anterior si existe (crossfade durante crossfade)
|
||||
if (outgoing_music.stream) {
|
||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||
outgoing_music.stream = nullptr;
|
||||
outgoing_music.fade.active = false;
|
||||
}
|
||||
|
||||
// Robar el stream de la musica actual al outgoing para el fade-out.
|
||||
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
|
||||
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
|
||||
if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) {
|
||||
JA_PreFillOutgoing(current_music, crossfade_ms);
|
||||
outgoing_music.stream = current_music->stream;
|
||||
outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume};
|
||||
current_music->stream = nullptr;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
|
||||
}
|
||||
|
||||
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
|
||||
current_music = music;
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
current_music->times = loop;
|
||||
|
||||
stb_vorbis_seek_start(current_music->vorbis);
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||
if (!current_music->stream) {
|
||||
std::cout << "Failed to create audio stream for crossfade!" << '\n';
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
return;
|
||||
}
|
||||
SDL_SetAudioStreamGain(current_music->stream, 0.0f);
|
||||
JA_PumpMusic(current_music); // pre-carrega abans de bindejar
|
||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||
|
||||
// Configurar fade-in
|
||||
incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f};
|
||||
}
|
||||
|
||||
inline JA_Music_state JA_GetMusicState() {
|
||||
@@ -355,31 +506,26 @@ inline void JA_EnableMusic(const bool value) {
|
||||
|
||||
// --- Sound Functions ---
|
||||
|
||||
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
sound->buffer = buffer;
|
||||
sound->length = length;
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
|
||||
delete sound;
|
||||
auto sound = std::make_unique<JA_Sound_t>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
|
||||
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
|
||||
delete sound;
|
||||
auto sound = std::make_unique<JA_Sound_t>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
|
||||
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
|
||||
@@ -387,7 +533,10 @@ inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group =
|
||||
|
||||
int channel = 0;
|
||||
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
||||
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
|
||||
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
// No hay canal libre, reemplazamos el primero
|
||||
channel = 0;
|
||||
}
|
||||
|
||||
return JA_PlaySoundOnChannel(sound, channel, loop, group);
|
||||
}
|
||||
@@ -406,12 +555,12 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
|
||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||
|
||||
if (!channels[channel].stream) {
|
||||
SDL_Log("Failed to create audio stream for sound!");
|
||||
std::cout << "Failed to create audio stream for sound!" << '\n';
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
|
||||
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||
|
||||
@@ -423,7 +572,7 @@ inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||
}
|
||||
SDL_free(sound->buffer);
|
||||
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
|
||||
delete sound;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,73 @@
|
||||
#include "core/input/gamepad.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/menu.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "core/rendering/overlay.hpp"
|
||||
|
||||
namespace Gamepad {
|
||||
|
||||
static SDL_Gamepad* pad_ = nullptr;
|
||||
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 std::string prettyName(const char* raw) {
|
||||
std::string name = (raw && *raw) ? 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)
|
||||
static constexpr Sint16 STICK_DEADZONE = 12000;
|
||||
|
||||
@@ -19,22 +76,41 @@ namespace Gamepad {
|
||||
static bool prev_down_ = false;
|
||||
static bool prev_left_ = false;
|
||||
static bool prev_right_ = false;
|
||||
static bool prev_a_ = false;
|
||||
static bool prev_b_ = false;
|
||||
static bool prev_south_ = false;
|
||||
static bool prev_east_ = false;
|
||||
static bool prev_west_ = 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() {
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetGamepads(&count);
|
||||
if (ids && count > 0) {
|
||||
pad_ = SDL_OpenGamepad(ids[0]);
|
||||
if (pad_) {
|
||||
pad_id_ = ids[0];
|
||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
||||
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||
if (ids) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
installWebStandardMapping(ids[i]);
|
||||
if (!SDL_IsGamepad(ids[i])) continue;
|
||||
pad_ = SDL_OpenGamepad(ids[i]);
|
||||
if (pad_) {
|
||||
pad_id_ = ids[i];
|
||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
||||
break;
|
||||
}
|
||||
}
|
||||
SDL_free(ids);
|
||||
}
|
||||
if (ids) SDL_free(ids);
|
||||
}
|
||||
|
||||
void init() {
|
||||
@@ -65,17 +141,26 @@ namespace Gamepad {
|
||||
}
|
||||
|
||||
void handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
// A Emscripten els dispositius web entren com a JOYSTICK_ADDED (no
|
||||
// GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
|
||||
// injectem el mapping estàndard abans d'obrir el mando.
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
|
||||
if (!pad_) {
|
||||
pad_ = SDL_OpenGamepad(event.gdevice.which);
|
||||
SDL_JoystickID jid = event.jdevice.which;
|
||||
installWebStandardMapping(jid);
|
||||
if (!SDL_IsGamepad(jid)) return;
|
||||
pad_ = SDL_OpenGamepad(jid);
|
||||
if (pad_) {
|
||||
pad_id_ = event.gdevice.which;
|
||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
||||
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) {
|
||||
if (pad_ && event.gdevice.which == pad_id_) {
|
||||
SDL_Log("Gamepad desconnectat");
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
if (pad_ && event.jdevice.which == pad_id_) {
|
||||
std::string saved_name = prettyName(SDL_GetGamepadName(pad_));
|
||||
SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
|
||||
SDL_CloseGamepad(pad_);
|
||||
pad_ = nullptr;
|
||||
pad_id_ = 0;
|
||||
@@ -84,6 +169,7 @@ namespace Gamepad {
|
||||
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);
|
||||
notifyDisconnected(saved_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,16 +211,18 @@ namespace Gamepad {
|
||||
bool lt = dlt || slt;
|
||||
bool rt = drt || srt;
|
||||
|
||||
// Botons
|
||||
bool a = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH); // A/Cross
|
||||
bool b = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST); // B/Circle
|
||||
// Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
|
||||
bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
bool east = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST);
|
||||
bool west = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_WEST);
|
||||
bool north = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_NORTH);
|
||||
bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START);
|
||||
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);
|
||||
// Select (Back) → obre/tanca menú de servei (flanc)
|
||||
if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle"));
|
||||
// Start → pausa (flanc)
|
||||
if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle"));
|
||||
|
||||
if (Menu::isOpen()) {
|
||||
// Navegació del menú per flanc
|
||||
@@ -142,8 +230,9 @@ namespace Gamepad {
|
||||
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);
|
||||
// EAST accepta, SOUTH cancela / endarrere
|
||||
if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN);
|
||||
if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE);
|
||||
|
||||
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
||||
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
||||
@@ -156,16 +245,21 @@ namespace Gamepad {
|
||||
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);
|
||||
// Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic)
|
||||
if ((south && !prev_south_) || (east && !prev_east_) ||
|
||||
(west && !prev_west_) || (north && !prev_north_)) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
}
|
||||
|
||||
prev_up_ = up;
|
||||
prev_down_ = dn;
|
||||
prev_left_ = lt;
|
||||
prev_right_ = rt;
|
||||
prev_a_ = a;
|
||||
prev_b_ = b;
|
||||
prev_south_ = south;
|
||||
prev_east_ = east;
|
||||
prev_west_ = west;
|
||||
prev_north_ = north;
|
||||
prev_start_ = start;
|
||||
prev_back_ = back;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/overlay.hpp"
|
||||
@@ -19,14 +20,14 @@ namespace GlobalInputs {
|
||||
static bool ss_prev = false;
|
||||
static bool next_shader_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;
|
||||
|
||||
auto handle() -> bool {
|
||||
bool consumed = false;
|
||||
|
||||
// F1 — Reduir zoom
|
||||
bool dec_zoom = JI_KeyPressed(Options::keys_gui.dec_zoom);
|
||||
bool dec_zoom = JI_KeyPressed(KeyConfig::scancode("dec_zoom"));
|
||||
if (dec_zoom && !dec_zoom_prev) {
|
||||
Screen::get()->decZoom();
|
||||
char msg[32];
|
||||
@@ -37,7 +38,7 @@ namespace GlobalInputs {
|
||||
dec_zoom_prev = dec_zoom;
|
||||
|
||||
// F2 — Augmentar zoom
|
||||
bool inc_zoom = JI_KeyPressed(Options::keys_gui.inc_zoom);
|
||||
bool inc_zoom = JI_KeyPressed(KeyConfig::scancode("inc_zoom"));
|
||||
if (inc_zoom && !inc_zoom_prev) {
|
||||
Screen::get()->incZoom();
|
||||
char msg[32];
|
||||
@@ -48,7 +49,7 @@ namespace GlobalInputs {
|
||||
inc_zoom_prev = inc_zoom;
|
||||
|
||||
// F3 — Toggle pantalla completa
|
||||
bool fullscreen = JI_KeyPressed(Options::keys_gui.fullscreen);
|
||||
bool fullscreen = JI_KeyPressed(KeyConfig::scancode("fullscreen"));
|
||||
if (fullscreen && !fullscreen_prev) {
|
||||
Screen::get()->toggleFullscreen();
|
||||
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
|
||||
@@ -57,7 +58,7 @@ namespace GlobalInputs {
|
||||
fullscreen_prev = fullscreen;
|
||||
|
||||
// F4 — Toggle shaders
|
||||
bool shader = JI_KeyPressed(Options::keys_gui.toggle_shader);
|
||||
bool shader = JI_KeyPressed(KeyConfig::scancode("toggle_shader"));
|
||||
if (shader && !shader_prev) {
|
||||
Screen::get()->toggleShaders();
|
||||
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
|
||||
@@ -66,7 +67,7 @@ namespace GlobalInputs {
|
||||
shader_prev = shader;
|
||||
|
||||
// F5 — Toggle aspect ratio 4:3
|
||||
bool aspect = JI_KeyPressed(Options::keys_gui.toggle_aspect_ratio);
|
||||
bool aspect = JI_KeyPressed(KeyConfig::scancode("toggle_aspect_ratio"));
|
||||
if (aspect && !aspect_prev) {
|
||||
Screen::get()->toggleAspectRatio();
|
||||
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
||||
@@ -75,47 +76,52 @@ namespace GlobalInputs {
|
||||
aspect_prev = aspect;
|
||||
|
||||
// F6 — Toggle supersampling
|
||||
bool ss = JI_KeyPressed(Options::keys_gui.toggle_supersampling);
|
||||
bool ss = JI_KeyPressed(KeyConfig::scancode("toggle_supersampling"));
|
||||
if (ss && !ss_prev) {
|
||||
Screen::get()->toggleSupersampling();
|
||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||
if (Screen::get()->toggleSupersampling()) {
|
||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||
}
|
||||
}
|
||||
if (ss) consumed = true;
|
||||
ss_prev = ss;
|
||||
|
||||
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
|
||||
bool next_shader = JI_KeyPressed(Options::keys_gui.next_shader);
|
||||
bool next_shader = JI_KeyPressed(KeyConfig::scancode("next_shader"));
|
||||
if (next_shader && !next_shader_prev) {
|
||||
Screen::get()->nextShaderType();
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
|
||||
Overlay::showNotification(msg);
|
||||
if (Screen::get()->nextShaderType()) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_shader) consumed = true;
|
||||
next_shader_prev = next_shader;
|
||||
|
||||
// F8 — Pròxim preset del shader actiu
|
||||
bool next_preset = JI_KeyPressed(Options::keys_gui.next_shader_preset);
|
||||
bool next_preset = JI_KeyPressed(KeyConfig::scancode("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 (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"));
|
||||
// F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
|
||||
bool texture_filter = JI_KeyPressed(KeyConfig::scancode("cycle_texture_filter"));
|
||||
if (texture_filter && !texture_filter_prev) {
|
||||
Screen::get()->cycleTextureFilter(+1);
|
||||
Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||
? Locale::get("notifications.filter_linear")
|
||||
: Locale::get("notifications.filter_nearest"));
|
||||
}
|
||||
if (stretch_filter) consumed = true;
|
||||
stretch_filter_prev = stretch_filter;
|
||||
if (texture_filter) consumed = true;
|
||||
texture_filter_prev = texture_filter;
|
||||
|
||||
// F10 — Toggle render info (FPS, driver, shader)
|
||||
bool render_info = JI_KeyPressed(Options::keys_gui.toggle_render_info);
|
||||
bool render_info = JI_KeyPressed(KeyConfig::scancode("toggle_render_info"));
|
||||
if (render_info && !render_info_prev) {
|
||||
Overlay::toggleRenderInfo();
|
||||
}
|
||||
|
||||
182
source/core/input/key_config.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "core/input/key_config.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace KeyConfig {
|
||||
|
||||
namespace {
|
||||
std::vector<KeyEntry> entries_;
|
||||
std::unordered_map<std::string, size_t> index_;
|
||||
std::string overrides_path_;
|
||||
|
||||
auto findIndex(const std::string& id) -> size_t {
|
||||
auto it = index_.find(id);
|
||||
if (it == index_.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_[entry.id] = entries_.size();
|
||||
entries_.push_back(std::move(entry));
|
||||
}
|
||||
std::cout << "KeyConfig: " << 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;
|
||||
}
|
||||
entries_[idx].scancode = sc;
|
||||
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) {
|
||||
entries_.clear();
|
||||
index_.clear();
|
||||
overrides_path_ = user_overrides_disk_path;
|
||||
|
||||
loadDefaults(defaults_resource_path);
|
||||
if (!overrides_path_.empty()) {
|
||||
applyOverrides(overrides_path_);
|
||||
}
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
entries_.clear();
|
||||
index_.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 entries_[idx].scancode;
|
||||
}
|
||||
|
||||
auto scancodePtr(const std::string& id) -> SDL_Scancode* {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return nullptr;
|
||||
return &entries_[idx].scancode;
|
||||
}
|
||||
|
||||
void setScancode(const std::string& id, SDL_Scancode sc) {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return;
|
||||
entries_[idx].scancode = sc;
|
||||
const char* name = SDL_GetScancodeName(sc);
|
||||
entries_[idx].code = (name != nullptr) ? name : "";
|
||||
}
|
||||
|
||||
auto isGuiKey(SDL_Scancode sc) -> bool {
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) return false;
|
||||
for (const auto& e : entries_) {
|
||||
if (e.scancode == sc) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto entries() -> const std::vector<KeyEntry>& {
|
||||
return 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 : 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
source/core/input/key_config.hpp
Normal 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
|
||||
@@ -1,12 +0,0 @@
|
||||
// Aquest fitxer existeix per a albergar la implementació completa de
|
||||
// stb_vorbis en una única unitat de compilació. Totes les funcions JA_*
|
||||
// viuen `inline` a jail_audio.hpp (header-only, inspirat en el motor de
|
||||
// jaildoctors_dilemma). Sense aquest stub tindríem múltiples definicions
|
||||
// de les funcions de stb_vorbis si més d'un .cpp inclou el header.
|
||||
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
// clang-format on
|
||||
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "core/jail/jfile.hpp"
|
||||
#include "core/system/fiber.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
||||
@@ -18,20 +19,23 @@
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
JD8_Surface screen = NULL;
|
||||
JD8_Palette main_palette = NULL;
|
||||
Uint32* pixel_data = NULL;
|
||||
JD8_Surface screen = nullptr;
|
||||
JD8_Palette main_palette = nullptr;
|
||||
Uint32* pixel_data = nullptr;
|
||||
|
||||
void JD8_Init() {
|
||||
screen = (JD8_Surface)calloc(1, 64000);
|
||||
main_palette = (JD8_Palette)calloc(1, 768);
|
||||
pixel_data = (Uint32*)calloc(1, 320 * 200 * 4);
|
||||
screen = new Uint8[64000]{};
|
||||
main_palette = new Color[256]{};
|
||||
pixel_data = new Uint32[320 * 200]{};
|
||||
}
|
||||
|
||||
void JD8_Quit() {
|
||||
if (screen != NULL) free(screen);
|
||||
if (main_palette != NULL) free(main_palette);
|
||||
if (pixel_data != NULL) free(pixel_data);
|
||||
delete[] screen;
|
||||
delete[] main_palette;
|
||||
delete[] pixel_data;
|
||||
screen = nullptr;
|
||||
main_palette = nullptr;
|
||||
pixel_data = nullptr;
|
||||
}
|
||||
|
||||
void JD8_ClearScreen(Uint8 color) {
|
||||
@@ -39,37 +43,71 @@ void JD8_ClearScreen(Uint8 color) {
|
||||
}
|
||||
|
||||
JD8_Surface JD8_NewSurface() {
|
||||
JD8_Surface surface = (JD8_Surface)malloc(64000);
|
||||
memset(surface, 0, 64000);
|
||||
return surface;
|
||||
return new Uint8[64000]{};
|
||||
}
|
||||
|
||||
// Helper intern: deriva el basename d'una ruta per a buscar al Cache.
|
||||
static std::string jd8_basename(const char* file) {
|
||||
std::string s = file;
|
||||
auto pos = s.find_last_of("/\\");
|
||||
return pos == std::string::npos ? s : s.substr(pos + 1);
|
||||
}
|
||||
|
||||
JD8_Surface JD8_LoadSurface(const char* file) {
|
||||
auto buffer = file_readfile(file);
|
||||
// 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(jd8_basename(file));
|
||||
JD8_Surface image = JD8_NewSurface();
|
||||
memcpy(image, cached.data(), 64000);
|
||||
return image;
|
||||
} catch (const std::exception&) {
|
||||
// No està al cache (asset no llistat al manifest). Fallback.
|
||||
}
|
||||
}
|
||||
|
||||
auto buffer = ResourceHelper::loadFile(file);
|
||||
unsigned short w, h;
|
||||
Uint8* pixels = LoadGif(reinterpret_cast<unsigned char*>(buffer.data()), &w, &h);
|
||||
|
||||
if (pixels == NULL) {
|
||||
Uint8* pixels = LoadGif(buffer.data(), &w, &h);
|
||||
if (pixels == nullptr) {
|
||||
printf("Unable to load bitmap: %s\n", SDL_GetError());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
JD8_Surface image = JD8_NewSurface();
|
||||
memcpy(image, pixels, 64000);
|
||||
|
||||
free(pixels);
|
||||
return image;
|
||||
}
|
||||
|
||||
JD8_Palette JD8_LoadPalette(const char* file) {
|
||||
auto buffer = file_readfile(file);
|
||||
return (JD8_Palette)LoadPalette(reinterpret_cast<unsigned char*>(buffer.data()));
|
||||
// Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
|
||||
// — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
|
||||
// l'ownership a `JD8_SetScreenPalette`).
|
||||
JD8_Palette palette = new Color[256];
|
||||
|
||||
if (Resource::Cache::get() != nullptr) {
|
||||
try {
|
||||
const auto& cached = Resource::Cache::get()->getPaletteBytes(jd8_basename(file));
|
||||
memcpy(palette, cached.data(), 768);
|
||||
return palette;
|
||||
} catch (const std::exception&) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
void JD8_SetScreenPalette(JD8_Palette palette) {
|
||||
if (main_palette == palette) return;
|
||||
if (main_palette != NULL) free(main_palette);
|
||||
delete[] main_palette;
|
||||
main_palette = palette;
|
||||
}
|
||||
|
||||
@@ -79,6 +117,23 @@ void JD8_FillSquare(int ini, int height, Uint8 color) {
|
||||
memset(&screen[offset], color, size);
|
||||
}
|
||||
|
||||
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(JD8_Surface surface) {
|
||||
memcpy(screen, surface, 64000);
|
||||
}
|
||||
@@ -151,16 +206,16 @@ void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int
|
||||
}
|
||||
|
||||
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 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);
|
||||
pixel_data[x + (y * 320)] = color;
|
||||
}
|
||||
}
|
||||
// Cedeix el control al Director. Quan Director::run() ens torne a fer
|
||||
// resume(), continuarem just ací i el joc continuarà amb la següent
|
||||
// iteració del seu loop sense bloquejos de mutex/cv.
|
||||
GameFiber::yield();
|
||||
}
|
||||
|
||||
Uint32* JD8_GetFramebuffer() {
|
||||
@@ -168,7 +223,7 @@ Uint32* JD8_GetFramebuffer() {
|
||||
}
|
||||
|
||||
void JD8_FreeSurface(JD8_Surface surface) {
|
||||
free(surface);
|
||||
delete[] surface;
|
||||
}
|
||||
|
||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) {
|
||||
@@ -190,84 +245,73 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
|
||||
// el caller decideix quan fer Flip.
|
||||
namespace {
|
||||
|
||||
enum FadeType {
|
||||
FADE_NONE = 0,
|
||||
FADE_OUT,
|
||||
FADE_TO_PAL,
|
||||
};
|
||||
enum class FadeType {
|
||||
None = 0,
|
||||
Out,
|
||||
ToPal,
|
||||
};
|
||||
|
||||
constexpr int FADE_STEPS = 32;
|
||||
constexpr int FADE_STEPS = 32;
|
||||
|
||||
FadeType fade_type = FADE_NONE;
|
||||
Color fade_target[256];
|
||||
int fade_step = 0;
|
||||
FadeType fade_type = FadeType::None;
|
||||
Color fade_target[256];
|
||||
int fade_step = 0;
|
||||
|
||||
void apply_fade_step() {
|
||||
if (fade_type == FADE_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 == FADE_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;
|
||||
void apply_fade_step() {
|
||||
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::ToPal) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void JD8_FadeStartOut() {
|
||||
fade_type = FADE_OUT;
|
||||
fade_type = FadeType::Out;
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
void JD8_FadeStartToPal(JD8_Palette pal) {
|
||||
fade_type = FADE_TO_PAL;
|
||||
fade_type = FadeType::ToPal;
|
||||
memcpy(fade_target, pal, sizeof(Color) * 256);
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
bool JD8_FadeIsActive() {
|
||||
return fade_type != FADE_NONE;
|
||||
return fade_type != FadeType::None;
|
||||
}
|
||||
|
||||
bool JD8_FadeTickStep() {
|
||||
if (fade_type == FADE_NONE) return true;
|
||||
if (fade_type == FadeType::None) return true;
|
||||
|
||||
apply_fade_step();
|
||||
fade_step++;
|
||||
|
||||
if (fade_step >= FADE_STEPS) {
|
||||
fade_type = FADE_NONE;
|
||||
fade_type = FadeType::None;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void JD8_FadeOut() {
|
||||
JD8_FadeStartOut();
|
||||
while (true) {
|
||||
const bool done = JD8_FadeTickStep();
|
||||
JD8_Flip();
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_FadeToPal(JD8_Palette pal) {
|
||||
JD8_FadeStartToPal(pal);
|
||||
while (true) {
|
||||
const bool done = JD8_FadeTickStep();
|
||||
JD8_Flip();
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
// 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`).
|
||||
|
||||
@@ -7,8 +7,8 @@ struct Color {
|
||||
Uint8 b;
|
||||
};
|
||||
|
||||
typedef Uint8* JD8_Surface;
|
||||
typedef Color* JD8_Palette;
|
||||
using JD8_Surface = Uint8*;
|
||||
using JD8_Palette = Color*;
|
||||
|
||||
void JD8_Init();
|
||||
|
||||
@@ -26,6 +26,10 @@ void JD8_SetScreenPalette(JD8_Palette palette);
|
||||
|
||||
void JD8_FillSquare(int ini, int height, Uint8 color);
|
||||
|
||||
// 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_FillRect(int x, int y, int w, int h, Uint8 color);
|
||||
|
||||
void JD8_Blit(JD8_Surface surface);
|
||||
|
||||
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh);
|
||||
@@ -40,10 +44,9 @@ void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8
|
||||
|
||||
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey);
|
||||
|
||||
// Converteix la pantalla indexada a ARGB i cedeix el control al Director
|
||||
// (GameFiber::yield). El Director llegirà el framebuffer convertit via
|
||||
// JD8_GetFramebuffer() i tornarà a cridar Fiber::resume() quan toque el
|
||||
// pròxim frame.
|
||||
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
|
||||
// funció al final de cada tick i després llegeix el framebuffer via
|
||||
// JD8_GetFramebuffer() per presentar-lo.
|
||||
void JD8_Flip();
|
||||
|
||||
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
|
||||
@@ -58,16 +61,13 @@ void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
|
||||
|
||||
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
||||
|
||||
// Fades legacy bloquejants (shim damunt la màquina d'estats de sota).
|
||||
void JD8_FadeOut();
|
||||
void JD8_FadeToPal(JD8_Palette pal);
|
||||
|
||||
// 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 JD8_FadeStartOut();
|
||||
void JD8_FadeStartToPal(JD8_Palette pal);
|
||||
bool JD8_FadeTickStep();
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
#include "core/jail/jfile.hpp"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -19,160 +14,82 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* DEFAULT_FILENAME = "data.jf2";
|
||||
constexpr const char* DEFAULT_FOLDER = "data/";
|
||||
struct keyvalue {
|
||||
std::string key;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct file_entry {
|
||||
std::string path;
|
||||
uint32_t size;
|
||||
uint32_t offset;
|
||||
};
|
||||
std::vector<keyvalue> config;
|
||||
std::string resource_folder;
|
||||
std::string config_folder;
|
||||
|
||||
struct keyvalue {
|
||||
std::string key;
|
||||
std::string value;
|
||||
};
|
||||
void load_config_values() {
|
||||
config.clear();
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ifstream fi(config_file);
|
||||
if (!fi.is_open()) return;
|
||||
|
||||
std::vector<file_entry> toc;
|
||||
std::vector<keyvalue> config;
|
||||
|
||||
std::string resource_filename;
|
||||
std::string resource_folder;
|
||||
std::string config_folder;
|
||||
int file_source = SOURCE_FILE;
|
||||
|
||||
bool dictionary_loaded() {
|
||||
if (resource_filename.empty()) resource_filename = 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(reinterpret_cast<char*>(&num_files), 4);
|
||||
fi.read(reinterpret_cast<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(reinterpret_cast<char*>(&file_offset), 4);
|
||||
fi.read(reinterpret_cast<char*>(&file_size), 4);
|
||||
uint8_t path_size;
|
||||
fi.read(reinterpret_cast<char*>(&path_size), 1);
|
||||
char file_name[256];
|
||||
fi.read(file_name, path_size);
|
||||
file_name[path_size] = 0;
|
||||
toc.push_back({std::string(file_name), file_size, file_offset});
|
||||
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)});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string filename_with_folder(const char* filename) {
|
||||
return resource_folder + filename;
|
||||
}
|
||||
|
||||
void load_config_values() {
|
||||
config.clear();
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ifstream fi(config_file);
|
||||
if (!fi.is_open()) 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)});
|
||||
void save_config_values() {
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void save_config_values() {
|
||||
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 file_setresourcefilename(const char* str) {
|
||||
resource_filename = str;
|
||||
}
|
||||
|
||||
void file_setresourcefolder(const char* str) {
|
||||
resource_folder = str;
|
||||
}
|
||||
|
||||
void file_setsource(const int src) {
|
||||
file_source = src % 2;
|
||||
if (src == SOURCE_FOLDER && resource_folder.empty()) file_setresourcefolder(DEFAULT_FOLDER);
|
||||
}
|
||||
|
||||
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary) {
|
||||
if (file_source == SOURCE_FILE && toc.empty()) {
|
||||
if (!dictionary_loaded()) file_setsource(SOURCE_FOLDER);
|
||||
}
|
||||
|
||||
FILE* f = nullptr;
|
||||
|
||||
if (file_source == SOURCE_FILE) {
|
||||
const std::string name(resourcename);
|
||||
size_t count = 0;
|
||||
for (; count < toc.size(); ++count) {
|
||||
if (toc[count].path == name) break;
|
||||
}
|
||||
if (count == toc.size()) {
|
||||
perror("El recurs no s'ha trobat en l'arxiu de recursos");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
filesize = static_cast<int>(toc[count].size);
|
||||
|
||||
f = fopen(resource_filename.c_str(), binary ? "rb" : "r");
|
||||
if (!f) {
|
||||
perror("No s'ha pogut obrir l'arxiu de recursos");
|
||||
exit(1);
|
||||
}
|
||||
fseek(f, toc[count].offset, SEEK_SET);
|
||||
} else {
|
||||
const std::string full = filename_with_folder(resourcename);
|
||||
f = fopen(full.c_str(), binary ? "rb" : "r");
|
||||
if (!f) return nullptr;
|
||||
fseek(f, 0, SEEK_END);
|
||||
filesize = static_cast<int>(ftell(f));
|
||||
fseek(f, 0, SEEK_SET);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
std::vector<char> file_readfile(const char* resourcename) {
|
||||
int filesize = 0;
|
||||
FILE* f = file_getfilepointer(resourcename, filesize, true);
|
||||
if (!f) return {};
|
||||
std::vector<char> buffer(filesize);
|
||||
fread(buffer.data(), filesize, 1, f);
|
||||
fclose(f);
|
||||
return buffer;
|
||||
const char* file_getresourcefolder() {
|
||||
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 file_setconfigfolder(const char* foldername) {
|
||||
#ifdef _WIN32
|
||||
config_folder = std::string(getenv("APPDATA")) + "/" + foldername;
|
||||
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_dir;
|
||||
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->pw_dir;
|
||||
const char* homedir = (pw && pw->pw_dir && pw->pw_dir[0]) ? pw->pw_dir : nullptr;
|
||||
if (!homedir || !homedir[0]) homedir = getenv("HOME");
|
||||
if (!homedir || !homedir[0]) homedir = "/tmp";
|
||||
config_folder = std::string(homedir) + "/.config/" + foldername;
|
||||
#endif
|
||||
|
||||
std::filesystem::create_directories(config_folder);
|
||||
if (config_folder.empty()) {
|
||||
config_folder = "/tmp/jailgames_config";
|
||||
}
|
||||
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.
|
||||
}
|
||||
|
||||
const char* file_getconfigfolder() {
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
#pragma once
|
||||
#include <stdio.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#define SOURCE_FILE 0
|
||||
#define SOURCE_FOLDER 1
|
||||
|
||||
void file_setconfigfolder(const char* foldername);
|
||||
const char* file_getconfigfolder();
|
||||
|
||||
void file_setresourcefilename(const char* str);
|
||||
void file_setresourcefolder(const char* str);
|
||||
void file_setsource(const int src);
|
||||
|
||||
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary = false);
|
||||
|
||||
// Llig tot el contingut d'un recurs (fitxer solt o entrada del .jrf).
|
||||
// Retorna un vector buit si el recurs no existeix. El vector es destrueix
|
||||
// automàticament en eixir d'àmbit — no fa falta cap free() manual. Mida =
|
||||
// bytes llegits (el buffer no està null-terminated).
|
||||
std::vector<char> file_readfile(const char* resourcename);
|
||||
const char* file_getresourcefolder();
|
||||
|
||||
const char* file_getconfigvalue(const char* key);
|
||||
void file_setconfigvalue(const char* key, const char* value);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
#include "core/system/fiber.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool quitting = false;
|
||||
Uint32 update_ticks = 0;
|
||||
Uint32 update_time = 0;
|
||||
Uint32 cycle_counter = 0;
|
||||
Uint32 last_delta_time = 0;
|
||||
bool quitting = false;
|
||||
Uint32 update_ticks = 0;
|
||||
Uint32 update_time = 0;
|
||||
Uint32 cycle_counter = 0;
|
||||
Uint32 last_delta_time = 0;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -41,12 +39,9 @@ bool JG_ShouldUpdate() {
|
||||
cycle_counter++;
|
||||
return true;
|
||||
}
|
||||
// Encara no toca update: cedim el control al Director per a que puga
|
||||
// processar events, animar l'overlay i mantindre l'àudio viu. Sense
|
||||
// aquest yield, els spin-waits típics de les cinemàtiques
|
||||
// (`while (!JG_ShouldUpdate()) { JI_Update(); ... }`) congelarien
|
||||
// tot el main loop — el fiber no cediria mai.
|
||||
GameFiber::yield();
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,35 +6,35 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
|
||||
const bool* keystates = nullptr;
|
||||
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
|
||||
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};
|
||||
// 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;
|
||||
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;
|
||||
// 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;
|
||||
// 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;
|
||||
bool input_blocked = false;
|
||||
|
||||
Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
|
||||
Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
|
||||
|
||||
Uint8 scancode_to_ascii(Uint8 scancode) {
|
||||
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
|
||||
Uint8 scancode_to_ascii(Uint8 scancode) {
|
||||
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/jail/jfile.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace Locale {
|
||||
@@ -27,12 +27,12 @@ namespace Locale {
|
||||
}
|
||||
|
||||
bool load(const char* filename) {
|
||||
auto buffer = file_readfile(filename);
|
||||
auto buffer = ResourceHelper::loadFile(filename);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Locale: unable to load " << filename << '\n';
|
||||
return false;
|
||||
}
|
||||
std::string content(buffer.data(), buffer.size());
|
||||
std::string content(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#include "core/rendering/menu.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/overlay.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "core/system/director.hpp"
|
||||
#include "game/defines.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "utils/easing.hpp"
|
||||
#include "version.h"
|
||||
|
||||
namespace Menu {
|
||||
|
||||
@@ -35,38 +40,60 @@ namespace Menu {
|
||||
static constexpr int ITEM_SPACING = 11;
|
||||
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 SUBTITLE_H = 8 + 3; // línia de subtítol + gap
|
||||
|
||||
// --- 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 ---
|
||||
enum class ItemKind { Toggle,
|
||||
Cycle,
|
||||
IntRange,
|
||||
Submenu,
|
||||
KeyBind };
|
||||
KeyBind,
|
||||
Action };
|
||||
|
||||
struct Item {
|
||||
const char* label;
|
||||
ItemKind kind;
|
||||
std::function<std::string()> getValue; // opcional
|
||||
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
|
||||
std::function<void()> enter; // per Submenu
|
||||
std::function<void()> enter; // per Submenu i Action
|
||||
SDL_Scancode* scancode{nullptr}; // per KeyBind
|
||||
std::function<bool()> visible; // nullptr ⇒ sempre visible
|
||||
};
|
||||
|
||||
struct Page {
|
||||
const char* title;
|
||||
std::vector<Item> items;
|
||||
int cursor{0};
|
||||
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
|
||||
};
|
||||
|
||||
static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
|
||||
|
||||
// Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
|
||||
// Si cap és visible retorna `from`.
|
||||
static int nextVisibleCursor(const Page& p, int from, int dir) {
|
||||
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 ---
|
||||
static std::vector<Page> stack_;
|
||||
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 float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible)
|
||||
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 ---
|
||||
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
|
||||
@@ -102,6 +129,7 @@ namespace Menu {
|
||||
static Page buildAudio();
|
||||
static Page buildControls();
|
||||
static Page buildGame();
|
||||
static Page buildSystem();
|
||||
|
||||
static Page buildRoot() {
|
||||
Page p{Locale::get("menu.titles.root"), {}, 0};
|
||||
@@ -109,50 +137,77 @@ namespace Menu {
|
||||
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.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;
|
||||
}
|
||||
|
||||
static Page buildVideo() {
|
||||
Page p{Locale::get("menu.titles.video"), {}, 0};
|
||||
|
||||
// Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
|
||||
#ifndef __EMSCRIPTEN__
|
||||
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
||||
return std::string(buf); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->decZoom();
|
||||
else if (dir > 0) Screen::get()->incZoom(); }, nullptr});
|
||||
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::IntRange, [] {
|
||||
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});
|
||||
|
||||
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
|
||||
#ifndef __EMSCRIPTEN__
|
||||
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->prevShaderType();
|
||||
else Screen::get()->nextShaderType(); }, nullptr});
|
||||
else Screen::get()->nextShaderType(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->prevPreset();
|
||||
else Screen::get()->nextPreset(); }, nullptr});
|
||||
else Screen::get()->nextPreset(); }, 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.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
const char* name = Screen::get()->getActiveShaderName();
|
||||
return name && std::string(name) == "POSTFX"; }});
|
||||
#endif
|
||||
|
||||
// Informació de render
|
||||
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
|
||||
switch (Options::render_info.position) {
|
||||
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::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;
|
||||
}
|
||||
@@ -181,7 +236,7 @@ namespace Menu {
|
||||
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down});
|
||||
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left});
|
||||
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.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_gui.menu_toggle});
|
||||
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -194,17 +249,17 @@ namespace Menu {
|
||||
|
||||
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.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music_enabled); }, [](int) {
|
||||
Options::audio.music_enabled = !Options::audio.music_enabled;
|
||||
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::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::IntRange, [] { 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) {
|
||||
Options::audio.sound_enabled = !Options::audio.sound_enabled;
|
||||
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::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::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
|
||||
|
||||
return p;
|
||||
}
|
||||
@@ -216,6 +271,29 @@ namespace Menu {
|
||||
|
||||
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 Page buildSystem() {
|
||||
Page p{Locale::get("menu.titles.system"), {}, 0};
|
||||
p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] {
|
||||
if (Director::get()) Director::get()->requestRestart();
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -262,11 +340,17 @@ namespace Menu {
|
||||
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*.
|
||||
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
|
||||
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
|
||||
static int boxHeight(const Page& page) {
|
||||
int n = static_cast<int>(page.items.size());
|
||||
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING;
|
||||
return HEADER_H + body + BOTTOM_PAD;
|
||||
int n = 0;
|
||||
for (const auto& it : page.items) {
|
||||
if (isVisible(it)) ++n;
|
||||
}
|
||||
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 ---
|
||||
@@ -275,34 +359,56 @@ namespace Menu {
|
||||
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||
stack_.clear();
|
||||
open_anim_ = 0.0F;
|
||||
closing_ = false;
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
font_.reset();
|
||||
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 {
|
||||
return !stack_.empty() && !closing_;
|
||||
}
|
||||
|
||||
// "Visible": encara hi ha caixa per pintar (incloent close animation).
|
||||
auto isVisible() -> bool {
|
||||
return !stack_.empty();
|
||||
}
|
||||
|
||||
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()) {
|
||||
close();
|
||||
} else {
|
||||
stack_.push_back(buildRoot());
|
||||
open_anim_ = 0.0F;
|
||||
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() {
|
||||
stack_.clear();
|
||||
open_anim_ = 0.0F;
|
||||
if (stack_.empty() || closing_) return;
|
||||
closing_ = true;
|
||||
capturing_ = nullptr;
|
||||
transition_active_ = false;
|
||||
transition_progress_ = 1.0F;
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
auto isCapturing() -> bool {
|
||||
@@ -333,13 +439,17 @@ namespace Menu {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const int n = static_cast<int>(page.items.size());
|
||||
// Si el cursor està sobre un ítem ocultat (p. ex. una acció anterior el va ocultar),
|
||||
// reubica'l al pròxim visible abans de processar l'entrada.
|
||||
if (!isVisible(page.items[page.cursor])) {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
}
|
||||
switch (sc) {
|
||||
case SDL_SCANCODE_UP:
|
||||
page.cursor = (page.cursor - 1 + n) % n;
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, -1);
|
||||
break;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
page.cursor = (page.cursor + 1) % n;
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
break;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
||||
@@ -355,7 +465,8 @@ namespace Menu {
|
||||
break;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
if (page.items[page.cursor].kind == ItemKind::Submenu) {
|
||||
if (page.items[page.cursor].kind == ItemKind::Submenu ||
|
||||
page.items[page.cursor].kind == ItemKind::Action) {
|
||||
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
|
||||
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
|
||||
capturing_ = page.items[page.cursor].scancode;
|
||||
@@ -372,6 +483,15 @@ namespace Menu {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Després de qualsevol acció, si el cursor quedara sobre un ítem ocult
|
||||
// (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual,
|
||||
// edge case defensiu), salta al següent visible.
|
||||
if (!stack_.empty()) {
|
||||
Page& top = stack_.back();
|
||||
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
|
||||
top.cursor = nextVisibleCursor(top, top.cursor, +1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
|
||||
@@ -395,25 +515,49 @@ 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;
|
||||
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)
|
||||
int visible_count = 0;
|
||||
for (const auto& it : page.items)
|
||||
if (isVisible(it)) ++visible_count;
|
||||
if (visible_count == 0) {
|
||||
const char* empty_text = Locale::get("menu.values.empty");
|
||||
int ew = font_->width(empty_text);
|
||||
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
|
||||
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
|
||||
for (size_t i = 0; i < page.items.size(); i++) {
|
||||
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
|
||||
bool selected = (static_cast<int>(i) == page.cursor);
|
||||
const Item& item = page.items[i];
|
||||
if (!isVisible(item)) continue;
|
||||
int y = items_y + y_slot * ITEM_SPACING;
|
||||
++y_slot;
|
||||
bool selected = (static_cast<int>(i) == page.cursor);
|
||||
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
|
||||
// Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
|
||||
if (item.kind == ItemKind::Action) {
|
||||
int lw = font_->width(item.label);
|
||||
int lx = box_x + (BOX_W - lw) / 2 + x_offset;
|
||||
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, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -438,13 +582,23 @@ namespace Menu {
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!isOpen() || !font_ || !pixel_data) return;
|
||||
if (!isVisible() || !font_ || !pixel_data) return;
|
||||
|
||||
// Delta time
|
||||
Uint32 now = SDL_GetTicks();
|
||||
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
|
||||
last_ticks_ = now;
|
||||
if (open_anim_ < 1.0F) {
|
||||
if (closing_) {
|
||||
open_anim_ -= CLOSE_SPEED * dt;
|
||||
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;
|
||||
if (open_anim_ > 1.0F) open_anim_ = 1.0F;
|
||||
}
|
||||
@@ -461,14 +615,30 @@ namespace Menu {
|
||||
const Page& page = stack_.back();
|
||||
const int current_h = boxHeight(page);
|
||||
|
||||
// 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;
|
||||
if (t > 1.0F) t = 1.0F;
|
||||
animated_h_ += diff * t;
|
||||
}
|
||||
}
|
||||
|
||||
float eased = Easing::outQuad(open_anim_);
|
||||
|
||||
// Calcula alçada (amb transició si escau)
|
||||
int target_h = current_h;
|
||||
int target_h = static_cast<int>(animated_h_);
|
||||
if (transition_active_) {
|
||||
int outgoing_h = boxHeight(transition_outgoing_);
|
||||
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
|
||||
|
||||
@@ -6,11 +6,15 @@ namespace Menu {
|
||||
void init();
|
||||
void destroy();
|
||||
|
||||
// "Actiu": el menú accepta input. Fals durant l'animació de tancament.
|
||||
[[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 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);
|
||||
|
||||
// Gestió d'input — cridat des del Director en KEY_DOWN
|
||||
|
||||
@@ -361,8 +361,8 @@ namespace Overlay {
|
||||
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()) {
|
||||
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
|
||||
if (Menu::isVisible()) {
|
||||
Menu::render(pixel_data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,68 @@
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/overlay.hpp"
|
||||
#ifndef NO_SHADERS
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
||||
#endif
|
||||
#include "game/defines.hpp"
|
||||
#include "game/options.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() {
|
||||
instance_ = new Screen();
|
||||
instance_ = std::unique_ptr<Screen>(new Screen());
|
||||
}
|
||||
|
||||
void Screen::destroy() {
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
instance_.reset();
|
||||
}
|
||||
|
||||
auto Screen::get() -> Screen* {
|
||||
return instance_;
|
||||
return instance_.get();
|
||||
}
|
||||
|
||||
Screen::Screen() {
|
||||
@@ -35,40 +79,70 @@ Screen::Screen() {
|
||||
if (zoom_ < 1) zoom_ = 1;
|
||||
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
|
||||
|
||||
// Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i
|
||||
// ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada
|
||||
// canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi
|
||||
// de monitor), baixem al màxim suportat.
|
||||
if (Options::video.internal_resolution < 1) Options::video.internal_resolution = 1;
|
||||
if (Options::video.internal_resolution > max_zoom_) Options::video.internal_resolution = max_zoom_;
|
||||
|
||||
int w = GAME_WIDTH * 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);
|
||||
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);
|
||||
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
|
||||
applyFallbackPresentation();
|
||||
|
||||
// Inicialitza backend GPU si l'acceleració està activada
|
||||
initShaders();
|
||||
|
||||
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() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
g_screen_instance = nullptr;
|
||||
#endif
|
||||
|
||||
// Guarda opcions abans de destruir
|
||||
Options::window.zoom = zoom_;
|
||||
Options::window.fullscreen = fullscreen_;
|
||||
|
||||
// Destrueix el backend GPU
|
||||
// Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
|
||||
if (shader_backend_) {
|
||||
#ifndef NO_SHADERS
|
||||
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
|
||||
if (gpu) gpu->destroy();
|
||||
#endif
|
||||
shader_backend_.reset();
|
||||
}
|
||||
|
||||
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
|
||||
if (texture_) SDL_DestroyTexture(texture_);
|
||||
if (renderer_) SDL_DestroyRenderer(renderer_);
|
||||
if (window_) SDL_DestroyWindow(window_);
|
||||
}
|
||||
|
||||
void Screen::initShaders() {
|
||||
#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>();
|
||||
@@ -88,17 +162,18 @@ void Screen::initShaders() {
|
||||
std::cout << "GPU driver: " << gpu_driver_ << '\n';
|
||||
|
||||
// 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_->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_->setLinearUpscale(Options::video.linear_upscale);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
|
||||
|
||||
if (Options::video.supersampling) {
|
||||
shader_backend_->setOversample(3);
|
||||
}
|
||||
|
||||
shader_backend_->setInternalResolution(Options::video.internal_resolution);
|
||||
|
||||
// Resol el shader actiu des del config
|
||||
if (Options::video.current_shader == "crtpi") {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
@@ -122,6 +197,7 @@ void Screen::initShaders() {
|
||||
|
||||
applyCurrentPostFXPreset();
|
||||
applyCurrentCrtPiPreset();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Screen::present(Uint32* pixel_data) {
|
||||
@@ -135,14 +211,62 @@ void Screen::present(Uint32* pixel_data) {
|
||||
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
|
||||
shader_backend_->render();
|
||||
} 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{};
|
||||
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_->render();
|
||||
if (prev_shader != Rendering::ShaderType::POSTFX) {
|
||||
shader_backend_->setActiveShader(prev_shader);
|
||||
}
|
||||
} 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));
|
||||
|
||||
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_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
||||
SDL_RenderPresent(renderer_);
|
||||
@@ -182,26 +306,40 @@ void Screen::toggleShaders() {
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::toggleSupersampling() {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
||||
auto Screen::toggleSupersampling() -> bool {
|
||||
// SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
|
||||
// i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
|
||||
// submostreig intern i no usa aquesta via).
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) return false;
|
||||
Options::video.supersampling = !Options::video.supersampling;
|
||||
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::toggleAspectRatio() {
|
||||
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
||||
} else {
|
||||
applyFallbackPresentation();
|
||||
}
|
||||
if (!fullscreen_) {
|
||||
adjustWindowSize();
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::toggleIntegerScale() {
|
||||
Options::video.integer_scale = !Options::video.integer_scale;
|
||||
void Screen::cycleScalingMode(int dir) {
|
||||
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_) {
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
shader_backend_->setScalingMode(Options::video.scaling_mode);
|
||||
} else {
|
||||
applyFallbackPresentation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,15 +350,39 @@ void Screen::toggleVSync() {
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::toggleStretchFilter() {
|
||||
Options::video.stretch_filter_linear = !Options::video.stretch_filter_linear;
|
||||
void Screen::cycleTextureFilter(int dir) {
|
||||
// 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_) {
|
||||
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear);
|
||||
shader_backend_->setTextureFilter(Options::video.texture_filter);
|
||||
} else {
|
||||
applyFallbackPresentation();
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::nextShaderType() {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
||||
void Screen::changeInternalResolution(int dir) {
|
||||
int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
|
||||
if (next < 1) next = 1;
|
||||
if (next > max_zoom_) next = max_zoom_;
|
||||
if (next == Options::video.internal_resolution) return;
|
||||
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) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
@@ -231,45 +393,50 @@ void Screen::nextShaderType() {
|
||||
Options::video.current_shader = "postfx";
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::nextPreset() {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
||||
auto Screen::nextPreset() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::postfx_presets.empty()) return;
|
||||
if (Options::postfx_presets.empty()) return false;
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
if (Options::crtpi_presets.empty()) return;
|
||||
if (Options::crtpi_presets.empty()) return false;
|
||||
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::prevShaderType() {
|
||||
auto Screen::prevShaderType() -> bool {
|
||||
// Només dues opcions — prev == next
|
||||
nextShaderType();
|
||||
return nextShaderType();
|
||||
}
|
||||
|
||||
void Screen::prevPreset() {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
||||
auto Screen::prevPreset() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::postfx_presets.empty()) return;
|
||||
if (Options::postfx_presets.empty()) return false;
|
||||
int n = static_cast<int>(Options::postfx_presets.size());
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
|
||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
if (Options::crtpi_presets.empty()) return;
|
||||
if (Options::crtpi_presets.empty()) return false;
|
||||
int n = static_cast<int>(Options::crtpi_presets.size());
|
||||
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
|
||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Screen::getCurrentPresetName() const -> const char* {
|
||||
@@ -371,6 +538,77 @@ void Screen::updateRenderInfo() {
|
||||
0b1001);
|
||||
}
|
||||
|
||||
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_) 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() {
|
||||
int w = GAME_WIDTH * zoom_;
|
||||
// Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2)
|
||||
@@ -389,3 +627,25 @@ void Screen::calculateMaxZoom() {
|
||||
if (max_zoom_ < 1) 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
|
||||
|
||||
@@ -13,6 +13,8 @@ class Screen {
|
||||
static void destroy();
|
||||
static auto get() -> Screen*;
|
||||
|
||||
~Screen(); // públic per a std::unique_ptr
|
||||
|
||||
// Presentació — rep el buffer ARGB de 320x200 de JD8
|
||||
void present(Uint32* pixel_data);
|
||||
|
||||
@@ -23,16 +25,21 @@ class Screen {
|
||||
void setZoom(int zoom);
|
||||
|
||||
// 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 toggleSupersampling();
|
||||
auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
|
||||
void toggleAspectRatio();
|
||||
void toggleIntegerScale();
|
||||
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
|
||||
void toggleVSync();
|
||||
void toggleStretchFilter();
|
||||
void nextShaderType(); // Cicla PostFX ↔ CrtPi (F7)
|
||||
void prevShaderType(); // Cicla al revés
|
||||
void nextPreset(); // Cicla presets del shader actiu (F8)
|
||||
void prevPreset(); // Cicla presets al revés
|
||||
void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
|
||||
void changeInternalResolution(int dir); // +/−1, clampat a [1, max_zoom_]
|
||||
auto nextShaderType() -> bool; // false si GPU off / shaders off
|
||||
auto prevShaderType() -> bool; // idem
|
||||
auto nextPreset() -> bool; // false si GPU off / shaders off
|
||||
auto prevPreset() -> bool; // idem
|
||||
[[nodiscard]] auto getCurrentPresetName() const -> const char*;
|
||||
void setActiveShader(Rendering::ShaderType type);
|
||||
void applyCurrentPostFXPreset();
|
||||
@@ -41,24 +48,36 @@ class Screen {
|
||||
// Getters
|
||||
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
|
||||
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
|
||||
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
|
||||
[[nodiscard]] auto getActiveShaderName() const -> const char*;
|
||||
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
|
||||
[[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:
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
void adjustWindowSize();
|
||||
void calculateMaxZoom();
|
||||
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_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)
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#include <iostream> // std::cout
|
||||
|
||||
#ifndef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/crtpi_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/downscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/postfx_vert_spv.h"
|
||||
#include "core/rendering/sdl3gpu/upscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -456,6 +456,11 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
// internal_texture_: si el multiplicador és > 1, es crea ací amb les
|
||||
// dimensions game·N × game·N. No bloqueja si falla — només deixa la
|
||||
// textura a nullptr i el pipeline ometrà la còpia.
|
||||
recreateInternalTexture();
|
||||
|
||||
// scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
|
||||
ss_factor_ = 0;
|
||||
|
||||
@@ -812,14 +817,50 @@ namespace Rendering {
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
|
||||
// ---- Upscale pass: scene_texture_ → scaled_texture_ ----
|
||||
// ---- Internal resolution NN upscale: scene_texture_ → internal_texture_ ----
|
||||
// Multiplicador enter. Si > 1, tot el pipeline downstream veu internal_texture_
|
||||
// com a "scene" (mida game·N × game·N) i els passos següents (SS, PostFX,
|
||||
// Lanczos, letterbox) operen sobre aquesta font més gran. L'objectiu: quan el
|
||||
// filtre final LINEAR estira a finestra, parteix d'una base més gran i es veu
|
||||
// menys borrós. Amb internal_res_ == 1, s'omet el pas (zero overhead).
|
||||
SDL_GPUTexture* source_texture = scene_texture_;
|
||||
int source_width = game_width_;
|
||||
int source_height = game_height_;
|
||||
if (internal_res_ > 1 && internal_texture_ != nullptr && upscale_pipeline_ != nullptr) {
|
||||
SDL_GPUColorTargetInfo internal_target = {};
|
||||
internal_target.texture = internal_texture_;
|
||||
internal_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
|
||||
internal_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
|
||||
SDL_GPURenderPass* ipass = SDL_BeginGPURenderPass(cmd, &internal_target, 1, nullptr);
|
||||
if (ipass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(ipass, upscale_pipeline_);
|
||||
SDL_GPUTextureSamplerBinding ibinding = {};
|
||||
ibinding.texture = scene_texture_;
|
||||
ibinding.sampler = sampler_; // sempre NEAREST per a la còpia de resolució interna
|
||||
SDL_BindGPUFragmentSamplers(ipass, 0, &ibinding, 1);
|
||||
SDL_DrawGPUPrimitives(ipass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(ipass);
|
||||
}
|
||||
source_texture = internal_texture_;
|
||||
source_width = game_width_ * internal_res_;
|
||||
source_height = game_height_ * internal_res_;
|
||||
}
|
||||
|
||||
// ---- Upscale pass: source_texture → scaled_texture_ ----
|
||||
// Si 4:3 actiu, l'estirament s'aplica ací directament (320x200 → W*factor × H*factor*1.2)
|
||||
// El filtre per al 4:3 és configurable (stretch_filter_linear_).
|
||||
// El filtre s'aplica sempre (texture_filter_linear_), independent de 4:3.
|
||||
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
|
||||
// Sense SS ni stretch: scene_texture_ a game_height_.
|
||||
// Amb SS o stretch: scaled_texture_ a l'alçada escalada (amb o sense 4:3).
|
||||
SDL_GPUTexture* effective_scene = scene_texture_;
|
||||
SDL_GPUTexture* effective_scene = source_texture;
|
||||
// `effective_height` reflecteix l'alçada lògica del frame (per a
|
||||
// scanlines i viewport), no la mida real de la textura. Es manté
|
||||
// a `game_height_` encara que internal_res_ > 1 — el multiplicador
|
||||
// només afecta la resolució física de la font, no l'aspect ni el
|
||||
// nombre de scanlines visibles.
|
||||
int effective_height = game_height_;
|
||||
(void)source_width; // només es fa servir com a context informatiu
|
||||
|
||||
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
|
||||
SDL_GPUColorTargetInfo upscale_target = {};
|
||||
@@ -827,15 +868,14 @@ namespace Rendering {
|
||||
upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
|
||||
upscale_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
|
||||
// Triar filtre: si 4:3 actiu, usar el filtre configurable per a l'estirament.
|
||||
// Si no, usar el filtre d'upscale normal (linear_upscale_).
|
||||
bool use_linear = stretch_4_3_ ? stretch_filter_linear_ : linear_upscale_;
|
||||
// Filtre global: s'aplica sempre (ja no depèn de 4:3).
|
||||
bool use_linear = texture_filter_linear_;
|
||||
|
||||
SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr);
|
||||
if (upass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
||||
SDL_GPUTextureSamplerBinding ubinding = {};
|
||||
ubinding.texture = scene_texture_;
|
||||
ubinding.texture = source_texture;
|
||||
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
||||
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
||||
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
||||
@@ -847,6 +887,7 @@ namespace Rendering {
|
||||
// Sense SS: el viewport s'encarrega de l'estirament geomètric
|
||||
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
|
||||
}
|
||||
(void)source_height;
|
||||
|
||||
// ---- Acquire swapchain texture ----
|
||||
SDL_GPUTexture* swapchain = nullptr;
|
||||
@@ -872,16 +913,37 @@ namespace Rendering {
|
||||
float vy = 0.0F;
|
||||
float vw = 0.0F;
|
||||
float vh = 0.0F;
|
||||
if (integer_scale_) {
|
||||
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(logical_w), static_cast<int>(sh) / static_cast<int>(logical_h)));
|
||||
vw = logical_w * static_cast<float>(SCALE);
|
||||
vh = logical_h * static_cast<float>(SCALE);
|
||||
} else {
|
||||
const float SCALE = std::min(
|
||||
static_cast<float>(sw) / logical_w,
|
||||
static_cast<float>(sh) / logical_h);
|
||||
vw = logical_w * SCALE;
|
||||
vh = logical_h * SCALE;
|
||||
switch (scaling_mode_) {
|
||||
case Options::ScalingMode::DISABLED:
|
||||
// 1:1, sense escala (pot ser diminut en finestres grans)
|
||||
vw = logical_w;
|
||||
vh = logical_h;
|
||||
break;
|
||||
case Options::ScalingMode::STRETCH:
|
||||
// Omple tota la finestra, escala no uniforme
|
||||
vw = static_cast<float>(sw);
|
||||
vh = static_cast<float>(sh);
|
||||
break;
|
||||
case Options::ScalingMode::LETTERBOX: {
|
||||
const float SCALE = std::min(static_cast<float>(sw) / logical_w,
|
||||
static_cast<float>(sh) / logical_h);
|
||||
vw = logical_w * SCALE;
|
||||
vh = logical_h * SCALE;
|
||||
break;
|
||||
}
|
||||
case Options::ScalingMode::OVERSCAN: {
|
||||
const float SCALE = std::max(static_cast<float>(sw) / logical_w,
|
||||
static_cast<float>(sh) / logical_h);
|
||||
vw = logical_w * SCALE;
|
||||
vh = logical_h * SCALE;
|
||||
break;
|
||||
}
|
||||
case Options::ScalingMode::INTEGER: {
|
||||
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(logical_w), static_cast<int>(sh) / static_cast<int>(logical_h)));
|
||||
vw = logical_w * static_cast<float>(SCALE);
|
||||
vh = logical_h * static_cast<float>(SCALE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
|
||||
@@ -914,9 +976,14 @@ namespace Rendering {
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
// El shader CrtPi tradicionalment usa NEAREST per a fer el seu
|
||||
// propi filtrat analític. Si l'usuari tria LINEAR explícitament,
|
||||
// respectem la preferència (la mostra arribarà pre-suavitzada).
|
||||
SDL_GPUTextureSamplerBinding binding = {};
|
||||
binding.texture = effective_scene;
|
||||
binding.sampler = sampler_; // NEAREST: el shader CrtPi fa el seu propi filtrat analític
|
||||
binding.sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||
? linear_sampler_
|
||||
: sampler_;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
// Injectar texture_width/height abans del push
|
||||
@@ -991,11 +1058,15 @@ namespace Rendering {
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
// Amb SS: llegir de scaled_texture_ amb LINEAR; sense SS: effective_scene amb NEAREST.
|
||||
// Font: amb SS scaled_texture_; sense SS, effective_scene (que ja
|
||||
// és internal_texture_ si internal_res_>1, o scene_texture_ si no).
|
||||
// Sampler: honora el filtre global que l'usuari tria al menú
|
||||
// (texture_filter_linear_). Abans estava hardcoded a NEAREST
|
||||
// quan SS era off — el menú no tenia efecte visible en aquest path.
|
||||
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
|
||||
? scaled_texture_
|
||||
: effective_scene;
|
||||
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
|
||||
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||
? linear_sampler_
|
||||
: sampler_;
|
||||
|
||||
@@ -1047,6 +1118,10 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||
scene_texture_ = nullptr;
|
||||
}
|
||||
if (internal_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||
internal_texture_ = nullptr;
|
||||
}
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
@@ -1193,8 +1268,20 @@ namespace Rendering {
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setScaleMode(bool integer_scale) {
|
||||
integer_scale_ = integer_scale;
|
||||
void SDL3GPUShader::setScalingMode(Options::ScalingMode mode) {
|
||||
scaling_mode_ = mode;
|
||||
}
|
||||
|
||||
// setInternalResolution — canvia el multiplicador de resolució interna.
|
||||
// Recrea la textura intermèdia amb les noves dimensions (320·N × 200·N).
|
||||
void SDL3GPUShader::setInternalResolution(int multiplier) {
|
||||
const int NEW = std::max(1, multiplier);
|
||||
if (NEW == internal_res_) return;
|
||||
internal_res_ = NEW;
|
||||
if (is_initialized_ && device_ != nullptr) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
recreateInternalTexture();
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setStretch4_3(bool enabled) {
|
||||
@@ -1221,10 +1308,6 @@ namespace Rendering {
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setLinearUpscale(bool linear) {
|
||||
linear_upscale_ = linear;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setDownscaleAlgo(int algo) {
|
||||
downscale_algo_ = std::max(0, std::min(algo, 2));
|
||||
}
|
||||
@@ -1246,6 +1329,10 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||
scene_texture_ = nullptr;
|
||||
}
|
||||
if (internal_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||
internal_texture_ = nullptr;
|
||||
}
|
||||
// scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
@@ -1288,10 +1375,15 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s (scaled se creará en render)",
|
||||
// Recrea la textura interna si internal_res_ > 1 — manté coherència
|
||||
// en canvis d'SS que passen per reinitTexturesAndBuffer().
|
||||
recreateInternalTexture();
|
||||
|
||||
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s, internal ×%d (scaled se creará en render)",
|
||||
game_width_,
|
||||
game_height_,
|
||||
oversample_ > 1 ? "on" : "off");
|
||||
oversample_ > 1 ? "on" : "off",
|
||||
internal_res_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1362,4 +1454,42 @@ namespace Rendering {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// recreateInternalTexture — libera y recrea internal_texture_ para el
|
||||
// multiplicador internal_res_ actual. Si val 1, allibera i queda a nullptr
|
||||
// (el pipeline ometrà la còpia al següent render).
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::recreateInternalTexture() -> bool {
|
||||
if (internal_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||
internal_texture_ = nullptr;
|
||||
}
|
||||
if (internal_res_ <= 1 || device_ == nullptr) return true;
|
||||
|
||||
const int W = game_width_ * internal_res_;
|
||||
const int H = game_height_ * internal_res_;
|
||||
|
||||
SDL_GPUTextureCreateInfo info = {};
|
||||
info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
info.width = static_cast<Uint32>(W);
|
||||
info.height = static_cast<Uint32>(H);
|
||||
info.layer_count_or_depth = 1;
|
||||
info.num_levels = 1;
|
||||
|
||||
internal_texture_ = SDL_CreateGPUTexture(device_, &info);
|
||||
if (internal_texture_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: failed to create internal texture %dx%d (×%d): %s",
|
||||
W,
|
||||
H,
|
||||
internal_res_,
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Log("SDL3GPUShader: internal texture %dx%d (×%d)", W, H, internal_res_);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -96,15 +96,12 @@ namespace Rendering {
|
||||
// Activa/desactiva VSync en el swapchain
|
||||
void setVSync(bool vsync) override;
|
||||
|
||||
// Activa/desactiva escalado entero (integer scale)
|
||||
void setScaleMode(bool integer_scale) override;
|
||||
// Selecciona el mode de presentació lògica (DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER)
|
||||
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;
|
||||
|
||||
@@ -123,7 +120,14 @@ namespace Rendering {
|
||||
// Estirament vertical 4:3 (fusionat amb l'upscale pass)
|
||||
void setStretch4_3(bool enabled) override;
|
||||
[[nodiscard]] auto isStretch4_3() 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:
|
||||
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||
@@ -145,6 +149,7 @@ namespace Rendering {
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
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
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
@@ -157,6 +162,7 @@ namespace Rendering {
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
|
||||
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
|
||||
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;
|
||||
@@ -172,14 +178,14 @@ namespace Rendering {
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
bool vsync_ = true;
|
||||
bool integer_scale_ = false;
|
||||
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
|
||||
Options::ScalingMode scaling_mode_ = Options::ScalingMode::INTEGER;
|
||||
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
|
||||
|
||||
2
source/core/rendering/sdl3gpu/spv/.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
DisableFormat: true
|
||||
SortIncludes: Never
|
||||
4
source/core/rendering/sdl3gpu/spv/.clang-tidy
Normal file
@@ -0,0 +1,4 @@
|
||||
# source/core/rendering/sdl3gpu/spv/.clang-tidy
|
||||
Checks: '-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "game/options.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/** @brief Identificador del shader de post-procesado activo */
|
||||
@@ -105,9 +107,9 @@ namespace Rendering {
|
||||
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)
|
||||
@@ -116,13 +118,6 @@ namespace Rendering {
|
||||
*/
|
||||
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),
|
||||
@@ -179,9 +174,16 @@ namespace Rendering {
|
||||
[[nodiscard]] virtual auto isStretch4_3() 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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <sstream>
|
||||
#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)
|
||||
struct rgb;
|
||||
@@ -19,10 +19,6 @@ Text::Text(const char* fnt_file, const char* gif_file) {
|
||||
loadFont(fnt_file);
|
||||
}
|
||||
|
||||
Text::~Text() {
|
||||
if (bitmap_) free(bitmap_);
|
||||
}
|
||||
|
||||
// --- UTF-8 ---
|
||||
|
||||
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
@@ -62,13 +58,13 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
// --- Càrrega de font ---
|
||||
|
||||
void Text::loadFont(const char* fnt_file) {
|
||||
auto buffer = file_readfile(fnt_file);
|
||||
auto buffer = ResourceHelper::loadFile(fnt_file);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
std::istringstream stream(std::string(buffer.data(), buffer.size()));
|
||||
std::istringstream stream(std::string(reinterpret_cast<const char*>(buffer.data()), buffer.size()));
|
||||
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
@@ -80,27 +76,27 @@ void Text::loadFont(const char* fnt_file) {
|
||||
// Elimina comentaris inline
|
||||
auto comment_pos = line.find('#');
|
||||
if (comment_pos != std::string::npos) {
|
||||
line = line.substr(0, comment_pos);
|
||||
line.resize(comment_pos);
|
||||
}
|
||||
|
||||
// Parseja directives
|
||||
if (line.find("box_width") == 0) {
|
||||
if (line.starts_with("box_width")) {
|
||||
sscanf(line.c_str(), "box_width %d", &box_width_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("box_height") == 0) {
|
||||
if (line.starts_with("box_height")) {
|
||||
sscanf(line.c_str(), "box_height %d", &box_height_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("columns") == 0) {
|
||||
if (line.starts_with("columns")) {
|
||||
sscanf(line.c_str(), "columns %d", &columns_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("cell_spacing") == 0) {
|
||||
if (line.starts_with("cell_spacing")) {
|
||||
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("row_spacing") == 0) {
|
||||
if (line.starts_with("row_spacing")) {
|
||||
sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
|
||||
continue;
|
||||
}
|
||||
@@ -126,14 +122,14 @@ void Text::loadFont(const char* fnt_file) {
|
||||
}
|
||||
|
||||
void Text::loadBitmap(const char* gif_file) {
|
||||
auto buffer = file_readfile(gif_file);
|
||||
auto buffer = ResourceHelper::loadFile(gif_file);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
|
||||
auto* raw = reinterpret_cast<unsigned char*>(buffer.data());
|
||||
auto* raw = buffer.data();
|
||||
int w = raw[6] | (raw[7] << 8);
|
||||
int h = raw[8] | (raw[9] << 8);
|
||||
|
||||
@@ -146,7 +142,8 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
|
||||
bitmap_width_ = w;
|
||||
bitmap_height_ = h;
|
||||
bitmap_ = pixels;
|
||||
bitmap_.assign(pixels, pixels + (static_cast<size_t>(w) * h));
|
||||
free(pixels); // LoadGif usa malloc internament
|
||||
|
||||
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
|
||||
}
|
||||
@@ -154,7 +151,7 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
// --- Renderitzat ---
|
||||
|
||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
||||
if (!bitmap_ || !pixel_data) return;
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
@@ -207,7 +204,7 @@ void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 colo
|
||||
}
|
||||
|
||||
void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
|
||||
if (!bitmap_ || !pixel_data) return;
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
// Descart ràpid si el glifo sencer cau fora verticalment
|
||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
|
||||
@@ -262,7 +259,7 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
|
||||
}
|
||||
|
||||
void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const {
|
||||
if (!bitmap_ || !pixel_data) return;
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
@@ -308,7 +305,7 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
|
||||
}
|
||||
|
||||
void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const {
|
||||
if (!bitmap_ || !pixel_data) return;
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class Text {
|
||||
public:
|
||||
Text(const char* fnt_file, const char* gif_file);
|
||||
~Text();
|
||||
|
||||
// Pinta texto sobre un buffer ARGB de 320x200
|
||||
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const;
|
||||
@@ -46,7 +46,7 @@ class Text {
|
||||
int cell_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_height_{0};
|
||||
|
||||
|
||||
269
source/core/resources/resource_cache.cpp
Normal file
@@ -0,0 +1,269 @@
|
||||
#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).
|
||||
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
||||
extern unsigned char* LoadPalette(unsigned char* data);
|
||||
|
||||
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_t* {
|
||||
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_t* {
|
||||
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() {
|
||||
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";
|
||||
}
|
||||
|
||||
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;
|
||||
auto* list = List::get();
|
||||
|
||||
while (stage_ != LoadStage::DONE) {
|
||||
switch (stage_) {
|
||||
case LoadStage::MUSICS: {
|
||||
auto items = list->getListByType(List::Type::MUSIC);
|
||||
if (stage_index_ == 0) musics_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::SOUNDS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneMusic(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::SOUNDS: {
|
||||
auto items = list->getListByType(List::Type::SOUND);
|
||||
if (stage_index_ == 0) sounds_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::BITMAPS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneSound(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::BITMAPS: {
|
||||
auto items = list->getListByType(List::Type::BITMAP);
|
||||
if (stage_index_ == 0) surfaces_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::TEXT_FILES;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneBitmap(stage_index_++);
|
||||
break;
|
||||
}
|
||||
case LoadStage::TEXT_FILES: {
|
||||
auto data_items = list->getListByType(List::Type::DATA);
|
||||
auto font_items = list->getListByType(List::Type::FONT);
|
||||
auto items = data_items;
|
||||
items.insert(items.end(), font_items.begin(), font_items.end());
|
||||
if (stage_index_ == 0) text_files_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::DONE;
|
||||
stage_index_ = 0;
|
||||
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
|
||||
break;
|
||||
}
|
||||
loadOneTextFile(stage_index_++);
|
||||
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_t* 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_t, 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_t* 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_t, 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
|
||||
73
source/core/resources/resource_cache.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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_t*;
|
||||
auto getSound(const std::string& name) -> JA_Sound_t*;
|
||||
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 {
|
||||
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);
|
||||
|
||||
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
|
||||
67
source/core/resources/resource_helper.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#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_;
|
||||
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(file_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_.loadPack(pack_file);
|
||||
|
||||
if (pack_loaded_) {
|
||||
std::cout << "ResourceHelper: pack loaded (" << pack_.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_.clear();
|
||||
pack_loaded_ = false;
|
||||
}
|
||||
|
||||
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t> {
|
||||
if (pack_loaded_ && pack_.hasResource(relative_path)) {
|
||||
return pack_.getResource(relative_path);
|
||||
}
|
||||
if (fallback_enabled_) {
|
||||
return readFromDisk(relative_path);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto hasPack() -> bool {
|
||||
return pack_loaded_;
|
||||
}
|
||||
|
||||
} // namespace ResourceHelper
|
||||
27
source/core/resources/resource_helper.hpp
Normal 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 `file_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
|
||||
111
source/core/resources/resource_list.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#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
|
||||
62
source/core/resources/resource_list.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#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 : int {
|
||||
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
|
||||
220
source/core/resources/resource_pack.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
#include "core/resources/resource_pack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
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).
|
||||
uint32_t checksum = 0x12345678;
|
||||
for (unsigned char b : data) {
|
||||
checksum = ((checksum << 5) + checksum) + b;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) return;
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= 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;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
||||
std::string filepath = entry.path().string();
|
||||
std::string filename = std::filesystem::relative(entry.path(), directory).string();
|
||||
std::ranges::replace(filename, '\\', '/');
|
||||
|
||||
if (!addFile(filename, filepath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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
source/core/resources/resource_pack.hpp
Normal 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);
|
||||
};
|
||||
61
source/core/resources/resource_types.hpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Forward declarations to keep this header light.
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
void JA_DeleteMusic(JA_Music_t* music);
|
||||
void JA_DeleteSound(JA_Sound_t* sound);
|
||||
|
||||
namespace Resource {
|
||||
|
||||
struct MusicDeleter {
|
||||
void operator()(JA_Music_t* music) const noexcept {
|
||||
if (music != nullptr) {
|
||||
JA_DeleteMusic(music);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct SoundDeleter {
|
||||
void operator()(JA_Sound_t* sound) const noexcept {
|
||||
if (sound != nullptr) {
|
||||
JA_DeleteSound(sound);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct MusicResource {
|
||||
std::string name;
|
||||
std::unique_ptr<JA_Music_t, MusicDeleter> music;
|
||||
};
|
||||
|
||||
struct SoundResource {
|
||||
std::string name;
|
||||
std::unique_ptr<JA_Sound_t, 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
|
||||
@@ -3,11 +3,12 @@
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/input/gamepad.hpp"
|
||||
#include "core/input/global_inputs.hpp"
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/input/key_remap.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/jinput.hpp"
|
||||
@@ -15,11 +16,12 @@
|
||||
#include "core/rendering/menu.hpp"
|
||||
#include "core/rendering/overlay.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/system/fiber.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/modulegame.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "scenes/banner_scene.hpp"
|
||||
#include "scenes/boot_loader_scene.hpp"
|
||||
#include "scenes/credits_scene.hpp"
|
||||
#include "scenes/intro_new_logo_scene.hpp"
|
||||
#include "scenes/intro_scene.hpp"
|
||||
@@ -33,16 +35,11 @@
|
||||
// Cheats del joc original — declarats a jinput.cpp
|
||||
extern void JI_moveCheats(Uint8 new_key);
|
||||
|
||||
Director* Director::instance_ = nullptr;
|
||||
std::unique_ptr<Director> Director::instance_;
|
||||
|
||||
namespace {
|
||||
Director::~Director() = default;
|
||||
|
||||
// Entry del fiber del joc. Dispatcha a una escena segons l'estat actual:
|
||||
// `gameState == 0` → ModuleGame (gameplay), `gameState == 1` → una
|
||||
// `scenes::Scene` del registry triada per `info::ctx.num_piramide`. Cada
|
||||
// escena és tick-based; el JD8_Flip() entre ticks cedeix al Director
|
||||
// via `GameFiber::yield()`.
|
||||
void gameFiberEntry() {
|
||||
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;
|
||||
@@ -57,55 +54,38 @@ void gameFiberEntry() {
|
||||
info::ctx.nou_personatge = true;
|
||||
fclose(ini);
|
||||
}
|
||||
|
||||
int gameState = 1;
|
||||
while (gameState != -1 && !JG_Quitting()) {
|
||||
std::unique_ptr<scenes::Scene> scene;
|
||||
|
||||
if (gameState == 0) {
|
||||
// Gameplay. ModuleGame és una scenes::Scene des de Phase A de
|
||||
// la migració — mateix mini-loop tick+flip que la resta.
|
||||
scene = std::make_unique<ModuleGame>();
|
||||
} else {
|
||||
// gameState == 1: dispatch al registry per num_piramide. El
|
||||
// vell ModuleSequence::Go() feia aquest redirect al principi:
|
||||
// si el jugador arriba a la Secreta (6) sense prou diners,
|
||||
// salta als slides de fracàs (7) abans de buscar l'escena.
|
||||
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
|
||||
info::ctx.num_piramide = 7;
|
||||
}
|
||||
scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
|
||||
}
|
||||
|
||||
if (!scene) {
|
||||
// State no registrat — indica un bug del dispatcher o del
|
||||
// registre d'escenes. Eixim ordenadament en lloc de cremar CPU.
|
||||
break;
|
||||
}
|
||||
|
||||
scene->onEnter();
|
||||
Uint32 last = SDL_GetTicks();
|
||||
while (!scene->done() && !JG_Quitting()) {
|
||||
JI_Update(); // refresca key_pressed/any_key per a les escenes
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
scene->tick(static_cast<int>(now - last));
|
||||
last = now;
|
||||
JD8_Flip(); // presenta i cedix al Director
|
||||
}
|
||||
gameState = scene->nextState();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
std::unique_ptr<scenes::Scene> Director::createNextScene() {
|
||||
// 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() {
|
||||
instance_ = new Director();
|
||||
instance_ = std::unique_ptr<Director>(new Director());
|
||||
Gamepad::init();
|
||||
|
||||
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
|
||||
// amb una factory de `scenes::Scene`. El gameFiberEntry consulta
|
||||
// aquest registry per a tots els states de seqüència; si una clau
|
||||
// no apareix ací, el fiber eixirà del loop.
|
||||
// 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>(); });
|
||||
@@ -132,27 +112,23 @@ void Director::init() {
|
||||
}
|
||||
return std::make_unique<scenes::IntroScene>();
|
||||
});
|
||||
|
||||
GameFiber::init(gameFiberEntry);
|
||||
}
|
||||
|
||||
void Director::destroy() {
|
||||
GameFiber::destroy();
|
||||
Gamepad::destroy();
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
instance_.reset();
|
||||
}
|
||||
|
||||
auto Director::get() -> Director* {
|
||||
return instance_;
|
||||
return instance_.get();
|
||||
}
|
||||
|
||||
void Director::togglePause() {
|
||||
paused_ = !paused_;
|
||||
if (paused_) {
|
||||
JA_PauseMusic();
|
||||
Audio::get()->pauseMusic();
|
||||
} else {
|
||||
JA_ResumeMusic();
|
||||
Audio::get()->resumeMusic();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,17 +140,36 @@ void Director::setup() {
|
||||
}
|
||||
|
||||
bool Director::iterate() {
|
||||
if (GameFiber::is_done() || quit_requested_) {
|
||||
// Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press),
|
||||
// li donem l'oportunitat de tornar net: marquem quit i reprenem el
|
||||
// fiber fins que detecte JG_Quitting() i retorne de forma natural.
|
||||
if (quit_requested_) {
|
||||
JG_QuitSignal();
|
||||
while (!GameFiber::is_done()) {
|
||||
GameFiber::resume();
|
||||
}
|
||||
current_scene_.reset(); // destrueix l'escena actual ordenadament
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reinici "suau": processat al començament del frame per no manipular
|
||||
// l'escena des d'una lambda del menú mentre encara s'està executant.
|
||||
if (restart_requested_) {
|
||||
restart_requested_ = false;
|
||||
Audio::get()->stopMusic();
|
||||
Audio::get()->stopAllSounds();
|
||||
// Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
|
||||
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
|
||||
initGameContext();
|
||||
// Força l'intro independentment de `piramide_inicial` (que pot estar
|
||||
// configurat a una piràmide intermèdia per a proves ràpides).
|
||||
info::ctx.num_piramide = 255;
|
||||
current_scene_.reset();
|
||||
game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide
|
||||
has_frame_ = false;
|
||||
Menu::close();
|
||||
JI_SetInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho
|
||||
}
|
||||
|
||||
if (!context_initialized_) {
|
||||
initGameContext();
|
||||
context_initialized_ = true;
|
||||
}
|
||||
|
||||
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
|
||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||
|
||||
@@ -188,7 +183,7 @@ bool Director::iterate() {
|
||||
// Bombeig de l'àudio: reomple l'stream de música i para els canals
|
||||
// drenats. Substituïx el callback de SDL_AddTimer de la versió
|
||||
// antiga — imprescindible per al port a emscripten.
|
||||
JA_Update();
|
||||
Audio::update();
|
||||
|
||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
||||
@@ -205,15 +200,41 @@ bool Director::iterate() {
|
||||
esc_blocked_ = false;
|
||||
}
|
||||
|
||||
// Cedeix el control al fiber del joc. Quan retorne (per un JD8_Flip()
|
||||
// dins del joc) tindrem un nou frame a pixel_data. Si estem en pausa,
|
||||
// no executem el fiber: es queda congelat al seu últim yield i
|
||||
// continuem presentant l'últim frame conegut.
|
||||
// Avança l'escena (si no estem pausats). En pausa, es manté l'escena
|
||||
// congelada i re-presentem l'últim frame amb l'overlay fresc per
|
||||
// damunt.
|
||||
if (!paused_) {
|
||||
GameFiber::resume();
|
||||
if (GameFiber::is_done()) {
|
||||
return false;
|
||||
// Transicions: si l'escena actual ha acabat (o s'ha senyalat
|
||||
// quit), llegim el seu next state i la destruïm per crear la
|
||||
// següent a continuació.
|
||||
if (current_scene_ && (current_scene_->done() || JG_Quitting())) {
|
||||
game_state_ = current_scene_->nextState();
|
||||
current_scene_.reset();
|
||||
}
|
||||
|
||||
// Si no hi ha escena activa, construeix la pròxima segons
|
||||
// game_state_ i info::ctx. Si és impossible (game_state_ == -1,
|
||||
// quit, o state no registrat), eixim del loop.
|
||||
if (!current_scene_) {
|
||||
if (game_state_ == -1 || JG_Quitting()) return false;
|
||||
current_scene_ = createNextScene();
|
||||
if (!current_scene_) return false;
|
||||
current_scene_->onEnter();
|
||||
last_tick_ms_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
// Tick de l'escena. JI_Update refresca key_pressed/any_key; el
|
||||
// delta_ms és el temps real transcorregut des de l'últim tick.
|
||||
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);
|
||||
|
||||
// Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
|
||||
// actual. JD8_Flip ja no fa yield (Phase B.2 eliminà els fibers);
|
||||
// ara només omple el framebuffer perquè el Director l'aprofite.
|
||||
JD8_Flip();
|
||||
std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_));
|
||||
has_frame_ = true;
|
||||
}
|
||||
@@ -238,12 +259,11 @@ bool Director::iterate() {
|
||||
}
|
||||
|
||||
void Director::teardown() {
|
||||
// Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del
|
||||
// sistema), li donem l'oportunitat de tornar net.
|
||||
// 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();
|
||||
while (!GameFiber::is_done()) {
|
||||
GameFiber::resume();
|
||||
}
|
||||
current_scene_.reset();
|
||||
}
|
||||
|
||||
void Director::run() {
|
||||
@@ -267,16 +287,13 @@ void Director::handleEvent(const SDL_Event& event) {
|
||||
JG_QuitSignal();
|
||||
requestQuit();
|
||||
}
|
||||
// Hot-plug de gamepad
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
// Hot-plug de gamepad (a Emscripten els dispositius web entren com
|
||||
// JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
|
||||
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;
|
||||
}
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
// 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]) {
|
||||
@@ -289,17 +306,18 @@ void Director::handleEvent(const SDL_Event& event) {
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
|
||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació.
|
||||
// No mostrem notificació — l'indicador persistent "Pausa" a la cantonada
|
||||
// superior dreta (pintat per Overlay) ja comunica l'estat.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == Options::keys_gui.pause_toggle) {
|
||||
event.key.scancode == KeyConfig::scancode("pause_toggle")) {
|
||||
togglePause();
|
||||
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// 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) {
|
||||
event.key.scancode == KeyConfig::scancode("menu_toggle")) {
|
||||
Menu::toggle();
|
||||
JI_SetInputBlocked(Menu::isOpen());
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
@@ -325,6 +343,14 @@ void Director::handleEvent(const SDL_Event& event) {
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
||||
return; // no deixem passar KEY_UP al joc tampoc
|
||||
}
|
||||
// Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS
|
||||
// del toggle del menú/pausa i del handling del menú obert — així F12 i
|
||||
// SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la
|
||||
// navegació per dins del menú tampoc els anul·la.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
|
||||
Overlay::cancelCredits();
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
@@ -354,19 +380,12 @@ void Director::handleEvent(const SDL_Event& event) {
|
||||
// quan l'overlay faça timeout
|
||||
return;
|
||||
} else {
|
||||
// Comprova si és una tecla GUI (no passa al joc)
|
||||
// Comprova si és una tecla d'UI registrada (no passa al joc).
|
||||
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
|
||||
// incloent pause_toggle i menu_toggle (defensa en profunditat:
|
||||
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
|
||||
const auto sc = event.key.scancode;
|
||||
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) {
|
||||
if (!KeyConfig::isGuiKey(sc)) {
|
||||
key_pressed_ = true;
|
||||
JI_moveCheats(sc);
|
||||
}
|
||||
@@ -380,6 +399,10 @@ void Director::requestQuit() {
|
||||
JG_QuitSignal();
|
||||
}
|
||||
|
||||
void Director::requestRestart() {
|
||||
restart_requested_ = true;
|
||||
}
|
||||
|
||||
auto Director::consumeKeyPressed() -> bool {
|
||||
return key_pressed_.exchange(false);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
// El Director és el thread principal que controla la presentació i els inputs.
|
||||
// El codi del joc s'executa dins d'un *fiber* cooperatiu (veure fiber.hpp):
|
||||
// el joc produeix un frame, crida JD8_Flip() que internament fa yield al
|
||||
// Director, i el Director el presenta abans de tornar-lo a reprendre amb
|
||||
// GameFiber::resume(). Tot ocorre en un únic thread — sense mutex, sense
|
||||
// condition_variable, compatible amb el futur port a SDL_AppIterate.
|
||||
#include "scenes/scene.hpp"
|
||||
|
||||
// El Director és l'únic thread del runtime. Cada iterate() fa input →
|
||||
// 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 {
|
||||
public:
|
||||
static void init();
|
||||
@@ -34,25 +36,39 @@ class Director {
|
||||
void requestQuit();
|
||||
auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||
|
||||
// Demana un reinici "suau": para música i sons, reseteja info::ctx i
|
||||
// torna a l'intro (state 255). Es processa al començament del pròxim
|
||||
// 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;
|
||||
|
||||
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
||||
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
||||
|
||||
// Pausa: mentre està activa, Director no fa resume() del fiber del joc,
|
||||
// així que el joc queda congelat al seu últim JD8_Flip.
|
||||
// Pausa: mentre està activa, iterate() no avança l'escena — es
|
||||
// continua presentant el darrer frame amb overlay fresc.
|
||||
void togglePause();
|
||||
auto isPaused() const -> bool { return paused_; }
|
||||
|
||||
public:
|
||||
~Director();
|
||||
|
||||
private:
|
||||
Director() = default;
|
||||
~Director() = default;
|
||||
|
||||
static Director* instance_;
|
||||
static std::unique_ptr<Director> instance_;
|
||||
|
||||
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
|
||||
|
||||
// Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
|
||||
// Es crida una sola vegada des d'iterate() a la primera invocació.
|
||||
void initGameContext();
|
||||
// Construeix l'escena apropiada segons game_state_ i info::ctx.
|
||||
// Retorna nullptr si l'state actual no té escena registrada (bug).
|
||||
std::unique_ptr<scenes::Scene> createNextScene();
|
||||
|
||||
// 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.
|
||||
@@ -60,7 +76,15 @@ class Director {
|
||||
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> restart_requested_{false};
|
||||
std::atomic<bool> key_pressed_{false};
|
||||
std::atomic<bool> esc_blocked_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
#include "core/system/fiber.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
// ucontext_t està marcat com a obsolet a POSIX.1-2008 però continua
|
||||
// funcional a glibc Linux i macOS. Si en el futur migrem a una alternativa
|
||||
// (boost::context, makecontext personalitzat) només cal tocar aquest fitxer.
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
|
||||
namespace GameFiber {
|
||||
|
||||
namespace {
|
||||
|
||||
bool initialized_ = false;
|
||||
bool done_ = false;
|
||||
EntryFn entry_fn_ = nullptr;
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
LPVOID main_fiber_ = nullptr;
|
||||
LPVOID game_fiber_ = nullptr;
|
||||
|
||||
void __stdcall trampoline(void* /*param*/) {
|
||||
if (entry_fn_) entry_fn_();
|
||||
done_ = true;
|
||||
SwitchToFiber(main_fiber_);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
ucontext_t main_ctx_{};
|
||||
ucontext_t fiber_ctx_{};
|
||||
void* fiber_stack_ = nullptr;
|
||||
|
||||
void trampoline() {
|
||||
if (entry_fn_) entry_fn_();
|
||||
done_ = true;
|
||||
// Retornar al main: uc_link apunta a main_ctx_ en init().
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void init(EntryFn entry, std::size_t stack_size) {
|
||||
if (initialized_) destroy();
|
||||
entry_fn_ = entry;
|
||||
done_ = false;
|
||||
|
||||
#if defined(_WIN32)
|
||||
main_fiber_ = ConvertThreadToFiber(nullptr);
|
||||
if (!main_fiber_) {
|
||||
// Ja era un fiber (no sol passar en el main thread d'una app SDL).
|
||||
main_fiber_ = GetCurrentFiber();
|
||||
}
|
||||
game_fiber_ = CreateFiber(stack_size, trampoline, nullptr);
|
||||
if (!game_fiber_) {
|
||||
std::cerr << "GameFiber::init: CreateFiber failed\n";
|
||||
return;
|
||||
}
|
||||
#else
|
||||
fiber_stack_ = std::malloc(stack_size);
|
||||
if (!fiber_stack_) {
|
||||
std::cerr << "GameFiber::init: malloc failed\n";
|
||||
return;
|
||||
}
|
||||
getcontext(&fiber_ctx_);
|
||||
fiber_ctx_.uc_stack.ss_sp = fiber_stack_;
|
||||
fiber_ctx_.uc_stack.ss_size = stack_size;
|
||||
fiber_ctx_.uc_link = &main_ctx_;
|
||||
makecontext(&fiber_ctx_, trampoline, 0);
|
||||
#endif
|
||||
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (!initialized_) return;
|
||||
#if defined(_WIN32)
|
||||
if (game_fiber_) {
|
||||
DeleteFiber(game_fiber_);
|
||||
game_fiber_ = nullptr;
|
||||
}
|
||||
// No desconvertim el main thread: SDL pot estar-ne pendent i ja no
|
||||
// tornem a crear fibers en aquesta execució. ConvertFiberToThread()
|
||||
// només cal si volguerem reutilitzar el main com a thread normal.
|
||||
#else
|
||||
if (fiber_stack_) {
|
||||
std::free(fiber_stack_);
|
||||
fiber_stack_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
initialized_ = false;
|
||||
done_ = false;
|
||||
entry_fn_ = nullptr;
|
||||
}
|
||||
|
||||
void resume() {
|
||||
if (!initialized_ || done_) return;
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(game_fiber_);
|
||||
#else
|
||||
swapcontext(&main_ctx_, &fiber_ctx_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void yield() {
|
||||
if (!initialized_) return;
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(main_fiber_);
|
||||
#else
|
||||
swapcontext(&fiber_ctx_, &main_ctx_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool is_done() { return done_; }
|
||||
bool is_initialized() { return initialized_; }
|
||||
|
||||
} // namespace GameFiber
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// Fiber minimalista sobre el suport natiu del SO (ucontext_t en POSIX,
|
||||
// Fibers API en Windows). Serveix per a implementar un yield/resume
|
||||
// cooperatiu entre el Director i el codi del joc sense un std::thread
|
||||
// ni mutex/condition_variable. Substituïx el bloqueig de publishFrame/
|
||||
// consumeFrame amb un mecanisme de control explícit.
|
||||
//
|
||||
// Contracte:
|
||||
// - GameFiber::init(entry) prepara un fiber que executarà `entry`
|
||||
// en un stack dedicat. No el comença a executar encara.
|
||||
// - GameFiber::resume() cedeix el control al fiber. Retorna quan el
|
||||
// fiber crida GameFiber::yield() o quan la funció entry retorna.
|
||||
// - GameFiber::yield() es crida des de dins del fiber per a tornar
|
||||
// el control al main (al punt just després de resume()).
|
||||
// - GameFiber::is_done() indica si la funció entry ha retornat.
|
||||
// - GameFiber::destroy() allibera el stack i reinicia l'estat.
|
||||
//
|
||||
// Per al port a emscripten (Fase 7) caldrà substituir aquesta capa per
|
||||
// Asyncify, però el contracte públic pot romandre idèntic.
|
||||
namespace GameFiber {
|
||||
|
||||
using EntryFn = void (*)();
|
||||
|
||||
void init(EntryFn entry, std::size_t stack_size = 256 * 1024);
|
||||
void destroy();
|
||||
|
||||
void resume();
|
||||
void yield();
|
||||
|
||||
bool is_done();
|
||||
bool is_initialized();
|
||||
|
||||
} // namespace GameFiber
|
||||
4
source/external/.clang-tidy
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# source/external/.clang-tidy
|
||||
Checks: '-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
class Bola : public Sprite {
|
||||
public:
|
||||
Bola(JD8_Surface gfx, Prota* sam);
|
||||
explicit Bola(JD8_Surface gfx, Prota* sam);
|
||||
|
||||
void draw();
|
||||
void draw() override;
|
||||
void update();
|
||||
|
||||
protected:
|
||||
|
||||
@@ -2,21 +2,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// Tecles GUI (capa de presentació — finestra, zoom, shaders, etc.)
|
||||
namespace Defaults::KeysGUI {
|
||||
constexpr SDL_Scancode DEC_ZOOM = SDL_SCANCODE_F1;
|
||||
constexpr SDL_Scancode INC_ZOOM = SDL_SCANCODE_F2;
|
||||
constexpr SDL_Scancode FULLSCREEN = SDL_SCANCODE_F3;
|
||||
constexpr SDL_Scancode TOGGLE_SHADER = SDL_SCANCODE_F4;
|
||||
constexpr SDL_Scancode TOGGLE_ASPECT_RATIO = SDL_SCANCODE_F5;
|
||||
constexpr SDL_Scancode TOGGLE_SUPERSAMPLING = SDL_SCANCODE_F6;
|
||||
constexpr SDL_Scancode NEXT_SHADER = SDL_SCANCODE_F7;
|
||||
constexpr SDL_Scancode NEXT_SHADER_PRESET = SDL_SCANCODE_F8;
|
||||
constexpr SDL_Scancode TOGGLE_STRETCH_FILTER = SDL_SCANCODE_F9;
|
||||
constexpr SDL_Scancode TOGGLE_RENDER_INFO = SDL_SCANCODE_F10;
|
||||
constexpr SDL_Scancode PAUSE_TOGGLE = SDL_SCANCODE_F11;
|
||||
constexpr SDL_Scancode MENU_TOGGLE = SDL_SCANCODE_F12;
|
||||
} // namespace Defaults::KeysGUI
|
||||
// Tecles GUI: viuen a data/input/keys.yaml (font única — KeyConfig).
|
||||
|
||||
// Tecles de joc (moviment del personatge, accions)
|
||||
namespace Defaults::KeysGame {
|
||||
@@ -31,12 +17,11 @@ namespace Defaults::Video {
|
||||
constexpr bool GPU_ACCELERATION = true;
|
||||
constexpr bool SHADER_ENABLED = false;
|
||||
constexpr bool SUPERSAMPLING = false;
|
||||
constexpr bool INTEGER_SCALE = true;
|
||||
constexpr bool VSYNC = true;
|
||||
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
|
||||
constexpr bool STRETCH_FILTER_LINEAR = false; // Filtre per a l'estirament 4:3 (false=NEAREST)
|
||||
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
|
||||
constexpr bool LINEAR_UPSCALE = false;
|
||||
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
|
||||
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
|
||||
constexpr int INTERNAL_RESOLUTION = 1; // Multiplicador enter de la textura font abans del pipeline
|
||||
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
|
||||
} // namespace Defaults::Video
|
||||
|
||||
namespace Defaults::Audio {
|
||||
@@ -61,4 +46,5 @@ namespace Defaults::Game {
|
||||
constexpr int DINERS_INICIAL = 0;
|
||||
constexpr bool USE_NEW_LOGO = true;
|
||||
constexpr bool SHOW_TITLE_CREDITS = true;
|
||||
constexpr bool SHOW_PRELOAD = false;
|
||||
} // namespace Defaults::Game
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Textos
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2000 Aventures en Egipte — JailDesigner";
|
||||
constexpr const char* VERSION = "1.11";
|
||||
constexpr const char* VERSION = "1.2";
|
||||
} // namespace Texts
|
||||
|
||||
// Resolución del juego
|
||||
|
||||
@@ -29,10 +29,6 @@ Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
||||
this->cycles_per_frame = 30;
|
||||
}
|
||||
|
||||
void Engendro::draw() {
|
||||
Sprite::draw();
|
||||
}
|
||||
|
||||
bool Engendro::update() {
|
||||
bool mort = false;
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
|
||||
class Engendro : public Sprite {
|
||||
public:
|
||||
Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
|
||||
explicit Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
|
||||
|
||||
void draw();
|
||||
bool update();
|
||||
|
||||
protected:
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
namespace info {
|
||||
|
||||
struct GameContext {
|
||||
int num_piramide = 0;
|
||||
int num_habitacio = 0;
|
||||
int diners = 0;
|
||||
int diamants = 0;
|
||||
int vida = 0;
|
||||
int momies = 0;
|
||||
int engendros = 0;
|
||||
bool nou_personatge = false;
|
||||
bool pepe_activat = false;
|
||||
struct GameContext {
|
||||
int num_piramide = 0;
|
||||
int num_habitacio = 0;
|
||||
int diners = 0;
|
||||
int diamants = 0;
|
||||
int vida = 0;
|
||||
int momies = 0;
|
||||
int engendros = 0;
|
||||
bool nou_personatge = false;
|
||||
bool pepe_activat = false;
|
||||
|
||||
void reset() { *this = GameContext{}; }
|
||||
};
|
||||
void reset() { *this = GameContext{}; }
|
||||
};
|
||||
|
||||
// Instància única de l'estat del joc. Reemplaça les variables soltes del
|
||||
// namespace `info::` per una struct encapsulada. A Fase 5 (single-threaded)
|
||||
// es podrà passar per referència als mòduls en lloc d'accedir via singleton.
|
||||
inline GameContext ctx;
|
||||
// Instància única de l'estat del joc. Reemplaça les variables soltes del
|
||||
// namespace `info::` per una struct encapsulada. A Fase 5 (single-threaded)
|
||||
// es podrà passar per referència als mòduls en lloc d'accedir via singleton.
|
||||
inline GameContext ctx;
|
||||
|
||||
} // namespace info
|
||||
|
||||
@@ -27,9 +27,14 @@ struct Vertex {
|
||||
|
||||
class Mapa {
|
||||
public:
|
||||
Mapa(JD8_Surface gfx, Prota* sam);
|
||||
explicit Mapa(JD8_Surface gfx, Prota* sam);
|
||||
~Mapa(void);
|
||||
|
||||
Mapa(const Mapa&) = delete;
|
||||
Mapa& operator=(const Mapa&) = delete;
|
||||
Mapa(Mapa&&) = delete;
|
||||
Mapa& operator=(Mapa&&) = delete;
|
||||
|
||||
void draw();
|
||||
void update();
|
||||
bool novaMomia();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
class Marcador {
|
||||
public:
|
||||
Marcador(JD8_Surface gfx, Prota* sam);
|
||||
explicit Marcador(JD8_Surface gfx, Prota* sam);
|
||||
~Marcador(void);
|
||||
|
||||
void draw();
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
#include "game/modulegame.hpp"
|
||||
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jfile.hpp"
|
||||
#include "core/jail/jgame.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
ModuleGame::ModuleGame() {
|
||||
this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "frames2.gif" : "frames.gif");
|
||||
this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
|
||||
JG_SetUpdateTicks(10);
|
||||
|
||||
this->sam = new Prota(this->gfx);
|
||||
this->mapa = new Mapa(this->gfx, this->sam);
|
||||
this->marcador = new Marcador(this->gfx, this->sam);
|
||||
this->sam = std::make_unique<Prota>(this->gfx);
|
||||
this->mapa = std::make_unique<Mapa>(this->gfx, this->sam.get());
|
||||
this->marcador = std::make_unique<Marcador>(this->gfx, this->sam.get());
|
||||
if (info::ctx.num_piramide == 2) {
|
||||
this->bola = new Bola(this->gfx, this->sam);
|
||||
} else {
|
||||
this->bola = nullptr;
|
||||
this->bola = std::make_unique<Bola>(this->gfx, this->sam.get());
|
||||
}
|
||||
this->momies = nullptr;
|
||||
|
||||
this->iniciarMomies();
|
||||
}
|
||||
|
||||
ModuleGame::~ModuleGame() {
|
||||
if (this->bola != nullptr) delete this->bola;
|
||||
if (this->momies != nullptr) {
|
||||
this->momies->clear();
|
||||
delete this->momies;
|
||||
}
|
||||
delete this->marcador;
|
||||
delete this->mapa;
|
||||
delete this->sam;
|
||||
|
||||
JD8_FreeSurface(this->gfx);
|
||||
}
|
||||
|
||||
@@ -42,22 +31,19 @@ void ModuleGame::onEnter() {
|
||||
// fade interpolarien cap a una paleta amb pantalla buida.
|
||||
this->Draw();
|
||||
|
||||
const char* music = info::ctx.num_piramide == 3 ? "00000008.ogg"
|
||||
: info::ctx.num_piramide == 2 ? "00000007.ogg"
|
||||
: info::ctx.num_piramide == 6 ? "00000002.ogg"
|
||||
: "00000006.ogg";
|
||||
const char* current_music = JA_GetMusicFilename();
|
||||
if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !current_music ||
|
||||
strcmp(music, current_music) != 0) {
|
||||
auto buffer = file_readfile(music);
|
||||
JA_PlayMusic(JA_LoadMusic(reinterpret_cast<Uint8*>(buffer.data()),
|
||||
static_cast<Uint32>(buffer.size()), music));
|
||||
}
|
||||
// Audio::playMusic ja és idempotent: si la pista actual coincideix amb la
|
||||
// demanada, no fa res. Per això podem cridar-lo cada onEnter sense
|
||||
// desencadenar restarts indesitjats.
|
||||
const char* music_name = info::ctx.num_piramide == 3 ? "piramide_3.ogg"
|
||||
: info::ctx.num_piramide == 2 ? "piramide_2.ogg"
|
||||
: info::ctx.num_piramide == 6 ? "secreta.ogg"
|
||||
: "piramide_1_4_5.ogg";
|
||||
Audio::get()->playMusic(music_name);
|
||||
|
||||
// Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de
|
||||
// 32) per cada tick; durant aquesta fase el gameplay no corre,
|
||||
// només Draw+fade. Substituïx la crida bloquejant `JD8_FadeToPal`.
|
||||
fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "frames2.gif" : "frames.gif"));
|
||||
fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
|
||||
phase_ = Phase::FadingIn;
|
||||
}
|
||||
|
||||
@@ -126,8 +112,8 @@ void ModuleGame::Draw() {
|
||||
this->mapa->draw();
|
||||
this->marcador->draw();
|
||||
this->sam->draw();
|
||||
if (this->momies != nullptr) this->momies->draw();
|
||||
if (this->bola != nullptr) this->bola->draw();
|
||||
for (auto& m : this->momies) m->draw();
|
||||
if (this->bola) this->bola->draw();
|
||||
}
|
||||
|
||||
void ModuleGame::Update() {
|
||||
@@ -135,32 +121,19 @@ void ModuleGame::Update() {
|
||||
JI_Update();
|
||||
|
||||
this->final_ = this->sam->update();
|
||||
if (this->momies != nullptr && this->momies->update()) {
|
||||
Momia* seguent = this->momies->next;
|
||||
delete this->momies;
|
||||
this->momies = seguent;
|
||||
info::ctx.momies--;
|
||||
}
|
||||
if (this->bola != nullptr) this->bola->update();
|
||||
const auto erased = std::erase_if(this->momies, [](auto& m) { return m->update(); });
|
||||
info::ctx.momies -= static_cast<int>(erased);
|
||||
if (this->bola) this->bola->update();
|
||||
this->mapa->update();
|
||||
if (this->mapa->novaMomia()) {
|
||||
if (this->momies != nullptr) {
|
||||
this->momies->insertar(new Momia(this->gfx, true, 0, 0, this->sam));
|
||||
info::ctx.momies++;
|
||||
} else {
|
||||
this->momies = new Momia(this->gfx, true, 0, 0, this->sam);
|
||||
info::ctx.momies++;
|
||||
}
|
||||
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, true, 0, 0, this->sam.get()));
|
||||
info::ctx.momies++;
|
||||
}
|
||||
|
||||
if (JI_CheatActivated("reviu")) info::ctx.vida = 5;
|
||||
if (JI_CheatActivated("alone")) {
|
||||
if (this->momies != nullptr) {
|
||||
this->momies->clear();
|
||||
delete this->momies;
|
||||
this->momies = nullptr;
|
||||
info::ctx.momies = 0;
|
||||
}
|
||||
this->momies.clear();
|
||||
info::ctx.momies = 0;
|
||||
}
|
||||
if (JI_CheatActivated("obert")) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
@@ -190,11 +163,7 @@ void ModuleGame::iniciarMomies() {
|
||||
int y = 170;
|
||||
bool dimonis = info::ctx.num_piramide == 6;
|
||||
for (int i = 0; i < info::ctx.momies; i++) {
|
||||
if (this->momies == nullptr) {
|
||||
this->momies = new Momia(this->gfx, dimonis, x, y, this->sam);
|
||||
} else {
|
||||
this->momies->insertar(new Momia(this->gfx, dimonis, x, y, this->sam));
|
||||
}
|
||||
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, dimonis, x, y, this->sam.get()));
|
||||
x += 65;
|
||||
if (x == 345) {
|
||||
x = 20;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "game/bola.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/mapa.hpp"
|
||||
@@ -28,6 +31,11 @@ class ModuleGame : public scenes::Scene {
|
||||
ModuleGame();
|
||||
~ModuleGame() override;
|
||||
|
||||
ModuleGame(const ModuleGame&) = delete;
|
||||
ModuleGame& operator=(const ModuleGame&) = delete;
|
||||
ModuleGame(ModuleGame&&) = delete;
|
||||
ModuleGame& operator=(ModuleGame&&) = delete;
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
@@ -52,9 +60,9 @@ class ModuleGame : public scenes::Scene {
|
||||
Uint8 final_{0};
|
||||
JD8_Surface gfx{nullptr};
|
||||
|
||||
Mapa* mapa{nullptr};
|
||||
Prota* sam{nullptr};
|
||||
Marcador* marcador{nullptr};
|
||||
Momia* momies{nullptr};
|
||||
Bola* bola{nullptr};
|
||||
std::unique_ptr<Mapa> mapa;
|
||||
std::unique_ptr<Prota> sam;
|
||||
std::unique_ptr<Marcador> marcador;
|
||||
std::vector<std::unique_ptr<Momia>> momies;
|
||||
std::unique_ptr<Bola> bola;
|
||||
};
|
||||
|
||||
@@ -10,15 +10,15 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
this->sam = sam;
|
||||
|
||||
entitat.frames.reserve(20);
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 5; x++) {
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 5; col++) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = (x * 15) + 75;
|
||||
f.x = (col * 15) + 75;
|
||||
if (this->dimoni) f.x += 75;
|
||||
f.y = 20 + (y * 15);
|
||||
f.y = 20 + (row * 15);
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
this->cur_frame = 0;
|
||||
this->o = rand() % 4;
|
||||
this->cycles_per_frame = 4;
|
||||
this->next = NULL;
|
||||
|
||||
if (this->dimoni) {
|
||||
if (x == 0) {
|
||||
@@ -57,22 +56,15 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
} else {
|
||||
this->y = y;
|
||||
}
|
||||
this->engendro = new Engendro(gfx, this->x, this->y);
|
||||
this->engendro = std::make_unique<Engendro>(gfx, this->x, this->y);
|
||||
} else {
|
||||
this->engendro = NULL;
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
}
|
||||
}
|
||||
|
||||
void Momia::clear() {
|
||||
if (this->next != NULL) this->next->clear();
|
||||
if (this->engendro != NULL) delete this->engendro;
|
||||
delete this->next;
|
||||
}
|
||||
|
||||
void Momia::draw() {
|
||||
if (this->engendro != NULL) {
|
||||
if (this->engendro) {
|
||||
this->engendro->draw();
|
||||
} else {
|
||||
Sprite::draw();
|
||||
@@ -85,95 +77,77 @@ void Momia::draw() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->next != NULL) this->next->draw();
|
||||
}
|
||||
|
||||
bool Momia::update() {
|
||||
bool morta = false;
|
||||
|
||||
if (this->engendro != NULL) {
|
||||
if (this->engendro) {
|
||||
if (this->engendro->update()) {
|
||||
delete this->engendro;
|
||||
this->engendro = NULL;
|
||||
}
|
||||
} else {
|
||||
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
|
||||
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
|
||||
if (this->dimoni) {
|
||||
if (rand() % 2 == 0) {
|
||||
if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
} else if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
}
|
||||
} else {
|
||||
if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
} else if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->o = rand() % 4;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this->o) {
|
||||
case 0:
|
||||
if (y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
|
||||
morta = true;
|
||||
if (this->sam->pergami) {
|
||||
this->sam->pergami = false;
|
||||
} else {
|
||||
info::ctx.vida--;
|
||||
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||
}
|
||||
}
|
||||
this->engendro.reset();
|
||||
}
|
||||
return morta;
|
||||
}
|
||||
|
||||
if (this->next != NULL) {
|
||||
if (this->next->update()) {
|
||||
Momia* seguent = this->next->next;
|
||||
delete this->next;
|
||||
this->next = seguent;
|
||||
info::ctx.momies--;
|
||||
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
|
||||
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
|
||||
if (this->dimoni) {
|
||||
if (rand() % 2 == 0) {
|
||||
if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
} else if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
}
|
||||
} else {
|
||||
if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
} else if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->o = rand() % 4;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this->o) {
|
||||
case 0:
|
||||
if (y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
|
||||
morta = true;
|
||||
if (this->sam->pergami) {
|
||||
this->sam->pergami = false;
|
||||
} else {
|
||||
info::ctx.vida--;
|
||||
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return morta;
|
||||
}
|
||||
|
||||
void Momia::insertar(Momia* momia) {
|
||||
if (this->next != NULL) {
|
||||
this->next->insertar(momia);
|
||||
} else {
|
||||
this->next = momia;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "game/engendro.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/prota.hpp"
|
||||
#include "game/sprite.hpp"
|
||||
|
||||
class Momia : public Sprite {
|
||||
public:
|
||||
Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
|
||||
|
||||
void clear();
|
||||
void draw();
|
||||
bool update();
|
||||
void insertar(Momia* momia);
|
||||
|
||||
bool dimoni;
|
||||
Momia* next;
|
||||
|
||||
protected:
|
||||
Prota* sam;
|
||||
Engendro* engendro;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "game/engendro.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/prota.hpp"
|
||||
#include "game/sprite.hpp"
|
||||
|
||||
class Momia : public Sprite {
|
||||
public:
|
||||
explicit Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
|
||||
|
||||
void draw() override;
|
||||
bool update();
|
||||
|
||||
bool dimoni;
|
||||
|
||||
protected:
|
||||
Prota* sam;
|
||||
std::unique_ptr<Engendro> engendro;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/defaults.hpp"
|
||||
#include "game/defines.hpp"
|
||||
@@ -16,12 +16,76 @@ namespace Options {
|
||||
config_file_path = path;
|
||||
}
|
||||
|
||||
void setDebugFile(const std::string& path) {
|
||||
debug_file_path = path;
|
||||
}
|
||||
|
||||
auto saveDebugToFile() -> bool {
|
||||
std::ofstream file(debug_file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Error: Unable to open file " << debug_file_path << " for writing\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Writing debug file: " << debug_file_path << '\n';
|
||||
|
||||
file << "# Aventures En Egipte - Debug Configuration File\n";
|
||||
file << "#\n";
|
||||
file << "# Loaded only in debug builds. Override gameplay starting state for testing.\n";
|
||||
file << "\n";
|
||||
file << "game:\n";
|
||||
file << " habitacio_inicial: " << game.habitacio_inicial << "\n";
|
||||
file << " piramide_inicial: " << game.piramide_inicial << "\n";
|
||||
file << " vides: " << game.vides << "\n";
|
||||
file << " diamants_inicial: " << game.diamants_inicial << "\n";
|
||||
file << " diners_inicial: " << game.diners_inicial << "\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto loadDebugFromFile() -> bool {
|
||||
std::ifstream file(debug_file_path);
|
||||
if (!file.good()) {
|
||||
std::cout << "Debug file not found, creating default: " << debug_file_path << '\n';
|
||||
return saveDebugToFile();
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
try {
|
||||
std::cout << "Reading debug file: " << debug_file_path << '\n';
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (yaml.contains("game")) {
|
||||
const auto& node = yaml["game"];
|
||||
if (node.contains("habitacio_inicial"))
|
||||
game.habitacio_inicial = node["habitacio_inicial"].get_value<int>();
|
||||
if (node.contains("piramide_inicial"))
|
||||
game.piramide_inicial = node["piramide_inicial"].get_value<int>();
|
||||
if (node.contains("vides"))
|
||||
game.vides = node["vides"].get_value<int>();
|
||||
if (node.contains("diamants_inicial"))
|
||||
game.diamants_inicial = node["diamants_inicial"].get_value<int>();
|
||||
if (node.contains("diners_inicial"))
|
||||
game.diners_inicial = node["diners_inicial"].get_value<int>();
|
||||
}
|
||||
return true;
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Error parsing YAML debug: " << e.what() << '\n';
|
||||
return saveDebugToFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Delega tots els canvis de l'estat d'àudio al wrapper Audio. Es manté
|
||||
// com a punt d'entrada únic per als callsites legacy del menú; el cos
|
||||
// ja no toca jail_audio directament.
|
||||
void applyAudio() {
|
||||
const float master = audio.enabled ? audio.volume : 0.0F;
|
||||
JA_EnableMusic(audio.music_enabled);
|
||||
JA_EnableSound(audio.sound_enabled);
|
||||
JA_SetMusicVolume(master * audio.music_volume);
|
||||
JA_SetSoundVolume(master * audio.sound_volume);
|
||||
if (::Audio::get() == nullptr) return;
|
||||
::Audio::get()->enable(audio.enabled);
|
||||
::Audio::get()->enableMusic(audio.music.enabled);
|
||||
::Audio::get()->enableSound(audio.sound.enabled);
|
||||
::Audio::get()->setMusicVolume(audio.music.volume);
|
||||
::Audio::get()->setSoundVolume(audio.sound.volume);
|
||||
}
|
||||
|
||||
// --- Funcions helper de càrrega ---
|
||||
@@ -39,17 +103,17 @@ namespace Options {
|
||||
if (node.contains("music")) {
|
||||
const auto& music = node["music"];
|
||||
if (music.contains("enabled"))
|
||||
audio.music_enabled = music["enabled"].get_value<bool>();
|
||||
audio.music.enabled = music["enabled"].get_value<bool>();
|
||||
if (music.contains("volume"))
|
||||
audio.music_volume = music["volume"].get_value<float>();
|
||||
audio.music.volume = music["volume"].get_value<float>();
|
||||
}
|
||||
|
||||
if (node.contains("sound")) {
|
||||
const auto& sound = node["sound"];
|
||||
if (sound.contains("enabled"))
|
||||
audio.sound_enabled = sound["enabled"].get_value<bool>();
|
||||
audio.sound.enabled = sound["enabled"].get_value<bool>();
|
||||
if (sound.contains("volume"))
|
||||
audio.sound_volume = sound["volume"].get_value<float>();
|
||||
audio.sound.volume = sound["volume"].get_value<float>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,18 +127,33 @@ namespace Options {
|
||||
video.shader_enabled = node["shader_enabled"].get_value<bool>();
|
||||
if (node.contains("supersampling"))
|
||||
video.supersampling = node["supersampling"].get_value<bool>();
|
||||
if (node.contains("integer_scale"))
|
||||
video.integer_scale = node["integer_scale"].get_value<bool>();
|
||||
if (node.contains("scaling_mode")) {
|
||||
auto s = node["scaling_mode"].get_value<std::string>();
|
||||
if (s == "disabled")
|
||||
video.scaling_mode = ScalingMode::DISABLED;
|
||||
else if (s == "stretch")
|
||||
video.scaling_mode = ScalingMode::STRETCH;
|
||||
else if (s == "letterbox")
|
||||
video.scaling_mode = ScalingMode::LETTERBOX;
|
||||
else if (s == "overscan")
|
||||
video.scaling_mode = ScalingMode::OVERSCAN;
|
||||
else
|
||||
video.scaling_mode = ScalingMode::INTEGER;
|
||||
}
|
||||
if (node.contains("vsync"))
|
||||
video.vsync = node["vsync"].get_value<bool>();
|
||||
if (node.contains("aspect_ratio_4_3"))
|
||||
video.aspect_ratio_4_3 = node["aspect_ratio_4_3"].get_value<bool>();
|
||||
if (node.contains("stretch_filter_linear"))
|
||||
video.stretch_filter_linear = node["stretch_filter_linear"].get_value<bool>();
|
||||
if (node.contains("texture_filter")) {
|
||||
auto s = node["texture_filter"].get_value<std::string>();
|
||||
video.texture_filter = (s == "linear") ? TextureFilter::LINEAR : TextureFilter::NEAREST;
|
||||
}
|
||||
if (node.contains("downscale_algo"))
|
||||
video.downscale_algo = node["downscale_algo"].get_value<int>();
|
||||
if (node.contains("linear_upscale"))
|
||||
video.linear_upscale = node["linear_upscale"].get_value<bool>();
|
||||
if (node.contains("internal_resolution")) {
|
||||
video.internal_resolution = node["internal_resolution"].get_value<int>();
|
||||
if (video.internal_resolution < 1) video.internal_resolution = 1;
|
||||
}
|
||||
if (node.contains("current_shader"))
|
||||
video.current_shader = node["current_shader"].get_value<std::string>();
|
||||
if (node.contains("current_postfx_preset"))
|
||||
@@ -129,8 +208,6 @@ namespace Options {
|
||||
loadScancodeField(node, "down", keys_game.down);
|
||||
loadScancodeField(node, "left", keys_game.left);
|
||||
loadScancodeField(node, "right", keys_game.right);
|
||||
loadScancodeField(node, "menu_toggle", keys_gui.menu_toggle);
|
||||
loadScancodeField(node, "pause_toggle", keys_gui.pause_toggle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,20 +215,12 @@ namespace Options {
|
||||
if (!yaml.contains("game")) return;
|
||||
const auto& node = yaml["game"];
|
||||
|
||||
if (node.contains("habitacio_inicial"))
|
||||
game.habitacio_inicial = node["habitacio_inicial"].get_value<int>();
|
||||
if (node.contains("piramide_inicial"))
|
||||
game.piramide_inicial = node["piramide_inicial"].get_value<int>();
|
||||
if (node.contains("vides"))
|
||||
game.vides = node["vides"].get_value<int>();
|
||||
if (node.contains("diamants_inicial"))
|
||||
game.diamants_inicial = node["diamants_inicial"].get_value<int>();
|
||||
if (node.contains("diners_inicial"))
|
||||
game.diners_inicial = node["diners_inicial"].get_value<int>();
|
||||
if (node.contains("use_new_logo"))
|
||||
game.use_new_logo = node["use_new_logo"].get_value<bool>();
|
||||
if (node.contains("show_title_credits"))
|
||||
game.show_title_credits = node["show_title_credits"].get_value<bool>();
|
||||
if (node.contains("show_preload"))
|
||||
game.show_preload = node["show_preload"].get_value<bool>();
|
||||
}
|
||||
|
||||
// Carrega les opcions des del fitxer configurat
|
||||
@@ -229,12 +298,32 @@ namespace Options {
|
||||
file << " gpu_acceleration: " << (video.gpu_acceleration ? "true" : "false") << "\n";
|
||||
file << " shader_enabled: " << (video.shader_enabled ? "true" : "false") << "\n";
|
||||
file << " supersampling: " << (video.supersampling ? "true" : "false") << "\n";
|
||||
file << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n";
|
||||
{
|
||||
const char* m = "integer";
|
||||
switch (video.scaling_mode) {
|
||||
case ScalingMode::DISABLED:
|
||||
m = "disabled";
|
||||
break;
|
||||
case ScalingMode::STRETCH:
|
||||
m = "stretch";
|
||||
break;
|
||||
case ScalingMode::LETTERBOX:
|
||||
m = "letterbox";
|
||||
break;
|
||||
case ScalingMode::OVERSCAN:
|
||||
m = "overscan";
|
||||
break;
|
||||
case ScalingMode::INTEGER:
|
||||
m = "integer";
|
||||
break;
|
||||
}
|
||||
file << " scaling_mode: " << m << " # disabled|stretch|letterbox|overscan|integer\n";
|
||||
}
|
||||
file << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n";
|
||||
file << " stretch_filter_linear: " << (video.stretch_filter_linear ? "true" : "false") << " # filtre 4:3: false=nearest, true=linear\n";
|
||||
file << " texture_filter: " << (video.texture_filter == TextureFilter::LINEAR ? "linear" : "nearest") << " # nearest|linear\n";
|
||||
file << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
|
||||
file << " linear_upscale: " << (video.linear_upscale ? "true" : "false") << "\n";
|
||||
file << " internal_resolution: " << video.internal_resolution << " # multiplicador enter font, clampat a max_zoom\n";
|
||||
file << " current_shader: " << video.current_shader << "\n";
|
||||
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
|
||||
file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n";
|
||||
@@ -269,34 +358,29 @@ namespace Options {
|
||||
file << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
||||
file << " volume: " << audio.volume << "\n";
|
||||
file << " music:\n";
|
||||
file << " enabled: " << (audio.music_enabled ? "true" : "false") << "\n";
|
||||
file << " volume: " << audio.music_volume << "\n";
|
||||
file << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
|
||||
file << " volume: " << audio.music.volume << "\n";
|
||||
file << " sound:\n";
|
||||
file << " enabled: " << (audio.sound_enabled ? "true" : "false") << "\n";
|
||||
file << " volume: " << audio.sound_volume << "\n";
|
||||
file << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
|
||||
file << " volume: " << audio.sound.volume << "\n";
|
||||
file << "\n";
|
||||
|
||||
// GAME
|
||||
file << "# GAME\n";
|
||||
file << "game:\n";
|
||||
file << " habitacio_inicial: " << game.habitacio_inicial << "\n";
|
||||
file << " piramide_inicial: " << game.piramide_inicial << "\n";
|
||||
file << " vides: " << game.vides << "\n";
|
||||
file << " diamants_inicial: " << game.diamants_inicial << "\n";
|
||||
file << " diners_inicial: " << game.diners_inicial << "\n";
|
||||
file << " use_new_logo: " << (game.use_new_logo ? "true" : "false") << "\n";
|
||||
file << " show_title_credits: " << (game.show_title_credits ? "true" : "false") << "\n";
|
||||
file << " show_preload: " << (game.show_preload ? "true" : "false") << "\n";
|
||||
file << "\n";
|
||||
|
||||
// CONTROLS
|
||||
file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", \"F12\", etc.)\n";
|
||||
// CONTROLS — només moviment del jugador. Les tecles d'UI viuen a
|
||||
// data/input/keys.yaml (defaults) + ~/.config/jailgames/aee/keys.yaml (overrides).
|
||||
file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", etc.)\n";
|
||||
file << "controls:\n";
|
||||
file << " up: \"" << SDL_GetScancodeName(keys_game.up) << "\"\n";
|
||||
file << " down: \"" << SDL_GetScancodeName(keys_game.down) << "\"\n";
|
||||
file << " left: \"" << SDL_GetScancodeName(keys_game.left) << "\"\n";
|
||||
file << " right: \"" << SDL_GetScancodeName(keys_game.right) << "\"\n";
|
||||
file << " menu_toggle: \"" << SDL_GetScancodeName(keys_gui.menu_toggle) << "\"\n";
|
||||
file << " pause_toggle: \"" << SDL_GetScancodeName(keys_gui.pause_toggle) << "\"\n";
|
||||
|
||||
file.close();
|
||||
|
||||
|
||||
@@ -8,23 +8,8 @@
|
||||
|
||||
namespace Options {
|
||||
|
||||
// Tecles GUI (finestra, zoom, shaders)
|
||||
struct KeysGUI {
|
||||
SDL_Scancode dec_zoom{Defaults::KeysGUI::DEC_ZOOM};
|
||||
SDL_Scancode inc_zoom{Defaults::KeysGUI::INC_ZOOM};
|
||||
SDL_Scancode fullscreen{Defaults::KeysGUI::FULLSCREEN};
|
||||
SDL_Scancode toggle_shader{Defaults::KeysGUI::TOGGLE_SHADER};
|
||||
SDL_Scancode toggle_aspect_ratio{Defaults::KeysGUI::TOGGLE_ASPECT_RATIO};
|
||||
SDL_Scancode toggle_supersampling{Defaults::KeysGUI::TOGGLE_SUPERSAMPLING};
|
||||
SDL_Scancode next_shader{Defaults::KeysGUI::NEXT_SHADER};
|
||||
SDL_Scancode next_shader_preset{Defaults::KeysGUI::NEXT_SHADER_PRESET};
|
||||
SDL_Scancode toggle_stretch_filter{Defaults::KeysGUI::TOGGLE_STRETCH_FILTER};
|
||||
SDL_Scancode toggle_render_info{Defaults::KeysGUI::TOGGLE_RENDER_INFO};
|
||||
SDL_Scancode pause_toggle{Defaults::KeysGUI::PAUSE_TOGGLE};
|
||||
SDL_Scancode menu_toggle{Defaults::KeysGUI::MENU_TOGGLE};
|
||||
};
|
||||
|
||||
// Tecles de joc (moviment, accions)
|
||||
// Tecles de joc (moviment, accions). Les tecles d'UI viuen ara a KeyConfig
|
||||
// (carregades de data/input/keys.yaml).
|
||||
struct KeysGame {
|
||||
SDL_Scancode up{Defaults::KeysGame::UP};
|
||||
SDL_Scancode down{Defaults::KeysGame::DOWN};
|
||||
@@ -38,20 +23,32 @@ namespace Options {
|
||||
TOP = 1,
|
||||
BOTTOM = 2 };
|
||||
|
||||
// Filtre de textura per a l'upscale final (sempre, no només en 4:3)
|
||||
enum class TextureFilter { NEAREST = 0,
|
||||
LINEAR = 1 };
|
||||
|
||||
// Mode de presentació lògica (escala finestra): mapeja directament
|
||||
// als valors de SDL_RendererLogicalPresentation.
|
||||
enum class ScalingMode { DISABLED = 0,
|
||||
STRETCH = 1,
|
||||
LETTERBOX = 2,
|
||||
OVERSCAN = 3,
|
||||
INTEGER = 4 };
|
||||
|
||||
// Opcions de vídeo
|
||||
struct Video {
|
||||
bool gpu_acceleration{Defaults::Video::GPU_ACCELERATION};
|
||||
bool shader_enabled{Defaults::Video::SHADER_ENABLED};
|
||||
bool supersampling{Defaults::Video::SUPERSAMPLING};
|
||||
bool integer_scale{Defaults::Video::INTEGER_SCALE};
|
||||
ScalingMode scaling_mode{ScalingMode::INTEGER};
|
||||
bool vsync{Defaults::Video::VSYNC};
|
||||
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
|
||||
bool stretch_filter_linear{Defaults::Video::STRETCH_FILTER_LINEAR};
|
||||
TextureFilter texture_filter{TextureFilter::NEAREST};
|
||||
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
|
||||
bool linear_upscale{Defaults::Video::LINEAR_UPSCALE};
|
||||
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
|
||||
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
|
||||
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
|
||||
int internal_resolution{Defaults::Video::INTERNAL_RESOLUTION}; // Multiplicador enter ≥ 1, clampat a max_zoom
|
||||
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
|
||||
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
|
||||
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
|
||||
};
|
||||
|
||||
// Opcions del render info
|
||||
@@ -62,13 +59,19 @@ namespace Options {
|
||||
Uint32 shadow_color{0xFF005A6B}; // Ombra daurada fosca (ABGR)
|
||||
};
|
||||
|
||||
// Opcions d'àudio
|
||||
// Opcions d'àudio (estructura compartida amb la resta de projectes)
|
||||
struct Music {
|
||||
bool enabled{Defaults::Audio::MUSIC_ENABLED};
|
||||
float volume{Defaults::Audio::MUSIC_VOLUME};
|
||||
};
|
||||
struct Sound {
|
||||
bool enabled{Defaults::Audio::SOUND_ENABLED};
|
||||
float volume{Defaults::Audio::SOUND_VOLUME};
|
||||
};
|
||||
struct Audio {
|
||||
Music music{};
|
||||
Sound sound{};
|
||||
bool enabled{Defaults::Audio::ENABLED}; // master enable
|
||||
bool music_enabled{Defaults::Audio::MUSIC_ENABLED};
|
||||
float music_volume{Defaults::Audio::MUSIC_VOLUME};
|
||||
bool sound_enabled{Defaults::Audio::SOUND_ENABLED};
|
||||
float sound_volume{Defaults::Audio::SOUND_VOLUME};
|
||||
float volume{Defaults::Audio::VOLUME};
|
||||
};
|
||||
|
||||
@@ -87,6 +90,7 @@ namespace Options {
|
||||
int diners_inicial{Defaults::Game::DINERS_INICIAL};
|
||||
bool use_new_logo{Defaults::Game::USE_NEW_LOGO};
|
||||
bool show_title_credits{Defaults::Game::SHOW_TITLE_CREDITS};
|
||||
bool show_preload{Defaults::Game::SHOW_PRELOAD};
|
||||
};
|
||||
|
||||
// Preset PostFX
|
||||
@@ -123,7 +127,6 @@ namespace Options {
|
||||
|
||||
// --- Variables globals ---
|
||||
inline std::string version{};
|
||||
inline KeysGUI keys_gui{};
|
||||
inline KeysGame keys_game{};
|
||||
inline Video video{};
|
||||
inline RenderInfo render_info{};
|
||||
@@ -141,11 +144,21 @@ namespace Options {
|
||||
inline std::string crtpi_file_path{};
|
||||
inline int current_crtpi_preset{0};
|
||||
|
||||
inline std::string debug_file_path{};
|
||||
|
||||
// --- API ---
|
||||
void setConfigFile(const std::string& path);
|
||||
auto loadFromFile() -> bool;
|
||||
auto saveToFile() -> bool;
|
||||
|
||||
// debug.yaml: estat inicial de gameplay per a tests ràpids
|
||||
// (`habitacio_inicial`, `piramide_inicial`, `vides`, `diamants_inicial`,
|
||||
// `diners_inicial`). Només es carrega/desa en builds de debug; en release
|
||||
// els camps queden als seus defaults.
|
||||
void setDebugFile(const std::string& path);
|
||||
auto loadDebugFromFile() -> bool;
|
||||
auto saveDebugToFile() -> bool;
|
||||
|
||||
void setPostFXFile(const std::string& path);
|
||||
auto loadPostFXFromFile() -> bool;
|
||||
|
||||
|
||||