From 63eaaa8b5c79031df5b61b19b4cde969c533cc0d Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 19 May 2026 19:16:36 +0200 Subject: [PATCH] demo time-based: porta el patro de CCAE (multi-set, index = elapsed_s*60, % size per safe loop), substitueix demo.bin per demo1/2/3.bin --- CMakeLists.txt | 1 + data/demo/demo.bin | Bin 12000 -> 0 bytes data/demo/demo1.bin | Bin 0 -> 12000 bytes data/demo/demo2.bin | Bin 0 -> 12000 bytes data/demo/demo3.bin | Bin 0 -> 12000 bytes source/core/resources/resource.cpp | 5 +- source/core/resources/resource.h | 5 +- source/core/system/demo.cpp | 17 +++ source/core/system/demo.hpp | 47 +++++++++ source/core/system/director.cpp | 4 +- source/game/game.cpp | 162 +++++++++++++++++------------ source/game/game.h | 13 +-- source/utils/utils.h | 16 +-- 13 files changed, 173 insertions(+), 97 deletions(-) delete mode 100644 data/demo/demo.bin create mode 100644 data/demo/demo1.bin create mode 100644 data/demo/demo2.bin create mode 100644 data/demo/demo3.bin create mode 100644 source/core/system/demo.cpp create mode 100644 source/core/system/demo.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 21ceb87..5a345f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ set(APP_SOURCES # --- core/system --- source/core/system/delta_time.cpp + source/core/system/demo.cpp source/core/system/director.cpp # --- game --- diff --git a/data/demo/demo.bin b/data/demo/demo.bin deleted file mode 100644 index cef4d56f1c5d552fb58b5c45d69c1471aadf7103..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12000 zcmeHLjdH^v2yWm1rDs^vV6gasVC*%!-DVK@e$aHc$Mdm$W&a!lAa*v2w(NWZIG=Dd zhd(|JT*XyuA9Jwkr<}A-5cE*KP^P)+WN0-o`G8(Z zk?K3=Sq;Hg-|4Unz;Oc{;fW4LeG?!fA22A}E7%-ubHVy8K26#6vgx3n>x zm)&liS1odTsw0~M8EJ%Mk}KjW3ZoO&s3U{6J2}VMffK{%hc%i{eJUb>O+`FT1u~;K z)DR|%<>k>an&<0T5MXe04{;P1+W>M zjol-Ag{o5Bgs*!2zjtfT4=}O~u3gqzBPT2>xAxHTwY?K|!WmN#!j&pm3Cae19Vjgt zf7dsDe{m0a)yy@{Fe9_2w0m+gi1424+)&Ocb`5GIP=PE0tudy(%%*}}EA@-fiTW4w zSB~!rs=)iJs4#cPmc8HW!IP6sQ}#DINlIm(sEERcAfj;6`lL19j~HEx>R}FK6Qu;3 j@j}uTw`ITnL^on=&KYs8%r8_uY&|tSq-s9{V1b^0bLR*0 diff --git a/data/demo/demo1.bin b/data/demo/demo1.bin new file mode 100644 index 0000000000000000000000000000000000000000..b95466e0a82e03d205a79dbcb7b609166d2efd4c GIT binary patch literal 12000 zcmeHJi;lz~2<-m`T@IIP)ysx?^t1@ef!Sv@!`VEMSnFtfe9k@%{7Oyk z#oS4lnLRg=>IS$5ppK20FdGU92{NIU{c7ZpnTm%R+2Az@9Q4M3kgpOY{= zpH>n5Wcn*!0K)5k5k&CXFV!t@Sf?oISfH9JE`J@(FL1%bI+eOd8vure#2?a3u+p%R z_?hkPN7pExveHsBR(lzzUBlF$y=6IE?4a_Jr#P?Kg!5zp+pKBXr(D@<6^$AjfTj}m zm5ON6kRQrt=F-25G^qtU9!ip_?%$Vjq(<E2#&P&n7mW_3&{l8oN9C*wU=*jpNT zDs3WK3ui`1PA9fjj4SlF=Pvmf($rXry_)dt;LL6H)C#zwks&^&RxX|`%o>iBEEJ`Q zVc&~Zg(XC)Esbz516WgviP6a|)VA`Wz zZ&{$C6zwa$jMdVL6em>J_C3(ri)gka!{K@O?#l%w62W`dzV1+uX8AtjRnKyMI(jP+ r*`F_Z5f_){0zB>@FJwbr6z(M9u*Y&gp2FH_FUL(dkAcQ#~*{;V~^@)!; z4tg*xo<*J2#mO-c5J@`=Q9H)zcY&@Zy$yOaZcRK~E-W*oeJE0woexlIKZ#vCVc($Y zYT!zJ)|f)TU)Nu2mGWQ3P@6Gc(}ZfC6DJ=p7SIF7C>;L{@^#Gv@ETnV;1sGt###D` z#C&F8dgR4>`wb5JB_OG>(k#7*0N`6P4g&0jaS#|?L++Ty^Mvm<$nl&3$onrxB>r9L z%DH03w_(WFyrAuPC!#-kYv!#&FZ4-RExn{KGcze=D_9i+45tty0=Ta5xC9Apks z#G^MlP2QgCsjEivb=@Uv7-+^2b}~JMH*e4N)Ey1=duuMYs&Pf?R!gIh^5l+X?}&yQ z=cxXUonCtkrLB8=`P*kCf}Ct#$2?nO&A^@$=gVpGCe)5HuZ&2e*%ep~of7m07Z^G) z(*E3&#qC^Q8VLc59$8b64KAkQB<5TV=n>?qz`CdVTR@4JDnNmvH?sq0wSBo{!u}EW36YR6&FqRltmQ~x=YkA@GnB~MvLrbAr4U?CJ`HE*z zI;a%36&KCpWXWapT8*eld;gKA2gA_a z0FB;SN*mzrRSh_AQFS@}WcJRRAfsgWjBSgSHRC&`8iGV*gsD5XS^-$-4C#`E;G@H6>5c%&>s18 zo$EC|k1uk`TY8PpQ&N{N^2b~Bb=;9`9>^EPP5Vm>><5pK?a4ogjG)Jsg;&zzMxzi+ z;O*9yV!kTu;LPX&KAY$cCyKFoiSm@0*bG|2pm*B`enJbSoa4tBFk}yPl`G27WIw7B z&`i-0` zuS~JyRt-PZwKe-7{4&_hFyuLp!9aq7rA+OKyeoTP$W1_QJW#u@}S-MZ;F zl+)D{jjjE#Df;)(mzvhpqmSyBb~OGap4J46)vgR$xOkAENe{~!F`^&oADW 5 && bname.substr(0, 4) == "demo" && bname.substr(bname.size() - 4) == ".bin") { + // Acumula tots els demo*.bin (demo1.bin, demo2.bin, ...) en ordre d'aparicio + demo_bytes_.push_back(bytes); } // Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos } diff --git a/source/core/resources/resource.h b/source/core/resources/resource.h index e41d428..5606789 100644 --- a/source/core/resources/resource.h +++ b/source/core/resources/resource.h @@ -29,7 +29,8 @@ class Resource { auto getAnimationLines(const std::string &name) -> std::vector &; auto getText(const std::string &name) -> Text *; // name sin extensión: "smb2", "nokia2", ... auto getMenu(const std::string &name) -> Menu *; // name sin extensión: "title", "options", ... - auto getDemoBytes() const -> const std::vector & { return demo_bytes_; } + [[nodiscard]] auto getDemoCount() const -> size_t { return demo_bytes_.size(); } + [[nodiscard]] auto getDemoBytes(size_t index) const -> const std::vector & { return demo_bytes_.at(index); } private: explicit Resource(SDL_Renderer *renderer); @@ -51,7 +52,7 @@ class Resource { std::unordered_map> animation_lines_; std::unordered_map texts_; std::unordered_map menus_; - std::vector demo_bytes_; + std::vector> demo_bytes_; static Resource *instance; }; diff --git a/source/core/system/demo.cpp b/source/core/system/demo.cpp new file mode 100644 index 0000000..0a8e45a --- /dev/null +++ b/source/core/system/demo.cpp @@ -0,0 +1,17 @@ +#include "core/system/demo.hpp" + +#include // for memcpy + +// Desempaqueta un blob binari amb TOTAL_DEMO_DATA registres consecutius +// de DemoKeys (struct POD de 6 bytes). Si el blob no te la mida esperada, +// torna un vector buit perque el playback el detecti i no peti. +auto loadDemoDataFromBytes(const std::vector &bytes) -> DemoData { + DemoData dd; + const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA; + if (bytes.size() < EXPECTED) { + return dd; + } + dd.resize(TOTAL_DEMO_DATA); + std::memcpy(dd.data(), bytes.data(), EXPECTED); + return dd; +} diff --git a/source/core/system/demo.hpp b/source/core/system/demo.hpp new file mode 100644 index 0000000..d2975a0 --- /dev/null +++ b/source/core/system/demo.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include + +// Total de "frames" gravats a 60Hz de referencia. Equival a 2000/60 ~ 33.3s +// reals, independentment del refresc real perque el playback es time-based +// (index = elapsed_s * 60). +constexpr int TOTAL_DEMO_DATA = 2000; + +// Pulsacions per frame de referencia gravades a disc / reproduides al playback. +struct DemoKeys { + Uint8 left; + Uint8 right; + Uint8 no_input; + Uint8 fire; + Uint8 fire_left; + Uint8 fire_right; + + explicit DemoKeys(Uint8 l = 0, Uint8 r = 0, Uint8 ni = 0, Uint8 f = 0, Uint8 fl = 0, Uint8 fr = 0) + : left(l), + right(r), + no_input(ni), + fire(f), + fire_left(fl), + fire_right(fr) {} +}; + +// Una demo completa: vector de frames. +using DemoData = std::vector; + +// Estat del subsistema de demo dins de Game. +struct Demo { + bool enabled{false}; // Mode demo actiu (reproduccio) + bool recording{false}; // Mode gravacio actiu + float elapsed_s{0.0F}; // Temps acumulat des de l'inici de la demo + int index{0}; // index = elapsed_s * 60 (derivat) + DemoKeys keys; // Buffer de tecles del frame actual (gravacio) + std::vector data; // Vector de sets de demo carregats (multi-fitxer) +}; + +// Carrega un fitxer .bin (TOTAL_DEMO_DATA * sizeof(DemoKeys) bytes) i +// retorna el DemoData. Si el fitxer no es troba, retorna un DemoData buit. +auto loadDemoDataFromBytes(const std::vector &bytes) -> DemoData; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 1b9ab73..d077248 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -338,7 +338,9 @@ auto Director::setFileList() -> bool { // Ficheros de configuración Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true); - Asset::get()->add(PREFIX + "/data/demo/demo.bin", Asset::Type::DATA); + Asset::get()->add(PREFIX + "/data/demo/demo1.bin", Asset::Type::DATA); + Asset::get()->add(PREFIX + "/data/demo/demo2.bin", Asset::Type::DATA); + Asset::get()->add(PREFIX + "/data/demo/demo3.bin", Asset::Type::DATA); // Musicas Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC); diff --git a/source/game/game.cpp b/source/game/game.cpp index 95e9878..2d34ecb 100644 --- a/source/game/game.cpp +++ b/source/game/game.cpp @@ -277,7 +277,8 @@ void Game::init() { // Modo demo demo_.recording = false; - demo_.counter = 0; + demo_.elapsed_s = 0.0F; + demo_.index = 0; // Inicializa el objeto para el fundido fade_->init(0x27, 0x27, 0x36); @@ -521,34 +522,31 @@ auto Game::loadScoreFile() -> bool { return success; } -// Carga el fichero de datos para la demo +// Carga els fitxers de dades de demo (multi-set) des de Resource. Tots els +// blobs es descompacten a DemoData via loadDemoDataFromBytes; si un blob no +// te la mida esperada s'omet. auto Game::loadDemoFile() -> bool { - // Lee los datos de la demo desde Resource (precargados al arrancar). - const auto &bytes = Resource::get()->getDemoBytes(); - const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA; - if (bytes.size() >= EXPECTED) { - for (int i = 0; i < TOTAL_DEMO_DATA; ++i) { - memcpy(&demo_.data_file[i], bytes.data() + (i * sizeof(DemoKeys)), sizeof(DemoKeys)); - } - if (Options::settings.console) { - std::cout << "Demo data loaded (" << bytes.size() << " bytes)" << '\n'; - } - } else { - // Si no hay datos (bytes vacíos o tamaño inválido), inicializamos a cero. - if (Options::settings.console) { - std::cout << "Warning: demo data missing or too small, initializing to zero" << '\n'; - } - for (auto &i : demo_.data_file) { - demo_.keys.left = 0; - demo_.keys.right = 0; - demo_.keys.no_input = 0; - demo_.keys.fire = 0; - demo_.keys.fire_left = 0; - demo_.keys.fire_right = 0; - i = demo_.keys; + demo_.data.clear(); + const size_t NUM = Resource::get()->getDemoCount(); + for (size_t i = 0; i < NUM; ++i) { + DemoData dd = loadDemoDataFromBytes(Resource::get()->getDemoBytes(i)); + if (!dd.empty()) { + demo_.data.push_back(std::move(dd)); } } - return true; + if (!demo_.data.empty()) { + demo_selected_set_ = static_cast(rand()) % demo_.data.size(); + } else { + demo_selected_set_ = 0; + } + if (Options::settings.console) { + if (demo_.data.empty()) { + std::cout << "Warning: no demo data loaded" << '\n'; + } else { + std::cout << "Demo data loaded (" << demo_.data.size() << " sets, playing #" << demo_selected_set_ << ")" << '\n'; + } + } + return !demo_.data.empty(); } // Guarda el fichero de puntos @@ -577,32 +575,29 @@ auto Game::saveScoreFile() -> bool { return success; } -// Guarda el fichero de datos para la demo +// Guarda el primer set de demo (gravat en mode RECORDING) a demo1.bin. auto Game::saveDemoFile() -> bool { - bool success = true; - const std::string P = Asset::get()->get("demo.bin"); - const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1); - if (demo_.recording) { - SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b"); - if (file != nullptr) { - // Guardamos los datos - for (auto &i : demo_.data_file) { - SDL_WriteIO(file, &i, sizeof(DemoKeys)); - } - - if (Options::settings.console) { - std::cout << "Writing file " << FILE_NAME.c_str() << '\n'; - } - - // Cerramos el fichero - SDL_CloseIO(file); - } else { - if (Options::settings.console) { - std::cout << "Error: Unable to save " << FILE_NAME.c_str() << " file! " << SDL_GetError() << '\n'; - } - } + if (!demo_.recording || demo_.data.empty()) { + return true; } - return success; + const std::string P = Asset::get()->get("demo1.bin"); + const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1); + SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b"); + if (file == nullptr) { + if (Options::settings.console) { + std::cout << "Error: Unable to save " << FILE_NAME << " file! " << SDL_GetError() << '\n'; + } + return false; + } + const auto &dd = demo_.data.at(0); + for (const auto &k : dd) { + SDL_WriteIO(file, &k, sizeof(DemoKeys)); + } + if (Options::settings.console) { + std::cout << "Writing file " << FILE_NAME << '\n'; + } + SDL_CloseIO(file); + return true; } // Inicializa las formaciones enemigas @@ -2276,6 +2271,12 @@ void Game::update(float dt_s) { elapsed_s_ += dt_s; counter_ = static_cast(elapsed_s_ * 60.0F); + // Avenc del temps de la demo (playback o gravacio). Index = elapsed_s * 60 + if (demo_.enabled || demo_.recording) { + demo_.elapsed_s += dt_s; + demo_.index = static_cast(demo_.elapsed_s * 60.0F); + } + checkGameInput(); updatePlayers(dt_s); updateBackground(dt_s); @@ -2446,10 +2447,33 @@ void Game::checkGameInput() { } } -// Rama de checkGameInput: reproduce el input grabado en data_file +// Rama de checkGameInput: reprodueix l'input gravat al set actiu de la demo. +// El index es time-based: index = elapsed_s * 60. L'avenc d'elapsed_s el fa +// Game::update() per evitar que el ritme de playback depengui dels frames +// que arribin a aquesta funcio. void Game::processDemoInput() { const int INDEX = 0; - const DemoKeys &keys = demo_.data_file[demo_.counter]; + + // Fi de la demo: salta a Title + if (demo_.index >= TOTAL_DEMO_DATA) { + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; + return; + } + + // Si no hi ha dades carregades, sortim al menu + if (demo_.data.empty()) { + section_->name = SECTION_PROG_TITLE; + return; + } + + // Accedeix al frame actual del set seleccionat amb % per seguretat + // davant de salts puntuals d'index. + const auto &dd = demo_.data.at(demo_selected_set_ % demo_.data.size()); + if (dd.empty()) { + return; + } + const DemoKeys &keys = dd.at(static_cast(demo_.index) % dd.size()); if (keys.left == 1) { players_[INDEX]->setInput(Input::Action::LEFT); @@ -2479,18 +2503,10 @@ void Game::processDemoInput() { players_[INDEX]->setFireCooldown(10); } - // Si se pulsa cualquier tecla, se sale del modo demo + // Si es prem qualsevol tecla, surt del mode demo if (Input::get()->checkAnyInput()) { section_->name = SECTION_PROG_TITLE; } - - // Incrementa el contador de la demo - if (demo_.counter < TOTAL_DEMO_DATA) { - demo_.counter++; - } else { - section_->name = SECTION_PROG_TITLE; - section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; - } } // Rama de checkGameInput: lee inputs reales del teclado/gamepad por jugador @@ -2553,14 +2569,22 @@ void Game::processPlayerLiveInput(Player *player, int i) { section_->subsection = SUBSECTION_GAME_PAUSE; } - // Grabación de demo - if (demo_.counter < TOTAL_DEMO_DATA) { - if (demo_.recording) { - demo_.data_file[demo_.counter] = demo_.keys; + // Gravacio de demo (mode recording). L'index ja s'ha actualitzat a + // Game::update() via elapsed_s; aqui nomes escrivim el frame actual al + // primer set, redimensionant on demand. + if (demo_.recording) { + if (demo_.index >= TOTAL_DEMO_DATA) { + section_->name = SECTION_PROG_QUIT; + } else { + if (demo_.data.empty()) { + demo_.data.emplace_back(); + } + auto &dd = demo_.data.at(0); + if (dd.size() <= static_cast(demo_.index)) { + dd.resize(demo_.index + 1); + } + dd.at(demo_.index) = demo_.keys; } - demo_.counter++; - } else if (demo_.recording) { - section_->name = SECTION_PROG_QUIT; } } @@ -2590,7 +2614,7 @@ void Game::renderMessages() { // D E M O if (demo_.enabled) { - if (demo_.counter % 30 > 14) { + if (demo_.index % 30 > 14) { text_nokia_big2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, PLAY_AREA_FIRST_QUARTER_Y, Lang::get()->getText(37), 0, NO_COLOR, 2, SHADOW_COLOR); } } diff --git a/source/game/game.h b/source/game/game.h index 4b88249..c851df6 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -6,9 +6,10 @@ #include // for string, basic_string #include // for vector +#include "core/system/demo.hpp" // for Demo (estat de la demo) #include "game/entities/bullet.h" // for Bullet::Kind (signatura de createBullet) #include "game/entities/item.h" // for Item::Id (signatura de dropItem/createItem) -#include "utils/utils.h" // for DemoKeys, Color +#include "utils/utils.h" // for Color, Section class Balloon; class Fade; class Menu; @@ -45,7 +46,6 @@ class Game { // Cantidad de elementos a escribir en los ficheros de datos static constexpr int TOTAL_SCORE_DATA = 3; - static constexpr int TOTAL_DEMO_DATA = 2000; // Contadores static constexpr int STAGE_COUNTER = 200; @@ -138,14 +138,6 @@ class Game { int item_coffee_machine_odds; // Probabilidad de aparición del objeto }; - struct Demo { - bool enabled; // Indica si está activo el modo demo - bool recording; // Indica si está activado el modo para grabar la demo - Uint16 counter; // Contador para el modo demo - DemoKeys keys; // Variable con las pulsaciones de teclas del modo demo - DemoKeys data_file[TOTAL_DEMO_DATA]; // Datos del fichero con los movimientos para la demo - }; - void update(float dt_s); // Actualiza el juego void render(); // Dibuja el juego void init(); // Inicializa las variables necesarias para la sección 'Game' @@ -389,6 +381,7 @@ class Game { EnemyPool enemy_pool_[10]; // Variable con los diferentes conjuntos de formaciones enemigas Uint8 last_stage_reached_; // Contiene el numero de la última pantalla que se ha alcanzado Demo demo_; // Variable con todas las variables relacionadas con el modo demo + size_t demo_selected_set_{0}; // Index del set de demo a reproduir (escollit a loadDemoFile) int total_power_to_complete_game_; // La suma del poder necesario para completar todas las fases int clouds_speed_{0}; // Velocidad a la que se desplazan las nubes int pause_counter_; // Contador per a sortir del menu de pausa (frame-based, frames) diff --git a/source/utils/utils.h b/source/utils/utils.h index 5f43eac..4510057 100644 --- a/source/utils/utils.h +++ b/source/utils/utils.h @@ -60,21 +60,11 @@ struct Section { Uint8 subsection; }; -// Estructura para mapear el teclado usado en la demo -struct DemoKeys { - Uint8 left; - Uint8 right; - Uint8 no_input; - Uint8 fire; - Uint8 fire_left; - Uint8 fire_right; -}; - // Estructura para albergar métodos de control struct InputDevice { - int id; // Identificador en el vector de mandos - std::string name; // Nombre del dispositivo - Input::Device device_type; // Tipo de dispositivo (teclado o mando) + int id; // Identificador en el vector de mandos + std::string name; // Nombre del dispositivo + Input::Device device_type; // Tipo de dispositivo (teclado o mando) }; // Calcula el cuadrado de la distancia entre dos puntos