`data` passa a ser una llista de files d'enters (tile index lineal),
sense la indirecció `legend:` + símbols. Desapareix el límit implícit
de ~60 tiles distints i el grid segueix llegint-se com a matriu visual
gràcies al padding de 3 dígits. `-1` marca cel·la buida.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Nou format de pack AE26 v1 (XOR + djb2), eina `pack_resources`,
regeneració automàtica de `build/resources.pack` a cada build.
- `Resource::Helper` amb init/shutdown, fallback a disc només en Debug
(apunta al source tree); Release exigeix pack present.
- Callers de YAML migrats a `loadFile`+deserialize des de buffer.
- Arrel neta: el pack viu a `build/`, eliminat `sync_data`.
- Placeholder font -> `ari-w9500-condensed-display`; esborrades variants
bold/condensed no usades a `data/fonts/`.
La mòmia ara decideix direcció a cada creuament de corredor (edge-detect
de travessar la línia central d'un corredor perpendicular) i no només
quan topa amb un mur o sarc. A la detecció, amarra la caixa al centre
exacte del creuament per evitar drift sub-píxel i tria entre continuar
recta o girar a una perpendicular lliure. Mai mitja volta a un creuament
— la mitja volta queda reservada al cas de bloqueig sense alternatives.
Fix: gif.cpp no desfeia l'interlace, trinxava qualsevol GIF entrellaçat
(tota la sortida de Pillow). Afegeix Defaults::UI::Font per centralitzar
noms/rutes i mou data/font → data/fonts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Abans:
make / make build -> Debug
make release -> Release (sense empaquetar)
Ara, mateixa convenció que els dos projectes germans:
make -> Release (binari d'ús normal)
make debug -> Debug (flux amb debug.yaml)
make release -> Release + empaquetat distribució (empaquetat TODO)
Afegit make run / make run-debug per conveniència i make help.
docs/convencions.md actualitzat amb la nova taula target -> comportament.
Abans el binari només arrencava si la CWD era la que contenia data/
(típicament build/). Executat des de qualsevol altra carpeta no trobava
keys.yaml, paletes, etc. i es moria al carregar el primer recurs.
Patró seguit de ../aee: Resource::Helper guarda un data root absolut
i resolve(rel) el prefixa davant de rutes no absolutes. Director fa
setDataRoot(SDL_GetBasePath()) com a primera acció del ctor, així tots
els loaders posteriors hi passen sense canvis al call site.
Migrats a Resource::Helper::resolve els loaders que obrien std::ifstream
directament amb el path rebut: Locale, KeyConfig, Player, Map, Tilemap
i EntityLoader. Els que ja usaven Resource::Helper::loadFile
(Resource::Cache, PaletteLoader) hereten el fix automàticament.
Els fitxers de system_folder_ (config.yaml, debug.yaml, postfx.yaml,
crtpi.yaml) es passen com a rutes absolutes i resolve() les deixa
intactes, així no es veuen afectats.
Test targets (test_entities_smoke, test_scene_flow) enllacen ara
resource_helper.cpp perquè el seu codi passa per Helper::resolve. Els
tests corren amb CWD = repo root, on les rutes "data/..." funcionen
amb data root buit sense canviar cap assert.
Verificat executant el binari des de build/, /tmp i / — tots tres
troben els recursos i arrenquen correctament, tant en debug com en
release (release sempre arranca a LOGO).
Documenta dues peces que no estaven enlloc:
- Missatges de commit: valencià, frases curtes, sense firma automàtica
(cap Co-Authored-By ni signatura de Claude Code).
- debug.yaml: ruta dins la carpeta de sistema, format, valors vàlids,
i la diferència de comportament entre build release (sempre LOGO,
ignora el fitxer) i build debug (llig el fitxer i el crea amb GAME
si no existeix).
Entrada nova a l'índex de docs/README.md.
Cobreix les dues peces pures del flux:
- parseStartScene / resolveStartScene: tots els valors vàlids, fallback
a GAME per valor invàlid, YAML corrupte o fitxer inexistent.
- createDefaultIfMissing: crea el fitxer amb start_scene=GAME si no
existeix i és idempotent quan ja hi és.
- isReservedScancode: F1/F12/Escape reservats, SPACE/fletxes no, i
UNKNOWN sempre fals.
La lògica de update/render dels placeholders no es testeja perquè
arrossegaria Screen/Notifier/RenderInfo (tot l'stack SDL). Aquesta
integració queda coberta pel smoke xvfb-run del binari.
Stand-alone target `test_scene_flow` que només enllaça els .cpp pures
necessaris, amb ctest afegit.
Director encadena LOGO → TITLE → INTRO → GAME amb factories injectades
al ctor de cada placeholder (sense que cap escena conegui el tipus de
la següent). L'escena d'arrencada surt de `debug.yaml` en debug build i
és sempre LOGO en release.
Comportament:
- Release: `debug.yaml` s'ignora; arranca sempre a LOGO.
- Debug: es llig `debug.yaml`. Si no existeix, es crea automàticament
amb `start_scene: GAME` per no arrossegar 15 s de placeholders al dia
a dia de gameplay. Valor invàlid o YAML corrupte → warning + GAME,
però el fitxer no es reescriu (respectem l'edició manual).
`debug.yaml` viu a `$XDG_CONFIG_HOME/jailgames/aee_2026/`, al costat
de `config.yaml`, i queda fora del repo (ajust per màquina de dev).
Tres escenes de 5 s cadascuna, activables al flux que precedeix GameScene.
Classes separades (sense base comuna) perquè divergiran quan es
desenvolupi contingut real.
Avancen per timeout o per qualsevol tecla no reservada. Les tecles
reservades (F1-F12, Escape) són les declarades a data/input/keys.yaml i
mantenen el seu comportament habitual via GlobalInputs. La
classificació viu a Placeholder::isReservedScancode, i KeyConfig exposa
bindings() per enumerar-les sense duplicar la llista.
Encara no integrats al Director: això es fa al següent commit quan
s'afegeix el selector d'escena inicial.
Taula mínima per distingir d'un cop d'ull les auditories arxivades
(audit-core-*, audit-game) dels documents de disseny vius
(entities-system, player-animations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tanca els 4 ítems DEFERRED de l'auditoria audit/game (COUP-02, COUP-03,
CONV-02 part Options, PATCH-01). Veure docs/audit-game.md → "Registro
de commits — refactor/options-class" per al desglossament dels commits
i les decisions preses durant el refactor.
- Registre de commits omplert amb els hashes reals dels 3 commits de la
rama (a3b03b8 / 64e0178 / d9f6fb4).
- player-animations.md: el recompte de smoke tests deia "14/14" però
`tests/test_entities_smoke.cpp` només invoca 13 funcions `test*` des
de `main()`. Nota corregida a "13/13" amb explicació.
Tanca els 4 ítems DEFERRED de `audit/game` (COUP-02, COUP-03,
CONV-02 part Options, PATCH-01).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Sprite: nou hook virtual `collisionBox() -> SDL_FRect` (default
`pos_`). Subclasses amb falsa perspectiva el sobreescriuen amb una
sub-caixa més petita.
- MovingSprite: nou `tryMove(dx, dy, CollisionFn)` que aplica eix a
eix i reverteix l'eix que col·lisiona; deixa lliscar el sprite
contra obstacles.
- Player: override `collisionBox()` retornant `getBaseBox()` i
`update()` reescrita per fer `animate(dt) + tryMove(vx*dt, vy*dt,
collidesFn)` en lloc del patró antic `update + revertir + reaplicar`.
El comportament observable és idèntic: la integració `v*dt` parteix
de la mateixa posició (post-snap), els revertits per col·lisió són
els mateixos i el `MOVED` que controla l'animació stand/walk segueix
comparant amb `FRAME_START_X/Y`.
Smoke tests 13/13 OK; log Game start → Bye! sota xvfb-run idèntic al
baseline. clang-tidy i cppcheck nets sobre els fitxers modificats.
Refs: docs/audit-game.md (PATCH-01).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Afegit `Map::Info` (POD trivialment copiable) que empaqueta playfield,
pitch, grid, tile_size, tilemap_origin + 2 punters a funció (collides,
inside_playfield). `Map::makeInfo()` el construeix a partir dels globals
del namespace.
`Player` ara rep `const Map::Info&` al ctor i en guarda una còpia plana
(`map_`), substituint totes les lectures de `Map::*` per `map_.*`. La
inclusió `#include "game/map/map.hpp"` puja al header per la dependència
de `Map::Info`; player.cpp deixa d'incloure'l (transitiu).
Només el Player es desacobla — momies, roques i `loadFromLevel`
continuen llegint `Map::*` directament, com diu la nota original del
audit (mentre hi hagi un Map global actiu, l'acoblament és acceptable).
Si quan s'acabi PATCH-01 algun consumidor extra chirría, COUP-02b en
una rama nova.
Smoke tests 13/13 OK; log Game start → Bye! sota xvfb-run idèntic al
baseline. clang-tidy i cppcheck nets sobre els fitxers modificats.
Refs: docs/audit-game.md (COUP-02).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'estat mutable d'Options (window, video, audio, locale, presets, paths)
es mou a un anonymous namespace dins options.cpp. L'API pública del
namespace passa a ser:
- Getters const-ref: window(), video(), audio(), locale(),
configVersion(), postfxPresets(), crtpiPresets().
- Setters específics que muten i persisteixen via saveToFile():
setWindowZoom, setWindowFullscreen, setVideoVSync,
setVideoScalingMode, setVideoRenderInfoPosition,
setVideoRenderInfoShowTime, setShaderEnabled, setActiveShaderName,
setPostFXPreset, setCrtPiPreset.
Els setters NO notifiquen els subsistemes: el caller (global_inputs.cpp)
crida explícitament Screen::setWindowZoom, reapplyShader, etc. La part
"observers" del audit es deixa WONTFIX justificat — notificar des
d'Options invertiria la direcció Director→Screen/Audio i Options
aprenria del layout dels subsistemes.
CONV-02 tanca al pas: la part Options dels "globals sense observers"
queda resolta amb la nova encapsulació. Map::* (l'altra meitat de
CONV-02) ja s'havia tancat amb PATCH-07.
Smoke tests 13/13 OK; log Game start → Bye! sota xvfb-run +
SDL_VIDEODRIVER=dummy + SIGINT idèntic al baseline de main.
clang-tidy i cppcheck nets sobre els fitxers modificats.
Refs: docs/audit-game.md (COUP-03, CONV-02, INCONS-01).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PATCH-01 (col·lisió per eix a Player::update) queda per a la branca
`refactor/options-class` junt amb COUP-02/03, ja que tryMove a
AnimatedSprite toca core/rendering/sprite/ i val la pena agrupar-ho
amb el refactor de MapInfo/Options. Recomptes: 28 DONE + 2 DONE
(parcial) + 5 WONTFIX + 4 DEFERRED.
- INCONS-05: `buildScreenConfig` passa a return amb designated
initializers, consistent amb `buildAudioConfig`.
- INCONS-01: marcat DONE (parcial) — queden 3 estils intencionals
(namespace per estat compartit, classes per entitats, constexpr per
alias), ja no barreja fortuïta.
- INCONS-03/04, PATCH-03, COUP-04, CONV-04: WONTFIX amb raó
documentada (contractes diferents, linter ho dicta, o sobre-abstracció
per a un únic caller).
- COUP-02/03, CONV-02 (parcial): DEFERRED a `refactor/options-class`
(equivalent a COUP-01 de l'audit d'àudio).
- CONV-01: DONE per HC-01/02/03/04/05/06.
Map no té estat d'instància (tots els membres són inline globals) i el
ctor ja s'havia eliminat a HC-03. Convertit a `namespace Map`: callers
no canvien perquè la sintaxi `Map::grid_cols`/`Map::collides(...)` és
idèntica. Pattern consistent amb `Options`, que és l'altre mòdul sense
estat al game/.
- PATCH-05: `if (Audio::get() != nullptr)` eliminat; Director::init
crida Audio::init abans de GameScene, comentari ho documenta. Si mai
salta, és un bug real que val detectar.
- PATCH-06/DEAD-05: short-circuit per `players_.size() <= 1` — no vector
temporal ni sort amb un sol jugador. La branca multi-jugador queda
preparada per quan n'hi haja >1 o sarcs-sprites.
`0xFF000000U` passa a `constexpr std::uint32_t ALPHA_MASK` local amb
comentari. No s'ha mogut a defines.hpp perquè només s'usa dins de
Tilemap::render; si apareix a altres llocs es podrà centralitzar.
Afegit `Defaults::UI::` amb els 5 índexs (RENDER_INFO_TEXT/SHADOW,
CANVAS_BG, NOTIFICATION_TEXT/BG, EXIT_TEXT/BG) i 2 durades
(NOTIFICATION_TIME_MS, EXIT_TIME_MS). `ui_palette.hpp` i
`notification_presets.hpp` queden com a alias: canviar el tema global
és tocar un sol bloc. Equivalent a tenir un "tema" (vindria d'un YAML el
dia que calga).
Els 4 `const auto` MIN_/MAX_ i els dos `std::clamp` es mouen a un mètode
privat `clampToPlayfield`. Separa la responsabilitat de mantenir la
caixa dins del playfield de la integració de moviment.
Les 7 funcions parse* i els dos parse*Preset repetien el patró
`if (node.contains("x")) { x = node["x"].get_value<T>(); }`. Centralitzat
en `readField(node, key, dst)` a l'anonymous namespace.
Tres parelles de `*ToString`/`*FromString` substituïdes per tres taules
`constexpr std::array<pair<Enum, string_view>, N>` i dos helpers plantilla
(`enumToString`, `enumFromString`). La primera entrada de cada taula és
el fallback quan l'enum és desconegut o el text no casa: així els valors
"letterbox"/"nearest"/"off" només apareixen una vegada. Resol també HC-07.
Les dues funcions compartien l'algorisme complet — només canvien l'eix
actiu (getPosY/setPosY vs getPosX/setPosX) i els paràmetres del Map.
`snapToLane` pren base, pitch, max_idx i dos member-fn pointers i és
cridat pels dos wrappers amb els valors corresponents.
`renderBackground`, `renderSarcsRow`, `getSarcRowBottomY` i els índexs de
paleta `HUD_BG/WALL/FLOOR/SARC` no tenien callers: el Tilemap s'encarrega
del render visual i els sarcs passaran a ser actors sprite. `getSarcRect`
es queda (la usa `collides`). Includes orfes de `screen.hpp`,
`resource_cache.hpp` i `defines.hpp` eliminats. DEAD-04 (ctor Map)
ja es va resoldre durant HC-03.
Les 10 ocurrències de std::cerr a options.cpp (incloent les de PATCH-04)
passen a std::cout per complir la convenció del projecte ("std::cout
only, no SDL_Log"). Prova manual: amb config.yaml trencat, els missatges
de backup i reescriptura es veuen per stdout.
Afegits `Defaults::Pyramid::{MAP_YAML, PLAYER_SPRITE, PLAYER_YAML,
MUSIC_TRACK}` a utils/defaults.hpp. `GameScene` exposa `Config` i
`defaultConfig()`; el ctor accepta Config (default = Defaults::Pyramid)
i usa els camps enlloc dels literals. `Player` passa a rebre el anim_key
per constructor en lloc de cablejar "player.yaml". Cache key = basename
(convenció preexistent). Cada piràmide futura serà un Config distint.
Afegit bloc `collision:` (size 16, base_size 12) al YAML del player; les
`static constexpr` de `player.hpp` passen a `inline static int` (snake_case)
i es calculen al `Player::loadFrom()`. `sprite_w/sprite_h` es llegeixen
dels mateixos frameWidth/frameHeight que ja hi viuen. GameScene crida el
load abans del spawn (mateix patró que `Map::loadFrom`). Permet ajustar
la caixa del jugador sense recompilar.
La velocitat base del jugador (60 px/s) passa de `constexpr` local a
`player.cpp` a `Defaults::Player::SPEED_PX_PER_S`, seguint el patró usat
a l'audit d'àudio. `player.cpp` manté un alias local per llegibilitat.
Elimina el comentari TODO sobre sprint/slow-zones que PATCH-02 volia fora.
Helper `backupFile(path)` que copia el YAML a `<path>.bak` si existeix.
Es crida a `loadFromFile` (versió mismatch i catch) i a
`loadPresetsFromFile<T>` (manca `presets:`, llista buida, catch).
La primera execució no genera .bak. Cada reescriptura forçada registra el
motiu per stderr (INCONS-02 migrarà a std::cout).
Prova manual: config.yaml amb contingut \"===broken===\" genera
config.yaml.bak amb el contingut previ i un nou config.yaml amb defaults.
Afegits `loadPresetsFromFile<T>` i `savePresetsToFile<T>` a l'anonymous
namespace; els quatre entry-points passen a ser one-liners que injecten
parse/write/defaults per tipus. Abans eren dues còpies del mateix codi
diferenciades només pel tipus concret de preset. Els YAML regenerats són
byte-a-byte idèntics als anteriors (el formatter i els comentaris es
preserven).
El fitxer definia presets del Notifier (que ja viu a core/rendering/), però
estava a game/ i era consumit únicament per core/input/global_inputs.cpp
— una dependència invertida core→game. El mogueu al costat del component
que consumeix; el comentari de `notifier.hpp` hi apunta. `game/ui/` es
queda només amb `ui_palette.hpp`.
El bloc `geometry:` de pyramid.yaml porta les 9 constants base (grid, sarc,
pitch, HUD, murs); els derivats (corridor, board, origin, playfield) es
recalculen al `Map::loadFrom()`. `constexpr` passa a `const` on calia i
els membres estàtics adopten snake_case. Desbloqueja la iteració
"pirámide a pirámide" sense recompilar.
Inventari de duplicació, hardcoding, parxes, acoplament, inconsistències,
codi mort i violacions de convencions. Tots els ítems PENDING; els commits
següents en tancaran un a un.
Tanca els DEFERRED de l'auditoria: COUP-01 (engine sense globals),
COUP-02 (header-only → .hpp/.cpp), PATCH-03 (stb_vorbis macros aïllades),
CONV-02 (engine ja és classe), CONV-03 (callback music-ended),
INCONS-05 complet (silenciar per gain, sense flags de motor).
5 commits atòmics amb build + xvfb entre cadascun. Detalls: plan a
/home/sergio/.claude/plans/crea-un-plan-para-twinkling-bubble.md i
docs/audit-core-audio.md actualitzat.
Únic DEFERRED restant de l'auditoria: PATCH-05 (voice-stealing policy)
— és feature, no deute.
Actualitza audit-core-audio.md amb els estats finals després del refactor
a refactor/audio-engine-class:
- COUP-01 (encapsular Ja::* globals): DONE — Ja::Engine classe RAII
- COUP-02 (header-only): DONE — jail_audio.cpp nou amb stb_vorbis.c
- PATCH-03 (#undef macros): DONE — migrats a jail_audio.cpp
- CONV-02 (engine no és classe): DONE — resolt amb COUP-01
- CONV-03 (callback music-ended): DONE — setOnMusicEnded
- INCONS-05 (enable/disable divergent): DONE complet — Audio silencia
per gain, flags de motor eliminats
Queda un sol ítem DEFERRED: PATCH-05 (voice-stealing policy), que és una
feature, no deute.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ja::Engine exposa setOnMusicEnded(std::function<void()>). Quan
updateCurrentMusic detecta que la pista actual ha acabat de drenar
(times == 0 && stream buit) s'atura primer via stopMusic i després
s'invoca el callback — així un eventual playMusic des del callback pot
començar amb l'engine en estat net.
Audio proxeia amb setOnMusicEnded. Contracte documentat: s'executa al
mateix thread que Audio::update (render loop); no fer operacions
blocants.
Cap consumidor actualment; només exposa l'API per quan calgui.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ja::Engine tenia music_enabled_ i sound_enabled_ com a membres amb el
seu propi enableMusic/enableSound setter, i disseminava checks
`if (!music_enabled_)` / `if (!sound_enabled_)` per gairebé totes les
operacions. Després del commit 3, ningú crida aquests setters: Audio
gestiona el gating per gain (effectiveVolume retorna 0 quan
music_enabled_/sound_enabled_ d'Audio són false). L'usuari ha confirmat
que vol mantenir aquesta semàntica (silenciar per gain, no parar).
Es retira tot el camí mort:
- Membres music_enabled_ / sound_enabled_ eliminats.
- Mètodes Engine::enableMusic(bool) / Engine::enableSound(bool)
eliminats.
- Checks `if (!music_enabled_)` / `if (!sound_enabled_)` eliminats de
playMusic, pauseMusic, resumeMusic, fadeOutMusic, crossfadeMusic,
updateCurrentMusic, playSound, playSoundOnChannel, pauseChannel,
resumeChannel, updateSoundChannels.
- Enum MusicState::DISABLED eliminat (inabastable sense el flag).
- Enum ChannelState::INVALID i SOUND_DISABLED eliminats (no usats
després de DEAD-04).
- Audio::getMusicState treu el case DISABLED del switch.
Audio::enableMusic/enableSound i els seus flags music_enabled_/
sound_enabled_ d'Audio romanen — són ells els qui porten la decisió al
motor via setXxxVolume(config_.X_volume) → effectiveVolume → gain.
Tanca la meitat restant d'INCONS-05.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audio guanya un std::unique_ptr<Ja::Engine> engine_ com a membre privat.
Es construeix a initSDLAudio amb els defaults de Defaults::Audio; es
destrueix sol al ~Audio (cap Ja::quit() explícit).
Totes les crides Ja::funcióN(...) d'audio.cpp passen a engine_->funcióN:
- playMusicInternal: crossfadeMusic/stopMusic/playMusic
- pauseMusic, resumeMusic, stopMusic, fadeOutMusic
- playSound x2, stopAllSounds
- setSoundVolume, setMusicVolume
- getMusicState (via instance->engine_ — és static però pot arribar-hi)
Les free functions forwarder del commit 2 (Ja::init/quit/update/playMusic/
pauseMusic/resumeMusic/stopMusic/fadeOutMusic/crossfadeMusic/
getMusicState/setMusicVolume/enableMusic i els equivalents de so) es
retiren del .hpp i del .cpp: ja no les crida ningú.
Queden com a free functions permanents (les usa Resource::Cache):
loadMusic (x2), loadSound, deleteMusic, deleteSound. Aquests darrers
consulten Engine::active() per notificar el motor abans d'alliberar.
audio.hpp forward-declara també `class Ja::Engine` perquè engine_ és
un std::unique_ptr i el dtor es defineix out-of-line al .cpp (= default)
— així no cal incloure jail_audio.hpp al header.
Els NOLINT(readability-make-member-function-const) d'audio.cpp es
mantenen amb raó actualitzada: std::unique_ptr::operator-> retorna T*
(no const T*), per tant clang-tidy veu un mètode d'Audio que podria ser
const encara que muti l'Engine via el punter. La mutació és real; el
NOLINT segueix sent honest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Encapsula l'estat del motor a Ja::Engine — classe amb RAII (ctor obre
SDL_AudioDevice, dtor el tanca) i `assert` al ctor contra instàncies
concurrents. Tots els globals del namespace anònim del commit 1
(current_music, channels[], audio_spec, music_volume, sound_volume[],
music_enabled, sound_enabled, sdl_audio_device, outgoing_music,
incoming_fade) es converteixen en membres privats amb sufix `_`. Els
helpers que toquen estat (stealCurrentIntoOutgoing,
updateOutgoing/IncomingFade, updateCurrent*, forEachTargetChannel)
passen a mètodes privats. Els stateless (feedMusicChunk, pumpMusic,
preFillOutgoing, fadeProgress) es queden al namespace anònim del .cpp.
Engine::active() static accessor: registrat al ctor, desregistrat al
dtor. Els deleters Ja::deleteMusic/deleteSound consulten Engine::active()
per parar canals/música abans d'alliberar recursos; si active() és
nullptr (shutdown-order invertit), fan cleanup local silenciós. Això
resol CONV-02 (engine ja és classe) i tanca COUP-01.
Compat amb els callers: les free functions Ja::init/quit/update,
Ja::playMusic, Ja::pauseMusic, Ja::resumeMusic, Ja::stopMusic,
Ja::fadeOutMusic, Ja::crossfadeMusic, Ja::getMusicState,
Ja::setMusicVolume, Ja::enableMusic, Ja::playSound, Ja::playSoundOnChannel,
Ja::pauseChannel, Ja::resumeChannel, Ja::stopChannel, Ja::setSoundVolume
i Ja::enableSound es mantenen com a thin forwarders contra Engine::active().
audio.cpp no canvia en aquest commit; les seves crides Ja:: continuen
funcionant. Al commit 3 Audio agafarà ownership directe via
std::unique_ptr<Engine> i aquests forwarders desapareixeran.
Ja::init() crea un std::unique_ptr<Engine> propietari dins d'un namespace
anònim del .cpp (encara no és Audio qui posseeix — això és el commit 3).
Ja::loadMusic/loadSound/deleteMusic/deleteSound es queden com a free
functions permanents (no depenen del motor per construir).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preparatori per COUP-01. No canvia comportament: només mou les
definicions inline de jail_audio.hpp a un jail_audio.cpp nou, deixant
el .hpp amb declaracions + tipus + constants.
Efectes col·laterals que es tanquen gratis:
- COUP-02: jail_audio deixa de ser header-only. Abans cada TU que
l'incloïa compilava 650 línies inline (amb stb_vorbis.c en mode
HEADER_ONLY). Ara el .hpp té ~130 línies de declaracions.
- PATCH-03: #include "external/stb_vorbis.c" + #undef L/C/R/PLAYBACK_*
viu només al final de jail_audio.cpp. audio.cpp perd el workaround
dels undefs.
Estat intern (current_music, channels[], audio_spec, music_volume,
sound_volume[], music_enabled, sound_enabled, sdl_audio_device,
outgoing_music, incoming_fade) baixa al .cpp dins d'un namespace
anònim (internal linkage). Els helpers privats (feedMusicChunk,
pumpMusic, preFillOutgoing, fadeProgress, update*Fade, updateCurrent*,
stealCurrentIntoOutgoing, forEachTargetChannel) també viuen a
l'anonymous namespace.
Ja::Music i Ja::Sound són públics; el .hpp forward-declara
`struct stb_vorbis` perquè els callers només els agafen per punter.
NOLINT(readability-identifier-naming) al forward-decl perquè el nom
l'imposa stb_vorbis.
jail_audio.cpp s'afegeix a APP_SOURCES al CMakeLists.txt.
.clang-tidy: desactivat clang-analyzer-core.UndefinedBinaryOperatorResult
globalment. Fals positiu conegut a external/stb_vorbis.c que abans no es
disparava (no hi havia TU que cridés directament stb_vorbis_seek_start
des del mateix fitxer on la funció està inlined); ara que jail_audio.cpp
sí que fa aquestes crides directes, el path d'anàlisi passa pel .c i el
check dispara. HeaderFilterRegex no aplica a clang-analyzer-*.
Preparació per COUP-01 (introduir Ja::Engine al commit següent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 ítems DONE (+1 parcial INCONS-05), 4 WONTFIX (HC-04, PATCH-03,
COUP-04, COUP-05, INCONS-04), 4 DEFERRED (COUP-01/02, PATCH-05,
CONV-02/03). CONV-01 passa a DONE parcial — la part del motor reutilizable
no es mou a Defaults per disseny.
Tots els DEFERRED pengen del refactor COUP-01 (encapsular Ja::* globals
en una classe), que tancarà també COUP-02, PATCH-03, PATCH-05, CONV-02,
CONV-03 i la meitat restant d'INCONS-05 (la divergència Ja::enable* vs
Audio::enable*).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Els comentaris d'audio.hpp i audio.cpp barrejaven castellà i català —
"Reproduce la música", "Detener", "Estado de la música"... conviven amb
"Reproduir", "Aturar", "Estat". Unifico tot a català, incloent-hi:
- Comentaris d'includes ("Para X" → "Per a X").
- Secció headers ("// --- Configuración general ---" → "// --- Configuració
general ---").
- Descripcions de mètodes i membres.
- Comentaris de l'enum Group/MusicState.
Els comentaris que ja estaven en català (els afegits a la mateixa
auditoria i els preexistents) no es toquen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
enableSound()/disableSound()/enableMusic()/disableMusic() eren inline
thunks que només cridaven la versió amb bool. Cap caller del projecte
les invoca. Es deixa només enableSound(bool) + toggleSound() (i
l'equivalent per música) com a API única.
Queda pendent la divergència semàntica amb Ja::enableSound/enableMusic
(Audio silencia per gain, Ja pararia els canals). No es resol aquí
perquè és una decisió de disseny que lliga amb l'encapsulament de
Ja::* (COUP-01).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verificat amb grep que cap caller dins de source/ invoca:
- Ja::setMusicPosition (stub no-op)
- Ja::getMusicPosition (stub que retornava 0)
- Ja::getMusicFilename (accessor a Music::filename, el camp s'accedeix
directament des d'Audio)
- Ja::getChannelState (getter sense consumidor públic)
- Ja::setVolume (combinació master — Audio fa el gate per canal)
Es manté loadMusic(buffer, length, filename) perquè Resource::Cache
l'utilitza per registrar el path dins del Ja::Music.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jail_audio.hpp és un header inline que arrossegava <iostream> a cada
TU que l'incloïa, només per a std::cout de missatges d'error. <iostream>
és un include pesat i aquí no aporta res: estem fent logging simple.
Passem a <cstdio> + std::fprintf(stderr, ...) als dos fitxers (jail_audio
i audio.cpp). Missatges prefixats "Ja::funció:" / "Audio:" seguint la
convenció "Module: message" que ja fan servir director.cpp, options.cpp,
resource_cache.cpp, etc.
Efecte col·lateral: stderr és l'stream correcte per als errors (abans
anaven a stdout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'overload playMusic(Ja::Music*, ...) buidava music_.name a cegues, així
que getCurrentMusicName() mentia després d'aquest camí tot i que el
Ja::Music que li passaves podia portar filename (Ja::loadMusic té un
overload que l'accepta).
Ara copiem music->filename — si el caller va usar l'overload amb
filename, el nom es conserva; si no, queda buit i el contracte és
explícit al comentari.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
music_.state era una cache d'estat que podia divergir del real —
precisament per això existia getRealMusicState() com a "consulta honesta".
Es descarta la cache:
- music_.state deixa d'existir; el struct privat Music només manté
name i loop.
- getMusicState() passa a ser static i consulta Ja::getMusicState(),
projectant INVALID/DISABLED al STOPPED que exposa Audio::MusicState.
- getRealMusicState() desapareix (getMusicState ja fa la consulta real).
- update() ja no necessita el bloc de sincronització.
- playMusicInternal/pauseMusic/resumeMusic/stopMusic/fadeOutMusic
llegeixen getMusicState() en comptes de music_.state i deixen de
reassignar-lo — Ja ja ho fa.
Externament: cap caller usa getMusicState/getRealMusicState, així que
canviar a static és segur (Audio::get()->getMusicState() continua
funcionant).
pauseMusic/resumeMusic/stopMusic es marquen amb NOLINT per
readability-make-member-function-const pel mateix motiu que INCONS-01
(muten globals de Ja::* que clang-tidy no veu).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
playSound (x2), stopAllSounds, fadeOutMusic, setSoundVolume i
setMusicVolume es declaraven const però mutaven l'estat global de Ja::*
(channels[], outgoing_music, music_volume, sound_volume[]). El const no
protegia res: el compilador no detectava les mutacions perquè anaven a
globals, no a membres, i podia induir a error a qui llegia el header.
Els callers externs van via Audio* (no Audio const*), així que el canvi
és source-compatible.
Clang-tidy falla amb readability-make-member-function-const perquè
tampoc veu la mutació — és exactament el símptoma que motiva el
hallazgo. S'afegeix NOLINT localitzat a cada mètode amb la raó. Quan
s'executi COUP-01 (encapsular Ja::* en classe membre) els NOLINT
cauran sols perquè la mutació serà visible al compilador.
effectiveVolume es queda const — és un càlcul pur sobre els membres.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pauseChannel, resumeChannel i stopChannel repetien tres vegades la
cadena "if channel == -1 forEach(channels) else if 0<=channel<MAX
apply()". S'extreu a forEachTargetChannel(channel, fn) i cada funció es
queda només amb la lògica per canal — pausar, reprendre, destruir.
Substitueix codi gairebé idèntic triplicat per una declaració de per
què fa el que fa cada funció.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fadeOutMusic i crossfadeMusic repetien el bloc "destruir outgoing
anterior + preFillOutgoing + robar stream + reset state+vorbis". Es
treu a stealCurrentIntoOutgoing(duration_ms) i els dos callers deleguen.
Com a efecte secundari, homogeneïtza la neteja: abans fadeOutMusic no
posava outgoing.fade.active = false en destruir l'anterior i
crossfadeMusic sí. L'helper fa sempre les dues coses. El comportament
observable no canvia perquè la fade s'assigna completament just després,
però elimina una inconsistència latent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
updateOutgoingFade i updateIncomingFade calculaven el mateix PERCENT
lineal (ELAPSED / duration_ms) per separat i prenien decisions sobre si
el fade havia acabat comparant bytes. Ara fadeProgress(FadeState)
retorna [0..1] i concentra la corba del fade — si cal passar-la a
logarítmica o quadràtica, es toca en un sol lloc.
També protegeix duration_ms<=0 com a "fade completat" (abans el divisor
donava una divisió per zero en debug o un progrés >> 1 en release).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setSoundVolume i setMusicVolume repetien la mateixa lògica de clamp +
gate (enabled_ && channel_enabled_) + multiplicació pel master
config_.volume. S'extreu effectiveVolume(volume, channel_enabled) privat
i els dos setters es limiten a la crida Ja::*.
Any canvi futur a la política (p.ex. una corba logarítmica) queda en un
sol lloc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Els dos playMusic compartien el dispatch crossfade-vs-stop+play i
l'actualització de music_.state/loop. S'extreu playMusicInternal privat
que rep el Ja::Music* ja resolt i fa el camí comú.
Cada overload es queda només amb el seu gating propi: la variant per
nom fa early-return si ja sona la mateixa pista i posa music_.name; la
variant per punter buida el nom (PATCH-02 recuperarà el filename del
propi Ja::Music).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Abans, toggleEnabled/toggleSound/toggleMusic i les variants sense bool
només flipaven el flag intern; els canals actius no notaven el canvi
fins a la pròxima crida explícita a setSoundVolume/setMusicVolume —
silenciós i fàcil de perdre.
Ara:
- toggleEnabled passa per enable(bool), que ja reaplicava els volums.
- enableSound(bool) i enableMusic(bool) es mouen a audio.cpp i
reapliquen el volum gated cap a Ja::*.
- Les variants enableSound()/disableSound()/enableMusic()/disableMusic()
i els toggles passen per aquestes dues funcions, així que qualsevol
camí d'activació produeix el mateix efecte sobre l'estat sonor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sound_volume[] és un inline float[] amb zero-init per defecte i la capa
Audio crida setSoundVolume(config_.sound_volume) via enable() just
després de Ja::init, així que el fill(0.5F) no arribava mai a l'usuari.
Era un valor màgic heretat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MUSIC_CHUNK_SHORTS i MUSIC_LOW_WATER_SECONDS són paràmetres del streaming
de vorbis/SDL; viuen dins de jail_audio i moure'ls a Defaults acoplaria
el motor al projecte. Mateix criteri que HC-03.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ja::MAX_SIMULTANEOUS_CHANNELS i Ja::MAX_GROUPS són límits interns del
motor. La proposta inicial era moure'ls a Defaults::Audio, però jail_audio
és reutilitzable entre projectes (no depèn d'Options ni d'utils/); moure
els límits trencaria aquesta independència.
El valor real del hallazgo era la invariant Audio::Group ↔ MAX_GROUPS,
que cap codi protegia. S'afegeix un static_assert a audio.cpp que falla
en compilació si s'afegeix una entrada nova a Group sense tocar
MAX_GROUPS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
{SDL_AUDIO_S16, 2, 48000} apareixia com a literal a tres llocs de
jail_audio (Sound/Music/audio_spec) i audio.cpp:208 passava SDL_AUDIO_S16LE
i 2 canals per separat — dues convencions de format conviuen i els 2
canals eren número màgic.
Defaults::Audio guanya FORMAT (SDL_AUDIO_S16) i CHANNELS (2). Audio::
initSDLAudio passa ambdós junts a Ja::init. jail_audio obté un fallback
local Ja::DEFAULT_SPEC que consolida les tres instàncies; es queda
independent del projecte (no inclou utils/defaults.hpp) perquè l'engine
és reutilitzable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VOLUME_STEP, FREQUENCY i DEFAULT_CROSSFADE_MS vivien com a membres
constexpr d'Audio. Eren valors de configuració — no invariants de la
classe — i la convenció del projecte és que tots els paràmetres
ajustables visquin a utils/defaults.hpp.
Passen a Defaults::Audio::{VOLUME_STEP, FREQUENCY, CROSSFADE_MS}. L'únic
ús intern (Ja::init al constructor d'Audio) es reapunta al Defaults. Cap
ús extern dels tres símbols.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plantilla d'auditoria per a core/audio/ amb els IDs estables (DUP/HC/
PATCH/COUP/INCONS/DEAD/CONV) i el top-5 prioritari. Mateix format que
l'auditoria de core/rendering/; les resolucions s'aniran anotant a
mesura que s'apliquin els canvis, commit a commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recull de 19 commits d'auditoria + 1 doc amb les troballes i el seu
estat. Veure docs/audit-core-rendering.md per al detall per-finding.
Resum per categoria:
- Duplicació: 7 items (surface blit, shaders MSL + factory, Text loops,
Text glyph resolution, GIF extension switch, GIF double-parse, CRT-Pi
defaults).
- Hardcoding: 9 items amb nom (INK_COLOR_INDEX, DEFAULT_TEXT_COLOR,
OPAQUE_ALPHA, LZW/GCT constants, segment indices, position count,
shadow offset, FONT_SIZE_PX, CRT reference height/subpixel div).
- Parxes: 5 neteja (readBytes comment, Sprite::update inline,
mono_digits dedup init, GIF extension labels, NOLINTs eliminats).
- Acoblament: 4 desacoblaments (Notifier→game/ui; RenderInfo→Options;
Screen→SDL3GPUShader concret; Surface→Screen; font+palette injectats).
- Codi mort: 9 items (writeShadowed/writeCentered/glyphWidth/
getGlyphClip/codepointToUtf8/getSprite/getRect/incX/incY/
getCurrentAnimationSize/getSurfaceData/Sprite::update/GIF extension
structs).
- Convencions: shaders MSL a fitxers dedicats amb naming conforme.
Build verificat a cada commit; smoke test de 5s sota xvfb-run a cada
finding (exit 124 = corregut sencer, sense crash).
Items restants DEFERRED (no es tanquen com a cleanup perquè són
features o decisions d'arquitectura global): HC-04 (YAML HUD config),
PATCH-01 (RAII Wayland), PATCH-02 (FSM RenderInfo), COUP-05
(AnimationLoader), COUP-06 (reemplaçar singletons), INC-01/-02/-03
(polítiques globals de smart-pointers, errors i logging).
Notifier::init i RenderInfo::init cridaven Resource::Cache::get() i
construïen cadascun la seua pròpia instància de Text a partir de
"8bithud.gif" + "data/font/8bithud.fnt" (strings hardcoded). Notifier
també llegia la paleta "ui" a cada render().
Ara:
- Director construeix una sola std::shared_ptr<Text> compartida.
- RenderInfo::init(Frame, shared_ptr<Text>) i Notifier::init
(shared_ptr<Text>, const Palette&) reben els recursos injectats.
- Notifier desa la Palette una volta i la consulta localment a
render(), sense tocar el cache global.
Resultat: els overlays perden la dependència dels noms dels assets
i del cache; el cicle de vida queda controlat pel Director. El camp
intern text_ passa a shared_ptr<Text>.
El document d'auditoria es tanca:
- PATCH-02, COUP-05 → DEFERRED (feature beyond cleanup scope).
- INC-04 → WONTFIX (coexistència justificada de writeDX/write*).
- Estat global: neteja completada.
Build + smoke 5s xvfb OK (exit 124).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notifier::TEXT_SIZE i el TEXT_SIZE del namespace anònim de render_info
declaraven, els dos, "8" com l'alçada del glif de 8bithud. Ara
Rendering::Hud::FONT_SIZE_PX (a core/rendering/hud_constants.hpp) és
l'única font de veritat; Notifier i RenderInfo l'inclouen.
HC-04 (moure la resta de constants HUD a YAML) es marca DEFERRED al
document: és una feature de runtime-config, no pura neteja.
Build + smoke 5s xvfb OK (exit 124).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Els defaults del shader CRT-Pi es duplicaven entre CrtPiParams
(shader_backend.hpp) i CrtPiUniforms (crtpi_pipeline.hpp). Ara
CrtPiUniforms es value-initializa a zeros i CrtPiPipeline::init(),
un cop el pipeline està creat, aplica setParams(CrtPiParams{}) per
a fixar els defaults canònics abans del primer render. Així queda
una sola font de veritat (els defaults del struct CrtPiParams al
interface ShaderBackend).
Build + smoke 5s xvfb OK (exit 124).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>