resource.pack

This commit is contained in:
2026-04-15 23:26:43 +02:00
parent c3534ace9c
commit 0faa605ad9
35 changed files with 1537 additions and 1851 deletions

3
.gitignore vendored
View File

@@ -1,7 +1,6 @@
.vscode
build/
dist/
data/config/config.txt
*.DS_Store
thumbs.db
*.exe
@@ -14,3 +13,5 @@ thumbs.db
coffee_crisis
coffee_crisis_debug
release/windows/coffee.res
resources.pack
tools/pack_resources/pack_resources

View File

@@ -72,7 +72,7 @@ Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bou
- `data/font/` — bitmap font files
- `data/music/` and `data/sound/` — audio assets
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
- `data/config/` — gamecontroller DB, demo recording data
- `data/demo/` — demo recording data (gamecontrollerdb.txt vive en la raíz del proyecto, fuera del pack)
- `data/menu/` — menu definition files
## Language

View File

@@ -88,7 +88,8 @@ elseif(APPLE)
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD)
target_link_options(${PROJECT_NAME} PRIVATE
--preload-file ${CMAKE_SOURCE_DIR}/data@/data
--preload-file ${CMAKE_SOURCE_DIR}/resources.pack@/resources.pack
--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
)

View File

@@ -3,6 +3,7 @@
# ==============================================================================
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
DIR_PACK_TOOL := tools/pack_resources
# ==============================================================================
# TARGET NAMES
@@ -60,14 +61,23 @@ endif
# ==============================================================================
# COMPILACIÓN CON CMAKE
# ==============================================================================
all:
all: resources.pack
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
debug:
debug: resources.pack
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
@cmake --build build
# ==============================================================================
# REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK
# ==============================================================================
pack_tool:
@$(MAKE) -C $(DIR_PACK_TOOL)
resources.pack:
@$(MAKE) -C $(DIR_PACK_TOOL) pack
# ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO)
# ==============================================================================
@@ -86,6 +96,7 @@ endif
# COMPILACIÓN PARA WINDOWS (RELEASE)
# ==============================================================================
windows_release:
@$(MAKE) resources.pack
@echo off
@echo Creando release para Windows - Version: $(VERSION)
@@ -99,7 +110,8 @@ windows_release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse -Force"
@powershell -Command "Copy-Item 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@@ -118,6 +130,7 @@ windows_release:
# COMPILACIÓN PARA MACOS (RELEASE)
# ==============================================================================
macos_release:
@$(MAKE) resources.pack
@echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario
@@ -140,7 +153,8 @@ macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
@@ -211,6 +225,7 @@ macos_release:
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
linux_release:
@$(MAKE) resources.pack
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake
@@ -222,7 +237,8 @@ linux_release:
$(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros
cp -R data "$(RELEASE_FOLDER)"
cp resources.pack "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
@@ -240,6 +256,7 @@ linux_release:
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
# ==============================================================================
wasm:
@$(MAKE) resources.pack
@echo "Compilando para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
@@ -295,4 +312,4 @@ help:
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda"
.PHONY: all debug release windows_release macos_release linux_release wasm controllerdb show_version help
.PHONY: all debug release windows_release macos_release linux_release wasm controllerdb pack_tool resources.pack show_version help

File diff suppressed because it is too large Load Diff

View File

@@ -6,28 +6,20 @@
#include "texture.h" // for Texture
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose) {
// Inicializa variables
// Parser compartido: lee un istream con el formato .ani
static animatedSprite_t parseAnimationStream(std::istream &file, Texture *texture, const std::string &filename, bool verbose) {
animatedSprite_t as;
as.texture = texture;
int framesPerRow = 0;
int frameWidth = 0;
int frameHeight = 0;
int maxTiles = 0;
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
std::ifstream file(filePath);
std::string line;
// El fichero se puede abrir
if (file.good()) {
// Procesa el fichero linea a linea
if (verbose) {
std::cout << "Animation loaded: " << filename << std::endl;
}
while (std::getline(file, line)) {
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
if (line == "[animation]") {
animation_t buffer;
buffer.counter = 0;
@@ -36,76 +28,47 @@ animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, b
do {
std::getline(file, line);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "name") {
buffer.name = line.substr(pos + 1, line.length());
}
else if (line.substr(0, pos) == "speed") {
} else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "loop") {
} else if (line.substr(0, pos) == "loop") {
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frames") {
// Se introducen los valores separados por comas en un vector
} else if (line.substr(0, pos) == "frames") {
std::stringstream ss(line.substr(pos + 1, line.length()));
std::string tmp;
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
while (getline(ss, tmp, ',')) {
// Comprueba que el tile no sea mayor que el maximo indice permitido
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
}
else {
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
}
} while (line != "[/animation]");
// Añade la animación al vector de animaciones
as.animations.push_back(buffer);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
} else {
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameWidth") {
} else if (line.substr(0, pos) == "frameWidth") {
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameHeight") {
} else if (line.substr(0, pos) == "frameHeight") {
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
}
else {
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
// Normaliza valores
if (framesPerRow == 0 && frameWidth > 0) {
framesPerRow = texture->getWidth() / frameWidth;
}
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
const int w = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight;
@@ -115,18 +78,35 @@ animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, b
}
}
// Cierra el fichero
file.close();
return as;
}
// El fichero no se puede abrir
else {
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose) {
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
std::ifstream file(filePath);
if (!file.good()) {
if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
animatedSprite_t as;
as.texture = texture;
return as;
}
return parseAnimationStream(file, texture, filename, verbose);
}
// Carga la animación desde bytes en memoria
animatedSprite_t loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &nameForLogs, bool verbose) {
if (bytes.empty()) {
animatedSprite_t as;
as.texture = texture;
return as;
}
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
return parseAnimationStream(ss, texture, nameForLogs, verbose);
}
// Constructor
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, std::string file, std::vector<std::string> *buffer) {

View File

@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string, basic_string
#include <vector> // for vector
@@ -26,6 +27,9 @@ struct animatedSprite_t {
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose = false);
// Carga la animación desde bytes en memoria
animatedSprite_t loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &nameForLogs = "", bool verbose = false);
class AnimatedSprite : public MovingSprite {
private:
// Variables

View File

@@ -5,6 +5,8 @@
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include "resource_helper.h"
// Constructor
Asset::Asset(std::string executablePath) {
this->executablePath = executablePath.substr(0, executablePath.find_last_of("\\/"));
@@ -96,15 +98,22 @@ bool Asset::checkFile(std::string path) {
bool success = false;
std::string result = "ERROR";
// Comprueba si existe el fichero
// Comprueba si existe el fichero (pack o filesystem)
const std::string filename = path.substr(path.find_last_of("\\/") + 1);
if (ResourceHelper::shouldUseResourcePack(path)) {
auto bytes = ResourceHelper::loadFile(path);
if (!bytes.empty()) {
result = "OK";
success = true;
}
} else {
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
if (file != nullptr) {
result = "OK";
success = true;
SDL_CloseIO(file);
}
}
if (verbose) {
std::cout.setf(std::ios::left, std::ios::adjustfield);

View File

@@ -18,15 +18,15 @@ enum assetType {
// Clase Asset
class Asset {
private:
public:
// Estructura para definir un item
struct item_t {
std::string file; // Ruta del fichero desde la raiz del directorio
enum assetType type; // Indica el tipo de recurso
bool required; // Indica si es un fichero que debe de existir
// bool absolute; // Indica si la ruta que se ha proporcionado es una ruta absoluta
};
private:
// Variables
int longestName; // Contiene la longitud del nombre de fichero mas largo
std::vector<item_t> fileList; // Listado con todas las rutas a los ficheros
@@ -49,6 +49,9 @@ class Asset {
// Devuelve un elemento de la lista a partir de una cadena
std::string get(std::string text);
// Devuelve toda la lista de items registrados
const std::vector<item_t> &getAll() const { return fileList; }
// Comprueba que existen todos los elementos
bool check();

View File

@@ -10,6 +10,7 @@
#endif
#include <cstdlib> // for exit, EXIT_FAILURE, srand
#include <filesystem>
#include <fstream> // for basic_ostream, operator<<, basi...
#include <iostream> // for cout
#include <memory>
@@ -23,6 +24,8 @@
#include "intro.h" // for Intro
#include "jail_audio.hpp" // for JA_Init
#include "lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
#include "resource.h"
#include "resource_helper.h"
#include "logo.h" // for Logo
#include "mouse.hpp" // for Mouse::handleEvent, Mouse::upda...
#include "screen.h" // for FILTER_NEAREST, Screen, FILTER_...
@@ -55,6 +58,19 @@ Director::Director(int argc, const char *argv[]) {
createSystemFolder("jailgames/coffee_crisis_debug");
#endif
// Inicializa el sistema de recursos (pack + fallback)
{
#ifdef RELEASE_BUILD
const bool enable_fallback = false;
#else
const bool enable_fallback = true;
#endif
if (!ResourceHelper::initializeResourceSystem("resources.pack", enable_fallback)) {
std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << std::endl;
exit(EXIT_FAILURE);
}
}
// Crea el objeto que controla los ficheros de recursos
asset = new Asset(executablePath);
asset->setVerbose(options->console);
@@ -80,9 +96,25 @@ Director::Director(int argc, const char *argv[]) {
lang = new Lang(asset);
lang->setLang(options->language);
input = new Input(asset->get("gamecontrollerdb.txt"));
#ifdef __EMSCRIPTEN__
input = new Input("/gamecontrollerdb.txt");
#else
{
const std::string binDir = std::filesystem::path(executablePath).parent_path().string();
#ifdef MACOS_BUNDLE
input = new Input(binDir + "/../Resources/gamecontrollerdb.txt");
#else
input = new Input(binDir + "/gamecontrollerdb.txt");
#endif
}
#endif
initInput();
// Precarga todos los recursos en memoria (texturas, sonidos, música, ...).
// Vivirán durante toda la vida de la app; las escenas leen handles compartidos.
// Debe ir antes de crear Screen porque Screen usa Text (propiedad de Resource).
Resource::init(renderer, asset, input);
screen = new Screen(window, renderer, asset, options);
activeSection = ActiveSection::None;
@@ -99,9 +131,15 @@ Director::~Director() {
title.reset();
game.reset();
// Screen puede tener referencias a Text propiedad de Resource: destruir
// Screen antes que Resource.
delete screen;
// Libera todos los recursos precargados antes de cerrar SDL.
Resource::destroy();
delete asset;
delete input;
delete screen;
delete lang;
delete options;
delete section;
@@ -111,6 +149,8 @@ Director::~Director() {
SDL_Quit();
ResourceHelper::shutdownResourceSystem();
std::cout << "\nBye!" << std::endl;
}
@@ -240,8 +280,7 @@ bool Director::setFileList() {
// Ficheros de configuración
asset->add(systemFolder + "/config.txt", t_data, false, true);
asset->add(systemFolder + "/score.bin", t_data, false, true);
asset->add(prefix + "/data/config/demo.bin", t_data);
asset->add(prefix + "/data/config/gamecontrollerdb.txt", t_data);
asset->add(prefix + "/data/demo/demo.bin", t_data);
// Musicas
asset->add(prefix + "/data/music/intro.ogg", t_music);

View File

@@ -15,6 +15,7 @@
#include "input.h" // for inputs_e, Input, REPEAT_TRUE, REPEAT_FALSE
#include "item.h" // for Item, ITEM_COFFEE_MACHINE, ITEM_CLOCK
#include "jail_audio.hpp" // for JA_PlaySound, JA_DeleteSound, JA_LoadSound
#include "resource.h"
#include "lang.h" // for Lang
#include "menu.h" // for Menu
#include "movingsprite.h" // for MovingSprite
@@ -98,84 +99,19 @@ Game::~Game() {
options->input[0].deviceType = onePlayerControl;
}
// Elimina todos los objetos contenidos en vectores
// Elimina todos los objetos contenidos en vectores (jugadores, balas, etc.)
deleteAllVectorObjects();
bulletTexture->unload();
delete bulletTexture;
gameBuildingsTexture->unload();
delete gameBuildingsTexture;
gameCloudsTexture->unload();
delete gameCloudsTexture;
gameGrassTexture->unload();
delete gameGrassTexture;
gamePowerMeterTexture->unload();
delete gamePowerMeterTexture;
gameSkyColorsTexture->unload();
delete gameSkyColorsTexture;
gameTextTexture->unload();
delete gameTextTexture;
gameOverTexture->unload();
delete gameOverTexture;
gameOverEndTexture->unload();
delete gameOverEndTexture;
// Animaciones
for (auto animation : playerAnimations) {
delete animation;
}
// Las texturas, animaciones, Text, Menu, sonidos y música son propiedad
// de Resource — no se liberan aquí. Solo limpiamos nuestras vistas.
playerAnimations.clear();
for (auto animation : balloonAnimations) {
delete animation;
}
balloonAnimations.clear();
for (auto animation : itemAnimations) {
delete animation;
}
itemAnimations.clear();
// Texturas
for (auto texture : player1Textures) {
texture->unload();
delete texture;
}
player1Textures.clear();
for (auto texture : player2Textures) {
texture->unload();
delete texture;
}
player2Textures.clear();
for (auto texture : itemTextures) {
texture->unload();
delete texture;
}
itemTextures.clear();
for (auto texture : balloonTextures) {
texture->unload();
delete texture;
}
balloonTextures.clear();
delete text;
delete textBig;
delete textScoreBoard;
delete textNokia2;
delete textNokiaBig2;
delete gameOverMenu;
delete pauseMenu;
delete fade;
delete eventHandler;
delete clouds1A;
@@ -192,24 +128,6 @@ Game::~Game() {
delete powerMeterSprite;
delete gameOverSprite;
delete gameOverEndSprite;
JA_DeleteSound(balloonSound);
JA_DeleteSound(bulletSound);
JA_DeleteSound(playerCollisionSound);
JA_DeleteSound(hiScoreSound);
JA_DeleteSound(itemDropSound);
JA_DeleteSound(itemPickUpSound);
JA_DeleteSound(coffeeOutSound);
JA_DeleteSound(stageChangeSound);
JA_DeleteSound(bubble1Sound);
JA_DeleteSound(bubble2Sound);
JA_DeleteSound(bubble3Sound);
JA_DeleteSound(bubble4Sound);
JA_DeleteSound(clockSound);
JA_DeleteSound(powerBallSound);
JA_DeleteSound(coffeeMachineSound);
JA_DeleteMusic(gameMusic);
}
// Inicializa las variables necesarias para la sección 'Game'
@@ -414,186 +332,106 @@ void Game::loadMedia() {
<< "** LOADING RESOURCES FOR GAME SECTION" << std::endl;
}
// Texturas
bulletTexture = new Texture(renderer, asset->get("bullet.png"));
gameBuildingsTexture = new Texture(renderer, asset->get("game_buildings.png"));
gameCloudsTexture = new Texture(renderer, asset->get("game_clouds.png"));
gameGrassTexture = new Texture(renderer, asset->get("game_grass.png"));
gamePowerMeterTexture = new Texture(renderer, asset->get("game_power_meter.png"));
gameSkyColorsTexture = new Texture(renderer, asset->get("game_sky_colors.png"));
gameTextTexture = new Texture(renderer, asset->get("game_text.png"));
gameOverTexture = new Texture(renderer, asset->get("menu_game_over.png"));
gameOverEndTexture = new Texture(renderer, asset->get("menu_game_over_end.png"));
Resource *R = Resource::get();
// Texturas (handles compartidos — no se liberan aquí)
bulletTexture = R->getTexture("bullet.png");
gameBuildingsTexture = R->getTexture("game_buildings.png");
gameCloudsTexture = R->getTexture("game_clouds.png");
gameGrassTexture = R->getTexture("game_grass.png");
gamePowerMeterTexture = R->getTexture("game_power_meter.png");
gameSkyColorsTexture = R->getTexture("game_sky_colors.png");
gameTextTexture = R->getTexture("game_text.png");
gameOverTexture = R->getTexture("menu_game_over.png");
gameOverEndTexture = R->getTexture("menu_game_over_end.png");
// Texturas - Globos
Texture *balloon1Texture = new Texture(renderer, asset->get("balloon1.png"));
balloonTextures.push_back(balloon1Texture);
Texture *balloon2Texture = new Texture(renderer, asset->get("balloon2.png"));
balloonTextures.push_back(balloon2Texture);
Texture *balloon3Texture = new Texture(renderer, asset->get("balloon3.png"));
balloonTextures.push_back(balloon3Texture);
Texture *balloon4Texture = new Texture(renderer, asset->get("balloon4.png"));
balloonTextures.push_back(balloon4Texture);
balloonTextures.push_back(R->getTexture("balloon1.png"));
balloonTextures.push_back(R->getTexture("balloon2.png"));
balloonTextures.push_back(R->getTexture("balloon3.png"));
balloonTextures.push_back(R->getTexture("balloon4.png"));
// Texturas - Items
Texture *item1 = new Texture(renderer, asset->get("item_points1_disk.png"));
itemTextures.push_back(item1);
Texture *item2 = new Texture(renderer, asset->get("item_points2_gavina.png"));
itemTextures.push_back(item2);
Texture *item3 = new Texture(renderer, asset->get("item_points3_pacmar.png"));
itemTextures.push_back(item3);
Texture *item4 = new Texture(renderer, asset->get("item_clock.png"));
itemTextures.push_back(item4);
Texture *item5 = new Texture(renderer, asset->get("item_coffee.png"));
itemTextures.push_back(item5);
Texture *item6 = new Texture(renderer, asset->get("item_coffee_machine.png"));
itemTextures.push_back(item6);
itemTextures.push_back(R->getTexture("item_points1_disk.png"));
itemTextures.push_back(R->getTexture("item_points2_gavina.png"));
itemTextures.push_back(R->getTexture("item_points3_pacmar.png"));
itemTextures.push_back(R->getTexture("item_clock.png"));
itemTextures.push_back(R->getTexture("item_coffee.png"));
itemTextures.push_back(R->getTexture("item_coffee_machine.png"));
// Texturas - Player1
Texture *player1Head = new Texture(renderer, asset->get("player_bal1_head.png"));
player1Textures.push_back(player1Head);
Texture *player1Body = new Texture(renderer, asset->get("player_bal1_body.png"));
player1Textures.push_back(player1Body);
Texture *player1Legs = new Texture(renderer, asset->get("player_bal1_legs.png"));
player1Textures.push_back(player1Legs);
Texture *player1Death = new Texture(renderer, asset->get("player_bal1_death.png"));
player1Textures.push_back(player1Death);
Texture *player1Fire = new Texture(renderer, asset->get("player_bal1_fire.png"));
player1Textures.push_back(player1Fire);
player1Textures.push_back(R->getTexture("player_bal1_head.png"));
player1Textures.push_back(R->getTexture("player_bal1_body.png"));
player1Textures.push_back(R->getTexture("player_bal1_legs.png"));
player1Textures.push_back(R->getTexture("player_bal1_death.png"));
player1Textures.push_back(R->getTexture("player_bal1_fire.png"));
playerTextures.push_back(player1Textures);
// Texturas - Player2
Texture *player2Head = new Texture(renderer, asset->get("player_arounder_head.png"));
player2Textures.push_back(player2Head);
Texture *player2Body = new Texture(renderer, asset->get("player_arounder_body.png"));
player2Textures.push_back(player2Body);
Texture *player2Legs = new Texture(renderer, asset->get("player_arounder_legs.png"));
player2Textures.push_back(player2Legs);
Texture *player2Death = new Texture(renderer, asset->get("player_arounder_death.png"));
player2Textures.push_back(player2Death);
Texture *player2Fire = new Texture(renderer, asset->get("player_arounder_fire.png"));
player2Textures.push_back(player2Fire);
player2Textures.push_back(R->getTexture("player_arounder_head.png"));
player2Textures.push_back(R->getTexture("player_arounder_body.png"));
player2Textures.push_back(R->getTexture("player_arounder_legs.png"));
player2Textures.push_back(R->getTexture("player_arounder_death.png"));
player2Textures.push_back(R->getTexture("player_arounder_fire.png"));
playerTextures.push_back(player2Textures);
// Animaciones -- Jugador
std::vector<std::string> *playerHeadAnimation = new std::vector<std::string>;
loadAnimations(asset->get("player_head.ani"), playerHeadAnimation);
playerAnimations.push_back(playerHeadAnimation);
// Animaciones (handles a los vectores raw de Resource — no se liberan)
playerAnimations.push_back(&R->getAnimationLines("player_head.ani"));
playerAnimations.push_back(&R->getAnimationLines("player_body.ani"));
playerAnimations.push_back(&R->getAnimationLines("player_legs.ani"));
playerAnimations.push_back(&R->getAnimationLines("player_death.ani"));
playerAnimations.push_back(&R->getAnimationLines("player_fire.ani"));
std::vector<std::string> *playerBodyAnimation = new std::vector<std::string>;
loadAnimations(asset->get("player_body.ani"), playerBodyAnimation);
playerAnimations.push_back(playerBodyAnimation);
balloonAnimations.push_back(&R->getAnimationLines("balloon1.ani"));
balloonAnimations.push_back(&R->getAnimationLines("balloon2.ani"));
balloonAnimations.push_back(&R->getAnimationLines("balloon3.ani"));
balloonAnimations.push_back(&R->getAnimationLines("balloon4.ani"));
std::vector<std::string> *playerLegsAnimation = new std::vector<std::string>;
loadAnimations(asset->get("player_legs.ani"), playerLegsAnimation);
playerAnimations.push_back(playerLegsAnimation);
std::vector<std::string> *playerDeathAnimation = new std::vector<std::string>;
loadAnimations(asset->get("player_death.ani"), playerDeathAnimation);
playerAnimations.push_back(playerDeathAnimation);
std::vector<std::string> *playerFireAnimation = new std::vector<std::string>;
loadAnimations(asset->get("player_fire.ani"), playerFireAnimation);
playerAnimations.push_back(playerFireAnimation);
// Animaciones -- Globos
std::vector<std::string> *balloon1Animation = new std::vector<std::string>;
loadAnimations(asset->get("balloon1.ani"), balloon1Animation);
balloonAnimations.push_back(balloon1Animation);
std::vector<std::string> *balloon2Animation = new std::vector<std::string>;
loadAnimations(asset->get("balloon2.ani"), balloon2Animation);
balloonAnimations.push_back(balloon2Animation);
std::vector<std::string> *balloon3Animation = new std::vector<std::string>;
loadAnimations(asset->get("balloon3.ani"), balloon3Animation);
balloonAnimations.push_back(balloon3Animation);
std::vector<std::string> *balloon4Animation = new std::vector<std::string>;
loadAnimations(asset->get("balloon4.ani"), balloon4Animation);
balloonAnimations.push_back(balloon4Animation);
// Animaciones -- Items
std::vector<std::string> *item1Animation = new std::vector<std::string>;
loadAnimations(asset->get("item_points1_disk.ani"), item1Animation);
itemAnimations.push_back(item1Animation);
std::vector<std::string> *item2Animation = new std::vector<std::string>;
loadAnimations(asset->get("item_points2_gavina.ani"), item2Animation);
itemAnimations.push_back(item2Animation);
std::vector<std::string> *item3Animation = new std::vector<std::string>;
loadAnimations(asset->get("item_points3_pacmar.ani"), item3Animation);
itemAnimations.push_back(item3Animation);
std::vector<std::string> *item4Animation = new std::vector<std::string>;
loadAnimations(asset->get("item_clock.ani"), item4Animation);
itemAnimations.push_back(item4Animation);
std::vector<std::string> *item5Animation = new std::vector<std::string>;
loadAnimations(asset->get("item_coffee.ani"), item5Animation);
itemAnimations.push_back(item5Animation);
std::vector<std::string> *item6Animation = new std::vector<std::string>;
loadAnimations(asset->get("item_coffee_machine.ani"), item6Animation);
itemAnimations.push_back(item6Animation);
itemAnimations.push_back(&R->getAnimationLines("item_points1_disk.ani"));
itemAnimations.push_back(&R->getAnimationLines("item_points2_gavina.ani"));
itemAnimations.push_back(&R->getAnimationLines("item_points3_pacmar.ani"));
itemAnimations.push_back(&R->getAnimationLines("item_clock.ani"));
itemAnimations.push_back(&R->getAnimationLines("item_coffee.ani"));
itemAnimations.push_back(&R->getAnimationLines("item_coffee_machine.ani"));
// Texto
text = new Text(asset->get("smb2.png"), asset->get("smb2.txt"), renderer);
textScoreBoard = new Text(asset->get("8bithud.png"), asset->get("8bithud.txt"), renderer);
textBig = new Text(asset->get("smb2_big.png"), asset->get("smb2_big.txt"), renderer);
textNokia2 = new Text(asset->get("nokia2.png"), asset->get("nokia2.txt"), renderer);
textNokiaBig2 = new Text(asset->get("nokia_big2.png"), asset->get("nokia_big2.txt"), renderer);
text = R->getText("smb2");
textScoreBoard = R->getText("8bithud");
textBig = R->getText("smb2_big");
textNokia2 = R->getText("nokia2");
textNokiaBig2 = R->getText("nokia_big2");
// Menus
gameOverMenu = new Menu(renderer, asset, input, asset->get("gameover.men"));
gameOverMenu = R->getMenu("gameover");
gameOverMenu->setItemCaption(0, lang->getText(48));
gameOverMenu->setItemCaption(1, lang->getText(49));
const int w = text->getCharacterSize() * lang->getText(45).length();
gameOverMenu->setRectSize(w, 0);
gameOverMenu->centerMenuOnX(199);
pauseMenu = new Menu(renderer, asset, input, asset->get("pause.men"));
pauseMenu = R->getMenu("pause");
pauseMenu->setItemCaption(0, lang->getText(41));
pauseMenu->setItemCaption(1, lang->getText(46));
pauseMenu->setItemCaption(2, lang->getText(47));
// Sonidos
balloonSound = JA_LoadSound(asset->get("balloon.wav").c_str());
bubble1Sound = JA_LoadSound(asset->get("bubble1.wav").c_str());
bubble2Sound = JA_LoadSound(asset->get("bubble2.wav").c_str());
bubble3Sound = JA_LoadSound(asset->get("bubble3.wav").c_str());
bubble4Sound = JA_LoadSound(asset->get("bubble4.wav").c_str());
bulletSound = JA_LoadSound(asset->get("bullet.wav").c_str());
clockSound = JA_LoadSound(asset->get("clock.wav").c_str());
coffeeOutSound = JA_LoadSound(asset->get("coffeeout.wav").c_str());
hiScoreSound = JA_LoadSound(asset->get("hiscore.wav").c_str());
itemDropSound = JA_LoadSound(asset->get("itemdrop.wav").c_str());
itemPickUpSound = JA_LoadSound(asset->get("itempickup.wav").c_str());
playerCollisionSound = JA_LoadSound(asset->get("player_collision.wav").c_str());
powerBallSound = JA_LoadSound(asset->get("powerball.wav").c_str());
stageChangeSound = JA_LoadSound(asset->get("stage_change.wav").c_str());
coffeeMachineSound = JA_LoadSound(asset->get("title.wav").c_str());
balloonSound = R->getSound("balloon.wav");
bubble1Sound = R->getSound("bubble1.wav");
bubble2Sound = R->getSound("bubble2.wav");
bubble3Sound = R->getSound("bubble3.wav");
bubble4Sound = R->getSound("bubble4.wav");
bulletSound = R->getSound("bullet.wav");
clockSound = R->getSound("clock.wav");
coffeeOutSound = R->getSound("coffeeout.wav");
hiScoreSound = R->getSound("hiscore.wav");
itemDropSound = R->getSound("itemdrop.wav");
itemPickUpSound = R->getSound("itempickup.wav");
playerCollisionSound = R->getSound("player_collision.wav");
powerBallSound = R->getSound("powerball.wav");
stageChangeSound = R->getSound("stage_change.wav");
coffeeMachineSound = R->getSound("title.wav");
// Musicas
gameMusic = JA_LoadMusic(asset->get("playing.ogg").c_str());
gameMusic = R->getMusic("playing.ogg");
if (options->console) {
std::cout << "** RESOURCES FOR GAME SECTION LOADED" << std::endl
@@ -666,26 +504,21 @@ bool Game::loadScoreFile() {
// Carga el fichero de datos para la demo
bool Game::loadDemoFile() {
// Indicador de éxito en la carga
bool success = true;
const std::string p = asset->get("demo.bin");
const std::string filename = p.substr(p.find_last_of("\\/") + 1);
SDL_IOStream *file = SDL_IOFromFile(p.c_str(), "r+b");
// El fichero no existe
if (file == nullptr) {
if (options->console) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
// Lee los datos de la demo desde Resource (precargados al arrancar).
const auto &bytes = Resource::get()->getDemoBytes();
const size_t expected = sizeof(demoKeys_t) * TOTAL_DEMO_DATA;
if (bytes.size() >= expected) {
for (int i = 0; i < TOTAL_DEMO_DATA; ++i) {
memcpy(&demo.dataFile[i], bytes.data() + i * sizeof(demoKeys_t), sizeof(demoKeys_t));
}
// Creamos el fichero para escritura
file = SDL_IOFromFile(p.c_str(), "w+b");
if (file != nullptr) {
if (options->console) {
std::cout << "New file (" << filename.c_str() << ") created!" << std::endl;
std::cout << "Demo data loaded (" << bytes.size() << " bytes)" << std::endl;
}
} else {
// Si no hay datos (bytes vacíos o tamaño inválido), inicializamos a cero.
if (options->console) {
std::cout << "Warning: demo data missing or too small, initializing to zero" << std::endl;
}
// Inicializamos los datos
for (int i = 0; i < TOTAL_DEMO_DATA; ++i) {
demo.keys.left = 0;
demo.keys.right = 0;
@@ -694,32 +527,9 @@ bool Game::loadDemoFile() {
demo.keys.fireLeft = 0;
demo.keys.fireRight = 0;
demo.dataFile[i] = demo.keys;
SDL_WriteIO(file, &demo.dataFile[i], sizeof(demoKeys_t));
}
// Cerramos el fichero
SDL_CloseIO(file);
} else {
if (options->console) {
std::cout << "Error: Unable to create file " << filename.c_str() << std::endl;
}
success = false;
}
}
// El fichero existe
else {
// Cargamos los datos
if (options->console) {
std::cout << "Reading file " << filename.c_str() << std::endl;
}
for (int i = 0; i < TOTAL_DEMO_DATA; ++i)
SDL_ReadIO(file, &demo.dataFile[i], sizeof(demoKeys_t));
// Cierra el fichero
SDL_CloseIO(file);
}
return success;
return true;
}
// Guarda el fichero de puntos
@@ -1690,10 +1500,12 @@ void Game::updateDeath() {
if ((deathCounter == 250) || (deathCounter == 200) || (deathCounter == 180) || (deathCounter == 120) || (deathCounter == 60)) {
// Hace sonar aleatoriamente uno de los 4 sonidos de burbujas
if (!demo.enabled) {
const Uint8 index = rand() % 4;
JA_Sound_t *sound[4] = {bubble1Sound, bubble2Sound, bubble3Sound, bubble4Sound};
JA_PlaySound(sound[index], 0);
}
}
} else {
section->subsection = SUBSECTION_GAME_GAMEOVER;
}
@@ -2373,11 +2185,15 @@ void Game::killPlayer(Player *player) {
if (player->hasExtraHit()) {
player->removeExtraHit();
throwCoffee(player->getPosX() + (player->getWidth() / 2), player->getPosY() + (player->getHeight() / 2));
if (!demo.enabled) {
JA_PlaySound(coffeeOutSound);
}
} else if (deathSequence.phase == DeathPhase::None) {
if (!demo.enabled) {
JA_PauseMusic();
stopAllBalloons(10);
JA_PlaySound(playerCollisionSound);
}
stopAllBalloons(10);
shakeScreen();
deathSequence.phase = DeathPhase::Shaking;
deathSequence.phaseStartTicks = SDL_GetTicks();
@@ -2404,13 +2220,15 @@ void Game::updateDeathSequence() {
case DeathPhase::Waiting:
// Espera 500ms antes de completar la muerte
if (SDL_GetTicks() - deathSequence.phaseStartTicks >= 500) {
if (!demo.enabled) {
JA_PlaySound(coffeeOutSound);
deathSequence.player->setAlive(false);
if (allPlayersAreDead()) {
JA_StopMusic();
} else {
JA_ResumeMusic();
}
}
deathSequence.player->setAlive(false);
deathSequence.phase = DeathPhase::Done;
deathSequence.player = nullptr;
}
@@ -2981,8 +2799,8 @@ void Game::iterate() {
// Si la música no está sonando
if ((JA_GetMusicState() == JA_MUSIC_INVALID) || (JA_GetMusicState() == JA_MUSIC_STOPPED)) {
// Reproduce la música
if (!gameCompleted) {
// Reproduce la música (nunca en modo demo: deja sonar la del título)
if (!gameCompleted && !demo.enabled) {
if (players[0]->isAlive()) {
JA_PlayMusic(gameMusic);
}
@@ -3445,22 +3263,6 @@ void Game::checkEvents() {
}
}
// Carga las animaciones
void Game::loadAnimations(std::string filePath, std::vector<std::string> *buffer) {
std::ifstream file(filePath);
std::string line;
if (file) {
if (options->console) {
std::cout << "Animation loaded: " << filePath.substr(filePath.find_last_of("\\/") + 1).c_str() << std::endl;
}
while (std::getline(file, line)) {
buffer->push_back(line);
}
file.close();
}
}
// Elimina todos los objetos contenidos en vectores
void Game::deleteAllVectorObjects() {
for (auto player : players) {

View File

@@ -535,9 +535,6 @@ class Game {
// Comprueba si todos los jugadores han muerto
bool allPlayersAreDead();
// Carga las animaciones
void loadAnimations(std::string filePath, std::vector<std::string> *buffer);
// Elimina todos los objetos contenidos en vectores
void deleteAllVectorObjects();

View File

@@ -11,6 +11,7 @@
#include "input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_StopMusic
#include "lang.h" // for Lang
#include "resource.h"
#include "screen.h" // for Screen
#include "sprite.h" // for Sprite
#include "text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_SHADOW
@@ -27,29 +28,19 @@ Instructions::Instructions(SDL_Renderer *renderer, Screen *screen, Asset *asset,
this->lang = lang;
this->section = section;
// Reserva memoria para los punteros
Texture *item1 = new Texture(renderer, asset->get("item_points1_disk.png"));
itemTextures.push_back(item1);
Texture *item2 = new Texture(renderer, asset->get("item_points2_gavina.png"));
itemTextures.push_back(item2);
Texture *item3 = new Texture(renderer, asset->get("item_points3_pacmar.png"));
itemTextures.push_back(item3);
Texture *item4 = new Texture(renderer, asset->get("item_clock.png"));
itemTextures.push_back(item4);
Texture *item5 = new Texture(renderer, asset->get("item_coffee.png"));
itemTextures.push_back(item5);
Texture *item6 = new Texture(renderer, asset->get("item_coffee_machine.png"));
itemTextures.push_back(item6);
// Texturas (handles compartidos de Resource)
Resource *R = Resource::get();
itemTextures.push_back(R->getTexture("item_points1_disk.png"));
itemTextures.push_back(R->getTexture("item_points2_gavina.png"));
itemTextures.push_back(R->getTexture("item_points3_pacmar.png"));
itemTextures.push_back(R->getTexture("item_clock.png"));
itemTextures.push_back(R->getTexture("item_coffee.png"));
itemTextures.push_back(R->getTexture("item_coffee_machine.png"));
eventHandler = new SDL_Event();
sprite = new Sprite(0, 0, 16, 16, itemTextures[0], renderer);
text = new Text(asset->get("smb2.png"), asset->get("smb2.txt"), renderer);
text = R->getText("smb2");
// Crea un backbuffer para el renderizador
backbuffer = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
@@ -71,21 +62,21 @@ Instructions::Instructions(SDL_Renderer *renderer, Screen *screen, Asset *asset,
// Destructor
Instructions::~Instructions() {
for (auto texture : itemTextures) {
texture->unload();
delete texture;
}
// itemTextures y text son propiedad de Resource — no liberar.
itemTextures.clear();
delete sprite;
delete eventHandler;
delete text;
SDL_DestroyTexture(backbuffer);
}
// Actualiza las variables
void Instructions::update() {
// Bombea el stream de música: si no se llama, el buffer se vacía y la
// música se corta hasta que volvamos a una escena que sí lo haga.
JA_Update();
// Comprueba las entradas
checkInput();
@@ -243,7 +234,6 @@ void Instructions::checkInput() {
else if (input->checkInput(input_pause, REPEAT_FALSE) || input->checkInput(input_accept, REPEAT_FALSE) || input->checkInput(input_fire_left, REPEAT_FALSE) || input->checkInput(input_fire_center, REPEAT_FALSE) || input->checkInput(input_fire_right, REPEAT_FALSE)) {
if (mode == m_auto) {
JA_StopMusic();
finished = true;
} else {
if (counter > 30) {

View File

@@ -9,6 +9,7 @@
#include "input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_StopMusic, JA_DeleteMusic, JA_LoadMusic
#include "lang.h" // for Lang
#include "resource.h"
#include "screen.h" // for Screen
#include "smartsprite.h" // for SmartSprite
#include "text.h" // for Text
@@ -28,11 +29,9 @@ Intro::Intro(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input,
// Reserva memoria para los objetos
eventHandler = new SDL_Event();
texture = new Texture(renderer, asset->get("intro.png"));
text = new Text(asset->get("nokia.png"), asset->get("nokia.txt"), renderer);
// Carga los recursos
loadMedia();
texture = Resource::get()->getTexture("intro.png");
text = Resource::get()->getText("nokia");
music = Resource::get()->getMusic("intro.ogg");
// Inicializa variables
section->name = SECTION_PROG_INTRO;
@@ -161,25 +160,18 @@ Intro::Intro(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input,
Intro::~Intro() {
delete eventHandler;
texture->unload();
delete texture;
// texture, text, music son propiedad de Resource — no liberar aquí.
for (auto bitmap : bitmaps) {
delete bitmap;
}
for (auto text : texts) {
delete text;
for (auto t : texts) {
delete t;
}
}
JA_DeleteMusic(music);
}
// Carga los recursos
// Carga los recursos (ya no carga nada, se mantiene por si hay callers)
bool Intro::loadMedia() {
// Musicas
music = JA_LoadMusic(asset->get("intro.ogg").c_str());
return true;
}

View File

@@ -1,8 +1,10 @@
#include "lang.h"
#include <fstream> // for basic_ifstream, basic_istream, ifstream
#include <sstream>
#include "asset.h" // for Asset
#include "resource_helper.h"
// Constructor
Lang::Lang(Asset *mAsset) {
@@ -38,16 +40,17 @@ bool Lang::setLang(Uint8 lang) {
for (int i = 0; i < MAX_TEXT_STRINGS; i++)
mTextStrings[i] = "";
bool success = false;
// Lee el fichero via ResourceHelper (pack o filesystem)
auto bytes = ResourceHelper::loadFile(file);
if (bytes.empty()) {
return false;
}
std::ifstream rfile(file);
if (rfile.is_open() && rfile.good()) {
success = true;
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
std::string line;
// lee el resto de datos del fichero
int index = 0;
while (std::getline(rfile, line)) {
while (std::getline(ss, line)) {
// Almacena solo las lineas que no empiezan por # o no esten vacias
const bool test1 = line.substr(0, 1) != "#";
const bool test2 = !line.empty();
@@ -55,10 +58,9 @@ bool Lang::setLang(Uint8 lang) {
mTextStrings[index] = line;
index++;
}
};
}
return success;
return true;
}
// Obtiene la cadena de texto del indice

View File

@@ -9,6 +9,7 @@
#include "const.h" // for bgColor, SECTION_PROG_LOGO, SECTION_PROG...
#include "input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_StopMusic
#include "resource.h"
#include "screen.h" // for Screen
#include "sprite.h" // for Sprite
#include "texture.h" // for Texture
@@ -29,7 +30,7 @@ Logo::Logo(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, s
// Reserva memoria para los punteros
eventHandler = new SDL_Event();
texture = new Texture(renderer, asset->get("logo.png"));
texture = Resource::get()->getTexture("logo.png");
sprite = new Sprite(14, 75, 226, 44, texture, renderer);
// Inicializa variables
@@ -44,9 +45,7 @@ Logo::Logo(SDL_Renderer *renderer, Screen *screen, Asset *asset, Input *input, s
// Destructor
Logo::~Logo() {
texture->unload();
delete texture;
// texture es propiedad de Resource — no liberar aquí.
delete sprite;
delete eventHandler;
}

View File

@@ -7,6 +7,7 @@
#include "asset.h" // for Asset
#include "input.h" // for Input, REPEAT_FALSE, inputs_e
#include "jail_audio.hpp" // for JA_LoadSound, JA_PlaySound, JA_DeleteSound
#include "resource_helper.h"
#include "text.h" // for Text
// Constructor
@@ -61,17 +62,15 @@ Menu::Menu(SDL_Renderer *renderer, Asset *asset, Input *input, std::string file)
selector.itemColor = {0, 0, 0};
selector.a = 255;
// Inicializa las variables desde un fichero
// Inicializa las variables desde un fichero. Si no se pasa fichero, el
// llamante (p.ej. Resource::preloadAll) usará loadFromBytes después —
// y ese método ya llama a setSelectorItemColors() y reset() al final.
if (file != "") {
load(file);
}
// Calcula los colores del selector para el degradado
setSelectorItemColors();
// Deja el cursor en el primer elemento
reset();
}
}
Menu::~Menu() {
renderer = nullptr;
@@ -95,22 +94,13 @@ Menu::~Menu() {
}
}
// Carga la configuración del menu desde un archivo de texto
bool Menu::load(std::string file_path) {
// Indicador de éxito en la carga
// Parser compartido (recibe cualquier istream)
bool Menu::parseFromStream(std::istream &file, const std::string &filename) {
bool success = true;
// Indica si se ha creado ya el objeto de texto
bool textAllocated = false;
const std::string filename = file_path.substr(file_path.find_last_of("\\/") + 1);
std::string line;
std::ifstream file(file_path);
(void)filename;
// El fichero se puede abrir
if (file.good()) {
// Procesa el fichero linea a linea
// std::cout << "Reading file " << filename.c_str() << std::endl;
while (std::getline(file, line)) {
if (line == "[item]") {
item_t item;
@@ -123,54 +113,54 @@ bool Menu::load(std::string file_path) {
item.line = false;
do {
// Lee la siguiente linea
std::getline(file, line);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (!setItem(&item, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
// std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
success = false;
}
} while (line != "[/item]");
addItem(item);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
} else {
int pos = line.find("=");
// Procesa las dos subcadenas
if (!setVars(line.substr(0, pos), line.substr(pos + 1, line.length()))) {
// std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
success = false;
}
// Crea el objeto text tan pronto como se pueda. Necesario para añadir items
// Crea el objeto text tan pronto como se pueda. Necesario para añadir items.
// Carga via ResourceHelper para que funcione tanto con pack como con filesystem.
if (font_png != "" && font_txt != "" && !textAllocated) {
text = new Text(asset->get(font_png), asset->get(font_txt), renderer);
auto pngBytes = ResourceHelper::loadFile(asset->get(font_png));
auto txtBytes = ResourceHelper::loadFile(asset->get(font_txt));
text = new Text(pngBytes, txtBytes, renderer);
textAllocated = true;
}
}
}
// Cierra el fichero
// std::cout << "Closing file " << filename.c_str() << std::endl;
file.close();
}
// El fichero no se puede abrir
else {
// std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
success = false;
}
return success;
}
// Carga la configuración del menu desde un archivo de texto
bool Menu::load(std::string file_path) {
const std::string filename = file_path.substr(file_path.find_last_of("\\/") + 1);
std::ifstream file(file_path);
if (!file.good()) {
return false;
}
return parseFromStream(file, filename);
}
// Carga el menu desde bytes en memoria
bool Menu::loadFromBytes(const std::vector<uint8_t> &bytes, const std::string &nameForLogs) {
if (bytes.empty()) return false;
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
bool ok = parseFromStream(ss, nameForLogs);
setSelectorItemColors();
reset();
return ok;
}
// Asigna variables a partir de dos cadenas
bool Menu::setItem(item_t *item, std::string var, std::string value) {
// Indicador de éxito en la asignación
@@ -228,15 +218,18 @@ bool Menu::setVars(std::string var, std::string value) {
}
else if (var == "sound_cancel") {
soundCancel = JA_LoadSound(asset->get(value).c_str());
auto bytes = ResourceHelper::loadFile(asset->get(value));
if (!bytes.empty()) soundCancel = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
}
else if (var == "sound_accept") {
soundAccept = JA_LoadSound(asset->get(value).c_str());
auto bytes = ResourceHelper::loadFile(asset->get(value));
if (!bytes.empty()) soundAccept = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
}
else if (var == "sound_move") {
soundMove = JA_LoadSound(asset->get(value).c_str());
auto bytes = ResourceHelper::loadFile(asset->get(value));
if (!bytes.empty()) soundMove = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
}
else if (var == "name") {

View File

@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string, basic_string
#include <vector> // for vector
@@ -101,6 +102,9 @@ class Menu {
// Carga la configuración del menu desde un archivo de texto
bool load(std::string file_path);
// Parser compartido (recibe cualquier istream)
bool parseFromStream(std::istream &file, const std::string &filename);
// Asigna variables a partir de dos cadenas
bool setVars(std::string var, std::string value);
@@ -147,6 +151,9 @@ class Menu {
// Destructor
~Menu();
// Carga el menu desde bytes en memoria
bool loadFromBytes(const std::vector<uint8_t> &bytes, const std::string &nameForLogs = "");
// Carga los ficheros de audio
void loadAudioFile(std::string file, int sound);

218
source/resource.cpp Normal file
View File

@@ -0,0 +1,218 @@
#include "resource.h"
#include <algorithm>
#include <filesystem>
#include <iostream>
#include <sstream>
#include "asset.h"
#include "jail_audio.hpp"
#include "menu.h"
#include "resource_helper.h"
#include "text.h"
#include "texture.h"
Resource *Resource::instance_ = nullptr;
static std::string basename(const std::string &path) {
return path.substr(path.find_last_of("\\/") + 1);
}
static std::string stem(const std::string &path) {
std::string b = basename(path);
size_t dot = b.find_last_of('.');
if (dot == std::string::npos) return b;
return b.substr(0, dot);
}
void Resource::init(SDL_Renderer *renderer, Asset *asset, Input *input) {
if (instance_ == nullptr) {
instance_ = new Resource(renderer, asset, input);
instance_->preloadAll();
}
}
void Resource::destroy() {
delete instance_;
instance_ = nullptr;
}
Resource *Resource::get() {
return instance_;
}
Resource::Resource(SDL_Renderer *renderer, Asset *asset, Input *input)
: renderer_(renderer),
asset_(asset),
input_(input) {}
Resource::~Resource() {
for (auto &[name, m] : menus_) delete m;
menus_.clear();
for (auto &[name, t] : texts_) delete t;
texts_.clear();
for (auto &[name, t] : textures_) delete t;
textures_.clear();
for (auto &[name, s] : sounds_) JA_DeleteSound(s);
sounds_.clear();
for (auto &[name, m] : musics_) JA_DeleteMusic(m);
musics_.clear();
}
void Resource::preloadAll() {
const auto &items = asset_->getAll();
// Pass 1: texturas, sonidos, músicas, animaciones (raw lines), demo, lenguajes
for (const auto &it : items) {
if (!ResourceHelper::shouldUseResourcePack(it.file) && it.type != t_lang) {
// Ficheros absolutos (config.txt, score.bin, systemFolder) — no se precargan
continue;
}
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) continue;
const std::string bname = basename(it.file);
switch (it.type) {
case t_bitmap: {
auto *tex = new Texture(renderer_, bytes);
textures_[bname] = tex;
break;
}
case t_sound: {
JA_Sound_t *s = JA_LoadSound(bytes.data(), (uint32_t)bytes.size());
if (s) sounds_[bname] = s;
break;
}
case t_music: {
JA_Music_t *m = JA_LoadMusic(bytes.data(), (Uint32)bytes.size());
if (m) musics_[bname] = m;
break;
}
case t_data: {
if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".ani") {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
std::vector<std::string> lines;
std::string line;
while (std::getline(ss, line)) {
lines.push_back(line);
}
animationLines_[bname] = std::move(lines);
} else if (bname == "demo.bin") {
demoBytes_ = bytes;
} else if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".men") {
// Menús: se construyen en pass 2 porque dependen de textos y sonidos
}
break;
}
case t_font:
// Fonts: se emparejan en pass 2
break;
case t_lang:
// Lenguaje: lo sigue leyendo la clase Lang a través de ResourceHelper
break;
default:
break;
}
}
// Pass 2: Text (fuentes emparejadas png+txt) y Menus (dependen de Text+sonidos)
// Fuentes: construimos un Text por cada par basename.png + basename.txt
// Acumulamos los bytes encontrados por stem (basename sin ext.)
std::unordered_map<std::string, std::vector<uint8_t>> fontPngs;
std::unordered_map<std::string, std::vector<uint8_t>> fontTxts;
for (const auto &it : items) {
if (it.type != t_font) continue;
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) continue;
const std::string s = stem(it.file);
const std::string bname = basename(it.file);
if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".png") {
fontPngs[s] = std::move(bytes);
} else if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".txt") {
fontTxts[s] = std::move(bytes);
}
}
for (const auto &[s, png] : fontPngs) {
auto itTxt = fontTxts.find(s);
if (itTxt == fontTxts.end()) continue;
Text *t = new Text(png, itTxt->second, renderer_);
texts_[s] = t;
}
// Menus: usan aún Menu::loadFromBytes que internamente llama a asset->get() y
// Text/JA_LoadSound por path. Funciona en modo fallback; en pack estricto
// requiere que Menu se adapte a cargar desde ResourceHelper. Por ahora
// lo dejamos así y será una migración del paso 7.
for (const auto &it : items) {
if (it.type != t_data) continue;
const std::string bname = basename(it.file);
if (bname.size() < 4 || bname.substr(bname.size() - 4) != ".men") continue;
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) continue;
Menu *m = new Menu(renderer_, asset_, input_, "");
m->loadFromBytes(bytes, bname);
const std::string s = stem(it.file);
menus_[s] = m;
}
}
Texture *Resource::getTexture(const std::string &name) {
auto it = textures_.find(name);
if (it == textures_.end()) {
std::cerr << "Resource::getTexture: missing " << name << '\n';
return nullptr;
}
return it->second;
}
JA_Sound_t *Resource::getSound(const std::string &name) {
auto it = sounds_.find(name);
if (it == sounds_.end()) {
std::cerr << "Resource::getSound: missing " << name << '\n';
return nullptr;
}
return it->second;
}
JA_Music_t *Resource::getMusic(const std::string &name) {
auto it = musics_.find(name);
if (it == musics_.end()) {
std::cerr << "Resource::getMusic: missing " << name << '\n';
return nullptr;
}
return it->second;
}
std::vector<std::string> &Resource::getAnimationLines(const std::string &name) {
auto it = animationLines_.find(name);
if (it == animationLines_.end()) {
static std::vector<std::string> empty;
std::cerr << "Resource::getAnimationLines: missing " << name << '\n';
return empty;
}
return it->second;
}
Text *Resource::getText(const std::string &name) {
auto it = texts_.find(name);
if (it == texts_.end()) {
std::cerr << "Resource::getText: missing " << name << '\n';
return nullptr;
}
return it->second;
}
Menu *Resource::getMenu(const std::string &name) {
auto it = menus_.find(name);
if (it == menus_.end()) {
std::cerr << "Resource::getMenu: missing " << name << '\n';
return nullptr;
}
return it->second;
}

53
source/resource.h Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
class Asset;
class Input;
class Menu;
class Text;
class Texture;
struct JA_Music_t;
struct JA_Sound_t;
// Precarga y posee todos los recursos del juego durante toda la vida de la app.
// Singleton inicializado desde Director; las escenas consultan handles via get*().
class Resource {
public:
static void init(SDL_Renderer *renderer, Asset *asset, Input *input);
static void destroy();
static Resource *get();
Texture *getTexture(const std::string &name);
JA_Sound_t *getSound(const std::string &name);
JA_Music_t *getMusic(const std::string &name);
std::vector<std::string> &getAnimationLines(const std::string &name);
Text *getText(const std::string &name); // name sin extensión: "smb2", "nokia2", ...
Menu *getMenu(const std::string &name); // name sin extensión: "title", "options", ...
const std::vector<uint8_t> &getDemoBytes() const { return demoBytes_; }
private:
Resource(SDL_Renderer *renderer, Asset *asset, Input *input);
~Resource();
void preloadAll();
SDL_Renderer *renderer_;
Asset *asset_;
Input *input_;
std::unordered_map<std::string, Texture *> textures_;
std::unordered_map<std::string, JA_Sound_t *> sounds_;
std::unordered_map<std::string, JA_Music_t *> musics_;
std::unordered_map<std::string, std::vector<std::string>> animationLines_;
std::unordered_map<std::string, Text *> texts_;
std::unordered_map<std::string, Menu *> menus_;
std::vector<uint8_t> demoBytes_;
static Resource *instance_;
};

View File

@@ -0,0 +1,77 @@
#include "resource_helper.h"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <iostream>
#include "resource_loader.h"
namespace ResourceHelper {
static bool resource_system_initialized = false;
bool initializeResourceSystem(const std::string& pack_file, bool enable_fallback) {
auto& loader = ResourceLoader::getInstance();
bool ok = loader.initialize(pack_file, enable_fallback);
resource_system_initialized = ok;
if (ok && loader.getLoadedResourceCount() > 0) {
std::cout << "Resource system initialized with pack: " << pack_file << '\n';
} else if (ok) {
std::cout << "Resource system using fallback mode (filesystem only)" << '\n';
}
return ok;
}
void shutdownResourceSystem() {
if (resource_system_initialized) {
ResourceLoader::getInstance().shutdown();
resource_system_initialized = false;
}
}
std::vector<uint8_t> loadFile(const std::string& filepath) {
if (resource_system_initialized && shouldUseResourcePack(filepath)) {
auto& loader = ResourceLoader::getInstance();
std::string pack_path = getPackPath(filepath);
auto data = loader.loadResource(pack_path);
if (!data.empty()) {
return data;
}
}
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
return {};
}
return data;
}
bool shouldUseResourcePack(const std::string& filepath) {
// Solo entran al pack los ficheros dentro de data/
return filepath.find("data/") != std::string::npos;
}
std::string getPackPath(const std::string& asset_path) {
std::string pack_path = asset_path;
std::replace(pack_path.begin(), pack_path.end(), '\\', '/');
// Toma la última aparición de "data/" como prefijo a quitar
size_t last_data = pack_path.rfind("data/");
if (last_data != std::string::npos) {
pack_path = pack_path.substr(last_data + 5);
}
return pack_path;
}
}

15
source/resource_helper.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace ResourceHelper {
bool initializeResourceSystem(const std::string& pack_file = "resources.pack", bool enable_fallback = true);
void shutdownResourceSystem();
std::vector<uint8_t> loadFile(const std::string& filepath);
bool shouldUseResourcePack(const std::string& filepath);
std::string getPackPath(const std::string& asset_path);
}

133
source/resource_loader.cpp Normal file
View File

@@ -0,0 +1,133 @@
#include "resource_loader.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include "resource_pack.h"
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
ResourceLoader::ResourceLoader()
: resource_pack_(nullptr),
fallback_to_files_(true) {}
ResourceLoader& ResourceLoader::getInstance() {
if (!instance) {
instance = std::unique_ptr<ResourceLoader>(new ResourceLoader());
}
return *instance;
}
ResourceLoader::~ResourceLoader() {
shutdown();
}
bool ResourceLoader::initialize(const std::string& pack_file, bool enable_fallback) {
shutdown();
fallback_to_files_ = enable_fallback;
pack_path_ = pack_file;
if (std::filesystem::exists(pack_file)) {
resource_pack_ = new ResourcePack();
if (resource_pack_->loadPack(pack_file)) {
return true;
}
delete resource_pack_;
resource_pack_ = nullptr;
std::cerr << "Failed to load resource pack: " << pack_file << '\n';
}
if (fallback_to_files_) {
return true;
}
std::cerr << "Resource pack not found and fallback disabled: " << pack_file << '\n';
return false;
}
void ResourceLoader::shutdown() {
if (resource_pack_ != nullptr) {
delete resource_pack_;
resource_pack_ = nullptr;
}
}
std::vector<uint8_t> ResourceLoader::loadResource(const std::string& filename) {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return resource_pack_->getResource(filename);
}
if (fallback_to_files_) {
return loadFromFile(filename);
}
std::cerr << "Resource not found: " << filename << '\n';
return {};
}
bool ResourceLoader::resourceExists(const std::string& filename) {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return true;
}
if (fallback_to_files_) {
std::string full_path = getDataPath(filename);
return std::filesystem::exists(full_path);
}
return false;
}
std::vector<uint8_t> ResourceLoader::loadFromFile(const std::string& filename) {
std::string full_path = getDataPath(filename);
std::ifstream file(full_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << full_path << '\n';
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << full_path << '\n';
return {};
}
return data;
}
std::string ResourceLoader::getDataPath(const std::string& filename) {
return "data/" + filename;
}
size_t ResourceLoader::getLoadedResourceCount() const {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceCount();
}
return 0;
}
std::vector<std::string> ResourceLoader::getAvailableResources() const {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceList();
}
std::vector<std::string> result;
if (fallback_to_files_ && std::filesystem::exists("data")) {
for (const auto& entry : std::filesystem::recursive_directory_iterator("data")) {
if (entry.is_regular_file()) {
std::string filename = std::filesystem::relative(entry.path(), "data").string();
std::replace(filename.begin(), filename.end(), '\\', '/');
result.push_back(filename);
}
}
}
return result;
}

39
source/resource_loader.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class ResourcePack;
class ResourceLoader {
private:
static std::unique_ptr<ResourceLoader> instance;
ResourcePack* resource_pack_;
std::string pack_path_;
bool fallback_to_files_;
ResourceLoader();
public:
static ResourceLoader& getInstance();
~ResourceLoader();
bool initialize(const std::string& pack_file, bool enable_fallback = true);
void shutdown();
std::vector<uint8_t> loadResource(const std::string& filename);
bool resourceExists(const std::string& filename);
void setFallbackToFiles(bool enable) { fallback_to_files_ = enable; }
bool getFallbackToFiles() const { return fallback_to_files_; }
size_t getLoadedResourceCount() const;
std::vector<std::string> getAvailableResources() const;
private:
static std::vector<uint8_t> loadFromFile(const std::string& filename);
static std::string getDataPath(const std::string& filename);
};

223
source/resource_pack.cpp Normal file
View File

@@ -0,0 +1,223 @@
#include "resource_pack.h"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <utility>
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "CCRS_RESOURCES__2026";
ResourcePack::ResourcePack()
: loaded_(false) {}
ResourcePack::~ResourcePack() {
clear();
}
uint32_t ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) {
uint32_t checksum = 0x12345678;
for (unsigned char i : data) {
checksum = ((checksum << 5) + checksum) + i;
}
return checksum;
}
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) return;
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
encryptData(data, key);
}
bool ResourcePack::loadPack(const std::string& pack_file) {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open pack file: " << pack_file << '\n';
return false;
}
std::array<char, 4> header;
file.read(header.data(), 4);
if (std::string(header.data(), 4) != "CCRS") {
std::cerr << "Error: Invalid pack file format" << '\n';
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != 1) {
std::cerr << "Error: Unsupported pack version: " << version << '\n';
return false;
}
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (uint32_t i = 0; i < resource_count; ++i) {
uint32_t filename_length;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
decryptData(data_, DEFAULT_ENCRYPT_KEY);
loaded_ = true;
return true;
}
bool ResourcePack::savePack(const std::string& pack_file) {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not create pack file: " << pack_file << '\n';
return false;
}
file.write("CCRS", 4);
uint32_t version = 1;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
uint32_t resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
uint32_t filename_length = static_cast<uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
uint64_t data_size = encrypted_data.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
return true;
}
bool ResourcePack::addFile(const std::string& filename, const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << filepath << '\n';
return false;
}
ResourceEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
bool ResourcePack::addDirectory(const std::string& directory) {
if (!std::filesystem::exists(directory)) {
std::cerr << "Error: Directory does not exist: " << directory << '\n';
return false;
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (entry.is_regular_file()) {
std::string filepath = entry.path().string();
std::string filename = std::filesystem::relative(entry.path(), directory).string();
std::replace(filename.begin(), filename.end(), '\\', '/');
if (!addFile(filename, filepath)) {
return false;
}
}
}
return true;
}
std::vector<uint8_t> ResourcePack::getResource(const std::string& filename) {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "Error: Resource not found: " << filename << '\n';
return {};
}
const ResourceEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "Error: Invalid resource data: " << filename << '\n';
return {};
}
std::vector<uint8_t> result(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "Warning: Checksum mismatch for resource: " << filename << '\n';
}
return result;
}
bool ResourcePack::hasResource(const std::string& filename) const {
return resources_.find(filename) != resources_.end();
}
void ResourcePack::clear() {
resources_.clear();
data_.clear();
loaded_ = false;
}
size_t ResourcePack::getResourceCount() const {
return resources_.size();
}
std::vector<std::string> ResourcePack::getResourceList() const {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}

44
source/resource_pack.h Normal file
View File

@@ -0,0 +1,44 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
struct ResourceEntry {
std::string filename;
uint64_t offset;
uint64_t size;
uint32_t checksum;
};
class ResourcePack {
private:
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
bool loaded_;
static uint32_t calculateChecksum(const std::vector<uint8_t>& data);
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
public:
ResourcePack();
~ResourcePack();
bool loadPack(const std::string& pack_file);
bool savePack(const std::string& pack_file);
bool addFile(const std::string& filename, const std::string& filepath);
bool addDirectory(const std::string& directory);
std::vector<uint8_t> getResource(const std::string& filename);
bool hasResource(const std::string& filename) const;
void clear();
size_t getResourceCount() const;
std::vector<std::string> getResourceList() const;
static const std::string DEFAULT_ENCRYPT_KEY;
};

View File

@@ -8,6 +8,7 @@
#include "asset.h" // for Asset
#include "mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime
#include "resource.h"
#include "text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_STROKE
#ifdef __EMSCRIPTEN__
@@ -82,8 +83,8 @@ Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options
// Establece el modo de video
setVideoMode(options->videoMode != 0);
// Inicializa el sistema de notificaciones
notificationText = new Text(asset->get("8bithud.png"), asset->get("8bithud.txt"), renderer);
// Inicializa el sistema de notificaciones (Text compartido de Resource)
notificationText = Resource::get()->getText("8bithud");
notificationMessage = "";
notificationTextColor = {0xFF, 0xFF, 0xFF};
notificationOutlineColor = {0x00, 0x00, 0x00};
@@ -96,7 +97,7 @@ Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options
// Destructor
Screen::~Screen() {
delete notificationText;
// notificationText es propiedad de Resource — no liberar.
SDL_DestroyTexture(gameCanvas);
}

View File

@@ -3,30 +3,15 @@
#include <fstream> // for char_traits, basic_ostream, basic_ifstream, ope...
#include <iostream> // for cout
#include <sstream>
#include "sprite.h" // for Sprite
#include "texture.h" // for Texture
#include "utils.h" // for color_t
// Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose) {
textFile_t tf;
// Inicializa a cero el vector con las coordenadas
for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0;
tf.offset[i].y = 0;
tf.offset[i].w = 0;
}
// Abre el fichero para leer los valores
const std::string filename = file.substr(file.find_last_of("\\/") + 1).c_str();
std::ifstream rfile(file);
if (rfile.is_open() && rfile.good()) {
// Parser compartido: rellena un textFile_t desde cualquier istream
static void parseTextFileStream(std::istream &rfile, textFile_t &tf) {
std::string buffer;
// Lee los dos primeros valores del fichero
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.boxWidth = std::stoi(buffer);
@@ -35,40 +20,65 @@ textFile_t LoadTextFile(std::string file, bool verbose) {
std::getline(rfile, buffer);
tf.boxHeight = std::stoi(buffer);
// lee el resto de datos del fichero
int index = 32;
int line_read = 0;
while (std::getline(rfile, buffer)) {
// Almacena solo las lineas impares
if (line_read % 2 == 1) {
tf.offset[index++].w = std::stoi(buffer);
}
// Limpia el buffer
buffer.clear();
line_read++;
};
// Cierra el fichero
if (verbose) {
std::cout << "Text loaded: " << filename.c_str() << std::endl;
}
rfile.close();
}
// El fichero no se puede abrir
else {
if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
}
// Establece las coordenadas para cada caracter ascii de la cadena y su ancho
static void computeTextFileOffsets(textFile_t &tf) {
for (int i = 32; i < 128; ++i) {
tf.offset[i].x = ((i - 32) % 15) * tf.boxWidth;
tf.offset[i].y = ((i - 32) / 15) * tf.boxHeight;
}
}
// Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose) {
textFile_t tf;
for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0;
tf.offset[i].y = 0;
tf.offset[i].w = 0;
}
const std::string filename = file.substr(file.find_last_of("\\/") + 1).c_str();
std::ifstream rfile(file);
if (rfile.is_open() && rfile.good()) {
parseTextFileStream(rfile, tf);
if (verbose) {
std::cout << "Text loaded: " << filename.c_str() << std::endl;
}
} else if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
computeTextFileOffsets(tf);
return tf;
}
// Llena una estructura textFile_t desde bytes en memoria
textFile_t LoadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose) {
textFile_t tf;
for (int i = 0; i < 128; ++i) {
tf.offset[i].x = 0;
tf.offset[i].y = 0;
tf.offset[i].w = 0;
}
if (!bytes.empty()) {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
parseTextFileStream(ss, tf);
if (verbose) {
std::cout << "Text loaded from memory" << std::endl;
}
}
computeTextFileOffsets(tf);
return tf;
}
@@ -135,6 +145,24 @@ Text::Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer) {
fixedWidth = false;
}
// Constructor desde bytes
Text::Text(const std::vector<uint8_t> &pngBytes, const std::vector<uint8_t> &txtBytes, SDL_Renderer *renderer) {
textFile_t tf = LoadTextFileFromMemory(txtBytes);
boxHeight = tf.boxHeight;
boxWidth = tf.boxWidth;
for (int i = 0; i < 128; ++i) {
offset[i].x = tf.offset[i].x;
offset[i].y = tf.offset[i].y;
offset[i].w = tf.offset[i].w;
}
// Crea la textura desde bytes (Text es dueño en este overload)
texture = new Texture(renderer, pngBytes);
sprite = new Sprite({0, 0, boxWidth, boxHeight}, texture, renderer);
fixedWidth = false;
}
// Destructor
Text::~Text() {
delete sprite;

View File

@@ -2,7 +2,9 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string
#include <vector>
class Sprite;
class Texture;
#include "utils.h"
@@ -28,6 +30,9 @@ struct textFile_t {
// Llena una estructuta textFile_t desde un fichero
textFile_t LoadTextFile(std::string file, bool verbose = false);
// Llena una estructura textFile_t desde bytes en memoria
textFile_t LoadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose = false);
// Clase texto. Pinta texto en pantalla a partir de un bitmap
class Text {
private:
@@ -47,6 +52,9 @@ class Text {
Text(std::string textFile, Texture *texture, SDL_Renderer *renderer);
Text(textFile_t *textFile, Texture *texture, SDL_Renderer *renderer);
// Constructor desde bytes en memoria: comparte ownership del texture (no lo libera)
Text(const std::vector<uint8_t> &pngBytes, const std::vector<uint8_t> &txtBytes, SDL_Renderer *renderer);
// Destructor
~Text();

View File

@@ -31,75 +31,86 @@ Texture::Texture(SDL_Renderer *renderer, std::string path, bool verbose) {
}
}
// Constructor desde bytes
Texture::Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose) {
this->renderer = renderer;
this->path = "";
texture = nullptr;
width = 0;
height = 0;
if (!bytes.empty()) {
loadFromMemory(bytes.data(), bytes.size(), renderer, verbose);
}
}
// Destructor
Texture::~Texture() {
// Libera memoria
unload();
}
// Helper: convierte píxeles RGBA decodificados por stbi en SDL_Texture
static SDL_Texture *createTextureFromPixels(SDL_Renderer *renderer, unsigned char *data, int w, int h, int *out_w, int *out_h) {
const int pitch = 4 * w;
SDL_Surface *loadedSurface = SDL_CreateSurfaceFrom(w, h, SDL_PIXELFORMAT_RGBA32, (void *)data, pitch);
if (loadedSurface == nullptr) {
return nullptr;
}
SDL_Texture *newTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
if (newTexture != nullptr) {
*out_w = loadedSurface->w;
*out_h = loadedSurface->h;
SDL_SetTextureScaleMode(newTexture, Texture::currentScaleMode);
}
SDL_DestroySurface(loadedSurface);
return newTexture;
}
// Carga una imagen desde un fichero
bool Texture::loadFromFile(std::string path, SDL_Renderer *renderer, bool verbose) {
const std::string filename = path.substr(path.find_last_of("\\/") + 1);
int req_format = STBI_rgb_alpha;
int width, height, orig_format;
unsigned char *data = stbi_load(path.c_str(), &width, &height, &orig_format, req_format);
int w, h, orig_format;
unsigned char *data = stbi_load(path.c_str(), &w, &h, &orig_format, req_format);
if (data == nullptr) {
SDL_Log("Loading image failed: %s", stbi_failure_reason());
exit(1);
} else {
if (verbose) {
} else if (verbose) {
std::cout << "Image loaded: " << filename.c_str() << std::endl;
}
}
int pitch;
SDL_PixelFormat pixel_format;
if (req_format == STBI_rgb) {
pitch = 3 * width; // 3 bytes por pixel * pixels per linea
pixel_format = SDL_PIXELFORMAT_RGB24;
} else { // STBI_rgb_alpha (RGBA)
pitch = 4 * width;
pixel_format = SDL_PIXELFORMAT_RGBA32;
}
// Limpia
unload();
// La textura final
SDL_Texture *newTexture = nullptr;
// Carga la imagen desde una ruta específica
SDL_Surface *loadedSurface = SDL_CreateSurfaceFrom(width, height, pixel_format, (void *)data, pitch);
if (loadedSurface == nullptr) {
if (verbose) {
SDL_Texture *newTexture = createTextureFromPixels(renderer, data, w, h, &this->width, &this->height);
if (newTexture == nullptr && verbose) {
std::cout << "Unable to load image " << path.c_str() << std::endl;
}
} else {
// Crea la textura desde los pixels de la surface
newTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
if (newTexture == nullptr) {
if (verbose) {
std::cout << "Unable to create texture from " << path.c_str() << "! SDL Error: " << SDL_GetError() << std::endl;
}
} else {
// Obtiene las dimensiones de la imagen
this->width = loadedSurface->w;
this->height = loadedSurface->h;
// Aplica el modo de escalado
SDL_SetTextureScaleMode(newTexture, currentScaleMode);
}
// Elimina la textura cargada
SDL_DestroySurface(loadedSurface);
}
// Return success
stbi_image_free(data);
texture = newTexture;
return texture != nullptr;
}
// Carga una imagen desde bytes en memoria
bool Texture::loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose) {
int w, h, orig_format;
unsigned char *pixels = stbi_load_from_memory(data, (int)size, &w, &h, &orig_format, STBI_rgb_alpha);
if (pixels == nullptr) {
SDL_Log("Loading image from memory failed: %s", stbi_failure_reason());
return false;
}
unload();
SDL_Texture *newTexture = createTextureFromPixels(renderer, pixels, w, h, &this->width, &this->height);
if (newTexture == nullptr && verbose) {
std::cout << "Unable to create texture from memory" << std::endl;
}
stbi_image_free(pixels);
texture = newTexture;
return texture != nullptr;
}
// Crea una textura en blanco
bool Texture::createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess access) {
// Crea una textura sin inicializar

View File

@@ -2,7 +2,9 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for basic_string, string
#include <vector>
class Texture {
private:
@@ -24,12 +26,18 @@ class Texture {
// Constructor
Texture(SDL_Renderer *renderer, std::string path = "", bool verbose = false);
// Constructor desde bytes (PNG en memoria)
Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose = false);
// Destructor
~Texture();
// Carga una imagen desde un fichero
bool loadFromFile(std::string path, SDL_Renderer *renderer, bool verbose = false);
// Carga una imagen desde bytes en memoria
bool loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose = false);
// Crea una textura en blanco
bool createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess = SDL_TEXTUREACCESS_STREAMING);

View File

@@ -15,6 +15,7 @@
#include "jail_audio.hpp" // for JA_StopMusic, JA_GetMusicState, JA_Play...
#include "lang.h" // for Lang, ba_BA, en_UK, es_ES
#include "menu.h" // for Menu
#include "resource.h"
#include "screen.h" // for Screen, FILTER_LINEAL, FILTER_NEAREST
#include "smartsprite.h" // for SmartSprite
#include "sprite.h" // for Sprite
@@ -36,28 +37,29 @@ Title::Title(SDL_Renderer *renderer, Screen *screen, Input *input, Asset *asset,
eventHandler = new SDL_Event();
fade = new Fade(renderer);
dustTexture = new Texture(renderer, asset->get("title_dust.png"));
coffeeTexture = new Texture(renderer, asset->get("title_coffee.png"));
crisisTexture = new Texture(renderer, asset->get("title_crisis.png"));
gradientTexture = new Texture(renderer, asset->get("title_gradient.png"));
Resource *R = Resource::get();
dustTexture = R->getTexture("title_dust.png");
coffeeTexture = R->getTexture("title_coffee.png");
crisisTexture = R->getTexture("title_crisis.png");
gradientTexture = R->getTexture("title_gradient.png");
coffeeBitmap = new SmartSprite(coffeeTexture, renderer);
crisisBitmap = new SmartSprite(crisisTexture, renderer);
dustBitmapL = new AnimatedSprite(dustTexture, renderer, asset->get("title_dust.ani"));
dustBitmapR = new AnimatedSprite(dustTexture, renderer, asset->get("title_dust.ani"));
dustBitmapL = new AnimatedSprite(dustTexture, renderer, "", &R->getAnimationLines("title_dust.ani"));
dustBitmapR = new AnimatedSprite(dustTexture, renderer, "", &R->getAnimationLines("title_dust.ani"));
gradient = new Sprite({0, 0, 256, 192}, gradientTexture, renderer);
text1 = new Text(asset->get("smb2.png"), asset->get("smb2.txt"), renderer);
text2 = new Text(asset->get("8bithud.png"), asset->get("8bithud.txt"), renderer);
text1 = R->getText("smb2");
text2 = R->getText("8bithud");
#ifdef GAME_CONSOLE
menu.title = new Menu(renderer, asset, input, asset->get("title_gc.men"));
menu.options = new Menu(renderer, asset, input, asset->get("options_gc.men"));
menu.title = R->getMenu("title_gc");
menu.options = R->getMenu("options_gc");
#else
menu.title = new Menu(renderer, asset, input, asset->get("title.men"));
menu.options = new Menu(renderer, asset, input, asset->get("options.men"));
menu.title = R->getMenu("title");
menu.options = R->getMenu("options");
#endif
menu.playerSelect = new Menu(renderer, asset, input, asset->get("player_select.men"));
menu.playerSelect = R->getMenu("player_select");
#ifdef __EMSCRIPTEN__
// En la versión web no se puede cerrar el programa: ocultamos la opción QUIT del menú de título
@@ -65,11 +67,9 @@ Title::Title(SDL_Renderer *renderer, Screen *screen, Input *input, Asset *asset,
menu.title->setSelectable(3, false);
#endif
// Sonidos
crashSound = JA_LoadSound(asset->get("title.wav").c_str());
// Musicas
titleMusic = JA_LoadMusic(asset->get("title.ogg").c_str());
// Sonidos y música (handles compartidos)
crashSound = R->getSound("title.wav");
titleMusic = R->getMusic("title.ogg");
// Inicializa los valores
init();
@@ -80,34 +80,14 @@ Title::~Title() {
delete eventHandler;
delete fade;
dustTexture->unload();
delete dustTexture;
coffeeTexture->unload();
delete coffeeTexture;
crisisTexture->unload();
delete crisisTexture;
gradientTexture->unload();
delete gradientTexture;
// Las texturas, Text, Menu, sonido y música son propiedad de Resource —
// no se liberan aquí. Solo los sprites que posee esta escena.
delete coffeeBitmap;
delete crisisBitmap;
delete dustBitmapL;
delete dustBitmapR;
delete gradient;
delete text1;
delete text2;
delete menu.title;
delete menu.options;
delete menu.playerSelect;
JA_DeleteSound(crashSound);
JA_DeleteMusic(titleMusic);
SDL_DestroyTexture(background);
}

View File

@@ -0,0 +1,52 @@
# Makefile para la herramienta de empaquetado de Coffee Crisis
# =============================================================
CXX := g++
CXXFLAGS := -std=c++20 -Wall -Os -I../../source
SOURCES := pack_resources.cpp ../../source/resource_pack.cpp
TARGET := pack_resources
CLEAN_FILES := pack_resources *.pack *.o
ifeq ($(OS),Windows_NT)
DETECTED_OS := Windows
TARGET := $(TARGET).exe
CLEAN_CMD := del /Q
FixPath = $(subst /,\\,$1)
else
DETECTED_OS := $(shell uname -s)
CLEAN_CMD := rm -f
FixPath = $1
endif
.PHONY: all pack_tool pack clean help
all: pack_tool
pack_tool:
@echo "Compilando herramienta de empaquetado para $(DETECTED_OS)..."
$(CXX) $(CXXFLAGS) $(SOURCES) -o $(TARGET)
@echo "Herramienta compilada: $(TARGET)"
clean:
@echo "Limpiando archivos generados..."
$(CLEAN_CMD) $(call FixPath,$(CLEAN_FILES))
@echo "Archivos limpiados"
# Crear pack final (invocado desde el Makefile raíz)
pack: pack_tool
ifeq ($(OS),Windows_NT)
.\$(TARGET) ..\..\data ..\..\resources.pack
else
./$(TARGET) ../../data ../../resources.pack
endif
help:
@echo "Makefile para herramientas de Coffee Crisis"
@echo "==========================================="
@echo ""
@echo "Comandos disponibles:"
@echo " all - Compilar herramienta de empaquetado (por defecto)"
@echo " pack_tool - Compilar herramienta de empaquetado"
@echo " pack - Crear pack final en ../../resources.pack"
@echo " clean - Limpiar archivos generados"
@echo " help - Mostrar esta ayuda"

View File

@@ -0,0 +1,104 @@
#include <filesystem>
#include <iostream>
#include "resource_pack.h"
static constexpr const char* APP_NAME = "Coffee Crisis";
void showHelp() {
std::cout << APP_NAME << " - Resource Packer" << '\n';
std::cout << "===============================================" << '\n';
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]" << '\n';
std::cout << '\n';
std::cout << "Options:" << '\n';
std::cout << " --help Show this help message" << '\n';
std::cout << " --list List contents of an existing pack file" << '\n';
std::cout << '\n';
std::cout << "Arguments:" << '\n';
std::cout << " input_dir Directory to pack (default: data)" << '\n';
std::cout << " output_file Pack file name (default: resources.pack)" << '\n';
}
void listPackContents(const std::string& packFile) {
ResourcePack pack;
if (!pack.loadPack(packFile)) {
std::cerr << "Error: Cannot open pack file: " << packFile << '\n';
return;
}
auto resources = pack.getResourceList();
std::cout << "Pack file: " << packFile << '\n';
std::cout << "Resources: " << resources.size() << '\n';
std::cout << "Contents:" << '\n';
for (const auto& resource : resources) {
std::cout << " " << resource << '\n';
}
}
int main(int argc, char* argv[]) {
std::string dataDir = "data";
std::string outputFile = "resources.pack";
bool listMode = false;
bool dataDirSet = false;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
showHelp();
return 0;
} else if (arg == "--list") {
listMode = true;
if (i + 1 < argc) {
outputFile = argv[++i];
}
} else if (!arg.empty() && arg[0] != '-') {
if (!dataDirSet) {
dataDir = arg;
dataDirSet = true;
} else {
outputFile = arg;
}
}
}
if (listMode) {
listPackContents(outputFile);
return 0;
}
std::cout << APP_NAME << " - Resource Packer" << '\n';
std::cout << "===============================================" << '\n';
std::cout << "Input directory: " << dataDir << '\n';
std::cout << "Output file: " << outputFile << '\n';
std::cout << '\n';
if (!std::filesystem::exists(dataDir)) {
std::cerr << "Error: Input directory does not exist: " << dataDir << '\n';
return 1;
}
ResourcePack pack;
std::cout << "Scanning and packing resources..." << '\n';
if (!pack.addDirectory(dataDir)) {
std::cerr << "Error: Failed to add directory to pack" << '\n';
return 1;
}
std::cout << "Found " << pack.getResourceCount() << " resources" << '\n';
std::cout << "Saving pack file..." << '\n';
if (!pack.savePack(outputFile)) {
std::cerr << "Error: Failed to save pack file" << '\n';
return 1;
}
std::filesystem::path packPath(outputFile);
auto fileSize = std::filesystem::file_size(packPath);
std::cout << "Pack file created successfully!" << '\n';
std::cout << "File size: " << (fileSize / 1024.0 / 1024.0) << " MB" << '\n';
return 0;
}