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

This commit is contained in:
2026-05-19 19:16:36 +02:00
parent 748673f41b
commit 63eaaa8b5c
13 changed files with 173 additions and 97 deletions
+1
View File
@@ -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 ---
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+3 -2
View File
@@ -144,8 +144,9 @@ void Resource::loadDataAsset(const std::string &bname, const std::vector<uint8_t
lines.push_back(line);
}
animation_lines_[bname] = std::move(lines);
} else if (bname == "demo.bin") {
demo_bytes_ = bytes;
} else if (bname.size() > 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
}
+3 -2
View File
@@ -29,7 +29,8 @@ class Resource {
auto getAnimationLines(const std::string &name) -> std::vector<std::string> &;
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<uint8_t> & { return demo_bytes_; }
[[nodiscard]] auto getDemoCount() const -> size_t { return demo_bytes_.size(); }
[[nodiscard]] auto getDemoBytes(size_t index) const -> const std::vector<uint8_t> & { return demo_bytes_.at(index); }
private:
explicit Resource(SDL_Renderer *renderer);
@@ -51,7 +52,7 @@ class Resource {
std::unordered_map<std::string, std::vector<std::string>> animation_lines_;
std::unordered_map<std::string, Text *> texts_;
std::unordered_map<std::string, Menu *> menus_;
std::vector<uint8_t> demo_bytes_;
std::vector<std::vector<uint8_t>> demo_bytes_;
static Resource *instance;
};
+17
View File
@@ -0,0 +1,17 @@
#include "core/system/demo.hpp"
#include <cstring> // 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<uint8_t> &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;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <vector>
// 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<DemoKeys>;
// 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<DemoData> 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<uint8_t> &bytes) -> DemoData;
+3 -1
View File
@@ -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);
+93 -69
View File
@@ -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<size_t>(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<Uint32>(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<int>(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<size_t>(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<size_t>(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);
}
}
+3 -10
View File
@@ -6,9 +6,10 @@
#include <string> // for string, basic_string
#include <vector> // 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)
+3 -13
View File
@@ -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