From 7bb1edd08aaf12becc9614e7905250fd3916b839 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 18 Apr 2026 19:56:59 +0200 Subject: [PATCH] Esquelet inicial: finestra negra i primera escena --- .clang-format | 22 +++++ .clang-tidy | 79 ++++++++++++++++ .githooks/pre-commit | 90 +++++++++++++++++++ .gitignore | 16 ++++ CMakeLists.txt | 134 ++++++++++++++++++++++++++++ Makefile | 65 ++++++++++++++ source/core/rendering/screen.cpp | 108 ++++++++++++++++++++++ source/core/rendering/screen.hpp | 28 ++++++ source/core/scene/scene.hpp | 18 ++++ source/core/system/director.cpp | 71 +++++++++++++++ source/core/system/director.hpp | 23 +++++ source/core/system/event_buffer.hpp | 9 ++ source/external/.clang-format | 2 + source/external/.clang-tidy | 4 + source/game/scenes/black_scene.cpp | 23 +++++ source/game/scenes/black_scene.hpp | 14 +++ source/main.cpp | 33 +++++++ source/utils/defaults.hpp | 54 +++++++++++ source/utils/defines.hpp | 19 ++++ source/version.h.in | 6 ++ 20 files changed, 818 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100755 .githooks/pre-commit create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 source/core/rendering/screen.cpp create mode 100644 source/core/rendering/screen.hpp create mode 100644 source/core/scene/scene.hpp create mode 100644 source/core/system/director.cpp create mode 100644 source/core/system/director.hpp create mode 100644 source/core/system/event_buffer.hpp create mode 100644 source/external/.clang-format create mode 100644 source/external/.clang-tidy create mode 100644 source/game/scenes/black_scene.cpp create mode 100644 source/game/scenes/black_scene.hpp create mode 100644 source/main.cpp create mode 100644 source/utils/defaults.hpp create mode 100644 source/utils/defines.hpp create mode 100644 source/version.h.in diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e148dad --- /dev/null +++ b/.clang-format @@ -0,0 +1,22 @@ +BasedOnStyle: Google +IndentWidth: 4 +NamespaceIndentation: All +IndentAccessModifiers: false +ColumnLimit: 0 # Sin límite de longitud de línea +BreakBeforeBraces: Attach # Llaves en la misma línea +AllowShortIfStatementsOnASingleLine: true +AllowShortBlocksOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AlignOperands: DontAlign +AlignAfterOpenBracket: DontAlign +BinPackArguments: false +BinPackParameters: false +ContinuationIndentWidth: 4 +ConstructorInitializerIndentWidth: 4 +IndentWrappedFunctionNames: false +Cpp11BracedListStyle: true +BreakConstructorInitializers: BeforeColon +AllowAllConstructorInitializersOnNextLine: false +PackConstructorInitializers: Never +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..91d231a --- /dev/null +++ b/.clang-tidy @@ -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 } diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..2d414d6 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Pre-commit hook: aplica clang-format als fitxers C++ staged abans del commit. +# - Només toca fitxers staged dins source/ (exclou source/external/). +# - Avorta el commit si hi ha canvis NO staged en aquests fitxers (per no incloure'ls sense voler). + +set -euo pipefail + +if ! command -v clang-format >/dev/null 2>&1; then + echo "pre-commit: clang-format no trobat — saltant format check" >&2 + exit 0 +fi + +mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=ACMR \ + | grep -E '^source/.*\.(cpp|hpp|h)$' \ + | grep -vE '^source/external/' || true) + +if [ ${#STAGED[@]} -eq 0 ]; then + exit 0 +fi + +UNSTAGED_DIRTY=() +for f in "${STAGED[@]}"; do + if ! git diff --quiet -- "$f"; then + UNSTAGED_DIRTY+=("$f") + fi +done + +if [ ${#UNSTAGED_DIRTY[@]} -gt 0 ]; then + echo "pre-commit: aquests fitxers tenen canvis NO staged i estan al commit." >&2 + echo " Fes 'git add' o 'git stash' abans de continuar:" >&2 + printf ' %s\n' "${UNSTAGED_DIRTY[@]}" >&2 + exit 1 +fi + +clang-format -i "${STAGED[@]}" +git add -- "${STAGED[@]}" + +# --- clang-tidy només sobre els fitxers staged --- +if ! command -v clang-tidy >/dev/null 2>&1; then + echo "pre-commit: clang-tidy no trobat — saltant tidy" >&2 + exit 0 +fi + +REPO_ROOT="$(git rev-parse --show-toplevel)" +BUILD_DIR="$REPO_ROOT/build" + +if [ ! -f "$BUILD_DIR/compile_commands.json" ]; then + echo "pre-commit: generant compile_commands.json (build dir buit)..." >&2 + cmake -S "$REPO_ROOT" -B "$BUILD_DIR" >/dev/null +fi + +echo "pre-commit: clang-tidy sobre ${#STAGED[@]} fitxer(s)..." >&2 +if ! clang-tidy -p "$BUILD_DIR" --quiet "${STAGED[@]}"; then + echo "pre-commit: clang-tidy ha trobat errors — commit avortat" >&2 + exit 1 +fi + +# --- cppcheck només sobre els .cpp staged --- +if ! command -v cppcheck >/dev/null 2>&1; then + echo "pre-commit: cppcheck no trobat — saltant cppcheck" >&2 + exit 0 +fi + +CPP_STAGED=() +for f in "${STAGED[@]}"; do + [[ "$f" == *.cpp ]] && CPP_STAGED+=("$f") +done + +if [ ${#CPP_STAGED[@]} -eq 0 ]; then + exit 0 +fi + +echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2 +if ! cppcheck \ + --enable=warning,style,performance,portability \ + --std=c++20 \ + --language=c++ \ + --inline-suppr \ + --suppress=missingIncludeSystem \ + --suppress=toomanyconfigs \ + --suppress='*:*/source/external/*' \ + -D_DEBUG \ + -DLINUX_BUILD \ + --quiet \ + --error-exitcode=1 \ + -I "$REPO_ROOT/source" \ + "${CPP_STAGED[@]}"; then + echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2 + exit 1 +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8175a11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +build/ +*.o +*.obj +*.a +*.lib +*.so +*.dylib +*.dll +*.exe +aee_2026 +_aee_2026 +.vscode/ +.idea/ +.cache/ +compile_commands.json +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2321431 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,134 @@ +cmake_minimum_required(VERSION 3.16) +project(aee_2026 VERSION 0.1.0 LANGUAGES CXX) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE) +endif() + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# --- GENERACIÓ DEL VERSION.H AMB GIT HASH --- +# Si GIT_HASH ve passat des de fora (Makefile), l'usem; si no, el resolem amb git. +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) + +set(APP_SOURCES + source/main.cpp + source/core/system/director.cpp + source/core/rendering/screen.cpp + source/game/scenes/black_scene.cpp +) + +find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) + +add_executable(${PROJECT_NAME} ${APP_SOURCES}) + +target_include_directories(${PROJECT_NAME} PRIVATE + "${CMAKE_SOURCE_DIR}/source" + "${CMAKE_BINARY_DIR}" +) + +target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3) + +target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) +target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$:_DEBUG> + $<$:RELEASE_BUILD> +) + +# --- STATIC ANALYSIS / FORMAT TARGETS --- +find_program(CLANG_TIDY_EXE NAMES clang-tidy) +find_program(CLANG_FORMAT_EXE NAMES clang-format) +find_program(CPPCHECK_EXE NAMES cppcheck) + +file(GLOB_RECURSE ALL_SOURCE_FILES + "${CMAKE_SOURCE_DIR}/source/*.cpp" + "${CMAKE_SOURCE_DIR}/source/*.hpp" + "${CMAKE_SOURCE_DIR}/source/*.h" +) +list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*") + +set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES}) +list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$") + +if(CLANG_TIDY_EXE) + add_custom_target(tidy + COMMAND ${CLANG_TIDY_EXE} + -p ${CMAKE_BINARY_DIR} + ${ALL_SOURCE_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Running clang-tidy..." + ) + + add_custom_target(tidy-fix + COMMAND ${CLANG_TIDY_EXE} + -p ${CMAKE_BINARY_DIR} + --fix + ${ALL_SOURCE_FILES} + 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() + +if(CLANG_FORMAT_EXE) + add_custom_target(format + COMMAND ${CLANG_FORMAT_EXE} + -i + ${ALL_SOURCE_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Running clang-format..." + ) + + add_custom_target(format-check + COMMAND ${CLANG_FORMAT_EXE} + --dry-run + --Werror + ${ALL_SOURCE_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Checking clang-format..." + ) +else() + message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles") +endif() + +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/* + -D_DEBUG + -DLINUX_BUILD + --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() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f11935e --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +PROJECT := aee_2026 +BUILDDIR := build +JOBS ?= $(shell nproc 2>/dev/null || echo 4) + +# VERSION (única font de veritat: source/utils/defines.hpp) +VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/') + +# GIT_HASH calculat al host i passat a CMake (evita problemes si CMake corre en +# entorns sense accés a git, p.ex. Docker/emscripten). +GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null) +ifeq ($(GIT_HASH),) + GIT_HASH := unknown +endif + +CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH) + +.PHONY: all configure build run clean rebuild release show-version format format-check tidy tidy-fix cppcheck hooks-install + +all: build + +configure: + @cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS) + +build: configure + @cmake --build $(BUILDDIR) -j$(JOBS) + +run: build + @./$(BUILDDIR)/$(PROJECT) + +release: + @cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS) + @cmake --build $(BUILDDIR) -j$(JOBS) + +show-version: + @echo "$(PROJECT) $(VERSION) ($(GIT_HASH))" + +clean: + @rm -rf $(BUILDDIR) + +rebuild: clean build + +# ============================================================================== +# CODE QUALITY (delegados a cmake) +# ============================================================================== +format: configure + @cmake --build $(BUILDDIR) --target format + +format-check: configure + @cmake --build $(BUILDDIR) --target format-check + +tidy: configure + @cmake --build $(BUILDDIR) --target tidy + +tidy-fix: configure + @cmake --build $(BUILDDIR) --target tidy-fix + +cppcheck: configure + @cmake --build $(BUILDDIR) --target cppcheck + +# ============================================================================== +# GIT HOOKS +# ============================================================================== +hooks-install: + @git config core.hooksPath .githooks + @echo "Git hooks activats: $(shell pwd)/.githooks" diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp new file mode 100644 index 0000000..c7450c5 --- /dev/null +++ b/source/core/rendering/screen.cpp @@ -0,0 +1,108 @@ +#include "core/rendering/screen.hpp" + +#include + +#include +#include + +#include "utils/defaults.hpp" +#include "utils/defines.hpp" + +namespace { + + auto toSdlPresentation(Defaults::ScalingMode mode) -> SDL_RendererLogicalPresentation { + switch (mode) { + case Defaults::ScalingMode::DISABLED: + return SDL_LOGICAL_PRESENTATION_DISABLED; + case Defaults::ScalingMode::STRETCH: + return SDL_LOGICAL_PRESENTATION_STRETCH; + case Defaults::ScalingMode::LETTERBOX: + return SDL_LOGICAL_PRESENTATION_LETTERBOX; + case Defaults::ScalingMode::OVERSCAN: + return SDL_LOGICAL_PRESENTATION_OVERSCAN; + case Defaults::ScalingMode::INTEGER: + return SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; + } + return SDL_LOGICAL_PRESENTATION_LETTERBOX; + } + + auto toSdlScaleMode(Defaults::TextureFilter filter) -> SDL_ScaleMode { + return filter == Defaults::TextureFilter::LINEAR ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST; + } + +} // namespace + +Screen* Screen::instance = nullptr; + +void Screen::init() { + if (instance == nullptr) { + instance = new Screen(); + } +} + +void Screen::destroy() { + delete instance; + instance = nullptr; +} + +auto Screen::get() -> Screen* { + return instance; +} + +Screen::Screen() { + if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) { + std::fprintf(stderr, "SDL_InitSubSystem(VIDEO) failed: %s\n", SDL_GetError()); + std::exit(EXIT_FAILURE); + } + + const int WIN_W = GameCanvas::WIDTH * Defaults::Window::ZOOM; + const int WIN_H = GameCanvas::HEIGHT * Defaults::Window::ZOOM; + + Uint32 win_flags = SDL_WINDOW_RESIZABLE; + if (Defaults::Window::FULLSCREEN) { win_flags |= SDL_WINDOW_FULLSCREEN; } + + window_ = SDL_CreateWindow(Texts::WINDOW_TITLE, WIN_W, WIN_H, win_flags); + if (window_ == nullptr) { + std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); + std::exit(EXIT_FAILURE); + } + + renderer_ = SDL_CreateRenderer(window_, nullptr); + if (renderer_ == nullptr) { + std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError()); + std::exit(EXIT_FAILURE); + } + + SDL_SetRenderVSync(renderer_, Defaults::Video::VSYNC ? 1 : 0); + SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(Defaults::Video::SCALING_MODE)); + + // Destination ARGB texture: paletted surfaces will be blitted onto this + // (each surface contributes its own palette, so the final image is full ARGB). + game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, GameCanvas::WIDTH, GameCanvas::HEIGHT); + if (game_texture_ == nullptr) { + std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError()); + std::exit(EXIT_FAILURE); + } + SDL_SetTextureScaleMode(game_texture_, toSdlScaleMode(Defaults::Video::TEXTURE_FILTER)); +} + +Screen::~Screen() { + if (game_texture_ != nullptr) { SDL_DestroyTexture(game_texture_); } + if (renderer_ != nullptr) { SDL_DestroyRenderer(renderer_); } + if (window_ != nullptr) { SDL_DestroyWindow(window_); } + SDL_QuitSubSystem(SDL_INIT_VIDEO); +} + +void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) { + SDL_SetRenderTarget(renderer_, game_texture_); + SDL_SetRenderDrawColor(renderer_, r, g, b, 0xFF); + SDL_RenderClear(renderer_); + SDL_SetRenderTarget(renderer_, nullptr); +} + +void Screen::present() { + SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF); + SDL_RenderClear(renderer_); + SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr); + SDL_RenderPresent(renderer_); +} diff --git a/source/core/rendering/screen.hpp b/source/core/rendering/screen.hpp new file mode 100644 index 0000000..936bed9 --- /dev/null +++ b/source/core/rendering/screen.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +class Screen { + public: + static void init(); + static void destroy(); + static auto get() -> Screen*; + + void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0); + void present(); + + [[nodiscard]] auto getRenderer() const -> SDL_Renderer* { return renderer_; } + [[nodiscard]] auto getTarget() const -> SDL_Texture* { return game_texture_; } + + private: + Screen(); + ~Screen(); + + static Screen* instance; + + SDL_Window* window_{nullptr}; + SDL_Renderer* renderer_{nullptr}; + SDL_Texture* game_texture_{nullptr}; +}; diff --git a/source/core/scene/scene.hpp b/source/core/scene/scene.hpp new file mode 100644 index 0000000..d9efe32 --- /dev/null +++ b/source/core/scene/scene.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +class Scene { + public: + virtual ~Scene() = default; + + virtual void update(int delta_ms) = 0; + virtual void render() = 0; + + [[nodiscard]] auto wantsQuit() const -> bool { return wants_quit_; } + [[nodiscard]] auto nextScene() -> std::unique_ptr { return std::move(next_scene_); } + + protected: + bool wants_quit_{false}; + std::unique_ptr next_scene_; +}; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp new file mode 100644 index 0000000..a40daac --- /dev/null +++ b/source/core/system/director.cpp @@ -0,0 +1,71 @@ +#include "core/system/director.hpp" + +#include + +#include +#include +#include +#include + +#include "core/rendering/screen.hpp" +#include "core/scene/scene.hpp" +#include "core/system/event_buffer.hpp" +#include "game/scenes/black_scene.hpp" + +Director::Director() { + if (!SDL_Init(0)) { + std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); + } + + Screen::init(); + + current_scene_ = std::make_unique(); + last_ticks_ms_ = SDL_GetTicks(); +} + +Director::~Director() { + current_scene_.reset(); + Screen::destroy(); + SDL_Quit(); +} + +void Director::processEvents() { + const bool QUIT_REQUESTED = std::ranges::any_of( + EventBuffer::events, + [](const SDL_Event& event) { return event.type == SDL_EVENT_QUIT; }); + if (QUIT_REQUESTED) { current_scene_.reset(); } +} + +void Director::switchSceneIfRequested() { + if (!current_scene_) { return; } + if (auto next = current_scene_->nextScene()) { + current_scene_ = std::move(next); + } +} + +auto Director::iterate() -> SDL_AppResult { + processEvents(); + + if (!current_scene_) { + EventBuffer::events.clear(); + return SDL_APP_SUCCESS; + } + + const std::uint64_t NOW_MS = SDL_GetTicks(); + int delta_ms = static_cast(NOW_MS - last_ticks_ms_); + delta_ms = std::max(delta_ms, 0); + last_ticks_ms_ = NOW_MS; + + current_scene_->update(delta_ms); + current_scene_->render(); + + if (current_scene_->wantsQuit()) { + EventBuffer::events.clear(); + return SDL_APP_SUCCESS; + } + + switchSceneIfRequested(); + + EventBuffer::events.clear(); + return SDL_APP_CONTINUE; +} diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp new file mode 100644 index 0000000..771b7d4 --- /dev/null +++ b/source/core/system/director.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include +#include + +class Scene; + +class Director { + public: + Director(); + ~Director(); + + auto iterate() -> SDL_AppResult; + + private: + std::unique_ptr current_scene_; + std::uint64_t last_ticks_ms_{0}; + + void processEvents(); + void switchSceneIfRequested(); +}; diff --git a/source/core/system/event_buffer.hpp b/source/core/system/event_buffer.hpp new file mode 100644 index 0000000..dee3696 --- /dev/null +++ b/source/core/system/event_buffer.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include + +namespace EventBuffer { + inline std::vector events; +} diff --git a/source/external/.clang-format b/source/external/.clang-format new file mode 100644 index 0000000..47a38a9 --- /dev/null +++ b/source/external/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: Never diff --git a/source/external/.clang-tidy b/source/external/.clang-tidy new file mode 100644 index 0000000..bbae176 --- /dev/null +++ b/source/external/.clang-tidy @@ -0,0 +1,4 @@ +# source/external/.clang-tidy +Checks: '-*' +WarningsAsErrors: '' +HeaderFilterRegex: '' \ No newline at end of file diff --git a/source/game/scenes/black_scene.cpp b/source/game/scenes/black_scene.cpp new file mode 100644 index 0000000..3c59857 --- /dev/null +++ b/source/game/scenes/black_scene.cpp @@ -0,0 +1,23 @@ +#include "game/scenes/black_scene.hpp" + +#include + +#include "core/rendering/screen.hpp" +#include "core/system/event_buffer.hpp" + +BlackScene::BlackScene() = default; + +void BlackScene::update(int delta_ms) { + elapsed_ms_ += delta_ms; + + for (const auto& event : EventBuffer::events) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) { + wants_quit_ = true; + } + } +} + +void BlackScene::render() { + Screen::get()->clear(0x00, 0x00, 0x00); + Screen::get()->present(); +} diff --git a/source/game/scenes/black_scene.hpp b/source/game/scenes/black_scene.hpp new file mode 100644 index 0000000..6cffd1c --- /dev/null +++ b/source/game/scenes/black_scene.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "core/scene/scene.hpp" + +class BlackScene : public Scene { + public: + BlackScene(); + + void update(int delta_ms) override; + void render() override; + + private: + int elapsed_ms_{0}; +}; diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..192ae4e --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,33 @@ +#define SDL_MAIN_USE_CALLBACKS 1 + +#include +#include + +#include "core/system/director.hpp" +#include "core/system/event_buffer.hpp" + +auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult { + (void)argc; + (void)argv; + + auto* director = new Director(); + *appstate = director; + return SDL_APP_CONTINUE; +} + +auto SDL_AppEvent(void* appstate, SDL_Event* event) -> SDL_AppResult { + (void)appstate; + EventBuffer::events.push_back(*event); + return SDL_APP_CONTINUE; +} + +auto SDL_AppIterate(void* appstate) -> SDL_AppResult { + auto* director = static_cast(appstate); + return director->iterate(); +} + +void SDL_AppQuit(void* appstate, SDL_AppResult result) { + (void)result; + auto* director = static_cast(appstate); + delete director; +} diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp new file mode 100644 index 0000000..a6e0778 --- /dev/null +++ b/source/utils/defaults.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +namespace Defaults { + + // Mode de presentació lògica (escala finestra). Mapeja als valors de + // SDL_RendererLogicalPresentation des de Screen via una funció helper, + // perquè aquest header no depengui de detalls de SDL3 més enllà del scancode. + enum class ScalingMode : std::uint8_t { + DISABLED = 0, + STRETCH = 1, + LETTERBOX = 2, + OVERSCAN = 3, + INTEGER = 4, + }; + + // Filtre de textura per a l'upscale final. + enum class TextureFilter : std::uint8_t { + NEAREST = 0, + LINEAR = 1, + }; + + namespace Window { + constexpr int ZOOM = 3; + constexpr bool FULLSCREEN = false; + } // namespace Window + + namespace Video { + constexpr bool VSYNC = true; + constexpr ScalingMode SCALING_MODE = ScalingMode::LETTERBOX; + constexpr TextureFilter TEXTURE_FILTER = TextureFilter::NEAREST; + } // namespace Video + + namespace Audio { + constexpr bool ENABLED = true; + constexpr float VOLUME = 1.0F; + constexpr bool MUSIC_ENABLED = true; + constexpr float MUSIC_VOLUME = 0.8F; + constexpr bool SOUND_ENABLED = true; + constexpr float SOUND_VOLUME = 1.0F; + } // namespace Audio + + namespace KeysGame { + constexpr SDL_Scancode UP = SDL_SCANCODE_UP; + constexpr SDL_Scancode DOWN = SDL_SCANCODE_DOWN; + constexpr SDL_Scancode LEFT = SDL_SCANCODE_LEFT; + constexpr SDL_Scancode RIGHT = SDL_SCANCODE_RIGHT; + constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE; + } // namespace KeysGame + +} // namespace Defaults diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp new file mode 100644 index 0000000..87d10d0 --- /dev/null +++ b/source/utils/defines.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace Texts { + constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; + constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; + constexpr const char* VERSION = "0.1.0"; // Single source of truth — Makefile el grep +} // namespace Texts + +namespace GameCanvas { + constexpr int WIDTH = 320; + constexpr int HEIGHT = 240; + constexpr int CENTER_X = WIDTH / 2; + constexpr int CENTER_Y = HEIGHT / 2; +} // namespace GameCanvas + +namespace Tile { + constexpr int SIZE = 8; + constexpr int HALF_SIZE = SIZE / 2; +} // namespace Tile diff --git a/source/version.h.in b/source/version.h.in new file mode 100644 index 0000000..bc0d7fd --- /dev/null +++ b/source/version.h.in @@ -0,0 +1,6 @@ +#pragma once + +namespace Version { + constexpr const char* GIT_HASH = "@GIT_HASH@"; + constexpr const char* APP_NAME = "Aventures en Egipte"; +} // namespace Version