— Cartman, South Park (paraphrased).
Plugin como unidad modularEl Plugin (módulo conectable al motor) de Bevy es probablemente la abstracción más infravalorada del framework. En cinco líneas te permite empaquetar sistemas + recursos + eventos + componentes + configuración en una unidad testeable, reutilizable y opcional. Sin plugins, tu main.rs se convierte en un monstruo de 2000 líneas; con plugins, crece como un Lego.
La forma idiomática de 0.19 tiene tres niveles:
//! cap-27 — sección 27.1: tres niveles de plugin.
use bevy::prelude::*;
// 1. Plugin vacío (marcador). Útil para agrupar.
pub struct MyGamePlugin;
impl Plugin for MyGamePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((PlayerPlugin, EnemyPlugin, HudPlugin));
}
}
// 2. Plugin con estado (configurable).
pub struct AudioPlugin { pub volume: f32 }
impl Plugin for AudioPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(MasterVolume(self.volume));
app.add_systems(Startup, audio_setup);
}
}
// 3. PluginGroup (conjunto que se registra como uno).
pub struct GameplayPlugins;
impl PluginGroup for GameplayPlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(PlayerPlugin).add(EnemyPlugin).add(HudPlugin);
}
}
Una regla útil: cada Plugin debe encapsular un dominio. Player, Enemy, Hud, Tilemap, Audio, Networking — son todos candidatos naturales. Si tu plugin tiene un sistema de pathfinding dentro, probablemente podrías partirlo en PathfindingPlugin y reutilizarlo en otra cosa.
Otro consejo práctico: los plugins deben ser idempotentes respecto a sus recursos. Si dos llamadas a PlayerPlugin::build rompen el juego (por duplicar un recurso), ese plugin está mal diseñado. La regla es: si el recurso ya existe, no lo reinsertes; si el evento ya está registrado, no lo re-registres. Bevy te ayuda: insert_resource con un recurso ya existente lo sobreescribe silenciosamente, lo cual puede ser útil o desastroso según el caso. Usa init_resource (que falla si ya existe) para detectar errores en tests.
//! cap-27 — sección 27.1: registro idempotente.
use bevy::prelude::*;
#[derive(Resource, Default)]
struct PlayerScore { value: u32 }
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
if !app.world().contains_resource::<PlayerScore>() {
app.init_resource::<PlayerScore>();
}
app.add_systems(Startup, player_setup);
app.add_systems(Update, player_movement);
}
}
Y el patrón trait-driven (que viste en el capítulo 2) brilla aquí: define un trait GamePlugin que extienda Plugin y le añada métodos como dependencies(&self) -> Vec<PluginId>. Así puedes escribir un sistema que "instala todos los plugins del juego en el orden correcto" sin repetir código:
//! cap-27 — sección 27.1: trait GamePlugin.
pub trait GamePlugin: Plugin {
fn dependencies(&self) -> Vec<&'static str> { vec![] }
fn name(&self) -> &'static str;
}
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin { /* ... */ }
impl GamePlugin for PlayerPlugin {
fn name(&self) -> &'static str { "player" }
fn dependencies(&self) -> Vec<&'static str> { vec!["input", "tilemap"] }
}
Plugin (trait): unidad modular de Bevy; agrupa sistemas, recursos, eventos y configuración.
PluginGroup (trait): conjunto de plugins que se registran juntos (útil para "GameplayPlugins").
Build (método): punto de entrada de un Plugin; recibe `&mut App` para añadir lo que sea.
insert_resource (método): añade un recurso singleton accesible vía `Res<T>` / `ResMut<T>`.
add_systems (método): registra sistemas en un schedule concreto.
PluginGroup y Plugin configurableCuando tu juego crece, los plugins empiezan a depender unos de otros. PluginGroup te permite declarar dependencias implícitas: si HudPlugin necesita UiPlugin y InputPlugin, el grupo puede encadenarlos. Pero cuidado: las dependencias explícitas son mejores que las implícitas. Si tu plugin necesita otro, dilo en la documentación y registra ambos en main.rs. Las cadenas mágicas de "este plugin requiere aquel" son la receta perfecta para errores en producción.
Lo que sí gana brillo con PluginGroup es el feature flag (interruptor de compilación condicional) #[cfg(feature = "dev")] para activar plugins solo en builds de desarrollo:
//! cap-27 — sección 27.2: plugins condicionales.
pub struct GamePlugins;
impl PluginGroup for GamePlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(CorePlugins).add(GameplayPlugins);
#[cfg(feature = "dev")]
group.add(DebugHudPlugin);
#[cfg(feature = "remote")]
group.add(RemoteInspectorPlugin);
}
}
Y en Cargo.toml:
[features]
default = []
dev = ["bevy/trace_tracy", "bevy/wayland"]
remote = ["bevy/bevy_remote"]
Así, cargo run --features dev arranca con Tracy y un HUD de FPS; cargo build --release sale sin ese overhead.
❌ Hacer que un plugin dependa de otro plugin en silencio (sin decirlo en docs).
El usuario añade `EnemyPlugin`, le faltan sistemas porque no añadió `TilemapPlugin`, y todo falla.
✅ Documentar la dependencia: "EnemyPlugin requiere TilemapPlugin y PhysicsPlugin en el App".
Mejor aún, exponer una `EnemyPlugin::new(tilemap: Res<...>)` que tome dependencias explícitas.
💡 Por qué: el compilador atrapa errores de tipos, no de orden de registro.
El hot-reload de Bevy 0.18/0.19 es bidireccional: no solo recarga assets cuando cambia el archivo, también puede recargar scenas bsn! completas y notificarte con eventos. La pieza central es AssetEvent::Modified { id }, que ya viste en el capítulo anterior, pero aquí vamos a ver cómo integrarlo con un sistema de "spawner reactivo" para no tener que reiniciar el juego cuando cambias un prefab.
//! cap-27 — sección 27.3: reaccionar a bsn! modificado.
use bevy::asset::AssetEvent;
use bevy::prelude::*;
use bevy::scene::DynamicScene;
#[derive(Resource, Default)]
struct SceneWatchdog { last_handle: Option<Handle<DynamicScene>> }
fn watch_scene(
mut events: EventReader<AssetEvent<DynamicScene>>,
mut watchdog: ResMut<SceneWatchdog>,
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
for evt in events.read() {
if let AssetEvent::Modified { id } = evt {
info!("Escena {:?} recargada", id);
// Re-spawnear: despawnear todas las entidades con un marker y volver a spawn.
commands.trigger(ReloadSceneEvent);
}
}
}
#[derive(Event)]
struct ReloadSceneEvent;
La parte interesante es la combinación AssetEvent::Modified + observe(ReloadSceneEvent) (un observer, no un sistema): puedes hacer que el reload sea instantáneo sin tener que meterlo en Update.
//! cap-27 — sección 27.3: observer para reload.
fn setup_reload_observer(app: &mut App) {
app.add_observer(reload_on_event);
}
fn reload_on_event(
trigger: Trigger<ReloadSceneEvent>,
query: Query<Entity, With<SceneEntity>>,
mut commands: Commands,
) {
for e in &query { commands.entity(e).despawn(); }
// ... respawnear desde bsn! ...
}
Para que funcione en disco, hay que registrar el FileWatcher:
//! cap-27 — sección 27.3: app con file watcher.
use bevy::prelude::*;
use std::time::Duration;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
file_watcher: Some(FileWatcher::Poll(Duration::from_millis(300))),
..default()
}))
.run();
}
FileWatcher::Poll revisa el disco cada 300 ms sin depender del SO (ideal para WSL, containers y CI); FileWatcher::OS usa notificaciones nativas (más eficiente pero menos portable).
FileWatcher (API): servicio que vigila cambios en disco; modo Poll (periódico) u OS (notificaciones nativas).
AssetEvent::Modified (evento): disparado cuando un asset en disco cambia y Bevy lo recarga.
DynamicScene (asset): escena serializable con entidades, componentes y relaciones; carga/guarda en `.scn.ron`.
observe (método): suscribe un observer a un evento en una entidad.
bsn! (macro): sintaxis declarativa para definir escenas (Bevy Scene Notation).
El Bevy Remote Protocol es la killer feature de 0.18/0.19 para desarrollo. Imagina: mientras tu juego corre, abres un cliente HTTP y preguntas "¿qué entidades tienen Player?"; obtienes un JSON con todas, con sus componentes, con sus Transform. Puedes modificar cualquier componente, spawnear entidades nuevas, despawnear existentes, todo desde fuera del juego. Es el "Live Coding" llevado al extremo.
Aclaración importante sobre el modelo. BRP no es una API REST con una ruta por operación (como a veces se describe en tutoriales antiguos). Es JSON-RPC 2.0 sobre HTTP: hay un único endpoint (
http://localhost:15702) y el verbo es siemprePOST. Lo que cambia entre operaciones es el campomethoddel cuerpo, con nombres comobevy/query,bevy/get,bevy/spawn, etc. Confundirlo con REST lleva a URLs como/bevy/queryo/bevy/commandsque no existen: el servidor devuelve 404.
Para activarlo:
//! cap-27 — sección 27.4: habilitar BRP.
use bevy::prelude::*;
use bevy::remote::{RemotePlugin, http::RemoteHttpPlugin};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RemotePlugin::default()) // núcleo de BRP.
.add_plugins(RemoteHttpPlugin::default()) // expone HTTP en localhost:15702.
.run();
}
Y desde tu terminal, con curl, contra el endpoint único. Ejemplos con los siete métodos estándar:
# `bevy/list`: lista los componentes/types registrados en el world.
curl -X POST http://localhost:15702 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"bevy/list","params":{},"id":1}'
# `bevy/query`: encuentra entidades que tengan un componente (p.ej. Transform).
curl -X POST http://localhost:15702 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"bevy/query","params":{
"data":{
"components":["bevy_transform::components::transform::Transform"],
"option":["bevy_transform::components::transform::Transform"]
}
},"id":1}'
# `bevy/get`: lee los componentes de una entidad concreta.
curl -X POST http://localhost:15702 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"bevy/get","params":{
"entity":42,
"components":["bevy_transform::components::transform::Transform"]
},"id":1}'
# `bevy/spawn`: crea una entidad nueva con componentes dados.
curl -X POST http://localhost:15702 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"bevy/spawn","params":{
"components":{
"bevy_transform::components::transform::Transform":{
"translation":[100.0,200.0,0.0]
}
}
},"id":1}'
# `bevy/insert`: inserta/sobrescribe componentes en una entidad existente.
# `bevy/remove`: quita componentes de una entidad existente.
# `bevy/despawn`: destruye una entidad (por id o por query).
Bevy responde con JSON-RPC: un campo result cuando todo va bien, o error con code y message si la petición está mal formada o el componente no existe. Puedes usar este endpoint desde una página web, un bot de Discord, un script de Python para testing, o un editor externo.
El caso de uso estrella es integración continua: en CI, lanzas el juego en headless (sin ventana), esperas 5 segundos, lees el estado vía BRP (bevy/query), compruebas invariantes ("¿hay exactamente 1 Player?", "¿el Boss está en posición X?"), modificas algo con bevy/insert ("mata al Boss"), y vuelves a leer. Todo sin test unitario, todo contra el juego real.
//! cap-27 — sección 27.4: cliente BRP real desde otro proceso Rust (HTTP + JSON-RPC).
use serde_json::json;
async fn integration_test() {
let client = reqwest::Client::new();
let body = json!({
"jsonrpc": "2.0",
"method": "bevy/query",
"params": { "data": { "components": ["my_game::Player"] } },
"id": 1
});
let resp = client.post("http://localhost:15702")
.json(&body)
.send().await.unwrap();
let json: serde_json::Value = resp.json().await.unwrap();
// JSON-RPC devuelve los datos en `result`; una query vacía significa "0 entidades".
assert!(json["result"].is_object());
}
Nota sobre
RemoteClient. Es habitual ver en tutoriales antiguos unuse bevy::remote::RemoteClient;invocando un cliente Rust nativo. Ese cliente no existe como API pública debevy::remote: BRP es la parte servidor del motor; el cliente vive en otra app/crate (puede ser Python, JavaScript, o cualquier proceso que hable HTTP+JSON-RPC). Para consumir BRP desde Rust, usareqwest+serde_jsoncomo en el ejemplo anterior. Para acceso más cómodo, existe el crate comunitariobevy_brp_extrasy el SDKbevy_brp_mcp(0.22.1 a julio 2026).
Las herramientas externas que ya hablan BRP son cada vez más: la extensión de VSCode splo.vscode-bevy-inspector, el IDE BerrysCode, y herramientas como bevy_mod_debugdump (exportar el schedule como SVG, aunque éste no usa BRP directamente). La tendencia es clara: el motor es una API HTTP, no solo una librería enlazada.
❌ Tratar BRP como REST y hacer POST a `/bevy/query` o `/bevy/commands`.
El servidor no conoce esas rutas; responde 404. El método va en el cuerpo (`"method":"bevy/query"`).
❌ Dejar BRP habilitado en builds de release.
Cualquiera con acceso a localhost puede mutar tu juego: en LAN de una conferencia, en un Docker compartido, en CI.
✅ Activar BRP solo con `--features remote` o detrás de un flag de runtime.
En producción, asegúrate de que el plugin no se compile.
💡 Por qué: BRP no tiene autenticación; expone el mundo entero sin pedir credenciales.
Si necesitas aislar BRP del público de la red, la forma estándar en 0.19 no es un constructor mágico del plugin, sino configurar el bind address del transporte HTTP a 127.0.0.1 (que es el valor por defecto) en lugar de 0.0.0.0, y/o añadir un proxy delante con autenticación. No existe un RemotePlugin::unix_socket(...) público en el crate oficial; el transporte Unix-socket es algo que tendrías que construirte por encima de los plugins de BRP.
BRP / Bevy Remote Protocol (API): protocolo JSON-RPC 2.0 sobre HTTP integrado en Bevy para inspeccionar/mutar el mundo en vivo. Endpoint único.
RemotePlugin (plugin): activa la parte servidor del BRP (núcleo).
RemoteHttpPlugin (plugin): transporte HTTP; expone 127.0.0.1:15702 por defecto.
Métodos BRP: bevy/query, bevy/get, bevy/spawn, bevy/despawn, bevy/list, bevy/insert, bevy/remove.
JSON-RPC 2.0 (protocolo): convención de RPC en JSON con `method`, `params` y `id`; soporta notificaciones y errores tipados.
Headless (modo): ejecutar el juego sin ventana; útil para servidores y CI.
QA manual (técnica): testing humano asistido por herramientas; BRP permite "teleport al boss" sin recompilar.
SettingsPlugin y SettingsGroupBevy 0.19 introduce un nuevo subsistema dentro de bevy_settings: el App Settings. La idea es simple: declaras SettingsGroups en tu código (cada uno con un file asociado) y el motor se encarga de cargarlos desde el directorio de settings al arranque, insertarlos como resources en el App, y persistirlos. Es el puente entre "configuración hardcodeada" y "configuración user-facing", sin que tengas que escribir el código de I/O.
Aviso sobre APIs antiguas. En este libro se publicaron en su día APIs como
WindowSettingsyRenderSettingspresentados como configuraciones nativas del motor. No existen en Bevy 0.19 core con ese nombre. La configuración de ventana va por los componentes debevy_window(víaWindow) y la del renderer porbevy_render; los "settings" en el sentido del subsistema nuevo son datos del propio juego (audio, controles, idioma, balance persistible), no del motor.
El patrón real en 0.19:
//! cap-27 — sección 27.5: App Settings con `SettingsPlugin` (Bevy 0.19).
use bevy::prelude::*;
use bevy::settings::{SettingsPlugin, SettingsGroup};
// 1. Define cada grupo de configuración como un `SettingsGroup`.
#[derive(SettingsGroup, Resource, Reflect, Default, Clone)]
#[settings_group(file = "audio.ron")]
pub struct AudioSettings {
pub master_volume: f32,
pub sfx_volume: f32,
pub music_volume: f32,
}
#[derive(SettingsGroup, Resource, Reflect, Default, Clone)]
#[settings_group(file = "controls.ron")]
pub struct ControlSettings {
pub keybindings: Vec<String>,
pub invert_y: bool,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// 2. Añade `SettingsPlugin` con un reverse-DNS name.
// Esto lee/escribe `settings/com.ejemplo.app/audio.ron`, etc.
.add_plugins(SettingsPlugin::new("com.ejemplo.app"))
// 3. Registra tus grupos; el plugin los carga y los inserta como resources.
.register_settings_group::<AudioSettings>()
.register_settings_group::<ControlSettings>()
.run();
}
Lo interesante de este diseño:
Startup: el plugin localiza el archivo (por defecto bajo settings/<reverse-dns>/), lo deserializa en RON/JSON y lo inserta como Resource. Si el archivo no existe, lo crea con Default.Resource, los lees con Res<AudioSettings> y los mutas con ResMut<AudioSettings>. Nada de APIs extrañas.bevy_settings).Para mutar un setting en runtime (p.ej., cuando el usuario mueve un slider):
//! cap-27 — sección 27.5: mutar y persistir un setting en runtime.
fn on_master_slider_changed(
mut settings: ResMut<AudioSettings>,
// events: EventReader<SliderChanged>
) {
settings.master_volume = 0.7; // mutación en runtime.
// La persistencia a disco se dispara con el comando oficial de bevy_settings
// (ver migration guide 0.18 → 0.19 para el método exacto).
}
Caveat de verificación. La firma exacta de
register_settings_group::<T>(), el nombre del método de persistencia y los atributos concretos del derive#[settings_group(file = ...)]están documentados en el módulobevy_settingsdel tagv0.19.0y en la migration guide 0.18→0.19. La estructura general descrita aquí (plugin + grupos derivados + carga al arranque + persistencia) sí corresponde a la API publicada.
bevy_settings (crate/módulo 0.19): subsistema nativo para settings persistentes del juego.
SettingsPlugin (plugin 0.19): activa el subsistema; toma un reverse-DNS name y carga/guarda grupos.
SettingsGroup (trait/derive 0.19): un tipo serializable declarado por el usuario; se asocia a un archivo.
#[settings_group(file = "...")] (atributo 0.19): vincula un SettingsGroup a su archivo de persistencia (RON/JSON).
init_resource (método): añade un recurso inicializado con `Default`; falla si ya existe.
contains_resource (método): comprueba si un recurso está en el mundo (útil para idempotencia).
GamePlugin (trait, ejemplo): patrón que extiende `Plugin` con nombre y dependencias.
trait-driven design (patrón): definir comportamiento por traits compartidos, no por herencia.
Plugin (trait): unidad modular que agrupa sistemas, recursos, eventos y configuración.
PluginGroup (trait): conjunto de plugins registrados como uno.
build (método): punto de entrada de un Plugin.
DefaultPlugins (struct): bundle de plugins oficiales (render, audio, input, UI).
insert_resource (método): añade un recurso singleton.
add_systems (método): registra sistemas en un schedule.
add_observer (método): registra un observer sobre un evento.
FileWatcher (API): vigila cambios en disco (modo Poll u OS).
AssetEvent::Modified (evento): un asset cambió en disco y Bevy lo recargó.
DynamicScene (asset): escena serializable (componentes + entidades + relaciones).
bsn! (macro): sintaxis declarativa para definir escenas.
BRP / Bevy Remote Protocol (API): JSON-RPC 2.0 sobre HTTP (endpoint único) para inspeccionar/mutar el mundo en vivo.
RemotePlugin (plugin): activa BRP (núcleo servidor); puerto por defecto 15702.
RemoteHttpPlugin (plugin): transporte HTTP de BRP (127.0.0.1:15702 por defecto).
JSON-RPC 2.0 (protocolo): RPC en JSON con `method`/`params`/`id`; el método va en el cuerpo, no en la URL.
Headless (modo): ejecutar sin ventana gráfica.
bevy_settings (módulo/crate 0.19): subsistema nativo de settings persistentes del juego.
SettingsPlugin (plugin 0.19): activa el subsistema; toma un reverse-DNS y carga/guarda grupos.
SettingsGroup (derive/trait 0.19): struct del usuario asociado a un archivo de persistencia (RON/JSON).
Window (componente): configuración de ventana en `bevy_window` (resolución, título, modo...); NO existe `WindowSettings`.
PresentMode (enum): cómo se sincroniza con el monitor (va en `Window`).
feature flag (cargo): atributo de compilación condicional.
Inscryption (Daniel Mullins, 2021) — su sistema de save con migración de versiones es el ejemplo canónico de hot-reload de datos sin reiniciar.
RollerCoaster Tycoon (Chris Sawyer, 1999) — escrito en ensamblador, pero ya tenía un sistema de plugins por DLL que cargaba atracciones custom.
Hollow Knight (Team Cherry, 2017) — modders han usado desensamblado + inyección de DLL para hot-reloadear DLLs de mods sin reiniciar; equivalente a BRP en su ecosistema.
Bevy Remote Protocol (2024) — inspirado por el "Live Linking" de Unreal y el inspector remoto de Rust Analyzer.
Problema: tu juego funciona, pero cada vez que cambias un valor (velocidad del jugador, color del HUD, posición inicial del Boss), tienes que recompilar 2 minutos. La productividad cae y el equipo se frustra.
Solución:
Plugin autocontenido (PlayerPlugin, HudPlugin, etc.).FileWatcher con AssetPlugin para hot-reload de assets.AssetEvent::Modified + observe para reaccionar a cambios.RemotePlugin en builds --features dev para inspeccionar/mutar en vivo.App Settings (0.19) mutables.Cuándo sí:
Cuándo no:
Cierre:
Los plugins son la columna vertebral de un proyecto Bevy mantenible. El hot-reload y BRP son las muletas que te permiten caminar más rápido durante desarrollo. Y los App Settings de 0.19 son el puente entre "configuración hardcodeada" y "configuración user-facing". Combina los tres y tu equipo pasa de "esperar 2 minutos por cambio" a "ver el cambio en pantalla en 300 ms". Es la diferencia entre iterar y sufrir.
El próximo capítulo cierra el libro: un Mini Metroidvania paso a paso que integra todo lo que hemos visto en un solo proyecto jugable.
PluginGroup los combina. Plugin con genéricos te permite plugins específicos de tu juego (PlayerPlugin, EnemyPlugin).bevy_asset::io::watcher detecta cambios en assets/ y recarga. El AssetEvent::Modified te avisa para reaplicar handles. Funciona en nativo; no en WASM.127.0.0.1:15702). Con RemotePlugin + RemoteHttpPlugin obtienes inspección y manipulación en runtime: cambiar componentes, ver entidades, todo sin recompilar. Los métodos (bevy/query, bevy/get, bevy/spawn, etc.) van en el cuerpo, no en la URL.App settings se inicializan al arranque y se guardan al cerrar. Bridge entre "hardcodeado" y "user-facing".States y SubStates: el patrón canónico de "modo de juego". OnEnter/OnExit/OnTransition scheduleros ejecutan lógica en bordes; el estado es un enum derivando States.SceneRoot + DynamicScene cargan escenas desde .scn (formato binario de Bevy) o desde código con bsn! (cap 12). Las escenas se componen, parchean y dependen entre sí.bevy::ecs::reflect::ReflectResource + serde (o ron). WorldSerializer round-trip; DynamicScene para snapshots parciales.Command reversible; el stack se navega con Ctrl+Z/Ctrl+Y.En el cap 28 (proyecto final) construimos un mini Metroidvania paso a paso que junta todo lo que hemos visto: render2D + cámara + tilemaps + físicas + input + estados + UI + audio + save data + networking. No es un proyecto "completo" en el sentido de 40 horas de juego, pero es el esqueleto de un metroidvania real, con todas las piezas que el género necesita. Es la prueba de fuego: si puedes seguir este proyecto, puedes hacer el tuyo. El cap 28B después es el release: cómo empaquetar, firmar, subir a Steam/itch.io, y dormir tranquilo. Si el cap 27 era "estructura para mantener", el cap 28 es "el juego entero en 5.000 líneas". Última parada, a por él.