#include "game/gameplay/room_format.hpp" #include // Para exception #include // Para cout, cerr #include "core/resources/resource_helper.hpp" // Para Resource::Helper #include "external/fkyaml_node.hpp" // Para fkyaml::node #include "game/gameplay/zone.hpp" // Para Zone::Data #include "game/gameplay/zone_manager.hpp" // Para ZoneManager #include "utils/defines.hpp" // Para Tile::SIZE, Map::WIDTH/HEIGHT #include "utils/utils.hpp" // Para safeStoi #ifdef _DEBUG #include // Para std::round #include // Para ofstream #include // Para ostringstream #endif // ============================================================================ // Helpers no-miembro (parser) // ============================================================================ namespace { // Lee un nodo de color como Uint8 (acepta entero directo o string numérico) auto readColorNode(const fkyaml::node& node) -> Uint8 { if (node.is_integer()) { return static_cast(node.get_value()); } if (node.is_string()) { return static_cast(safeStoi(node.get_value(), 0)); } return 0; } // Lee un array 2D de enteros desde un nodo YAML auto readTilemap2D(const fkyaml::node& node) -> std::vector> { std::vector> tilemap_2d; tilemap_2d.reserve(Map::HEIGHT); for (const auto& row_node : node) { std::vector row; row.reserve(Map::WIDTH); for (const auto& tile_node : row_node) { row.push_back(tile_node.get_value()); } tilemap_2d.push_back(row); } return tilemap_2d; } } // namespace // ============================================================================ // Conversores básicos // ============================================================================ auto RoomFormat::convertRoomConnection(const std::string& value) -> std::string { if (value == "null" || value.empty()) { return "0"; } if (value.size() > 5 && value.substr(value.size() - 5) == ".yaml") { return value; } return value + ".yaml"; } auto RoomFormat::convertAutoSurface(const fkyaml::node& node) -> int { if (node.is_integer()) { return node.get_value(); } if (node.is_string()) { const auto VALUE = node.get_value(); if (VALUE == "left") { return -1; } if (VALUE == "right") { return 1; } } return 0; } auto RoomFormat::flattenTilemap(const std::vector>& tilemap_2d) -> std::vector { std::vector tilemap_flat; tilemap_flat.reserve(Map::WIDTH * Map::HEIGHT); for (const auto& row : tilemap_2d) { for (int tile : row) { tilemap_flat.push_back(tile); } } return tilemap_flat; } // ============================================================================ // Parseo de la sección room // ============================================================================ void RoomFormat::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) { if (!yaml.contains("room")) { return; } const auto& room_node = yaml["room"]; // Extract room number from filename (e.g., "01.yaml" → "01") room.number = file_name.substr(0, file_name.find_last_of('.')); // --- Resolución de zona + overrides (tileSetFile, music) --- std::string zone_name; if (room_node.contains("zone")) { zone_name = room_node["zone"].get_value(); } else { std::cerr << "Warning: room " << file_name << " has no 'zone' field, using default\n"; const Zone::Data* default_zone = ZoneManager::get()->getDefaultZone(); if (default_zone != nullptr) { zone_name = default_zone->name; } } room.zone = zone_name; const Zone::Data* zone = ZoneManager::get()->getZone(zone_name); if (zone == nullptr) { std::cerr << "Warning: unknown zone '" << zone_name << "' in " << file_name << ", using default\n"; zone = ZoneManager::get()->getDefaultZone(); } // tileSetFile: zona, override si está en el yaml if (room_node.contains("tileSetFile")) { room.tile_set_file = room_node["tileSetFile"].get_value(); room.tile_set_overridden = true; } else if (zone != nullptr) { room.tile_set_file = zone->tile_set_file; } // music: zona, override si está en el yaml if (room_node.contains("music")) { room.music = room_node["music"].get_value(); room.music_overridden = true; } else if (zone != nullptr) { room.music = zone->music; } // Room connections if (room_node.contains("connections")) { parseRoomConnections(room_node["connections"], room); } // Item colors if (room_node.contains("itemColor1")) { room.item_color1 = readColorNode(room_node["itemColor1"]); } if (room_node.contains("itemColor2")) { room.item_color2 = readColorNode(room_node["itemColor2"]); } // Dirección de la cinta transportadora (left/none/right) room.conveyor_belt_direction = room_node.contains("conveyorBelt") ? convertAutoSurface(room_node["conveyorBelt"]) : 0; } void RoomFormat::parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room) { room.upper_room = conn_node.contains("up") ? convertRoomConnection(conn_node["up"].get_value_or("null")) : "0"; room.lower_room = conn_node.contains("down") ? convertRoomConnection(conn_node["down"].get_value_or("null")) : "0"; room.left_room = conn_node.contains("left") ? convertRoomConnection(conn_node["left"].get_value_or("null")) : "0"; room.right_room = conn_node.contains("right") ? convertRoomConnection(conn_node["right"].get_value_or("null")) : "0"; } // ============================================================================ // Parseo del tilemap // ============================================================================ // Solo soporta el formato actual: tilemap.draw + tilemap.collision. // La rama de retrocompatibilidad para tilemap directo se eliminó porque dejaba // collision_tile_map vacío y causaba un crash diferido en CollisionMap. void RoomFormat::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { if (!yaml.contains("tilemap")) { std::cerr << "Error: No tilemap found in " << file_name << '\n'; return; } const auto& tilemap_node = yaml["tilemap"]; if (!tilemap_node.contains("draw") || !tilemap_node.contains("collision")) { std::cerr << "Error: " << file_name << " has malformed tilemap (missing draw or collision)\n"; return; } room.tile_map = flattenTilemap(readTilemap2D(tilemap_node["draw"])); room.collision_tile_map = flattenTilemap(readTilemap2D(tilemap_node["collision"])); if (verbose) { std::cout << "Loaded tilemap: " << room.tile_map.size() << " tiles + collision: " << room.collision_tile_map.size() << " tiles\n"; } } // ============================================================================ // Parseo de enemigos // ============================================================================ void RoomFormat::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) { // Nuevo formato: position1 y position2 if (bounds_node.contains("position1")) { const auto& pos1 = bounds_node["position1"]; if (pos1.contains("x")) { enemy.x1 = pos1["x"].get_value() * Tile::SIZE; } if (pos1.contains("y")) { enemy.y1 = pos1["y"].get_value() * Tile::SIZE; } } if (bounds_node.contains("position2")) { const auto& pos2 = bounds_node["position2"]; if (pos2.contains("x")) { enemy.x2 = pos2["x"].get_value() * Tile::SIZE; } if (pos2.contains("y")) { enemy.y2 = pos2["y"].get_value() * Tile::SIZE; } } // Formato antiguo: x1/y1/x2/y2 (compatibilidad) if (bounds_node.contains("x1")) { enemy.x1 = bounds_node["x1"].get_value() * Tile::SIZE; } if (bounds_node.contains("y1")) { enemy.y1 = bounds_node["y1"].get_value() * Tile::SIZE; } if (bounds_node.contains("x2")) { enemy.x2 = bounds_node["x2"].get_value() * Tile::SIZE; } if (bounds_node.contains("y2")) { enemy.y2 = bounds_node["y2"].get_value() * Tile::SIZE; } } auto RoomFormat::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { Enemy::Data enemy; if (enemy_node.contains("type")) { enemy.type = enemy_node["type"].get_value(); } if (enemy_node.contains("animation")) { enemy.animation_path = enemy_node["animation"].get_value(); } if (enemy_node.contains("position")) { const auto& pos = enemy_node["position"]; if (pos.contains("x")) { enemy.x = pos["x"].get_value() * Tile::SIZE; } if (pos.contains("y")) { enemy.y = pos["y"].get_value() * Tile::SIZE; } } if (enemy_node.contains("velocity")) { const auto& vel = enemy_node["velocity"]; if (vel.contains("x")) { enemy.vx = vel["x"].get_value(); } if (vel.contains("y")) { enemy.vy = vel["y"].get_value(); } } if (enemy_node.contains("boundaries")) { parseEnemyBoundaries(enemy_node["boundaries"], enemy); } enemy.flip = enemy_node.contains("flip") ? enemy_node["flip"].get_value_or(false) : false; enemy.mirror = enemy_node.contains("mirror") ? enemy_node["mirror"].get_value_or(false) : false; enemy.frame = enemy_node.contains("frame") ? enemy_node["frame"].get_value_or(-1) : -1; return enemy; } void RoomFormat::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) { if (!yaml.contains("enemies") || yaml["enemies"].is_null()) { return; } for (const auto& enemy_node : yaml["enemies"]) { room.enemies.push_back(parseEnemyData(enemy_node)); } if (verbose) { std::cout << "Loaded " << room.enemies.size() << " enemies\n"; } } // ============================================================================ // Parseo de items // ============================================================================ auto RoomFormat::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data { Item::Data item; if (item_node.contains("tileSetFile")) { item.tile_set_file = item_node["tileSetFile"].get_value(); } if (item_node.contains("tile")) { item.tile = item_node["tile"].get_value(); } if (item_node.contains("position")) { const auto& pos = item_node["position"]; if (pos.contains("x")) { item.x = pos["x"].get_value() * Tile::SIZE; } if (pos.contains("y")) { item.y = pos["y"].get_value() * Tile::SIZE; } } item.counter = item_node.contains("counter") ? item_node["counter"].get_value_or(0) : 0; item.color1 = room.item_color1; item.color2 = room.item_color2; return item; } void RoomFormat::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) { if (!yaml.contains("items") || yaml["items"].is_null()) { return; } for (const auto& item_node : yaml["items"]) { room.items.push_back(parseItemData(item_node, room)); } if (verbose) { std::cout << "Loaded " << room.items.size() << " items\n"; } } // ============================================================================ // Parseo de plataformas // ============================================================================ auto RoomFormat::parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data { MovingPlatform::Data platform; if (platform_node.contains("animation")) { platform.animation_path = platform_node["animation"].get_value(); } if (platform_node.contains("speed")) { platform.speed = platform_node["speed"].get_value(); } if (platform_node.contains("loop")) { auto loop_str = platform_node["loop"].get_value(); platform.loop = (loop_str == "circular") ? LoopMode::CIRCULAR : LoopMode::PINGPONG; } if (platform_node.contains("easing")) { platform.easing = platform_node["easing"].get_value(); } platform.frame = platform_node.contains("frame") ? platform_node["frame"].get_value_or(-1) : -1; if (platform_node.contains("path")) { for (const auto& wp_node : platform_node["path"]) { Waypoint wp; wp.x = wp_node["x"].get_value() * Tile::SIZE; wp.y = wp_node["y"].get_value() * Tile::SIZE; if (wp_node.contains("wait")) { wp.wait = wp_node["wait"].get_value(); } platform.path.push_back(wp); } } return platform; } void RoomFormat::parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose) { if (!yaml.contains("platforms") || yaml["platforms"].is_null()) { return; } for (const auto& platform_node : yaml["platforms"]) { room.platforms.push_back(parsePlatformData(platform_node)); } if (verbose) { std::cout << "Loaded " << room.platforms.size() << " platforms\n"; } } // ============================================================================ // Parseo de llaves // ============================================================================ auto RoomFormat::parseKeyData(const fkyaml::node& key_node) -> Key::Data { Key::Data key; if (key_node.contains("animation")) { key.animation_path = key_node["animation"].get_value(); } if (key_node.contains("id")) { key.id = key_node["id"].get_value(); } if (key_node.contains("position")) { const auto& pos = key_node["position"]; if (pos.contains("x")) { key.x = pos["x"].get_value() * Tile::SIZE; } if (pos.contains("y")) { key.y = pos["y"].get_value() * Tile::SIZE; } } return key; } void RoomFormat::parseKeys(const fkyaml::node& yaml, Room::Data& room, bool verbose) { if (!yaml.contains("keys") || yaml["keys"].is_null()) { return; } for (const auto& key_node : yaml["keys"]) { room.keys.push_back(parseKeyData(key_node)); } if (verbose) { std::cout << "Loaded " << room.keys.size() << " keys\n"; } } // ============================================================================ // Parseo de puertas // ============================================================================ auto RoomFormat::parseDoorData(const fkyaml::node& door_node) -> Door::Data { Door::Data door; if (door_node.contains("animation")) { door.animation_path = door_node["animation"].get_value(); } if (door_node.contains("id")) { door.id = door_node["id"].get_value(); } if (door_node.contains("position")) { const auto& pos = door_node["position"]; if (pos.contains("x")) { door.x = pos["x"].get_value() * Tile::SIZE; } if (pos.contains("y")) { door.y = pos["y"].get_value() * Tile::SIZE; } } return door; } void RoomFormat::parseDoors(const fkyaml::node& yaml, Room::Data& room, bool verbose) { if (!yaml.contains("doors") || yaml["doors"].is_null()) { return; } for (const auto& door_node : yaml["doors"]) { room.doors.push_back(parseDoorData(door_node)); } if (verbose) { std::cout << "Loaded " << room.doors.size() << " doors\n"; } } // ============================================================================ // Punto de entrada del parser // ============================================================================ void RoomFormat::parseAll(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { parseRoomConfig(yaml, room, file_name); parseTilemap(yaml, room, file_name, verbose); parseEnemies(yaml, room, verbose); parseItems(yaml, room, verbose); parsePlatforms(yaml, room, verbose); parseKeys(yaml, room, verbose); parseDoors(yaml, room, verbose); } auto RoomFormat::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { Room::Data room; const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); try { auto file_data = Resource::Helper::loadFile(file_path); if (file_data.empty()) { std::cerr << "Error: Unable to load file " << FILE_NAME << '\n'; return room; } const std::string YAML_CONTENT(file_data.begin(), file_data.end()); auto yaml = fkyaml::node::deserialize(YAML_CONTENT); parseAll(yaml, room, FILE_NAME, verbose); if (verbose) { std::cout << "Room loaded successfully: " << FILE_NAME << '\n'; } } catch (const fkyaml::exception& e) { std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n'; } catch (const std::exception& e) { std::cerr << "Error loading room " << FILE_NAME << ": " << e.what() << '\n'; } return room; } #ifdef _DEBUG auto RoomFormat::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data { Room::Data room; try { auto yaml = fkyaml::node::deserialize(yaml_content); parseAll(yaml, room, file_name, false); } catch (const fkyaml::exception& e) { std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n'; } catch (const std::exception& e) { std::cerr << "Error loading room " << file_name << ": " << e.what() << '\n'; } return room; } // ============================================================================ // createDefault: construye un Room::Data válido para una room nueva // ============================================================================ auto RoomFormat::createDefault() -> Room::Data { Room::Data data; // Zona por defecto (resuelve tile_set_file y music desde ZoneManager) const Zone::Data* zone = ZoneManager::get()->getDefaultZone(); if (zone != nullptr) { data.zone = zone->name; data.tile_set_file = zone->tile_set_file; data.music = zone->music; } data.tile_set_overridden = false; data.music_overridden = false; // Conexiones a cero data.upper_room = "0"; data.lower_room = "0"; data.left_room = "0"; data.right_room = "0"; // Colores de items por defecto (números, no strings) data.item_color1 = 11; data.item_color2 = 12; // Conveyor belt apagado data.conveyor_belt_direction = 0; // Tilemaps del tamaño correcto, vacíos data.tile_map.resize(Map::WIDTH * Map::HEIGHT, -1); data.collision_tile_map.resize(Map::WIDTH * Map::HEIGHT, 0); return data; } // ============================================================================ // Serialización // ============================================================================ auto RoomFormat::roomConnectionToYAML(const std::string& connection) -> std::string { if (connection == "0" || connection.empty()) { return "null"; } return connection; } auto RoomFormat::conveyorBeltToString(int direction) -> std::string { if (direction < 0) { return "left"; } if (direction > 0) { return "right"; } return "none"; } auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity) std::ostringstream out; // --- Sección room --- out << "room:\n"; // zone es siempre obligatoria out << " zone: " << room_data.zone << "\n"; // tileSetFile solo si es override explícito del valor heredado de la zona if (room_data.tile_set_overridden) { out << " tileSetFile: " << room_data.tile_set_file << "\n"; } // music solo si es override explícito del valor heredado de la zona if (room_data.music_overridden) { out << " music: " << room_data.music << "\n"; } // Conexiones out << "\n"; out << " # Conexiones de la habitación (null = sin conexión)\n"; out << " connections:\n"; out << " up: " << roomConnectionToYAML(room_data.upper_room) << "\n"; out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n"; out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n"; out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n"; // Colores de items out << "\n"; out << " # Colores de los objetos\n"; out << " itemColor1: " << static_cast(room_data.item_color1) << "\n"; out << " itemColor2: " << static_cast(room_data.item_color2) << "\n"; // Conveyor belt out << "\n"; out << " # Dirección de la cinta transportadora: left, none, right\n"; out << " conveyorBelt: " << conveyorBeltToString(room_data.conveyor_belt_direction) << "\n"; // --- Tilemap (MAP_HEIGHT filas × MAP_WIDTH columnas, formato flow) --- out << "\n"; out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n"; out << "tilemap:\n"; // Mapa de dibujo out << " # Mapa de dibujo (indices de tiles, -1 = vacio)\n"; out << " draw:\n"; for (int row = 0; row < Map::HEIGHT; ++row) { out << " - ["; for (int col = 0; col < Map::WIDTH; ++col) { int index = (row * Map::WIDTH) + col; if (index < static_cast(room_data.tile_map.size())) { out << room_data.tile_map[index]; } else { out << -1; } if (col < Map::WIDTH - 1) { out << ", "; } } out << "]\n"; } // Mapa de colisiones out << " # Mapa de colisiones (0 = vacio, 1 = solido)\n"; out << " collision:\n"; for (int row = 0; row < Map::HEIGHT; ++row) { out << " - ["; for (int col = 0; col < Map::WIDTH; ++col) { int index = (row * Map::WIDTH) + col; if (index < static_cast(room_data.collision_tile_map.size())) { out << room_data.collision_tile_map[index]; } else { out << 0; } if (col < Map::WIDTH - 1) { out << ", "; } } out << "]\n"; } // --- Enemigos --- if (!room_data.enemies.empty()) { out << "\n"; out << "# Enemigos en esta habitación\n"; out << "enemies:\n"; for (const auto& enemy : room_data.enemies) { out << " - animation: " << enemy.animation_path << "\n"; if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; } int pos_x = static_cast(std::round(enemy.x / Tile::SIZE)); int pos_y = static_cast(std::round(enemy.y / Tile::SIZE)); out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n"; out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n"; int b1_x = enemy.x1 / Tile::SIZE; int b1_y = enemy.y1 / Tile::SIZE; int b2_x = enemy.x2 / Tile::SIZE; int b2_y = enemy.y2 / Tile::SIZE; out << " boundaries:\n"; out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n"; out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n"; if (enemy.flip) { out << " flip: true\n"; } if (enemy.mirror) { out << " mirror: true\n"; } if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; } out << "\n"; } } // --- Items --- if (!room_data.items.empty()) { out << "# Objetos en esta habitación\n"; out << "items:\n"; for (const auto& item : room_data.items) { out << " - tileSetFile: " << item.tile_set_file << "\n"; out << " tile: " << item.tile << "\n"; int item_x = static_cast(std::round(item.x / Tile::SIZE)); int item_y = static_cast(std::round(item.y / Tile::SIZE)); out << " position: {x: " << item_x << ", y: " << item_y << "}\n"; if (item.counter != 0) { out << " counter: " << item.counter << "\n"; } out << "\n"; } } // --- Plataformas --- if (!room_data.platforms.empty()) { out << "# Plataformas móviles en esta habitación\n"; out << "platforms:\n"; for (const auto& plat : room_data.platforms) { out << " - animation: " << plat.animation_path << "\n"; out << " speed: " << plat.speed << "\n"; out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n"; if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; } if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } out << " path:\n"; for (const auto& wp : plat.path) { int wx = static_cast(std::round(wp.x / Tile::SIZE)); int wy = static_cast(std::round(wp.y / Tile::SIZE)); out << " - {x: " << wx << ", y: " << wy; if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; } out << "}\n"; } out << "\n"; } } // --- Llaves --- if (!room_data.keys.empty()) { out << "# Llaves en esta habitación\n"; out << "keys:\n"; for (const auto& key : room_data.keys) { out << " - animation: " << key.animation_path << "\n"; out << " id: \"" << key.id << "\"\n"; int kx = static_cast(std::round(key.x / Tile::SIZE)); int ky = static_cast(std::round(key.y / Tile::SIZE)); out << " position: {x: " << kx << ", y: " << ky << "}\n"; out << "\n"; } } // --- Puertas --- if (!room_data.doors.empty()) { out << "# Puertas en esta habitación\n"; out << "doors:\n"; for (const auto& door : room_data.doors) { out << " - animation: " << door.animation_path << "\n"; out << " id: \"" << door.id << "\"\n"; int dx = static_cast(std::round(door.x / Tile::SIZE)); int dy = static_cast(std::round(door.y / Tile::SIZE)); out << " position: {x: " << dx << ", y: " << dy << "}\n"; out << "\n"; } } return out.str(); } auto RoomFormat::saveYAML(const std::string& file_path, const Room::Data& data) -> std::string { const std::string CONTENT = buildContent(data); std::ofstream file(file_path); if (!file.is_open()) { std::cerr << "RoomFormat: Cannot write to " << file_path << "\n"; return "Error: Cannot write to " + file_path; } file << CONTENT; file.close(); const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); std::cout << "RoomFormat: Saved " << FILE_NAME << "\n"; return "Saved " + FILE_NAME; } #endif // _DEBUG