diff --git a/CMakeLists.txt b/CMakeLists.txt index 36200cb..396154b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,7 @@ set(APP_SOURCES source/core/resources/resource_pack.cpp source/core/resources/palette_loader.cpp source/utils/easing.cpp + source/utils/log.cpp source/utils/utils.cpp source/game/debug_config.cpp source/game/options.cpp @@ -365,6 +366,7 @@ if(BUILD_TESTS) source/core/resources/resource_helper.cpp source/core/resources/resource_pack.cpp source/game/map/map.cpp + source/utils/log.cpp ${ENTITY_SOURCES_BASE} ) target_include_directories(test_entities_smoke PRIVATE @@ -394,6 +396,7 @@ if(BUILD_TESTS) source/core/resources/resource_helper.cpp source/core/resources/resource_pack.cpp source/game/map/map.cpp + source/utils/log.cpp ${ENTITY_SOURCES_BASE} source/game/entities/concrete/whip_attack.cpp ) @@ -418,6 +421,7 @@ if(BUILD_TESTS) source/core/input/key_config.cpp source/core/resources/resource_helper.cpp source/core/resources/resource_pack.cpp + source/utils/log.cpp ) target_include_directories(test_scene_flow PRIVATE "${CMAKE_SOURCE_DIR}/source" @@ -457,6 +461,7 @@ if(BUILD_TESTS) source/core/resources/resource_helper.cpp source/core/resources/resource_pack.cpp source/utils/easing.cpp + source/utils/log.cpp ) target_include_directories(test_game_fsm PRIVATE "${CMAKE_SOURCE_DIR}/source" diff --git a/source/core/resources/resource_helper.cpp b/source/core/resources/resource_helper.cpp index 1b9571b..4ead5ba 100644 --- a/source/core/resources/resource_helper.cpp +++ b/source/core/resources/resource_helper.cpp @@ -1,12 +1,12 @@ #include "core/resources/resource_helper.hpp" #include -#include #include #include #include "core/resources/resource_pack.hpp" #include "utils/defaults.hpp" +#include "utils/log.hpp" namespace Resource::Helper { @@ -27,14 +27,14 @@ namespace Resource::Helper { // Silenciem si veníem de prefer_disk: aleshores caurem // al pack i no és cap error real que el fitxer no estigui // al disc. - if (verbose) { std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; } + if (verbose) { Log::error("Resource::Helper", "cannot open {}", FULL); } return {}; } std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector data(static_cast(size)); if (!file.read(reinterpret_cast(data.data()), size)) { - std::cerr << "Resource::Helper: failed to read " << FULL << '\n'; + Log::error("Resource::Helper", "failed to read {}", FULL); return {}; } return data; @@ -52,14 +52,11 @@ namespace Resource::Helper { g_pack_loaded = g_pack.loadPack(pack_file); if (g_pack_loaded) { - std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount() - << " entries) from " << pack_file << '\n'; + Log::info("Resource::Helper", "pack loaded ({} entries) from {}", g_pack.getResourceCount(), pack_file); } else if (enable_fallback) { - std::cout << "Resource::Helper: no pack at " << pack_file - << " — using filesystem fallback\n"; + Log::info("Resource::Helper", "no pack at {} — using filesystem fallback", pack_file); } else { - std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file - << " and fallback disabled\n"; + Log::critical("Resource::Helper", "FATAL — no pack at {} and fallback disabled", pack_file); return false; } return true; diff --git a/source/main.cpp b/source/main.cpp index fda7842..84b569a 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -3,18 +3,20 @@ #include #include -#include #include #include "core/system/director.hpp" #include "core/system/event_buffer.hpp" +#include "utils/log.hpp" auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult { (void)argc; (void)argv; + // Logger primer de tot perquè l'arrencada del Director ja emet logs. + Log::init(); // Marcador d'inici: ha de ser la PRIMERA línia de log del programa. - std::cout << "Game start\n"; + Log::info("App", "Game start"); // Construïm el Director amb make_unique i l'entreguem en cru a SDL. // El SDL_AppQuit reclamarà la propietat amb un altre unique_ptr (RAII). @@ -43,5 +45,5 @@ void SDL_AppQuit(void* appstate, SDL_AppResult result) { std::unique_ptr director(static_cast(appstate)); } // Marcador de fi: ha de ser l'ÚLTIMA línia de log del programa. - std::cout << "Bye!\n"; + Log::info("App", "Bye!"); } diff --git a/source/utils/log.cpp b/source/utils/log.cpp new file mode 100644 index 0000000..9fa1a63 --- /dev/null +++ b/source/utils/log.cpp @@ -0,0 +1,150 @@ +#include "utils/log.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Log { + + namespace { + + std::mutex g_mutex; + std::unordered_map g_categories; + int g_next_category = SDL_LOG_CATEGORY_CUSTOM; + // Prioritat per defecte aplicada a tota CUSTOM nova. Es modifica només + // si l'usuari ha posat `*:level` (o un nivell pelat) a AEE_LOG. + SDL_LogPriority g_default_priority = SDL_LOG_PRIORITY_INFO; + + auto categoryFor(std::string_view tag) -> int { + std::lock_guard lk{g_mutex}; + const std::string KEY{tag}; + auto it = g_categories.find(KEY); + if (it != g_categories.end()) { return it->second; } + const int CAT = g_next_category++; + g_categories.emplace(KEY, CAT); + // Cada categoria CUSTOM neix amb la prioritat default de l'app + // (sense aquesta línia, SDL retorna ERROR per a qualsevol CUSTOM + // no inicialitzada, silenciant els nostres `info`). + SDL_SetLogPriority(CAT, g_default_priority); + return CAT; + } + + auto toSDL(Level lvl) -> SDL_LogPriority { + switch (lvl) { + case Level::TRACE: + return SDL_LOG_PRIORITY_TRACE; + case Level::DEBUG: + return SDL_LOG_PRIORITY_DEBUG; + case Level::INFO: + return SDL_LOG_PRIORITY_INFO; + case Level::WARN: + return SDL_LOG_PRIORITY_WARN; + case Level::ERROR: + return SDL_LOG_PRIORITY_ERROR; + case Level::CRITICAL: + return SDL_LOG_PRIORITY_CRITICAL; + } + return SDL_LOG_PRIORITY_INFO; + } + + // Parser tolerant de noms de prioritat. Sinònims acceptats per costum: + // verbose↔debug, warning↔warn, err↔error, fatal/crit↔critical. + auto parsePriority(std::string_view s) -> SDL_LogPriority { + std::string lower(s.size(), '\0'); + std::ranges::transform(s, lower.begin(), [](char c) { + return static_cast(std::tolower(static_cast(c))); + }); + if (lower == "trace") { return SDL_LOG_PRIORITY_TRACE; } + if (lower == "debug" || lower == "verbose") { return SDL_LOG_PRIORITY_DEBUG; } + if (lower == "info") { return SDL_LOG_PRIORITY_INFO; } + if (lower == "warn" || lower == "warning") { return SDL_LOG_PRIORITY_WARN; } + if (lower == "error" || lower == "err") { return SDL_LOG_PRIORITY_ERROR; } + if (lower == "critical" || lower == "crit" || lower == "fatal") { + return SDL_LOG_PRIORITY_CRITICAL; + } + return SDL_LOG_PRIORITY_INFO; + } + + auto trim(std::string_view s) -> std::string_view { + while (!s.empty() && (std::isspace(static_cast(s.front())) != 0)) { + s.remove_prefix(1); + } + while (!s.empty() && (std::isspace(static_cast(s.back())) != 0)) { + s.remove_suffix(1); + } + return s; + } + + // Itera entrades d'una especificació AEE_LOG separades per `,`, + // saltant les buides. + template + void forEachEntry(std::string_view spec, Fn fn) { + std::size_t i = 0; + while (i <= spec.size()) { + const std::size_t COMMA = spec.find(',', i); + const std::size_t END = (COMMA == std::string_view::npos) ? spec.size() : COMMA; + const std::string_view ENTRY = trim(spec.substr(i, END - i)); + i = (COMMA == std::string_view::npos) ? spec.size() + 1 : COMMA + 1; + if (!ENTRY.empty()) { fn(ENTRY); } + } + } + + // Una entrada AEE_LOG global aplica a totes les categories (SDL i + // CUSTOM). Forma curta `level` ↔ tag explícit `*:level`. + void applyGlobalEntry(std::string_view entry) { + const std::size_t COLON = entry.rfind(':'); + const bool IS_BARE = (COLON == std::string_view::npos); + const std::string_view TAG = + IS_BARE ? std::string_view{} : trim(entry.substr(0, COLON)); + if (!IS_BARE && TAG != "*" && !TAG.empty()) { return; } + const SDL_LogPriority PRIO = + parsePriority(IS_BARE ? entry : trim(entry.substr(COLON + 1))); + SDL_SetLogPriorities(PRIO); + g_default_priority = PRIO; + } + + // Override per tag concret (sobreescriu el global per a aquesta + // categoria). Si la categoria és nova es registra al vol. + void applyTagEntry(std::string_view entry) { + const std::size_t COLON = entry.rfind(':'); + if (COLON == std::string_view::npos) { return; } + const std::string_view TAG = trim(entry.substr(0, COLON)); + if (TAG == "*" || TAG.empty()) { return; } + const SDL_LogPriority PRIO = parsePriority(trim(entry.substr(COLON + 1))); + SDL_SetLogPriority(categoryFor(TAG), PRIO); + } + + } // namespace + + void log(Level lvl, std::string_view tag, std::string_view msg) { + // `"%s"` + cos com a argument: si `msg` conté `%` (rar però possible + // en paths o rutes amb urlencoded), no s'interpreta com a format spec. + const std::string BODY = std::format("{}: {}", tag, msg); + SDL_LogMessage(categoryFor(tag), toSDL(lvl), "%s", BODY.c_str()); + } + + void init() { + // No toquem cap categoria SDL nativa per defecte: així preservem el + // silenci de SDL_LOG_CATEGORY_GPU, RENDER, etc. (default = ERROR), i + // només el que arriba via Log::* es veu, mantenint l'output històric. + g_default_priority = SDL_LOG_PRIORITY_INFO; + + const char* env = std::getenv("AEE_LOG"); + if (env == nullptr || *env == '\0') { return; } + + // Tags poden contenir `::` (p. ex. `Resource::Helper`); busquem el + // separador amb rfind. Dues passades per fer l'override per-tag + // insensible a l'ordre (`Tag:debug,*:warn` deixa Tag a debug). + const std::string_view SPEC{env}; + forEachEntry(SPEC, applyGlobalEntry); + forEachEntry(SPEC, applyTagEntry); + } + +} // namespace Log diff --git a/source/utils/log.hpp b/source/utils/log.hpp new file mode 100644 index 0000000..eacc287 --- /dev/null +++ b/source/utils/log.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include + +// Logger lleuger embolicant `SDL_Log*`. Una sola entrada per nivell, format +// type-safe via `std::format`. Filtre runtime amb la variable d'entorn +// AEE_LOG (sintaxi `*:info,Resource::Helper:warn,scene:debug`). Ús típic: +// +// Log::info("Resource::Helper", "pack loaded ({} entries)", n); +// Log::warn("Options", "preset \"{}\" no trobat; aplicant defaults.", name); +// Log::error("Director", "no s'ha pogut carregar config.yaml a {}", path); +// +namespace Log { + + enum class Level : std::uint8_t { + TRACE, + DEBUG, + INFO, + WARN, + ERROR, + CRITICAL, + }; + + // Llig AEE_LOG i configura les prioritats SDL. Crida una sola vegada, + // tan aviat com sigui possible (abans del primer `Log::*`). + void init(); + + // Backend usat per les funcions per nivell. Composa "tag: msg" i ho + // entrega a SDL_LogMessage amb la categoria registrada per al tag. + void log(Level lvl, std::string_view tag, std::string_view msg); + + template + void trace(std::string_view tag, std::format_string fmt, Args&&... args) { + log(Level::TRACE, tag, std::format(fmt, std::forward(args)...)); + } + template + void debug(std::string_view tag, std::format_string fmt, Args&&... args) { + log(Level::DEBUG, tag, std::format(fmt, std::forward(args)...)); + } + template + void info(std::string_view tag, std::format_string fmt, Args&&... args) { + log(Level::INFO, tag, std::format(fmt, std::forward(args)...)); + } + template + void warn(std::string_view tag, std::format_string fmt, Args&&... args) { + log(Level::WARN, tag, std::format(fmt, std::forward(args)...)); + } + template + void error(std::string_view tag, std::format_string fmt, Args&&... args) { + log(Level::ERROR, tag, std::format(fmt, std::forward(args)...)); + } + template + void critical(std::string_view tag, std::format_string fmt, Args&&... args) { + log(Level::CRITICAL, tag, std::format(fmt, std::forward(args)...)); + } + +} // namespace Log