Compare commits
54 Commits
2c833d086e
...
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 | |||
| 4e18f83ec5 | |||
| f9346add79 | |||
| b3ff620c81 | |||
| d343e719ca | |||
| e18b7321eb | |||
| 6125277d70 | |||
| 6063b1c606 | |||
| 829d7431c1 | |||
| 605c273173 | |||
| ad38fc09cf | |||
| 8720e775a0 | |||
| 2cb38ffb49 | |||
| d86cb21efa | |||
| 4436f7f569 | |||
| 1507a1c740 | |||
| 801a8ad1bd | |||
| 80fa7b46e7 | |||
| 7f85b50c63 |
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
|
||||||
aee.exe
|
aee.exe
|
||||||
.DS_Store
|
*.o
|
||||||
trick.ini
|
*.obj
|
||||||
.vscode/
|
*.exe
|
||||||
|
*.app
|
||||||
|
|
||||||
|
# --- Generated assets ---
|
||||||
|
resources.pack
|
||||||
data.jrf
|
data.jrf
|
||||||
build/
|
|
||||||
dist/
|
# --- Runtime / debug junk ---
|
||||||
|
trick.ini
|
||||||
|
*.log
|
||||||
|
*.dmp
|
||||||
|
|
||||||
|
# --- Editor / IDE ---
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# --- macOS ---
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
.fseventsd
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.VolumeIcon.icns
|
||||||
|
Icon?
|
||||||
|
|
||||||
|
# --- Windows ---
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
Desktop.ini
|
||||||
|
desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# --- Linux ---
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
|||||||
87
CHANGELOG.md
@@ -1,6 +1,88 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Tots els canvis de la reconstrucció moderna (C++/SDL3) d'**Aventures en Egipte**, des de l'inici del port fins a la v1.1.
|
Tots els canvis de la reconstrucció moderna (C++/SDL3) d'**Aventures en Egipte**.
|
||||||
|
|
||||||
|
## [1.2] — 2026-04-18
|
||||||
|
|
||||||
|
Versió de modernització profunda: desapareix el model *threads estil emulador* i tot el runtime passa a un sol fil tick-based compatible amb emscripten. Zero regressions de gameplay.
|
||||||
|
|
||||||
|
### Afegit
|
||||||
|
|
||||||
|
#### Arquitectura: capa `scenes::` tick-based
|
||||||
|
- Infraestructura `scenes::` ([source/scenes/](source/scenes/)): `Scene`, `SceneRegistry`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, helper `playMusic` (`4436f7f`)
|
||||||
|
- **MortScene** substitueix `doMort()` (`d86cb21`)
|
||||||
|
- **BannerScene** substitueix `doBanner()` per piràmides 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
|
## [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
|
||||||
|
|||||||
197
CLAUDE.md
@@ -24,26 +24,74 @@ The executable is output to the project root. The `data/` folder must be in the
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Golden Rule: Do Not Touch Gameplay
|
### New Rules (Modernization Phase)
|
||||||
|
|
||||||
The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only. Any new feature must be implemented as an overlay on top of the existing game, never by modifying original gameplay code.
|
The old "Golden Rule: Do Not Touch Gameplay" has been **revoked**. The original C-style code (jail engine + gameplay modules) is now a **modernization target**, not a sacred zone. The parallel-overlay approach has reached its ceiling: fades and cinematics are still blocking loops, audio relies on an async `SDL_AddTimer`, and the emulator-style game thread blocking at `publishFrame` is incompatible with an emscripten port.
|
||||||
|
|
||||||
|
The five current objectives are:
|
||||||
|
|
||||||
|
1. **Idiomatic C++**: RAII, `std::vector`/`std::string`/`std::optional`, classes with real constructors/destructors. No more raw `malloc/free` in structs.
|
||||||
|
2. **Zero blocking events**: no `while (...) { poll; }`, no `SDL_Delay` inside gameplay, no `cv.wait()` in `publishFrame`. Every subsystem must be able to advance in a single tick call.
|
||||||
|
3. **Time-based**: animations, cinematics and fades measured in milliseconds, not frames. `JG_ShouldUpdate()` as gameplay gate is on its way out.
|
||||||
|
4. **Overlay integrated**: overlay stops being a post-game layer painted by Director — it becomes part of the same render pass the game tick produces.
|
||||||
|
5. **SDL3 callbacks**: main loop handed over to `SDL_AppInit` / `SDL_AppIterate` / `SDL_AppEvent` / `SDL_AppQuit`, single-threaded, compatible with emscripten.
|
||||||
|
|
||||||
|
**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan.
|
||||||
|
|
||||||
|
### Migration Status (2026-04-16)
|
||||||
|
|
||||||
|
**Completat.** Totes les fases del pla original (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.
|
||||||
|
|
||||||
|
**Arquitectura actual**:
|
||||||
|
- Un sol thread (Director). Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit/Iterate/Event/Quit` a [main.cpp](source/main.cpp).
|
||||||
|
- `Director::iterate()` posseeix l'estat d'escena (`current_scene_`, `game_state_`) i fa input → tick de l'escena → `JD8_Flip` (sense yield, només converteix `screen` → `pixel_data`) → overlay → present. Tot en línia recta, zero fibers, zero mutex.
|
||||||
|
- Totes les escenes (inclòs `ModuleGame`) implementen `scenes::Scene` amb `onEnter/tick(delta_ms)/done/nextState`.
|
||||||
|
- `ModuleSequence` (el vell dispatcher) eliminat. Despatxa via `game_state_ == 0` (gameplay → `ModuleGame`) o `game_state_ == 1` (cinemàtica → `SceneRegistry::tryCreate(num_piramide)`).
|
||||||
|
|
||||||
|
**Escenes migrades** (totes registrades a `Director::init` via `SceneRegistry`):
|
||||||
|
- `MortScene` (state 100) · `BannerScene` (2..5) · `MenuScene` (0) · `SlidesScene` (1, 7)
|
||||||
|
- `CreditsScene` (8) · `SecretaScene` (6) · `IntroNewLogoScene` (255, `use_new_logo=true`)
|
||||||
|
- `IntroScene` (255, `use_new_logo=false`) · `IntroSpritesScene` (sub-escena de les dues intros)
|
||||||
|
|
||||||
|
**Files d'`Options::game` exposats per a tests ràpids** (persistits a `config.yaml`):
|
||||||
|
`piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits`.
|
||||||
|
|
||||||
|
**La capa `scenes::`** ([source/scenes/](source/scenes/)): `scene.hpp` (interfície), `scene_registry.hpp/.cpp`, `timeline`, `sprite_mover`, `frame_animator`, `palette_fade`, `surface_handle`, `scene_utils` (`playMusic`). Pures tick-based, zero while, zero `JG_ShouldUpdate`.
|
||||||
|
|
||||||
|
### Modernization Targets
|
||||||
|
|
||||||
|
**Invariants to preserve** (touch these and you broke the game):
|
||||||
|
- Gameplay feel, movement speed, enemy AI behavior
|
||||||
|
- Collision detection, scoring, lives, level progression
|
||||||
|
- Visible animation cadence (once translated to ms, must look identical)
|
||||||
|
- Difficulty curves and cinematic timings
|
||||||
|
- Cheat codes (`reviu`, `alone`, `obert`)
|
||||||
|
- Original palettes, fades, music cues
|
||||||
|
|
||||||
|
**Free to change** (internal representation):
|
||||||
|
- Data structures (structs → classes with RAII)
|
||||||
|
- Ownership (raw pointers → `std::unique_ptr`/`std::vector`/`std::string`)
|
||||||
|
- Timing representation (frame counters → ms accumulators)
|
||||||
|
- Threading model (game thread → single-threaded state machine)
|
||||||
|
- Global state (the old `info::` namespace is now an `inline` singleton `info::ctx` of type `GameContext`; access is `info::ctx.X` instead of `info::X`. Can be reset with `info::ctx.reset()`)
|
||||||
|
- API shapes of jail subsystems (as long as callers are updated consistently)
|
||||||
|
|
||||||
### Boundary: Original vs New Code
|
### Boundary: Original vs New Code
|
||||||
|
|
||||||
| Path | Owner | Rule |
|
| Path | Owner | Rule |
|
||||||
|------|-------|------|
|
|------|-------|------|
|
||||||
| `source/core/jail/` | Original engine | **Do not modify** gameplay behavior |
|
| `source/core/jail/` | Legacy engine, modernization target | Free to modify with care — preserve external behavior |
|
||||||
| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** |
|
| `source/game/*.cpp/hpp` | Legacy gameplay, modernization target | Free to modify with care — preserve gameplay invariants |
|
||||||
| `source/core/rendering/` | New presentation layer | Free to modify |
|
| `source/core/rendering/` | New presentation layer | Free to modify |
|
||||||
| `source/core/input/` | New input layer | Free to modify |
|
| `source/core/input/` | New input layer | Free to modify |
|
||||||
| `source/utils/` | New utilities | Free to modify |
|
| `source/utils/` | New utilities | Free to modify |
|
||||||
| `source/game/options,defines,defaults` | New config system | Free to modify |
|
| `source/game/options,defines,defaults` | New config system | Free to modify |
|
||||||
| `data/*.gif, *.ogg` | Original assets | **Do not modify** |
|
| `data/gfx/, data/music/` | Original assets | **Do not modify** — assets remain untouchable |
|
||||||
| `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify |
|
| `data/fonts/, data/shaders/, data/locale/` | New assets | Free to modify |
|
||||||
|
|
||||||
### Original "Jail" Engine (`source/core/jail/`)
|
### Legacy "Jail" Engine (`source/core/jail/`) — modernization target
|
||||||
|
|
||||||
Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay logic.**
|
Flat C-style APIs (no classes), prefixed by subsystem. Being progressively converted to idiomatic C++ (see Phase 1 of the plan). External API names are kept stable during the transition to avoid churning call sites.
|
||||||
|
|
||||||
- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()`
|
- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()`
|
||||||
- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()`
|
- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()`
|
||||||
@@ -53,7 +101,7 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
|
|||||||
|
|
||||||
### System Layer (`source/core/system/`)
|
### System Layer (`source/core/system/`)
|
||||||
|
|
||||||
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()` → `Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top
|
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**, únic thread del runtime. Posseeix l'estat d'escena (`current_scene_: unique_ptr<Scene>`, `game_state_`, `last_tick_ms_`) directament com a members. `iterate()` fa: poll events (via `SDL_AppEvent`) → input (Gamepad/KeyRemap/GlobalInputs/Mouse) → `JA_Update` → transició d'escena si `done()` → `scene->tick(delta_ms)` → `JD8_Flip` (converteix `screen` → `pixel_data`) → overlay → present → `SDL_Delay` al frame target. Dispatcher: `game_state_ == 0` → `new ModuleGame`, `game_state_ == 1` → `SceneRegistry::tryCreate(info::ctx.num_piramide)` (amb redirect `num_piramide == 6 && diners < 200 → 7` replicant el vell `ModuleSequence::Go`).
|
||||||
|
|
||||||
### Presentation Layer (`source/core/rendering/`)
|
### Presentation Layer (`source/core/rendering/`)
|
||||||
|
|
||||||
@@ -65,9 +113,10 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
|
|||||||
|
|
||||||
### Input Layer (`source/core/input/`)
|
### Input Layer (`source/core/input/`)
|
||||||
|
|
||||||
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
|
- **KeyConfig** (`key_config.hpp/cpp`) — **Font única de veritat per a les tecles d'UI/sistema**. Carrega `data/input/keys.yaml` al boot (12 entrades: F1-F10 GlobalInputs + F11 pausa + F12 menú de servei) i opcionalment aplica overrides des de `~/.config/jailgames/aee/keys.yaml`. Exposa `KeyConfig::scancode("id")`, `scancodePtr("id")` (per a Menu KeyBind), `setScancode(...)`, `isGuiKey(sc)` (filtre del Director per a no propagar tecles d'UI a `JI_AnyKey`). `saveOverrides()` només persistix les entrades que difereixen del default. Les tecles de moviment del jugador NO viuen ací — es queden a `Options::keys_game`
|
||||||
|
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps function keys (via `KeyConfig::scancode("dec_zoom")`, etc.) to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
|
||||||
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
|
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
|
||||||
- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement; A/B buttons, Start, Back translate to synthetic SDL key events (F12/ESC/Enter/Backspace) when menu is open, so Director handles them exactly like keyboard. Loads extra mappings from `gamecontrollerdb.txt` (next to the executable) at init via `SDL_AddGamepadMappingsFromFile`, extending SDL's built-in controller database
|
- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug + overlay notification with controller name. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement. Mapeig: SOUTH/EAST/WEST/NORTH (4 botons frontals) → Enter sintètic per avançar escenes; al menú EAST=accept, SOUTH=cancel/back. SELECT → menu_toggle (servei), START → pause_toggle (via `KeyConfig::scancode(...)`). Loads extra mappings from `gamecontrollerdb.txt` at init via `SDL_AddGamepadMappingsFromFile`
|
||||||
- **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp`
|
- **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp`
|
||||||
|
|
||||||
### Locale Layer (`source/core/locale/`)
|
### Locale Layer (`source/core/locale/`)
|
||||||
@@ -79,8 +128,8 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
|
|||||||
Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
||||||
|
|
||||||
- **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
|
- **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
|
||||||
- **defaults.hpp** — Default values: `Defaults::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`
|
- **defaults.hpp** — Default values: `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`. (Les tecles d'UI viuen a `data/input/keys.yaml` via `KeyConfig`)
|
||||||
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
|
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
|
||||||
|
|
||||||
### Utilities (`source/utils/`)
|
### Utilities (`source/utils/`)
|
||||||
|
|
||||||
@@ -99,40 +148,58 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
|||||||
| F6 | Toggle supersampling |
|
| F6 | Toggle supersampling |
|
||||||
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
|
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
|
||||||
| F8 | Cycle shader presets |
|
| F8 | Cycle shader presets |
|
||||||
| F9 | Toggle stretch filter (nearest ↔ linear) |
|
| F9 | Cycle texture filter (nearest ↔ linear) — sempre aplicat, independent de 4:3 |
|
||||||
| F10 | Cycle render info (off → top → bottom → off) |
|
| F10 | Cycle render info (off → top → bottom → off) |
|
||||||
| F11 | Toggle pause (blocks game thread at publishFrame + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
| F11 | Toggle pause (Director skips `scene->tick()` + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
||||||
| F12 | Toggle floating options menu |
|
| F12 | Toggle floating options menu |
|
||||||
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
||||||
| Backspace | Go up one menu level / close menu if at root |
|
| Backspace | Go up one menu level / close menu if at root |
|
||||||
| ↑↓←→ / Enter | Menu navigation |
|
| ↑↓←→ / Enter | Menu navigation |
|
||||||
|
|
||||||
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.
|
UI/system key bindings are loaded from [data/input/keys.yaml](data/input/keys.yaml) via `KeyConfig`. Overrides fets des del menú es persistixen a `~/.config/jailgames/aee/keys.yaml` (només les que difereixen del default). Game movement keys (`Options::keys_game.up/down/left/right`) viuen separadament a `config.yaml` (secció `controls:`) i es remapejen via la CONTROLS submenu — el `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.
|
||||||
|
|
||||||
### Threading Model (Emulator Architecture)
|
### Execution Model (Single-threaded, Scene-based)
|
||||||
|
|
||||||
|
Zero threads, zero fibers, zero mutex. Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`) a [main.cpp](source/main.cpp). Cada frame entra pel `SDL_AppIterate` → `Director::iterate()`:
|
||||||
|
|
||||||
```
|
```
|
||||||
Main thread (Director) Game thread (ModuleGame/Sequence::Go())
|
SDL_AppIterate → Director::iterate() {
|
||||||
──────────────────── ────────────────────────────────────
|
if (quit_requested_) { scene.reset(); return false; }
|
||||||
loop at ~60 FPS { loop {
|
if (!context_initialized_) initGameContext();
|
||||||
SDL_PollEvent() ... game logic ...
|
|
||||||
GlobalInputs, Mouse JD8_Flip():
|
Gamepad/KeyRemap/GlobalInputs/Mouse::update
|
||||||
if new_frame_available: palette→ARGB in pixel_data
|
JA_Update() ← audio pump
|
||||||
copy to game_frame publishFrame(pixel_data) ⏸
|
|
||||||
signal → ────────────────────→ (blocks until Director consumes)
|
if (!paused_) {
|
||||||
copy game_frame → present_buffer ←──── signal_consumed
|
if (scene && (scene->done() || JG_Quitting()))
|
||||||
Overlay::render(present_buffer) continue game loop
|
game_state_ = scene->nextState(); scene.reset();
|
||||||
Screen::present(present_buffer) }
|
if (!scene) {
|
||||||
SDL_Delay to hit 60fps
|
if (game_state_ == -1 || JG_Quitting()) return false;
|
||||||
|
scene = createNextScene(); ← ModuleGame o registry.tryCreate()
|
||||||
|
scene->onEnter();
|
||||||
|
}
|
||||||
|
JI_Update()
|
||||||
|
scene->tick(now - last_tick_ms_)
|
||||||
|
JD8_Flip() ← converteix screen indexat → pixel_data
|
||||||
|
memcpy pixel_data → game_frame
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy game_frame → presentation_buffer
|
||||||
|
Overlay::render(presentation_buffer)
|
||||||
|
Screen::present(presentation_buffer)
|
||||||
|
SDL_Delay(frame_target - elapsed)
|
||||||
}
|
}
|
||||||
|
SDL_AppEvent → Director::handleEvent() ← events lliurats un a un per SDL
|
||||||
|
SDL_AppQuit → Director::teardown() ← Options::save + descàrrega ordenada
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key points:**
|
**Key points:**
|
||||||
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay
|
- `Director` posseeix `current_scene_`, `game_state_`, `last_tick_ms_`, `context_initialized_` com a members — abans vivien al stack del fiber.
|
||||||
- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame)
|
- `JD8_Flip()` només converteix paleta + `screen` a ARGB (`pixel_data`). Ja **no fa yield** — tot corre lineal.
|
||||||
- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point
|
- `JG_ShouldUpdate()` encara existeix a `jgame.cpp` com a timing-gate per a `ModuleGame::Update()` (10 ms fix), però ja no fa yield. Cap caller fa spin-wait.
|
||||||
- SDL events processed ONLY on main thread (SDL requirement)
|
- Pausa (F11) simplement salta el bloc de tick; overlay i present continuen, es re-presenta l'últim frame congelat.
|
||||||
- `JI_Update()` no longer polls events — reads Director's state
|
- Doble buffer (`game_frame` + `presentation_buffer`) es manté perquè el Director pot presentar múltiples frames per cada tick durant pausa; el cost (256 KB memcpy) és trivial a 320×200.
|
||||||
|
- SDL3 Callback API compatible amb emscripten: el navegador posseeix el main loop i ens crida via `requestAnimationFrame`. Zero canvis de codi per a portabilitat.
|
||||||
|
|
||||||
### Rendering Pipeline (inside Screen::present)
|
### Rendering Pipeline (inside Screen::present)
|
||||||
|
|
||||||
@@ -158,10 +225,40 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
|||||||
|
|
||||||
| File | Content |
|
| File | Content |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (movement keys + menu_toggle + pause_toggle) |
|
| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `scaling_mode`, `texture_filter`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (només moviment del jugador) |
|
||||||
|
| `~/.config/jailgames/aee/keys.yaml` | UI key overrides (només entrades que difereixen del default de [data/input/keys.yaml](data/input/keys.yaml)). Generat per `KeyConfig::saveOverrides()` |
|
||||||
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
|
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
|
||||||
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |
|
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |
|
||||||
|
|
||||||
|
### Resource Pack (`source/core/resources/`)
|
||||||
|
|
||||||
|
Sistema d'empaquetat d'assets a l'estil `coffee_crisis_arcade_edition`. Genera un sol fitxer binari opac `resources.pack` que substitueix la carpeta `data/` als releases natius.
|
||||||
|
|
||||||
|
**Format AEE1** (fidel a CCAE amb clau pròpia):
|
||||||
|
```
|
||||||
|
Header: "AEE1" (4B) + version uint32 + resource_count uint32
|
||||||
|
Index: per recurs → filename_len uint32 + filename + offset uint64 + size uint64 + checksum uint32
|
||||||
|
Payload: data_size uint64 + bytes XOR-xifrats amb "AEE_RESOURCES__2026"
|
||||||
|
```
|
||||||
|
Checksum: djb2-like amb seed `0x12345678`. Càrrega full-to-RAM (sense mmap).
|
||||||
|
|
||||||
|
**Fitxers**:
|
||||||
|
- [source/core/resources/resource_pack.hpp/cpp](source/core/resources/) — classe `ResourcePack`: `loadPack`, `savePack`, `addFile`, `addDirectory`, `getResource(name) → std::vector<uint8_t>`, `hasResource`
|
||||||
|
- [source/core/resources/resource_helper.hpp/cpp](source/core/resources/) — namespace `ResourceHelper`: `initializeResourceSystem(pack, enable_fallback)`, `loadFile(relative_path)`, `shutdownResourceSystem`. Prova el pack primer, cau a `file_getresourcefolder()+path` si el fallback està actiu.
|
||||||
|
- [tools/pack_resources/pack_resources.cpp](tools/pack_resources/pack_resources.cpp) — eina standalone CLI: `pack_resources [input_dir=data] [output=resources.pack]` + `--list pack`.
|
||||||
|
|
||||||
|
**Build**:
|
||||||
|
- `make pack` compila l'eina (target `pack_resources` a `EXCLUDE_FROM_ALL` de [CMakeLists.txt](CMakeLists.txt)) i genera `resources.pack` a la rel. 33 entrades ≈ 4 MB.
|
||||||
|
- `./build/pack_resources --list resources.pack` inspecciona el pack.
|
||||||
|
|
||||||
|
**Estat actual (Fases 1-6 completades, 2026-04-16)**:
|
||||||
|
- `ResourcePack` + `ResourceHelper` + eina `pack_resources` compilen i funcionen. El pack genera 33 entrades ≈ 4 MB.
|
||||||
|
- Cablejat al joc via `ResourceHelper::initializeResourceSystem` a [main.cpp](source/main.cpp) (amb `return SDL_APP_FAILURE` si falla), i `shutdownResourceSystem` a `SDL_AppQuit`.
|
||||||
|
- Tots els callsites de recursos usen `ResourceHelper::loadFile` (`std::vector<uint8_t>`): [locale.cpp](source/core/locale/locale.cpp), [text.cpp](source/core/rendering/text.cpp), [scene_utils.cpp](source/scenes/scene_utils.cpp), [modulegame.cpp](source/game/modulegame.cpp), [jdraw8.cpp](source/core/jail/jdraw8.cpp).
|
||||||
|
- Scaffold `.jrf` eliminat de [jfile.cpp](source/core/jail/jfile.cpp): `file_setresourcefilename`, `file_setsource`, `SOURCE_FILE`/`SOURCE_FOLDER`, `dictionary_loaded`, `file_getfilepointer`, `file_readfile`. Només queden config-folder i resource-folder getters/setters.
|
||||||
|
- Targets release a [Makefile](Makefile) (`_linux_release`/`_windows_release`/`_macos_release`) depenen de `pack` i copien `resources.pack` en lloc de `data/`. WASM intacte (`--preload-file data@/data`).
|
||||||
|
- `enable_fallback = false` a Release natiu (`NDEBUG && !__EMSCRIPTEN__`): el pack és obligatori. Debug i WASM mantenen el fallback actiu.
|
||||||
|
|
||||||
### External Libraries (`source/external/`)
|
### External Libraries (`source/external/`)
|
||||||
|
|
||||||
- `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
|
- `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
|
||||||
@@ -170,20 +267,41 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
|||||||
|
|
||||||
### Data Assets (`data/`)
|
### Data Assets (`data/`)
|
||||||
|
|
||||||
- `*.gif`, `*.ogg` — Original game assets (**do not modify**)
|
- `gfx/` — Original game GIFs (**do not modify content**): `frames.gif`/`frames2.gif` (sprite sheet del joc), `logo.gif`/`logo_new.gif` (intros), `menu.gif`/`menu2.gif`, `intro.gif`/`intro2.gif`/`intro3.gif` (slides), `ffase.gif` (banner nivells), `final.gif`/`finals.gif` (crèdits), `gameover.gif`, `tomba1.gif`/`tomba2.gif` (escena secreta)
|
||||||
|
- `music/` — 8 pistes OGG originals amb noms temàtics: `mort.ogg` (game over), `secreta.ogg` (escena secreta + piràmide 6), `menu.ogg` (menú + intros), `banner.ogg` (banner de fase), `final.ogg` (slides finals + crèdits), `piramide_1_4_5.ogg` (gameplay default), `piramide_2.ogg`, `piramide_3.ogg`
|
||||||
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
|
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
|
||||||
- `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
|
- `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
|
||||||
- `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart
|
- `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart
|
||||||
- `ui/` — Reserved for future UI graphics
|
|
||||||
|
|
||||||
### Known Issues & Technical Debt
|
### Known Issues & Technical Debt
|
||||||
|
|
||||||
1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
|
1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
|
||||||
2. **Cheats are broken (`reviu`, `alone`, `obert`)**: `JI_CheatActivated` in [jinput.cpp:46](source/core/jail/jinput.cpp#L46) compares `SDL_Scancode` values (e.g. `SDL_SCANCODE_R`=21) against ASCII chars (`'r'`=114). They never match. Regression from SDL3 migration. Fix requires either scancode→char conversion in `JI_moveCheats` or storing chars directly.
|
2. ~~**Cheats are broken (`reviu`, `alone`, `obert`)**~~: Fixed. `JI_moveCheats` tradueix `SDL_Scancode` → ASCII via `scancode_to_ascii` abans de ficar-los al buffer ([jinput.cpp:32-37, 55-61](source/core/jail/jinput.cpp#L32-L61)), i `JI_CheatActivated` compara ASCII amb ASCII.
|
||||||
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
|
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
|
||||||
|
4. ~~**Raw `malloc`/`free` in gameplay structs**~~: Majoritàriament arreglat. `Sprite`/`Entitat` usen `std::vector<Frame>` i `std::vector<Animacio>` ([sprite.hpp](source/game/sprite.hpp)). `jfile.cpp` ja no té el global `scratch[255]` (substituït per `thread_local std::string`). L'API `file_getfilebuffer` (que tornava raw `char*` amb `malloc`) s'ha substituït per `file_readfile` que retorna `std::vector<char>` RAII — elimina els leaks silenciosos que hi havia a `JD8_LoadPalette`, `ModuleGame::Go()` i `scenes::playMusic`. Queda `jail_audio.hpp` barrejant `new`/`malloc`/`SDL_malloc` de forma pairada i correcta (no leak), pendent de polir amb `std::unique_ptr` quan toque.
|
||||||
|
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. Migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 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
|
### Pending / Ideas for Later
|
||||||
|
|
||||||
|
- **Sound effects**: infraestructura `JA_PlaySound*`/`JA_LoadSound` ja preparada. Cablejar events de gameplay (col·lisió momia, mort, recollir objecte, trencar tomba, cheat activat). Menus SONS/VOL SONS ja controlen el volum del pool.
|
||||||
|
- **IDBFS persistence a WASM**: montar `/home/web_user/.config` com a IDBFS a l'init i `FS.syncfs` després de cada save. Opcional — ara per ara la config no persistix entre recàrregues de pàgina.
|
||||||
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
|
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
|
||||||
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
|
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
|
||||||
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
|
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
|
||||||
@@ -191,6 +309,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
|||||||
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
|
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
|
||||||
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
|
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
|
||||||
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
|
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
|
||||||
|
- **jail_audio `JA_Sound_t` RAII**: `JA_Music_t` ja està net (vector + string), però `JA_Sound_t` encara usa `Uint8*` via `SDL_LoadWAV` out-param. Petit polish per a completar la coherència RAII.
|
||||||
|
|
||||||
### Previously Fixed (kept for reference)
|
### Previously Fixed (kept for reference)
|
||||||
|
|
||||||
@@ -212,4 +331,4 @@ Director loop uses `FRAME_MS_VSYNC = 16` (60 FPS) or `FRAME_MS_NO_VSYNC = 4` (~2
|
|||||||
|
|
||||||
Init order: `file_setconfigfolder` → `Options::load` → `Locale::load("locale/ca.yaml")` → `Options::loadPostFX/CrtPi` → `JG_Init` → `Screen::init` → `JD8_Init` → `JA_Init` → `Options::applyAudio()` → `Overlay::init` → `Menu::init` → `Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save` → `Director::destroy` → `Menu::destroy` → `Overlay::destroy` → `JA_Quit` → `JD8_Quit` → `Screen::destroy` → `JG_Finalize`.
|
Init order: `file_setconfigfolder` → `Options::load` → `Locale::load("locale/ca.yaml")` → `Options::loadPostFX/CrtPi` → `JG_Init` → `Screen::init` → `JD8_Init` → `JA_Init` → `Options::applyAudio()` → `Overlay::init` → `Menu::init` → `Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save` → `Director::destroy` → `Menu::destroy` → `Overlay::destroy` → `JA_Quit` → `JD8_Quit` → `Screen::destroy` → `JG_Finalize`.
|
||||||
|
|
||||||
The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) now lives inside `Director::gameThreadFunc()`, running on the game thread.
|
The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) lives inside `gameFiberEntry()` in an anonymous namespace in [director.cpp](source/core/system/director.cpp), invoked as the entry of the cooperative fiber (single-threaded).
|
||||||
|
|||||||
215
CMakeLists.txt
@@ -3,6 +3,11 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(aee VERSION 1.00)
|
project(aee VERSION 1.00)
|
||||||
|
|
||||||
|
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Estándar de C++
|
# Estándar de C++
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
@@ -10,18 +15,50 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
|
|||||||
# Exportar comandos de compilación para herramientas de análisis
|
# Exportar comandos de compilación para herramientas de análisis
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# --- GENERACIÓ AUTOMÀTICA DE VERSIÓ ---
|
||||||
|
# Si GIT_HASH ve de fora (p. ex. el Makefile via -DGIT_HASH=xxx), l'usem tal
|
||||||
|
# qual. Això evita problemes amb Docker/emscripten on git avorta per
|
||||||
|
# "dubious ownership" al volum muntat. En builds locals sense -DGIT_HASH
|
||||||
|
# resolem ací executant git directament.
|
||||||
|
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND)
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||||
|
set(GIT_HASH "unknown")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
|
||||||
|
|
||||||
# --- LISTA EXPLÍCITA DE FUENTES ---
|
# --- LISTA EXPLÍCITA DE FUENTES ---
|
||||||
set(APP_SOURCES
|
set(APP_SOURCES
|
||||||
# Core - Motor original "Jail" (no tocar gameplay)
|
# Core - Motor original "Jail" (no tocar gameplay)
|
||||||
source/core/jail/jail_audio.cpp
|
|
||||||
source/core/jail/jdraw8.cpp
|
source/core/jail/jdraw8.cpp
|
||||||
source/core/jail/jfile.cpp
|
source/core/jail/jfile.cpp
|
||||||
source/core/jail/jgame.cpp
|
source/core/jail/jgame.cpp
|
||||||
source/core/jail/jinput.cpp
|
source/core/jail/jinput.cpp
|
||||||
|
|
||||||
|
# Core - Audio (wrapper canònic compartit amb la resta de projectes)
|
||||||
|
source/core/audio/audio.cpp
|
||||||
|
source/core/audio/audio_adapter.cpp
|
||||||
|
|
||||||
# Core - Locale (nova capa)
|
# Core - Locale (nova capa)
|
||||||
source/core/locale/locale.cpp
|
source/core/locale/locale.cpp
|
||||||
|
|
||||||
|
# Core - Resources (pack binari AEE1 + cache d'assets precarregats)
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_list.cpp
|
||||||
|
source/core/resources/resource_cache.cpp
|
||||||
|
|
||||||
# Core - Capa de presentación (nueva)
|
# Core - Capa de presentación (nueva)
|
||||||
source/core/rendering/menu.cpp
|
source/core/rendering/menu.cpp
|
||||||
source/core/rendering/overlay.cpp
|
source/core/rendering/overlay.cpp
|
||||||
@@ -34,12 +71,32 @@ set(APP_SOURCES
|
|||||||
# Core - Input (nova capa)
|
# Core - Input (nova capa)
|
||||||
source/core/input/gamepad.cpp
|
source/core/input/gamepad.cpp
|
||||||
source/core/input/global_inputs.cpp
|
source/core/input/global_inputs.cpp
|
||||||
|
source/core/input/key_config.cpp
|
||||||
source/core/input/key_remap.cpp
|
source/core/input/key_remap.cpp
|
||||||
source/core/input/mouse.cpp
|
source/core/input/mouse.cpp
|
||||||
|
|
||||||
# Core - System (nova capa)
|
# Core - System (nova capa)
|
||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
|
|
||||||
|
# Scenes (cinemàtiques i menús reescrits)
|
||||||
|
source/scenes/timeline.cpp
|
||||||
|
source/scenes/sprite_mover.cpp
|
||||||
|
source/scenes/frame_animator.cpp
|
||||||
|
source/scenes/palette_fade.cpp
|
||||||
|
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
|
||||||
|
source/scenes/intro_new_logo_scene.cpp
|
||||||
|
source/scenes/intro_scene.cpp
|
||||||
|
source/scenes/intro_sprites_scene.cpp
|
||||||
|
source/scenes/slides_scene.cpp
|
||||||
|
source/scenes/credits_scene.cpp
|
||||||
|
source/scenes/secreta_scene.cpp
|
||||||
|
|
||||||
# Game
|
# Game
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/bola.cpp
|
source/game/bola.cpp
|
||||||
@@ -48,7 +105,6 @@ set(APP_SOURCES
|
|||||||
source/game/mapa.cpp
|
source/game/mapa.cpp
|
||||||
source/game/marcador.cpp
|
source/game/marcador.cpp
|
||||||
source/game/modulegame.cpp
|
source/game/modulegame.cpp
|
||||||
source/game/modulesequence.cpp
|
|
||||||
source/game/momia.cpp
|
source/game/momia.cpp
|
||||||
source/game/prota.cpp
|
source/game/prota.cpp
|
||||||
source/game/sprite.cpp
|
source/game/sprite.cpp
|
||||||
@@ -63,8 +119,22 @@ set(APP_SOURCES
|
|||||||
|
|
||||||
# Configuración de SDL3
|
# Configuración de SDL3
|
||||||
# En macOS bundle mode usamos el xcframework (universal arm64+x86_64).
|
# En macOS bundle mode usamos el xcframework (universal arm64+x86_64).
|
||||||
# En el resto de casos, o en macOS sin bundle, usamos SDL3 del sistema via find_package.
|
# En emscripten compilamos SDL3 desde source con FetchContent (no hi ha paquet de sistema).
|
||||||
if(APPLE AND MACOS_BUNDLE)
|
# En el resto de casos, usamos SDL3 del sistema via find_package.
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
SDL3
|
||||||
|
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
|
||||||
|
GIT_TAG release-3.4.4
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_MakeAvailable(SDL3)
|
||||||
|
message(STATUS "SDL3: compilat des de source per a Emscripten (FetchContent)")
|
||||||
|
elseif(APPLE AND MACOS_BUNDLE)
|
||||||
set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64")
|
set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64")
|
||||||
message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})")
|
message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})")
|
||||||
else()
|
else()
|
||||||
@@ -72,12 +142,12 @@ else()
|
|||||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal) ---
|
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal, Emscripten no suporta SDL3 GPU) ---
|
||||||
if(NOT APPLE)
|
if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||||
find_program(GLSLC_EXE NAMES glslc)
|
find_program(GLSLC_EXE NAMES glslc)
|
||||||
|
|
||||||
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
|
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
|
||||||
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu")
|
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
|
||||||
|
|
||||||
set(ALL_SHADER_HEADERS
|
set(ALL_SHADER_HEADERS
|
||||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||||
@@ -120,21 +190,32 @@ if(NOT APPLE)
|
|||||||
endforeach()
|
endforeach()
|
||||||
message(STATUS "glslc no trobat — usant headers SPIR-V precompilats")
|
message(STATUS "glslc no trobat — usant headers SPIR-V precompilats")
|
||||||
endif()
|
endif()
|
||||||
|
elseif(EMSCRIPTEN)
|
||||||
|
message(STATUS "Emscripten: shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2)")
|
||||||
else()
|
else()
|
||||||
message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)")
|
message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# --- EJECUTABLE ---
|
# --- EJECUTABLE ---
|
||||||
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
# A emscripten excloïm sdl3gpu_shader.cpp — SDL3 GPU no suporta WebGL2, i el
|
||||||
|
# fallback SDL_Renderer de Screen (amb NO_SHADERS) fa tota la presentació.
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
set(APP_SOURCES_WASM ${APP_SOURCES})
|
||||||
|
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
|
||||||
|
else()
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
# Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc)
|
# Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc)
|
||||||
if(NOT APPLE AND GLSLC_EXE)
|
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
|
||||||
add_dependencies(${PROJECT_NAME} shaders)
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# --- DIRECTORIOS DE INCLUSIÓN ---
|
# --- DIRECTORIOS DE INCLUSIÓN ---
|
||||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||||
"${CMAKE_SOURCE_DIR}/source"
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enlazar SDL3
|
# Enlazar SDL3
|
||||||
@@ -159,15 +240,72 @@ target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunctio
|
|||||||
# --- CONFIGURACIÓN POR PLATAFORMA ---
|
# --- CONFIGURACIÓN POR PLATAFORMA ---
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
|
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
|
||||||
|
elseif(EMSCRIPTEN)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
|
||||||
|
# -fexceptions: SDL3 i fkyaml llancen std::exception; sense això, `throw`
|
||||||
|
# acaba en `abort()`. També requerit al link per congruència ABI.
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE
|
||||||
|
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
|
||||||
|
-fexceptions
|
||||||
|
-sALLOW_MEMORY_GROWTH=1
|
||||||
|
-sMAX_WEBGL_VERSION=2
|
||||||
|
-sINITIAL_MEMORY=67108864
|
||||||
|
-sASSERTIONS=1
|
||||||
|
# ASYNCIFY permet que Emscripten gestione yields durant la precarga
|
||||||
|
# d'assets. El main loop del joc ja usa SDL3 Callback API i no depén
|
||||||
|
# d'Asyncify — però el preloader del `.data` sí.
|
||||||
|
-sASYNCIFY=1
|
||||||
|
)
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Ejecutable en la raíz del proyecto
|
# Ejecutable en la raíz del proyecto (solo nativos). A Emscripten queda dins build/.
|
||||||
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
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(CLANG_FORMAT_EXE NAMES clang-format)
|
||||||
|
find_program(CPPCHECK_EXE NAMES cppcheck)
|
||||||
|
|
||||||
# Recopilar todos los archivos fuente para formateo (excluir external/)
|
# Recopilar todos los archivos fuente (excluir external/)
|
||||||
file(GLOB_RECURSE ALL_SOURCE_FILES
|
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||||
"${CMAKE_SOURCE_DIR}/source/*.cpp"
|
"${CMAKE_SOURCE_DIR}/source/*.cpp"
|
||||||
"${CMAKE_SOURCE_DIR}/source/*.hpp"
|
"${CMAKE_SOURCE_DIR}/source/*.hpp"
|
||||||
@@ -175,6 +313,35 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
|
|||||||
)
|
)
|
||||||
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
|
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
|
||||||
|
|
||||||
|
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
||||||
|
|
||||||
|
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
|
||||||
|
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
|
||||||
|
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
|
||||||
|
|
||||||
|
# Targets de clang-tidy
|
||||||
|
if(CLANG_TIDY_EXE)
|
||||||
|
add_custom_target(tidy
|
||||||
|
COMMAND ${CLANG_TIDY_EXE}
|
||||||
|
-p ${CMAKE_BINARY_DIR}
|
||||||
|
${CLANG_TIDY_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running clang-tidy..."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(tidy-fix
|
||||||
|
COMMAND ${CLANG_TIDY_EXE}
|
||||||
|
-p ${CMAKE_BINARY_DIR}
|
||||||
|
--fix
|
||||||
|
${CLANG_TIDY_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running clang-tidy with fixes..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "clang-tidy no encontrado - targets 'tidy' y 'tidy-fix' no disponibles")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Targets de clang-format
|
||||||
if(CLANG_FORMAT_EXE)
|
if(CLANG_FORMAT_EXE)
|
||||||
add_custom_target(format
|
add_custom_target(format
|
||||||
COMMAND ${CLANG_FORMAT_EXE}
|
COMMAND ${CLANG_FORMAT_EXE}
|
||||||
@@ -195,3 +362,25 @@ if(CLANG_FORMAT_EXE)
|
|||||||
else()
|
else()
|
||||||
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Target de cppcheck
|
||||||
|
if(CPPCHECK_EXE)
|
||||||
|
add_custom_target(cppcheck
|
||||||
|
COMMAND ${CPPCHECK_EXE}
|
||||||
|
--enable=warning,style,performance,portability
|
||||||
|
--std=c++20
|
||||||
|
--language=c++
|
||||||
|
--inline-suppr
|
||||||
|
--suppress=missingIncludeSystem
|
||||||
|
--suppress=toomanyconfigs
|
||||||
|
--suppress=*:*/source/external/*
|
||||||
|
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
|
||||||
|
--quiet
|
||||||
|
-I ${CMAKE_SOURCE_DIR}/source
|
||||||
|
${CPPCHECK_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running cppcheck..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
|
||||||
|
endif()
|
||||||
|
|||||||
289
Makefile
@@ -4,6 +4,18 @@
|
|||||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
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
|
# TARGET NAMES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -18,9 +30,23 @@ RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
|||||||
# VERSION (extracted from defines.hpp)
|
# VERSION (extracted from defines.hpp)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
VERSION := v$(shell powershell -Command "(Select-String -Path 'source/game/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
|
VERSION := $(shell powershell -Command "(Select-String -Path 'source/game/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
|
||||||
else
|
else
|
||||||
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
VERSION := $(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||||
|
endif
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
|
||||||
|
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
|
||||||
|
# "dubious ownership" del volum muntat.
|
||||||
|
# ==============================================================================
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
|
||||||
|
else
|
||||||
|
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
|
||||||
|
endif
|
||||||
|
ifeq ($(GIT_HASH),)
|
||||||
|
GIT_HASH := unknown
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -37,9 +63,13 @@ endif
|
|||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
||||||
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||||
|
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
|
||||||
|
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
|
||||||
|
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
|
||||||
else
|
else
|
||||||
WIN_TARGET_FILE := $(TARGET_FILE)
|
WIN_TARGET_FILE := $(TARGET_FILE)
|
||||||
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
||||||
|
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -65,17 +95,42 @@ else
|
|||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
endif
|
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
|
# COMPILACIÓN CON CMAKE
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
all:
|
all:
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 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)
|
# RELEASE AUTOMÁTICO (detecta SO)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -93,12 +148,12 @@ endif
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
_windows_release:
|
_windows_release: pack
|
||||||
@echo off
|
@echo off
|
||||||
@echo Creando release para Windows - Version: $(VERSION)
|
@echo Creando release para Windows - Version: $(VERSION)
|
||||||
|
|
||||||
# Compila con cmake
|
# Compila con cmake
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
|
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
|
||||||
@@ -106,13 +161,13 @@ _windows_release:
|
|||||||
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
|
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
|
||||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||||
|
|
||||||
# Copia ficheros
|
# Copia ficheros (resources.pack substitueix la carpeta data/)
|
||||||
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse"
|
@powershell -Command "Copy-Item 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE).exe'"
|
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
|
||||||
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||||
|
|
||||||
# Crea el fichero .zip
|
# Crea el fichero .zip
|
||||||
@@ -126,15 +181,31 @@ _windows_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
_macos_release:
|
_macos_release: pack
|
||||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||||
|
|
||||||
# Verificar e instalar create-dmg si es necesario
|
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||||
|
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||||
# Compila la versión para procesadores Intel con cmake
|
echo ""; \
|
||||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
|
echo "============================================"; \
|
||||||
@cmake --build build/intel
|
echo " Falta la dependencia: create-dmg"; \
|
||||||
|
echo "============================================"; \
|
||||||
|
if command -v brew >/dev/null 2>&1; then \
|
||||||
|
echo " Instalando con: brew install create-dmg"; \
|
||||||
|
brew install create-dmg || { \
|
||||||
|
echo ""; \
|
||||||
|
echo " ERROR: 'brew install create-dmg' ha fallado."; \
|
||||||
|
echo " Ejecuta el comando manualmente y vuelve a probar."; \
|
||||||
|
exit 1; \
|
||||||
|
}; \
|
||||||
|
else \
|
||||||
|
echo " Homebrew no está instalado."; \
|
||||||
|
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
|
||||||
|
echo " brew install create-dmg"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
}
|
||||||
|
|
||||||
# Elimina datos de compilaciones anteriores
|
# Elimina datos de compilaciones anteriores
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
@@ -148,8 +219,8 @@ _macos_release:
|
|||||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
|
||||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
|
||||||
# Copia carpetas y ficheros
|
# Copia carpetas y ficheros (resources.pack substitueix la carpeta data/)
|
||||||
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
@@ -163,31 +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>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
|
||||||
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||||
|
|
||||||
# Copia el ejecutable Intel al bundle
|
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
# DMG Intel y continúa con la build de Apple Silicon).
|
||||||
|
@echo ""
|
||||||
# Firma la aplicación
|
@echo "============================================"
|
||||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
@echo " Compilando version Intel (x86_64)"
|
||||||
|
@echo "============================================"
|
||||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||||
@echo "Creando DMG Intel con iconos de 96x96..."
|
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||||
create-dmg \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||||
--volname "$(APP_NAME)" \
|
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
|
||||||
--window-pos 200 120 \
|
&& cmake --build build/intel; then \
|
||||||
--window-size 720 300 \
|
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||||
--icon-size 96 \
|
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||||
--text-size 12 \
|
echo "Creando DMG Intel con iconos de 96x96..."; \
|
||||||
--icon "$(APP_NAME).app" 278 102 \
|
create-dmg \
|
||||||
--icon "LICENSE" 441 102 \
|
--volname "$(APP_NAME)" \
|
||||||
--icon "README.md" 604 102 \
|
--window-pos 200 120 \
|
||||||
--app-drop-link 115 102 \
|
--window-size 720 300 \
|
||||||
--hide-extension "$(APP_NAME).app" \
|
--icon-size 96 \
|
||||||
"$(MACOS_INTEL_RELEASE)" \
|
--text-size 12 \
|
||||||
"$(RELEASE_FOLDER)" || true
|
--icon "$(APP_NAME).app" 278 102 \
|
||||||
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
|
--icon "LICENSE" 441 102 \
|
||||||
|
--icon "README.md" 604 102 \
|
||||||
|
--app-drop-link 115 102 \
|
||||||
|
--hide-extension "$(APP_NAME).app" \
|
||||||
|
"$(MACOS_INTEL_RELEASE)" \
|
||||||
|
"$(RELEASE_FOLDER)" || true; \
|
||||||
|
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
|
||||||
|
else \
|
||||||
|
echo ""; \
|
||||||
|
echo "============================================"; \
|
||||||
|
echo " WARNING: la build Intel ha fallado."; \
|
||||||
|
echo " Se omite el DMG Intel y se continúa con"; \
|
||||||
|
echo " la build de Apple Silicon."; \
|
||||||
|
echo "============================================"; \
|
||||||
|
echo ""; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Compila la versión para procesadores Apple Silicon con cmake
|
# Compila la versión para procesadores Apple Silicon con cmake
|
||||||
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
|
@echo ""
|
||||||
|
@echo "============================================"
|
||||||
|
@echo " Compilando version Apple Silicon (arm64)"
|
||||||
|
@echo "============================================"
|
||||||
|
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build/arm
|
@cmake --build build/arm
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||||
|
|
||||||
@@ -217,22 +307,64 @@ _macos_release:
|
|||||||
$(RMDIR) build/arm
|
$(RMDIR) build/arm
|
||||||
$(RMFILE) "$(DIST_DIR)"/rw.*
|
$(RMFILE) "$(DIST_DIR)"/rw.*
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker amb emscripten/emsdk)
|
||||||
|
# ==============================================================================
|
||||||
|
# Genera aee.{html,js,wasm,data} a dist/wasm/. Es pot provar servint amb un
|
||||||
|
# servidor HTTP local (els navegadors no carreguen `file://` WASM):
|
||||||
|
# cd dist/wasm && python3 -m http.server 8000
|
||||||
|
# # després obrir http://localhost:8000/aee.html
|
||||||
|
wasm:
|
||||||
|
@echo "Creando release para WebAssembly - Version: $(VERSION)"
|
||||||
|
docker run --rm \
|
||||||
|
--user $(shell id -u):$(shell id -g) \
|
||||||
|
-v $(DIR_ROOT):/src \
|
||||||
|
-w /src \
|
||||||
|
emscripten/emsdk:latest \
|
||||||
|
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
|
||||||
|
@$(MKDIR) "$(DIST_DIR)/wasm"
|
||||||
|
@cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
|
||||||
|
@cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
|
||||||
|
@cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
|
||||||
|
@cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
|
||||||
|
@echo "Output: $(DIST_DIR)/wasm/$(TARGET_NAME).html"
|
||||||
|
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
|
||||||
|
maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/
|
||||||
|
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
|
||||||
|
@echo "Deployed to maverick"
|
||||||
|
|
||||||
|
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm_debug/.
|
||||||
|
wasm_debug:
|
||||||
|
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
|
||||||
|
docker run --rm \
|
||||||
|
--user $(shell id -u):$(shell id -g) \
|
||||||
|
-v $(DIR_ROOT):/src \
|
||||||
|
-w /src \
|
||||||
|
emscripten/emsdk:latest \
|
||||||
|
bash -c "emcmake cmake -S . -B build/wasm_debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm_debug"
|
||||||
|
@$(MKDIR) "$(DIST_DIR)/wasm_debug"
|
||||||
|
@cp build/wasm_debug/$(TARGET_NAME).html $(DIST_DIR)/wasm_debug/
|
||||||
|
@cp build/wasm_debug/$(TARGET_NAME).js $(DIST_DIR)/wasm_debug/
|
||||||
|
@cp build/wasm_debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm_debug/
|
||||||
|
@cp build/wasm_debug/$(TARGET_NAME).data $(DIST_DIR)/wasm_debug/
|
||||||
|
@echo "Output: $(DIST_DIR)/wasm_debug/$(TARGET_NAME).html"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA LINUX (RELEASE)
|
# COMPILACIÓN PARA LINUX (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
_linux_release:
|
_linux_release: pack
|
||||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||||
|
|
||||||
# Compila con cmake
|
# Compila con cmake
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
|
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
# Copia ficheros
|
# Copia ficheros (resources.pack substitueix la carpeta data/)
|
||||||
cp -r data "$(RELEASE_FOLDER)"
|
cp resources.pack "$(RELEASE_FOLDER)"
|
||||||
cp LICENSE "$(RELEASE_FOLDER)"
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||||
@@ -247,4 +379,69 @@ _linux_release:
|
|||||||
# Elimina la carpeta temporal
|
# Elimina la carpeta temporal
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
.PHONY: all debug release _windows_release _linux_release _macos_release
|
# ==============================================================================
|
||||||
|
# ==============================================================================
|
||||||
|
# CODE QUALITY (delegados a cmake)
|
||||||
|
# ==============================================================================
|
||||||
|
format:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target format
|
||||||
|
|
||||||
|
format-check:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target format-check
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target tidy
|
||||||
|
|
||||||
|
tidy-fix:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target tidy-fix
|
||||||
|
|
||||||
|
cppcheck:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target cppcheck
|
||||||
|
|
||||||
|
# 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:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
root: "OPCIONS"
|
root: "Opcions"
|
||||||
video: "VIDEO"
|
video: "Vídeo"
|
||||||
audio: "AUDIO"
|
audio: "Àudio"
|
||||||
controls: "CONTROLS"
|
controls: "Controls"
|
||||||
game: "JOC"
|
game: "Joc"
|
||||||
|
system: "Sistema"
|
||||||
|
|
||||||
items:
|
items:
|
||||||
video: "VIDEO"
|
video: "Vídeo"
|
||||||
audio: "AUDIO"
|
audio: "Àudio"
|
||||||
controls: "CONTROLS"
|
controls: "Controls"
|
||||||
game: "JOC"
|
game: "Joc"
|
||||||
use_new_logo: "LOGO NOU"
|
system: "Sistema"
|
||||||
show_title_credits: "CREDITS DEL PORT"
|
restart: "Reinicia"
|
||||||
zoom: "ZOOM"
|
exit_game: "Eixir del joc"
|
||||||
screen: "PANTALLA"
|
use_new_logo: "Logo nou"
|
||||||
shader: "SHADER"
|
show_title_credits: "Crèdits del port"
|
||||||
aspect_4_3: "ASPECTE 4:3"
|
show_preload: "Barra de precàrrega"
|
||||||
supersampling: "SUPERSAMPLING"
|
zoom: "Zoom"
|
||||||
vsync: "VSYNC"
|
screen: "Pantalla"
|
||||||
integer_scale: "ESCALA ENTERA"
|
shader: "Shader"
|
||||||
shader_type: "TIPUS SHADER"
|
aspect_4_3: "Aspecte 4:3"
|
||||||
preset: "PRESET"
|
supersampling: "Supersampling"
|
||||||
stretch_filter: "FILTRE 4:3"
|
vsync: "Vsync"
|
||||||
render_info: "RENDER INFO"
|
scaling_mode: "Escala"
|
||||||
uptime: "TEMPS DE JOC"
|
shader_type: "Tipus shader"
|
||||||
master_enable: "AUDIO"
|
preset: "Preset"
|
||||||
master_volume: "MASTER"
|
texture_filter: "Filtre textura"
|
||||||
music: "MUSICA"
|
render_info: "Render info"
|
||||||
music_volume: "VOL MUSICA"
|
uptime: "Temps de joc"
|
||||||
sounds: "SONS"
|
internal_resolution: "Resolució interna"
|
||||||
sounds_volume: "VOL SONS"
|
master_enable: "Àudio"
|
||||||
move_up: "MOU AMUNT"
|
master_volume: "Màster"
|
||||||
move_down: "MOU AVALL"
|
music: "Música"
|
||||||
move_left: "MOU ESQUERRA"
|
music_volume: "Vol música"
|
||||||
move_right: "MOU DRETA"
|
sounds: "Sons"
|
||||||
menu_key: "TECLA MENU"
|
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:
|
values:
|
||||||
"yes": "SI"
|
"yes": "Sí"
|
||||||
"no": "NO"
|
"no": "No"
|
||||||
"on": "ON"
|
"on": "On"
|
||||||
"off": "OFF"
|
"off": "Off"
|
||||||
fullscreen: "COMPLETA"
|
fullscreen: "Completa"
|
||||||
windowed: "FINESTRA"
|
windowed: "Finestra"
|
||||||
linear: "LINEAR"
|
linear: "Linear"
|
||||||
nearest: "NEAREST"
|
nearest: "Nearest"
|
||||||
top: "TOP"
|
top: "Top"
|
||||||
bottom: "BOTTOM"
|
bottom: "Bottom"
|
||||||
press_key: "<PREM TECLA>"
|
press_key: "<Prem tecla>"
|
||||||
empty: "(BUIT)"
|
empty: "(Buit)"
|
||||||
unknown: "---"
|
unknown: "---"
|
||||||
|
scaling_disabled: "Sense escala"
|
||||||
|
scaling_stretch: "Estirada"
|
||||||
|
scaling_letterbox: "Letterbox"
|
||||||
|
scaling_overscan: "Overscan"
|
||||||
|
scaling_integer: "Entera"
|
||||||
|
|
||||||
window:
|
window:
|
||||||
title: "© 2000 Aventures en Egipte — JailDesigner"
|
title: "© 2000 Aventures en Egipte — JailDesigner"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
exit_double_esc: "TORNA A PULSAR ESC PER EIXIR"
|
exit_double_esc: "Torna a pulsar ESC per a eixir"
|
||||||
zoom_fmt: "ZOOM %dX"
|
zoom_fmt: "Zoom %dX"
|
||||||
fullscreen: "PANTALLA COMPLETA"
|
fullscreen: "Pantalla completa"
|
||||||
windowed: "FINESTRA"
|
windowed: "Finestra"
|
||||||
shader_on: "SHADER ON"
|
shader_on: "Shader on"
|
||||||
shader_off: "SHADER OFF"
|
shader_off: "Shader off"
|
||||||
aspect_43: "4:3 CRT"
|
aspect_43: "4:3 CRT"
|
||||||
aspect_square: "PIXELS QUADRATS"
|
aspect_square: "Píxels quadrats"
|
||||||
ss_on: "SUPERSAMPLING ON"
|
ss_on: "Supersampling on"
|
||||||
ss_off: "SUPERSAMPLING OFF"
|
ss_off: "Supersampling off"
|
||||||
preset_fmt: "PRESET: %s"
|
preset_fmt: "Preset: %s"
|
||||||
filter_linear: "FILTRE: LINEAR"
|
filter_linear: "Filtre: linear"
|
||||||
filter_nearest: "FILTRE: NEAREST"
|
filter_nearest: "Filtre: nearest"
|
||||||
pause: "PAUSA"
|
pause: "Pausa"
|
||||||
resume: "REPRES"
|
gamepad_connected: "connectat"
|
||||||
|
gamepad_disconnected: "desconnectat"
|
||||||
|
|
||||||
credits:
|
credits:
|
||||||
port_role: "Conversio a C++ i SDL3"
|
port_role: "Conversio a C++ i SDL3"
|
||||||
|
|||||||
463
docs/scenes-migration-plan.md
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
# Reescritura de cinemáticas: capa `scenes::` + migración escena a escena
|
||||||
|
|
||||||
|
## Current Status (actualitzat 2026-04-16)
|
||||||
|
|
||||||
|
**Steps completats** — capa `scenes::` estable i 7 de 9 escenes migrades:
|
||||||
|
|
||||||
|
- ✅ **Step 0** — Infraestructura: `Scene`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, `SceneRegistry`, `scene_utils`, dispatch al `gameFiberEntry`.
|
||||||
|
- ✅ **Step 1** — `MortScene` (state 100). Pantalla game over + fade-in/out + música "00000001.ogg" → "00000003.ogg".
|
||||||
|
- ✅ **Step 2** — `BannerScene` (states 2..5). Banner pre-piràmide amb les 4 variants consolidades a `(idx%2)*160, (idx/2)*75`.
|
||||||
|
- ✅ **Step 3** — `MenuScene` (state 0). Primera ús real de `FrameAnimator` (camell 8×160ms). Scrollers manuals amb acumulador ms per palmeres/horitzó. Parpalleig "polsa tecla" time-based.
|
||||||
|
- ✅ **Step 4** — `IntroNewLogoScene` (state 255, condicional a `use_new_logo`). Revelat lletra a lletra + cicle de paleta 256 passos. **Delega temporalment a `ModuleSequence::doIntroSprites()`** via `SurfaceHandle::release()` perquè el legacy allibera `gfx` internament. La delegació desapareixerà al Step 9.
|
||||||
|
- ✅ **Step 5** — `SlidesScene` (states 1 i 7). Wipe suau amb `Easing::outCubic` (el "rasca" del vell s'ha evaporat). Redirect `6→7` replicat al `gameFiberEntry` abans del `tryCreate` perquè el flux "no tens prou diners" caiga a slides de fracàs.
|
||||||
|
- ✅ **Step 6** — `CreditsScene` (state 8). Scroll vertical + parallax condicional si `diamants == 16`. Música heretable (només arranca si no en sona cap ja). Escriu `trick.ini` al final.
|
||||||
|
- ✅ **Step 7** — `SecretaScene` (state 6). 11 fases amb swap de `tomba1.gif→tomba2.gif` via `SurfaceHandle::reset()` i efecte "red pulse" sobre els índexs 254/253 de la paleta. Primera ús d'`InitialFadeOut` (fade-out sobre la paleta prèvia abans de muntar la nova).
|
||||||
|
|
||||||
|
**Steps pendents** — ataquen el cor de la intro:
|
||||||
|
|
||||||
|
- 📋 **Step 8** — `IntroScene` (state 255 quan `use_new_logo == false`). 11 passos lineals del wordmark "JAILGAMES" llegat + cicle de paleta. Delegaria a `doIntroSprites` legacy igual que `IntroNewLogoScene`. Estimació: ~150 línies. Complexitat Media-Alta, però lineal.
|
||||||
|
- 📋 **Step 9** — `IntroSpritesScene`. **El hueso**. `switch (rand() % 3)` amb 3 variants completament diferents (~900–1100 frames cada una), 6–8 loops anidats per variant, frames subsamplejats amb màscares diferents. Mateix arxiu `gfx` que la intro que la crida. Si l'API escala mal, s'acceptarà un `tick()` manual sense Timeline. En migrar aquest step s'elimina la delegació temporal `IntroNewLogoScene → doIntroSprites` i `doIntroSprites` pot passar de `public` a privat/eliminat. Complexitat Alta.
|
||||||
|
- 📋 **Step 10** — Neteja final. `ModuleSequence::doIntro()` legacy també desapareix quan `IntroScene` + `IntroSpritesScene` estan fetes. `wait_frame_or_skip()` helper s'elimina. `ModuleSequence::Go()` queda reduït a ~5 línies o desapareix del tot si es pot treure del `gameFiberEntry`. Pot ser també aquí on s'elimine el `fiber` per fi quan `ModuleGame` siga tick-based, però això és un altre capítol.
|
||||||
|
|
||||||
|
**Configuració per a proves ràpides** — afegits al `Options::game` (persistents a `config.yaml`):
|
||||||
|
|
||||||
|
- `piramide_inicial` (ja existia) — state d'entrada. Valors útils: `255` = intro normal, `0` = menú, `5` = banner piràmide 5, `6` = SecretaScene, `8` = Credits, `100` = Mort.
|
||||||
|
- `habitacio_inicial` (ja existia) — sala d'entrada dins la piràmide (1..5).
|
||||||
|
- `vides` (ja existia).
|
||||||
|
- `diamants_inicial` — per al final "bo" dels crèdits amb parallax + cotxe, posar a `16`.
|
||||||
|
- `diners_inicial` — necessari posar `200` per entrar a `SecretaScene` sense el redirect a slides-fracàs (si entres directament en state 6 o hi arribes des del gameplay).
|
||||||
|
- `show_title_credits` (ja existia) — desactivar-ho accelera els tests.
|
||||||
|
|
||||||
|
**Bugs notables resolts al llarg del camí** (mantenir present — poden reaparèixer si es toca codi similar):
|
||||||
|
|
||||||
|
1. `JI_Update()` no es cridava dins del mini-while del fiber → `JI_AnyKey()` no es refrescava → les escenes ignoraven les tecles de skip. Fix a [director.cpp:gameFiberEntry](source/core/system/director.cpp) al Step 3.
|
||||||
|
2. `IntroNewLogoScene::~` doble-free de `gfx_` perquè `doIntroSprites` sempre allibera el `gfx` que rep (tant al final normal com als paths de skip). Fix: `SurfaceHandle::release()` abans de delegar. Step 4.
|
||||||
|
3. `IntroNewLogoScene` no mutava `info::ctx.num_piramide = 0` al terminar, el fiber tornava a crear la mateixa escena — loop infinit. El `Go()` vell ho feia post-switch. Step 4.
|
||||||
|
4. Skip per tecla durant el revelat del logo nou saltava només les lletres i executava igualment `doIntroSprites`. El vell retornava abans de cridar doIntroSprites. Fix al Step 4: `Phase::Done` direct en skip, `Phase::Delegate` només per terminació natural.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Las fases 0–7b del plan anterior están completas. El runtime de AEE ya es moderno: fibers cooperativos, audio streaming sin `SDL_AddTimer`, callbacks `SDL_AppInit/Iterate/Event/Quit`, C++ idiomático en la capa jail. Lo que queda de *legacy pesado* es [source/game/modulesequence.cpp](source/game/modulesequence.cpp): **1309 líneas** con 9 funciones de cinemáticas lineales, 38+ `wait_frame_or_skip()` calls, constantes mágicas esparcidas, tres sub-variantes aleatorias en `doIntroSprites`, y código procedural difícil de editar.
|
||||||
|
|
||||||
|
Un refactor mecánico de eso no tiene sentido — las escenas son contenido, no plumbing. Cada una tiene su propia lógica específica y no se benefician de una state machine genérica ni de sed. Lo que sí tiene sentido es **reescribirlas de arriba a abajo** sobre una capa fina `scenes::` reutilizable (Timeline, SpriteMover, FrameAnimator, PaletteFade, surface handle RAII), convirtiendo cada función en una clase `Scene`. Cada escena migrada elimina su código legacy del modulesequence, hasta que la función Go() sólo quede como un delegador hacia el registry.
|
||||||
|
|
||||||
|
**Objetivos**:
|
||||||
|
|
||||||
|
1. Capa `scenes::` **pequeña y reutilizable** — helpers obvios, sin sobreingeniería, reusando [easing.hpp](source/utils/easing.hpp) y los `JD8_*` existentes.
|
||||||
|
2. Cada escena nueva: **~20–80 líneas** de código declarativo (vs los cientos actuales).
|
||||||
|
3. **Fácil de añadir escenas nuevas** — derivar de `scenes::Scene`, llenar un Timeline o un `tick()` directo, registrar en el `SceneRegistry`.
|
||||||
|
4. **Time-based**: todo `delta_ms` explícito. Las escenas no tocan fibers, no tienen whiles, no llaman `JG_ShouldUpdate`.
|
||||||
|
5. **Migración gradual**: el fiber existente sigue corriendo por debajo. Las escenas nuevas se ejecutan *dentro* del fiber (por debajo del capó) pero su código es puro tick-based. Cuando las 9 estén migradas + ModuleGame también, el fiber se elimina de una pieza.
|
||||||
|
6. **Zero regresiones visuales** — cada escena nueva debe verse/sonar indistinguible de la vieja antes de eliminar el código legacy asociado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capa `scenes::` — API
|
||||||
|
|
||||||
|
Namespace `scenes::` (plano, consistente con `Overlay::`, `Screen::`, `Menu::`).
|
||||||
|
|
||||||
|
### `scenes::Scene` — interfaz base [source/scenes/scene.hpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Scene {
|
||||||
|
public:
|
||||||
|
virtual ~Scene() = default;
|
||||||
|
|
||||||
|
// Llamado una vez cuando el Director la activa. Buen sitio para arrancar
|
||||||
|
// música o disparar un fade-in. Los assets pueden cargarse aquí o en el
|
||||||
|
// constructor (ambos válidos).
|
||||||
|
virtual void onEnter() {}
|
||||||
|
|
||||||
|
// Un paso de la escena. No debe bloquear, no debe llamar a JD8_Flip
|
||||||
|
// (lo hace el caller). delta_ms = tiempo real transcurrido desde el
|
||||||
|
// tick anterior.
|
||||||
|
virtual void tick(int delta_ms) = 0;
|
||||||
|
|
||||||
|
// True cuando la escena ha acabado y el Director debe pasar a la siguiente.
|
||||||
|
virtual bool done() const = 0;
|
||||||
|
|
||||||
|
// Valor de retorno equivalente al int que devolvía Go(). El caller lo
|
||||||
|
// usa para decidir el siguiente módulo. Consultado sólo cuando done().
|
||||||
|
virtual int nextState() const { return 1; }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scenes::Timeline` — secuencia de steps [source/scenes/timeline.hpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Timeline {
|
||||||
|
public:
|
||||||
|
using StepFn = std::function<void(float progress_0_1)>;
|
||||||
|
|
||||||
|
// Step con duración y callback que recibe el progreso [0..1] cada tick.
|
||||||
|
// Si fn es nullptr, el step es una espera pura.
|
||||||
|
Timeline& step(int duration_ms, StepFn fn = nullptr);
|
||||||
|
|
||||||
|
// Step que se ejecuta una sola vez al entrar (pinta algo estático y listo).
|
||||||
|
Timeline& once(std::function<void()> fn);
|
||||||
|
|
||||||
|
void tick(int delta_ms);
|
||||||
|
void skip(); // marca todos los steps restantes como done inmediatamente
|
||||||
|
void reset();
|
||||||
|
bool done() const;
|
||||||
|
int currentStepIndex() const;
|
||||||
|
float currentProgress() const; // 0..1 dentro del step actual
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scenes::SpriteMover` — movimiento 2D con easing [source/scenes/sprite_mover.hpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class SpriteMover {
|
||||||
|
public:
|
||||||
|
using EaseFn = float(*)(float);
|
||||||
|
void moveTo(int x0, int y0, int x1, int y1, int duration_ms,
|
||||||
|
EaseFn ease = Easing::linear);
|
||||||
|
void tick(int delta_ms);
|
||||||
|
int x() const;
|
||||||
|
int y() const;
|
||||||
|
bool done() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
No gestiona surfaces — sólo posición. La escena hace `JD8_BlitCK(mover.x(), mover.y(), gfx, ...)` ella misma. Reutilizable para el coche de créditos, slides, Sam caminando, etc.
|
||||||
|
|
||||||
|
### `scenes::FrameAnimator` — iteración de frames subsampleados [source/scenes/frame_animator.hpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class FrameAnimator {
|
||||||
|
public:
|
||||||
|
FrameAnimator(int num_frames, int frame_ms, bool loop = true);
|
||||||
|
void tick(int delta_ms);
|
||||||
|
int frame() const; // índice [0, num_frames)
|
||||||
|
bool done() const; // sólo relevante si loop=false
|
||||||
|
void reset();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Cubre camello (8 frames × 4 ticks), palmeras (4 × 8 ticks), Sam caminando con `(i/5) % fr`.
|
||||||
|
|
||||||
|
### `scenes::PaletteFade` — wrapper time-based de `JD8_Fade*` [source/scenes/palette_fade.hpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class PaletteFade {
|
||||||
|
public:
|
||||||
|
void startFadeOut();
|
||||||
|
void startFadeTo(JD8_Palette target);
|
||||||
|
void tick(int delta_ms); // avanza un step de fade por tick
|
||||||
|
bool active() const;
|
||||||
|
bool done() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrapper sobre `JD8_FadeStartOut` / `JD8_FadeStartToPal` / `JD8_FadeTickStep` que ya existen.
|
||||||
|
|
||||||
|
### `scenes::SurfaceHandle` — RAII para `JD8_Surface` [source/scenes/surface_handle.hpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class SurfaceHandle {
|
||||||
|
public:
|
||||||
|
SurfaceHandle() = default;
|
||||||
|
explicit SurfaceHandle(const char* file);
|
||||||
|
~SurfaceHandle();
|
||||||
|
SurfaceHandle(const SurfaceHandle&) = delete;
|
||||||
|
SurfaceHandle& operator=(const SurfaceHandle&) = delete;
|
||||||
|
SurfaceHandle(SurfaceHandle&&) noexcept;
|
||||||
|
SurfaceHandle& operator=(SurfaceHandle&&) noexcept;
|
||||||
|
|
||||||
|
operator JD8_Surface() const; // conversión implícita → pasable a JD8_Blit*
|
||||||
|
JD8_Surface get() const;
|
||||||
|
bool valid() const;
|
||||||
|
void reset(const char* file); // libera + recarga (doSecreta lo necesita)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scenes::SceneRegistry` — factory [source/scenes/scene_registry.hpp/cpp]
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class SceneRegistry {
|
||||||
|
public:
|
||||||
|
using Factory = std::function<std::unique_ptr<Scene>()>;
|
||||||
|
|
||||||
|
// Llamado al boot para registrar cada escena migrada.
|
||||||
|
void registerScene(int state_key, Factory f);
|
||||||
|
|
||||||
|
// Intenta crear la escena para un state dado. nullptr si no registrada.
|
||||||
|
// El caller (gameFiberEntry) cae al viejo Go() legacy si devuelve null.
|
||||||
|
std::unique_ptr<Scene> tryCreate(int state_key) const;
|
||||||
|
|
||||||
|
// Singleton accedido desde el Director al boot.
|
||||||
|
static SceneRegistry& instance();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
El `state_key` es un valor sintético que combina `info::ctx.num_piramide` con el módulo objetivo (sequence vs game). Los valores exactos los resolvemos al implementar — podría ser el propio `num_piramide` si es suficiente para distinguir (255=intro, 0=menu, 1/7=slides, 2-5=banner, 6=secreta, 8=credits, 100=mort).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Organización de archivos
|
||||||
|
|
||||||
|
```
|
||||||
|
source/scenes/
|
||||||
|
├── scene.hpp
|
||||||
|
├── scene_registry.hpp/.cpp
|
||||||
|
├── timeline.hpp/.cpp
|
||||||
|
├── sprite_mover.hpp/.cpp
|
||||||
|
├── frame_animator.hpp/.cpp
|
||||||
|
├── palette_fade.hpp/.cpp
|
||||||
|
├── surface_handle.hpp/.cpp
|
||||||
|
├── mort_scene.hpp/.cpp # orden de migración
|
||||||
|
├── banner_scene.hpp/.cpp
|
||||||
|
├── menu_scene.hpp/.cpp
|
||||||
|
├── intro_new_logo_scene.hpp/.cpp
|
||||||
|
├── slides_scene.hpp/.cpp
|
||||||
|
├── credits_scene.hpp/.cpp
|
||||||
|
├── secreta_scene.hpp/.cpp
|
||||||
|
├── intro_scene.hpp/.cpp
|
||||||
|
└── intro_sprites_scene.hpp/.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
Estructura plana — sin subdirectorios `helpers/` o `concrete/`. Añadir archivo nuevo = una línea al `CMakeLists.txt`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integración con el Director existente
|
||||||
|
|
||||||
|
**No creo un Director nuevo**. Modifico [source/core/system/director.cpp](source/core/system/director.cpp) — concretamente `gameFiberEntry()` en el namespace anónimo — para que consulte el `SceneRegistry` antes de caer al viejo `ModuleSequence::Go()`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// pseudocodigo dentro de gameFiberEntry()
|
||||||
|
int state = 1;
|
||||||
|
while (state != -1 && !JG_Quitting()) {
|
||||||
|
// Intentamos resolver la escena por el state actual.
|
||||||
|
if (auto scene = SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
|
||||||
|
scene->onEnter();
|
||||||
|
Uint32 last = SDL_GetTicks();
|
||||||
|
while (!scene->done() && !JG_Quitting()) {
|
||||||
|
Uint32 now = SDL_GetTicks();
|
||||||
|
scene->tick(static_cast<int>(now - last));
|
||||||
|
last = now;
|
||||||
|
JD8_Flip(); // yields al Director (presenta con overlay encima)
|
||||||
|
}
|
||||||
|
state = scene->nextState();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Fallback: todavía no migrada, usa el Go() legacy
|
||||||
|
if (state == 1) {
|
||||||
|
auto* ms = new ModuleSequence();
|
||||||
|
state = ms->Go();
|
||||||
|
delete ms;
|
||||||
|
} else if (state == 0) {
|
||||||
|
auto* mg = new ModuleGame();
|
||||||
|
state = mg->Go();
|
||||||
|
delete mg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claves**:
|
||||||
|
- Las escenas nuevas son puras tick-based. `tick(delta_ms)` no sabe del fiber.
|
||||||
|
- El mini-while que las ejecuta vive en `gameFiberEntry`, que sí corre dentro del fiber. `JD8_Flip()` es el que hace el yield al Director — igual que ahora.
|
||||||
|
- Cuando todas las escenas + `ModuleGame` estén migradas, este mini-while migra al `Director::iterate()` directo y se elimina `gameFiberEntry` + `GameFiber::*`. Pero eso no es para esta tanda.
|
||||||
|
- Registro de escenas: se hace en `Director::init()` llamando a `SceneRegistry::instance().registerScene(state_key, []{ return std::make_unique<scenes::MortScene>(); })` para cada escena ya migrada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Orden de migración (simple → complejo)
|
||||||
|
|
||||||
|
Cada paso = una PR / commit / validación visual antes de seguir. Al migrar una escena, **se elimina la función legacy correspondiente** de modulesequence.cpp.
|
||||||
|
|
||||||
|
### Step 0 — Infraestructura
|
||||||
|
Crear los archivos de la capa `scenes::` (scene, timeline, sprite_mover, frame_animator, palette_fade, surface_handle, scene_registry) sin ninguna escena concreta todavía. Compilar para confirmar que la capa es sólida.
|
||||||
|
|
||||||
|
### Step 1 — `MortScene` (complejidad **Baja**)
|
||||||
|
Reemplaza `ModuleSequence::doMort()`. ~15 líneas originales: blit fullscreen `gameover.gif` + `JD8_FadeToPal` + música `00000001.ogg` + espera 1000ms o tecla + `info::ctx.vida = 5`. Es la primera víctima: valida la API mínima (`Scene` + `PaletteFade` + `SurfaceHandle`).
|
||||||
|
|
||||||
|
### Step 2 — `BannerScene` (complejidad **Baja**)
|
||||||
|
Reemplaza `ModuleSequence::doBanner()`. Blits estáticos "PIRÀMIDE X" + número + fade entrada + espera 5000ms + `JA_FadeOutMusic(250)` + fade salida. Primera prueba de `Timeline::step()` con `once()`.
|
||||||
|
|
||||||
|
### Step 3 — `MenuScene` (complejidad **Media-Baja**)
|
||||||
|
Reemplaza `ModuleSequence::doMenu()`. Primera prueba de `FrameAnimator` (palmeras, camello, horizonte). Bucle infinito hasta input. Lee/escribe `info::ctx.pepe_activat` y `info::ctx.nou_personatge`. Texto condicional con `Locale::get`.
|
||||||
|
|
||||||
|
### Step 4 — `IntroNewLogoScene` (complejidad **Media**)
|
||||||
|
Reemplaza `ModuleSequence::doIntroNewLogo()`. Revelado letra a letra (9 letras × 150ms) + cursor parpadeando + logo completo + ciclo de paleta 256 pasos. Timeline con 20+ steps. Mantiene la llamada final a `doIntroSprites` (que aún no está migrada — delegación legacy temporal).
|
||||||
|
|
||||||
|
### Step 5 — `SlidesScene` (complejidad **Media**)
|
||||||
|
Reemplaza `ModuleSequence::doSlides()`. 3 slides con scroll entrada-derecha + espera + scroll salida-izquierda. Primera prueba seria de `SpriteMover` con `Easing::outCubic`. Elige asset según `info::ctx.num_piramide` + `info::ctx.diners`. Fade de música al final.
|
||||||
|
|
||||||
|
### Step 6 — `CreditsScene` (complejidad **Media**)
|
||||||
|
Reemplaza `ModuleSequence::doCredits()`. Scroll vertical largo (~3100 frames = ~62s a 20ms) + scroll parallax condicional si `info::ctx.diamants == 16` con animación de coche. Escribe `info::ctx.nou_personatge = true` y crea `trick.ini`.
|
||||||
|
|
||||||
|
### Step 7 — `SecretaScene` (complejidad **Media-Alta**)
|
||||||
|
Reemplaza `ModuleSequence::doSecreta()`. 11 estados originales: scroll + recarga de asset a mitad (`SurfaceHandle::reset`) + animación RGB dinámica del rojo (`JD8_SetPaletteColor`). Primera escena que usa `SurfaceHandle::reset()`.
|
||||||
|
|
||||||
|
### Step 8 — `IntroScene` (complejidad **Media-Alta**)
|
||||||
|
Reemplaza `ModuleSequence::doIntro()` (el logo JAILGAMES legacy). 11 pasos lineales de construcción del wordmark + ciclo de paleta + delegación a `IntroSpritesScene`. Timeline con muchos `once()` + `step()`.
|
||||||
|
|
||||||
|
### Step 9 — `IntroSpritesScene` (complejidad **Alta**)
|
||||||
|
Reemplaza `ModuleSequence::doIntroSprites()`. La bestia: `switch(rand() % 3)` con 3 variantes completamente distintas (~900-1100 frames cada una). Cada variante tiene 6-8 loops anidados. Aquí probablemente hace falta combinar `Timeline` + `SpriteMover` + `FrameAnimator` + lógica ad-hoc. Si la API no escala limpia, se acepta que esta escena tenga `tick()` manual sin Timeline.
|
||||||
|
|
||||||
|
### Step 10 — Limpieza final
|
||||||
|
En este punto `ModuleSequence` ya no tiene ninguna función `doX()` — sólo el `Go()` que delega al registry. Se puede:
|
||||||
|
- Eliminar `ModuleSequence` completo y mover el dispatch al `gameFiberEntry` directo.
|
||||||
|
- Eliminar el helper `wait_frame_or_skip()`.
|
||||||
|
- Eliminar el include de `fiber.hpp` desde `jgame.cpp` si `ModuleGame` también es tick-based (fuera de scope de este plan, pero queda preparado).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invariantes por escena
|
||||||
|
|
||||||
|
Cada paso **debe cumplir**:
|
||||||
|
1. Visualmente indistinguible de la vieja versión (mismo timing, mismas transiciones, mismo feel). Validar jugándolo.
|
||||||
|
2. Skip por tecla funciona idéntico (misma tecla, mismo momento).
|
||||||
|
3. Build nativo compila limpio, sin warnings nuevos.
|
||||||
|
4. Audio sigue: música arranca, fades suaves, no hay cortes.
|
||||||
|
5. Overlay sigue animándose encima (pause, notificaciones, render info) — lo hace el Director sin tocar la escena.
|
||||||
|
6. La función legacy `doX()` se elimina en el mismo commit que su `XScene`, no se deja código muerto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fuera de scope (explícito)
|
||||||
|
|
||||||
|
- **`ModuleGame`** (gameplay puro). Sigue con Go() + fiber. Se migrará más tarde con otra estructura (probablemente no Scene — es interactivo y no lineal).
|
||||||
|
- **Emscripten fiber backend** + build WASM (fases 7c/7d del plan anterior). Cuando estén migradas las escenas + ModuleGame, los fibers se eliminan y este punto se vuelve trivial.
|
||||||
|
- **Fase 6** (time-based total con accumulator pattern). La saltamos — no aporta valor real con el framerate actual.
|
||||||
|
- **Multi-language** de textos en escenas. Se usa `Locale::get` directamente donde haga falta, sin envoltorio nuevo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical files
|
||||||
|
|
||||||
|
| Archivo | Step | Tipo |
|
||||||
|
|---|---|---|
|
||||||
|
| [source/scenes/scene.hpp](source/scenes/scene.hpp) | 0 | nuevo, interfaz base |
|
||||||
|
| [source/scenes/timeline.hpp](source/scenes/timeline.hpp) + .cpp | 0 | nuevo, helper central |
|
||||||
|
| [source/scenes/sprite_mover.hpp](source/scenes/sprite_mover.hpp) + .cpp | 0 | nuevo |
|
||||||
|
| [source/scenes/frame_animator.hpp](source/scenes/frame_animator.hpp) + .cpp | 0 | nuevo |
|
||||||
|
| [source/scenes/palette_fade.hpp](source/scenes/palette_fade.hpp) + .cpp | 0 | nuevo |
|
||||||
|
| [source/scenes/surface_handle.hpp](source/scenes/surface_handle.hpp) + .cpp | 0 | nuevo, RAII |
|
||||||
|
| [source/scenes/scene_registry.hpp](source/scenes/scene_registry.hpp) + .cpp | 0 | nuevo, factory |
|
||||||
|
| [source/scenes/*_scene.hpp](source/scenes/) + .cpp | 1–9 | una por paso |
|
||||||
|
| [source/core/system/director.cpp](source/core/system/director.cpp) | 0 | modificar `gameFiberEntry` |
|
||||||
|
| [source/game/modulesequence.cpp](source/game/modulesequence.cpp) | 1–9 | borrar funciones `doX()` una a una |
|
||||||
|
| [CMakeLists.txt](CMakeLists.txt) | 0–9 | añadir archivos nuevos |
|
||||||
|
|
||||||
|
## Reusables existentes
|
||||||
|
|
||||||
|
- [source/utils/easing.hpp](source/utils/easing.hpp) — `Easing::linear`, `outQuad`, `outCubic`, `inOutQuad`, `lerp`, `lerpInt`. Usados por `SpriteMover` y cualquier step de `Timeline` que reciba progress.
|
||||||
|
- [source/core/jail/jdraw8.hpp](source/core/jail/jdraw8.hpp) — `JD8_FadeStartOut`, `JD8_FadeStartToPal`, `JD8_FadeTickStep`, `JD8_FadeIsActive`. Usados por `PaletteFade`.
|
||||||
|
- [source/core/jail/jail_audio.hpp](source/core/jail/jail_audio.hpp) — `JA_PlayMusic`, `JA_FadeOutMusic`, `JA_PauseMusic`, `JA_ResumeMusic`.
|
||||||
|
- [source/core/locale/locale.hpp](source/core/locale/locale.hpp) — `Locale::get("key")` para strings de UI en las escenas.
|
||||||
|
- [source/core/rendering/overlay.hpp](source/core/rendering/overlay.hpp) — sigue siendo responsabilidad del Director; las escenas no tocan overlay.
|
||||||
|
- [source/core/jail/jinput.hpp](source/core/jail/jinput.hpp) — `JI_AnyKey`, `JI_KeyPressed` para detectar skip y navegación de menú.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplos concretos
|
||||||
|
|
||||||
|
### `MortScene` (Step 1) — ~20 líneas de lógica
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// mort_scene.hpp
|
||||||
|
namespace scenes {
|
||||||
|
class MortScene : public Scene {
|
||||||
|
public:
|
||||||
|
void onEnter() override;
|
||||||
|
void tick(int delta_ms) override;
|
||||||
|
bool done() const override { return done_; }
|
||||||
|
int nextState() const override { return 1; } // igual que doMort → vuelve a seq
|
||||||
|
private:
|
||||||
|
SurfaceHandle gfx_;
|
||||||
|
PaletteFade fade_;
|
||||||
|
int remaining_ms_ = 1000;
|
||||||
|
bool done_ = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// mort_scene.cpp
|
||||||
|
void MortScene::onEnter() {
|
||||||
|
// Lo que hacía ModuleSequence::doMort() linealmente, declarativo.
|
||||||
|
int size = 0;
|
||||||
|
char* buf = file_getfilebuffer("00000001.ogg", size);
|
||||||
|
JA_PlayMusic(JA_LoadMusic((Uint8*)buf, size, "00000001.ogg"));
|
||||||
|
JI_DisableKeyboard(60);
|
||||||
|
info::ctx.vida = 5;
|
||||||
|
|
||||||
|
gfx_ = SurfaceHandle("gameover.gif");
|
||||||
|
JD8_Palette pal = JD8_LoadPalette("gameover.gif");
|
||||||
|
JD8_ClearScreen(0);
|
||||||
|
JD8_Blit(gfx_);
|
||||||
|
fade_.startFadeTo(pal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MortScene::tick(int delta_ms) {
|
||||||
|
fade_.tick(delta_ms);
|
||||||
|
if (JI_AnyKey()) { done_ = true; return; }
|
||||||
|
remaining_ms_ -= delta_ms;
|
||||||
|
if (remaining_ms_ <= 0) done_ = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `BannerScene` (Step 2) — Timeline + fades
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void BannerScene::onEnter() {
|
||||||
|
play_music("00000004.ogg");
|
||||||
|
gfx_ = SurfaceHandle("ffase.gif");
|
||||||
|
JD8_Palette pal = JD8_LoadPalette("ffase.gif");
|
||||||
|
|
||||||
|
timeline_
|
||||||
|
.once([this]{
|
||||||
|
JD8_ClearScreen(0);
|
||||||
|
// blits del banner + número según info::ctx.num_piramide
|
||||||
|
fade_in_.startFadeTo(pal);
|
||||||
|
})
|
||||||
|
.step(5000); // espera. Cualquier tecla hace timeline_.skip().
|
||||||
|
}
|
||||||
|
|
||||||
|
void BannerScene::tick(int delta_ms) {
|
||||||
|
fade_in_.tick(delta_ms);
|
||||||
|
if (!timeline_.done()) {
|
||||||
|
if (JI_AnyKey()) timeline_.skip();
|
||||||
|
timeline_.tick(delta_ms);
|
||||||
|
if (timeline_.done() && !fade_out_started_) {
|
||||||
|
JA_FadeOutMusic(250);
|
||||||
|
fade_out_.startFadeOut();
|
||||||
|
fade_out_started_ = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fade_out_.tick(delta_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BannerScene::done() const { return timeline_.done() && fade_out_.done(); }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Tras **cada step**:
|
||||||
|
|
||||||
|
1. `cmake --build build` limpio, sin warnings nuevos.
|
||||||
|
2. Ejecutar el juego entero desde intro hasta muerte, con atención específica a la escena migrada. Comparar con un git stash temporal del viejo código si hace falta.
|
||||||
|
3. **Skip por tecla** en la escena migrada — debe saltar a la siguiente igual que antes.
|
||||||
|
4. **Pausa F11** durante la escena — el juego se congela, el overlay sigue animándose.
|
||||||
|
5. **Menú F12** durante la escena — debe abrir encima.
|
||||||
|
6. **Cerrar ventana** durante la escena — responde al instante (sin el viejo bug de congelamiento).
|
||||||
|
7. **Audio** — la música debe arrancar cuando toca, los fades suaves, sin cortes.
|
||||||
|
8. **ESC doble-press** — sale limpiamente.
|
||||||
|
|
||||||
|
Tras el **step 10** (limpieza final):
|
||||||
|
- `modulesequence.cpp` tiene ~50 líneas (solo `Go()` mínimo) o desaparece del todo.
|
||||||
|
- El juego entero es jugable de principio a fin.
|
||||||
|
- FPS estable ≥60 con vsync.
|
||||||
|
- Cero referencias a `wait_frame_or_skip` en el código.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cadencia
|
||||||
|
|
||||||
|
Igual que antes: **paso a paso con pausa**. Cada step (0–9) se cierra con build verde + validación visual antes de arrancar el siguiente. No encadeno pasos automáticamente.
|
||||||
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
|
||||||
679
source/core/audio/jail_audio.hpp
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// --- Includes ---
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <stdint.h> // Para uint32_t, uint8_t
|
||||||
|
#include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
|
||||||
|
#include <stdlib.h> // Para free, malloc
|
||||||
|
|
||||||
|
#include <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.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 {
|
||||||
|
JA_CHANNEL_INVALID,
|
||||||
|
JA_CHANNEL_FREE,
|
||||||
|
JA_CHANNEL_PLAYING,
|
||||||
|
JA_CHANNEL_PAUSED,
|
||||||
|
JA_SOUND_DISABLED,
|
||||||
|
};
|
||||||
|
enum JA_Music_state {
|
||||||
|
JA_MUSIC_INVALID,
|
||||||
|
JA_MUSIC_PLAYING,
|
||||||
|
JA_MUSIC_PAUSED,
|
||||||
|
JA_MUSIC_STOPPED,
|
||||||
|
JA_MUSIC_DISABLED,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Struct Definitions ---
|
||||||
|
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
|
||||||
|
#define JA_MAX_GROUPS 2
|
||||||
|
|
||||||
|
struct JA_Sound_t {
|
||||||
|
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
Uint32 length{0};
|
||||||
|
// 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 {
|
||||||
|
JA_Sound_t* sound{nullptr};
|
||||||
|
int pos{0};
|
||||||
|
int times{0};
|
||||||
|
int group{0};
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
JA_Channel_state state{JA_CHANNEL_FREE};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JA_Music_t {
|
||||||
|
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
|
||||||
|
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
|
||||||
|
// d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks
|
||||||
|
// per streaming. Com que stb_vorbis guarda un punter persistent al
|
||||||
|
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
|
||||||
|
// (una reallocation invalidaria el punter que el decoder conserva).
|
||||||
|
std::vector<Uint8> ogg_data;
|
||||||
|
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t
|
||||||
|
|
||||||
|
std::string filename;
|
||||||
|
|
||||||
|
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
JA_Music_state state{JA_MUSIC_INVALID};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Internal Global State (inline, C++17) ---
|
||||||
|
|
||||||
|
inline JA_Music_t* current_music{nullptr};
|
||||||
|
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
|
||||||
|
|
||||||
|
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
inline float JA_musicVolume{1.0f};
|
||||||
|
inline float JA_soundVolume[JA_MAX_GROUPS];
|
||||||
|
inline bool JA_musicEnabled{true};
|
||||||
|
inline bool JA_soundEnabled{true};
|
||||||
|
inline SDL_AudioDeviceID sdlAudioDevice{0};
|
||||||
|
|
||||||
|
// --- 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)
|
||||||
|
static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2;
|
||||||
|
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||||
|
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||||
|
static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192;
|
||||||
|
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||||
|
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||||
|
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
|
||||||
|
|
||||||
|
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
|
||||||
|
// decodificats per canal (0 = EOF de l'stream vorbis).
|
||||||
|
inline int JA_FeedMusicChunk(JA_Music_t* music) {
|
||||||
|
if (!music || !music->vorbis || !music->stream) return 0;
|
||||||
|
|
||||||
|
short chunk[JA_MUSIC_CHUNK_SHORTS];
|
||||||
|
const int num_channels = music->spec.channels;
|
||||||
|
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
|
||||||
|
music->vorbis,
|
||||||
|
num_channels,
|
||||||
|
chunk,
|
||||||
|
JA_MUSIC_CHUNK_SHORTS);
|
||||||
|
if (samples_per_channel <= 0) return 0;
|
||||||
|
|
||||||
|
const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
SDL_PutAudioStreamData(music->stream, chunk, bytes);
|
||||||
|
return samples_per_channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats.
|
||||||
|
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
|
||||||
|
inline void JA_PumpMusic(JA_Music_t* music) {
|
||||||
|
if (!music || !music->vorbis || !music->stream) return;
|
||||||
|
|
||||||
|
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
const int low_water_bytes = static_cast<int>(JA_MUSIC_LOW_WATER_SECONDS * static_cast<float>(bytes_per_second));
|
||||||
|
|
||||||
|
while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) {
|
||||||
|
const int decoded = JA_FeedMusicChunk(music);
|
||||||
|
if (decoded > 0) continue;
|
||||||
|
|
||||||
|
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
|
||||||
|
if (music->times != 0) {
|
||||||
|
stb_vorbis_seek_start(music->vorbis);
|
||||||
|
if (music->times > 0) music->times--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ---
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 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 {
|
||||||
|
float percent = (float)elapsed / (float)incoming_fade.duration_ms;
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
|
||||||
|
// vorbis s'ha esgotat i no queden loops.
|
||||||
|
JA_PumpMusic(current_music);
|
||||||
|
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
|
||||||
|
JA_StopMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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.get(), channels[i].sound->length);
|
||||||
|
if (channels[i].times > 0) channels[i].times--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||||
|
JA_audioSpec = {format, num_channels, freq};
|
||||||
|
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||||
|
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||||
|
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||||
|
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_Quit() {
|
||||||
|
if (outgoing_music.stream) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||||
|
outgoing_music.stream = nullptr;
|
||||||
|
}
|
||||||
|
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
||||||
|
sdlAudioDevice = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Music Functions ---
|
||||||
|
|
||||||
|
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||||
|
if (!buffer || length == 0) return nullptr;
|
||||||
|
|
||||||
|
// Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
|
||||||
|
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||||
|
// persistent al buffer; com que ací no el resize'jem, el .data() és
|
||||||
|
// estable durant tot el cicle de vida del music.
|
||||||
|
auto* music = new JA_Music_t();
|
||||||
|
music->ogg_data.assign(buffer, buffer + length);
|
||||||
|
|
||||||
|
int vorbis_error = 0;
|
||||||
|
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||||
|
static_cast<int>(length),
|
||||||
|
&vorbis_error,
|
||||||
|
nullptr);
|
||||||
|
if (!music->vorbis) {
|
||||||
|
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
|
||||||
|
delete music;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stb_vorbis_info info = stb_vorbis_get_info(music->vorbis);
|
||||||
|
music->spec.channels = info.channels;
|
||||||
|
music->spec.freq = static_cast<int>(info.sample_rate);
|
||||||
|
music->spec.format = SDL_AUDIO_S16;
|
||||||
|
music->state = JA_MUSIC_STOPPED;
|
||||||
|
|
||||||
|
return music;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload amb filename — els callers l'usen per poder comparar la música
|
||||||
|
// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa.
|
||||||
|
inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
|
||||||
|
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), length);
|
||||||
|
if (music && filename) music->filename = filename;
|
||||||
|
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;
|
||||||
|
|
||||||
|
JA_StopMusic();
|
||||||
|
|
||||||
|
current_music = music;
|
||||||
|
current_music->state = JA_MUSIC_PLAYING;
|
||||||
|
current_music->times = loop;
|
||||||
|
|
||||||
|
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
|
||||||
|
// vegada com replays/canvis de track que tornen a la mateixa pista.
|
||||||
|
stb_vorbis_seek_start(current_music->vorbis);
|
||||||
|
|
||||||
|
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||||
|
if (!current_music->stream) {
|
||||||
|
std::cout << "Failed to create audio stream!" << '\n';
|
||||||
|
current_music->state = JA_MUSIC_STOPPED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||||
|
|
||||||
|
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
|
||||||
|
JA_PumpMusic(current_music);
|
||||||
|
|
||||||
|
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) {
|
||||||
|
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_PauseMusic() {
|
||||||
|
if (!JA_musicEnabled) return;
|
||||||
|
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
|
||||||
|
|
||||||
|
current_music->state = JA_MUSIC_PAUSED;
|
||||||
|
SDL_UnbindAudioStream(current_music->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_ResumeMusic() {
|
||||||
|
if (!JA_musicEnabled) return;
|
||||||
|
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return;
|
||||||
|
|
||||||
|
current_music->state = JA_MUSIC_PLAYING;
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_StopMusic() {
|
||||||
|
// 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;
|
||||||
|
if (current_music->stream) {
|
||||||
|
SDL_DestroyAudioStream(current_music->stream);
|
||||||
|
current_music->stream = nullptr;
|
||||||
|
}
|
||||||
|
// Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic.
|
||||||
|
// Rebobinem perquè un futur JA_PlayMusic comence des del principi.
|
||||||
|
if (current_music->vorbis) {
|
||||||
|
stb_vorbis_seek_start(current_music->vorbis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_FadeOutMusic(const int milliseconds) {
|
||||||
|
if (!JA_musicEnabled) return;
|
||||||
|
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
||||||
|
if (!current_music) return JA_MUSIC_INVALID;
|
||||||
|
|
||||||
|
return current_music->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_DeleteMusic(JA_Music_t* music) {
|
||||||
|
if (!music) return;
|
||||||
|
if (current_music == music) {
|
||||||
|
JA_StopMusic();
|
||||||
|
current_music = nullptr;
|
||||||
|
}
|
||||||
|
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
||||||
|
if (music->vorbis) stb_vorbis_close(music->vorbis);
|
||||||
|
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
|
||||||
|
// al destructor de JA_Music_t.
|
||||||
|
delete music;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_SetMusicVolume(float volume) {
|
||||||
|
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
||||||
|
if (current_music && current_music->stream) {
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||||
|
}
|
||||||
|
return JA_musicVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_SetMusicPosition(float /*value*/) {
|
||||||
|
// No implementat amb el backend de streaming.
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_GetMusicPosition() {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_EnableMusic(const bool value) {
|
||||||
|
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
||||||
|
JA_musicEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sound Functions ---
|
||||||
|
|
||||||
|
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||||
|
return sound.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (!JA_soundEnabled || !sound) return -1;
|
||||||
|
|
||||||
|
int channel = 0;
|
||||||
|
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
||||||
|
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
// No hay canal libre, reemplazamos el primero
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JA_PlaySoundOnChannel(sound, channel, loop, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
|
||||||
|
if (!JA_soundEnabled || !sound) return -1;
|
||||||
|
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
|
||||||
|
|
||||||
|
JA_StopChannel(channel);
|
||||||
|
|
||||||
|
channels[channel].sound = sound;
|
||||||
|
channels[channel].times = loop;
|
||||||
|
channels[channel].pos = 0;
|
||||||
|
channels[channel].group = group;
|
||||||
|
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||||
|
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||||
|
|
||||||
|
if (!channels[channel].stream) {
|
||||||
|
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.get(), channels[channel].sound->length);
|
||||||
|
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||||
|
if (!sound) return;
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||||
|
}
|
||||||
|
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
|
||||||
|
delete sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_PauseChannel(const int channel) {
|
||||||
|
if (!JA_soundEnabled) return;
|
||||||
|
|
||||||
|
if (channel == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||||
|
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||||
|
channels[i].state = JA_CHANNEL_PAUSED;
|
||||||
|
SDL_UnbindAudioStream(channels[i].stream);
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
||||||
|
channels[channel].state = JA_CHANNEL_PAUSED;
|
||||||
|
SDL_UnbindAudioStream(channels[channel].stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_ResumeChannel(const int channel) {
|
||||||
|
if (!JA_soundEnabled) return;
|
||||||
|
|
||||||
|
if (channel == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||||
|
if (channels[i].state == JA_CHANNEL_PAUSED) {
|
||||||
|
channels[i].state = JA_CHANNEL_PLAYING;
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
||||||
|
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||||
|
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_StopChannel(const int channel) {
|
||||||
|
if (channel == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if (channels[i].state != JA_CHANNEL_FREE) {
|
||||||
|
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
|
||||||
|
channels[i].stream = nullptr;
|
||||||
|
channels[i].state = JA_CHANNEL_FREE;
|
||||||
|
channels[i].pos = 0;
|
||||||
|
channels[i].sound = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state != JA_CHANNEL_FREE) {
|
||||||
|
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
|
||||||
|
channels[channel].stream = nullptr;
|
||||||
|
channels[channel].state = JA_CHANNEL_FREE;
|
||||||
|
channels[channel].pos = 0;
|
||||||
|
channels[channel].sound = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline JA_Channel_state JA_GetChannelState(const int channel) {
|
||||||
|
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
|
||||||
|
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
|
||||||
|
|
||||||
|
return channels[channel].state;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_SetSoundVolume(float volume, const int group = -1) {
|
||||||
|
const float v = SDL_clamp(volume, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
if (group == -1) {
|
||||||
|
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
|
||||||
|
JA_soundVolume[i] = v;
|
||||||
|
}
|
||||||
|
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
||||||
|
JA_soundVolume[group] = v;
|
||||||
|
} else {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar volum als canals actius.
|
||||||
|
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
||||||
|
if (group == -1 || channels[i].group == group) {
|
||||||
|
if (channels[i].stream) {
|
||||||
|
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void JA_EnableSound(const bool value) {
|
||||||
|
if (!value) {
|
||||||
|
JA_StopChannel(-1);
|
||||||
|
}
|
||||||
|
JA_soundEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float JA_SetVolume(float volume) {
|
||||||
|
float v = JA_SetMusicVolume(volume);
|
||||||
|
JA_SetSoundVolume(v, -1);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
@@ -1,16 +1,73 @@
|
|||||||
#include "core/input/gamepad.hpp"
|
#include "core/input/gamepad.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/jail/jinput.hpp"
|
#include "core/jail/jinput.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/menu.hpp"
|
#include "core/rendering/menu.hpp"
|
||||||
#include "game/options.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
|
|
||||||
namespace Gamepad {
|
namespace Gamepad {
|
||||||
|
|
||||||
static SDL_Gamepad* pad_ = nullptr;
|
static SDL_Gamepad* pad_ = nullptr;
|
||||||
static SDL_JoystickID pad_id_ = 0;
|
static SDL_JoystickID pad_id_ = 0;
|
||||||
|
|
||||||
|
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id
|
||||||
|
// de Chrome/Android no porta Vendor/Product, el parser extreu valors
|
||||||
|
// escombraries, el GUID no està a gamecontrollerdb i el gamepad queda
|
||||||
|
// obert amb un mapping incorrecte). Com el W3C Gamepad API garanteix
|
||||||
|
// layout estàndard quan mapping=="standard", injectem un mapping SDL
|
||||||
|
// amb eixe layout per al GUID del joystick abans d'obrir-lo com gamepad.
|
||||||
|
// Fora d'Emscripten és un no-op.
|
||||||
|
static void installWebStandardMapping(SDL_JoystickID jid) {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
|
||||||
|
char guidStr[33];
|
||||||
|
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
|
||||||
|
const char* name = SDL_GetJoystickNameForID(jid);
|
||||||
|
if (!name || !*name) name = "Standard Gamepad";
|
||||||
|
|
||||||
|
char mapping[512];
|
||||||
|
SDL_snprintf(mapping, sizeof(mapping),
|
||||||
|
"%s,%s,"
|
||||||
|
"a:b0,b:b1,x:b2,y:b3,"
|
||||||
|
"leftshoulder:b4,rightshoulder:b5,"
|
||||||
|
"lefttrigger:b6,righttrigger:b7,"
|
||||||
|
"back:b8,start:b9,"
|
||||||
|
"leftstick:b10,rightstick:b11,"
|
||||||
|
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
|
||||||
|
"guide:b16,"
|
||||||
|
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
|
||||||
|
"platform:Emscripten",
|
||||||
|
guidStr,
|
||||||
|
name);
|
||||||
|
SDL_AddGamepadMapping(mapping);
|
||||||
|
#else
|
||||||
|
(void)jid;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recorta el nom visible del mando: trim des del primer '(' o '['
|
||||||
|
// (per a evitar coses com "Retroid Controller (vendor: 1001) ..."),
|
||||||
|
// elimina espais finals i talla a 25 caràcters.
|
||||||
|
static 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)
|
// Dead-zone del stick esquerre (rang Sint16: -32768..32767)
|
||||||
static constexpr Sint16 STICK_DEADZONE = 12000;
|
static constexpr Sint16 STICK_DEADZONE = 12000;
|
||||||
|
|
||||||
@@ -19,22 +76,41 @@ namespace Gamepad {
|
|||||||
static bool prev_down_ = false;
|
static bool prev_down_ = false;
|
||||||
static bool prev_left_ = false;
|
static bool prev_left_ = false;
|
||||||
static bool prev_right_ = false;
|
static bool prev_right_ = false;
|
||||||
static bool prev_a_ = false;
|
static bool prev_south_ = false;
|
||||||
static bool prev_b_ = false;
|
static bool prev_east_ = false;
|
||||||
|
static bool prev_west_ = false;
|
||||||
|
static bool prev_north_ = false;
|
||||||
static bool prev_start_ = false;
|
static bool prev_start_ = false;
|
||||||
static bool prev_back_ = false;
|
static bool prev_back_ = false;
|
||||||
|
|
||||||
|
static void notify(const std::string& name, const char* status_key) {
|
||||||
|
std::string msg = name.empty() ? "Gamepad" : name;
|
||||||
|
msg += ' ';
|
||||||
|
msg += Locale::get(status_key);
|
||||||
|
Overlay::showNotification(msg.c_str(), 2.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void notifyConnected(const std::string& name) { notify(name, "notifications.gamepad_connected"); }
|
||||||
|
static void notifyDisconnected(const std::string& name) { notify(name, "notifications.gamepad_disconnected"); }
|
||||||
|
|
||||||
|
// Obri el primer joystick disponible que siga reconegut com a gamepad
|
||||||
|
// (o que ho esdevinga després d'injectar el mapping web estàndard).
|
||||||
static void openFirstGamepad() {
|
static void openFirstGamepad() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
SDL_JoystickID* ids = SDL_GetGamepads(&count);
|
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||||
if (ids && count > 0) {
|
if (ids) {
|
||||||
pad_ = SDL_OpenGamepad(ids[0]);
|
for (int i = 0; i < count; ++i) {
|
||||||
if (pad_) {
|
installWebStandardMapping(ids[i]);
|
||||||
pad_id_ = ids[0];
|
if (!SDL_IsGamepad(ids[i])) continue;
|
||||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
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() {
|
void init() {
|
||||||
@@ -65,17 +141,26 @@ namespace Gamepad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleEvent(const SDL_Event& event) {
|
void handleEvent(const SDL_Event& event) {
|
||||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
|
// A Emscripten els dispositius web entren com a JOYSTICK_ADDED (no
|
||||||
|
// GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
|
||||||
|
// injectem el mapping estàndard abans d'obrir el mando.
|
||||||
|
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
|
||||||
if (!pad_) {
|
if (!pad_) {
|
||||||
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_) {
|
if (pad_) {
|
||||||
pad_id_ = event.gdevice.which;
|
pad_id_ = jid;
|
||||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
std::string name = prettyName(SDL_GetGamepadName(pad_));
|
||||||
|
SDL_Log("Gamepad connectat: %s", name.c_str());
|
||||||
|
notifyConnected(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||||
if (pad_ && event.gdevice.which == pad_id_) {
|
if (pad_ && event.jdevice.which == pad_id_) {
|
||||||
SDL_Log("Gamepad desconnectat");
|
std::string saved_name = prettyName(SDL_GetGamepadName(pad_));
|
||||||
|
SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
|
||||||
SDL_CloseGamepad(pad_);
|
SDL_CloseGamepad(pad_);
|
||||||
pad_ = nullptr;
|
pad_ = nullptr;
|
||||||
pad_id_ = 0;
|
pad_id_ = 0;
|
||||||
@@ -84,6 +169,7 @@ namespace Gamepad {
|
|||||||
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
|
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
|
||||||
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
|
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
|
||||||
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, 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 lt = dlt || slt;
|
||||||
bool rt = drt || srt;
|
bool rt = drt || srt;
|
||||||
|
|
||||||
// Botons
|
// Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
|
||||||
bool a = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH); // A/Cross
|
bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH);
|
||||||
bool b = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST); // B/Circle
|
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 start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START);
|
||||||
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
|
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
|
||||||
|
|
||||||
// Start → obre/tanca menú (flanc)
|
// Select (Back) → obre/tanca menú de servei (flanc)
|
||||||
if (start && !prev_start_) pushKey(Options::keys_gui.menu_toggle);
|
if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle"));
|
||||||
// Back → ESC (flanc)
|
// Start → pausa (flanc)
|
||||||
if (back && !prev_back_) pushKey(SDL_SCANCODE_ESCAPE);
|
if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle"));
|
||||||
|
|
||||||
if (Menu::isOpen()) {
|
if (Menu::isOpen()) {
|
||||||
// Navegació del menú per flanc
|
// Navegació del menú per flanc
|
||||||
@@ -142,8 +230,9 @@ namespace Gamepad {
|
|||||||
if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN);
|
if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN);
|
||||||
if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT);
|
if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT);
|
||||||
if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT);
|
if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT);
|
||||||
if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN);
|
// EAST accepta, SOUTH cancela / endarrere
|
||||||
if (b && !prev_b_) pushKey(SDL_SCANCODE_BACKSPACE);
|
if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN);
|
||||||
|
if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE);
|
||||||
|
|
||||||
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
||||||
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
||||||
@@ -156,16 +245,21 @@ namespace Gamepad {
|
|||||||
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
|
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
|
||||||
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
|
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
|
||||||
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
|
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
|
||||||
// Botó A al joc: emet Enter per avançar seqüències (JI_AnyKey)
|
// Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic)
|
||||||
if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN);
|
if ((south && !prev_south_) || (east && !prev_east_) ||
|
||||||
|
(west && !prev_west_) || (north && !prev_north_)) {
|
||||||
|
pushKey(SDL_SCANCODE_RETURN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prev_up_ = up;
|
prev_up_ = up;
|
||||||
prev_down_ = dn;
|
prev_down_ = dn;
|
||||||
prev_left_ = lt;
|
prev_left_ = lt;
|
||||||
prev_right_ = rt;
|
prev_right_ = rt;
|
||||||
prev_a_ = a;
|
prev_south_ = south;
|
||||||
prev_b_ = b;
|
prev_east_ = east;
|
||||||
|
prev_west_ = west;
|
||||||
|
prev_north_ = north;
|
||||||
prev_start_ = start;
|
prev_start_ = start;
|
||||||
prev_back_ = back;
|
prev_back_ = back;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/jail/jinput.hpp"
|
#include "core/jail/jinput.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/overlay.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
@@ -19,14 +20,14 @@ namespace GlobalInputs {
|
|||||||
static bool ss_prev = false;
|
static bool ss_prev = false;
|
||||||
static bool next_shader_prev = false;
|
static bool next_shader_prev = false;
|
||||||
static bool next_preset_prev = false;
|
static bool next_preset_prev = false;
|
||||||
static bool stretch_filter_prev = false;
|
static bool texture_filter_prev = false;
|
||||||
static bool render_info_prev = false;
|
static bool render_info_prev = false;
|
||||||
|
|
||||||
auto handle() -> bool {
|
auto handle() -> bool {
|
||||||
bool consumed = false;
|
bool consumed = false;
|
||||||
|
|
||||||
// F1 — Reduir zoom
|
// 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) {
|
if (dec_zoom && !dec_zoom_prev) {
|
||||||
Screen::get()->decZoom();
|
Screen::get()->decZoom();
|
||||||
char msg[32];
|
char msg[32];
|
||||||
@@ -37,7 +38,7 @@ namespace GlobalInputs {
|
|||||||
dec_zoom_prev = dec_zoom;
|
dec_zoom_prev = dec_zoom;
|
||||||
|
|
||||||
// F2 — Augmentar 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) {
|
if (inc_zoom && !inc_zoom_prev) {
|
||||||
Screen::get()->incZoom();
|
Screen::get()->incZoom();
|
||||||
char msg[32];
|
char msg[32];
|
||||||
@@ -48,7 +49,7 @@ namespace GlobalInputs {
|
|||||||
inc_zoom_prev = inc_zoom;
|
inc_zoom_prev = inc_zoom;
|
||||||
|
|
||||||
// F3 — Toggle pantalla completa
|
// F3 — Toggle pantalla completa
|
||||||
bool fullscreen = JI_KeyPressed(Options::keys_gui.fullscreen);
|
bool fullscreen = JI_KeyPressed(KeyConfig::scancode("fullscreen"));
|
||||||
if (fullscreen && !fullscreen_prev) {
|
if (fullscreen && !fullscreen_prev) {
|
||||||
Screen::get()->toggleFullscreen();
|
Screen::get()->toggleFullscreen();
|
||||||
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
|
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
|
||||||
@@ -57,7 +58,7 @@ namespace GlobalInputs {
|
|||||||
fullscreen_prev = fullscreen;
|
fullscreen_prev = fullscreen;
|
||||||
|
|
||||||
// F4 — Toggle shaders
|
// F4 — Toggle shaders
|
||||||
bool shader = JI_KeyPressed(Options::keys_gui.toggle_shader);
|
bool shader = JI_KeyPressed(KeyConfig::scancode("toggle_shader"));
|
||||||
if (shader && !shader_prev) {
|
if (shader && !shader_prev) {
|
||||||
Screen::get()->toggleShaders();
|
Screen::get()->toggleShaders();
|
||||||
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
|
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
|
||||||
@@ -66,7 +67,7 @@ namespace GlobalInputs {
|
|||||||
shader_prev = shader;
|
shader_prev = shader;
|
||||||
|
|
||||||
// F5 — Toggle aspect ratio 4:3
|
// 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) {
|
if (aspect && !aspect_prev) {
|
||||||
Screen::get()->toggleAspectRatio();
|
Screen::get()->toggleAspectRatio();
|
||||||
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
||||||
@@ -75,47 +76,52 @@ namespace GlobalInputs {
|
|||||||
aspect_prev = aspect;
|
aspect_prev = aspect;
|
||||||
|
|
||||||
// F6 — Toggle supersampling
|
// F6 — Toggle supersampling
|
||||||
bool ss = JI_KeyPressed(Options::keys_gui.toggle_supersampling);
|
bool ss = JI_KeyPressed(KeyConfig::scancode("toggle_supersampling"));
|
||||||
if (ss && !ss_prev) {
|
if (ss && !ss_prev) {
|
||||||
Screen::get()->toggleSupersampling();
|
if (Screen::get()->toggleSupersampling()) {
|
||||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ss) consumed = true;
|
if (ss) consumed = true;
|
||||||
ss_prev = ss;
|
ss_prev = ss;
|
||||||
|
|
||||||
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
|
// 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) {
|
if (next_shader && !next_shader_prev) {
|
||||||
Screen::get()->nextShaderType();
|
if (Screen::get()->nextShaderType()) {
|
||||||
char msg[64];
|
char msg[64];
|
||||||
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
|
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
|
||||||
Overlay::showNotification(msg);
|
Overlay::showNotification(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (next_shader) consumed = true;
|
if (next_shader) consumed = true;
|
||||||
next_shader_prev = next_shader;
|
next_shader_prev = next_shader;
|
||||||
|
|
||||||
// F8 — Pròxim preset del shader actiu
|
// 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) {
|
if (next_preset && !next_preset_prev) {
|
||||||
Screen::get()->nextPreset();
|
if (Screen::get()->nextPreset()) {
|
||||||
char msg[64];
|
char msg[64];
|
||||||
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
|
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
|
||||||
Overlay::showNotification(msg);
|
Overlay::showNotification(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (next_preset) consumed = true;
|
if (next_preset) consumed = true;
|
||||||
next_preset_prev = next_preset;
|
next_preset_prev = next_preset;
|
||||||
|
|
||||||
// F9 — Toggle filtre d'estirament 4:3 (NEAREST ↔ LINEAR)
|
// F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
|
||||||
bool stretch_filter = JI_KeyPressed(Options::keys_gui.toggle_stretch_filter);
|
bool texture_filter = JI_KeyPressed(KeyConfig::scancode("cycle_texture_filter"));
|
||||||
if (stretch_filter && !stretch_filter_prev) {
|
if (texture_filter && !texture_filter_prev) {
|
||||||
Screen::get()->toggleStretchFilter();
|
Screen::get()->cycleTextureFilter(+1);
|
||||||
Overlay::showNotification(Options::video.stretch_filter_linear ? Locale::get("notifications.filter_linear") : Locale::get("notifications.filter_nearest"));
|
Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||||
|
? Locale::get("notifications.filter_linear")
|
||||||
|
: Locale::get("notifications.filter_nearest"));
|
||||||
}
|
}
|
||||||
if (stretch_filter) consumed = true;
|
if (texture_filter) consumed = true;
|
||||||
stretch_filter_prev = stretch_filter;
|
texture_filter_prev = texture_filter;
|
||||||
|
|
||||||
// F10 — Toggle render info (FPS, driver, shader)
|
// 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) {
|
if (render_info && !render_info_prev) {
|
||||||
Overlay::toggleRenderInfo();
|
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,450 +0,0 @@
|
|||||||
#ifndef JA_USESDLMIXER
|
|
||||||
#include "core/jail/jail_audio.hpp"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
#include "external/stb_vorbis.h"
|
|
||||||
|
|
||||||
#define JA_MAX_SIMULTANEOUS_CHANNELS 5
|
|
||||||
|
|
||||||
struct JA_Sound_t {
|
|
||||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
|
||||||
Uint32 length{0};
|
|
||||||
Uint8* buffer{NULL};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct JA_Channel_t {
|
|
||||||
JA_Sound_t* sound{nullptr};
|
|
||||||
int pos{0};
|
|
||||||
int times{0};
|
|
||||||
SDL_AudioStream* stream{nullptr};
|
|
||||||
JA_Channel_state state{JA_CHANNEL_FREE};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct JA_Music_t {
|
|
||||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
|
||||||
Uint32 length{0};
|
|
||||||
Uint8* buffer{nullptr};
|
|
||||||
char* filename{nullptr};
|
|
||||||
|
|
||||||
int pos{0};
|
|
||||||
int times{0};
|
|
||||||
SDL_AudioStream* stream{nullptr};
|
|
||||||
JA_Music_state state{JA_MUSIC_INVALID};
|
|
||||||
};
|
|
||||||
|
|
||||||
JA_Music_t* current_music{nullptr};
|
|
||||||
JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
|
|
||||||
|
|
||||||
SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
|
|
||||||
float JA_musicVolume{1.0f};
|
|
||||||
float JA_soundVolume{0.5f};
|
|
||||||
bool JA_musicEnabled{true};
|
|
||||||
bool JA_soundEnabled{true};
|
|
||||||
SDL_AudioDeviceID sdlAudioDevice{0};
|
|
||||||
SDL_TimerID JA_timerID{0};
|
|
||||||
|
|
||||||
bool fading = false;
|
|
||||||
int fade_start_time;
|
|
||||||
int fade_duration;
|
|
||||||
int fade_initial_volume;
|
|
||||||
|
|
||||||
/*
|
|
||||||
void audioCallback(void * userdata, uint8_t * stream, int len) {
|
|
||||||
SDL_memset(stream, 0, len);
|
|
||||||
if (current_music != NULL && current_music->state == JA_MUSIC_PLAYING) {
|
|
||||||
const int size = SDL_min(len, current_music->samples*2-current_music->pos);
|
|
||||||
SDL_MixAudioFormat(stream, (Uint8*)(current_music->output+current_music->pos), AUDIO_S16, size, JA_musicVolume);
|
|
||||||
current_music->pos += size/2;
|
|
||||||
if (size < len) {
|
|
||||||
if (current_music->times != 0) {
|
|
||||||
SDL_MixAudioFormat(stream+size, (Uint8*)current_music->output, AUDIO_S16, len-size, JA_musicVolume);
|
|
||||||
current_music->pos = (len-size)/2;
|
|
||||||
if (current_music->times > 0) current_music->times--;
|
|
||||||
} else {
|
|
||||||
current_music->pos = 0;
|
|
||||||
current_music->state = JA_MUSIC_STOPPED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Mixar els channels mi amol
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
|
||||||
const int size = SDL_min(len, channels[i].sound->length - channels[i].pos);
|
|
||||||
SDL_MixAudioFormat(stream, channels[i].sound->buffer + channels[i].pos, AUDIO_S16, size, JA_soundVolume);
|
|
||||||
channels[i].pos += size;
|
|
||||||
if (size < len) {
|
|
||||||
if (channels[i].times != 0) {
|
|
||||||
SDL_MixAudioFormat(stream + size, channels[i].sound->buffer, AUDIO_S16, len-size, JA_soundVolume);
|
|
||||||
channels[i].pos = len-size;
|
|
||||||
if (channels[i].times > 0) channels[i].times--;
|
|
||||||
} else {
|
|
||||||
JA_StopChannel(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
Uint32 JA_UpdateCallback(void* userdata, SDL_TimerID timerID, Uint32 interval) {
|
|
||||||
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
|
|
||||||
if (fading) {
|
|
||||||
int time = SDL_GetTicks();
|
|
||||||
if (time > (fade_start_time + fade_duration)) {
|
|
||||||
fading = false;
|
|
||||||
JA_StopMusic();
|
|
||||||
return 30;
|
|
||||||
} else {
|
|
||||||
const int time_passed = time - fade_start_time;
|
|
||||||
const float percent = (float)time_passed / (float)fade_duration;
|
|
||||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_music->times != 0) {
|
|
||||||
if (SDL_GetAudioStreamAvailable(current_music->stream) < int(current_music->length / 2)) {
|
|
||||||
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
|
|
||||||
}
|
|
||||||
if (current_music->times > 0) current_music->times--;
|
|
||||||
} else {
|
|
||||||
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JA_soundEnabled) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
|
|
||||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
|
||||||
if (channels[i].times != 0) {
|
|
||||||
if (SDL_GetAudioStreamAvailable(channels[i].stream) < int(channels[i].sound->length / 2)) {
|
|
||||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
|
|
||||||
if (channels[i].times > 0) channels[i].times--;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SDL_Log("Iniciant JailAudio...");
|
|
||||||
JA_audioSpec = {format, num_channels, freq};
|
|
||||||
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
|
||||||
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
|
||||||
SDL_Log((sdlAudioDevice == 0) ? "Failed to initialize SDL audio!\n" : "OK!\n");
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
|
||||||
// SDL_PauseAudioDevice(sdlAudioDevice);
|
|
||||||
JA_timerID = SDL_AddTimer(30, JA_UpdateCallback, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_Quit() {
|
|
||||||
if (JA_timerID) SDL_RemoveTimer(JA_timerID);
|
|
||||||
|
|
||||||
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
|
|
||||||
sdlAudioDevice = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
|
|
||||||
JA_Music_t* music = new JA_Music_t();
|
|
||||||
|
|
||||||
int chan, samplerate;
|
|
||||||
short* output;
|
|
||||||
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
|
|
||||||
|
|
||||||
music->spec.channels = chan;
|
|
||||||
music->spec.freq = samplerate;
|
|
||||||
music->spec.format = SDL_AUDIO_S16;
|
|
||||||
music->buffer = (Uint8*)SDL_malloc(music->length);
|
|
||||||
SDL_memcpy(music->buffer, output, music->length);
|
|
||||||
free(output);
|
|
||||||
music->pos = 0;
|
|
||||||
music->state = JA_MUSIC_STOPPED;
|
|
||||||
if (filename) {
|
|
||||||
music->filename = (char*)malloc(strlen(filename) + 1);
|
|
||||||
strcpy(music->filename, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
return music;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Music_t* JA_LoadMusic(const char* filename) {
|
|
||||||
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
|
|
||||||
FILE* f = fopen(filename, "rb");
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long fsize = ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
Uint8* buffer = (Uint8*)malloc(fsize + 1);
|
|
||||||
if (fread(buffer, fsize, 1, f) != 1) return NULL;
|
|
||||||
fclose(f);
|
|
||||||
|
|
||||||
JA_Music_t* music = JA_LoadMusic(buffer, fsize, filename);
|
|
||||||
|
|
||||||
free(buffer);
|
|
||||||
|
|
||||||
return music;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_PlayMusic(JA_Music_t* music, const int loop) {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
|
|
||||||
JA_StopMusic();
|
|
||||||
|
|
||||||
current_music = music;
|
|
||||||
current_music->pos = 0;
|
|
||||||
current_music->state = JA_MUSIC_PLAYING;
|
|
||||||
current_music->times = loop;
|
|
||||||
|
|
||||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
|
||||||
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
|
|
||||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
|
||||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
|
||||||
// SDL_ResumeAudioStreamDevice(current_music->stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* JA_GetMusicFilename(JA_Music_t* music) {
|
|
||||||
if (!music) music = current_music;
|
|
||||||
return music->filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_PauseMusic() {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
|
|
||||||
|
|
||||||
current_music->state = JA_MUSIC_PAUSED;
|
|
||||||
// SDL_PauseAudioStreamDevice(current_music->stream);
|
|
||||||
SDL_UnbindAudioStream(current_music->stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_ResumeMusic() {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
|
|
||||||
|
|
||||||
current_music->state = JA_MUSIC_PLAYING;
|
|
||||||
// SDL_ResumeAudioStreamDevice(current_music->stream);
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_StopMusic() {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
|
|
||||||
|
|
||||||
current_music->pos = 0;
|
|
||||||
current_music->state = JA_MUSIC_STOPPED;
|
|
||||||
// SDL_PauseAudioStreamDevice(current_music->stream);
|
|
||||||
SDL_DestroyAudioStream(current_music->stream);
|
|
||||||
current_music->stream = nullptr;
|
|
||||||
free(current_music->filename);
|
|
||||||
current_music->filename = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_FadeOutMusic(const int milliseconds) {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
|
|
||||||
|
|
||||||
fading = true;
|
|
||||||
fade_start_time = SDL_GetTicks();
|
|
||||||
fade_duration = milliseconds;
|
|
||||||
fade_initial_volume = JA_musicVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Music_state JA_GetMusicState() {
|
|
||||||
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
|
||||||
if (!current_music) return JA_MUSIC_INVALID;
|
|
||||||
|
|
||||||
return current_music->state;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_DeleteMusic(JA_Music_t* music) {
|
|
||||||
if (current_music == music) current_music = nullptr;
|
|
||||||
SDL_free(music->buffer);
|
|
||||||
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
|
||||||
delete music;
|
|
||||||
}
|
|
||||||
|
|
||||||
float JA_SetMusicVolume(float volume) {
|
|
||||||
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
|
||||||
if (current_music) SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
|
||||||
return JA_musicVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_SetMusicPosition(float value) {
|
|
||||||
if (!current_music) return;
|
|
||||||
current_music->pos = value * current_music->spec.freq;
|
|
||||||
}
|
|
||||||
|
|
||||||
float JA_GetMusicPosition() {
|
|
||||||
if (!current_music) return 0;
|
|
||||||
return float(current_music->pos) / float(current_music->spec.freq);
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_EnableMusic(const bool value) {
|
|
||||||
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
|
||||||
|
|
||||||
JA_musicEnabled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
|
|
||||||
JA_Sound_t* sound = new JA_Sound_t();
|
|
||||||
sound->buffer = buffer;
|
|
||||||
sound->length = length;
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
|
||||||
JA_Sound_t* sound = new JA_Sound_t();
|
|
||||||
SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length);
|
|
||||||
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Sound_t* JA_LoadSound(const char* filename) {
|
|
||||||
JA_Sound_t* sound = new JA_Sound_t();
|
|
||||||
SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length);
|
|
||||||
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
int JA_PlaySound(JA_Sound_t* sound, const int loop) {
|
|
||||||
if (!JA_soundEnabled) return -1;
|
|
||||||
|
|
||||||
int channel = 0;
|
|
||||||
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
|
||||||
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
|
|
||||||
JA_StopChannel(channel);
|
|
||||||
|
|
||||||
channels[channel].sound = sound;
|
|
||||||
channels[channel].times = loop;
|
|
||||||
channels[channel].pos = 0;
|
|
||||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
|
||||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
|
||||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
|
||||||
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume);
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
|
||||||
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop) {
|
|
||||||
if (!JA_soundEnabled) return -1;
|
|
||||||
|
|
||||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
|
|
||||||
JA_StopChannel(channel);
|
|
||||||
|
|
||||||
channels[channel].sound = sound;
|
|
||||||
channels[channel].times = loop;
|
|
||||||
channels[channel].pos = 0;
|
|
||||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
|
||||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
|
||||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
|
||||||
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume);
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
|
||||||
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_DeleteSound(JA_Sound_t* sound) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
|
||||||
}
|
|
||||||
SDL_free(sound->buffer);
|
|
||||||
delete sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_PauseChannel(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return;
|
|
||||||
|
|
||||||
if (channel == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
|
||||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
|
||||||
channels[i].state = JA_CHANNEL_PAUSED;
|
|
||||||
// SDL_PauseAudioStreamDevice(channels[i].stream);
|
|
||||||
SDL_UnbindAudioStream(channels[i].stream);
|
|
||||||
}
|
|
||||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
|
||||||
channels[channel].state = JA_CHANNEL_PAUSED;
|
|
||||||
// SDL_PauseAudioStreamDevice(channels[channel].stream);
|
|
||||||
SDL_UnbindAudioStream(channels[channel].stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_ResumeChannel(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return;
|
|
||||||
|
|
||||||
if (channel == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
|
||||||
if (channels[i].state == JA_CHANNEL_PAUSED) {
|
|
||||||
channels[i].state = JA_CHANNEL_PLAYING;
|
|
||||||
// SDL_ResumeAudioStreamDevice(channels[i].stream);
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
|
||||||
}
|
|
||||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
|
||||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
|
||||||
// SDL_ResumeAudioStreamDevice(channels[channel].stream);
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_StopChannel(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return;
|
|
||||||
|
|
||||||
if (channel == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if (channels[i].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[i].stream);
|
|
||||||
channels[i].stream = nullptr;
|
|
||||||
channels[i].state = JA_CHANNEL_FREE;
|
|
||||||
channels[i].pos = 0;
|
|
||||||
channels[i].sound = NULL;
|
|
||||||
}
|
|
||||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
if (channels[channel].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[channel].stream);
|
|
||||||
channels[channel].stream = nullptr;
|
|
||||||
channels[channel].state = JA_CHANNEL_FREE;
|
|
||||||
channels[channel].pos = 0;
|
|
||||||
channels[channel].sound = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Channel_state JA_GetChannelState(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
|
|
||||||
|
|
||||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
|
|
||||||
|
|
||||||
return channels[channel].state;
|
|
||||||
}
|
|
||||||
|
|
||||||
float JA_SetSoundVolume(float volume) {
|
|
||||||
JA_soundVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
|
||||||
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
|
||||||
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED))
|
|
||||||
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume);
|
|
||||||
|
|
||||||
return JA_soundVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JA_EnableSound(const bool value) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if (channels[i].state == JA_CHANNEL_PLAYING) JA_StopChannel(i);
|
|
||||||
}
|
|
||||||
JA_soundEnabled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
float JA_SetVolume(float volume) {
|
|
||||||
JA_SetSoundVolume(JA_SetMusicVolume(volume) / 2.0f);
|
|
||||||
|
|
||||||
return JA_musicVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
enum JA_Channel_state { JA_CHANNEL_INVALID,
|
|
||||||
JA_CHANNEL_FREE,
|
|
||||||
JA_CHANNEL_PLAYING,
|
|
||||||
JA_CHANNEL_PAUSED,
|
|
||||||
JA_SOUND_DISABLED };
|
|
||||||
enum JA_Music_state { JA_MUSIC_INVALID,
|
|
||||||
JA_MUSIC_PLAYING,
|
|
||||||
JA_MUSIC_PAUSED,
|
|
||||||
JA_MUSIC_STOPPED,
|
|
||||||
JA_MUSIC_DISABLED };
|
|
||||||
|
|
||||||
struct JA_Sound_t;
|
|
||||||
struct JA_Music_t;
|
|
||||||
|
|
||||||
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels);
|
|
||||||
void JA_Quit();
|
|
||||||
|
|
||||||
JA_Music_t* JA_LoadMusic(const char* filename);
|
|
||||||
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename = nullptr);
|
|
||||||
void JA_PlayMusic(JA_Music_t* music, const int loop = -1);
|
|
||||||
char* JA_GetMusicFilename(JA_Music_t* music = nullptr);
|
|
||||||
void JA_PauseMusic();
|
|
||||||
void JA_ResumeMusic();
|
|
||||||
void JA_StopMusic();
|
|
||||||
void JA_FadeOutMusic(const int milliseconds);
|
|
||||||
JA_Music_state JA_GetMusicState();
|
|
||||||
void JA_DeleteMusic(JA_Music_t* music);
|
|
||||||
float JA_SetMusicVolume(float volume);
|
|
||||||
void JA_SetMusicPosition(float value);
|
|
||||||
float JA_GetMusicPosition();
|
|
||||||
void JA_EnableMusic(const bool value);
|
|
||||||
|
|
||||||
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length);
|
|
||||||
JA_Sound_t* JA_LoadSound(Uint8* buffer, Uint32 length);
|
|
||||||
JA_Sound_t* JA_LoadSound(const char* filename);
|
|
||||||
int JA_PlaySound(JA_Sound_t* sound, const int loop = 0);
|
|
||||||
int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0);
|
|
||||||
void JA_PauseChannel(const int channel);
|
|
||||||
void JA_ResumeChannel(const int channel);
|
|
||||||
void JA_StopChannel(const int channel);
|
|
||||||
JA_Channel_state JA_GetChannelState(const int channel);
|
|
||||||
void JA_DeleteSound(JA_Sound_t* sound);
|
|
||||||
float JA_SetSoundVolume(float volume);
|
|
||||||
void JA_EnableSound(const bool value);
|
|
||||||
|
|
||||||
float JA_SetVolume(float volume);
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#include "core/jail/jdraw8.hpp"
|
#include "core/jail/jdraw8.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/system/director.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#if defined(__clang__)
|
#if defined(__clang__)
|
||||||
#pragma clang diagnostic push
|
#pragma clang diagnostic push
|
||||||
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
||||||
@@ -18,20 +19,23 @@
|
|||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
JD8_Surface screen = NULL;
|
JD8_Surface screen = nullptr;
|
||||||
JD8_Palette main_palette = NULL;
|
JD8_Palette main_palette = nullptr;
|
||||||
Uint32* pixel_data = NULL;
|
Uint32* pixel_data = nullptr;
|
||||||
|
|
||||||
void JD8_Init() {
|
void JD8_Init() {
|
||||||
screen = (JD8_Surface)calloc(1, 64000);
|
screen = new Uint8[64000]{};
|
||||||
main_palette = (JD8_Palette)calloc(1, 768);
|
main_palette = new Color[256]{};
|
||||||
pixel_data = (Uint32*)calloc(1, 320 * 200 * 4);
|
pixel_data = new Uint32[320 * 200]{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void JD8_Quit() {
|
void JD8_Quit() {
|
||||||
if (screen != NULL) free(screen);
|
delete[] screen;
|
||||||
if (main_palette != NULL) free(main_palette);
|
delete[] main_palette;
|
||||||
if (pixel_data != NULL) free(pixel_data);
|
delete[] pixel_data;
|
||||||
|
screen = nullptr;
|
||||||
|
main_palette = nullptr;
|
||||||
|
pixel_data = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void JD8_ClearScreen(Uint8 color) {
|
void JD8_ClearScreen(Uint8 color) {
|
||||||
@@ -39,45 +43,71 @@ void JD8_ClearScreen(Uint8 color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JD8_Surface JD8_NewSurface() {
|
JD8_Surface JD8_NewSurface() {
|
||||||
JD8_Surface surface = (JD8_Surface)malloc(64000);
|
return new Uint8[64000]{};
|
||||||
memset(surface, 0, 64000);
|
}
|
||||||
return surface;
|
|
||||||
|
// 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) {
|
JD8_Surface JD8_LoadSurface(const char* file) {
|
||||||
int filesize = 0;
|
// Prova primer el Resource::Cache. Si l'asset és precarregat, copiem
|
||||||
char* buffer = file_getfilebuffer(file, filesize);
|
// 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;
|
unsigned short w, h;
|
||||||
Uint8* pixels = LoadGif((unsigned char*)buffer, &w, &h);
|
Uint8* pixels = LoadGif(buffer.data(), &w, &h);
|
||||||
|
if (pixels == nullptr) {
|
||||||
free(buffer);
|
|
||||||
|
|
||||||
if (pixels == NULL) {
|
|
||||||
printf("Unable to load bitmap: %s\n", SDL_GetError());
|
printf("Unable to load bitmap: %s\n", SDL_GetError());
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
JD8_Surface image = JD8_NewSurface();
|
JD8_Surface image = JD8_NewSurface();
|
||||||
memcpy(image, pixels, 64000);
|
memcpy(image, pixels, 64000);
|
||||||
|
|
||||||
free(pixels);
|
free(pixels);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
JD8_Palette JD8_LoadPalette(const char* file) {
|
JD8_Palette JD8_LoadPalette(const char* file) {
|
||||||
int filesize = 0;
|
// Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
|
||||||
char* buffer = NULL;
|
// — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
|
||||||
buffer = file_getfilebuffer(file, filesize);
|
// l'ownership a `JD8_SetScreenPalette`).
|
||||||
|
JD8_Palette palette = new Color[256];
|
||||||
|
|
||||||
JD8_Palette palette = (JD8_Palette)LoadPalette((unsigned char*)buffer);
|
if (Resource::Cache::get() != nullptr) {
|
||||||
|
try {
|
||||||
|
const auto& cached = Resource::Cache::get()->getPaletteBytes(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;
|
return palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
void JD8_SetScreenPalette(JD8_Palette palette) {
|
void JD8_SetScreenPalette(JD8_Palette palette) {
|
||||||
if (main_palette == palette) return;
|
if (main_palette == palette) return;
|
||||||
if (main_palette != NULL) free(main_palette);
|
delete[] main_palette;
|
||||||
main_palette = palette;
|
main_palette = palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +117,23 @@ void JD8_FillSquare(int ini, int height, Uint8 color) {
|
|||||||
memset(&screen[offset], color, size);
|
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) {
|
void JD8_Blit(JD8_Surface surface) {
|
||||||
memcpy(screen, surface, 64000);
|
memcpy(screen, surface, 64000);
|
||||||
}
|
}
|
||||||
@@ -159,17 +206,24 @@ void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
void JD8_Flip() {
|
void JD8_Flip() {
|
||||||
|
// Converteix el framebuffer indexat (paletted) a ARGB (pixel_data).
|
||||||
|
// El Director crida aquesta funció després del tick de cada escena
|
||||||
|
// per preparar el frame abans de presentar-lo. Ja no fa yield —
|
||||||
|
// tot corre en un sol thread sense fibers des de Phase B.2.
|
||||||
for (int x = 0; x < 320; x++) {
|
for (int x = 0; x < 320; x++) {
|
||||||
for (int y = 0; y < 200; y++) {
|
for (int y = 0; y < 200; y++) {
|
||||||
Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16);
|
Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16);
|
||||||
pixel_data[x + (y * 320)] = color;
|
pixel_data[x + (y * 320)] = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Director::get()->publishFrame(pixel_data);
|
}
|
||||||
|
|
||||||
|
Uint32* JD8_GetFramebuffer() {
|
||||||
|
return pixel_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void JD8_FreeSurface(JD8_Surface surface) {
|
void JD8_FreeSurface(JD8_Surface surface) {
|
||||||
free(surface);
|
delete[] surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) {
|
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) {
|
||||||
@@ -186,44 +240,78 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
|
|||||||
main_palette[index].b = b << 2;
|
main_palette[index].b = b << 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void JD8_FadeOut() {
|
// Màquina d'estats del fade. Evita que JD8_FadeOut/JD8_FadeToPal hagen de
|
||||||
for (int j = 0; j < 32; j++) {
|
// mantindre whiles interns. Cada pas aplica un delta a la paleta activa i
|
||||||
for (int i = 0; i < 256; i++) {
|
// el caller decideix quan fer Flip.
|
||||||
if (main_palette[i].r >= 8)
|
namespace {
|
||||||
main_palette[i].r -= 8;
|
|
||||||
else
|
enum class FadeType {
|
||||||
main_palette[i].r = 0;
|
None = 0,
|
||||||
if (main_palette[i].g >= 8)
|
Out,
|
||||||
main_palette[i].g -= 8;
|
ToPal,
|
||||||
else
|
};
|
||||||
main_palette[i].g = 0;
|
|
||||||
if (main_palette[i].b >= 8)
|
constexpr int FADE_STEPS = 32;
|
||||||
main_palette[i].b -= 8;
|
|
||||||
else
|
FadeType fade_type = FadeType::None;
|
||||||
main_palette[i].b = 0;
|
Color fade_target[256];
|
||||||
|
int fade_step = 0;
|
||||||
|
|
||||||
|
void 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
JD8_Flip();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void JD8_FadeStartOut() {
|
||||||
|
fade_type = FadeType::Out;
|
||||||
|
fade_step = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define MAX(a, b) (a) > (b) ? (a) : (b)
|
void JD8_FadeStartToPal(JD8_Palette pal) {
|
||||||
|
fade_type = FadeType::ToPal;
|
||||||
void JD8_FadeToPal(JD8_Palette pal) {
|
memcpy(fade_target, pal, sizeof(Color) * 256);
|
||||||
for (int j = 0; j < 32; j++) {
|
fade_step = 0;
|
||||||
for (int i = 0; i < 256; i++) {
|
|
||||||
if (main_palette[i].r <= int(pal[i].r) - 8)
|
|
||||||
main_palette[i].r += 8;
|
|
||||||
else
|
|
||||||
main_palette[i].r = pal[i].r;
|
|
||||||
if (main_palette[i].g <= int(pal[i].g) - 8)
|
|
||||||
main_palette[i].g += 8;
|
|
||||||
else
|
|
||||||
main_palette[i].g = pal[i].g;
|
|
||||||
if (main_palette[i].b <= int(pal[i].b) - 8)
|
|
||||||
main_palette[i].b += 8;
|
|
||||||
else
|
|
||||||
main_palette[i].b = pal[i].b;
|
|
||||||
}
|
|
||||||
JD8_Flip();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool JD8_FadeIsActive() {
|
||||||
|
return fade_type != FadeType::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JD8_FadeTickStep() {
|
||||||
|
if (fade_type == FadeType::None) return true;
|
||||||
|
|
||||||
|
apply_fade_step();
|
||||||
|
fade_step++;
|
||||||
|
|
||||||
|
if (fade_step >= FADE_STEPS) {
|
||||||
|
fade_type = FadeType::None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat
|
||||||
|
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `JD8_Flip`
|
||||||
|
// entre cada una que només funcionava mentre l'entorn tenia fibers i
|
||||||
|
// `JD8_Flip` cedia el control al Director. Ara tot fade es fa tick a
|
||||||
|
// tick via `scenes::PaletteFade` (que encapsula `JD8_FadeStartOut` /
|
||||||
|
// `JD8_FadeStartToPal` + `JD8_FadeTickStep`).
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ struct Color {
|
|||||||
Uint8 b;
|
Uint8 b;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef Uint8* JD8_Surface;
|
using JD8_Surface = Uint8*;
|
||||||
typedef Color* JD8_Palette;
|
using JD8_Palette = Color*;
|
||||||
|
|
||||||
void JD8_Init();
|
void JD8_Init();
|
||||||
|
|
||||||
@@ -26,6 +26,10 @@ void JD8_SetScreenPalette(JD8_Palette palette);
|
|||||||
|
|
||||||
void JD8_FillSquare(int ini, int height, Uint8 color);
|
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(JD8_Surface surface);
|
||||||
|
|
||||||
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh);
|
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh);
|
||||||
@@ -40,8 +44,15 @@ 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);
|
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. 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();
|
void JD8_Flip();
|
||||||
|
|
||||||
|
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
|
||||||
|
// JD8_Flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
|
||||||
|
Uint32* JD8_GetFramebuffer();
|
||||||
|
|
||||||
void JD8_FreeSurface(JD8_Surface surface);
|
void JD8_FreeSurface(JD8_Surface surface);
|
||||||
|
|
||||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
||||||
@@ -50,9 +61,17 @@ void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
|
|||||||
|
|
||||||
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
||||||
|
|
||||||
void JD8_FadeOut();
|
// API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el
|
||||||
|
// fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha
|
||||||
void JD8_FadeToPal(JD8_Palette pal);
|
// 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();
|
||||||
|
bool JD8_FadeIsActive();
|
||||||
|
|
||||||
// JD_Font JD_LoadFont( char *file, int width, int height);
|
// JD_Font JD_LoadFont( char *file, int width, int height);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
#include "core/jail/jfile.hpp"
|
#include "core/jail/jfile.hpp"
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -17,202 +12,113 @@
|
|||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define DEFAULT_FILENAME "data.jf2"
|
namespace {
|
||||||
#define DEFAULT_FOLDER "data/"
|
|
||||||
#define CONFIG_FILENAME "config.txt"
|
|
||||||
|
|
||||||
struct file_t {
|
struct keyvalue {
|
||||||
std::string path;
|
std::string key;
|
||||||
uint32_t size;
|
std::string value;
|
||||||
uint32_t offset;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<file_t> toc;
|
std::vector<keyvalue> config;
|
||||||
|
std::string resource_folder;
|
||||||
|
std::string config_folder;
|
||||||
|
|
||||||
/* El std::map me fa coses rares, vaig a usar un good old std::vector amb una estructura key,value propia i au, que sempre funciona */
|
void load_config_values() {
|
||||||
struct keyvalue_t {
|
config.clear();
|
||||||
std::string key, value;
|
const std::string config_file = config_folder + "/config.txt";
|
||||||
};
|
std::ifstream fi(config_file);
|
||||||
|
if (!fi.is_open()) return;
|
||||||
|
|
||||||
char* resource_filename = NULL;
|
std::string line;
|
||||||
char* resource_folder = NULL;
|
while (std::getline(fi, line)) {
|
||||||
int file_source = SOURCE_FILE;
|
const auto eq = line.find('=');
|
||||||
char scratch[255];
|
if (eq == std::string::npos) continue;
|
||||||
static std::string config_folder;
|
config.push_back({line.substr(0, eq), line.substr(eq + 1)});
|
||||||
std::vector<keyvalue_t> config;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void file_setresourcefilename(const char* str) {
|
void save_config_values() {
|
||||||
if (resource_filename != NULL) free(resource_filename);
|
const std::string config_file = config_folder + "/config.txt";
|
||||||
resource_filename = (char*)malloc(strlen(str) + 1);
|
std::ofstream fo(config_file);
|
||||||
strcpy(resource_filename, str);
|
if (!fo.is_open()) return;
|
||||||
}
|
for (const auto& pair : config) {
|
||||||
|
fo << pair.key << '=' << pair.value << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void file_setresourcefolder(const char* str) {
|
void file_setresourcefolder(const char* str) {
|
||||||
if (resource_folder != NULL) free(resource_folder);
|
resource_folder = str;
|
||||||
resource_folder = (char*)malloc(strlen(str) + 1);
|
|
||||||
strcpy(resource_folder, str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void file_setsource(const int src) {
|
const char* file_getresourcefolder() {
|
||||||
file_source = src % 2; // mod 2 so it always is a valid value, 0 (file) or 1 (folder)
|
return resource_folder.c_str();
|
||||||
if (src == SOURCE_FOLDER && resource_folder == NULL) file_setresourcefolder(DEFAULT_FOLDER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool file_getdictionary() {
|
// Crea la carpeta del sistema on guardar les dades.
|
||||||
if (resource_filename == NULL) file_setresourcefilename(DEFAULT_FILENAME);
|
// Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
|
||||||
|
|
||||||
std::ifstream fi(resource_filename, std::ios::binary);
|
|
||||||
if (!fi.is_open()) return false;
|
|
||||||
char header[4];
|
|
||||||
fi.read(header, 4);
|
|
||||||
uint32_t num_files, toc_offset;
|
|
||||||
fi.read((char*)&num_files, 4);
|
|
||||||
fi.read((char*)&toc_offset, 4);
|
|
||||||
fi.seekg(toc_offset);
|
|
||||||
|
|
||||||
for (uint32_t i = 0; i < num_files; ++i) {
|
|
||||||
uint32_t file_offset, file_size;
|
|
||||||
fi.read((char*)&file_offset, 4);
|
|
||||||
fi.read((char*)&file_size, 4);
|
|
||||||
uint8_t path_size;
|
|
||||||
fi.read((char*)&path_size, 1);
|
|
||||||
char file_name[256];
|
|
||||||
fi.read(file_name, path_size);
|
|
||||||
file_name[path_size] = 0;
|
|
||||||
std::string filename = file_name;
|
|
||||||
toc.push_back({filename, file_size, file_offset});
|
|
||||||
}
|
|
||||||
fi.close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
char* file_getfilenamewithfolder(const char* filename) {
|
|
||||||
strcpy(scratch, resource_folder);
|
|
||||||
strcat(scratch, filename);
|
|
||||||
return scratch;
|
|
||||||
}
|
|
||||||
|
|
||||||
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary) {
|
|
||||||
if (file_source == SOURCE_FILE and toc.size() == 0) {
|
|
||||||
if (not file_getdictionary()) file_setsource(SOURCE_FOLDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
FILE* f;
|
|
||||||
|
|
||||||
if (file_source == SOURCE_FILE) {
|
|
||||||
bool found = false;
|
|
||||||
uint32_t count = 0;
|
|
||||||
while (!found && count < toc.size()) {
|
|
||||||
found = (std::string(resourcename) == toc[count].path);
|
|
||||||
if (!found) count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
perror("El recurs no s'ha trobat en l'arxiu de recursos");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
filesize = toc[count].size;
|
|
||||||
|
|
||||||
f = fopen(resource_filename, binary ? "rb" : "r");
|
|
||||||
if (not f) {
|
|
||||||
perror("No s'ha pogut obrir l'arxiu de recursos");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
fseek(f, toc[count].offset, SEEK_SET);
|
|
||||||
} else {
|
|
||||||
f = fopen(file_getfilenamewithfolder(resourcename), binary ? "rb" : "r");
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
filesize = ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
}
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate) {
|
|
||||||
FILE* f = file_getfilepointer(resourcename, filesize, true);
|
|
||||||
char* buffer = (char*)malloc(zero_terminate ? filesize : filesize + 1);
|
|
||||||
fread(buffer, filesize, 1, f);
|
|
||||||
if (zero_terminate) buffer[filesize] = 0;
|
|
||||||
fclose(f);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos.
|
|
||||||
// Acepta rutas con subdirectorios (ej: "jailgames/aee") y crea toda la jerarquía.
|
|
||||||
void file_setconfigfolder(const char* foldername) {
|
void file_setconfigfolder(const char* foldername) {
|
||||||
#ifdef _WIN32
|
#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__
|
#elif __APPLE__
|
||||||
struct passwd* pw = getpwuid(getuid());
|
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;
|
config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
|
||||||
#elif __linux__
|
#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());
|
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;
|
config_folder = std::string(homedir) + "/.config/" + foldername;
|
||||||
#endif
|
#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() {
|
const char* file_getconfigfolder() {
|
||||||
static std::string folder;
|
thread_local std::string folder;
|
||||||
folder = config_folder + "/";
|
folder = config_folder + "/";
|
||||||
return folder.c_str();
|
return folder.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
void file_loadconfigvalues() {
|
|
||||||
config.clear();
|
|
||||||
std::string config_file = config_folder + "/config.txt";
|
|
||||||
FILE* f = fopen(config_file.c_str(), "r");
|
|
||||||
if (!f) return;
|
|
||||||
|
|
||||||
char line[1024];
|
|
||||||
while (fgets(line, sizeof(line), f)) {
|
|
||||||
char* value = strchr(line, '=');
|
|
||||||
if (value) {
|
|
||||||
*value = '\0';
|
|
||||||
value++;
|
|
||||||
value[strlen(value) - 1] = '\0';
|
|
||||||
config.push_back({line, value});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void file_saveconfigvalues() {
|
|
||||||
std::string config_file = config_folder + "/config.txt";
|
|
||||||
FILE* f = fopen(config_file.c_str(), "w");
|
|
||||||
if (f) {
|
|
||||||
for (auto pair : config) {
|
|
||||||
fprintf(f, "%s=%s\n", pair.key.c_str(), pair.value.c_str());
|
|
||||||
}
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* file_getconfigvalue(const char* key) {
|
const char* file_getconfigvalue(const char* key) {
|
||||||
if (config.empty()) file_loadconfigvalues();
|
if (config.empty()) load_config_values();
|
||||||
for (auto pair : config) {
|
for (const auto& pair : config) {
|
||||||
if (pair.key == std::string(key)) {
|
if (pair.key == key) {
|
||||||
strcpy(scratch, pair.value.c_str());
|
thread_local std::string value_cache;
|
||||||
return scratch;
|
value_cache = pair.value;
|
||||||
|
return value_cache.c_str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NULL;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void file_setconfigvalue(const char* key, const char* value) {
|
void file_setconfigvalue(const char* key, const char* value) {
|
||||||
if (config.empty()) file_loadconfigvalues();
|
if (config.empty()) load_config_values();
|
||||||
for (auto& pair : config) {
|
for (auto& pair : config) {
|
||||||
if (pair.key == std::string(key)) {
|
if (pair.key == key) {
|
||||||
pair.value = value;
|
pair.value = value;
|
||||||
file_saveconfigvalues();
|
save_config_values();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.push_back({key, value});
|
config.push_back({std::string(key), std::string(value)});
|
||||||
file_saveconfigvalues();
|
save_config_values();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
#define SOURCE_FILE 0
|
|
||||||
#define SOURCE_FOLDER 1
|
|
||||||
|
|
||||||
void file_setconfigfolder(const char* foldername);
|
void file_setconfigfolder(const char* foldername);
|
||||||
const char* file_getconfigfolder();
|
const char* file_getconfigfolder();
|
||||||
|
|
||||||
void file_setresourcefilename(const char* str);
|
|
||||||
void file_setresourcefolder(const char* str);
|
void file_setresourcefolder(const char* str);
|
||||||
void file_setsource(const int src);
|
const char* file_getresourcefolder();
|
||||||
|
|
||||||
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary = false);
|
|
||||||
char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate = false);
|
|
||||||
|
|
||||||
const char* file_getconfigvalue(const char* key);
|
const char* file_getconfigvalue(const char* key);
|
||||||
void file_setconfigvalue(const char* key, const char* value);
|
void file_setconfigvalue(const char* key, const char* value);
|
||||||
|
|||||||
@@ -1,42 +1,57 @@
|
|||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
|
|
||||||
bool eixir = false;
|
namespace {
|
||||||
Uint32 updateTicks = 0;
|
|
||||||
Uint32 updateTime = 0;
|
bool quitting = false;
|
||||||
Uint32 cycle_counter = 0;
|
Uint32 update_ticks = 0;
|
||||||
|
Uint32 update_time = 0;
|
||||||
void JG_Init() {
|
Uint32 cycle_counter = 0;
|
||||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
Uint32 last_delta_time = 0;
|
||||||
// SDL_WM_SetCaption( title, NULL );
|
|
||||||
updateTime = SDL_GetTicks();
|
} // namespace
|
||||||
}
|
|
||||||
|
void JG_Init() {
|
||||||
void JG_Finalize() {
|
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||||
SDL_Quit();
|
update_time = SDL_GetTicks();
|
||||||
}
|
last_delta_time = update_time;
|
||||||
|
}
|
||||||
void JG_QuitSignal() {
|
|
||||||
eixir = true;
|
void JG_Finalize() {
|
||||||
}
|
SDL_Quit();
|
||||||
|
}
|
||||||
bool JG_Quitting() {
|
|
||||||
return eixir;
|
void JG_QuitSignal() {
|
||||||
}
|
quitting = true;
|
||||||
|
}
|
||||||
void JG_SetUpdateTicks(Uint32 milliseconds) {
|
|
||||||
updateTicks = milliseconds;
|
bool JG_Quitting() {
|
||||||
}
|
return quitting;
|
||||||
|
}
|
||||||
bool JG_ShouldUpdate() {
|
|
||||||
if (SDL_GetTicks() - updateTime > updateTicks) {
|
void JG_SetUpdateTicks(Uint32 milliseconds) {
|
||||||
updateTime = SDL_GetTicks();
|
update_ticks = milliseconds;
|
||||||
cycle_counter++;
|
}
|
||||||
return true;
|
|
||||||
} else {
|
bool JG_ShouldUpdate() {
|
||||||
return false;
|
const Uint32 now = SDL_GetTicks();
|
||||||
}
|
if (now - update_time > update_ticks) {
|
||||||
}
|
update_time = now;
|
||||||
|
cycle_counter++;
|
||||||
Uint32 JG_GetCycleCounter() {
|
return true;
|
||||||
return cycle_counter;
|
}
|
||||||
}
|
// No toca update — retornem false sense més. Des de Phase B.2 ja no
|
||||||
|
// hi ha fibers: cap caller fa spin-waits (`while (!JG_ShouldUpdate())`)
|
||||||
|
// i el Director pren el control del main loop frame a frame.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 JG_GetCycleCounter() {
|
||||||
|
return cycle_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 JG_GetDeltaMs() {
|
||||||
|
const Uint32 now = SDL_GetTicks();
|
||||||
|
const Uint32 delta = now - last_delta_time;
|
||||||
|
last_delta_time = now;
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ void JG_SetUpdateTicks(Uint32 milliseconds);
|
|||||||
bool JG_ShouldUpdate();
|
bool JG_ShouldUpdate();
|
||||||
|
|
||||||
Uint32 JG_GetCycleCounter();
|
Uint32 JG_GetCycleCounter();
|
||||||
|
|
||||||
|
// Temps transcorregut (en ms) des de l'última crida a JG_GetDeltaMs.
|
||||||
|
// Helper per a la migració progressiva a time-based (Fase 4+).
|
||||||
|
Uint32 JG_GetDeltaMs();
|
||||||
|
|||||||
@@ -1,39 +1,63 @@
|
|||||||
#include "core/jail/jinput.hpp"
|
#include "core/jail/jinput.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <cstring>
|
||||||
|
|
||||||
#include "core/system/director.hpp"
|
#include "core/system/director.hpp"
|
||||||
|
|
||||||
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
|
namespace {
|
||||||
const bool* keystates = nullptr;
|
|
||||||
Uint8 cheat[5];
|
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
|
||||||
bool key_pressed = false;
|
const bool* keystates = nullptr;
|
||||||
int waitTime = 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;
|
||||||
|
|
||||||
|
// Temps restant en mil·lisegons durant el qual JI_KeyPressed/JI_AnyKey
|
||||||
|
// retornen false. Utilitzat per a evitar que pulsacions fortuïtes
|
||||||
|
// saltin cinemàtiques al començament.
|
||||||
|
float wait_ms = 0.0f;
|
||||||
|
|
||||||
|
// Per a calcular el delta entre crides a JI_Update sense que els callers
|
||||||
|
// hagen de passar-lo explícitament. Es reinicia a la primera crida.
|
||||||
|
Uint64 last_update_tick = 0;
|
||||||
|
|
||||||
|
bool input_blocked = false;
|
||||||
|
|
||||||
|
Uint8 virtual_keystates[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));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void JI_DisableKeyboard(Uint32 time) {
|
void JI_DisableKeyboard(Uint32 time) {
|
||||||
waitTime = time;
|
wait_ms = static_cast<float>(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool input_blocked = false;
|
|
||||||
|
|
||||||
void JI_SetInputBlocked(bool blocked) {
|
void JI_SetInputBlocked(bool blocked) {
|
||||||
input_blocked = blocked;
|
input_blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
|
|
||||||
|
|
||||||
void JI_SetVirtualKey(int scancode, int source, bool pressed) {
|
void JI_SetVirtualKey(int scancode, int source, bool pressed) {
|
||||||
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return;
|
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return;
|
||||||
if (source < 0 || source >= JI_VSRC_COUNT) return;
|
if (source < 0 || source >= JI_VSRC_COUNT) return;
|
||||||
virtual_keystates[source][scancode] = pressed ? 1 : 0;
|
virtual_keystates[source][scancode] = pressed ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void JI_moveCheats(Uint8 new_key) {
|
void JI_moveCheats(Uint8 scancode) {
|
||||||
cheat[0] = cheat[1];
|
cheat[0] = cheat[1];
|
||||||
cheat[1] = cheat[2];
|
cheat[1] = cheat[2];
|
||||||
cheat[2] = cheat[3];
|
cheat[2] = cheat[3];
|
||||||
cheat[3] = cheat[4];
|
cheat[3] = cheat[4];
|
||||||
cheat[4] = new_key;
|
cheat[4] = scancode_to_ascii(scancode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void JI_Update() {
|
void JI_Update() {
|
||||||
@@ -43,14 +67,22 @@ void JI_Update() {
|
|||||||
keystates = SDL_GetKeyboardState(NULL);
|
keystates = SDL_GetKeyboardState(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (waitTime > 0) waitTime--;
|
const Uint64 now = SDL_GetTicks();
|
||||||
|
if (last_update_tick == 0) last_update_tick = now;
|
||||||
|
const float delta_ms = static_cast<float>(now - last_update_tick);
|
||||||
|
last_update_tick = now;
|
||||||
|
|
||||||
|
if (wait_ms > 0.0f) {
|
||||||
|
wait_ms -= delta_ms;
|
||||||
|
if (wait_ms < 0.0f) wait_ms = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Consumim el flag de "alguna tecla no-GUI polsada" del director
|
// Consumim el flag de "alguna tecla no-GUI polsada" del director
|
||||||
key_pressed = Director::get()->consumeKeyPressed();
|
key_pressed = Director::get()->consumeKeyPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool JI_KeyPressed(int key) {
|
bool JI_KeyPressed(int key) {
|
||||||
if (waitTime > 0 || keystates == nullptr) return false;
|
if (wait_ms > 0.0f || keystates == nullptr) return false;
|
||||||
// Input bloquejat (p.ex. menú flotant obert)
|
// Input bloquejat (p.ex. menú flotant obert)
|
||||||
if (input_blocked) return false;
|
if (input_blocked) return false;
|
||||||
// ESC bloquejada pel Director (primera pulsació mostra notificació)
|
// ESC bloquejada pel Director (primera pulsació mostra notificació)
|
||||||
@@ -64,13 +96,17 @@ bool JI_KeyPressed(int key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool JI_CheatActivated(const char* cheat_code) {
|
bool JI_CheatActivated(const char* cheat_code) {
|
||||||
bool found = true;
|
const size_t len = std::strlen(cheat_code);
|
||||||
for (size_t i = 0; i < strlen(cheat_code); i++) {
|
if (len > sizeof(cheat)) return false;
|
||||||
if (cheat[i] != cheat_code[i]) found = false;
|
// Compara contra els últims `len` caràcters del buffer. El buffer té
|
||||||
|
// mida fixa 5 i acumula sempre el darrer tecle a la posició 4.
|
||||||
|
const size_t offset = sizeof(cheat) - len;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (cheat[offset + i] != static_cast<Uint8>(cheat_code[i])) return false;
|
||||||
}
|
}
|
||||||
return found;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool JI_AnyKey() {
|
bool JI_AnyKey() {
|
||||||
return waitTime > 0 ? false : key_pressed;
|
return wait_ms > 0.0f ? false : key_pressed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
namespace Locale {
|
namespace Locale {
|
||||||
@@ -27,14 +27,12 @@ namespace Locale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool load(const char* filename) {
|
bool load(const char* filename) {
|
||||||
int size = 0;
|
auto buffer = ResourceHelper::loadFile(filename);
|
||||||
char* buffer = file_getfilebuffer(filename, size, true);
|
if (buffer.empty()) {
|
||||||
if (!buffer || size <= 0) {
|
|
||||||
std::cerr << "Locale: unable to load " << filename << '\n';
|
std::cerr << "Locale: unable to load " << filename << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::string content(buffer, size);
|
std::string content(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||||
free(buffer);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(content);
|
auto yaml = fkyaml::node::deserialize(content);
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
#include "core/rendering/menu.hpp"
|
#include "core/rendering/menu.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/overlay.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/text.hpp"
|
#include "core/rendering/text.hpp"
|
||||||
|
#include "core/system/director.hpp"
|
||||||
|
#include "game/defines.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "utils/easing.hpp"
|
#include "utils/easing.hpp"
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
namespace Menu {
|
namespace Menu {
|
||||||
|
|
||||||
@@ -35,38 +40,60 @@ namespace Menu {
|
|||||||
static constexpr int ITEM_SPACING = 11;
|
static constexpr int ITEM_SPACING = 11;
|
||||||
static constexpr int BOTTOM_PAD = 6;
|
static constexpr int BOTTOM_PAD = 6;
|
||||||
static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap
|
static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap
|
||||||
|
static constexpr int SUBTITLE_H = 8 + 3; // línia de subtítol + gap
|
||||||
|
|
||||||
// --- Animació ---
|
// --- Animació ---
|
||||||
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
|
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
|
||||||
|
static constexpr float CLOSE_SPEED = 10.0F; // 1.0 / 0.1s (una mica més ràpida que l'obertura)
|
||||||
|
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
|
||||||
|
|
||||||
// --- Items ---
|
// --- Items ---
|
||||||
enum class ItemKind { Toggle,
|
enum class ItemKind { Toggle,
|
||||||
Cycle,
|
Cycle,
|
||||||
IntRange,
|
IntRange,
|
||||||
Submenu,
|
Submenu,
|
||||||
KeyBind };
|
KeyBind,
|
||||||
|
Action };
|
||||||
|
|
||||||
struct Item {
|
struct Item {
|
||||||
const char* label;
|
const char* label;
|
||||||
ItemKind kind;
|
ItemKind kind;
|
||||||
std::function<std::string()> getValue; // opcional
|
std::function<std::string()> getValue; // opcional
|
||||||
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
|
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
|
SDL_Scancode* scancode{nullptr}; // per KeyBind
|
||||||
|
std::function<bool()> visible; // nullptr ⇒ sempre visible
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Page {
|
struct Page {
|
||||||
const char* title;
|
const char* title;
|
||||||
std::vector<Item> items;
|
std::vector<Item> items;
|
||||||
int cursor{0};
|
int cursor{0};
|
||||||
|
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static 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 ---
|
// --- Estat ---
|
||||||
static std::vector<Page> stack_;
|
static std::vector<Page> stack_;
|
||||||
static std::unique_ptr<Text> font_;
|
static std::unique_ptr<Text> font_;
|
||||||
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
|
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
|
||||||
|
static float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible)
|
||||||
static Uint32 last_ticks_{0};
|
static Uint32 last_ticks_{0};
|
||||||
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
|
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
|
||||||
|
static bool closing_{false}; // true mentre l'animació de tancament és en curs
|
||||||
|
|
||||||
// --- Transició entre pàgines ---
|
// --- Transició entre pàgines ---
|
||||||
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
|
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
|
||||||
@@ -102,6 +129,7 @@ namespace Menu {
|
|||||||
static Page buildAudio();
|
static Page buildAudio();
|
||||||
static Page buildControls();
|
static Page buildControls();
|
||||||
static Page buildGame();
|
static Page buildGame();
|
||||||
|
static Page buildSystem();
|
||||||
|
|
||||||
static Page buildRoot() {
|
static Page buildRoot() {
|
||||||
Page p{Locale::get("menu.titles.root"), {}, 0};
|
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.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
|
||||||
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
|
||||||
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
|
||||||
|
p.items.push_back({Locale::get("menu.items.system"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Page buildVideo() {
|
static Page buildVideo() {
|
||||||
Page p{Locale::get("menu.titles.video"), {}, 0};
|
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, [] {
|
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
|
||||||
char buf[16];
|
char buf[16];
|
||||||
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
||||||
return std::string(buf); }, [](int dir) {
|
return std::string(buf); }, [](int dir) {
|
||||||
if (dir < 0) Screen::get()->decZoom();
|
if (dir < 0) Screen::get()->decZoom();
|
||||||
else if (dir > 0) Screen::get()->incZoom(); }, nullptr});
|
else if (dir > 0) Screen::get()->incZoom(); }, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
|
||||||
|
#endif
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr});
|
// Opcions visuals generals (sempre visibles)
|
||||||
|
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::Cycle, [] {
|
||||||
|
switch (Options::video.scaling_mode) {
|
||||||
|
case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled"));
|
||||||
|
case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch"));
|
||||||
|
case Options::ScalingMode::LETTERBOX: return std::string(Locale::get("menu.values.scaling_letterbox"));
|
||||||
|
case Options::ScalingMode::OVERSCAN: return std::string(Locale::get("menu.values.scaling_overscan"));
|
||||||
|
case Options::ScalingMode::INTEGER: return std::string(Locale::get("menu.values.scaling_integer"));
|
||||||
|
}
|
||||||
|
return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::Cycle, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||||
|
? Locale::get("menu.values.linear")
|
||||||
|
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.integer_scale"), ItemKind::Toggle, [] { return onOff(Options::video.integer_scale); }, [](int) { Screen::get()->toggleIntegerScale(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::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) {
|
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();
|
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) {
|
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();
|
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, [] {
|
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
|
||||||
switch (Options::render_info.position) {
|
switch (Options::render_info.position) {
|
||||||
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
|
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
|
||||||
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
|
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
|
||||||
case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
|
case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
|
||||||
}
|
}
|
||||||
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr});
|
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr});
|
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -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_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_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.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;
|
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.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) {
|
p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music.enabled); }, [](int) {
|
||||||
Options::audio.music_enabled = !Options::audio.music_enabled;
|
Options::audio.music.enabled = !Options::audio.music.enabled;
|
||||||
Options::applyAudio(); }, nullptr});
|
Options::applyAudio(); }, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music_volume); }, [](int dir) { stepVolume(Options::audio.music_volume, dir); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::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) {
|
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
|
||||||
Options::audio.sound_enabled = !Options::audio.sound_enabled;
|
Options::audio.sound.enabled = !Options::audio.sound.enabled;
|
||||||
Options::applyAudio(); }, nullptr});
|
Options::applyAudio(); }, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound_volume); }, [](int dir) { stepVolume(Options::audio.sound_volume, dir); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
|
||||||
|
|
||||||
return p;
|
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_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;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,11 +340,17 @@ namespace Menu {
|
|||||||
fillRect(buf, x + w - 1, y, 1, h, color);
|
fillRect(buf, x + w - 1, y, 1, h, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mida final de la caixa segons el nombre d'items
|
// Mida final de la caixa segons el nombre d'items *visibles*.
|
||||||
|
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
|
||||||
|
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
|
||||||
static int boxHeight(const Page& page) {
|
static int boxHeight(const Page& page) {
|
||||||
int n = static_cast<int>(page.items.size());
|
int n = 0;
|
||||||
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING;
|
for (const auto& it : page.items) {
|
||||||
return HEADER_H + body + BOTTOM_PAD;
|
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 ---
|
// --- API pública ---
|
||||||
@@ -275,34 +359,56 @@ namespace Menu {
|
|||||||
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||||
stack_.clear();
|
stack_.clear();
|
||||||
open_anim_ = 0.0F;
|
open_anim_ = 0.0F;
|
||||||
|
closing_ = false;
|
||||||
last_ticks_ = SDL_GetTicks();
|
last_ticks_ = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void destroy() {
|
void destroy() {
|
||||||
font_.reset();
|
font_.reset();
|
||||||
stack_.clear();
|
stack_.clear();
|
||||||
|
closing_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Actiu": accepta input. Durant l'animació de tancament la pila encara
|
||||||
|
// té pàgines però ja no ha de processar tecles.
|
||||||
auto isOpen() -> bool {
|
auto isOpen() -> bool {
|
||||||
|
return !stack_.empty() && !closing_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Visible": encara hi ha caixa per pintar (incloent close animation).
|
||||||
|
auto isVisible() -> bool {
|
||||||
return !stack_.empty();
|
return !stack_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggle() {
|
void toggle() {
|
||||||
|
if (closing_ && !stack_.empty()) {
|
||||||
|
// Cancel·la el tancament en curs — continua l'animació cap a "obert"
|
||||||
|
// des del valor actual d'open_anim_.
|
||||||
|
closing_ = false;
|
||||||
|
last_ticks_ = SDL_GetTicks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isOpen()) {
|
if (isOpen()) {
|
||||||
close();
|
close();
|
||||||
} else {
|
} else {
|
||||||
stack_.push_back(buildRoot());
|
stack_.push_back(buildRoot());
|
||||||
open_anim_ = 0.0F;
|
open_anim_ = 0.0F;
|
||||||
|
closing_ = false;
|
||||||
|
animated_h_ = static_cast<float>(boxHeight(stack_.back()));
|
||||||
last_ticks_ = SDL_GetTicks();
|
last_ticks_ = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// close() no buida la pila immediatament: marca closing_ i deixa que
|
||||||
|
// render() faça decréixer open_anim_ fins a 0. En aquell moment es neteja
|
||||||
|
// l'estat. Si es crida estant ja tancat o tancant-se, no-op.
|
||||||
void close() {
|
void close() {
|
||||||
stack_.clear();
|
if (stack_.empty() || closing_) return;
|
||||||
open_anim_ = 0.0F;
|
closing_ = true;
|
||||||
capturing_ = nullptr;
|
capturing_ = nullptr;
|
||||||
transition_active_ = false;
|
transition_active_ = false;
|
||||||
transition_progress_ = 1.0F;
|
transition_progress_ = 1.0F;
|
||||||
|
last_ticks_ = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto isCapturing() -> bool {
|
auto isCapturing() -> bool {
|
||||||
@@ -333,13 +439,17 @@ namespace Menu {
|
|||||||
}
|
}
|
||||||
return;
|
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) {
|
switch (sc) {
|
||||||
case SDL_SCANCODE_UP:
|
case SDL_SCANCODE_UP:
|
||||||
page.cursor = (page.cursor - 1 + n) % n;
|
page.cursor = nextVisibleCursor(page, page.cursor, -1);
|
||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
page.cursor = (page.cursor + 1) % n;
|
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
||||||
@@ -355,7 +465,8 @@ namespace Menu {
|
|||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_RETURN:
|
case SDL_SCANCODE_RETURN:
|
||||||
case SDL_SCANCODE_KP_ENTER:
|
case SDL_SCANCODE_KP_ENTER:
|
||||||
if (page.items[page.cursor].kind == ItemKind::Submenu) {
|
if (page.items[page.cursor].kind == ItemKind::Submenu ||
|
||||||
|
page.items[page.cursor].kind == ItemKind::Action) {
|
||||||
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
|
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
|
||||||
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
|
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
|
||||||
capturing_ = page.items[page.cursor].scancode;
|
capturing_ = page.items[page.cursor].scancode;
|
||||||
@@ -372,6 +483,15 @@ namespace Menu {
|
|||||||
default:
|
default:
|
||||||
break;
|
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.
|
// 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;
|
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");
|
const char* empty_text = Locale::get("menu.values.empty");
|
||||||
int ew = font_->width(empty_text);
|
int ew = font_->width(empty_text);
|
||||||
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
|
||||||
for (size_t i = 0; i < page.items.size(); i++) {
|
for (size_t i = 0; i < page.items.size(); i++) {
|
||||||
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
|
|
||||||
bool selected = (static_cast<int>(i) == page.cursor);
|
|
||||||
const Item& item = page.items[i];
|
const Item& item = page.items[i];
|
||||||
|
if (!isVisible(item)) 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) {
|
if (selected) {
|
||||||
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
font_->drawClipped(pixel_data, box_x + 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);
|
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) {
|
if (item.kind == ItemKind::Submenu) {
|
||||||
@@ -438,13 +582,23 @@ namespace Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void render(Uint32* pixel_data) {
|
void render(Uint32* pixel_data) {
|
||||||
if (!isOpen() || !font_ || !pixel_data) return;
|
if (!isVisible() || !font_ || !pixel_data) return;
|
||||||
|
|
||||||
// Delta time
|
// Delta time
|
||||||
Uint32 now = SDL_GetTicks();
|
Uint32 now = SDL_GetTicks();
|
||||||
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
|
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
|
||||||
last_ticks_ = now;
|
last_ticks_ = now;
|
||||||
if (open_anim_ < 1.0F) {
|
if (closing_) {
|
||||||
|
open_anim_ -= 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;
|
open_anim_ += OPEN_SPEED * dt;
|
||||||
if (open_anim_ > 1.0F) open_anim_ = 1.0F;
|
if (open_anim_ > 1.0F) open_anim_ = 1.0F;
|
||||||
}
|
}
|
||||||
@@ -461,14 +615,30 @@ namespace Menu {
|
|||||||
const Page& page = stack_.back();
|
const Page& page = stack_.back();
|
||||||
const int current_h = boxHeight(page);
|
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_);
|
float eased = Easing::outQuad(open_anim_);
|
||||||
|
|
||||||
// Calcula alçada (amb transició si escau)
|
// Calcula alçada (amb transició si escau)
|
||||||
int target_h = current_h;
|
int target_h = static_cast<int>(animated_h_);
|
||||||
if (transition_active_) {
|
if (transition_active_) {
|
||||||
int outgoing_h = boxHeight(transition_outgoing_);
|
int outgoing_h = boxHeight(transition_outgoing_);
|
||||||
float tp = Easing::outQuad(transition_progress_);
|
float tp = Easing::outQuad(transition_progress_);
|
||||||
target_h = Easing::lerpInt(outgoing_h, current_h, tp);
|
target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h_), tp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caixa creix verticalment durant l'obertura
|
// Caixa creix verticalment durant l'obertura
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ namespace Menu {
|
|||||||
void init();
|
void init();
|
||||||
void destroy();
|
void destroy();
|
||||||
|
|
||||||
|
// "Actiu": el menú accepta input. Fals durant l'animació de tancament.
|
||||||
[[nodiscard]] auto isOpen() -> bool;
|
[[nodiscard]] auto isOpen() -> bool;
|
||||||
|
// "Visible": hi ha una caixa pintada (incloent l'animació de tancament).
|
||||||
|
// Overlay la usa per a decidir si cridar render().
|
||||||
|
[[nodiscard]] auto isVisible() -> bool;
|
||||||
void toggle();
|
void toggle();
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
// Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està obert
|
// Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està visible
|
||||||
void render(Uint32* pixel_data);
|
void render(Uint32* pixel_data);
|
||||||
|
|
||||||
// Gestió d'input — cridat des del Director en KEY_DOWN
|
// Gestió d'input — cridat des del Director en KEY_DOWN
|
||||||
|
|||||||
@@ -361,8 +361,8 @@ namespace Overlay {
|
|||||||
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
||||||
notifications_.end());
|
notifications_.end());
|
||||||
|
|
||||||
// Menú flotant per damunt de tot
|
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
|
||||||
if (Menu::isOpen()) {
|
if (Menu::isVisible()) {
|
||||||
Menu::render(pixel_data);
|
Menu::render(pixel_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,68 @@
|
|||||||
|
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/overlay.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
|
#ifndef NO_SHADERS
|
||||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
||||||
|
#endif
|
||||||
#include "game/defines.hpp"
|
#include "game/defines.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "utils/utils.hpp"
|
#include "utils/utils.hpp"
|
||||||
|
|
||||||
Screen* Screen::instance_ = nullptr;
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten.h>
|
||||||
|
#include <emscripten/html5.h>
|
||||||
|
|
||||||
|
// --- Fix per a fullscreen/resize en Emscripten ---
|
||||||
|
//
|
||||||
|
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
|
||||||
|
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
|
||||||
|
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
|
||||||
|
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
|
||||||
|
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
|
||||||
|
// anterior i el viewport queda desencuadrat.
|
||||||
|
//
|
||||||
|
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
|
||||||
|
// del event loop (el canvas encara no està estable en el moment del callback)
|
||||||
|
// i re-sincronitzar SDL cridant SDL_SetWindowFullscreen + applyFallbackPresentation.
|
||||||
|
// La crida interna a SDL_SetWindowFullscreen és la peça que realment fa
|
||||||
|
// resincronitzar l'estat intern de SDL — sense això la logical presentation
|
||||||
|
// no encaixa amb el canvas real.
|
||||||
|
namespace {
|
||||||
|
Screen* g_screen_instance = nullptr;
|
||||||
|
|
||||||
|
void deferredCanvasResize(void* /*userData*/) {
|
||||||
|
if (g_screen_instance != nullptr) {
|
||||||
|
g_screen_instance->handleCanvasResized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent* event, void* /*userData*/) {
|
||||||
|
if (g_screen_instance != nullptr && event != nullptr) {
|
||||||
|
g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
|
||||||
|
}
|
||||||
|
emscripten_async_call(deferredCanvasResize, nullptr, 0);
|
||||||
|
return EM_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*userData*/) {
|
||||||
|
emscripten_async_call(deferredCanvasResize, nullptr, 0);
|
||||||
|
return EM_FALSE;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
|
std::unique_ptr<Screen> Screen::instance_;
|
||||||
|
|
||||||
void Screen::init() {
|
void Screen::init() {
|
||||||
instance_ = new Screen();
|
instance_ = std::unique_ptr<Screen>(new Screen());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::destroy() {
|
void Screen::destroy() {
|
||||||
delete instance_;
|
instance_.reset();
|
||||||
instance_ = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Screen::get() -> Screen* {
|
auto Screen::get() -> Screen* {
|
||||||
return instance_;
|
return instance_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Screen::Screen() {
|
Screen::Screen() {
|
||||||
@@ -35,40 +79,70 @@ Screen::Screen() {
|
|||||||
if (zoom_ < 1) zoom_ = 1;
|
if (zoom_ < 1) zoom_ = 1;
|
||||||
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
|
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 w = GAME_WIDTH * zoom_;
|
||||||
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
|
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
|
||||||
|
|
||||||
window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
|
window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
|
||||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||||
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
|
||||||
|
|
||||||
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
|
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
|
||||||
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
|
applyFallbackPresentation();
|
||||||
|
|
||||||
// Inicialitza backend GPU si l'acceleració està activada
|
// Inicialitza backend GPU si l'acceleració està activada
|
||||||
initShaders();
|
initShaders();
|
||||||
|
|
||||||
std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";
|
std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// IMPORTANT: NO registrem resize callback genèric. En mòbil, fer scroll
|
||||||
|
// fa que el navegador oculti/mostri la barra d'URL, disparant un resize
|
||||||
|
// del DOM per cada scroll. Això portaria a re-aplicar logical presentation
|
||||||
|
// per cada scroll i corrompria el viewport intern de SDL.
|
||||||
|
g_screen_instance = this;
|
||||||
|
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
|
||||||
|
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Screen::~Screen() {
|
Screen::~Screen() {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
g_screen_instance = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Guarda opcions abans de destruir
|
// Guarda opcions abans de destruir
|
||||||
Options::window.zoom = zoom_;
|
Options::window.zoom = zoom_;
|
||||||
Options::window.fullscreen = fullscreen_;
|
Options::window.fullscreen = fullscreen_;
|
||||||
|
|
||||||
// Destrueix el backend GPU
|
// Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
|
||||||
if (shader_backend_) {
|
if (shader_backend_) {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
|
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
|
||||||
if (gpu) gpu->destroy();
|
if (gpu) gpu->destroy();
|
||||||
|
#endif
|
||||||
shader_backend_.reset();
|
shader_backend_.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
|
||||||
if (texture_) SDL_DestroyTexture(texture_);
|
if (texture_) SDL_DestroyTexture(texture_);
|
||||||
if (renderer_) SDL_DestroyRenderer(renderer_);
|
if (renderer_) SDL_DestroyRenderer(renderer_);
|
||||||
if (window_) SDL_DestroyWindow(window_);
|
if (window_) SDL_DestroyWindow(window_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::initShaders() {
|
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;
|
if (!Options::video.gpu_acceleration) return;
|
||||||
|
|
||||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||||
@@ -88,17 +162,18 @@ void Screen::initShaders() {
|
|||||||
std::cout << "GPU driver: " << gpu_driver_ << '\n';
|
std::cout << "GPU driver: " << gpu_driver_ << '\n';
|
||||||
|
|
||||||
// Aplica opcions de vídeo
|
// Aplica opcions de vídeo
|
||||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
shader_backend_->setScalingMode(Options::video.scaling_mode);
|
||||||
shader_backend_->setVSync(Options::video.vsync);
|
shader_backend_->setVSync(Options::video.vsync);
|
||||||
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear);
|
shader_backend_->setTextureFilter(Options::video.texture_filter);
|
||||||
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
||||||
shader_backend_->setLinearUpscale(Options::video.linear_upscale);
|
|
||||||
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
|
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
|
||||||
|
|
||||||
if (Options::video.supersampling) {
|
if (Options::video.supersampling) {
|
||||||
shader_backend_->setOversample(3);
|
shader_backend_->setOversample(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shader_backend_->setInternalResolution(Options::video.internal_resolution);
|
||||||
|
|
||||||
// Resol el shader actiu des del config
|
// Resol el shader actiu des del config
|
||||||
if (Options::video.current_shader == "crtpi") {
|
if (Options::video.current_shader == "crtpi") {
|
||||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||||
@@ -122,6 +197,7 @@ void Screen::initShaders() {
|
|||||||
|
|
||||||
applyCurrentPostFXPreset();
|
applyCurrentPostFXPreset();
|
||||||
applyCurrentCrtPiPreset();
|
applyCurrentCrtPiPreset();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::present(Uint32* pixel_data) {
|
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_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
|
||||||
shader_backend_->render();
|
shader_backend_->render();
|
||||||
} else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
} else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||||
// GPU activa però shaders desactivats: renderitza net (sense efectes)
|
// GPU activa però shaders desactivats: renderitza net (sense efectes).
|
||||||
|
// Força POSTFX amb params zerats — altrament, si l'actiu és CRTPI,
|
||||||
|
// els seus efectes (scanlines, curvatura) seguirien aplicant-se encara
|
||||||
|
// que shader_enabled sigui false. Restaurem l'actiu al final per a
|
||||||
|
// no trencar la selecció de l'usuari.
|
||||||
Rendering::PostFXParams clean{};
|
Rendering::PostFXParams clean{};
|
||||||
shader_backend_->setPostFXParams(clean);
|
shader_backend_->setPostFXParams(clean);
|
||||||
|
const auto prev_shader = shader_backend_->getActiveShader();
|
||||||
|
if (prev_shader != Rendering::ShaderType::POSTFX) {
|
||||||
|
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||||
|
}
|
||||||
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
|
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
|
||||||
shader_backend_->render();
|
shader_backend_->render();
|
||||||
|
if (prev_shader != Rendering::ShaderType::POSTFX) {
|
||||||
|
shader_backend_->setActiveShader(prev_shader);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback SDL_Renderer
|
// Fallback SDL_Renderer. A mult=1, flux directe original: logical
|
||||||
|
// presentation (setada per applyFallbackPresentation) + scale mode de
|
||||||
|
// texture_ segons l'opció. A mult>1, la còpia intermèdia crea la
|
||||||
|
// font ampliada (NN via GPU), i es presenta via logical presentation
|
||||||
|
// a la mida de la font intermèdia.
|
||||||
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
|
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
|
||||||
|
|
||||||
|
const int mult = Options::video.internal_resolution;
|
||||||
|
if (mult > 1) {
|
||||||
|
ensureFallbackInternalTexture();
|
||||||
|
if (internal_texture_sdl_ != nullptr) {
|
||||||
|
// Còpia NN a la textura intermèdia (mult·game). Sampler NN
|
||||||
|
// per construcció: volem píxels grans i nets.
|
||||||
|
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
|
||||||
|
SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
// Filtre global al pas final → finestra (via logical presentation
|
||||||
|
// que applyFallbackPresentation ja configura amb mida game·mult).
|
||||||
|
SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||||
|
? SDL_SCALEMODE_LINEAR
|
||||||
|
: SDL_SCALEMODE_NEAREST;
|
||||||
|
SDL_SetTextureScaleMode(internal_texture_sdl_, final_scale);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
SDL_RenderTexture(renderer_, internal_texture_sdl_, nullptr, nullptr);
|
||||||
|
SDL_RenderPresent(renderer_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Si la creació de la textura intermèdia ha fallat, caiem al path normal.
|
||||||
|
}
|
||||||
|
// mult=1 (o fallback-del-fallback): texture_ directament. El scale mode
|
||||||
|
// el manté applyFallbackPresentation — però el re-apliquem per si la
|
||||||
|
// ruta mult>1 el va sobreescriure anteriorment.
|
||||||
|
SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||||
|
? SDL_SCALEMODE_LINEAR
|
||||||
|
: SDL_SCALEMODE_NEAREST;
|
||||||
|
SDL_SetTextureScaleMode(texture_, direct_scale);
|
||||||
SDL_RenderClear(renderer_);
|
SDL_RenderClear(renderer_);
|
||||||
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
||||||
SDL_RenderPresent(renderer_);
|
SDL_RenderPresent(renderer_);
|
||||||
@@ -182,26 +306,40 @@ void Screen::toggleShaders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::toggleSupersampling() {
|
auto Screen::toggleSupersampling() -> bool {
|
||||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
// 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;
|
Options::video.supersampling = !Options::video.supersampling;
|
||||||
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::toggleAspectRatio() {
|
void Screen::toggleAspectRatio() {
|
||||||
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
|
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
|
||||||
if (shader_backend_) {
|
if (shader_backend_) {
|
||||||
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
||||||
|
} else {
|
||||||
|
applyFallbackPresentation();
|
||||||
}
|
}
|
||||||
if (!fullscreen_) {
|
if (!fullscreen_) {
|
||||||
adjustWindowSize();
|
adjustWindowSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::toggleIntegerScale() {
|
void Screen::cycleScalingMode(int dir) {
|
||||||
Options::video.integer_scale = !Options::video.integer_scale;
|
constexpr int N = 5; // DISABLED, STRETCH, LETTERBOX, OVERSCAN, INTEGER
|
||||||
|
int cur = static_cast<int>(Options::video.scaling_mode);
|
||||||
|
int step = (dir >= 0) ? 1 : -1;
|
||||||
|
cur = ((cur + step) % N + N) % N;
|
||||||
|
Options::video.scaling_mode = static_cast<Options::ScalingMode>(cur);
|
||||||
if (shader_backend_) {
|
if (shader_backend_) {
|
||||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
shader_backend_->setScalingMode(Options::video.scaling_mode);
|
||||||
|
} else {
|
||||||
|
applyFallbackPresentation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,15 +350,39 @@ void Screen::toggleVSync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::toggleStretchFilter() {
|
void Screen::cycleTextureFilter(int dir) {
|
||||||
Options::video.stretch_filter_linear = !Options::video.stretch_filter_linear;
|
// NEAREST <-> LINEAR (només 2 valors, dir no importa més enllà de canviar)
|
||||||
|
(void)dir;
|
||||||
|
Options::video.texture_filter =
|
||||||
|
(Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||||
|
? Options::TextureFilter::NEAREST
|
||||||
|
: Options::TextureFilter::LINEAR;
|
||||||
if (shader_backend_) {
|
if (shader_backend_) {
|
||||||
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear);
|
shader_backend_->setTextureFilter(Options::video.texture_filter);
|
||||||
|
} else {
|
||||||
|
applyFallbackPresentation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::nextShaderType() {
|
void Screen::changeInternalResolution(int dir) {
|
||||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
|
||||||
|
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) {
|
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||||
@@ -231,45 +393,50 @@ void Screen::nextShaderType() {
|
|||||||
Options::video.current_shader = "postfx";
|
Options::video.current_shader = "postfx";
|
||||||
applyCurrentPostFXPreset();
|
applyCurrentPostFXPreset();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::nextPreset() {
|
auto Screen::nextPreset() -> bool {
|
||||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||||
|
if (!Options::video.shader_enabled) return false;
|
||||||
|
|
||||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||||
if (Options::postfx_presets.empty()) return;
|
if (Options::postfx_presets.empty()) return false;
|
||||||
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||||
applyCurrentPostFXPreset();
|
applyCurrentPostFXPreset();
|
||||||
} else {
|
} else {
|
||||||
if (Options::crtpi_presets.empty()) return;
|
if (Options::crtpi_presets.empty()) return false;
|
||||||
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
||||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||||
applyCurrentCrtPiPreset();
|
applyCurrentCrtPiPreset();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::prevShaderType() {
|
auto Screen::prevShaderType() -> bool {
|
||||||
// Només dues opcions — prev == next
|
// Només dues opcions — prev == next
|
||||||
nextShaderType();
|
return nextShaderType();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::prevPreset() {
|
auto Screen::prevPreset() -> bool {
|
||||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||||
|
if (!Options::video.shader_enabled) return false;
|
||||||
|
|
||||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||||
if (Options::postfx_presets.empty()) return;
|
if (Options::postfx_presets.empty()) return false;
|
||||||
int n = static_cast<int>(Options::postfx_presets.size());
|
int n = static_cast<int>(Options::postfx_presets.size());
|
||||||
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
|
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
|
||||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||||
applyCurrentPostFXPreset();
|
applyCurrentPostFXPreset();
|
||||||
} else {
|
} else {
|
||||||
if (Options::crtpi_presets.empty()) return;
|
if (Options::crtpi_presets.empty()) return false;
|
||||||
int n = static_cast<int>(Options::crtpi_presets.size());
|
int n = static_cast<int>(Options::crtpi_presets.size());
|
||||||
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
|
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
|
||||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||||
applyCurrentCrtPiPreset();
|
applyCurrentCrtPiPreset();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Screen::getCurrentPresetName() const -> const char* {
|
auto Screen::getCurrentPresetName() const -> const char* {
|
||||||
@@ -371,6 +538,77 @@ void Screen::updateRenderInfo() {
|
|||||||
0b1001);
|
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() {
|
void Screen::adjustWindowSize() {
|
||||||
int w = GAME_WIDTH * zoom_;
|
int w = GAME_WIDTH * zoom_;
|
||||||
// Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2)
|
// 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;
|
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 void destroy();
|
||||||
static auto get() -> Screen*;
|
static auto get() -> Screen*;
|
||||||
|
|
||||||
|
~Screen(); // públic per a std::unique_ptr
|
||||||
|
|
||||||
// Presentació — rep el buffer ARGB de 320x200 de JD8
|
// Presentació — rep el buffer ARGB de 320x200 de JD8
|
||||||
void present(Uint32* pixel_data);
|
void present(Uint32* pixel_data);
|
||||||
|
|
||||||
@@ -23,16 +25,21 @@ class Screen {
|
|||||||
void setZoom(int zoom);
|
void setZoom(int zoom);
|
||||||
|
|
||||||
// Shaders i vídeo
|
// Shaders i vídeo
|
||||||
|
// Mètodes que depenen d'una precondició (GPU present, shaders on, etc.)
|
||||||
|
// retornen `bool`: true si l'acció s'ha aplicat, false si la precondició
|
||||||
|
// no es complia. Els callers (F-keys, menú) poden suprimir notificacions
|
||||||
|
// o feedback quan la crida no ha tingut efecte.
|
||||||
void toggleShaders();
|
void toggleShaders();
|
||||||
void toggleSupersampling();
|
auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
|
||||||
void toggleAspectRatio();
|
void toggleAspectRatio();
|
||||||
void toggleIntegerScale();
|
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
|
||||||
void toggleVSync();
|
void toggleVSync();
|
||||||
void toggleStretchFilter();
|
void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
|
||||||
void nextShaderType(); // Cicla PostFX ↔ CrtPi (F7)
|
void changeInternalResolution(int dir); // +/−1, clampat a [1, max_zoom_]
|
||||||
void prevShaderType(); // Cicla al revés
|
auto nextShaderType() -> bool; // false si GPU off / shaders off
|
||||||
void nextPreset(); // Cicla presets del shader actiu (F8)
|
auto prevShaderType() -> bool; // idem
|
||||||
void prevPreset(); // Cicla presets al revés
|
auto nextPreset() -> bool; // false si GPU off / shaders off
|
||||||
|
auto prevPreset() -> bool; // idem
|
||||||
[[nodiscard]] auto getCurrentPresetName() const -> const char*;
|
[[nodiscard]] auto getCurrentPresetName() const -> const char*;
|
||||||
void setActiveShader(Rendering::ShaderType type);
|
void setActiveShader(Rendering::ShaderType type);
|
||||||
void applyCurrentPostFXPreset();
|
void applyCurrentPostFXPreset();
|
||||||
@@ -41,24 +48,36 @@ class Screen {
|
|||||||
// Getters
|
// Getters
|
||||||
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
|
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
|
||||||
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
|
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
|
||||||
|
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
|
||||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
|
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
|
||||||
[[nodiscard]] auto getActiveShaderName() const -> const char*;
|
[[nodiscard]] auto getActiveShaderName() const -> const char*;
|
||||||
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
|
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
|
||||||
[[nodiscard]] auto getRenderer() -> SDL_Renderer* { return renderer_; }
|
[[nodiscard]] auto getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// Sincronització amb el canvas HTML quan el navegador canvia la mida
|
||||||
|
// (fullscreen entrant/eixint, rotació de mòbil). Cridat pels callbacks
|
||||||
|
// natius d'Emscripten registrats al constructor.
|
||||||
|
void handleCanvasResized();
|
||||||
|
void syncFullscreenFlagFromBrowser(bool is_fullscreen);
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Screen();
|
Screen();
|
||||||
~Screen();
|
|
||||||
|
|
||||||
void adjustWindowSize();
|
void adjustWindowSize();
|
||||||
void calculateMaxZoom();
|
void calculateMaxZoom();
|
||||||
void initShaders();
|
void initShaders();
|
||||||
|
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
|
||||||
|
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
|
||||||
|
|
||||||
static Screen* instance_;
|
static std::unique_ptr<Screen> instance_;
|
||||||
|
|
||||||
SDL_Window* window_{nullptr};
|
SDL_Window* window_{nullptr};
|
||||||
SDL_Renderer* renderer_{nullptr};
|
SDL_Renderer* renderer_{nullptr};
|
||||||
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
|
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
|
||||||
|
SDL_Texture* internal_texture_sdl_{nullptr}; // 320·N x 200·N TARGET (fallback path, només si N>1)
|
||||||
|
int internal_texture_mult_{0}; // Multiplicador amb què es va crear internal_texture_sdl_
|
||||||
|
|
||||||
// Backend GPU (nullptr si no disponible o desactivat)
|
// Backend GPU (nullptr si no disponible o desactivat)
|
||||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
|
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
#include <iostream> // std::cout
|
#include <iostream> // std::cout
|
||||||
|
|
||||||
#ifndef __APPLE__
|
#ifndef __APPLE__
|
||||||
#include "core/rendering/sdl3gpu/crtpi_frag_spv.h"
|
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
|
||||||
#include "core/rendering/sdl3gpu/downscale_frag_spv.h"
|
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
|
||||||
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
|
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
|
||||||
#include "core/rendering/sdl3gpu/postfx_vert_spv.h"
|
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
|
||||||
#include "core/rendering/sdl3gpu/upscale_frag_spv.h"
|
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
@@ -456,6 +456,11 @@ namespace Rendering {
|
|||||||
return false;
|
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
|
// scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
|
||||||
ss_factor_ = 0;
|
ss_factor_ = 0;
|
||||||
|
|
||||||
@@ -812,14 +817,50 @@ namespace Rendering {
|
|||||||
SDL_EndGPUCopyPass(copy);
|
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)
|
// 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.
|
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
|
||||||
// Sense SS ni stretch: scene_texture_ a game_height_.
|
// 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).
|
// 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_;
|
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) {
|
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
|
||||||
SDL_GPUColorTargetInfo upscale_target = {};
|
SDL_GPUColorTargetInfo upscale_target = {};
|
||||||
@@ -827,15 +868,14 @@ namespace Rendering {
|
|||||||
upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
|
upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
|
||||||
upscale_target.store_op = SDL_GPU_STOREOP_STORE;
|
upscale_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
|
||||||
// Triar filtre: si 4:3 actiu, usar el filtre configurable per a l'estirament.
|
// Filtre global: s'aplica sempre (ja no depèn de 4:3).
|
||||||
// Si no, usar el filtre d'upscale normal (linear_upscale_).
|
bool use_linear = texture_filter_linear_;
|
||||||
bool use_linear = stretch_4_3_ ? stretch_filter_linear_ : linear_upscale_;
|
|
||||||
|
|
||||||
SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr);
|
SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr);
|
||||||
if (upass != nullptr) {
|
if (upass != nullptr) {
|
||||||
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
||||||
SDL_GPUTextureSamplerBinding ubinding = {};
|
SDL_GPUTextureSamplerBinding ubinding = {};
|
||||||
ubinding.texture = scene_texture_;
|
ubinding.texture = source_texture;
|
||||||
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
||||||
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
||||||
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
||||||
@@ -847,6 +887,7 @@ namespace Rendering {
|
|||||||
// Sense SS: el viewport s'encarrega de l'estirament geomètric
|
// Sense SS: el viewport s'encarrega de l'estirament geomètric
|
||||||
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
|
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
|
||||||
}
|
}
|
||||||
|
(void)source_height;
|
||||||
|
|
||||||
// ---- Acquire swapchain texture ----
|
// ---- Acquire swapchain texture ----
|
||||||
SDL_GPUTexture* swapchain = nullptr;
|
SDL_GPUTexture* swapchain = nullptr;
|
||||||
@@ -872,16 +913,37 @@ namespace Rendering {
|
|||||||
float vy = 0.0F;
|
float vy = 0.0F;
|
||||||
float vw = 0.0F;
|
float vw = 0.0F;
|
||||||
float vh = 0.0F;
|
float vh = 0.0F;
|
||||||
if (integer_scale_) {
|
switch (scaling_mode_) {
|
||||||
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)));
|
case Options::ScalingMode::DISABLED:
|
||||||
vw = logical_w * static_cast<float>(SCALE);
|
// 1:1, sense escala (pot ser diminut en finestres grans)
|
||||||
vh = logical_h * static_cast<float>(SCALE);
|
vw = logical_w;
|
||||||
} else {
|
vh = logical_h;
|
||||||
const float SCALE = std::min(
|
break;
|
||||||
static_cast<float>(sw) / logical_w,
|
case Options::ScalingMode::STRETCH:
|
||||||
static_cast<float>(sh) / logical_h);
|
// Omple tota la finestra, escala no uniforme
|
||||||
vw = logical_w * SCALE;
|
vw = static_cast<float>(sw);
|
||||||
vh = logical_h * SCALE;
|
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);
|
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||||
vy = std::floor((static_cast<float>(sh) - vh) * 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_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
SDL_SetGPUViewport(pass, &vp);
|
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 = {};
|
SDL_GPUTextureSamplerBinding binding = {};
|
||||||
binding.texture = effective_scene;
|
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);
|
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||||
|
|
||||||
// Injectar texture_width/height abans del push
|
// 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_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
SDL_SetGPUViewport(pass, &vp);
|
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)
|
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
|
||||||
? scaled_texture_
|
? scaled_texture_
|
||||||
: effective_scene;
|
: effective_scene;
|
||||||
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
|
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||||
? linear_sampler_
|
? linear_sampler_
|
||||||
: sampler_;
|
: sampler_;
|
||||||
|
|
||||||
@@ -1047,6 +1118,10 @@ namespace Rendering {
|
|||||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
scene_texture_ = nullptr;
|
scene_texture_ = nullptr;
|
||||||
}
|
}
|
||||||
|
if (internal_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||||
|
internal_texture_ = nullptr;
|
||||||
|
}
|
||||||
if (scaled_texture_ != nullptr) {
|
if (scaled_texture_ != nullptr) {
|
||||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||||
scaled_texture_ = nullptr;
|
scaled_texture_ = nullptr;
|
||||||
@@ -1193,8 +1268,20 @@ namespace Rendering {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDL3GPUShader::setScaleMode(bool integer_scale) {
|
void SDL3GPUShader::setScalingMode(Options::ScalingMode mode) {
|
||||||
integer_scale_ = integer_scale;
|
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) {
|
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) {
|
void SDL3GPUShader::setDownscaleAlgo(int algo) {
|
||||||
downscale_algo_ = std::max(0, std::min(algo, 2));
|
downscale_algo_ = std::max(0, std::min(algo, 2));
|
||||||
}
|
}
|
||||||
@@ -1246,6 +1329,10 @@ namespace Rendering {
|
|||||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
scene_texture_ = nullptr;
|
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
|
// scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
|
||||||
if (scaled_texture_ != nullptr) {
|
if (scaled_texture_ != nullptr) {
|
||||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||||
@@ -1288,10 +1375,15 @@ namespace Rendering {
|
|||||||
return false;
|
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_width_,
|
||||||
game_height_,
|
game_height_,
|
||||||
oversample_ > 1 ? "on" : "off");
|
oversample_ > 1 ? "on" : "off",
|
||||||
|
internal_res_);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1362,4 +1454,42 @@ namespace Rendering {
|
|||||||
return true;
|
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
|
} // namespace Rendering
|
||||||
|
|||||||
@@ -96,15 +96,12 @@ namespace Rendering {
|
|||||||
// Activa/desactiva VSync en el swapchain
|
// Activa/desactiva VSync en el swapchain
|
||||||
void setVSync(bool vsync) override;
|
void setVSync(bool vsync) override;
|
||||||
|
|
||||||
// Activa/desactiva escalado entero (integer scale)
|
// Selecciona el mode de presentació lògica (DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER)
|
||||||
void setScaleMode(bool integer_scale) override;
|
void setScalingMode(Options::ScalingMode mode) override;
|
||||||
|
|
||||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||||
void setOversample(int factor) override;
|
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
|
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||||
void setDownscaleAlgo(int algo) override;
|
void setDownscaleAlgo(int algo) override;
|
||||||
|
|
||||||
@@ -123,7 +120,14 @@ namespace Rendering {
|
|||||||
// Estirament vertical 4:3 (fusionat amb l'upscale pass)
|
// Estirament vertical 4:3 (fusionat amb l'upscale pass)
|
||||||
void setStretch4_3(bool enabled) override;
|
void setStretch4_3(bool enabled) override;
|
||||||
[[nodiscard]] auto isStretch4_3() const -> bool override { return stretch_4_3_; }
|
[[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:
|
private:
|
||||||
static auto createShaderMSL(SDL_GPUDevice* device,
|
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||||
@@ -145,6 +149,7 @@ namespace Rendering {
|
|||||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
auto 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)
|
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||||
@@ -157,6 +162,7 @@ namespace Rendering {
|
|||||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
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* 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* 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_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
|
||||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
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 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 oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
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 driver_name_;
|
||||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||||
bool is_initialized_ = false;
|
bool is_initialized_ = false;
|
||||||
bool vsync_ = true;
|
bool vsync_ = true;
|
||||||
bool integer_scale_ = false;
|
Options::ScalingMode scaling_mode_ = Options::ScalingMode::INTEGER;
|
||||||
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
|
|
||||||
bool stretch_4_3_ = false; // Estirament vertical 4:3
|
bool stretch_4_3_ = false; // Estirament vertical 4:3
|
||||||
bool stretch_filter_linear_ = false; // Filtre per a l'estirament 4:3 (false=NEAREST, true=LINEAR)
|
bool texture_filter_linear_ = false; // Filtre global (false=NEAREST, true=LINEAR)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Rendering
|
} // namespace Rendering
|
||||||
|
|||||||
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 <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "game/options.hpp"
|
||||||
|
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
/** @brief Identificador del shader de post-procesado activo */
|
/** @brief Identificador del shader de post-procesado activo */
|
||||||
@@ -105,9 +107,9 @@ namespace Rendering {
|
|||||||
virtual void setVSync(bool /*vsync*/) {}
|
virtual void setVSync(bool /*vsync*/) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Activa o desactiva el escalado entero (integer scale)
|
* @brief Selecciona el mode d'escala de la finestra (mapeja SDL_RendererLogicalPresentation).
|
||||||
*/
|
*/
|
||||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
virtual void setScalingMode(Options::ScalingMode /*mode*/) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||||
@@ -116,13 +118,6 @@ namespace Rendering {
|
|||||||
*/
|
*/
|
||||||
virtual void setOversample(int /*factor*/) {}
|
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).
|
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||||
@@ -179,9 +174,16 @@ namespace Rendering {
|
|||||||
[[nodiscard]] virtual auto isStretch4_3() const -> bool { return false; }
|
[[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
|
} // namespace Rendering
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
|
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
|
||||||
struct rgb;
|
struct rgb;
|
||||||
@@ -19,10 +19,6 @@ Text::Text(const char* fnt_file, const char* gif_file) {
|
|||||||
loadFont(fnt_file);
|
loadFont(fnt_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
Text::~Text() {
|
|
||||||
if (bitmap_) free(bitmap_);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UTF-8 ---
|
// --- UTF-8 ---
|
||||||
|
|
||||||
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||||
@@ -62,15 +58,13 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
|||||||
// --- Càrrega de font ---
|
// --- Càrrega de font ---
|
||||||
|
|
||||||
void Text::loadFont(const char* fnt_file) {
|
void Text::loadFont(const char* fnt_file) {
|
||||||
int filesize = 0;
|
auto buffer = ResourceHelper::loadFile(fnt_file);
|
||||||
char* buffer = file_getfilebuffer(fnt_file, filesize, true);
|
if (buffer.empty()) {
|
||||||
if (!buffer) {
|
|
||||||
std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
|
std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::istringstream stream(std::string(buffer, filesize));
|
std::istringstream stream(std::string(reinterpret_cast<const char*>(buffer.data()), buffer.size()));
|
||||||
free(buffer);
|
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
int glyph_index = 0;
|
int glyph_index = 0;
|
||||||
@@ -82,27 +76,27 @@ void Text::loadFont(const char* fnt_file) {
|
|||||||
// Elimina comentaris inline
|
// Elimina comentaris inline
|
||||||
auto comment_pos = line.find('#');
|
auto comment_pos = line.find('#');
|
||||||
if (comment_pos != std::string::npos) {
|
if (comment_pos != std::string::npos) {
|
||||||
line = line.substr(0, comment_pos);
|
line.resize(comment_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parseja directives
|
// Parseja directives
|
||||||
if (line.find("box_width") == 0) {
|
if (line.starts_with("box_width")) {
|
||||||
sscanf(line.c_str(), "box_width %d", &box_width_);
|
sscanf(line.c_str(), "box_width %d", &box_width_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.find("box_height") == 0) {
|
if (line.starts_with("box_height")) {
|
||||||
sscanf(line.c_str(), "box_height %d", &box_height_);
|
sscanf(line.c_str(), "box_height %d", &box_height_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.find("columns") == 0) {
|
if (line.starts_with("columns")) {
|
||||||
sscanf(line.c_str(), "columns %d", &columns_);
|
sscanf(line.c_str(), "columns %d", &columns_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.find("cell_spacing") == 0) {
|
if (line.starts_with("cell_spacing")) {
|
||||||
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
|
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.find("row_spacing") == 0) {
|
if (line.starts_with("row_spacing")) {
|
||||||
sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
|
sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -128,15 +122,14 @@ void Text::loadFont(const char* fnt_file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Text::loadBitmap(const char* gif_file) {
|
void Text::loadBitmap(const char* gif_file) {
|
||||||
int filesize = 0;
|
auto buffer = ResourceHelper::loadFile(gif_file);
|
||||||
char* buffer = file_getfilebuffer(gif_file, filesize);
|
if (buffer.empty()) {
|
||||||
if (!buffer) {
|
|
||||||
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
|
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
|
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
|
||||||
auto* raw = reinterpret_cast<unsigned char*>(buffer);
|
auto* raw = buffer.data();
|
||||||
int w = raw[6] | (raw[7] << 8);
|
int w = raw[6] | (raw[7] << 8);
|
||||||
int h = raw[8] | (raw[9] << 8);
|
int h = raw[8] | (raw[9] << 8);
|
||||||
|
|
||||||
@@ -144,22 +137,21 @@ void Text::loadBitmap(const char* gif_file) {
|
|||||||
Uint8* pixels = LoadGif(raw, &gw, &gh);
|
Uint8* pixels = LoadGif(raw, &gw, &gh);
|
||||||
if (!pixels) {
|
if (!pixels) {
|
||||||
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
|
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
|
||||||
free(buffer);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap_width_ = w;
|
bitmap_width_ = w;
|
||||||
bitmap_height_ = h;
|
bitmap_height_ = h;
|
||||||
bitmap_ = pixels;
|
bitmap_.assign(pixels, pixels + (static_cast<size_t>(w) * h));
|
||||||
|
free(pixels); // LoadGif usa malloc internament
|
||||||
|
|
||||||
free(buffer);
|
|
||||||
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
|
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Renderitzat ---
|
// --- Renderitzat ---
|
||||||
|
|
||||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
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;
|
const char* ptr = text;
|
||||||
int cursor_x = x;
|
int cursor_x = x;
|
||||||
@@ -212,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 {
|
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
|
// Descart ràpid si el glifo sencer cau fora verticalment
|
||||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
|
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
|
||||||
@@ -267,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 {
|
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;
|
const char* ptr = text;
|
||||||
int cursor_x = x;
|
int cursor_x = x;
|
||||||
@@ -313,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 {
|
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;
|
const char* ptr = text;
|
||||||
int cursor_x = x;
|
int cursor_x = x;
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class Text {
|
class Text {
|
||||||
public:
|
public:
|
||||||
Text(const char* fnt_file, const char* gif_file);
|
Text(const char* fnt_file, const char* gif_file);
|
||||||
~Text();
|
|
||||||
|
|
||||||
// Pinta texto sobre un buffer ARGB de 320x200
|
// Pinta texto sobre un buffer ARGB de 320x200
|
||||||
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const;
|
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const;
|
||||||
@@ -46,7 +46,7 @@ class Text {
|
|||||||
int cell_spacing_{0};
|
int cell_spacing_{0};
|
||||||
int row_spacing_{0};
|
int row_spacing_{0};
|
||||||
|
|
||||||
Uint8* bitmap_{nullptr}; // píxels 8-bit del GIF de la font
|
std::vector<Uint8> bitmap_; // píxels 8-bit del GIF de la font
|
||||||
int bitmap_width_{0};
|
int bitmap_width_{0};
|
||||||
int bitmap_height_{0};
|
int bitmap_height_{0};
|
||||||
|
|
||||||
|
|||||||
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,316 +3,406 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/input/gamepad.hpp"
|
#include "core/input/gamepad.hpp"
|
||||||
#include "core/input/global_inputs.hpp"
|
#include "core/input/global_inputs.hpp"
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/input/key_remap.hpp"
|
#include "core/input/key_remap.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/jail/jail_audio.hpp"
|
#include "core/jail/jdraw8.hpp"
|
||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
#include "core/jail/jinput.hpp"
|
#include "core/jail/jinput.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/menu.hpp"
|
#include "core/rendering/menu.hpp"
|
||||||
#include "core/rendering/overlay.hpp"
|
#include "core/rendering/overlay.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "game/info.hpp"
|
#include "game/info.hpp"
|
||||||
#include "game/modulegame.hpp"
|
#include "game/modulegame.hpp"
|
||||||
#include "game/modulesequence.hpp"
|
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "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"
|
||||||
|
#include "scenes/menu_scene.hpp"
|
||||||
|
#include "scenes/mort_scene.hpp"
|
||||||
|
#include "scenes/scene.hpp"
|
||||||
|
#include "scenes/scene_registry.hpp"
|
||||||
|
#include "scenes/secreta_scene.hpp"
|
||||||
|
#include "scenes/slides_scene.hpp"
|
||||||
|
|
||||||
// Cheats del joc original — declarats a jinput.cpp
|
// Cheats del joc original — declarats a jinput.cpp
|
||||||
extern void JI_moveCheats(Uint8 new_key);
|
extern void JI_moveCheats(Uint8 new_key);
|
||||||
|
|
||||||
Director* Director::instance_ = nullptr;
|
std::unique_ptr<Director> Director::instance_;
|
||||||
|
|
||||||
|
Director::~Director() = default;
|
||||||
|
|
||||||
|
void Director::initGameContext() {
|
||||||
|
info::ctx.num_habitacio = Options::game.habitacio_inicial;
|
||||||
|
info::ctx.num_piramide = Options::game.piramide_inicial;
|
||||||
|
info::ctx.diners = Options::game.diners_inicial;
|
||||||
|
info::ctx.diamants = Options::game.diamants_inicial;
|
||||||
|
info::ctx.vida = Options::game.vides;
|
||||||
|
info::ctx.momies = 0;
|
||||||
|
info::ctx.nou_personatge = false;
|
||||||
|
info::ctx.pepe_activat = false;
|
||||||
|
|
||||||
|
FILE* ini = fopen("trick.ini", "rb");
|
||||||
|
if (ini != nullptr) {
|
||||||
|
info::ctx.nou_personatge = true;
|
||||||
|
fclose(ini);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
void Director::init() {
|
||||||
instance_ = new Director();
|
instance_ = std::unique_ptr<Director>(new Director());
|
||||||
Gamepad::init();
|
Gamepad::init();
|
||||||
|
|
||||||
|
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
|
||||||
|
// amb una factory de `scenes::Scene`. iterate() consulta aquest
|
||||||
|
// registry per a tots els states de seqüència (game_state_ == 1); si
|
||||||
|
// una clau no apareix ací, Director surt ordenadament.
|
||||||
|
auto& registry = scenes::SceneRegistry::instance();
|
||||||
|
registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); });
|
||||||
|
registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); });
|
||||||
|
// BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix
|
||||||
|
// pel switch intern llegint info::ctx.num_piramide).
|
||||||
|
for (int p = 2; p <= 5; ++p) {
|
||||||
|
registry.registerScene(p, [] { return std::make_unique<scenes::BannerScene>(); });
|
||||||
|
}
|
||||||
|
// SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
|
||||||
|
// - num_piramide == 1: slides narratius inicials (entrada al joc)
|
||||||
|
// - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
|
||||||
|
// l'usuari no té prou diners per a la Secreta)
|
||||||
|
registry.registerScene(1, [] { return std::make_unique<scenes::SlidesScene>(); });
|
||||||
|
registry.registerScene(7, [] { return std::make_unique<scenes::SlidesScene>(); });
|
||||||
|
registry.registerScene(6, [] { return std::make_unique<scenes::SecretaScene>(); });
|
||||||
|
registry.registerScene(8, [] { return std::make_unique<scenes::CreditsScene>(); });
|
||||||
|
// State 255 (intro): dues variants segons `Options::game.use_new_logo`.
|
||||||
|
// La factory tria a runtime — així es pot togglar des del menú sense
|
||||||
|
// re-registrar. Les dues escenes construeixen una IntroSpritesScene
|
||||||
|
// com a sub-escena per a la part d'animacions de sprites.
|
||||||
|
registry.registerScene(255, []() -> std::unique_ptr<scenes::Scene> {
|
||||||
|
if (Options::game.use_new_logo) {
|
||||||
|
return std::make_unique<scenes::IntroNewLogoScene>();
|
||||||
|
}
|
||||||
|
return std::make_unique<scenes::IntroScene>();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::destroy() {
|
void Director::destroy() {
|
||||||
Gamepad::destroy();
|
Gamepad::destroy();
|
||||||
delete instance_;
|
instance_.reset();
|
||||||
instance_ = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Director::get() -> Director* {
|
auto Director::get() -> Director* {
|
||||||
return instance_;
|
return instance_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::togglePause() {
|
void Director::togglePause() {
|
||||||
paused_ = !paused_;
|
paused_ = !paused_;
|
||||||
if (paused_) {
|
if (paused_) {
|
||||||
JA_PauseMusic();
|
Audio::get()->pauseMusic();
|
||||||
} else {
|
} else {
|
||||||
JA_ResumeMusic();
|
Audio::get()->resumeMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::run() {
|
void Director::setup() {
|
||||||
// Llança el game thread
|
// Els buffers són membres (director.hpp); només els inicialitzem.
|
||||||
game_thread_ = std::thread(&Director::gameThreadFunc, this);
|
std::memset(game_frame_, 0, sizeof(game_frame_));
|
||||||
|
std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_));
|
||||||
|
has_frame_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Doble buffer: game_frame és el frame net del joc, presentation_buffer
|
bool Director::iterate() {
|
||||||
// és el frame + overlay (es regenera cada iteració des de game_frame)
|
if (quit_requested_) {
|
||||||
Uint32 game_frame[320 * 200]{};
|
JG_QuitSignal();
|
||||||
Uint32 presentation_buffer[320 * 200]{};
|
current_scene_.reset(); // destrueix l'escena actual ordenadament
|
||||||
bool has_frame = false;
|
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_VSYNC = 16; // ~60 FPS amb VSync
|
||||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||||
|
|
||||||
// Bucle principal del director (no-bloquejant)
|
const Uint32 frame_start = SDL_GetTicks();
|
||||||
while (!game_thread_done_ && !quit_requested_) {
|
|
||||||
Uint32 frame_start = SDL_GetTicks();
|
|
||||||
|
|
||||||
handleEvents();
|
Gamepad::update();
|
||||||
Gamepad::update();
|
KeyRemap::update();
|
||||||
KeyRemap::update();
|
GlobalInputs::handle();
|
||||||
GlobalInputs::handle();
|
Mouse::updateCursorVisibility();
|
||||||
Mouse::updateCursorVisibility();
|
|
||||||
|
|
||||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
// Bombeig de l'àudio: reomple l'stream de música i para els canals
|
||||||
// arriba al menú del títol (info::num_piramide == 0). Lectura no
|
// drenats. Substituïx el callback de SDL_AddTimer de la versió
|
||||||
// atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.
|
// antiga — imprescindible per al port a emscripten.
|
||||||
static bool credits_triggered = false;
|
Audio::update();
|
||||||
if (!credits_triggered && info::num_piramide == 0) {
|
|
||||||
if (Options::game.show_title_credits) {
|
|
||||||
Overlay::startCredits();
|
|
||||||
}
|
|
||||||
credits_triggered = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||||
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
||||||
esc_blocked_ = false;
|
static bool credits_triggered = false;
|
||||||
}
|
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
||||||
|
if (Options::game.show_title_credits) {
|
||||||
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja).
|
Overlay::startCredits();
|
||||||
// Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame.
|
|
||||||
bool new_frame = false;
|
|
||||||
if (!paused_) {
|
|
||||||
std::lock_guard lock(mutex_);
|
|
||||||
if (frame_ready_ && latest_frame_ != nullptr) {
|
|
||||||
memcpy(game_frame, latest_frame_, sizeof(game_frame));
|
|
||||||
frame_ready_ = false;
|
|
||||||
frame_consumed_ = true;
|
|
||||||
has_frame = true;
|
|
||||||
new_frame = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (new_frame) {
|
|
||||||
frame_consumed_cv_.notify_one(); // desbloqueja el joc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
|
||||||
if (has_frame) {
|
|
||||||
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer));
|
|
||||||
Screen::get()->present(presentation_buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Límit de framerate segons VSync
|
|
||||||
Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
|
|
||||||
Uint32 elapsed = SDL_GetTicks() - frame_start;
|
|
||||||
if (elapsed < target_ms) {
|
|
||||||
SDL_Delay(target_ms - elapsed);
|
|
||||||
}
|
}
|
||||||
|
credits_triggered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assegura que el game thread ix (despertar-lo per si està esperant)
|
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
||||||
quit_requested_ = true;
|
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
||||||
JG_QuitSignal();
|
esc_blocked_ = false;
|
||||||
{
|
|
||||||
std::lock_guard lock(mutex_);
|
|
||||||
frame_consumed_ = true;
|
|
||||||
}
|
}
|
||||||
frame_consumed_cv_.notify_all();
|
|
||||||
|
|
||||||
if (game_thread_.joinable()) {
|
// Avança l'escena (si no estem pausats). En pausa, es manté l'escena
|
||||||
game_thread_.join();
|
// congelada i re-presentem l'últim frame amb l'overlay fresc per
|
||||||
|
// damunt.
|
||||||
|
if (!paused_) {
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
||||||
|
if (has_frame_) {
|
||||||
|
std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
|
||||||
|
Screen::get()->present(presentation_buffer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Límit de framerate segons VSync.
|
||||||
|
// Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
|
||||||
|
// emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode
|
||||||
|
// que es basa en el timing intern de SDL en lloc del delay explícit.
|
||||||
|
const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
|
||||||
|
const Uint32 elapsed = SDL_GetTicks() - frame_start;
|
||||||
|
if (elapsed < target_ms) {
|
||||||
|
SDL_Delay(target_ms - elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::handleEvents() {
|
void Director::teardown() {
|
||||||
|
// Senyal de quit i descàrrega ordenada de l'escena en curs. Els
|
||||||
|
// destructors de cada escena són no-bloquejants — ja no fan fades
|
||||||
|
// bloquejants. La resta de cleanup la gestiona `destroy()`.
|
||||||
|
JG_QuitSignal();
|
||||||
|
current_scene_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Director::run() {
|
||||||
|
setup();
|
||||||
|
while (true) {
|
||||||
|
pollAllEvents();
|
||||||
|
if (!iterate()) break;
|
||||||
|
}
|
||||||
|
teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Director::pollAllEvents() {
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
if (event.type == SDL_EVENT_QUIT) {
|
handleEvent(event);
|
||||||
JG_QuitSignal();
|
|
||||||
requestQuit();
|
|
||||||
}
|
|
||||||
// Hot-plug de gamepad
|
|
||||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
|
||||||
Gamepad::handleEvent(event);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
|
|
||||||
Overlay::cancelCredits();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
|
|
||||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
|
|
||||||
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
|
||||||
menu_keys_held_[event.key.scancode] = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
|
|
||||||
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
|
||||||
Menu::captureKey(event.key.scancode);
|
|
||||||
menu_keys_held_[event.key.scancode] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
|
||||||
event.key.scancode == Options::keys_gui.pause_toggle) {
|
|
||||||
togglePause();
|
|
||||||
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
|
|
||||||
menu_keys_held_[event.key.scancode] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
|
||||||
event.key.scancode == Options::keys_gui.menu_toggle) {
|
|
||||||
Menu::toggle();
|
|
||||||
JI_SetInputBlocked(Menu::isOpen());
|
|
||||||
menu_keys_held_[event.key.scancode] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Si el menú està obert, consumeix tot l'input de teclat
|
|
||||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
|
||||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
|
||||||
Menu::close();
|
|
||||||
JI_SetInputBlocked(false);
|
|
||||||
// Empassa l'ESC fins al release perquè el joc no la veja per polling
|
|
||||||
esc_swallow_until_release_ = true;
|
|
||||||
} else {
|
|
||||||
Menu::handleKey(event.key.scancode);
|
|
||||||
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
|
|
||||||
if (!Menu::isOpen()) {
|
|
||||||
JI_SetInputBlocked(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menu_keys_held_[event.key.scancode] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
|
||||||
continue; // no deixem passar KEY_UP al joc tampoc
|
|
||||||
}
|
|
||||||
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar
|
|
||||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
|
|
||||||
esc_swallow_until_release_ = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
|
|
||||||
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
|
|
||||||
if (!Overlay::isEscConsumed()) {
|
|
||||||
// Primera pulsació: mostra notificació
|
|
||||||
Overlay::handleEscape();
|
|
||||||
} else {
|
|
||||||
// Segona pulsació: senyal d'eixida al joc
|
|
||||||
esc_blocked_ = false;
|
|
||||||
key_pressed_ = true;
|
|
||||||
JG_QuitSignal();
|
|
||||||
// Si estem en pausa, la desactivem (sense reprendre la música,
|
|
||||||
// estem eixint): el game thread està bloquejat a publishFrame
|
|
||||||
// i necessita que Director consumeixca frames per despertar-lo
|
|
||||||
// i poder veure la senyal de quit.
|
|
||||||
paused_ = false;
|
|
||||||
}
|
|
||||||
continue; // no processa més aquest event
|
|
||||||
}
|
|
||||||
if (event.type == SDL_EVENT_KEY_UP) {
|
|
||||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
|
||||||
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
|
||||||
// quan l'overlay faça timeout
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// Comprova si és una tecla GUI (no passa al joc)
|
|
||||||
const auto sc = event.key.scancode;
|
|
||||||
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
|
|
||||||
sc == Options::keys_gui.inc_zoom ||
|
|
||||||
sc == Options::keys_gui.fullscreen ||
|
|
||||||
sc == Options::keys_gui.toggle_shader ||
|
|
||||||
sc == Options::keys_gui.toggle_aspect_ratio ||
|
|
||||||
sc == Options::keys_gui.toggle_supersampling ||
|
|
||||||
sc == Options::keys_gui.next_shader ||
|
|
||||||
sc == Options::keys_gui.next_shader_preset ||
|
|
||||||
sc == Options::keys_gui.toggle_stretch_filter ||
|
|
||||||
sc == Options::keys_gui.toggle_render_info);
|
|
||||||
if (!is_gui_key) {
|
|
||||||
key_pressed_ = true;
|
|
||||||
JI_moveCheats(sc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mouse::handleEvent(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::publishFrame(Uint32* pixels) {
|
void Director::handleEvent(const SDL_Event& event) {
|
||||||
{
|
if (event.type == SDL_EVENT_QUIT) {
|
||||||
std::lock_guard lock(mutex_);
|
JG_QuitSignal();
|
||||||
latest_frame_ = pixels;
|
requestQuit();
|
||||||
frame_ready_ = true;
|
|
||||||
frame_consumed_ = false;
|
|
||||||
}
|
}
|
||||||
frame_produced_cv_.notify_one();
|
// Hot-plug de gamepad (a Emscripten els dispositius web entren com
|
||||||
|
// JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
|
||||||
// Espera que el director consumeixca el frame
|
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) {
|
||||||
std::unique_lock lock(mutex_);
|
Gamepad::handleEvent(event);
|
||||||
frame_consumed_cv_.wait(lock, [this] {
|
return;
|
||||||
return frame_consumed_ || quit_requested_;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
|
||||||
|
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
|
||||||
|
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
||||||
|
menu_keys_held_[event.key.scancode] = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
|
||||||
|
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||||
|
Menu::captureKey(event.key.scancode);
|
||||||
|
menu_keys_held_[event.key.scancode] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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 == KeyConfig::scancode("pause_toggle")) {
|
||||||
|
togglePause();
|
||||||
|
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 == KeyConfig::scancode("menu_toggle")) {
|
||||||
|
Menu::toggle();
|
||||||
|
JI_SetInputBlocked(Menu::isOpen());
|
||||||
|
menu_keys_held_[event.key.scancode] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Si el menú està obert, consumeix tot l'input de teclat
|
||||||
|
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||||
|
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
Menu::close();
|
||||||
|
JI_SetInputBlocked(false);
|
||||||
|
// Empassa l'ESC fins al release perquè el joc no la veja per polling
|
||||||
|
esc_swallow_until_release_ = true;
|
||||||
|
} else {
|
||||||
|
Menu::handleKey(event.key.scancode);
|
||||||
|
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
|
||||||
|
if (!Menu::isOpen()) {
|
||||||
|
JI_SetInputBlocked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu_keys_held_[event.key.scancode] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
|
||||||
|
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
|
||||||
|
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
|
||||||
|
if (!Overlay::isEscConsumed()) {
|
||||||
|
// Primera pulsació: mostra notificació
|
||||||
|
Overlay::handleEscape();
|
||||||
|
} else {
|
||||||
|
// Segona pulsació: senyal d'eixida al joc
|
||||||
|
esc_blocked_ = false;
|
||||||
|
key_pressed_ = true;
|
||||||
|
JG_QuitSignal();
|
||||||
|
// Si estem en pausa, la desactivem: el fiber del joc està
|
||||||
|
// congelat i necessita ser reprès per veure la senyal de
|
||||||
|
// quit i poder tornar de forma natural.
|
||||||
|
paused_ = false;
|
||||||
|
}
|
||||||
|
return; // no processa més aquest event
|
||||||
|
}
|
||||||
|
if (event.type == SDL_EVENT_KEY_UP) {
|
||||||
|
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
||||||
|
// quan l'overlay faça timeout
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Comprova si és una tecla d'UI registrada (no passa al joc).
|
||||||
|
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
|
||||||
|
// incloent pause_toggle i menu_toggle (defensa en profunditat:
|
||||||
|
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
|
||||||
|
const auto sc = event.key.scancode;
|
||||||
|
if (!KeyConfig::isGuiKey(sc)) {
|
||||||
|
key_pressed_ = true;
|
||||||
|
JI_moveCheats(sc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mouse::handleEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::requestQuit() {
|
void Director::requestQuit() {
|
||||||
quit_requested_ = true;
|
quit_requested_ = true;
|
||||||
JG_QuitSignal();
|
JG_QuitSignal();
|
||||||
frame_consumed_cv_.notify_all();
|
}
|
||||||
frame_produced_cv_.notify_all();
|
|
||||||
|
void Director::requestRestart() {
|
||||||
|
restart_requested_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Director::consumeKeyPressed() -> bool {
|
auto Director::consumeKeyPressed() -> bool {
|
||||||
return key_pressed_.exchange(false);
|
return key_pressed_.exchange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::gameThreadFunc() {
|
|
||||||
info::num_habitacio = Options::game.habitacio_inicial;
|
|
||||||
info::num_piramide = Options::game.piramide_inicial;
|
|
||||||
info::diners = 0;
|
|
||||||
info::diamants = 0;
|
|
||||||
info::vida = Options::game.vides;
|
|
||||||
info::momies = 0;
|
|
||||||
info::nou_personatge = false;
|
|
||||||
info::pepe_activat = false;
|
|
||||||
|
|
||||||
FILE* ini = fopen("trick.ini", "rb");
|
|
||||||
if (ini != nullptr) {
|
|
||||||
info::nou_personatge = true;
|
|
||||||
fclose(ini);
|
|
||||||
}
|
|
||||||
|
|
||||||
int gameState = 1;
|
|
||||||
while (gameState != -1 && !quit_requested_) {
|
|
||||||
switch (gameState) {
|
|
||||||
case 0: {
|
|
||||||
auto* moduleGame = new ModuleGame();
|
|
||||||
gameState = moduleGame->Go();
|
|
||||||
delete moduleGame;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1: {
|
|
||||||
auto* moduleSequence = new ModuleSequence();
|
|
||||||
gameState = moduleSequence->Go();
|
|
||||||
delete moduleSequence;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game_thread_done_ = true;
|
|
||||||
// Despertar el director per si esperava un frame
|
|
||||||
frame_produced_cv_.notify_all();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,30 +3,43 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <mutex>
|
#include <memory>
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
// El Director és el thread principal que controla la presentació i els inputs.
|
#include "scenes/scene.hpp"
|
||||||
// Executa el joc en un thread secundari (game thread) com si fos una "fibra emulada":
|
|
||||||
// el joc produeix un frame, es bloqueja a JD8_Flip(), i el director el presenta
|
// El Director és l'únic thread del runtime. Cada iterate() fa input →
|
||||||
// abans de donar-li via per produir el següent.
|
// tick de l'escena actual → JD8_Flip → overlay → present → sleep al frame
|
||||||
|
// target. Totes les escenes (`scenes::Scene` i `ModuleGame`) són
|
||||||
|
// tick-based i no bloquegen — no hi ha fibers, mutex ni condition_variable.
|
||||||
|
// Compatible amb SDL_AppIterate i amb el futur port a emscripten.
|
||||||
class Director {
|
class Director {
|
||||||
public:
|
public:
|
||||||
static void init();
|
static void init();
|
||||||
static void destroy();
|
static void destroy();
|
||||||
static auto get() -> Director*;
|
static auto get() -> Director*;
|
||||||
|
|
||||||
// Bucle principal del director. Crida des de main().
|
// Bucle principal clàssic (build natiu sense SDL_MAIN_USE_CALLBACKS).
|
||||||
|
// Internament crida setup() + bucle d'iterate() + teardown(). Crida des de main().
|
||||||
void run();
|
void run();
|
||||||
|
|
||||||
// Invocat pel game thread des de JD8_Flip(). Bloqueja fins que el director
|
// Punts d'entrada compatibles amb SDL_AppInit / SDL_AppIterate /
|
||||||
// consumeix el frame i dona via per produir el següent.
|
// SDL_AppEvent / SDL_AppQuit. Permeten que el Director siga driven
|
||||||
void publishFrame(Uint32* pixels);
|
// per l'event loop de SDL3 en lloc d'un bucle propi — imprescindible
|
||||||
|
// per al port a emscripten, on el runtime posseïx el main loop.
|
||||||
|
void setup();
|
||||||
|
bool iterate(); // torna false quan el joc vol eixir
|
||||||
|
void teardown();
|
||||||
|
void handleEvent(const SDL_Event& event);
|
||||||
|
|
||||||
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
|
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
|
||||||
void requestQuit();
|
void requestQuit();
|
||||||
|
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)
|
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
|
||||||
auto consumeKeyPressed() -> bool;
|
auto consumeKeyPressed() -> bool;
|
||||||
@@ -34,30 +47,44 @@ class Director {
|
|||||||
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
||||||
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
||||||
|
|
||||||
// Pausa: bloqueja el consum de frames del game thread + pausa la música
|
// Pausa: mentre està activa, iterate() no avança l'escena — es
|
||||||
|
// continua presentant el darrer frame amb overlay fresc.
|
||||||
void togglePause();
|
void togglePause();
|
||||||
auto isPaused() const -> bool { return paused_; }
|
auto isPaused() const -> bool { return paused_; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
~Director();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Director() = default;
|
Director() = default;
|
||||||
~Director() = default;
|
|
||||||
|
|
||||||
static Director* instance_;
|
static std::unique_ptr<Director> instance_;
|
||||||
|
|
||||||
void gameThreadFunc();
|
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
|
||||||
void handleEvents();
|
|
||||||
|
|
||||||
std::thread game_thread_;
|
// Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
|
||||||
std::mutex mutex_;
|
// Es crida una sola vegada des d'iterate() a la primera invocació.
|
||||||
std::condition_variable frame_produced_cv_;
|
void initGameContext();
|
||||||
std::condition_variable frame_consumed_cv_;
|
// Construeix l'escena apropiada segons game_state_ i info::ctx.
|
||||||
|
// Retorna nullptr si l'state actual no té escena registrada (bug).
|
||||||
|
std::unique_ptr<scenes::Scene> createNextScene();
|
||||||
|
|
||||||
Uint32* latest_frame_{nullptr};
|
// Buffers persistents entre iteracions. Abans eren locals a run(),
|
||||||
bool frame_ready_{false};
|
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
|
||||||
bool frame_consumed_{true};
|
// a reservar en cada crida del callback.
|
||||||
|
Uint32 game_frame_[320 * 200]{};
|
||||||
|
Uint32 presentation_buffer_[320 * 200]{};
|
||||||
|
bool has_frame_{false};
|
||||||
|
|
||||||
|
// Estat de l'escena actual. Abans vivia al stack del GameFiber; des
|
||||||
|
// de la Phase B.2 de la migració viu directament al Director.
|
||||||
|
std::unique_ptr<scenes::Scene> current_scene_;
|
||||||
|
int game_state_{1}; // 0 = gameplay (ModuleGame), 1 = via SceneRegistry, -1 = quit
|
||||||
|
Uint32 last_tick_ms_{0};
|
||||||
|
bool context_initialized_{false};
|
||||||
|
|
||||||
std::atomic<bool> quit_requested_{false};
|
std::atomic<bool> quit_requested_{false};
|
||||||
std::atomic<bool> game_thread_done_{false};
|
std::atomic<bool> restart_requested_{false};
|
||||||
std::atomic<bool> key_pressed_{false};
|
std::atomic<bool> key_pressed_{false};
|
||||||
std::atomic<bool> esc_blocked_{false};
|
std::atomic<bool> esc_blocked_{false};
|
||||||
std::atomic<bool> paused_{false};
|
std::atomic<bool> paused_{false};
|
||||||
|
|||||||
4
source/external/.clang-tidy
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# source/external/.clang-tidy
|
||||||
|
Checks: '-*'
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
HeaderFilterRegex: ''
|
||||||
@@ -1,66 +1,52 @@
|
|||||||
#include "game/bola.hpp"
|
#include "game/bola.hpp"
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
|
|
||||||
Bola::Bola(JD8_Surface gfx, Prota* sam)
|
Bola::Bola(JD8_Surface gfx, Prota* sam)
|
||||||
: Sprite(gfx) {
|
: Sprite(gfx) {
|
||||||
this->sam = sam;
|
this->sam = sam;
|
||||||
|
|
||||||
this->entitat = (Entitat*)malloc(sizeof(Entitat));
|
entitat.frames.reserve(2);
|
||||||
// Frames
|
entitat.frames.push_back({30, 155, 15, 15});
|
||||||
this->entitat->num_frames = 2;
|
entitat.frames.push_back({45, 155, 15, 15});
|
||||||
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame));
|
|
||||||
this->entitat->frames[0].w = 15;
|
entitat.animacions.resize(1);
|
||||||
this->entitat->frames[0].h = 15;
|
entitat.animacions[0].frames = {0, 1};
|
||||||
this->entitat->frames[0].x = 30;
|
|
||||||
this->entitat->frames[0].y = 155;
|
this->cur_frame = 0;
|
||||||
this->entitat->frames[1].w = 15;
|
this->o = 0;
|
||||||
this->entitat->frames[1].h = 15;
|
this->cycles_per_frame = 4;
|
||||||
this->entitat->frames[1].x = 45;
|
this->x = 20;
|
||||||
this->entitat->frames[1].y = 155;
|
this->y = 100;
|
||||||
|
this->contador = 0;
|
||||||
// Animacions
|
}
|
||||||
this->entitat->num_animacions = 1;
|
|
||||||
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
|
void Bola::draw() {
|
||||||
this->entitat->animacions[0].num_frames = 2;
|
if (this->contador == 0) Sprite::draw();
|
||||||
this->entitat->animacions[0].frames = (Uint8*)malloc(2);
|
}
|
||||||
this->entitat->animacions[0].frames[0] = 0;
|
|
||||||
this->entitat->animacions[0].frames[1] = 1;
|
void Bola::update() {
|
||||||
|
if (this->contador == 0) {
|
||||||
this->cur_frame = 0;
|
// Augmentem la x
|
||||||
this->o = 0;
|
this->x++;
|
||||||
this->cycles_per_frame = 4;
|
if (this->x == 280) this->contador = 200;
|
||||||
this->x = 20;
|
|
||||||
this->y = 100;
|
// Augmentem el frame
|
||||||
this->contador = 0;
|
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;
|
||||||
void Bola::draw() {
|
}
|
||||||
if (this->contador == 0) Sprite::draw();
|
|
||||||
}
|
// Comprovem si ha tocat a Sam
|
||||||
|
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)) {
|
||||||
void Bola::update() {
|
this->contador = 200;
|
||||||
if (this->contador == 0) {
|
info::ctx.vida--;
|
||||||
// Augmentem la x
|
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||||
this->x++;
|
}
|
||||||
if (this->x == 280) this->contador = 200;
|
} else {
|
||||||
|
this->contador--;
|
||||||
// Augmentem el frame
|
if (this->contador == 0) this->x = 20;
|
||||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
}
|
||||||
this->cur_frame++;
|
}
|
||||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprovem si ha tocat a Sam
|
|
||||||
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)) {
|
|
||||||
this->contador = 200;
|
|
||||||
info::vida--;
|
|
||||||
if (info::vida == 0) this->sam->o = 5;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->contador--;
|
|
||||||
if (this->contador == 0) this->x = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
class Bola : public Sprite {
|
class Bola : public Sprite {
|
||||||
public:
|
public:
|
||||||
Bola(JD8_Surface gfx, Prota* sam);
|
explicit Bola(JD8_Surface gfx, Prota* sam);
|
||||||
|
|
||||||
void draw();
|
void draw() override;
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -2,21 +2,7 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
// Tecles GUI (capa de presentació — finestra, zoom, shaders, etc.)
|
// Tecles GUI: viuen a data/input/keys.yaml (font única — KeyConfig).
|
||||||
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 de joc (moviment del personatge, accions)
|
// Tecles de joc (moviment del personatge, accions)
|
||||||
namespace Defaults::KeysGame {
|
namespace Defaults::KeysGame {
|
||||||
@@ -31,12 +17,11 @@ namespace Defaults::Video {
|
|||||||
constexpr bool GPU_ACCELERATION = true;
|
constexpr bool GPU_ACCELERATION = true;
|
||||||
constexpr bool SHADER_ENABLED = false;
|
constexpr bool SHADER_ENABLED = false;
|
||||||
constexpr bool SUPERSAMPLING = false;
|
constexpr bool SUPERSAMPLING = false;
|
||||||
constexpr bool INTEGER_SCALE = true;
|
|
||||||
constexpr bool VSYNC = true;
|
constexpr bool VSYNC = true;
|
||||||
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
|
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 int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
|
constexpr int INTERNAL_RESOLUTION = 1; // Multiplicador enter de la textura font abans del pipeline
|
||||||
constexpr bool LINEAR_UPSCALE = false;
|
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
|
||||||
} // namespace Defaults::Video
|
} // namespace Defaults::Video
|
||||||
|
|
||||||
namespace Defaults::Audio {
|
namespace Defaults::Audio {
|
||||||
@@ -57,6 +42,9 @@ namespace Defaults::Game {
|
|||||||
constexpr int HABITACIO_INICIAL = 1;
|
constexpr int HABITACIO_INICIAL = 1;
|
||||||
constexpr int PIRAMIDE_INICIAL = 255;
|
constexpr int PIRAMIDE_INICIAL = 255;
|
||||||
constexpr int VIDES = 5;
|
constexpr int VIDES = 5;
|
||||||
|
constexpr int DIAMANTS_INICIAL = 0;
|
||||||
|
constexpr int DINERS_INICIAL = 0;
|
||||||
constexpr bool USE_NEW_LOGO = true;
|
constexpr bool USE_NEW_LOGO = true;
|
||||||
constexpr bool SHOW_TITLE_CREDITS = true;
|
constexpr bool SHOW_TITLE_CREDITS = true;
|
||||||
|
constexpr bool SHOW_PRELOAD = false;
|
||||||
} // namespace Defaults::Game
|
} // namespace Defaults::Game
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Textos
|
// Textos
|
||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2000 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2000 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* VERSION = "1.11";
|
constexpr const char* VERSION = "1.2";
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
// Resolución del juego
|
// Resolución del juego
|
||||||
|
|||||||
@@ -1,61 +1,44 @@
|
|||||||
#include "game/engendro.hpp"
|
#include "game/engendro.hpp"
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
|
|
||||||
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
||||||
: Sprite(gfx) {
|
: Sprite(gfx) {
|
||||||
this->entitat = (Entitat*)malloc(sizeof(Entitat));
|
entitat.frames.reserve(4);
|
||||||
// Frames
|
for (int py = 50; py <= 65; py += 15) {
|
||||||
this->entitat->num_frames = 4;
|
for (int px = 225; px <= 240; px += 15) {
|
||||||
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame));
|
Frame f;
|
||||||
|
f.w = 15;
|
||||||
Uint8 frame = 0;
|
f.h = 15;
|
||||||
for (int y = 50; y <= 65; y += 15) {
|
f.x = px;
|
||||||
for (int x = 225; x <= 240; x += 15) {
|
f.y = py;
|
||||||
this->entitat->frames[frame].w = 15;
|
entitat.frames.push_back(f);
|
||||||
this->entitat->frames[frame].h = 15;
|
}
|
||||||
this->entitat->frames[frame].x = x;
|
}
|
||||||
this->entitat->frames[frame].y = y;
|
|
||||||
frame++;
|
entitat.animacions.resize(1);
|
||||||
}
|
entitat.animacions[0].frames = {0, 1, 2, 3, 2, 1};
|
||||||
}
|
|
||||||
|
this->cur_frame = 0;
|
||||||
// Animacions
|
this->vida = 18;
|
||||||
this->entitat->num_animacions = 1;
|
this->x = x;
|
||||||
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
|
this->y = y;
|
||||||
this->entitat->animacions[0].num_frames = 6;
|
this->o = 0;
|
||||||
this->entitat->animacions[0].frames = (Uint8*)malloc(6);
|
this->cycles_per_frame = 30;
|
||||||
this->entitat->animacions[0].frames[0] = 0;
|
}
|
||||||
this->entitat->animacions[0].frames[1] = 1;
|
|
||||||
this->entitat->animacions[0].frames[2] = 2;
|
bool Engendro::update() {
|
||||||
this->entitat->animacions[0].frames[3] = 3;
|
bool mort = false;
|
||||||
this->entitat->animacions[0].frames[4] = 2;
|
|
||||||
this->entitat->animacions[0].frames[5] = 1;
|
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||||
|
this->cur_frame++;
|
||||||
this->cur_frame = 0;
|
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||||
this->vida = 18;
|
this->vida--;
|
||||||
this->x = x;
|
}
|
||||||
this->y = y;
|
|
||||||
this->o = 0;
|
if (vida == 0) mort = true;
|
||||||
this->cycles_per_frame = 30;
|
|
||||||
}
|
return mort;
|
||||||
|
}
|
||||||
void Engendro::draw() {
|
|
||||||
Sprite::draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Engendro::update() {
|
|
||||||
bool mort = false;
|
|
||||||
|
|
||||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
|
||||||
this->cur_frame++;
|
|
||||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0;
|
|
||||||
this->vida--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vida == 0) mort = true;
|
|
||||||
|
|
||||||
return mort;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
|
|
||||||
class Engendro : public Sprite {
|
class Engendro : public Sprite {
|
||||||
public:
|
public:
|
||||||
Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
|
explicit Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
|
||||||
|
|
||||||
void draw();
|
|
||||||
bool update();
|
bool update();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
#include "game/info.hpp"
|
#include "game/info.hpp"
|
||||||
|
|
||||||
namespace info {
|
// La instància `info::ctx` està definida com a `inline` al header;
|
||||||
int num_piramide;
|
// aquest fitxer es manté per a si cal afegir lògica addicional més endavant.
|
||||||
int num_habitacio;
|
|
||||||
int diners;
|
|
||||||
int diamants;
|
|
||||||
int vida;
|
|
||||||
int momies;
|
|
||||||
int engendros;
|
|
||||||
bool nou_personatge;
|
|
||||||
bool pepe_activat;
|
|
||||||
}; // namespace info
|
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace info {
|
namespace info {
|
||||||
extern int num_piramide;
|
|
||||||
extern int num_habitacio;
|
struct GameContext {
|
||||||
extern int diners;
|
int num_piramide = 0;
|
||||||
extern int diamants;
|
int num_habitacio = 0;
|
||||||
extern int vida;
|
int diners = 0;
|
||||||
extern int momies;
|
int diamants = 0;
|
||||||
extern int engendros;
|
int vida = 0;
|
||||||
extern bool nou_personatge;
|
int momies = 0;
|
||||||
extern bool pepe_activat;
|
int engendros = 0;
|
||||||
}; // namespace info
|
bool nou_personatge = false;
|
||||||
|
bool pepe_activat = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
} // namespace info
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Mapa::~Mapa(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Mapa::draw() {
|
void Mapa::draw() {
|
||||||
if (info::num_piramide != 4) {
|
if (info::ctx.num_piramide != 4) {
|
||||||
switch (sam->o) {
|
switch (sam->o) {
|
||||||
case 0: // Down
|
case 0: // Down
|
||||||
JD8_BlitCKToSurface(sam->x, sam->y, this->gfx, 15, 125 + sam->frame_pejades, 15, 1, this->fondo, 255);
|
JD8_BlitCKToSurface(sam->x, sam->y, this->gfx, 15, 125 + sam->frame_pejades, 15, 1, this->fondo, 255);
|
||||||
@@ -88,7 +88,7 @@ bool Mapa::novaMomia() {
|
|||||||
void Mapa::preparaFondoEstatic() {
|
void Mapa::preparaFondoEstatic() {
|
||||||
// Prepara el fondo est<73>tic de l'habitaci<63>
|
// Prepara el fondo est<73>tic de l'habitaci<63>
|
||||||
this->fondo = JD8_NewSurface();
|
this->fondo = JD8_NewSurface();
|
||||||
if (info::num_piramide == 6) {
|
if (info::ctx.num_piramide == 6) {
|
||||||
JD8_BlitToSurface(9, 2, this->gfx, 227, 185, 92, 7, this->fondo); // Text "SECRETA"
|
JD8_BlitToSurface(9, 2, this->gfx, 227, 185, 92, 7, this->fondo); // Text "SECRETA"
|
||||||
} else {
|
} else {
|
||||||
JD8_BlitToSurface(9, 2, this->gfx, 60, 185, 39, 7, this->fondo); // Text "NIVELL"
|
JD8_BlitToSurface(9, 2, this->gfx, 60, 185, 39, 7, this->fondo); // Text "NIVELL"
|
||||||
@@ -96,12 +96,12 @@ void Mapa::preparaFondoEstatic() {
|
|||||||
}
|
}
|
||||||
JD8_BlitToSurface(130, 2, this->gfx, 225, 192, 19, 8, this->fondo); // Montonet de monedes + signe '='
|
JD8_BlitToSurface(130, 2, this->gfx, 225, 192, 19, 8, this->fondo); // Montonet de monedes + signe '='
|
||||||
JD8_BlitToSurface(220, 2, this->gfx, 160, 185, 48, 7, this->fondo); // Text "ENERGIA"
|
JD8_BlitToSurface(220, 2, this->gfx, 160, 185, 48, 7, this->fondo); // Text "ENERGIA"
|
||||||
if (info::diners >= 200) JD8_BlitToSurface(175, 3, this->gfx, 60, 193, 7, 6, this->fondo);
|
if (info::ctx.diners >= 200) JD8_BlitToSurface(175, 3, this->gfx, 60, 193, 7, 6, this->fondo);
|
||||||
|
|
||||||
// Pinta taulells
|
// Pinta taulells
|
||||||
for (int y = 0; y < 11; y++) {
|
for (int y = 0; y < 11; y++) {
|
||||||
for (int x = 0; x < 19; x++) {
|
for (int x = 0; x < 19; x++) {
|
||||||
switch (info::num_piramide) {
|
switch (info::ctx.num_piramide) {
|
||||||
case 1:
|
case 1:
|
||||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo);
|
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo);
|
||||||
break;
|
break;
|
||||||
@@ -145,7 +145,7 @@ void Mapa::preparaFondoEstatic() {
|
|||||||
// Pinta la porta
|
// Pinta la porta
|
||||||
JD8_BlitCKToSurface(150, 18, this->gfx, 0, 143, 15, 12, this->fondo, 255);
|
JD8_BlitCKToSurface(150, 18, this->gfx, 0, 143, 15, 12, this->fondo, 255);
|
||||||
|
|
||||||
if (info::num_piramide == 2) {
|
if (info::ctx.num_piramide == 2) {
|
||||||
JD8_BlitToSurface(5, 100, this->gfx, 30, 140, 15, 15, this->fondo);
|
JD8_BlitToSurface(5, 100, this->gfx, 30, 140, 15, 15, this->fondo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,9 +157,9 @@ void swap(Uint8& a, Uint8& b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Mapa::preparaTombes() {
|
void Mapa::preparaTombes() {
|
||||||
const Uint8 contingut = info::num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
|
const Uint8 contingut = info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
|
||||||
int cx = info::num_piramide == 6 ? 270 : 0;
|
int cx = info::ctx.num_piramide == 6 ? 270 : 0;
|
||||||
int cy = info::num_piramide == 6 ? 50 : 0;
|
int cy = info::ctx.num_piramide == 6 ? 50 : 0;
|
||||||
|
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
this->tombes[i].contingut = contingut;
|
this->tombes[i].contingut = contingut;
|
||||||
@@ -171,7 +171,7 @@ void Mapa::preparaTombes() {
|
|||||||
this->tombes[i].x = cx;
|
this->tombes[i].x = cx;
|
||||||
this->tombes[i].y = cy;
|
this->tombes[i].y = cy;
|
||||||
}
|
}
|
||||||
if (info::num_piramide == 6) return;
|
if (info::ctx.num_piramide == 6) return;
|
||||||
this->tombes[0].contingut = CONTE_FARAO;
|
this->tombes[0].contingut = CONTE_FARAO;
|
||||||
this->tombes[1].contingut = CONTE_CLAU;
|
this->tombes[1].contingut = CONTE_CLAU;
|
||||||
this->tombes[2].contingut = CONTE_PERGAMI;
|
this->tombes[2].contingut = CONTE_PERGAMI;
|
||||||
@@ -241,7 +241,7 @@ void Mapa::comprovaCaixa(Uint8 num) {
|
|||||||
break;
|
break;
|
||||||
case CONTE_TRESOR:
|
case CONTE_TRESOR:
|
||||||
this->tombes[num].x = 100;
|
this->tombes[num].x = 100;
|
||||||
info::diners++;
|
info::ctx.diners++;
|
||||||
break;
|
break;
|
||||||
case CONTE_FARAO:
|
case CONTE_FARAO:
|
||||||
this->tombes[num].x = 150;
|
this->tombes[num].x = 150;
|
||||||
@@ -261,9 +261,9 @@ void Mapa::comprovaCaixa(Uint8 num) {
|
|||||||
break;
|
break;
|
||||||
case CONTE_DIAMANT:
|
case CONTE_DIAMANT:
|
||||||
this->tombes[num].y = 70;
|
this->tombes[num].y = 70;
|
||||||
info::diamants++;
|
info::ctx.diamants++;
|
||||||
info::diners += VALOR_DIAMANT;
|
info::ctx.diners += VALOR_DIAMANT;
|
||||||
if (info::diamants == 16) this->farao = this->clau = true;
|
if (info::ctx.diamants == 16) this->farao = this->clau = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ struct Vertex {
|
|||||||
|
|
||||||
class Mapa {
|
class Mapa {
|
||||||
public:
|
public:
|
||||||
Mapa(JD8_Surface gfx, Prota* sam);
|
explicit Mapa(JD8_Surface gfx, Prota* sam);
|
||||||
~Mapa(void);
|
~Mapa(void);
|
||||||
|
|
||||||
|
Mapa(const Mapa&) = delete;
|
||||||
|
Mapa& operator=(const Mapa&) = delete;
|
||||||
|
Mapa(Mapa&&) = delete;
|
||||||
|
Mapa& operator=(Mapa&&) = delete;
|
||||||
|
|
||||||
void draw();
|
void draw();
|
||||||
void update();
|
void update();
|
||||||
bool novaMomia();
|
bool novaMomia();
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ Marcador::~Marcador(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Marcador::draw() {
|
void Marcador::draw() {
|
||||||
if (info::num_piramide < 6) {
|
if (info::ctx.num_piramide < 6) {
|
||||||
this->pintaNumero(55, 2, info::num_piramide);
|
this->pintaNumero(55, 2, info::ctx.num_piramide);
|
||||||
this->pintaNumero(80, 2, info::num_habitacio);
|
this->pintaNumero(80, 2, info::ctx.num_habitacio);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->pintaNumero(149, 2, info::diners / 100);
|
this->pintaNumero(149, 2, info::ctx.diners / 100);
|
||||||
this->pintaNumero(156, 2, (info::diners % 100) / 10);
|
this->pintaNumero(156, 2, (info::ctx.diners % 100) / 10);
|
||||||
this->pintaNumero(163, 2, info::diners % 10);
|
this->pintaNumero(163, 2, info::ctx.diners % 10);
|
||||||
|
|
||||||
if (this->sam->pergami) JD8_BlitCK(190, 1, this->gfx, 209, 185, 15, 14, 255);
|
if (this->sam->pergami) JD8_BlitCK(190, 1, this->gfx, 209, 185, 15, 14, 255);
|
||||||
|
|
||||||
JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::vida * 3, 255);
|
JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::ctx.vida * 3, 255);
|
||||||
if (info::vida < 5) JD8_BlitCK(271, 1 + (info::vida * 3), this->gfx, 75, 20, 15, 15 - (info::vida * 3), 255);
|
if (info::ctx.vida < 5) JD8_BlitCK(271, 1 + (info::ctx.vida * 3), this->gfx, 75, 20, 15, 15 - (info::ctx.vida * 3), 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) {
|
void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
class Marcador {
|
class Marcador {
|
||||||
public:
|
public:
|
||||||
Marcador(JD8_Surface gfx, Prota* sam);
|
explicit Marcador(JD8_Surface gfx, Prota* sam);
|
||||||
~Marcador(void);
|
~Marcador(void);
|
||||||
|
|
||||||
void draw();
|
void draw();
|
||||||
|
|||||||
@@ -1,165 +1,173 @@
|
|||||||
#include "game/modulegame.hpp"
|
#include "game/modulegame.hpp"
|
||||||
|
|
||||||
#include "core/jail/jail_audio.hpp"
|
#include <algorithm>
|
||||||
#include "core/jail/jdraw8.hpp"
|
|
||||||
#include "core/jail/jfile.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/jail/jgame.hpp"
|
#include "core/jail/jdraw8.hpp"
|
||||||
#include "core/jail/jinput.hpp"
|
#include "core/jail/jgame.hpp"
|
||||||
|
#include "core/jail/jinput.hpp"
|
||||||
ModuleGame::ModuleGame() {
|
|
||||||
this->gfx = JD8_LoadSurface(info::pepe_activat ? "frames2.gif" : "frames.gif");
|
ModuleGame::ModuleGame() {
|
||||||
JG_SetUpdateTicks(10);
|
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->sam = std::make_unique<Prota>(this->gfx);
|
||||||
this->marcador = new Marcador(this->gfx, this->sam);
|
this->mapa = std::make_unique<Mapa>(this->gfx, this->sam.get());
|
||||||
if (info::num_piramide == 2) {
|
this->marcador = std::make_unique<Marcador>(this->gfx, this->sam.get());
|
||||||
this->bola = new Bola(this->gfx, this->sam);
|
if (info::ctx.num_piramide == 2) {
|
||||||
} else {
|
this->bola = std::make_unique<Bola>(this->gfx, this->sam.get());
|
||||||
this->bola = NULL;
|
}
|
||||||
}
|
|
||||||
this->momies = NULL;
|
this->iniciarMomies();
|
||||||
|
}
|
||||||
this->final = 0;
|
|
||||||
this->iniciarMomies();
|
ModuleGame::~ModuleGame() {
|
||||||
}
|
JD8_FreeSurface(this->gfx);
|
||||||
|
}
|
||||||
ModuleGame::~ModuleGame(void) {
|
|
||||||
JD8_FadeOut();
|
void ModuleGame::onEnter() {
|
||||||
|
// Primera Draw per omplir `screen` amb el contingut del gameplay
|
||||||
if (this->bola != NULL) delete this->bola;
|
// abans que el fade-in arranque. Si no, les primeres iteracions del
|
||||||
if (this->momies != NULL) {
|
// fade interpolarien cap a una paleta amb pantalla buida.
|
||||||
this->momies->clear();
|
this->Draw();
|
||||||
delete this->momies;
|
|
||||||
}
|
// Audio::playMusic ja és idempotent: si la pista actual coincideix amb la
|
||||||
delete this->marcador;
|
// demanada, no fa res. Per això podem cridar-lo cada onEnter sense
|
||||||
delete this->mapa;
|
// desencadenar restarts indesitjats.
|
||||||
delete this->sam;
|
const char* music_name = info::ctx.num_piramide == 3 ? "piramide_3.ogg"
|
||||||
|
: info::ctx.num_piramide == 2 ? "piramide_2.ogg"
|
||||||
JD8_FreeSurface(this->gfx);
|
: info::ctx.num_piramide == 6 ? "secreta.ogg"
|
||||||
}
|
: "piramide_1_4_5.ogg";
|
||||||
|
Audio::get()->playMusic(music_name);
|
||||||
int ModuleGame::Go() {
|
|
||||||
this->Draw();
|
// Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de
|
||||||
|
// 32) per cada tick; durant aquesta fase el gameplay no corre,
|
||||||
const char* music = info::num_piramide == 3 ? "00000008.ogg" : (info::num_piramide == 2 ? "00000007.ogg" : (info::num_piramide == 6 ? "00000002.ogg" : "00000006.ogg"));
|
// només Draw+fade. Substituïx la crida bloquejant `JD8_FadeToPal`.
|
||||||
const char* current_music = JA_GetMusicFilename();
|
fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
|
||||||
if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !(strcmp(music, current_music) == 0)) {
|
phase_ = Phase::FadingIn;
|
||||||
int size;
|
}
|
||||||
char* buffer = file_getfilebuffer(music, size);
|
|
||||||
JA_PlayMusic(JA_LoadMusic((Uint8*)buffer, size, music));
|
void ModuleGame::tick(int delta_ms) {
|
||||||
}
|
switch (phase_) {
|
||||||
|
case Phase::FadingIn:
|
||||||
JD8_FadeToPal(JD8_LoadPalette(info::pepe_activat ? "frames2.gif" : "frames.gif"));
|
// No redibuixem durant el fade: el `screen` ja va ser omplit
|
||||||
|
// per la Draw() d'onEnter. Només el JD8_Flip del caller muta
|
||||||
while (this->final == 0 && !JG_Quitting()) {
|
// pixel_data segons la paleta que avança pas a pas.
|
||||||
this->Draw();
|
fade_.tick(delta_ms);
|
||||||
this->Update();
|
if (fade_.done()) phase_ = Phase::Playing;
|
||||||
}
|
break;
|
||||||
|
|
||||||
// JS_FadeOutMusic();
|
case Phase::Playing:
|
||||||
|
this->Draw();
|
||||||
if (this->final == 1) {
|
this->Update();
|
||||||
info::num_habitacio++;
|
if (this->final_ != 0) {
|
||||||
if (info::num_habitacio == 6) {
|
this->applyFinalTransitions();
|
||||||
info::num_habitacio = 1;
|
fade_.startFadeOut();
|
||||||
info::num_piramide++;
|
phase_ = Phase::FadingOut;
|
||||||
}
|
}
|
||||||
if (info::num_piramide == 6 && info::num_habitacio == 2) info::num_piramide++;
|
break;
|
||||||
} else if (this->final == 2) {
|
|
||||||
info::num_piramide = 100;
|
case Phase::FadingOut:
|
||||||
}
|
// No redibuixem: el `screen` té l'últim frame pintat per la
|
||||||
|
// fase Playing (just abans que Update() setegés `final_`).
|
||||||
if (JG_Quitting()) {
|
// El vell `JD8_FadeOut` feia exactament això — flips amb
|
||||||
return -1;
|
// paleta fading però sense tocar el buffer. Redibuixar ací
|
||||||
} else {
|
// mostraria l'estat post-Update del sprite (p.ex. el prota
|
||||||
if (info::num_habitacio == 1 || info::num_piramide == 100 || info::num_piramide == 7) {
|
// "tornant" davant la porta després d'haver eixit).
|
||||||
return 1;
|
fade_.tick(delta_ms);
|
||||||
} else {
|
if (fade_.done()) phase_ = Phase::Done;
|
||||||
return 0;
|
break;
|
||||||
}
|
|
||||||
}
|
case Phase::Done:
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
void ModuleGame::Draw() {
|
}
|
||||||
this->mapa->draw();
|
|
||||||
this->marcador->draw();
|
int ModuleGame::nextState() const {
|
||||||
this->sam->draw();
|
if (JG_Quitting()) return -1;
|
||||||
if (this->momies != NULL) this->momies->draw();
|
if (info::ctx.num_habitacio == 1 ||
|
||||||
if (this->bola != NULL) this->bola->draw();
|
info::ctx.num_piramide == 100 ||
|
||||||
|
info::ctx.num_piramide == 7) {
|
||||||
JD8_Flip();
|
return 1;
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
void ModuleGame::Update() {
|
}
|
||||||
if (JG_ShouldUpdate()) {
|
|
||||||
JI_Update();
|
void ModuleGame::applyFinalTransitions() {
|
||||||
|
if (this->final_ == 1) {
|
||||||
this->final = this->sam->update();
|
info::ctx.num_habitacio++;
|
||||||
if (this->momies != NULL && this->momies->update()) {
|
if (info::ctx.num_habitacio == 6) {
|
||||||
Momia* seguent = this->momies->next;
|
info::ctx.num_habitacio = 1;
|
||||||
delete this->momies;
|
info::ctx.num_piramide++;
|
||||||
this->momies = seguent;
|
}
|
||||||
info::momies--;
|
if (info::ctx.num_piramide == 6 && info::ctx.num_habitacio == 2) info::ctx.num_piramide++;
|
||||||
}
|
} else if (this->final_ == 2) {
|
||||||
if (this->bola != NULL) this->bola->update();
|
info::ctx.num_piramide = 100;
|
||||||
this->mapa->update();
|
}
|
||||||
if (this->mapa->novaMomia()) {
|
}
|
||||||
if (this->momies != NULL) {
|
|
||||||
this->momies->insertar(new Momia(this->gfx, true, 0, 0, this->sam));
|
void ModuleGame::Draw() {
|
||||||
info::momies++;
|
// No crida JD8_Flip — el caller (mini-loop del fiber, o Director a
|
||||||
} else {
|
// Phase B.2) ho fa després de cada tick.
|
||||||
this->momies = new Momia(this->gfx, true, 0, 0, this->sam);
|
this->mapa->draw();
|
||||||
info::momies++;
|
this->marcador->draw();
|
||||||
}
|
this->sam->draw();
|
||||||
}
|
for (auto& m : this->momies) m->draw();
|
||||||
|
if (this->bola) this->bola->draw();
|
||||||
if (JI_CheatActivated("reviu")) info::vida = 5;
|
}
|
||||||
if (JI_CheatActivated("alone")) {
|
|
||||||
if (this->momies != NULL) {
|
void ModuleGame::Update() {
|
||||||
this->momies->clear();
|
if (JG_ShouldUpdate()) {
|
||||||
delete this->momies;
|
JI_Update();
|
||||||
this->momies = NULL;
|
|
||||||
info::momies = 0;
|
this->final_ = this->sam->update();
|
||||||
}
|
const auto erased = std::erase_if(this->momies, [](auto& m) { return m->update(); });
|
||||||
}
|
info::ctx.momies -= static_cast<int>(erased);
|
||||||
if (JI_CheatActivated("obert")) {
|
if (this->bola) this->bola->update();
|
||||||
for (int i = 0; i < 16; i++) {
|
this->mapa->update();
|
||||||
this->mapa->tombes[i].costat[0] = true;
|
if (this->mapa->novaMomia()) {
|
||||||
this->mapa->tombes[i].costat[1] = true;
|
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, true, 0, 0, this->sam.get()));
|
||||||
this->mapa->tombes[i].costat[2] = true;
|
info::ctx.momies++;
|
||||||
this->mapa->tombes[i].costat[3] = true;
|
}
|
||||||
this->mapa->comprovaCaixa(i);
|
|
||||||
}
|
if (JI_CheatActivated("reviu")) info::ctx.vida = 5;
|
||||||
}
|
if (JI_CheatActivated("alone")) {
|
||||||
|
this->momies.clear();
|
||||||
if (JI_KeyPressed(SDL_SCANCODE_ESCAPE)) {
|
info::ctx.momies = 0;
|
||||||
JG_QuitSignal();
|
}
|
||||||
}
|
if (JI_CheatActivated("obert")) {
|
||||||
}
|
for (int i = 0; i < 16; i++) {
|
||||||
}
|
this->mapa->tombes[i].costat[0] = true;
|
||||||
|
this->mapa->tombes[i].costat[1] = true;
|
||||||
void ModuleGame::iniciarMomies() {
|
this->mapa->tombes[i].costat[2] = true;
|
||||||
if (info::num_habitacio == 1) {
|
this->mapa->tombes[i].costat[3] = true;
|
||||||
info::momies = 1;
|
this->mapa->comprovaCaixa(i);
|
||||||
} else {
|
}
|
||||||
info::momies++;
|
}
|
||||||
}
|
|
||||||
if (info::num_piramide == 6) info::momies = 8;
|
if (JI_KeyPressed(SDL_SCANCODE_ESCAPE)) {
|
||||||
|
JG_QuitSignal();
|
||||||
int x = 20;
|
}
|
||||||
int y = 170;
|
}
|
||||||
bool dimonis = info::num_piramide == 6;
|
}
|
||||||
for (int i = 0; i < info::momies; i++) {
|
|
||||||
if (this->momies == NULL) {
|
void ModuleGame::iniciarMomies() {
|
||||||
this->momies = new Momia(this->gfx, dimonis, x, y, this->sam);
|
if (info::ctx.num_habitacio == 1) {
|
||||||
} else {
|
info::ctx.momies = 1;
|
||||||
this->momies->insertar(new Momia(this->gfx, dimonis, x, y, this->sam));
|
} else {
|
||||||
}
|
info::ctx.momies++;
|
||||||
x += 65;
|
}
|
||||||
if (x == 345) {
|
if (info::ctx.num_piramide == 6) info::ctx.momies = 8;
|
||||||
x = 20;
|
|
||||||
y -= 35;
|
int x = 20;
|
||||||
}
|
int y = 170;
|
||||||
}
|
bool dimonis = info::ctx.num_piramide == 6;
|
||||||
}
|
for (int i = 0; i < info::ctx.momies; i++) {
|
||||||
|
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, dimonis, x, y, this->sam.get()));
|
||||||
|
x += 65;
|
||||||
|
if (x == 345) {
|
||||||
|
x = 20;
|
||||||
|
y -= 35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||