{"schemaVersion":1,"sdkVersion":"0.2.0","book":"de520020-2026-4000-8000-000000000001","revision":"b1817244d4cfa94f0d68dcf09d7210254512266f907284366af53e2399dcafa7","chapters":[{"id":"de520020-2026-4000-8000-000000000001","language":"es","title":"SDK de Nelkano","path":"/es/sdk-de-nelkano","content":"<p>Con el SDK puedes añadir botones, formularios y funciones a Nelkano, o crear un tema propio. Escribes el módulo en JavaScript o TypeScript, lo pruebas en Studio y lo instalas en la app como un archivo <code>.nelkmod</code>.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-01-studio-editor-visor.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-01-studio-editor-visor.webp\" width=\"1440\" height=\"1000\" alt=\"El proyecto Hola en Studio: código y visor.\" data-caption=\"El proyecto Hola en Studio: código y visor.\"></a>\n<h2>Crea tu primer módulo</h2>\n<ol><li><a href=\"/es/sdk-de-nelkano/simulador\">Instala y abre SDK Studio</a>. Descarga el instalador para Windows x64; Node.js y Java vienen incluidos. No necesitas el código del emulador ni Android Studio.</li><li>Crea un proyecto con la plantilla Básico y sigue <a href=\"/es/sdk-de-nelkano/ejemplo-minimo\">Tu primer botón</a>. Añadirás una opción a Ajustes que abre un panel.</li><li><a href=\"/es/sdk-de-nelkano/crear-modulo\">Exporta el módulo firmado e instálalo</a> en Android o escritorio. Conserva la clave de firma: la necesitarás para actualizarlo.</li></ol>\n<p>Android, Desktop y SDK Studio comparten la versión <code>3.0.3-beta</code>. El contrato que declaras en <code>sdkVersion</code> sigue siendo <code>0.2.0</code>; la versión de tu módulo la eliges tú.</p>\n<h2>Elige qué quieres añadir</h2>\n<ul><li>Pausar, guardar o continuar una partida: <a href=\"/es/sdk-de-nelkano/emulacion\">Emulación: operaciones y eventos</a>.</li><li>Configurar un mando o una segunda pantalla: <a href=\"/es/sdk-de-nelkano/mandos-segunda-pantalla\">Mandos y segunda pantalla</a>.</li><li>Ofrecer colores o un idioma: <a href=\"/es/sdk-de-nelkano/idioma-temas\">Idioma y temas</a>.</li><li>Programar la aplicación de un parche: <a href=\"/es/sdk-de-nelkano/parches\">Parches de juegos</a>.</li><li>Emitir una partida: <a href=\"/es/sdk-de-nelkano/streaming\">Streaming</a>.</li></ul>\n<p>También puedes guardar datos propios, crear formularios y conectar servicios. Cada capítulo indica los permisos necesarios y cómo comprobar si el dispositivo o el motor admite la función.</p>\n<h2>Prueba en Studio y después en la app</h2>\n<p>El visor de Studio usa juegos, cuentas, guardados y hardware simulados. Sirve para probar el código y la interfaz; las conexiones externas autorizadas sí pueden usar Internet.</p>\n<p>Prueba después el paquete en cada plataforma que declares en el manifiesto. En Nelkano, abre Ajustes → General → Módulos: Instalados permite importar un archivo y Explorar abre el catálogo. La app verifica el paquete y muestra sus permisos antes de instalarlo.</p>\n<p>Android, Desktop y Studio mantienen el mismo número de versión, con un instalador y un canal de actualización propios para cada destino. Los módulos se versionan y actualizan por separado.</p>\n<h2>Añadir un motor de emulación</h2>\n<p>Un paquete V2 puede incluir un motor nativo compilado por ti para Android o escritorio. Necesitarás preparar sus binarios, describir sus capacidades y probar cada destino. Sigue <a href=\"/es/sdk-de-nelkano/crear-modulo\">Paquete V2 con un motor compilado</a> y consulta <a href=\"/es/sdk-de-nelkano/emulacion\">las operaciones del motor</a>. Studio simula su integración; no ejecuta una prueba real del motor.</p>"},{"id":"de520020-2026-4000-8000-000000000002","language":"es","title":"Cómo funciona un módulo","path":"/es/sdk-de-nelkano/funcionamiento","content":"<p>El archivo principal del módulo exporta <code>createModule(host)</code>. <code>host</code> reúne las funciones del SDK: consultar juegos, guardar datos o actuar sobre una partida, entre otras.</p>\n<h2>Mostrar un botón y responder al pulsarlo</h2>\n<ol><li><code>activate()</code> prepara el módulo al activarlo.</li><li><code>contributions(context)</code> devuelve los botones, secciones o ajustes que deben aparecer en la pantalla actual.</li><li><code>dispatch(action, payload, context)</code> atiende la acción del usuario. Puede abrir un formulario o cerrar el panel.</li><li><code>deactivate()</code> libera los recursos al desactivarlo.</li></ol>\n<p>Por ejemplo, un módulo de notas añade un botón a la ficha del juego. La acción lee su nota y devuelve un formulario. Al guardar, escribe el dato y devuelve <code>{kind:'close-panel'}</code>.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/module-lifecycle.svg\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/module-lifecycle.svg\" width=\"1120\" height=\"640\" alt=\"Del botón a la acción del módulo.\" data-caption=\"Del botón a la acción del módulo.\"></a>\n<h2>Saber desde dónde se llama al módulo</h2>\n<p><code>context.surface</code> identifica la pantalla. El contexto puede incluir también <code>gameId</code>, <code>systemId</code>, <code>session</code> y <code>selection</code>. En Biblioteca, <code>selection</code> contiene los juegos seleccionados; en Ajustes puede no haber ningún juego abierto.</p>\n<p>Para actuar sobre una partida, usa la sesión recibida en esa acción. Su <code>id</code> y <code>generation</code> identifican esa apertura del juego. Si la partida cambia, la app rechaza la referencia anterior.</p>\n<h2>Actualizar los datos cuando cambian</h2>\n<p>Implementa <code>onEvent(event)</code> para recibir avisos:</p>\n<ul><li><code>data-changed</code>: han cambiado datos.</li><li><code>hooks-changed</code>: ha cambiado una integración entre módulos.</li><li><code>library-imported</code> y <code>context-changed</code>: avisos adicionales de Android.</li></ul>\n<p>Los eventos de partida y guardados se describen en <a href=\"/es/sdk-de-nelkano/emulacion\">Emulación</a>. Lee los datos cuando los necesites y responde a sus avisos, sin volver a consultarlos continuamente.</p>\n<p>Prueba los cambios de sesión en la app. Studio reconstruye el módulo al recargar y no reproduce todos los detalles de su cierre en Android.</p>\n<h2>Mantener la interfaz al día</h2>\n<p><code>contributions(context)</code> debe describir la interfaz usando el contexto y tus datos. Guarda los cambios en las acciones o los eventos: Nelkano puede reutilizar las contribuciones ya calculadas entre repintados.</p>\n<ul><li>Una acción actualiza las contribuciones de su módulo.</li><li>Los cambios de biblioteca, guardados, cuenta, idioma o ganchos actualizan también los módulos afectados.</li><li><code>close-panel</code> cierra el formulario después de actualizar contribuciones y filtros.</li><li>Desactivar o desinstalar retira los elementos del módulo.</li></ul>\n<p>Mantén breves <code>activate()</code> y <code>contributions(context)</code>: pueden ejecutarse durante el arranque. Inicia el trabajo costoso desde una acción del usuario o una tarea.</p>\n<h2>Atender llamadas de otros módulos</h2>\n<ul><li><code>onHook(request)</code> responde a un gancho: una integración que otro módulo ha declarado.</li><li><code>onService(request)</code> ejecuta un método de un servicio registrado por tu módulo.</li><li><code>onProfile(request)</code> valida y normaliza documentos del perfil. Durante esa validación no se puede llamar al SDK.</li></ul>\n<h2>Ejecutar una tarea en segundo plano</h2>\n<p><code>onBackground(event)</code> recibe <code>task-due</code> y, mientras trabaja, <code>connection-changed</code>. Debe funcionar tanto en una instancia nueva, sin pasar por <code>activate()</code>, como en una instancia ya activa.</p>\n<p>Studio simula las tareas y entrega sus avisos a <code>onEvent</code>. No utiliza el planificador nativo de Android o escritorio.</p>"},{"id":"de520020-2026-4000-8000-000000000003","language":"es","title":"Crear y llevar tu módulo al teléfono","path":"/es/sdk-de-nelkano/crear-modulo","content":"<p>Crea un proyecto, prueba una acción en Studio y exporta el paquete firmado. Después instala ese mismo archivo en la app para probarlo en Android o escritorio.</p>\n<h2>Instalar el SDK</h2>\n<ol><li>En <a href=\"/modulos\">Módulos</a>, pulsa Descargar SDK y ejecuta el instalador <code>.exe</code> para Windows x64.</li><li>Completa la instalación y abre <strong>Nelkano SDK Studio</strong> desde Inicio o el acceso del escritorio.</li></ol>\n<p>Incluye Node.js, Java, los tipos y los ejemplos. No necesitas instalar npm, un JDK ni Android Studio para crear módulos JavaScript o TypeScript.</p>\n<h2>Crear un proyecto</h2>\n<ol><li>Entra en Ajustes → Nuevo módulo.</li><li>Elige una carpeta para tus proyectos, nombre, identificador y plantilla.</li></ol>\n<p>Guarda tus proyectos fuera de la carpeta de instalación. Si prefieres la terminal, abre PowerShell en la carpeta de instalación de Studio y utiliza <code>.\\nelkmod.cmd</code>:</p>\n<pre><code>.\\nelkmod.cmd init C:/mis-modulos/mi-modulo\n.\\nelkmod.cmd dev C:/mis-modulos/mi-modulo</code></pre>\n<p>El proyecto contiene <code>manifest.json</code>, el código en <code>src</code> y sus recursos. El manifiesto declara nombre, ID, versión, permisos y archivos incluidos. Añade cada archivo nuevo a <code>files</code>; Studio ofrece hacerlo al crearlo.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-02-nuevo-modulo-basico.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-02-nuevo-modulo-basico.webp\" width=\"1440\" height=\"1000\" alt=\"Crear un proyecto con la plantilla Básico.\" data-caption=\"Crear un proyecto con la plantilla Básico.\"></a>\n<h2>Probar y exportar desde Studio</h2>\n<ol><li>Guarda el código para actualizar el visor y prueba tu acción.</li><li>Pulsa Exportar para generar el <code>.nelkmod</code> firmado.</li><li>Guarda una copia privada de la clave de firma. Las futuras versiones deben conservar la identidad del autor.</li></ol>\n<p>Al abrir Studio desde Inicio, la clave de firma está en <code>%LOCALAPPDATA%\\Nelkano\\SDK\\.nelkdev\\author.pem</code>. Si abres una carpeta con la CLI, se guarda en <code>.nelkdev/author.pem</code> dentro de esa carpeta. No incluyas la clave en <code>files</code> ni la publiques en Git.</p>\n<h2>Exportar desde la terminal</h2>\n<p>Crea primero <code>C:/claves</code>. Usa destinos que todavía no existan para generar las claves y el paquete:</p>\n<pre><code>.\\nelkmod.cmd check C:/mis-modulos/mi-modulo\n.\\nelkmod.cmd keygen C:/claves/author.pem C:/claves/author-public.pem\n.\\nelkmod.cmd pack C:/mis-modulos/mi-modulo C:/claves/author.pem C:/mis-modulos/mi-modulo.nelkmod\n.\\nelkmod.cmd verify C:/mis-modulos/mi-modulo.nelkmod</code></pre>\n<h2>Instalar el archivo en Nelkano</h2>\n<ol><li>Abre Ajustes → General → Módulos → Instalados.</li><li>Pulsa Importar módulo y elige el archivo <code>.nelkmod</code>.</li><li>Revisa los permisos y pulsa Instalar.</li></ol>\n<p>Para importar una actualización, desactiva primero el módulo. El nuevo archivo debe conservar su ID y la clave del autor.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-06-exportado-nelkmod.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-06-exportado-nelkmod.webp\" width=\"1440\" height=\"1000\" alt=\"Archivo .nelkmod exportado desde Studio.\" data-caption=\"Archivo .nelkmod exportado desde Studio.\"></a>\n<h2>Instalar desde el catálogo</h2>\n<ol><li>Abre Ajustes → General → Módulos → Explorar.</li><li>Busca por nombre o sistema y abre la ficha.</li><li>Pulsa Instalar o Actualizar. La app descarga y verifica el paquete.</li><li>Revisa el autor, la compatibilidad y los permisos. Pulsa Instalar para aceptarlos.</li></ol>\n<p>La verificación comprueba tamaño, SHA-256, firma Ed25519, autor y compatibilidad. La firma identifica al autor y detecta cambios en sus archivos; sigue siendo necesario revisar lo que permite hacer al módulo.</p>\n<p>La ficha agrupa la información adicional en Compatibilidad, Versiones y Detalles técnicos. Este último permite copiar identificadores, dependencias e identidad del autor.</p>\n<p>En Instalados puedes activar y desactivar módulos opcionales. Desinstalar pide confirmación y conserva sus datos, salvo que marques Eliminar también sus datos. Los de Módulos esenciales vienen con Nelkano y se actualizan con la app.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-11-install-review.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-11-install-review.webp\" width=\"600\" height=\"672\" alt=\"Autor, compatibilidad y permisos antes de instalar (ejemplo).\" data-caption=\"Autor, compatibilidad y permisos antes de instalar (ejemplo).\"></a>\n<h2>Publicar una nueva versión</h2>\n<ol><li>Exporta un paquete con el mismo ID y la misma clave de autor.</li><li>Aumenta la versión <code>X.Y.Z</code>, por ejemplo de <code>0.1.0</code> a <code>0.1.1</code>.</li><li>En la ficha del catálogo web, pulsa Añadir versión y explica los cambios.</li><li>Para un módulo público, facilita su enlace directo de descarga. Para Sólo Tester, sube el archivo privado desde una cuenta con permiso de administración de módulos.</li></ol>\n<p>El servidor verifica el paquete y conserva las versiones anteriores con sus hashes. Reparar una versión ya registrada exige subir exactamente los mismos bytes.</p>\n<h2>Cómo recibe la app las actualizaciones</h2>\n<p>Nelkano comprueba al arrancar y al volver al primer plano, con seis horas entre comprobaciones automáticas. Puedes comprobar antes desde Módulos → Instalados → Comprobar actualizaciones.</p>\n<p>Se ofrecen versiones superiores de módulos opcionales, con el mismo autor y compatibles con el SDK y la plataforma actuales. Cuando la app está libre, el aviso permite Ver actualizaciones o Más tarde. También aparece Actualizar módulo en Instalados; no se envía una notificación del sistema operativo.</p>\n<ol><li>Pulsa Actualizar módulo para abrir la ficha.</li><li>Si está activo, confirma Desactivar y continuar.</li><li>Espera a la descarga y verificación.</li><li>Revisa todos los permisos, incluidos los nuevos, y pulsa Instalar.</li></ol>\n<p>Cancelar la descarga o la revisión no instala nada. La actualización conserva los datos y recupera la instalación anterior si falla la sustitución. Gestionar módulo abre su entrada en Instalados cuando corresponde. También puedes actualizar mediante Importar módulo, con el módulo desactivado y la misma clave.</p>\n<p>Los esenciales se actualizan con Nelkano. En primer plano, la app revisa el acceso de la cuenta cada minuto: cerrar sesión o perder el rol o permiso retira las actualizaciones privadas.</p>\n<h2>Distribuir un módulo Sólo Tester</h2>\n<p>Una cuenta con permiso de administración de módulos puede crear la ficha con acceso Sólo Tester y subir el <code>.nelkmod</code> firmado. El archivo se aloja en almacenamiento privado de la web; la ficha no utiliza repositorio ni admite imágenes públicas.</p>\n<p>El acceso se elige al crear la ficha. Sus siguientes versiones deben mantener el ID del módulo y la clave del autor. Se admiten paquetes V1 de hasta 32 MiB, comprobando firma, inventario, tamaño y SHA-256.</p>\n<p>Tester y quienes administran módulos ven y descargan tanto los públicos como los reservados. Las demás cuentas y los visitantes sólo ven los públicos. La app utiliza la sesión para autorizar cada descarga privada; cerrar sesión o perder el acceso retira sus fichas, versiones y descargas. La instalación sigue exigiendo verificación y revisión de permisos.</p>\n<p>Los módulos públicos usan un enlace externo de descarga y pueden añadir un repositorio opcional.</p>\n<h2>Elegir JavaScript o TypeScript</h2>\n<p>Ambos sirven. En TypeScript importa los tipos con <code>import type { Host, Extension } from '@nelkano/sdk'</code>. Para archivos locales usa rutas como <code>./model.js</code>.</p>\n<p>V1 contiene JavaScript y recursos; no incluye Node.js ni librerías npm ejecutables. Revisa <code>files</code> antes de exportar y deja fuera las ROMs y las claves privadas. V2 añade motores nativos compilados, descritos más abajo.</p>\n<h2>Elegir catálogo o importación local</h2>\n<p>El catálogo acepta paquetes V1 de hasta 32 MiB y sólo los permisos indicados como admitidos en Permisos y datos. Si tu V1 utiliza otros, como <code>network.external</code>, <code>modules.services</code>, <code>runtime.tasks</code> o <code>emulation.input</code>, distribúyelo para Importar módulo. Los paquetes V2 con motores nativos también requieren importación local.</p>\n<h2>Preparar tareas persistentes</h2>\n<p>Una tarea creada con <code>tasks.schedule({id,delayMs,intervalMs?,payload?,background:true})</code> puede iniciar el módulo y llamar a <code>onBackground(event)</code> sin ejecutar <code>activate()</code>.</p>\n<ul><li>Construye el objeto en <code>createModule(host)</code> sin inicializar la interfaz ni depender de variables preparadas en <code>activate()</code>.</li><li>Lee la tarea vencida del evento y publica progreso y resultado con <code>tasks.update</code>.</li><li>Guarda puntos de recuperación y evita repetir efectos: el sistema puede volver a intentar una ejecución interrumpida.</li></ul>\n<p>Las tareas normales de la app siguen usando <code>onEvent</code> con <code>task-due</code>.</p>\n<h2>Paquete V2 con un motor compilado</h2>\n<ol><li>Prepara <code>native/build.json</code> con repositorio, commit, receta y hashes de las herramientas.</li><li>Compila en tu PC con <code>nelkmod engine build ./mi-modulo --target windows-x86_64</code>.</li><li>Declara el motor y sus artefactos en <code>nativeEngines</code> del manifiesto, con <code>formatVersion:2</code>.</li><li>Para escritorio, usa <code>nelkmod engine archive</code> para reunir <code>nk-engine-worker(.exe)</code>, la DLL/SO y sus dependencias.</li><li>Ejecuta <code>nelkmod pack</code> para generar el único <code>.nelkmod</code> que importará el usuario.</li></ol>\n<p>La ABI, el ejecutor, la plantilla Android y un ejemplo están en <code>native/</code>, dentro de la instalación de Studio.</p>\n<h2>Fijar las herramientas de compilación</h2>\n<p>La receta comprueba el commit y los cambios en archivos seguidos por Git. Cada ejecutable debe ser un alias o una ruta absoluta de la lista verificada: se comprueba su hash antes de ejecutarlo, sin buscarlo en <code>PATH</code>. Fija también los compiladores y las herramientas invocadas por CMake.</p>\n<p>La compilación sólo se ejecuta al pedirla. Abrir o guardar el proyecto no lanza la receta.</p>\n<h2>Describir el motor y sus destinos</h2>\n<p><code>nativeEngines</code> declara motor, release, protocolo/ABI, descriptor y hash de su JSON canónico. El descriptor recoge sistemas y extensiones, controles, opciones, recursos o BIOS, API gráficas y compatibilidad de estados y almacenamiento.</p>\n<p>Cada artefacto indica target, ruta, tamaño y SHA-256: <code>engines/id/android-arm64-v8a.apk</code>, <code>windows-x86_64.zip</code> o <code>linux-x86_64.zip</code>. Incluye sus dependencias.</p>\n<ul><li>Android: declara también <code>packageId</code>, <code>service</code> y <code>certificateSha256</code>.</li><li>Escritorio: declara el <code>entryPoint</code> de la DLL/SO.</li><li><code>minApi</code>, CPU y runtime describen restricciones; no hacen compatible un binario con otra plataforma.</li></ul>\n<h2>Límites del paquete V2</h2>\n<p>El contenedor usa ZIP32 STORE. Manifest y firma van primero; no admite ZIP64, cifrado, enlaces, campos extra ni descriptores de datos. La firma Ed25519 incluye la lista de archivos, tamaños y hashes de todos los destinos.</p>\n<ul><li>Paquete completo: 2 GiB, hasta 16 artefactos y 128 entradas combinadas.</li><li>Artefacto nativo: 1 GiB.</li><li>JavaScript y recursos: 8 MiB por archivo y 32 MiB acumulados.</li><li>Metadatos: 128 KiB por archivo.</li><li>ZIP interior de escritorio: 4096 entradas y 2 GiB expandidos, con rutas y tipos comprobados.</li></ul>\n<p>La verificación lee los artefactos por tramos y sólo extrae a disco el destino actual; no los carga en el mapa de bytes de JavaScript. Los paquetes V1 siguen siendo compatibles.</p>\n<h2>Instalar y recuperar un motor</h2>\n<p>En Android, prepara un APK sin interfaz, firmado con tu certificado, distinto del de Nelkano. La app comprueba paquete, servicio, certificado y contrato antes de conceder recursos y superficie gráfica.</p>\n<p>El motor utiliza un proceso con UID propio. La ruta gráfica emplea un UID normal dedicado, sin permisos de red o almacenamiento. En escritorio, el proceso separado utiliza los permisos del usuario. La firma identifica el código; instalarlo requiere confiar en su autor.</p>\n<ol><li>Cada versión del APK usa una identidad instalable propia para conservar la anterior.</li><li>Nelkano guarda la aprobación del módulo, autor y hash antes de abrir el instalador de Android.</li><li>Al volver, comprueba la instalación y retoma esa aprobación. Si cancelas, conserva la revisión pendiente.</li><li>Activa el JavaScript después de verificar archivos y conexión inicial con el motor.</li></ol>\n<p>Recuperar una instalación anterior no rebaja automáticamente un APK ya instalado. Android sigue gestionando esos APK y su desinstalación. El paquete se puede importar localmente sin publicarlo en el catálogo.</p>"},{"id":"de520020-2026-4000-8000-000000000011","language":"es","title":"Usar SDK Studio","path":"/es/sdk-de-nelkano/simulador","content":"<p>Studio reúne el editor del módulo, un visor de Nelkano y las herramientas para probarlo y exportarlo.</p>\n<h2>Instalar y abrir Studio</h2><ol><li>Descarga el instalador de SDK Studio para Windows x64 desde <a href=\"/modulos\">Módulos</a>.</li><li>Ejecuta el <code>.exe</code> y completa la instalación.</li><li>Abre <strong>Nelkano SDK Studio</strong> desde Inicio o el acceso del escritorio.</li></ol><p>Node.js y Java vienen incluidos. Crea tus proyectos en una carpeta propia, fuera de la instalación. El manifiesto del módulo utiliza <code>sdkVersion: \"0.2.0\"</code>.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-01-studio-editor-visor.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-01-studio-editor-visor.webp\" width=\"1440\" height=\"1000\" alt=\"El proyecto Hola en Studio: código y visor.\" data-caption=\"El proyecto Hola en Studio: código y visor.\"></a><h2>Editar y probar un cambio</h2><ol><li>Selecciona el proyecto a la izquierda y abre un archivo en el editor central.</li><li>Edita el código y guarda con Ctrl+S. El visor de la derecha se recompila y recarga.</li><li>Prueba la acción en el visor. Si la compilación falla, revisa Problemas: el visor conserva el último código válido.</li></ol><p>Puedes crear archivos y carpetas e importar PNG y TTF. Los archivos pendientes de guardar llevan un asterisco. Si otro programa cambia uno que estás editando, Studio avisa antes de sobrescribirlo.</p><ul><li><strong>Ctrl+Mayús+S:</strong> guardar todos los archivos.</li><li><strong>Ctrl+F:</strong> buscar en el archivo.</li><li><strong>Intro o F3:</strong> siguiente coincidencia; Mayús+F3 vuelve a la anterior.</li></ul>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-04-arbol-manifest.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-04-arbol-manifest.webp\" width=\"1440\" height=\"1000\" alt=\"Archivos del proyecto y manifest.json en Studio.\" data-caption=\"Archivos del proyecto y manifest.json en Studio.\"></a><h2>Qué comprueba el visor</h2><p>El visor reproduce las pantallas de Nelkano con la presentación Android. Incluye biblioteca, fichas, guardados y ajustes de prueba. Permite comprobar la navegación y las acciones del módulo, pero no ejecuta ROMs ni reproduce audio o mandos físicos.</p><p>Desde Ajustes puedes cambiar entre vertical y horizontal. El visor utiliza 420 × 800 y 800 × 420 puntos, ajustados al espacio disponible. Restablecer datos recupera los juegos de ejemplo y borra las preferencias simuladas, tras pedir confirmación.</p><p>Prueba también el paquete en cada plataforma de destino. Guardar el código recarga tu módulo, pero las actualizaciones de las pantallas compartidas necesitan una nueva distribución de Studio.</p>\n<h2>Revisar los módulos en el visor</h2><p>Abre Ajustes → General → Módulos. Instalados muestra los proyectos de ejemplo y separa los opcionales; su catálogo simulado no acredita los paquetes incluidos en la app. Explorar usa un catálogo local de los proyectos de tu carpeta, con búsqueda, filtros y fichas.</p><p>Este catálogo no consulta producción ni instala paquetes descargados. Comprobar actualizaciones revisa los metadatos locales. La comprobación de Actualización de Nelkano también es local y no instala una versión de la app.</p><p>Para comprobar la retirada de un proyecto, utiliza su interruptor en el panel Módulos de Studio. Esta herramienta de desarrollo permite apagar proyectos aunque el visor los presente como esenciales.</p>\n<h2>Trabajar con Codex o Claude Code</h2>\n<ol><li>Instala el CLI del agente y comprueba que <code>codex</code> o <code>claude</code> funciona en una terminal.</li><li>Inicia sesión con <code>codex login</code> o abriendo <code>claude</code>.</li><li>En Studio, abre Ajustes → Chat. Elige un modelo de la lista y, si ofrece niveles, selecciona uno. Escribe qué quieres cambiar en el módulo.</li></ol>\n<p>Studio consulta los modelos y niveles que comunica la sesión del CLI; no mantiene una lista fija. Si no puede confirmar la sesión o recibir un catálogo, explica el motivo y no ofrece modelos de ese agente. En Codex, comprueba las instalaciones detectadas y utiliza la más reciente. El servicio comprueba el acceso al enviar cada petición.</p>\n<p>El chat trabaja en la carpeta del proyecto seleccionado. Antes de enviar, Studio guarda los archivos; al terminar, actualiza el editor y el visor. Detener cancela el proceso y conserva las ediciones ya hechas. Las conversaciones se guardan por carpeta y agente.</p>\n<h3>Qué documentación puede consultar el chat</h3>\n<p>Antes de cada mensaje, también al reanudar una conversación, Studio prepara <code>.nelkano-sdk</code> dentro del proyecto. Su <code>README.md</code> contiene el índice de:</p>\n<ul><li>El libro público completo del SDK, organizado por capítulos.</li><li>Los tipos TypeScript de la entrega instalada.</li><li>Los ejemplos públicos incluidos en el SDK.</li></ul>\n<p>El agente recibe la ruta del índice y la revisión de la referencia, y puede leer los capítulos, tipos y ejemplos que necesite. Las firmas de los tipos instalados determinan qué API puede utilizar. Esta carpeta es generada por Studio y se excluye de Git; no la edites como parte de tu módulo.</p>\n<p>Al enviar mensajes, Studio revalida el libro con ETag si han pasado cinco minutos desde la última consulta. Si no hay conexión, conserva la última copia válida o utiliza la incluida en el SDK. Los tipos y ejemplos proceden de la instalación local: actualizar el libro no añade API a una entrega antigua.</p>\n<p>La fuente predeterminada es <a href=\"https://nelkano.com/api/nelkano/v1/sdk/reference\">la referencia pública del SDK</a>. Si desarrollas Studio contra otra web, puedes cambiarla con <code>NELKANO_SDK_REFERENCE_URL</code> o con <code>tooling/reference/source.local.json</code>. Ese archivo es configuración del entorno local y no se distribuye.</p>\n<p>El CLI utiliza tu sesión y configuración para enviar la petición y los archivos que lea a su servicio. Studio no instala el agente ni inicia sesión por ti. El chat tampoco exporta el módulo, publica versiones o hace operaciones de Git automáticamente.</p>\n<h2>Exportar el módulo</h2><ol><li>Guarda los archivos y resuelve los errores de compilación.</li><li>Elige Exportar .nelkmod en Ajustes.</li><li>Instala el archivo firmado en la app y prueba la misma tarea.</li></ol><p>Studio utiliza una identidad de firma estable. Consérvala para las actualizaciones del módulo. Para empaquetar con otra clave utiliza la CLI; Studio no tiene selector de clave.</p>\n<h2>Probar Amigos y los controles</h2><p>Crea un proyecto con la plantilla Amigos (simulación). En Biblioteca del visor, pulsa Amigos y accede con demo@nelkano.test y Nelkano123. El registro y las fotos son ficticios; no utilices credenciales reales.</p><p>Los juegos de prueba incluyen PSP, GameCube y Switch para revisar sus controles. Switch es solo una simulación: su core no está incluido en las apps Android ni Desktop. Si no aparecen en una biblioteca simulada antigua, restablece los datos del visor. Las pulsaciones físicas se comprueban después en el dispositivo.</p>\n<h2>Explorar memoria y generar código</h2><ol><li>Abre una sesión GBA de prueba en el visor.</li><li>Entra en Ajustes → Explorar memoria y elige la dirección, el tipo y el rango.</li><li>Lee los valores o inicia una búsqueda conocida o desconocida. Cambia el valor de prueba y filtra los resultados.</li><li>Selecciona un resultado y genera una lectura, escritura comprobada u observación en JavaScript o TypeScript.</li><li>Copia la función a tu módulo y declara sus permisos.</li></ol><p>El explorador utiliza memoria simulada. Consulta <a href=\"/es/sdk-de-nelkano/modificar-juego\">Modificar un juego</a> para los pasos y límites de cada operación.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-08-explorador-memoria.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-08-explorador-memoria.webp\" width=\"1440\" height=\"1000\" alt=\"Explorador de memoria con una sesión GBA simulada.\" data-caption=\"Explorador de memoria con una sesión GBA simulada.\"></a><h2>Usar las herramientas de terminal</h2><p>Abre PowerShell en la carpeta de instalación y ejecuta <code>.\\nelkmod.cmd</code> con el comando que necesites. Utiliza rutas completas para tus proyectos; el lanzador usa el Node.js incluido. Para los motores V2 debes instalar también las herramientas de compilación que requiera tu receta.</p><h2>Probar una integración multijugador</h2><pre><code>.\\nelkmod.cmd example multiplayer C:/mis-modulos/mi-multiplayer\n.\\nelkmod.cmd example multiplayer-adapter C:/mis-modulos/mi-adaptador</code></pre><p>Carga Amigos, Multijugador y el adaptador de ejemplo. Abre la sesión ficticia de CHIP-8 y comprueba sus botones y mensajes. Las cuentas, partidas y conexiones multijugador son simuladas.</p><p>Los formularios de un módulo se abren desde la pantalla que amplía. Si requieren cuenta, inicia sesión con una ficticia. La simulación mantiene las comprobaciones de identidad y permisos; la conexión real se prueba al exportar.</p>\n<h2>Abrir varios proyectos</h2><p>Ejecuta <code>.\\nelkmod.cmd dev C:/mis-modulos</code> o usa Ajustes → Seleccionar carpeta de módulos. Studio detecta los manifiestos de las subcarpetas inmediatas y admite hasta doce proyectos; omite carpetas sin manifiesto y enlaces simbólicos.</p><p>El desplegable de Proyectos cambia el árbol y las pestañas al módulo seleccionado, conservando los borradores de los demás. Crear archivos e importar recursos actúa sobre ese módulo. También puedes abrir directamente un proyecto incompleto para repararlo. La ruta de trabajo aparece al pie y se puede copiar.</p>\n<h2>Encontrar y resolver problemas</h2><p>Abre el panel inferior desde Ajustes. Sus tres pestañas sirven para:</p><ul><li><strong>Problemas:</strong> identificar el módulo y el error. Detalles permite copiar el mensaje; Abrir ruta[:línea] lleva al archivo si hay una ubicación válida.</li><li><strong>Registro:</strong> seguir una operación, filtrando por módulo o tipo. Limpiar vista oculta las entradas anteriores. Pausar congela la vista mientras el módulo sigue trabajando; Reanudar incorpora las nuevas.</li><li><strong>Módulos:</strong> consultar versión, estado, permisos e integraciones. Desactiva y reactiva un proyecto para comprobar cómo retira y recupera sus elementos.</li></ul>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-05-problema-ubicacion.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-05-problema-ubicacion.webp\" width=\"1440\" height=\"1000\" alt=\"Error de compilación y su ubicación en Studio.\" data-caption=\"Error de compilación y su ubicación en Studio.\"></a><h2>Ajustar el espacio de trabajo</h2><p>Arrastra los separadores para ampliar el editor o el visor. En ventanas pequeñas puedes alternar Editor y Vista previa; Proyectos se repliega cuando falta espacio. Ajustes permite recuperar los paneles cerrados y Restablecer distribución vuelve a la disposición inicial conservando los datos.</p>\n<h2>Probar conexiones y tareas</h2><p><code>connections</code> realiza HTTP y WebSocket reales a los destinos autorizados. Usa servidores y credenciales de prueba. Las tareas del entorno de desarrollo se eliminan al detener o reconstruir el módulo.</p>\n<h2>Actualizar Studio</h2><ol><li>En Windows x64, abre Ajustes → Actualización de SDK Studio.</li><li>Descarga la versión disponible. Puedes cancelar o posponer.</li><li>Guarda los archivos y termina las operaciones y los agentes antes de instalar.</li></ol><p>Studio verifica el ZIP oficial, cierra el programa y abre la versión nueva con tus proyectos. Conserva fuentes, claves y datos de desarrollo. La actualización incluye las herramientas, Node.js y Java.</p><p>La comprobación también se realiza al abrir, recuperar el foco y cada seis horas. La primera entrega se instala por el procedimiento habitual; las siguientes aparecerán cuando estén disponibles en el canal de nelkano.com.</p>\n<h2>Probar transferencias</h2><p><code>host.transfers</code> utiliza juegos y guardados sintéticos. Permite comprobar bloques, hashes, revisiones, reintentos, revocación y caducidad entre entornos de desarrollo.</p><p>En Studio, los límites son 32 MiB por archivo, 127 miembros y 8 MiB por payload; en la app nativa, hasta 64 GiB y 100 000 archivos. Los paquetes de prueba solo se importan en Studio. La compatibilidad de estados reales y el rendimiento se comprueban en los dispositivos de destino.</p>\n<h2>Probar funciones de hardware</h2><p>Studio ofrece datos de prueba para traducción, capturas, clips, packs, gráficos, micrófono, audio, timeline y cámara. Consulta <code>capabilities</code> y <code>simulated</code> para distinguir el soporte simulado.</p><p>Puedes comprobar argumentos, formularios, permisos, conflictos y liberación de recursos. Las repeticiones del visor no son compatibles con un motor nativo. La exportación de vídeo se prueba con el selector de documentos de la app instalada. Los perfiles sí utilizan y validan los documentos registrados por los proveedores del entorno.</p>\n<h3>Tareas persistentes</h3><p>Studio no ejecuta el planificador nativo de <code>onBackground</code>. Sus temporizadores avisan mediante <code>onEvent</code> y anuncian <code>persistent:false</code> y <code>closedApp:false</code>.</p><p>Prueba las tareas persistentes en la app. El callback debe funcionar tanto en una instancia nueva sin <code>activate()</code> como en una ya activa. El sistema operativo decide cuándo ejecutarlo.</p>\n<h2>Compilar y exportar un motor V2</h2><p>Prepara la receta de tu proyecto y selecciona <strong>Compilar motores nativos · receta del proyecto</strong>. Studio la ejecuta en tu PC y muestra los resultados. Revisa esa receta y sus herramientas antes de ejecutarlas: son código del autor.</p><p>Exportar genera un .nelkmod autocontenido y aplica las mismas verificaciones V2 que la CLI.</p><p>El visor simula <code>host.engines</code>: selección de motor, controles, opciones, recursos y medios, con <code>simulated:true</code> y <code>native:false</code>. Los binarios del motor, la GPU, los estados y el rendimiento se prueban en Android o escritorio.</p>\n"},{"id":"de520020-2026-4000-8000-000000000008","language":"es","title":"Tu primer botón","path":"/es/sdk-de-nelkano/ejemplo-minimo","content":"<p>Vas a añadir la opción Hola a Ajustes. Al pulsarla se abrirá un panel llamado Hola desde mi módulo. Este ejemplo no necesita permisos porque no consulta ni modifica datos de la app.</p>\n<h2>Preparar el proyecto</h2>\n<ol><li>Crea un módulo con la plantilla Básico.</li><li>Abre <code>src/index.js</code>, o créalo si la plantilla utiliza TypeScript.</li><li>Copia el código del siguiente apartado.</li></ol>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-01-studio-editor-visor.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-01-studio-editor-visor.webp\" width=\"1440\" height=\"1000\" alt=\"El proyecto Hola en Studio: código y visor.\" data-caption=\"El proyecto Hola en Studio: código y visor.\"></a>\n<h2>Añadir la opción y abrir el panel</h2>\n<pre><code>export function createModule(host) {\n  return {\n    async activate() {},\n    async deactivate() {},\n    async contributions(context) {\n      if (context.surface !== 'settings') return [];\n      return [{\n        moduleId: 'example.hello', id: 'hello',\n        slot: 'SETTINGS_SECTION', section: 'Mis módulos',\n        label: 'Hola', actionId: 'open', icon: 'standard:settings'\n      }];\n    },\n    async dispatch(action) {\n      if (action === 'close') return { kind: 'close-panel' };\n      return {\n        kind: 'panel', layout: 'form', id: 'hello',\n        title: 'Hola desde mi módulo', dismissAction: 'close',\n        fields: [], rows: [],\n        actions: [{ actionId: 'close', label: 'Cerrar', submitFields: false }]\n      };\n    }\n  };\n}</code></pre>\n<p><code>contributions</code> ofrece la opción sólo en Ajustes. <code>dispatch</code> abre el panel al pulsarla y lo cierra con la acción <code>close</code>. El panel no tiene campos ni filas: primero comprobarás que la integración funciona.</p>\n<h2>Indicar el archivo de entrada</h2>\n<p>Guarda el código en <code>src/index.js</code> y comprueba estos valores de <code>manifest.json</code>:</p>\n<ul><li><code>\"id\":\"example.hello\"</code></li><li><code>\"entry\":\"src/index.js\"</code></li><li><code>\"files\":[\"src/index.js\"]</code></li></ul>\n<p>Si la plantilla usaba <code>src/index.ts</code>, sustituye esa ruta en <code>entry</code> y <code>files</code>. El código anterior se copia sin cambios en el archivo JavaScript.</p>\n<h2>Probarlo en el visor</h2>\n<ol><li>Guarda el proyecto para actualizar Studio.</li><li>Abre Ajustes en el visor y pulsa Hola.</li><li>Comprueba que aparece el panel y que puedes cerrarlo.</li></ol>\n<p>Para dar una función al botón, continúa con <a href=\"/es/sdk-de-nelkano/emulacion\">Emulación: operaciones y eventos</a>, donde puedes añadir pausa o guardado.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-03-hola-panel.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-03-hola-panel.webp\" width=\"446\" height=\"712\" alt=\"El botón Hola abre el panel del módulo en Studio.\" data-caption=\"El botón Hola abre el panel del módulo en Studio.\"></a>"},{"id":"de520020-2026-4000-8000-000000000004","language":"es","title":"Permisos y datos","path":"/es/sdk-de-nelkano/permisos","content":"<p>Declara en <code>capabilities</code> los permisos que necesita tu módulo. Por ejemplo, <code>storage.private.cas</code> para guardar un ajuste y <code>library.read</code> para consultar juegos. La app los muestra al instalar y rechaza las llamadas no autorizadas.</p>\n<h2>Permisos básicos</h2>\n<ul><li><strong>library.read</strong>: Consultar juegos.</li><li><strong>library.delete</strong>: Pedir un borrado con confirmación del usuario.</li><li><strong>library.launch</strong>: Abrir o continuar un juego de la biblioteca; puedes indicar un slot de guardado.</li><li><strong>library.cover</strong>: Usar la captura de un guardado como carátula o restaurar la portada.</li><li><strong>saves.read</strong>: Consultar los guardados.</li><li><strong>saves.write</strong>: Guardar la partida actual en un slot manual.</li><li><strong>saves.load</strong>: Cargar un guardado en la partida actual.</li><li><strong>saves.autosave</strong>: Solicitar autoguardados.</li><li><strong>emulation.read</strong>: Consultar la partida y recibir sus eventos.</li><li><strong>emulation.control</strong>: Pausar, reanudar y reiniciar.</li><li><strong>presentation.player</strong>: Cambiar pantalla completa, estiramiento, pantalla NDS/3DS y slot elegido.</li><li><strong>storage.private.cas</strong>: Guardar preferencias propias.</li><li><strong>modules.hooks</strong>: Usar y ofrecer ganchos entre módulos.</li><li><strong>environment.display</strong>: Leer tamaño y densidad de pantalla.</li><li><strong>presentation.control-layouts</strong>: Publicar posiciones, tamaños e iconos de controles.</li><li><strong>presentation.appearance</strong>: Ofrecer estilos de apariencia.</li></ul>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-11-install-review.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-11-install-review.webp\" width=\"600\" height=\"672\" alt=\"Autor, compatibilidad y permisos antes de instalar (ejemplo).\" data-caption=\"Autor, compatibilidad y permisos antes de instalar (ejemplo).\"></a>\n<h2>Guardar una preferencia</h2>\n<pre><code>const doc = await host.storage.read('preferences');\nconst saved = await host.storage.compareAndSwap(\n  'preferences', doc.revision, { enabled: true }\n);\nif (!saved) throw new Error('El ajuste cambió. Vuelve a intentarlo.');</code></pre>\n<p>La revisión evita sobrescribir un cambio más reciente. Los datos pertenecen a tu módulo y autor: desactivar los conserva; al desinstalar, el usuario puede borrarlos.</p>\n<h2>Consultar o borrar juegos</h2>\n<pre><code>const games = await host.library.list();\nif (games.length === 0) throw new Error('La biblioteca está vacía.');\nawait host.library.requestDelete([games[0].id]);</code></pre>\n<p><code>requestDelete</code> pide confirmación y permite cancelar. En Android elimina la copia privada del juego y sus guardados, conservando el archivo original importado. Studio sólo elimina datos ficticios.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-07-modulos-permisos.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-07-modulos-permisos.webp\" width=\"1440\" height=\"440\" alt=\"Permisos de Colecciones en la consola de Studio.\" data-caption=\"Permisos de Colecciones en la consola de Studio.\"></a>\n<h2>Acceder a la cuenta</h2>\n<ul><li><code>account.session</code>: identidad, inicio de sesión, registro y cierre de sesión.</li><li><code>network.account</code>: peticiones a los endpoints de cuenta aprobados de <code>nelkano.com</code>.</li><li><code>resources.image</code>: referencia de una foto elegida por el usuario, sin revelar su ruta privada.</li></ul>\n<p>Android utiliza una sesión cifrada y permisos firmados. Studio utiliza cuentas ficticias. Para conectar con otro servidor necesitas <code>network.external</code>.</p>\n<h2>Usar funciones del dispositivo</h2>\n<ul><li><code>input.gamepad</code>: detectar mandos y aplicar o capturar asignaciones.</li><li><code>media.second-screen</code>: buscar y conectar un receptor local.</li><li><code>media.streaming</code>: iniciar y detener WebRTC con confirmación del usuario.</li><li><code>appearance.locale</code>: aplicar un diccionario. Elegir un tema requiere también <code>presentation.appearance</code>.</li><li><code>resources.patch</code>: elegir un parche y crear una copia del juego.</li></ul>\n<h2>Qué ocurre si falla un módulo</h2>\n<p>Actualizar la app o perder el proceso no cambia la elección de activar o desactivar un módulo opcional. En Android y escritorio, la recuperación en primer plano funciona así:</p>\n<ul><li>Esenciales: se vuelven a iniciar ante un fallo que detenga su ejecución, sin límite de intentos y con una espera creciente de hasta 30 segundos.</li><li>Opcionales: hasta tres intentos por ronda ante cierres recuperables. Los errores de código se muestran sin repetirlos indefinidamente.</li><li>Una acción que falla sin dañar el proceso muestra el error y mantiene el módulo.</li></ul>\n<p>Los cinco esenciales permanecen habilitados: Apariencia, Nelkano básicos, Editor de controles, Amigos y Multijugador. Si una versión anterior los dejó apagados o desinstalados, la app los recupera con los datos que existan.</p>\n<h2>Trabajar con memoria, recursos y multijugador</h2>\n<ul><li><strong>emulation.memory.read</strong>: Leer rangos de memoria GBA permitidos.</li><li><strong>emulation.memory.write</strong>: Cambiar RAM con comprobación de los bytes anteriores.</li><li><strong>emulation.resources</strong>: Leer o buscar en la ROM y obtener una imagen reducida de la emulación.</li><li><strong>emulation.handoff</strong>: Crear y restaurar checkpoints, preparar una copia privada de un proyecto y usar su puente RGSS.</li><li><strong>presentation.overlay</strong>: Dibujar imágenes y etiquetas sobre la pantalla del juego.</li><li><strong>network.multiplayer</strong>: Conectar con un amigo aceptado usando la cuenta de Nelkano y su relay.</li><li><strong>runtime.events</strong>: Suscribirse al aviso runtime-tick de la sesión activa.</li></ul>\n<p>Consulta las capacidades antes de ofrecer una acción que dependa del motor. Usa la sesión actual: una referencia antigua se rechaza. El SDK entrega datos y referencias limitadas, sin revelar al JavaScript rutas privadas ni tokens de cuenta.</p>\n<h2>Modificar un juego durante la sesión</h2>\n<p><code>host.gameMod</code> requiere <code>emulation.game-mod</code> y el permiso de cada operación:</p>\n<ul><li><code>emulation.read</code>: consultar capacidades.</li><li><code>emulation.memory.read</code>: leer y listar reglas.</li><li><code>emulation.memory.write</code>: escribir o retirar una regla.</li><li>Lectura y escritura: instalar reglas.</li></ul>\n<p>Los hosts antiguos rechazan el permiso nuevo; no ejecutan el paquete parcialmente. Consulta los límites y la retirada de cambios en <a href=\"/es/sdk-de-nelkano/modificar-juego\">Modificar un juego</a>. Estos permisos no dan acceso a archivos privados ni envían datos a servidores.</p>\n<h2>Guardar registros y consultarlos</h2>\n<p>Con <code>storage.entities</code>, define los tipos mediante <code>entities.define({name,version,schema,indexes,relations})</code>. Después utiliza <code>get</code>, <code>query</code>, <code>transact</code> y <code>migrate</code>.</p>\n<p>Admite relaciones dentro del módulo, filtros de igualdad, índices, ordenación y páginas. Los lotes usan <code>expectedRevision</code> y se aplican completos; las migraciones son transaccionales.</p>\n<ul><li>64 tipos y 10.000 registros por tipo.</li><li>32 KiB por registro y 16 MiB por identidad.</li><li>128 cambios por lote; respuestas de consultas y lotes de hasta 96 KiB.</li></ul>\n<p>Los datos pertenecen al módulo y autor. Se guardan localmente en SQLite en Android/escritorio y en JSON en Studio; no se suben a Nelkano. Una actualización fallida recupera las entidades anteriores. Las preferencias CAS mantienen su formato.</p>\n<h2>Conectar con otro servidor</h2>\n<ol><li>Declara <code>network.external</code> y los destinos en <code>origins</code>, con origen HTTP o HTTPS, servidor y puerto concretos.</li><li>Usa <code>connections.request/socket/read/send/close</code>. Un origen autoriza también su WebSocket WS o WSS correspondiente.</li><li>Para añadir un destino, solicita <code>requestOrigin</code>. La decisión se conserva para el módulo y autor.</li></ol>\n<p>Android admite HTTPS/WSS; escritorio y Studio también HTTP/WS. Las peticiones no heredan cookies ni claves de la cuenta Nelkano y no siguen redirecciones.</p>\n<p>Hay hasta 12 trabajos simultáneos: 8 HTTP y 4 sockets. Los cuerpos y mensajes admiten 64 KiB. <code>onEvent</code> recibe <code>connection-changed</code> y <code>connection-origin-reviewed</code>; desactivar cancela las conexiones. Studio sí utiliza la red: usa datos de prueba.</p>\n<h2>Programar una tarea</h2>\n<p>Con <code>runtime.tasks</code> puedes consultar <code>host.tasks.capabilities()</code> y utilizar:</p>\n<ul><li><code>schedule({id,delayMs,intervalMs?,payload?,background?})</code>: programar.</li><li><code>read(id)</code>: consultar el estado.</li><li><code>update(id,{state?,progress?,message?,result?})</code>: publicar avances y resultado.</li><li><code>cancel(id)</code>: cancelar.</li></ul>\n<p>Las tareas normales entregan <code>task-due</code> a <code>onEvent</code>. Admiten 32 registros por módulo, intervalos de 100 ms a 24 h y datos de 32 KiB. Comprueba <code>persistent</code> y <code>closedApp</code> antes de ofrecer trabajo con la interfaz cerrada.</p>\n<h2>Continuar una tarea con la interfaz cerrada</h2>\n<p>Declara también <code>runtime.tasks.background</code> y programa con <code>background:true</code>. El planificador nativo conserva el registro y ejecuta <code>onBackground(event)</code>. Puede reutilizar la instancia activa o crear otra sin llamar a <code>activate()</code>.</p>\n<ul><li>Retraso mínimo: un minuto. Intervalo periódico mínimo: quince minutos.</li><li>El sistema decide cuándo ejecutar y puede repetir un intento interrumpido. Diseña la tarea para que repetirla no duplique sus efectos y publica el progreso con <code>update</code>.</li><li>Cerrar la interfaz conserva el registro; desactivar o desinstalar lo cancela.</li></ul>\n<p>Android utiliza JobScheduler. En escritorio se utiliza Task Scheduler del usuario o <code>systemd --user</code>, con la sesión iniciada y acceso exclusivo a los datos. Antes de ejecutar se comprueban paquete, firma, habilitación y permisos.</p>\n<p>En esa ejecución puedes usar tareas, transferencias, secretos, CAS, listados de biblioteca y guardados, y HTTP a destinos autorizados. No abre juegos, ventanas ni sockets, ni solicita permisos. Las transferencias requieren la partida cerrada.</p>\n<h2>Guardar archivos privados</h2>\n<p>Declara <code>storage.files</code>. Puedes listar, leer y eliminar con <code>files.list/read/remove</code>; para escribir:</p>\n<ol><li><code>begin</code> recibe <code>name</code>, <code>expectedRevision</code> y, opcionalmente, <code>mimeType</code>.</li><li><code>write</code> añade tramos consecutivos de hasta 64 KiB.</li><li><code>commit</code> publica el archivo completo. Si otra revisión ha cambiado, devuelve un conflicto. <code>abort</code> descarta la escritura.</li></ol>\n<p>Límites: 16 archivos, 1 MiB por archivo, 4 MiB por módulo/autor y dos escritores abiertos por módulo. Desactivar descarta escrituras sin publicar y conserva los archivos terminados. Una actualización fallida recupera los anteriores.</p>\n<h2>Importar y exportar archivos del usuario</h2>\n<p><code>files.import/export</code> requiere también <code>resources.documents</code>. Abre el selector de Android o escritorio para que el usuario elija origen o destino; el módulo no recibe rutas ni URI.</p>\n<p>Studio guarda archivos locales, pero anuncia <code>importExport:false</code> para el selector nativo. Una página puede usar un PNG privado con <code>resource: file:imagen.png</code>, ligado al registro activo y de hasta 1024×1024.</p>\n<h2>Guardar claves de servicios propios</h2>\n<p>Con <code>storage.secrets</code>, usa <code>secrets.get/put/remove/capabilities</code>. Admite 64 claves, 4 KiB por valor y 64 KiB por módulo/autor.</p>\n<ul><li>Android: clave Keystore separada y almacenamiento excluido de las copias de seguridad.</li><li>Windows: cifrado DPAPI.</li><li>Linux: Secret Service si está disponible; si falla, los cambios permanecen sólo en memoria.</li><li>Studio: sólo memoria; utiliza claves de prueba.</li></ul>\n<p>Consulta <code>persistent</code> después de guardar. La recuperación de actualizaciones utiliza copias cifradas o referencias opacas, sin escribir secretos en texto plano en el registro de recuperación. Este servicio no comparte la sesión de Nelkano.</p>\n<h2>Transferir juegos y partidas</h2>\n<p><code>library.transfer</code> y <code>saves.transfer</code> permiten importar y exportar mediante <code>host.transfers</code>, con referencias privadas del módulo y autor. La app verifica los archivos antes de incorporarlos.</p>\n<p>Reemplazar guardados exige la revisión actual del destino: si cambió, se rechaza. La importación sustituye el conjunto completo de progreso y elimina los archivos de progreso que no estén en la copia recibida. La categoría de juegos no sustituye silenciosamente otro contenido con la misma identidad.</p>\n<p>Para transferir por red necesitas sus permisos y orígenes. Consulta <a href=\"/es/sdk-de-nelkano/emulacion\">Emulación y transferencias</a> para el contrato completo.</p>\n<h2>Traducir, capturar y preparar recursos</h2>\n<ul><li><code>translation.local</code>: analizar, editar y reconstruir traducciones con los proveedores anunciados por el host.</li><li><code>media.capture</code>: capturar la imagen del juego.</li><li><code>media.record</code>: grabar clips.</li><li><code>resources.packs</code>: preparar capas de archivos propias del módulo.</li></ul>\n<h2>Incluir preferencias en el perfil</h2>\n<ol><li>Declara <code>profile.provide</code> y registra los documentos CAS con <code>host.profile.register({version:1,keys})</code>.</li><li>Implementa <code>onProfile({operation:'validate',version,documents})</code>. Devuelve <code>{documents}</code> normalizados con las mismas claves; el host valida un documento por llamada y no permite llamadas al SDK durante ella.</li><li>Al recibir <code>profile-restored</code>, recarga el estado publicado.</li></ol>\n<h2>Exportar y restaurar el perfil</h2>\n<p>Declara <code>profile.backup</code>. Consulta <code>host.profile.capabilities()</code> y <code>list()</code>; después:</p>\n<ul><li>Exportar: <code>export({moduleIds?})</code> devuelve ID, tamaño y SHA-256. Lee con <code>read({id,offset,length})</code>.</li><li>Importar: <code>beginImport({size,sha256})</code>, <code>write({id,offset,dataBase64})</code> y <code>preview({id})</code>. La vista previa devuelve la revisión agregada del destino.</li><li>Confirmar: <code>commit({id,expectedRevision})</code>.</li><li>Descartar o liberar una referencia: <code>abort({id})</code> o <code>release({id})</code>.</li></ul>\n<p>La restauración requiere proveedores activos con la misma identidad y autor. Valida todos los documentos y publica el lote completo con revisiones locales nuevas. El módulo importador no puede restaurarse a sí mismo durante esa llamada.</p>\n<p>Básicos, Editor y Apariencia registran preferencias, colecciones/favoritos y controles. Restaurar no reconecta el streaming. El perfil excluye secretos, archivos arbitrarios, entidades y partidas; para el progreso utiliza <code>host.transfers</code>. Tampoco incluye claves que no se hayan registrado.</p>\n<p>Límites: 32 proveedores, 16 claves por proveedor, 96 KiB por documento y 64 MiB por contenedor. Tramos de 64 KiB, dos referencias abiertas por módulo y ocho globales; caducan tras diez minutos sin actividad.</p>\n<h2>Procesar imagen y audio</h2>\n<ul><li><strong><code>graphics.shaders</code></strong>: Postprocesar en GPU la imagen presentada del juego, separado de controles y texto superpuesto.</li><li><strong><code>graphics.options</code></strong>: Cambiar opciones gráficas que anuncie el core, inicialmente raster GameCube 1×/2×/4×.</li><li><strong><code>graphics.textures</code></strong>: Cargar PNG privados y sustituir texturas observadas por su hash dentro del core compatible.</li><li><strong><code>media.microphone</code></strong>: Capturar PCM del micrófono después del permiso nativo; se detiene al pasar a segundo plano.</li><li><strong><code>audio.dsp</code></strong>: Procesar el PCM del core antes de reproducirlo mediante ganancias, biquads y FIR.</li><li><strong><code>runtime.tasks.background</code></strong>: Junto con <code>runtime.tasks</code>, conservar y ejecutar registros <code>background:true</code> con la interfaz cerrada.</li></ul>\n<p>Consulta <code>capabilities</code> antes de ofrecer la función. Los permisos gráficos no entregan contextos GPU, JNI ni rutas y rechazan sesiones antiguas. Grabar el juego no autoriza el micrófono.</p>\n<h2>Rebobinado, sonido y cámara</h2>\n<ul><li><code>emulation.timeline</code>: historial, rebobinado, runahead, avance y repeticiones editables cuando el core lo admite; inicialmente CHIP-8.</li><li><code>audio.mix</code>: clips PCM16LE privados, sincronizados con la partida antes del DSP y la reproducción.</li><li><code>media.camera</code>: captura Camera2 en Android con permiso nativo. Entregar la imagen a CAM 3DS necesita también <code>emulation.input</code>.</li></ul>\n<p>Cada servicio requiere su permiso. Las tareas con la interfaz cerrada mantienen sus restricciones: estos permisos no les permiten abrir juegos, mostrar interfaz, solicitar permisos ni usar cámara, reproducción o un core.</p>\n<h2>Instalar y ejecutar un motor externo</h2>\n<p><code>native.execute</code> autoriza los artefactos nativos declarados y revisados de un paquete V2; <code>native.gpu</code>, su integración gráfica. El JavaScript no recibe punteros, JNI, contextos GL ni rutas privadas.</p>\n<p>Las operaciones de <code>host.engines</code> requieren además <code>emulation.read</code>, <code>emulation.control</code>, <code>emulation.input</code> o <code>emulation.resources</code>, según la acción. <code>engines.importGame</code> necesita también <code>library.transfer</code> y <code>resources.documents</code>: abre el selector con motor y sistema explícitos.</p>\n<p>El código nativo se ejecuta fuera de QuickJS y requiere confiar en su autor. Desactivar retira su proveedor y recursos, y cierra las sesiones que dependían de él. Declarar una capacidad no demuestra que el motor implemente memoria, cámara o timeline.</p>\n<h2>Comprobar si puedes publicarlo en el catálogo</h2>\n<p>El catálogo web acepta V1 de hasta 32 MiB con los siguientes permisos:</p>\n<ul><li>Juegos y guardados: <code>library.read</code>, <code>library.delete</code>, <code>library.launch</code>, <code>library.cover</code>, <code>saves.read</code>, <code>saves.autosave</code>, <code>saves.write</code>, <code>saves.load</code>.</li><li>Emulación: <code>emulation.game-mod</code>, <code>emulation.memory.read</code>, <code>emulation.memory.write</code>, <code>emulation.resources</code>, <code>emulation.handoff</code>, <code>emulation.read</code>, <code>emulation.control</code>.</li><li>Interfaz: <code>presentation.overlay</code>, <code>presentation.player</code>, <code>presentation.appearance</code>, <code>presentation.control-layouts</code>, <code>environment.display</code>, <code>appearance.locale</code>.</li><li>Datos e integración: <code>runtime.events</code>, <code>storage.private.cas</code>, <code>modules.hooks</code>.</li><li>Cuenta y conexiones: <code>account.session</code>, <code>network.account</code>, <code>network.multiplayer</code>.</li><li>Dispositivo y recursos: <code>resources.image</code>, <code>resources.patch</code>, <code>input.gamepad</code>, <code>media.second-screen</code>, <code>media.streaming</code>.</li></ul>\n<p>Los demás permisos de esta página requieren un paquete compatible instalado mediante Importar módulo. Ser V1 no basta para entrar en el catálogo. V2 se distribuye por importación local.</p>"},{"id":"de520020-2026-4000-8000-000000000005","language":"es","title":"Añadir botones y ajustes","path":"/es/sdk-de-nelkano/ganchos-android","content":"<p>Devuelve un botón o ajuste desde <code>contributions(context)</code>, indicando dónde aparece y qué acción llama. Nelkano lo dibuja con el estilo de la app.</p>\n<pre><code>const contribution = {\n  moduleId: 'example.tools', id: 'open',\n  slot: 'SETTINGS_SECTION', section: 'Biblioteca',\n  label: 'Mis herramientas', actionId: 'open',\n  icon: 'standard:settings'\n};</code></pre>\n<h2>Elegir dónde aparece</h2>\n<ul><li><code>HOME_SECTION</code>: sección en Home.</li><li><code>LIBRARY_ACTION</code>: botón en los filtros de Biblioteca, alineado con los controles nativos y con la altura del tema.</li><li><code>LIBRARY_SELECTION_ACTION</code>: acción para los juegos seleccionados.</li><li><code>GAME_HEADER_ACTION</code>: icono en la cabecera de la ficha.</li><li><code>GAME_ARTWORK_ACTION</code>: botón en la esquina inferior de la carátula.</li><li><code>GAME_LAUNCH_ACTION</code>: acción dentro de la ficha.</li><li><code>SAVE_ACTION</code>: acción sobre un guardado.</li><li><code>SETTINGS_SECTION</code>: fila en Ajustes → General.</li><li><code>SYSTEM_ACTION</code> y <code>SYSTEM_SETTINGS_SECTION</code>: acciones y ajustes de un sistema.</li><li><code>EMULATION_SETTINGS_SECTION</code>: ajuste durante una partida.</li><li><code>PLAYER_ACTION</code> y <code>PLAYER_MENU</code>: acciones durante la partida y en su menú.</li><li><code>PLAYER_SYSTEM_BUTTON</code>: botón sobre el área de emulación.</li></ul>\n<p><code>section</code> agrupa filas dentro de General; no crea una pestaña. Para un interruptor, usa <code>kind:'toggle'</code> y <code>checked</code>. La acción recibe el nuevo valor en <code>payload.checked</code>.</p>\n<h2>Abrir un formulario</h2>\n<p>Desde <code>dispatch</code>, devuelve un panel con <code>title</code>, <code>fields</code>, <code>rows</code> y <code>actions</code>. Puedes añadir campos de texto, selección y JSON. Devuelve <code>{kind:'close-panel'}</code> para cerrarlo. <a href=\"/es/sdk-de-nelkano/ejemplo-minimo\">El ejemplo mínimo</a> contiene un panel completo.</p>\n<p><code>enabled</code> desactiva una acción. <code>visibility</code> filtra por sistema, capacidades o disponibilidad de juego, sesión y conexión. Para limitarla a un juego, comprueba <code>context.gameId</code> en <code>contributions(context)</code>: <code>visibility</code> no filtra por ese ID. Vuelve a comprobar la disponibilidad al ejecutar.</p>\n<h2>Integrar una acción con los controles de la app</h2>\n<ul><li><code>PLAYER_SYSTEM_BUTTON</code>: <code>payload.controlId</code> reutiliza un control existente.</li><li><code>GAME_LAUNCH_ACTION</code>: <code>payload.primaryLaunch:true</code> sitúa la acción junto a Jugar.</li><li><code>PLAYER_MENU</code>: <code>payload.menuAction</code> con <code>save</code>, <code>load</code> o <code>reset</code> utiliza las tarjetas del menú de guardados.</li></ul>\n<p>Estos valores eligen ubicación y estilo. <code>actionId</code> sigue llamando a tu módulo: debes implementar la operación.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-10-checkbox-selector.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-10-checkbox-selector.webp\" width=\"600\" height=\"420\" alt=\"Formulario con texto, casilla y selector (ejemplo).\" data-caption=\"Formulario con texto, casilla y selector (ejemplo).\"></a>\n<h2>Mostrar una carátula o captura</h2>\n<ul><li>En una fila, <code>thumbnail:{gameId,slot}</code> muestra la captura junto al texto.</li><li><code>slot:null</code> usa la carátula actual; añade <code>original:true</code> para la portada original.</li><li>En el panel, <code>preview:{gameId,slot}</code> muestra una imagen grande arriba.</li></ul>\n<p>Usa los IDs devueltos por la app, sin pasar rutas ni PNG. Una captura requiere <code>saves.read</code>; una carátula, <code>library.read</code>. Si falta la imagen, se muestra la portada de reserva.</p>\n<h2>Añadir acciones junto al buscador</h2>\n<p><code>LIBRARY_HEADER_ACTION</code> añade acciones al buscador de Biblioteca y Guardados. Comprueba <code>surface:library</code> o <code>surface:saves</code> para decidir dónde contribuir.</p>\n<p><code>LIBRARY_ACTION</code> permanece en los filtros de Biblioteca. <code>SAVE_ACTION</code> conserva las acciones de Guardados sin <code>saveId</code>. El host reúne buscador, importar y filtros en una fila horizontal; en vertical utiliza dos.</p>\n<p>La búsqueda de Guardados es local por título y se combina con sistema, orden y filtros del módulo. Restablecer limpia también la búsqueda.</p>\n<h2>Abrir una página completa</h2>\n<p>Devuelve <code>kind:page</code>, <code>title</code>, <code>fields</code>, <code>rows</code> y <code>actions</code>. La página mantiene la navegación; <code>kind:panel</code> abre un diálogo.</p>\n<p>Los campos admiten también <code>password</code>, <code>email</code> y <code>checkbox</code>. En una casilla, <code>initial</code> es booleano. En un <code>select</code> puede ser texto, número finito, booleano o <code>null</code>.</p>\n<p>Conserva el tipo de cada opción: <code>initial:0</code> selecciona el número cero; <code>initial:\"0\"</code>, un texto. Aplicar sin editar y <code>onChangeAction</code> envían ese mismo tipo. Un botón con <code>submitFields:false</code> puede navegar aunque queden campos sin completar.</p>\n<h2>Responder mientras se edita</h2>\n<p><code>onChangeAction</code> envía las ediciones tras 350 ms sin cambios, incluso al volver al valor anterior. Actualizar <code>initial</code> desde el módulo no genera una edición. En un selector se envía el valor con su tipo.</p>\n<p><code>onChangeMinLength</code> fija la longitud mínima; usa <code>0</code> si necesitas recibir una búsqueda vacía.</p>\n<h2>Colocar acciones y filas</h2>\n<ul><li><code>placement:header</code> sin icono mantiene una acción encima del formulario.</li><li>En un panel, hasta dos acciones de cabecera pueden usar <code>icon:profile</code> o <code>icon:refresh</code>. Aparecen junto al título fijo, usan <code>label</code> como nombre accesible y tienen un área táctil de 48 dp.</li><li>Una fila con avatar y <code>presentation:compact</code> reduce el avatar y ajusta el texto de los botones.</li><li>En acciones de fila, <code>placement:overflow</code> añade la acción al menú ⋮ y conserva payload, confirmación, condiciones y permisos. No se admite en acciones superiores; las acciones de fila no admiten <code>icon</code>.</li><li><code>position:header</code> sitúa una fila antes de los campos.</li></ul>\n<h2>Mostrar avatares</h2>\n<p>Una fila admite <code>avatar</code> con <code>label</code>, <code>color</code>, imagen PNG en Base64 o <code>url</code>. La imagen local tiene prioridad.</p>\n<p>Android carga de forma asíncrona las URL del origen de cuenta permitido y requiere <code>network.account</code>. Studio no descarga imágenes remotas.</p>\n<h2>Crear una página de ajustes</h2>\n<p>Usa <code>kind:\"page\",layout:\"settings\"</code> para tener cabecera y botón de volver. En un selector, <code>presentation:\"tabs\"</code> muestra pestañas; <code>initial</code> selecciona la activa y <code>onChangeAction</code> recibe su valor sin añadir comillas.</p>\n<p>En <code>rows</code>, elige una presentación:</p>\n<ul><li><code>section</code>: encabezado.</li><li><code>actions</code>: botones agrupados en una fila, del mismo tamaño y respetando <code>primary</code>.</li><li><code>setting</code>: título, <code>subtitle</code> y <code>value</code>, con <code>badge</code> o <code>icon</code> opcional.</li><li><code>list</code>: fila sencilla que se puede pulsar.</li></ul>\n<p>La primera acción se ejecuta al pulsar una fila de ajuste o lista. Su icono puede ser un recurso declarado en <code>files</code> o un icono <code>standard</code>. <code>placement:\"footer\"</code> sitúa una acción al pie del formulario.</p>\n<h2>Mantener visibles el título y los botones de un panel</h2>\n<p>Todos los <code>kind:\"panel\"</code>, con o sin <code>layout:\"form\"</code>, comparten el mismo diálogo en Android, Desktop y Studio: título y × fijos, cuerpo desplazable y acciones <code>placement:\"footer\"</code> al pie. Las demás acciones, incluidas las de filas, permanecen en el cuerpo.</p>\n<p>Las páginas conservan su navegación; <code>layout:\"settings\"</code> se utiliza sólo con <code>kind:\"page\"</code>.</p>\n<h2>Controlar el cierre y conservar lo escrito</h2>\n<p><code>dismissAction:\"close\"</code> utiliza una acción existente con <code>submitFields:false</code> para la × y para cerrar el diálogo, respetando su confirmación.</p>\n<p>Sin <code>dismissAction</code>, el host sólo recoge una acción inequívoca de cerrar/cancelar que no envíe campos ni pida confirmación. Si hay varias candidatas, conserva los botones y la × cierra desde el host. No se permite cerrar mientras se procesa una acción.</p>\n<p>Asigna un <code>id</code> estable al formulario: el borrador se conserva al actualizar el mismo módulo, formulario y sesión. Cambiar de formulario o sesión lo reinicia. En paneles antiguos sin ID se distinguen título y esquema de campos.</p>\n<p>Los avisos nativos y los formularios de cuenta, colecciones, mandos, streaming, parches y módulos utilizan esta presentación. Los lienzos del editor y las superficies de vídeo o mando mantienen su función.</p>\n<h2>Mostrar campos según una elección</h2>\n<p>Usa <code>visibleWhen:{field:\"role\",values:[\"host\"]}</code>, o combina condiciones con <code>{all:[...]}</code> y <code>{any:[...]}</code>. El campo referido debe ser un selector o una casilla del mismo formulario. Los valores deben existir entre sus opciones; las casillas usan booleanos.</p>\n<p>Las condiciones se evalúan localmente al seleccionar, sin ejecutar JavaScript ni consultar el servidor. Admiten cuatro niveles, 32 nodos por condición y 32 valores por hoja.</p>\n<p>Un campo oculto conserva el borrador, pero no se valida como requerido ni se envía en el payload.</p>\n<h2>Habilitar una acción según el formulario</h2>\n<p><code>enabledWhen</code> utiliza las mismas condiciones y se combina con <code>enabled</code>. <code>description</code> añade una ayuda de hasta 256 caracteres bajo el botón.</p>\n<p>Son condiciones de presentación: valida de nuevo los parámetros y permisos al ejecutar. Para elecciones cortas, como Crear partida/Unirse o 2/3/4, puedes usar <code>kind:\"select\",presentation:\"tabs\"</code>. El borrador resiste las actualizaciones de estado y se reinicia al cambiar la sesión.</p>\n<h2>Crear componentes y ampliar pantallas</h2>\n<p><code>presentation.extend</code> permite <code>ui.page</code>, <code>mount</code>, <code>patch</code>, <code>remove</code>, <code>open</code>, <code>close</code> y <code>targets</code>. Puedes crear secciones, filas, botones, formularios, pestañas, modales, imágenes PNG y lienzos con figuras. <code>dispatch</code> recibe acciones y valores.</p>\n<ul><li>Monta contenido en <code>home/library/game/saves/settings/emulation.before</code> y <code>.after</code>.</li><li>Usa <code>.content</code> para ocultar y sustituir el cuerpo.</li><li>Los componentes publicados ofrecen puntos antes/después y cambios de texto, visibilidad, disponibilidad y <code>actionId</code>. También puedes ampliar componentes de otros módulos.</li></ul>\n<p>Los conflictos entre autores se rechazan. Desactivar retira los cambios. Límites: 32 registros por módulo, 256 globales, 96 KiB de contenido, 512 nodos y profundidad 16.</p>\n<p>La app y Studio comparten el renderizador. Los componentes se programan en JavaScript o TypeScript; no ejecutan HTML arbitrario. Revisa el resultado en el visor.</p>\n<h2>Fijar las acciones al pie de un modal</h2>\n<p>En <code>presentation.extend</code>, <code>kind:\"modal\"</code> usa el mismo marco. <code>label</code> es el título y <code>actionId</code> recibe el cierre. Sólo sus hijos directos pueden usar <code>placement:\"footer\"</code>; el resto se desplaza con el contenido.</p>\n<pre><code>const dialog = {id:\"dialog\",kind:\"modal\",label:\"Editar nombre\",actionId:\"cancel\",children:[\n  {id:\"name\",kind:\"field\",label:\"Nombre\",value:\"Ada\"},\n  {id:\"actions\",kind:\"row\",placement:\"footer\",children:[\n    {id:\"save\",kind:\"button\",label:\"Guardar\",actionId:\"save\"}\n  ]}\n]};</code></pre>\n<p>El host rechaza <code>placement</code> fuera de un hijo directo de modal o con otro valor. Las acciones siguen sujetas a la propiedad del módulo, el registro, la revisión y sus permisos.</p>\n<h2>Usar imágenes generadas o descargadas</h2>\n<ol><li>Genera o descarga el PNG y guárdalo mediante <code>files</code>.</li><li>Añade el nodo <code>{id:\"imagen\",kind:\"image\",resource:\"file:imagen.png\"}</code>.</li><li>Declara <code>storage.files</code> y <code>presentation.extend</code>.</li></ol>\n<p>El host valida formato y dimensiones. La imagen queda ligada al módulo/autor y a la revisión del registro de interfaz; otro módulo no puede leerla. Funciona en la app y Studio. También puedes componer nodos y dibujar figuras sin generar una imagen.</p>\n<h2>Recorrer un catálogo por páginas</h2>\n<p><code>ui.targets({offset,limit})</code>, <code>entities.types({offset,limit})</code> y <code>services.list(name,{offset,limit})</code> devuelven 32 entradas por defecto, con un máximo de 128 y 112 KiB.</p>\n<p>Aumenta <code>offset</code> por el número de entradas recibidas hasta obtener una página vacía. Una página puede terminar antes de <code>limit</code> por su tamaño.</p>"},{"id":"de520020-2026-4000-8000-000000000012","language":"es","title":"Home, ficha y Favoritos","path":"/es/sdk-de-nelkano/home-ficha-favoritos","content":"<p>Puedes añadir secciones a Home y acciones a la ficha de un juego. Nelkano básicos utiliza estas contribuciones para Favoritos, Continuar y la elección de carátula.</p>\n<h2>Mostrar juegos en Home</h2><p>Publica una sección con los ID de los juegos que quieras mostrar:</p><pre><code>{\n  moduleId: 'example.home', id: 'my-games',\n  slot: 'HOME_SECTION', label: 'Mis juegos', actionId: 'open',\n  payload: { section: {\n    kind: 'game-carousel', gameIds: ['id-del-juego'],\n    emptyLabel: 'Todavía no hay juegos'\n  }}\n}</code></pre><p>Los ID deben existir en la biblioteca. Nelkano dibuja las tarjetas y abre su ficha al pulsarlas. emptyLabel es el texto que aparece cuando la sección está vacía.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-01-home.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-01-home.webp\" width=\"960\" height=\"715\" alt=\"Juegos recientes en Home (datos de ejemplo).\" data-caption=\"Juegos recientes en Home (datos de ejemplo).\"></a><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-03-favorites.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-03-favorites.webp\" width=\"960\" height=\"715\" alt=\"Sección Favoritos añadida por un módulo (ejemplo).\" data-caption=\"Sección Favoritos añadida por un módulo (ejemplo).\"></a><p>Favoritos ya viene integrado en Básicos. La estrella de la ficha añade o quita el juego de esa colección, y Home muestra su sección. No necesitas instalar Colecciones por separado; los datos se conservan al actualizar.</p>\n<h2>Añadir un icono a la ficha</h2><p>Usa GAME_HEADER_ACTION. Para una acción que se pueda marcar, indica su estado con checked; así funciona la estrella de Favoritos.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-02-game-details.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-02-game-details.webp\" width=\"860\" height=\"580\" alt=\"Ficha del juego y estados guardados (ejemplo).\" data-caption=\"Ficha del juego y estados guardados (ejemplo).\"></a><h2>Ofrecer Continuar</h2><ol><li>Consulta saves.list para encontrar los estados disponibles.</li><li>Publica GAME_LAUNCH_ACTION con payload.primaryLaunch: true. La acción aparece junto a Jugar.</li><li>Cuando se pulse, elige el estado y llama a library.launch(gameId, slot).</li></ol>\n<p><strong>Jugar</strong> abre el juego sin cargar un estado de Nelkano; su progreso interno se recupera desde los menús del propio juego. También se puede cargar un estado concreto desde su miniatura, aunque los autoguardados estén desactivados.</p>\n<h2>Elegir una carátula de un guardado</h2><p>GAME_ARTWORK_ACTION coloca una acción en la esquina inferior de la portada. Básicos la utiliza para abrir el selector de capturas. Para cambiar la carátula desde tu módulo:</p>\n<pre><code>// Requiere library.cover. Los IDs vienen de library.list y saves.list.\nawait host.library.setCover(gameId, slot);\n// Volver a la portada original:\nawait host.library.setCover(gameId, null);</code></pre><p>Obtén los juegos con library.list y los guardados con saves.list. hasThumbnail indica si un estado tiene captura; coverSlot identifica la carátula elegida. Solo se aceptan juegos existentes, slots del 0 al 6 y guardados con captura. La elección se conserva al volver a abrir Nelkano.</p>"},{"id":"de520020-2026-4000-8000-000000000006","language":"es","title":"Conectar dos módulos","path":"/es/sdk-de-nelkano/ganchos-modulos","content":"<p>Un módulo puede publicar funciones que otros utilicen. Por ejemplo, el editor de controles admite botones e iconos aportados por otro módulo.</p>\n<h2>Declarar una integración</h2>\n<ol><li>El proveedor declara sus ganchos en <code>provides</code>.</li><li>El consumidor declara proveedor, gancho y versión en <code>consumes</code>.</li><li>Ambos declaran el permiso <code>modules.hooks</code>.</li></ol>\n<p>Marca <code>optional:true</code> si el consumidor debe funcionar aunque falte el proveedor.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/module-hooks.svg\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/module-hooks.svg\" width=\"1120\" height=\"640\" alt=\"Conexión entre dos módulos a través del host.\" data-caption=\"Conexión entre dos módulos a través del host.\"></a>\n<h2>Añadir un botón al editor de controles</h2>\n<pre><code>await host.hooks.contribute(\n  'nelkano.control-editor', 'system-buttons', 'my-button',\n  { actionId: 'my-action', label: 'Mi botón', icon: 'assets/button.png' }\n);</code></pre>\n<p>Añade <code>assets/button.png</code> a <code>files</code>. Cuando el usuario pulse el botón, tu módulo recibe <code>onHook</code> con <code>operation:'my-action'</code>. El editor entrega el contexto de partida en <code>request.value.context</code>; úsalo en las operaciones de emulación.</p>\n<p>La app incluye el proveedor <code>nelkano.control-editor</code>. La plantilla Editor de controles de Studio utiliza <code>example.control-editor</code>: para probar con ella, usa ese ID tanto en <code>consumes.provider</code> como en <code>hooks.contribute</code> y carga también la carpeta del proveedor en Studio.</p>\n<p>Con <code>target:'sys-pause'</code> puedes reutilizar un control existente y conservar su posición. Sin <code>target</code>, se añade un control independiente.</p>\n<h2>Consultar y llamar a un gancho</h2>\n<ul><li><code>hooks.status</code>: comprobar disponibilidad.</li><li><code>hooks.list</code>: obtener aportaciones.</li><li><code>hooks.invoke</code>: llamar al proveedor.</li><li><code>invokeContributor</code>: responder al consumidor desde el proveedor.</li></ul>\n<p>Entradas y salidas deben cumplir el esquema del gancho. Al desactivar o retirar un módulo desaparecen sus botones e iconos. El editor conserva las elecciones para cuando se reactive.</p>\n<h2>Publicar un servicio</h2>\n<p>Declara <code>modules.services</code> y registra <code>name</code>, <code>version</code> y <code>methods</code>. Cada método define <code>input/output</code> con los mismos tipos de esquema que los ganchos.</p>\n<pre><code>await host.services.register({\n  name: \"example.counter\", version: \"1.0.0\",\n  methods: { add: { input: {type: \"integer\"}, output: {type: \"integer\"} } }\n});\n// Otro módulo, con modules.services:\nconst next = await host.services.call(\"example.counter\", \"add\", 4);\n// El proveedor implementa onService(request) y devuelve el resultado.</code></pre>\n<p>Implementa <code>onService</code>: recibe <code>service</code>, <code>method</code>, <code>value</code>, <code>caller</code> y <code>provider</code>, con la identidad real del módulo y autor. La base usa <code>author:builtin</code>. El servicio se ejecuta dentro del entorno aislado del proveedor.</p>\n<ul><li><code>list(name)</code> encuentra proveedores.</li><li><code>unregister(name)</code> retira únicamente los tuyos.</li><li><code>call</code> admite proveedor, autor y versión mínima compatible dentro de la misma versión mayor.</li></ul>\n<p>Un mismo nombre y versión mayor deben mantener métodos y esquemas. El servicio no comparte código ni datos privados. Se rechazan ciclos y llamadas a un entorno ocupado. Límites: 256 proveedores, 32 métodos, profundidad de esquema 8 y datos de hasta 96 KiB.</p>\n<h2>Elegir el proveedor de una llamada</h2>\n<p>Indica <code>options.provider</code> en <code>services.call</code> para elegir uno concreto. Sin proveedor explícito ni preferencia guardada, se utiliza la implementación integrada; si no existe, se puede elegir al único proveedor disponible. Si hay varios y ninguno integrado, debes elegir.</p>\n<p>Cada llamada verifica permisos y compatibilidad del contrato.</p>\n<h2>Ofrecer otro comportamiento de emulación</h2>\n<p>La base publica <code>nelkano.emulation</code> versión <code>1.0.0</code>, método <code>execute</code>, entrada <code>{operation,args}</code> y salida <code>{value:resultado}</code>. Consulta <code>list(\"nelkano.emulation\")</code> para copiar el contrato exacto.</p>\n<p>Abarca snapshot, pausa/reset, presentación, guardar/cargar, <code>library.launch</code> y <code>library.setCover</code>. Las API anteriores delegan en él conservando sus permisos. Otro proveedor puede cambiar el comportamiento y delegar a la base con <code>provider:\"nelkano.base\",author:\"builtin\"</code>.</p>\n<p>Consumidor y proveedor necesitan el permiso original de cada operación. Este servicio afecta a las operaciones solicitadas por módulos; no sustituye las llamadas internas de la app ni los motores nativos.</p>\n<h2>Enviar eventos entre módulos</h2>\n<p>Con <code>modules.events</code>:</p>\n<ol><li>Publica un tema con <code>define(topic,schema)</code>. Su nombre debe empezar por el ID de tu módulo y un punto.</li><li>Otro módulo se suscribe con <code>subscribe(topic)</code>, incluso antes de que exista el publicador.</li><li>El publicador envía con <code>emit(topic,value)</code>. Sólo él puede emitir.</li><li>El suscriptor recibe <code>onEvent</code> con <code>{type:\"module-event\",topic,value,sender:{moduleId,author}}</code>. <code>unsubscribe(topic)</code> cancela la suscripción.</li></ol>\n<p>Son eventos locales; no se envían a servidores. Límites: 256 temas, 64 suscripciones y 64 eventos pendientes por módulo. Si se llena una cola, se rechaza todo el envío sin entregas parciales.</p>\n<p>Se entregan hasta ocho eventos por ronda, aproximadamente cada 50 ms en primer plano, sin garantía de persistencia ni de tiempo exacto. Desactivar limpia servicios, temas, suscripciones y colas. Los ganchos siguen disponibles y Studio reproduce estos contratos.</p>\n<h2>Transformar una llamada antes o después de ejecutarla</h2>\n<p>Registra <code>services.intercept({id,name,method,phase,order})</code> con fase <code>before</code> o <code>after</code>. <code>onService</code> recibe <code>phase</code> y <code>hookId</code>; devuelve un valor compatible con el esquema del método. Retira la transformación con <code>removeInterceptor(id)</code>.</p>\n<p>Admite 32 por módulo y 128 globales. Las llamadas propias no vuelven a entrar en su interceptor. Un error posterior a la operación no deshace sus efectos.</p>"},{"id":"de520020-2026-4000-8000-000000000009","language":"es","title":"Qué hacen los módulos incluidos","path":"/es/sdk-de-nelkano/ejemplos-distribuidos","content":"<p>Nelkano incluye cinco módulos esenciales: Básicos, Editor de controles, Apariencia, Amigos y Multijugador. Sus funciones también sirven de ejemplo para crear las tuyas con el SDK.</p>\n<h2>Encontrar los módulos</h2><p>Abre Ajustes → General → Módulos → Instalados. Los esenciales están agrupados en Módulos esenciales: permanecen activos, no se pueden desinstalar y se actualizan con la app. Si uno falla al arrancar, puedes reintentar su activación.</p>\n<p>Los módulos que crees serán opcionales. El usuario podrá importar su archivo .nelkmod, desactivarlo conservando los datos o desinstalarlo. Declarar un valor en el manifiesto no permite convertirlo en esencial.</p>\n<h2>Nelkano básicos</h2><p>Reúne las colecciones y Favoritos, la gestión de juegos, Continuar, los autoguardados, las carátulas y los botones de emulación. También ofrece la configuración de mandos, segunda pantalla y streaming.</p>\n<ul><li><strong>Colecciones:</strong> se crean y filtran desde Biblioteca. La estrella de la ficha añade un juego a Favoritos, que también aparece en Home. Borrar una colección conserva sus juegos.</li><li><strong>Ordenación:</strong> ABC, más antiguos o más recientes. En Biblioteca se usa la fecha de incorporación; en Guardados, la de la partida. Cambiar el orden conserva el filtro de colección.</li><li><strong>Autoguardados:</strong> Ajustes → General → Biblioteca.</li><li><strong>Segunda pantalla:</strong> Ajustes → General → Apariencia.</li><li><strong>Streaming:</strong> ajustes de la partida.</li></ul>\n<p>Las colecciones, los favoritos y el filtro se conservan al actualizar. Colecciones ya está integrado en Básicos: el ejemplo independiente del SDK es material para autores y no necesita instalarse en la app.</p>\n<h2>Editor de controles</h2><p>Permite mover, redimensionar y ocultar controles. Se abre desde Ajustes → General → Controles → Editor de controles o desde el menú de la partida. Otros módulos pueden añadirle botones e iconos mediante sus ganchos.</p>\n<h2>Apariencia</h2><p>Ofrece el idioma y los temas de la app. Un módulo puede aportar temas mediante sus contratos.</p>\n<h2>Amigos</h2><p>Gestiona la cuenta, las solicitudes de amistad y el perfil. Sus ganchos permiten añadir acciones y consultar los datos compartidos. Studio utiliza cuentas ficticias para probarlo.</p>\n<p>Parches está temporalmente excluido de los módulos cargados en Android y escritorio. Sus fuentes y el servicio del SDK se conservan para revisión; no aparece Aplicar parche en la ficha.</p>\n<h2>Multijugador</h2><p>Ofrece la interfaz de conexión entre jugadores. Cada modo necesita un motor y un adaptador compatibles; la interfaz por sí sola no añade multijugador a cualquier juego.</p>\n\n<h2>Usarlos como referencia en Studio</h2><p>Abre la plantilla del módulo que quieras estudiar; la de Nelkano básicos incluye las colecciones. Studio permite activar y desactivar los proyectos para comprobar cómo añaden y retiran sus funciones. Las plantillas adicionales, incluida Parches, no establecen paridad con los paquetes incluidos en la app.</p>\n<h2>Seguir el estilo de la app</h2><p>Utiliza los formularios del SDK: mantienen el título y el cierre arriba, las acciones al pie y el contenido desplazable. Este patrón se usa en Amigos, Apariencia y los formularios auxiliares del Editor.</p><p>Módulos tiene pestañas Instalados y Explorar; ocupa una pantalla en Android y una ventana en escritorio. La ficha del juego conserva Jugar y Continuar al pie. Reutiliza estas superficies para que tu función encaje en la navegación de Nelkano.</p>"},{"id":"de520020-2026-4000-8000-000000000007","language":"es","title":"Cambiar la apariencia","path":"/es/sdk-de-nelkano/apariencia","content":"<p>Un módulo de apariencia puede ofrecer colores, fondo, fuente, iconos, tamaños y opciones de distribución. Parte de la plantilla Apariencia de Studio para probarlos en el visor.</p>\n<h2>Definir un estilo</h2>\n<ol><li>Declara el permiso <code>presentation.appearance</code>.</li><li>Añade el estilo en <code>skins</code>, con <code>id</code> y <code>label</code>.</li><li>Configura las propiedades que quieras cambiar.</li><li>Guarda y selecciona el estilo en Tema, dentro de Ajustes.</li></ol>\n<p>Las propiedades disponibles son <code>colors</code>, <code>background</code>, <code>font</code>, <code>icons</code>, <code>cornerRadius</code>, <code>spacing</code>, <code>controlHeight</code>, <code>headerHeight</code>, <code>navigation</code> y <code>libraryView</code>.</p>\n<ul><li><code>background</code> e <code>icons</code>: recursos del paquete.</li><li><code>font</code>: archivo TTF declarado en <code>files</code>.</li><li><code>navigation</code>: <code>top</code> o <code>bottom</code>.</li><li><code>libraryView</code>: <code>grid</code> o <code>list</code>.</li></ul>\n<p>En un formulario, <code>select</code> muestra un desplegable. <code>presentation:tabs</code> lo presenta como pestañas, conservando el valor JSON seleccionado.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-07-language-dark.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-07-language-dark.webp\" width=\"600\" height=\"370\" alt=\"Apariencia con el tema Oscuro (ejemplo).\" data-caption=\"Apariencia con el tema Oscuro (ejemplo).\"></a>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-08-language-light.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-08-language-light.webp\" width=\"600\" height=\"370\" alt=\"Tema Claro e interfaz en inglés (ejemplo).\" data-caption=\"Tema Claro e interfaz en inglés (ejemplo).\"></a>\n<h2>Comprobar el resultado</h2>\n<p>Selecciona el estilo en el visor y revísalo también en Android antes de distribuirlo.</p>\n<p>Un estilo modifica los componentes que Nelkano ya muestra. Para añadir páginas o formularios, utiliza la API de interfaz: el estilo no ejecuta HTML ni instala vistas Android.</p>"},{"id":"de520020-2026-4000-8000-000000000010","language":"es","title":"Qué puedes hacer y qué falta","path":"/es/sdk-de-nelkano/limites","content":"<p>Para elegir las API de tu módulo, empieza por la tarea que quieres resolver. La disponibilidad depende de la plataforma y del motor: consulta sus capacidades antes de ofrecer una acción.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/sdk-environments.svg\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/sdk-environments.svg\" width=\"1120\" height=\"540\" alt=\"Android, Desktop y el simulador de Studio.\" data-caption=\"Android, Desktop y el simulador de Studio.\"></a><h2>Elegir las API</h2><ul><li><strong>Botones, páginas y formularios:</strong> contribuciones y componentes de interfaz.</li><li><strong>Colecciones, guardados y Continuar:</strong> biblioteca, guardados y emulación.</li><li><strong>Datos propios:</strong> almacenamiento con revisiones CAS, entidades locales, archivos y secretos.</li><li><strong>Integración con otro módulo:</strong> ganchos, servicios, proveedores, eventos e interceptores publicados.</li><li><strong>Conexión a tu servicio:</strong> HTTP o WebSocket a los destinos autorizados.</li><li><strong>Trabajos programados:</strong> tareas normales o persistentes, según el dispositivo.</li></ul>\n<p>La interfaz se construye con los componentes de Nelkano. El JavaScript del módulo utiliza las operaciones autorizadas del SDK: no dispone de Node.js, acceso libre a archivos ni vistas Android. Los proveedores permiten sustituir los puntos que la app u otro módulo hayan publicado.</p>\n<h2>Modificar un juego</h2><p><code>host.gameMod</code> permite leer y buscar memoria, escribir comprobando los valores anteriores, ejecutar reglas antes de cada fotograma, enviar pulsaciones y modificar temporalmente la ROM cargada. La memoria y la ROM están conectadas a GBA en Android y escritorio. GB/GBC y NDS ofrecen lecturas específicas mediante <code>host.gameRuntime</code>; comprueba qué admite la sesión.</p>\n<p>El archivo ROM original se conserva, pero los efectos pueden quedar en los guardados. Puedes declarar que una partida necesite tu módulo o requiera empezar de nuevo cuando el motor lo permita. Consulta <a href=\"/es/sdk-de-nelkano/modificar-juego\">Modificar un juego</a> antes de escribir: explica los conflictos, la retirada del módulo y los requisitos de guardados.</p>\n<h2>Capturar imagen y traducir texto</h2><p><code>host.media</code> ofrece capturas y clips cuando el motor entrega la imagen o el audio necesarios. <code>host.translation</code> analiza, edita y reconstruye los formatos de texto compatibles de GBA/NDS. Android puede traducir con modelos locales descargables; escritorio permite editar las entradas o recibir traducciones de tu servicio. Para reconocer texto dentro de una imagen, aporta un OCR.</p>\n<h2>Cambiar gráficos y sonido</h2><p><code>host.graphics</code> ofrece filtros y las opciones gráficas del motor. El postprocesamiento requiere Android API 33+ o Skia en escritorio. PS1, PSP y GameCube admiten reemplazos PNG de texturas; GameCube ofrece resolución interna 1×, 2× y 4×. Consulta las capacidades y los límites del modo de renderizado: el mismo pack no sirve para todos los juegos.</p>\n<p><code>host.audioDsp</code> filtra el PCM del juego y <code>host.audioMix</code> mezcla PCM externo cuando la sesión lo admite. <code>host.microphone</code> captura audio del dispositivo con su permiso. <code>host.camera</code> captura imágenes en Android; escritorio anuncia <code>capture:false</code>. Tu módulo debe aportar el reconocimiento de voz, el doblaje o la interpretación de imágenes. Entregar una imagen a la cámara emulada de 3DS requiere además su capacidad y permiso.</p>\n<p><code>host.timeline</code> permite rebobinado, avance y repeticiones editables en motores con ejecución determinista. La integración inicial es CHIP-8. Poder guardar y cargar estados no implica que un motor admita estas funciones.</p>\n<h2>Transferir juegos y progreso</h2><p><code>host.transfers</code> exporta e importa juegos y partidas en Android y escritorio. Tu módulo resuelve la conexión entre dispositivos y los conflictos. El destino necesita un motor compatible con el formato del estado; los guardados internos también pueden depender de la plataforma o versión. Los guardados MV/MZ de WebView quedan fuera.</p>\n<p><code>host.resourcePacks</code> prepara capas de archivos y <code>host.profile</code> permite copiar las preferencias que registren sus proveedores. Los packs excluyen los proyectos RPG Maker que guardan su progreso dentro del proyecto. Los reemplazos gráficos por hash utilizan el servicio de texturas.</p>\n<p>Para sincronizar con tu servicio, combina transferencias, conexiones, secretos y tareas. Implementa su autenticación y declara los destinos de red. La transferencia de juegos y progreso necesita la partida cerrada.</p>\n<h2>Elegir Android o escritorio</h2><p>Declara las plataformas en el manifiesto y prueba el paquete en cada una. Ten en cuenta estas diferencias:</p><ul><li>Ambas importan archivos individuales y ZIP, RAR o 7z. Un disco CUE necesita todas sus pistas.</li><li>RPG Maker XP requiere el proyecto completo y funciona en ambas plataformas. MV/MZ funciona en Android.</li><li>SNES, PSP y GameCube admiten estados según las capacidades del motor. PSP conserva además sus guardados internos en una Memory Stick privada.</li><li>3DS es experimental y requiere Vulkan y ARM64 en Android. Switch está excluido de las apps Android y Desktop. Se conservan sus fuentes y las pruebas del core en PC; no forman parte del producto publicado.</li><li>Segunda pantalla necesita una red local y un receptor emparejado.</li><li>Streaming requiere una cuenta autorizada y una partida capturable. El transporte de respaldo HTTP de Android no está conectado en escritorio.</li></ul><p>Comprueba también mandos, gráficos y rendimiento en los dispositivos que quieras admitir, incluida Steam Deck si es uno de tus destinos.</p>\n<h2>Conectar jugadores</h2><p>Amigos y Multijugador vienen incluidos. Multijugador ofrece su interfaz en GB/GBC, GBA y NDS; cada modo necesita un motor y un adaptador compatibles.</p>\n<h2>Distribuir tu propio motor</h2><p>Un paquete V2 puede incluir un motor compilado para Android o escritorio. Su descriptor declara sistemas, controles, opciones, discos, recursos y formatos de guardado. La partida conserva el motor y la versión con los que se abrió. El ejemplo nativo del SDK sirve para probar vídeo CPU/OpenGL/Vulkan, audio PCM, entrada y estados.</p>\n<p>En escritorio, el motor se ejecuta con los permisos del usuario. En Android se instala un APK auxiliar con UID y certificado propios; la ruta gráfica no utiliza <code>isolatedProcess</code>. Revisa el código nativo y sus dependencias antes de distribuirlos. Linux utiliza X11/GLX o Vulkan y necesita XWayland en un escritorio Wayland.</p>\n<h2>Comprobar el resultado</h2><ol><li>En Studio, prueba la interfaz, los permisos, los errores y qué ocurre al desactivar el módulo o cambiar de sesión.</li><li>Exporta el paquete y repite la tarea en la app de cada plataforma que declares.</li></ol><p>Los juegos y dispositivos de Studio son simulados; las conexiones externas autorizadas sí usan la red. Los módulos opcionales se distribuyen por separado. Los cinco esenciales se actualizan con Nelkano.</p>"},{"id":"de520020-2026-4000-8000-000000000013","language":"es","title":"Editor de controles","path":"/es/sdk-de-nelkano/editor-de-controles","content":"<p>El Editor de controles permite cambiar posición, tamaño, visibilidad e iconos. Guarda un diseño por perfil y orientación. GB y GBC comparten <code>GAME_BOY</code>; NDS y SNES utilizan <code>NDS</code>.</p><p>Pulsa <strong>Guardar</strong> para aplicar los cambios. Cerrar con la × descarta el borrador.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-05-controls-before.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-05-controls-before.webp\" width=\"960\" height=\"720\" alt=\"Distribución inicial de los controles de GBA.\" data-caption=\"Distribución inicial de los controles de GBA.\"></a><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-06-controls-after.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-06-controls-after.webp\" width=\"960\" height=\"720\" alt=\"Botón A después de moverlo y ampliarlo.\" data-caption=\"Botón A después de moverlo y ampliarlo.\"></a><h2>Leer la pantalla y aplicar un diseño</h2><ul><li><code>environment.snapshot()</code> obtiene el tamaño y la densidad de pantalla. Requiere <code>environment.display</code>.</li><li><code>presentation.applyControlLayouts()</code> aplica el diseño guardado y <code>releaseControlLayouts()</code> lo retira. Ambas necesitan <code>presentation.control-layouts</code>.</li></ul><p>Guarda las preferencias del módulo en su almacenamiento privado, con el permiso correspondiente.</p>\n<p>Cada control utiliza x e y entre 0 y 1, scale entre 0.5 y 2 y visible. Los perfiles separan PORTRAIT y LANDSCAPE. El editor y la partida utilizan las mismas coordenadas.</p>\n<h2>Añadir iconos o botones</h2><p>El Editor publica dos ganchos, ambos en la versión 1.0.0:</p><ul><li><strong>icons:</strong> ofrece al usuario un icono PNG del paquete para un control.</li><li><strong>system-buttons:</strong> añade un botón y la acción que ejecutará tu módulo.</li></ul>\n<h3>Elegir dónde aparece el botón</h3><ul><li><strong>systems:</strong> lista de identificadores de sistemas donde estará disponible. Si la omites, se ofrece en todos los perfiles.</li><li><strong>placement:</strong> posiciones iniciales portrait y landscape, cada una con x, y, scale y visible.</li><li><strong>target:</strong> permite editar un control existente. Si lo omites, el Editor crea un botón nuevo.</li></ul><p>Si tu módulo publica su propio PLAYER_SYSTEM_BUTTON, usa como target module:ID-del-consumidor:ID-del-botón. Así el Editor lo mueve sin duplicarlo y tu módulo conserva sus acciones y paneles. Publica las mismas posiciones iniciales en el botón y en placement.</p>\n<h3>Responder a la pulsación</h3><p>Si el Editor publica el botón, llama a tu módulo mediante <code>invokeContributor</code> con el contexto de la partida. Si publicas tu propio <code>PLAYER_SYSTEM_BUTTON</code> y su <code>target</code> empieza por <code>module:ID_DEL_CONSUMIDOR:</code>, la acción se envía directamente a tu módulo; el Editor conserva la colocación.</p><p>Implementa la operación en tu módulo. Aportar un icono de captura o streaming no implementa esas funciones. Los iconos se dibujan dentro del marco común de los botones de sistema.</p>\n<h2>Reutilizar los controles incluidos</h2><p>Básicos aporta pausa, guardar, cargar, estirar, pantalla completa y el selector de pantalla NDS/3DS. Reutiliza sus ID estables, como sys-pause y sys-save, para conservar las posiciones que haya elegido el usuario.</p><p>Definir un control en el perfil no basta para que aparezca: necesita una acción disponible en la partida.</p>\n<h2>Perfiles de PSP, GameCube y Switch</h2><p>Sus claves son <code>PSP</code>, <code>GAMECUBE</code> y <code>SWITCH</code>. GameCube ofrece stick principal y C-stick; PSP, un stick principal. <code>LEFT_STICK</code> y <code>RIGHT_STICK</code> identifican controles <code>JOYSTICK</code>. Actualmente envían direcciones independientes, sin intensidad proporcional de inclinación.</p><p>Los tipos <code>BuiltinControlProfile</code> y <code>StickControlInput</code> ayudan a programarlos. El contrato también admite claves de texto para crear perfiles propios. Las plantillas del Editor y de Básicos incluyen estos perfiles. El perfil Switch se conserva en el SDK y en Studio; no habilita su core, excluido de las apps Android y Desktop.</p>\n<h2>Teclado de RPG Maker en Android</h2><p>El perfil RPG Maker incluye Teclado, identificado por <code>sys-keyboard</code>. Se puede mover, redimensionar y ocultar por orientación. Durante la partida abre el teclado de Android para escribir, borrar con Backspace y confirmar con Enter.</p><p>Esta función pertenece al reproductor Android; no requiere un permiso nuevo del SDK ni ofrece un teclado virtual en escritorio o Studio.</p>"},{"id":"de520020-2026-4000-8000-000000000014","language":"es","title":"Emulación: operaciones y eventos","path":"/es/sdk-de-nelkano/emulacion","content":"<p>Usa estas operaciones para pausar, guardar, cambiar la presentación o crear herramientas sobre una partida. Android y escritorio trabajan con el juego abierto; Studio utiliza datos simulados.</p>\n<h2>Consultar la partida abierta</h2><pre><code>const state = await host.emulation.snapshot();\nif (!state) return; // No hay partida abierta.\nconst session = state.session;</code></pre><p>Declara emulation.read. La respuesta incluye gameId, systemId, paused, capabilities, slots, canSave y display. En display encontrarás fullscreen, stretch, screenMode y selectedSlot. Consulta esas capacidades antes de ofrecer una operación.</p>\n<h2>Pausar, guardar y cambiar la pantalla</h2><pre><code>await host.emulation.setPaused(session, true);  // Pausar.\nawait host.emulation.setPaused(session, false); // Reanudar.\nawait host.emulation.reset(session);\nawait host.emulation.setFullscreen(session, true);\nawait host.emulation.setStretch(session, true);\nawait host.emulation.setScreenMode(session, 1);\nawait host.emulation.selectSlot(session, 2);\nawait host.saves.save(session, 2);\nawait host.saves.load(session, 2);</code></pre><ul><li><strong>emulation.control:</strong> pausar, reanudar y reiniciar.</li><li><strong>presentation.player:</strong> pantalla completa, estirar, pantalla seleccionada y slot.</li><li><strong>saves.write:</strong> guardar un estado.</li><li><strong>saves.load:</strong> cargar un estado existente.</li></ul><p>screenMode admite 0 (Ambas), 1 (Superior) y 2 (Inferior), en motores con DUAL_SCREEN.</p>\n<p>Los slots manuales van del 0 al 5. El 6 es el autoguardado: puedes cargarlo, pero saves.save no permite escribir en él. Comprueba canSave y capabilities; algunos motores no admiten estados.</p>\n<p>SNES, PSP y GameCube utilizan estas mismas operaciones. Para cargar necesitas el mismo juego y un formato de estado compatible. Las pruebas de Switch con Dynarmic pertenecen al core de desarrollo PC; Switch está excluido de las apps Android y Desktop. Los perfiles y botones del editor no habilitan un core ni sus estados.</p>\n<h2>Abrir o continuar un juego</h2><pre><code>await host.library.launch(gameId);    // Abre el juego sin cargar un estado de Nelkano.\nawait host.library.launch(gameId, 6); // Desde el autoguardado.</code></pre><p>Requiere <code>library.launch</code>. El gameId debe existir en la biblioteca y, si indicas un slot, ese estado debe estar guardado. Sin slot se abre el juego sin cargar un estado de Nelkano; el progreso interno se recupera desde los menús del juego. No se trabaja con rutas de ROM.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-02-game-details.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-02-game-details.webp\" width=\"860\" height=\"580\" alt=\"Ficha del juego y estados guardados (ejemplo).\" data-caption=\"Ficha del juego y estados guardados (ejemplo).\"></a><h2>Añadir un botón de pausa</h2><p>Declara emulation.read y emulation.control. Publica el botón en contributions y responde a su pulsación en dispatch:</p><pre><code>async contributions(context) {\n  if (context.surface !== 'emulation' || !context.session) return [];\n  return [{\n    moduleId: 'example.pause', id: 'pause',\n    slot: 'PLAYER_SYSTEM_BUTTON', label: 'Pausa',\n    actionId: 'pause', icon: 'assets/pause.png',\n    payload: { controlId: 'sys-pause' }\n  }];\n},\nasync dispatch(action, payload, context) {\n  if (action !== 'pause') return null;\n  const state = await host.emulation.snapshot();\n  if (!state || state.session.id !== context.session?.id ||\n      state.session.generation !== context.session?.generation) {\n    throw new Error('Esta partida ya terminó.');\n  }\n  await host.emulation.setPaused(context.session, !state.paused);\n  return null;\n}</code></pre><p>Incluye el PNG en files. controlId reutiliza sys-pause y la posición elegida en el Editor. Si lo omites, creas otro botón. Evita que varios módulos publiquen simultáneamente el mismo control.</p>\n<p>Al guardar se captura la imagen del motor, sin los controles. En NDS y 3DS, las miniaturas muestran la pantalla superior aunque estés viendo otra. Los estados anteriores sin captura la obtienen al volver a guardarse.</p>\n<h2>Solicitar autoguardados</h2><pre><code>await host.saves.setAutoSave(true);\nawait host.saves.setAutoSave(false);</code></pre><p>Requiere saves.autosave. La app sobrescribe el slot 6 cada 60 segundos de juego sin pausa, cuando el motor lo permite. Los slots manuales se conservan.</p><p>Cada módulo tiene su propia solicitud: basta una activa para mantener el autoguardado. Al desactivar o desinstalar el módulo se retira la suya. Guarda tu preferencia y aplícala de nuevo en activate. El intervalo, el destino y la cantidad de copias no son configurables desde esta API.</p>\n<h2>Responder a los cambios de la partida</h2><p>Con emulation.read, onEvent recibe session-started, session-paused, session-resumed, session-closed, session-reset, save-created, save-loaded y display-changed.</p><pre><code>async onEvent(event) {\n  if (event.type === 'save-created') {\n    console.log('Guardado', event.slot, event.automatic);\n  }\n  return null;\n}</code></pre><p>Los avisos incluyen session y gameId. Los de guardado añaden slot y automatic, que es true para el autoguardado. Consulta snapshot si necesitas el estado actualizado. Estos eventos no se emiten por fotograma.</p>\n<h2>Manejar sesiones y errores</h2><p>Utiliza la sesión recibida en la acción. La app comprueba su id y generation y rechaza referencias de una partida anterior. Captura los errores para explicar al usuario qué operación falló.</p><p>Studio permite probar las llamadas y miniaturas simuladas. Para comprobar que un estado recupera el juego correctamente, pruébalo en la app de destino. library.setCover puede utilizar su captura como portada.</p>\n<h2>Leer memoria y recursos con gameRuntime</h2><pre><code>const active = await host.emulation.snapshot();\nif (!active || active.systemId !== \"gba\") return;\nconst result = await host.gameRuntime.read(active.session, {\n  ranges: [{address: 0x02000000, length: 16}]\n});\n// result.ranges[0].data contiene los bytes en Base64.</code></pre><p><code>host.gameRuntime.read()</code> admite hasta 32 rangos y 64 KiB por petición; cada rango puede tener hasta 32 KiB. <code>patch()</code> recibe <code>writes</code> con <code>address</code>, <code>data</code> y <code>expected</code>. Solo funciona en adaptadores con escritura y compara los bytes esperados antes de aplicar el cambio.</p><p>Los permisos de estas operaciones son:</p><ul><li><code>read</code>: <code>emulation.memory.read</code>.</li><li><code>patch</code>: <code>emulation.memory.write</code>.</li><li><code>rom</code>, <code>romSearch</code> y <code>frame</code>: <code>emulation.resources</code>.</li><li>Checkpoints y proyectos: <code>emulation.handoff</code>.</li><li><code>subscribe</code>: <code>runtime.events</code>.</li><li><code>overlay</code>: <code>presentation.overlay</code>.</li></ul>\n<h3>ROM, imagen y avisos periódicos</h3><ul><li><strong>rom:</strong> lee {offset, length}.</li><li><strong>romSearch:</strong> busca {pattern, offset?, end?, alignment?, maximum?}; pattern es Base64.</li><li><strong>frame:</strong> recibe {width, height} y devuelve {width, height, argb}. Es una imagen CPU reducida, ARGB de 32 bits en little endian y Base64, de hasta 160 píxeles por eje y 16 384 píxeles en total. No exporta a la galería.</li><li><strong>subscribe(session, true):</strong> activa runtime-tick, por defecto cada 250 ms. Puedes pedir {intervalMs:100}; el host lo limita a 100–1000 ms. Una operación larga puede retrasar el aviso. Usa subscribe(session, false) para retirarlo.</li></ul>\n<h3>Superponer imágenes y etiquetas</h3><p>overlay(session, nodes) publica hasta 32 PNG y etiquetas en coordenadas del frame. Respeta los límites de imagen anunciados. Una lista vacía limpia la capa; cerrar la sesión o desactivar el módulo también la retira.</p>\n<h3>Crear un checkpoint privado</h3><p>checkpoint conserva un estado privado del módulo y su autor. checkpointRead lo lee por bloques; checkpointPatch crea otro comprobando expected y las regiones RAM del motor. restore exige el mismo juego. No aparece entre los slots manuales.</p>\n<h3>Trabajar con una copia de RPG Maker</h3><p>projectWrite y projectRead utilizan una copia privada del proyecto. Admiten Save/, Graphics/Characters/ y Graphics/Trainers/, con bloques de hasta 32 KiB y archivos de hasta 2 MiB. projectLaunch abre esa copia.</p><p>invoke llama a un método sin argumentos de Game o de una constante RGSS terminada en Bridge, mediante {receiver, method}. El juego debe ofrecer ese puente. external configura el transporte de mensajes RGSS. Estas operaciones requieren emulation.handoff.</p><p>Save/ es una ruta lógica de la copia privada. El host admite los archivos RGSS relativos a la raíz o a Save/ y lee la copia más reciente. El proyecto importado original se conserva.</p><p>Antes de invocar funciones de una copia privada, consulta projectActive(session, {gameId}). Su respuesta {active} confirma si esa partida está usando la copia de tu módulo, sin revelar su ruta.</p>\n<h2>Hacer que un sprite siga la cámara</h2><p>Añade id y camera al nodo de overlay. Con camera, x e y son coordenadas del mundo; x e y dentro de camera indican el origen de cámara observado al preparar el nodo. El host compara ese origen con los registros del fotograma y ajusta el dibujo al desplazarse el mapa.</p>\n<pre><code>await host.gameRuntime.overlay(session, [{\n  id: \"jugador-2\", x: 620, y: 180, width: 16, height: 32,\n  image: pngBase64, label: \"Mi amigo\",\n  camera: {xAddress: 0x04000014, yAddress: 0x04000016,\n    x: 500, y: 100, modulus: 512}\n}]);</code></pre><p>Requiere <code>emulation.memory.read</code> y <code>presentation.overlay</code>. Se admiten los registros GBA alineados de 16 bits entre <code>0x04000010</code> y <code>0x0400001e</code>, y los registros GB/GBC <code>SCY/SCX</code> en <code>0xff42/0xff43</code>. Utiliza los registros que entregue el adaptador: si faltan en el frame, el nodo se oculta.</p><p><code>modulus</code> debe ser una potencia de dos entre 2 y 65536. Las coordenadas de cámara se limitan a ±65536. Sin camera, el nodo usa coordenadas de pantalla. Un <code>id</code> estable conserva su identidad y suaviza los desplazamientos remotos entre posiciones.</p>\n<h2>Comprobar si se puede pausar</h2><p>Durante una conexión online en espera, activa o recuperándose, la partida debe avanzar: el host oculta Pausa y rechaza la pausa hasta desconectar. Studio reproduce esta regla con su conexión ficticia.</p><p>Consulta <code>pauseAllowed</code> en <code>host.emulation.snapshot()</code>. Si es <code>false</code>, <code>setPaused(session, true)</code> devuelve <code>false</code> y la partida continúa. Si falta el campo en un host anterior, trátalo como permitido. Reanudar sigue disponible.</p>\n<h2>Modificar el comportamiento del juego</h2><p>Utiliza <code>host.gameMod</code> para los espacios de memoria, escrituras por lotes y reglas. GBA ofrece memoria y ROM temporal; Studio las simula. <code>host.gameRuntime</code> conserva sus operaciones específicas. El capítulo <a href=\"/es/sdk-de-nelkano/modificar-juego\">Modificar un juego</a> explica capacidades, permisos, conflictos y retirada del módulo.</p>\n<p>RPG Maker XP/RGSS y MV/MZ usan los guardados internos del juego y no admiten guardar, cargar ni autoguardar estados de emulación. Los estados anteriores se conservan como archivos, pero no aparecen en Guardados o Continuar.</p>\n<h2>Transferir juegos y progreso</h2><p><code>host.transfers</code> intercambia contenido entre Android y escritorio. Declara <code>library.transfer</code> para juegos y <code>saves.transfer</code> para progreso. Cada permiso permite exportar e importar su categoría. Implementa la conexión y la interfaz de usuario; el host valida los datos y los incorpora a la biblioteca.</p>\n<h3>Exportar</h3><ol><li>Consulta <code>capabilities()</code> para obtener disponibilidad y cuotas.</li><li>Llama a <code>inspect({gameId})</code>. Recibes <code>contentFingerprint</code>, <code>savesRevision</code>, <code>saveKinds</code>, plataforma y limitaciones.</li><li>Crea una copia estable con <code>exportGame({gameId})</code> o <code>exportSaves({gameId})</code>. La respuesta es <code>{id,format,kind,gameId,title,systemId,size,sha256}</code>.</li><li>Lee bloques con <code>read({id,offset,length})</code>, que devuelve <code>dataBase64</code>.</li><li>Libera la exportación con <code>close({id})</code>.</li></ol>\n<h3>Importar</h3><ol><li><code>beginImport({kind,gameId,size,sha256})</code> crea una recepción privada.</li><li>Envía bloques consecutivos con <code>write({id,offset,dataBase64})</code> y comprueba el total recibido.</li><li><code>commit({id,expectedRevision?})</code> verifica tamaño, SHA-256, manifiesto y hashes antes de publicar el contenido.</li></ol><p>La respuesta incluye la identidad, <code>importedGame</code>, <code>importedSaves</code> (número de slots) e <code>importedSaveFiles</code> (total de archivos de progreso). El éxito consume la recepción. Si falla, puedes reintentar o descartarla con <code>abort({id})</code>. <code>close</code> y <code>abort</code> devuelven false si el identificador ya no existe o no es tuyo.</p>\n<h3>Reemplazar progreso existente</h3><p>Obtén la revisión en el dispositivo de destino con <code>inspect</code> y envíala como <code>expectedRevision</code>. El host comprueba que no haya cambiado mientras preparabas la transferencia. Sin revisión solo se importa progreso si aún no existe.</p><p>Se reemplaza el conjunto completo: también se eliminan los archivos de progreso ausentes del paquete. Explica la sustitución al usuario y resuelve los conflictos. Si ya existe el juego con contenido diferente, se rechaza la importación sin sobrescribirlo.</p>\n<h3>Qué contiene una transferencia</h3><p><code>nelkano-transfer/1</code> es un ZIP de datos con <code>manifest.json</code> y <code>data/</code>. Es independiente del paquete ejecutable <code>.nelkmod</code>. Conserva el <code>gameId</code> original y verifica la identidad del sistema, la entrada y el índice de archivos con tamaños y hashes.</p><p>Los juegos incluyen ROM, disco y pistas o proyecto completo, también sus directorios vacíos. El progreso incluye slots 0–6, capturas, batería/SRAM, carátula, archivos nativos por juego y <code>Save</code> de RGSS. Conserva los requisitos de módulos y autores de los guardados. Los datos privados de otros módulos quedan fuera.</p>\n<h3>Cerrar la partida y comprobar compatibilidad</h3><p>Inspeccionar, preparar una exportación y publicar una importación requieren la sesión cerrada. Puedes recibir bloques o leer una exportación ya preparada mientras se usa la app. Tras una interrupción, el host completa o revierte la publicación antes de abrir otra partida.</p><p>La transferencia conserva bytes; no convierte estados entre motores. El destino necesita un formato compatible y algunos guardados internos dependen de la plataforma o versión. Consulta la inspección y el manifiesto. Los guardados MV/MZ de WebView quedan fuera. Los paquetes sintéticos de Studio solo se importan en Studio.</p>\n<h3>Límites de transferencia</h3><ul><li>Bloques de hasta 65 536 bytes.</li><li>Hasta 64 GiB de archivo y contenido expandido; 100 000 archivos y manifiesto de hasta 32 MiB.</li><li>Dos operaciones abiertas por módulo y ocho globales; caducan tras diez minutos sin actividad.</li><li>Se rechazan rutas inseguras, enlaces, duplicados por mayúsculas, miembros extra y tamaños o hashes incorrectos.</li></ul><p>El espacio y la velocidad dependen del dispositivo. Las transferencias largas tienen su propio límite de comunicación entre procesos; no se sujetan al de cinco segundos del código JavaScript.</p>\n<h2>Capturar una imagen o grabar un clip</h2><ol><li>Consulta <code>host.media.capabilities()</code> para saber si la sesión entrega imagen, audio y captura CPU o de superficie.</li><li>Usa <code>capture({session,screen?,region?,format?})</code> para capturar en PNG o JPEG, con resolución completa o recorte.</li><li>Lee el resultado por bloques de hasta 64 KiB con <code>read({id,offset,length})</code>.</li><li>Libéralo con <code>release({id})</code>.</li></ol>\n<p><code>startRecording({session,fps?,maxSeconds?})</code>, <code>recording({id})</code> y <code>stopRecording({id})</code> graban un AVI con MJPEG y PCM si el motor entrega audio. Los límites son 30 fps, 300 segundos y 1 GiB. <code>export({id})</code> abre el selector de documentos para guardarlo y <code>remove({id})</code> lo descarta.</p><p>Se graba el juego, sin escritorio, micrófono ni cámara. Pasar al fondo termina la grabación; revocar el módulo elimina sus recursos. Cada resultado conserva el permiso y la generación de sesión con los que se creó.</p>\n<h3>Ajustar las etiquetas superpuestas</h3><p><code>runtime.overlay</code> admite textos de hasta 4096 caracteres. Puedes indicar <code>textWidth</code> en píxeles del juego, <code>fontSize</code> de 8 a 48 sp, <code>maxLines</code> de 1 a 16 y <code>textColor</code>/<code>backgroundColor</code> con formato <code>#AARRGGBB</code>. Omitir estos campos conserva la presentación anterior.</p>\n<h2>Analizar y traducir texto de un juego</h2><ol><li>Consulta <code>host.translation.capabilities()</code> para conocer los formatos y proveedores.</li><li><code>projects()</code> recupera los proyectos de tu módulo, también tras reiniciar la app.</li><li>Inicia el análisis con <code>analyze({gameId})</code>. Usa <code>job({jobId})</code> para seguirlo o <code>cancel({jobId})</code> para cancelarlo.</li><li>Lee las entradas por páginas con <code>entries({projectId,offset?,limit?})</code> y guarda ediciones con <code>update({projectId,resourceId,textId,translation,expectedRevision})</code>.</li></ol>\n<p><code>translate({projectId,provider,sourceLanguage,targetLanguage,limit?})</code> prepara un borrador automático. También admite traducir <code>{text,provider,sourceLanguage,targetLanguage,context?}</code> sin crear un proyecto. Con la partida cerrada, <code>build({projectId,title?,allowPartial?})</code> reconstruye y registra una copia independiente del juego. <code>remove({projectId})</code> elimina el proyecto privado.</p><p>Se admiten los formatos de texto reconocidos de GBA/NDS. Android utiliza ML Kit y descarga los modelos al solicitarlos. Escritorio permite analizar, editar y reconstruir; puedes incorporar traducciones de un servicio externo mediante update, pero no incluye traducción automática local.</p><p>Esta API no incluye OCR. Comprueba el espacio disponible, las fuentes y la maquetación en el juego reconstruido.</p>\n<h2>Aplicar un shader a la imagen</h2><p>Declara <code>graphics.shaders</code> y consulta <code>host.graphics.capabilities({session})</code>. <code>shader({session,id,source,uniforms?})</code> aplica el efecto y devuelve <code>{id,revision}</code>; <code>removeShader({session,id})</code> devuelve <code>{removed}</code> al retirarlo.</p><p>La fuente SkSL/AGSL utiliza <code>uniform shader content</code>, <code>uniform float2 resolution</code> y <code>half4 main(float2 p)</code>. Los uniforms adicionales deben ser floats finitos. Se admite un efecto y propietario por sesión, hasta 16 KiB de fuente y 16 uniforms adicionales, sin bucles for, while o do.</p><p>El shader solo filtra los píxeles del juego. En Android requiere API 33 y una salida CPU; en escritorio utiliza Skia. Las superficies nativas separadas no ofrecen ese postprocesamiento. Las capturas conservan la imagen original del motor. Cerrar la sesión o revocar el módulo retira el efecto.</p>\n<h2>Cambiar resolución y texturas</h2><h3>Resolución interna</h3><p>Con <code>graphics.options</code>, llama a <code>host.graphics.options({session,resolutionScale})</code>; devuelve <code>{resolutionScale}</code>. GameCube admite 1×, 2× y 4× en Android y escritorio. Se vuelven a dibujar las primitivas, conservando la memoria y los estados que utiliza el juego.</p><p>La superficie máxima es 2560×2112, con hasta dos copias visuales escaladas de 720×576. Otros tamaños mantienen su salida nativa. La imagen de mayor resolución solo se muestra si coincide con el framebuffer que presenta el juego y la CPU no lo ha modificado.</p><p>Activar escala o reemplazos en GameCube desactiva Vulkan y utiliza el rasterizador software. Al retirarlos puede recuperar la selección anterior. Prueba el rendimiento y la imagen en cada juego.</p>\n<h3>Identificar texturas</h3><p><code>graphics.textures</code> permite reemplazos PNG en GameCube, PS1 y PSP. Se conservan la VRAM y las lecturas originales del juego. <code>textureIds({session,offset?,limit?})</code> devuelve <code>{items:[{textureId,width,height}],total,offset}</code>. La observación empieza al consultar y conserva hasta 512 identidades SHA-256 basadas en formato, dimensiones, datos y paleta.</p><ul><li><strong>PS1:</strong> identifica atlas de 256×256 con profundidad y CLUT. El reemplazo utiliza un pase visual RGB555 a resolución nativa, conservando máscaras y semitransparencia. El display de 24 bits y las primitivas que solapan su página o paleta utilizan la ruta original. Las escrituras del juego invalidan el reemplazo visual, incluso si repiten el valor; las copias de framebuffer no lo transportan.</li><li><strong>PSP:</strong> utiliza un pase software separado, con el nivel base del reemplazo. Mientras está activo deshabilita las rutas GPU, SIMD y paralelas.</li></ul><p>PS1 y PSP anuncian resolución 1×: un reemplazo de textura no aumenta la resolución de la geometría.</p>\n<h3>Cargar un reemplazo</h3><ol><li>Prepara un PNG con dimensiones que sean múltiplos enteros uniformes de la textura observada.</li><li>Inicia la carga con <code>beginTexture({session,textureId,width,height,size,sha256})</code>.</li><li>Envía bloques consecutivos con <code>writeTexture({id,offset,dataBase64})</code>.</li><li>Termina con <code>commitTexture({id})</code>; el host verifica hash y dimensiones.</li></ol><p><code>textures({session})</code> lista tus reemplazos y <code>removeTexture({session,textureId})</code> los retira. Límites: 2048×2048, 16 MiB por PNG, bloques de 64 KiB, 64 reemplazos y 128 MiB de píxeles en el banco del motor. Las cargas caducan a los diez minutos. Otro módulo no puede sustituir un recurso activo y no se genera una pirámide mip HD. Revocar el módulo o cerrar la sesión retira los recursos.</p>\n<h2>Capturar el micrófono</h2><p>Declara <code>media.microphone</code>. Consulta <code>host.microphone.capabilities()</code>, inicia con <code>start({sampleRate?,blockMs?,maxSeconds?})</code>, recoge bloques con <code>read({id,maxBlocks?})</code> y termina con <code>stop({id})</code>. Android solicita RECORD_AUDIO; escritorio utiliza los permisos de audio del sistema.</p><p>El audio es PCM16LE mono, a 16/24/48 kHz, con bloques de 20/40/100 ms en Base64 y número de secuencia. Se admite una captura activa y hasta cinco minutos. Lee a tiempo: <code>droppedBlocks</code> indica los bloques perdidos.</p><p>Pasar al fondo, revocar el permiso o cerrar cancela la captura y las lecturas pendientes. El reconocimiento de voz debe implementarlo tu módulo.</p>\n<h2>Filtrar el audio del juego</h2><p>Declara <code>audio.dsp</code> y consulta <code>host.audioDsp.capabilities({session})</code>. <code>list({session})</code> devuelve tus cadenas de filtros. <code>set({session,id,expectedRevision,sampleRate,stages})</code> crea o actualiza una cadena comprobando expectedRevision.</p><p>Las etapas admitidas son <code>{kind:'gain',gain}</code>, <code>{kind:'biquad',b0,b1,b2,a1,a2}</code> y <code>{kind:'fir',coefficients}</code>. La respuesta indica si se aplicó el cambio, su revisión y la cadena resultante. <code>remove({session,id,expectedRevision})</code> la retira. El host comprueba valores finitos, estabilidad, coste y saturación PCM16.</p><p>Se admiten cuatro cadenas por propietario, dieciséis globales y filtros FIR de hasta 64 coeficientes. Se aplican por propietario e ID. El historial del filtro se reinicia al pausar, cambiar de generación o modificar la configuración; una frecuencia distinta de la declarada omite la cadena.</p><p>Solo se filtra el PCM del motor antes de reproducirlo. Los sonidos por órdenes, películas, WebAudio y beep quedan fuera. Grabaciones y observadores reciben el PCM original.</p>\n<h2>Rebobinar y editar repeticiones</h2><p>Declara <code>emulation.timeline</code> para utilizar <code>host.timeline</code>. Consulta <code>capabilities({session})</code>: el motor debe ofrecer un estado completo, avance exacto por fotograma y ausencia de efectos externos. La integración inicial es CHIP-8. Un único propietario controla el historial y las repeticiones de la sesión.</p>\n<h3>Configurar el historial y avanzar</h3><p><code>configure({session,historyFrames?,historyBytes?,runaheadFrames?,rewinding?})</code> configura el historial y <code>status({session})</code> consulta su posición, pausa y actividad. <code>rewind({session,frames})</code> retrocede y pausa; <code>rewinding:true</code> mantiene el rebobinado. Runahead admite 0–2 fotogramas especulativos: presenta vídeo adelantado, mantiene el audio confirmado y restaura el estado confirmado.</p>\n<p><code>step({session,frames?,inputs?,commit?})</code> necesita la partida pausada y admite hasta 120 fotogramas. Cada entrada contiene <code>keys</code> y, opcionalmente, touch. Sin entradas se usa una neutra; una entrada única se repite. <code>commit:false</code> restaura el estado al terminar, sin publicar imagen o audio especulativos. Si esa restauración falla, la sesión queda no disponible.</p>\n<h3>Grabar y reproducir entradas</h3><p><code>startRecording({session})</code> inicia una grabación, <code>stopRecording({session})</code> la termina y <code>recording({session,id?,offset?,limit?})</code> permite leerla por páginas. Incluye las entradas efectivas físicas y de módulos y, cuando está disponible, el hash del estado.</p><p><code>replay({session,id,fromFrame?,toFrame?})</code> reproduce las entradas y pausa al terminar. <code>seek({session,id?,frame})</code> reconstruye una posición sin mostrar resultados intermedios. Se registra por fotogramas, sin tiempos inferiores a uno. Reiniciar, cargar un estado externo, cerrar o revocar termina el controlador. <code>release({session})</code> libera sus datos.</p>\n<h3>Exportar y editar una repetición</h3><p><code>export({session,id?})</code> devuelve <code>{id,format:'nelkano-replay/1',size,sha256}</code>. Lee los bloques Base64 con <code>read({id,offset,length})</code>. Para importar, utiliza <code>beginImport({session,size,sha256})</code>, <code>write({id,offset,dataBase64})</code> y <code>commit({session,id})</code>; <code>abort({id})</code> cancela.</p><p>El JSON editable contiene <code>formatVersion:1</code>, <code>coreFormat</code>, <code>romSha256</code>, <code>firstFrame</code>, un estado inicial opaco y las entradas por fotograma. En un editor TAS puedes modificar entradas y retirar los <code>stateSha256</code> posteriores que hayan dejado de ser válidos. El host verifica ROM, formato, hash y estado inicial antes de restaurar.</p><p>Límites: 3600 estados y 64 MiB de historial; dos grabaciones privadas de hasta 36 000 fotogramas y 8 MiB cada una. Una repetición exportada admite 16 MiB y bloques de 64 KiB. Hay dos operaciones abiertas por módulo y ocho globales, con caducidad tras diez minutos sin actividad. La duración del historial depende del tamaño de los estados y del rendimiento.</p>\n<h2>Mezclar audio externo con la partida</h2><p>Declara <code>audio.mix</code> para utilizar <code>host.audioMix</code>. Consulta <code>capabilities({session})</code> para conocer el formato PCM16LE, la frecuencia, los canales y el reloj <code>{frame,epoch,paused}</code>. El frame es la referencia publicada por el host, no necesariamente el VBlank físico del motor. El audio se mezcla antes de los filtros DSP y la reproducción.</p>\n<ol><li>Prepara el clip con <code>begin({session,size,sha256,sampleRate,channels})</code>, <code>write({id,offset,dataBase64})</code> y <code>commit({id})</code>. Su PCM16LE mono o estéreo debe tener la misma frecuencia y canales que el motor; no hay remuestreo automático.</li><li>Programa la reproducción con <code>play({session,id,atFrame?,gain?,originalGain?})</code>. Devuelve playbackId y la frontera elegida. Puedes mezclarlo con el original o silenciar este durante el clip.</li><li>Consulta <code>status({session})</code> o detén las reproducciones con <code>stop({session,playbackId?})</code>.</li><li><code>remove({id})</code> descarta una carga o clip y sus reproducciones.</li></ol>\n<p>Se admiten 16 MiB por clip, 32 MiB por propietario, 64 MiB globales, ocho reproducciones programadas y bloques de 64 KiB. Los recursos pertenecen a la sesión y caducan tras diez minutos sin actividad.</p><p>Pausar, restaurar, cambiar de generación o formato, pasar al fondo o revocar cancela las reproducciones. Capturas, grabaciones y observadores conservan el PCM original. No permite separar voces, música o efectos que el motor no entregue como pistas distintas; sonidos por órdenes, películas, WebAudio y beep quedan fuera.</p>\n<h2>Capturar una imagen de cámara</h2><p>Declara <code>media.camera</code> y utiliza <code>host.camera.capabilities()</code>, <code>list()</code>, <code>start</code>, <code>read</code> y <code>stop</code>. Android solicita CAMERA; escritorio anuncia captura no disponible y Studio utiliza una imagen sintética.</p>\n<ol><li>Solicita una cámara anunciada con <code>start({cameraId,width?,height?,fps?,format?})</code>. El valor predeterminado es JPEG a 640×480 y 5 fps; se admiten JPEG/PNG hasta 1280×720 y 15 fps. Comprueba el tamaño real devuelto.</li><li><code>read({id})</code> fija una instantánea y devuelve un frame o null.</li><li>Lee los bytes con <code>read({id,frameId,offset,length})</code> en bloques de hasta 64 KiB, conservando el mismo frameId durante toda la lectura.</li></ol><p>Se admite una captura activa, dos instantáneas por captura y hasta 4 MiB por imagen.</p>\n<h3>Entregar la imagen al juego</h3><p>Consulta <code>capabilities({session})</code> para conocer los puertos. <code>deliver({id,session,port})</code> necesita además <code>emulation.input</code> y envía la última imagen al puerto compatible.</p><p>La integración inicial es la cámara emulada de 3DS: <code>cam1</code> y <code>cam2</code> convierten a RGB565/YUYV con el tamaño solicitado por el juego. La imagen externa no se guarda dentro de sus estados. NDS normal no anuncia cámara; la óptica, calibración estéreo y sincronización física de 3DS son parciales.</p><p><code>stop({id})</code>, pasar al fondo o revocar el módulo cierra la captura y retira entregas y solicitudes pendientes.</p>\n<h2>Elegir un motor externo</h2><p>Consulta <code>host.engines.capabilities()</code>. <code>list({gameId})</code> o <code>list({systemId})</code> lista los proveedores, identidad, versión, selección y compatibilidad de guardados.</p><p><code>select({gameId,engineId})</code> elige el motor de un juego; <code>select({systemId,engineId})</code>, el de un sistema. Indica solo uno de los dos. engineId null recupera el predeterminado. La elección se aplica al volver a abrir: una partida en marcha conserva su motor.</p>\n<h3>Controles, opciones y recursos</h3><ul><li><code>controls({session})</code> describe los puertos y controles. <code>input({session,port,control,value})</code> envía valores dentro de sus rangos, incluidos ejes y gatillos.</li><li><code>options({session})</code> devuelve el esquema y los valores. <code>configure</code> aplica opciones o guarda las de la próxima apertura por engineId, respetando tipos, rangos y requisitos de reinicio.</li><li><code>resources({engineId,gameId?})</code> describe los archivos o BIOS necesarios. <code>importResource</code> abre el selector del host sin revelar rutas privadas.</li><li><code>importGame({engineId,systemId})</code> abre el selector y vincula el juego importado al motor. Requiere <code>library.transfer</code> y <code>resources.documents</code>. Indicar systemId resuelve extensiones ambiguas, como ISO.</li><li><code>media({session})</code> y <code>changeMedia({session,slot,contentId?})</code> consultan y cambian los discos o medios anunciados.</li></ul><p>Cada operación exige sus permisos de lectura, control, entrada o recursos.</p>\n<h3>Guardar el progreso de un motor propio</h3><p>Los estados manuales admiten hasta 128 MiB y viajan en bloques de 64 KiB. El puerto timeline tiene un límite independiente de 16 MiB. Los estados y el progreso persistente utilizan identificadores de compatibilidad distintos y un espacio de almacenamiento asociado al motor.</p><p>Coincidir en el juego no permite cargar el estado de otro motor. Adapta la VFS del core para utilizar los recursos autorizados y el almacenamiento privado del host: escribir solo en su carpeta temporal no conserva el progreso.</p>\n<h2>Implementar las capacidades de tu motor</h2><p>La ABI C versionada comprueba que el descriptor corresponda al binario compilado. Memoria, reglas, observación, texturas, resolución, cámara, medios y timeline deben declararse e implementarse con sus callbacks. Si faltan, esas capacidades permanecen no disponibles.</p><p>El puente opcional utiliza métodos JSON acotados y bloques para estados e imágenes. Poder ejecutar una ROM o guardar estados no implica admitir escritura de memoria o rebobinado determinista. El ejemplo de GPU no anuncia esos puertos.</p><p>El motor entrega PCM al transporte de audio y puede utilizar superficies GPU separadas. Las capturas compuestas requieren su implementación en el adaptador. Consulta las capacidades concretas: una salida GPU no habilita automáticamente shaders, capturas u otras operaciones gráficas.</p>\n"},{"id":"de520020-2026-4000-8000-000000000021","language":"es","title":"Modificar un juego","path":"/es/sdk-de-nelkano/modificar-juego","content":"<p>Usa <code>host.gameMod</code> para leer datos de una partida y cambiar su comportamiento. Tu módulo debe conocer el juego: qué representan sus datos, dónde están y cuándo es seguro modificarlos.</p><p>GBA admite lectura, escritura, valores numéricos, observaciones y reglas en Android y Windows/Linux. Studio ofrece memoria de prueba. En cualquier motor, consulta primero las capacidades de la sesión.</p>\n<h2>Comprobar permisos y capacidades</h2><p>Declara en capabilities los permisos de las operaciones que uses. Los ejemplos de este capítulo utilizan:</p><pre><code>\"capabilities\": [\n  \"emulation.game-mod\", \"emulation.read\",\n  \"emulation.memory.read\", \"emulation.memory.write\", \"runtime.events\"\n]</code></pre><p><code>emulation.game-mod</code> identifica este contrato; un host antiguo rechaza el paquete. Cada operación necesita además sus permisos:</p><ul><li>Consultar capacidades: <code>emulation.read</code>.</li><li>Leer memoria o listar reglas: <code>emulation.memory.read</code>.</li><li>Escribir memoria o retirar reglas: <code>emulation.memory.write</code>.</li><li>Instalar reglas: lectura y escritura.</li></ul><pre><code>const active = await host.emulation.snapshot();\nif (active) {\n  const support = await host.gameMod.capabilities(active.session);\n  console.log(support.systemId, support.spaces, support.frameRules);\n}</code></pre><p>Revisa estos campos de la respuesta antes de operar:</p><ul><li>session, gameId, systemId y simulated: partida, juego, sistema y si estás en una simulación.</li><li>memoryRead, memoryWrite, atomicWrite, frameRules y memoryObserve: operaciones disponibles.</li><li>spaces: espacios con id, addressBits, byteOrder y regions. Cada región indica id, start, length, readable y writable.</li><li>maxRanges, maxBytes, maxRules y maxRuleBytes: límites anunciados por el motor.</li></ul><p>Un core sin acceso a memoria devuelve espacios vacíos, funciones de memoria en false y límites en cero. Comprueba input, saveRequirements y newGame por separado: pueden tener otra disponibilidad.</p>\n<h2>Elegir una dirección de GBA</h2><p>En GBA, el espacio <code>cpu</code> utiliza direcciones absolutas de 32 bits y orden little endian:</p><ul><li>EWRAM: 0x02000000, 256 KiB, lectura y escritura.</li><li>IWRAM: 0x03000000, 32 KiB, lectura y escritura.</li><li>IO, paleta, VRAM, OAM y bus de ROM: solo lectura.</li></ul><p>Cada operación debe quedar dentro de una región anunciada.</p><p>Las direcciones del bus ROM incluyen sus espejos de memoria; no son offsets del archivo. Para leer el archivo original utiliza <code>host.gameRuntime.rom</code>. La inspección conserva el estado del cartucho y de la comunicación serie.</p><p>cpu no permite escribir en IO ni en su bus ROM. Para cambiar el cartucho cargado, utiliza el espacio rom descrito más abajo.</p>\n<h2>Leer y escribir por lotes</h2><p>El ejemplo usa una ROM propia que reserva 0x0203FFF0 como byte de prueba. En otro juego debes localizar una dirección adecuada antes de escribir.</p><pre><code>async function changeScratchByte(host, session) {\n  const support = await host.gameMod.capabilities(session);\n  if (!support.memoryRead || !support.atomicWrite) {\n    throw new Error(\"Este core no permite esta modificación.\");\n  }\n  const address = 0x0203FFF0;\n  const current = await host.gameMod.read(session, {\n    ranges: [{space: \"cpu\", address, length: 1}]\n  });\n  return host.gameMod.write(session, {\n    writes: [{space: \"cpu\", address,\n      expected: current.ranges[0].data, data: \"AQ==\"}]\n  });\n}</code></pre><ul><li>read devuelve ranges con space, address y data en Base64.</li><li>write recibe data y expected: Base64 canónico de igual longitud.</li><li><code>AQ==</code> representa un byte con valor 1.</li><li>Las escrituras de un lote no pueden solaparse.</li></ul><p>Antes de escribir, el host compara todos los expected. Si alguno ha cambiado, devuelve <code>{applied:false, conflictIndex}</code> con el primer conflicto, sin aplicar el lote. Si coinciden, devuelve <code>{applied:true}</code>.</p><p>Ante un conflicto, decide si debes volver a leer. Repetir la escritura sin revisar los datos puede sobrescribir un cambio del juego.</p>\n<h2>Trabajar directamente con números</h2><p>readValues admite u8, i8, u16, i16, u32, i32, f32 y f64. Cada entrada lleva space, address y type; endian puede ser little o big y usa por defecto el orden del espacio.</p><p>La respuesta incluye value, data Base64 y el endian efectivo. Un float no finito devuelve value:null y conserva data. Utiliza read/write de bytes para comparar bits exactos, NaN, infinito o cero con signo.</p>\n<pre><code>const current = await host.gameMod.readValues(session, {\n  values: [{space: \"cpu\", address: 0x0203FFF0, type: \"u16\"}]\n});\nconst previous = current.values[0].value;\nconst result = await host.gameMod.writeValues(session, {\n  writes: [{space: \"cpu\", address: 0x0203FFF0, type: \"u16\",\n    expected: previous, value: (previous + 1) &amp; 65535}]\n});</code></pre><p>Este ejemplo necesita dos bytes reservados por tu ROM. writeValues valida y convierte el lote completo antes de escribir:</p><ul><li>Conserva expected, applied/conflictIndex, propiedad y permisos de write.</li><li>Rechaza enteros fuera de rango, fracciones en tipos enteros y desbordamientos de float.</li><li>Admite hasta 32 valores.</li></ul><p>En hosts anteriores, comprueba que existan readValues y writeValues.</p>\n<h2>Recibir avisos cuando cambien los datos</h2><p>observe necesita emulation.game-mod, emulation.memory.read y runtime.events. Comprueba memoryObserve, maxWatches, maxWatchBytes y maxWatchRanges.</p><p>El adaptador toma muestras y el host agrupa los avisos; tu JavaScript no se ejecuta en cada frame.</p>\n<pre><code>await host.gameMod.observe(session, {\n  id: \"scratch-value\", everyFrames: 4,\n  ranges: [{space: \"cpu\", address: 0x0203FFF0, length: 2}]\n});\n// Dentro de onEvent(event):\nif (event.type === \"game-memory-changed\") {\n  console.log(event.watchId, event.changes, event.ranges);\n}\n// Para dejar de observar:\nawait host.gameMod.unobserve(session, \"scratch-value\");</code></pre><p>La primera muestra se toma al registrar. everyFrames admite 1–600, con valor inicial 1. Las siguientes muestras llegan después de avanzar el juego; pausar o capturar una imagen no cuenta.</p><p>Reiniciar o cargar dentro de la misma sesión conserva la observación. La comparación continúa cuando el juego vuelve a avanzar.</p><p>game-memory-changed entrega session, watchId, sequence, frame, changes y ranges. Cada rango contiene space, address, before y data en Base64.</p><p>Solo hay un aviso pendiente por observación: conserva el primer before, el último data y el número de muestras con cambios. Si un valor cambia y vuelve al inicial, changes puede ser mayor que cero aunque before y data coincidan. Los cambios entre muestras pueden pasar inadvertidos.</p><ul><li>El host consulta avisos aproximadamente cada 100 ms en primer plano; el intervalo de entrega puede variar.</li><li>frame es el contador del adaptador. En Studio cuenta pasos sintéticos.</li><li>Cada módulo recibe solo sus observaciones.</li><li>Repetir un id propio sustituye la observación y descarta el aviso pendiente.</li><li>unobserve descarta también ese aviso y devuelve true si existía la observación, o false si no.</li><li>Cerrar o cambiar de sesión elimina las observaciones.</li></ul><p>En GBA, todos los módulos de una sesión comparten 16 observaciones, 32 rangos y 16 KiB observados. Observar no reserva zonas de escritura. Retirar un módulo que solo observa mantiene abierta la partida.</p>\n<h2>Buscar valores que cambian</h2><p>Solicita emulation.game-mod y emulation.memory.read. Comprueba memorySearch y los límites maxSearches, maxScanBytes, maxSearchBytes y maxSearchResults.</p><p>GBA y Studio admiten por sesión hasta cuatro búsquedas, 256 KiB por búsqueda y 1 MiB de capturas en total. Cada página devuelve hasta 128 resultados. Otros motores, incluidos los V2 con un puerto propio, pueden anunciar límites distintos.</p>\n<pre><code>const search = await host.gameMod.searchStart(session, {\n  space: \"cpu\", address: 0x02000000, length: 262144,\n  type: \"u16\", mode: \"unknown\"\n});\n// Después de cambiar algo en el juego:\nconst refined = await host.gameMod.searchRefine(session, {\n  id: search.id, mode: \"increased\"\n});\nconst page = await host.gameMod.searchRead(session, {\n  id: search.id, offset: 0, limit: 32\n});\nawait host.gameMod.searchClose(session, {id: search.id});</code></pre>\n<p>Configura searchStart con:</p><ul><li>equal y value para buscar un número conocido, o unknown sin valor.</li><li>Uno de los ocho tipos numéricos.</li><li>endian little/big; por defecto se usa el orden del espacio.</li><li>alignment 1, 2, 4 u 8: paso entre candidatos desde la dirección inicial. Por defecto coincide con el tamaño del tipo.</li></ul><p>El rango completo debe caber en una región legible.</p><p>searchRefine conserva los candidatos que cumplen la comparación y actualiza la captura de referencia al terminar:</p><ul><li>equal/notEqual: comparan con value.</li><li>changed/unchanged: comparan los bits con la captura anterior, sin value.</li><li>increased/decreased: comparan números finitos con la captura anterior, sin value.</li></ul><p>equal compara números y redondea f32 a su representación. Los valores no finitos se entregan como null y no coinciden con uno finito.</p>\n<p>Las respuestas incluyen id, matchCount, scannedBytes, revision, space, address, length, type, endian, alignment y paged. searchRead añade offset, results y hasMore.</p><p>Cada resultado contiene dirección, tipo, orden, value y data Base64 de la última captura. Consultar otra página no vuelve a leer el juego ni pierde los candidatos restantes.</p><p>Las capturas usan tramos de hasta 32 KiB. Si necesitas un instante coherente entre tramos, pausa el juego antes de capturar.</p><p>Las búsquedas pertenecen al módulo y autor que las creó. searchClose devuelve false si no encuentra una búsqueda propia. Retirar un módulo que solo busca conserva abierta la partida.</p><p>Cerrar o cambiar la sesión descarta las búsquedas. Reiniciar o cargar dentro de la misma sesión conserva la captura hasta la siguiente refinación.</p>\n<h2>Explorador de memoria en Studio</h2><ol><li>Abre una sesión GBA de prueba en el visor.</li><li>Entra en Ajustes → Explorar memoria y elige dirección, tipo y rango.</li><li>Busca un valor conocido o empieza con valor desconocido. Refina por cambios, aumentos o disminuciones.</li><li>Selecciona un resultado y genera una lectura, escritura comprobada u observación en JavaScript o TypeScript.</li><li>Pulsa Copiar código e incorpora las funciones a tu módulo con sus permisos.</li></ol><p>La escritura generada lee expected y comprueba el resultado. La observación requiere también runtime.events.</p><p>Cambiar valor de prueba modifica RAM sintética y respeta las zonas reservadas por módulos. Cerrar el explorador libera sus búsquedas; los valores se conservan hasta cerrar o restablecer la sesión.</p><p>El explorador no se conecta a partidas reales ni edita tus archivos automáticamente.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-08-explorador-memoria.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/studio-08-explorador-memoria.webp\" width=\"1440\" height=\"1000\" alt=\"Explorador de memoria con una sesión GBA simulada.\" data-caption=\"Explorador de memoria con una sesión GBA simulada.\"></a><h2>Aplicar una regla en cada frame</h2><p>installRule registra una regla de la sesión. Antes de cada frame, el core comprueba sus condiciones y todos los expected; aplica el lote solo si coinciden. La regla se ejecuta junto al core, sin llamadas a JavaScript ni esperas de red, y se detiene mientras el juego está pausado.</p><p>En esta ROM propia, 0x0203FFF1 es una señal. Cuando vale 1 y el byte anterior vale 0, la regla escribe 2 en ese byte anterior:</p><pre><code>await host.gameMod.installRule(session, {\n  id: \"scratch-rule\",\n  conditions: [{space: \"cpu\", address: 0x0203FFF1,\n    expected: \"AQ==\"}],\n  writes: [{space: \"cpu\", address: 0x0203FFF0,\n    expected: \"AA==\", data: \"Ag==\"}]\n});</code></pre><ul><li>conditions es opcional y puede leer también IO o bus ROM.</li><li>Las escrituras acceden a RAM o al espacio rom de GBA, no a IO.</li><li>Después de escribir 2, expected deja de coincidir. La regla solo vuelve a actuar si el juego restablece los valores esperados.</li><li>Reinstalar un id propio sustituye la regla y devuelve <code>{id}</code>.</li></ul><pre><code>const myRules = await host.gameMod.listRules(session);\nconst removed = await host.gameMod.removeRule(session, \"scratch-rule\");</code></pre><p>listRules devuelve tus reglas con id, conditions y writes. removeRule devuelve true si retira una regla propia y false si no existía. Un módulo no puede listar ni retirar las de otro, aunque compartan autor.</p>\n<h2>Límites y conflictos</h2><ul><li>GBA: hasta 32 rangos y 32 KiB por operación.</li><li>Cada regla: hasta 32 condiciones y escrituras en total.</li><li>Por sesión, compartido entre módulos: 16 reglas y 32 KiB de condiciones y escrituras.</li></ul><p>Utiliza los límites anunciados por el host de destino.</p><p>Escribir en una zona reservada por otro módulo devuelve <code>modification-conflict</code>. Retirar una regla detiene su ejecución, pero no libera las zonas reservadas hasta cerrar la sesión: todavía pueden conservar sus efectos.</p><p>El control de solapamientos incluye las escrituras de <code>host.gameRuntime.patch</code>. Los módulos que solo usan ese puerto anterior conservan su comportamiento al retirarse; el cierre de sesión descrito aquí se aplica a las nuevas escrituras o reglas de gameMod.</p>\n<h2>Retirar el módulo y conservar partidas</h2><p>Cerrar la sesión elimina reglas y modificaciones activas; el archivo ROM original permanece intacto. Reiniciar o cargar un estado dentro de la misma sesión conserva las reglas y vuelve a comprobar conditions y expected.</p><p>Retirar una regla no restaura los bytes anteriores. Si un módulo ha modificado la sesión, desactivarlo, actualizarlo o perder su proceso cierra esa sesión, aunque ya hubiera retirado las reglas. Las referencias antiguas de sesión dejan de ser válidas.</p><p>Los efectos pueden quedar en los guardados habituales. Informa al usuario antes de modificar su progreso. Si cargar la partida exige tu módulo o empezar de cero, declara los requisitos de guardado explicados más abajo.</p>\n<h2>Probar el módulo</h2><p>En Studio, simulated:true permite probar el código y el contrato. Revisa permisos, expected incorrectos, rangos no disponibles, conflictos entre módulos, carga/reinicio, desactivación y cambio de sesión. Después prueba el paquete con una ROM propia en la app de destino.</p><p>Construye la interfaz con páginas y formularios del SDK. Ofrece las operaciones de memoria o ROM temporal que anuncie cada motor.</p>\n<h2>Introducir botones desde un módulo</h2><p>Con emulation.game-mod y emulation.input, llama a gameMod.input(session,{buttons:[{code,pressed}]}). Las pulsaciones del módulo se combinan con las del usuario y otros módulos; al desactivarlo se liberan solo las suyas.</p><p>Consulta capabilities y usa los códigos admitidos por el core. Studio simula las pulsaciones; este soporte se consulta por separado del acceso a memoria.</p>\n<h2>Cambiar la ROM cargada sin alterar el archivo</h2><p>GBA ofrece el espacio rom desde el offset cero hasta la longitud real del cartucho. Admite lecturas, escrituras comprobadas, valores numéricos, búsquedas, observaciones y reglas. Un lote mixto RAM/ROM se aplica entero o no cambia nada.</p><p>Los cambios afectan al código o recursos de esa sesión. Al volver a abrir se lee el original. El bus ROM del espacio cpu sigue siendo de solo lectura.</p><pre><code>const original = await host.gameMod.read(session, {ranges:[{space:\"rom\",address:offset,length:1}]});\nawait host.gameMod.write(session, {writes:[{space:\"rom\",address:offset,expected:original.ranges[0].data,data:nuevoByteBase64}]});</code></pre><p>Solicita emulation.game-mod y emulation.memory.read/write. Ten en cuenta:</p><ul><li>No se amplía la ROM ni se vuelve a detectar el tipo de guardado al cambiar cabeceras.</li><li>Los estados no incluyen la ROM modificada. Declara la dependencia del módulo y prepara sus cambios al cargar.</li><li>Studio utiliza una ROM sintética.</li><li>Los recursos y scripts con nombre de RPG Maker utilizan gameRuntime y sus límites; el resto de cores no recibe un motor de scripts general.</li></ul><h2>Declarar que una partida necesita el módulo</h2><pre><code>await host.gameMod.configureSave({gameId,newGame:true});</code></pre><p>Llama a configureSave antes de abrir el juego y solicita emulation.game-mod. La llamada registra el módulo y su autor como requisitos del guardado. newGame es opcional y exige empezar una partida nueva.</p><p>Los datos se guardan en slots, autoguardado y batería habituales. Si falta el módulo o cambia su autor, la app rechaza la carga y conserva el archivo. Una partida anterior sin marca de nueva no cumple newGame, aunque tenga otra dependencia.</p><ul><li>Los guardados sin requisitos conservan sus bytes. Las cabeceras de requisitos están versionadas y admiten la versión anterior.</li><li>Una sesión con requisitos empieza pausada para preparar el módulo antes de continuar.</li><li>Retirar un módulo que solo exige presencia pausa la partida y permite guardar antes de salir; no permite continuar sin él.</li><li>Si el módulo escribió memoria, ROM o reglas, se mantiene el cierre de sesión de gameMod.</li></ul><p>Para exigir una partida nueva consulta <code>capabilities(session).newGame</code>. Los cores incluidos que ofrecen ese contrato lo aplican; un motor sin soporte rechaza la apertura en vez de reutilizar progreso. Studio simula los metadatos, no una batería real.</p><h2>Crear un pack de archivos</h2><p>Un pack sustituye archivos al abrir el juego y conserva el contenido original. Para prepararlo:</p><ol><li>Solicita <code>resources.packs</code> y consulta <code>host.resourcePacks.capabilities({gameId})</code>.</li><li>Crea un pack con <code>create({gameId,id,label})</code>. Usa <code>list({gameId?})</code> para consultar packId, revisión, estado y archivos.</li><li>Abre la carga de cada archivo con <code>beginFile({packId,path,size,sha256})</code>, envía bloques con <code>write({id,offset,dataBase64})</code> y termina con <code>commitFile({id})</code>.</li></ol><p><code>readFile({packId,path,offset,length})</code> lee por bloques. Para quitar un archivo de un pack desactivado, usa <code>removeFile({packId,path})</code>.</p><p><code>enable({packId,enabled,expectedRevision})</code> y <code>remove({packId,expectedRevision})</code> requieren la sesión cerrada y la revisión vigente. Para eliminar el pack, desactívalo primero.</p><ul><li>Por identidad: hasta 32 packs.</li><li>Por pack: hasta 1024 archivos y 1 GiB en la app; Studio limita cada pack a 32 MiB.</li><li>Bloques de transferencia: 64 KiB.</li><li>Caducidad de las cargas: diez minutos.</li><li>Índice agregado por identidad: 95 KiB, para caber en el límite IPC.</li></ul><p>El host comprueba rutas, tamaños, hashes y contenido base antes de aplicar las capas.</p><p>Al abrir el juego, el host crea una copia privada con las capas habilitadas y conserva el ID y el original. Las ordena por identidad del propietario y después por ID del pack.</p><p>Al revocar un módulo, sus capas dejan de aplicarse en la siguiente apertura. La sesión abierta conserva la copia con la que arrancó.</p><p>Los packs admiten sustituir una ROM completa o archivos de contenedores de discos de solo lectura. Debes preparar los recursos o la ROM reconstruida y comprobar las capacidades del formato.</p><p>RPG Maker no está admitido: sus guardados nativos no deben copiarse junto con los recursos.</p><h2>Elegir entre packs y texturas</h2><p>Elige el servicio según lo que quieras cambiar:</p><ul><li><code>host.resourcePacks</code>: capas de archivos preparadas al abrir el juego. Cambia su selección con la sesión cerrada.</li><li><code>host.graphics</code>: opciones y texturas del core. <code>graphics.textures</code> reemplaza PNG por hash y <code>graphics.options</code> selecciona la resolución.</li></ul><p>GameCube admite raster host 1×/2×/4×. PS1, PSP y GameCube admiten reemplazos de texturas en Android/Desktop. Son cambios limitados a la sesión y se retiran al revocar el módulo; un pack de archivos no habilita texturas HD en cualquier core.</p><p>Consulta <a href=\"/es/sdk-de-nelkano/emulacion\">Emulación</a> para formatos, límites y correspondencia XFB/VI.</p>"},{"id":"de520020-2026-4000-8000-000000000015","language":"es","title":"Amigos: cuenta, solicitudes y perfil","path":"/es/sdk-de-nelkano/amigos","content":"<p>Amigos gestiona el acceso a la cuenta, las solicitudes y el perfil. Tu módulo puede abrir sus formularios, añadir acciones junto a los amigos o recibir avisos de cambios.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-19-friends.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-19-friends.webp\" width=\"600\" height=\"373\" alt=\"Amigos y solicitudes con contactos ficticios.\" data-caption=\"Amigos y solicitudes con contactos ficticios.\"></a><h2>Probarlo en Studio</h2><ol><li>Abre modules/friends o crea un proyecto con Amigos (simulación).</li><li>En el visor, pulsa Amigos junto al buscador de Biblioteca o Guardados. También puedes abrirlo desde la sección CUENTA de Ajustes.</li><li>Accede con demo@nelkano.test y Nelkano123, o registra una cuenta ficticia.</li><li>Busca otro usuario escribiendo al menos dos caracteres y prueba las solicitudes.</li><li>Abre Editar perfil para cambiar el nombre, estado o color.</li></ol><p>Puedes enviar, cancelar, aceptar y rechazar solicitudes, y eliminar amistades con confirmación. El rol no es editable y la foto es simulada. Las cuentas y amistades de Studio se reinician al cerrar el servidor de desarrollo: no se consulta nelkano.com ni se envían correos. Utiliza credenciales de prueba.</p>\n<h2>Usar la cuenta en la app</h2><p>Abre Amigos desde Biblioteca o Guardados, o pulsa Iniciar sesión en Ajustes → General → CUENTA. Accede con tu cuenta de nelkano.com; si te registras, actívala desde el correo. Una vez dentro puedes gestionar amistades y editar el perfil con foto. El rol lo decide el servidor.</p>\n<h2>Crear tus propios formularios de acceso</h2><pre><code>const account = await host.account.snapshot();\nawait host.account.login({email, password});\nawait host.account.register({name, email, password,\n  password_confirm: confirmation, terms: true});\nawait host.account.logout();</code></pre><p>Declara account.session. La identidad pública incluye uid, signedIn, nombre, correo, permiso friends y rol. El módulo define sus formularios y validaciones; los tokens los conserva la app.</p>\n<h3>Consultar una ruta de cuenta autorizada</h3><pre><code>const account = await host.account.snapshot();\nconst response = await host.network.request({\n  method: \"GET\", path: \"/api/nelkano/friends\",\n  accountUid: account.uid\n});</code></pre><p>Declara <code>network.account</code>. La app comprueba la cuenta de la petición y devuelve <code>status</code> y <code>body</code>. Android realiza la petición autenticada; Studio devuelve datos ficticios. Para un servidor propio utiliza las conexiones externas y sus permisos.</p>\n<h3>Seleccionar una foto</h3><pre><code>const image = await host.resources.pickImage();\nif (!image.cancelled) {\n  // Conserva image.resourceId hasta guardar el perfil.\n}</code></pre><p>Requiere resources.image. Recibes una referencia del host y, si está disponible, una vista previa. La imagen se publica al guardar el perfil con avatarResource en POST /api/nelkano/profile; elegirla no la publica.</p><p>Se admiten PNG, JPEG y WebP de hasta 4 MiB. La referencia pertenece al módulo y a la cuenta que la eligieron y caduca a los diez minutos. Las filas pueden mostrar avatar.url; Android descarga las imágenes de nelkano.com/sites/ y Studio utiliza una foto simulada.</p>\n<h2>Conservar la sesión</h2><p>Android cifra la sesión con Android Keystore, valida los permisos firmados y renueva los tokens. Cerrar sesión elimina la copia local e intenta revocarla en el servidor. Los tokens no llegan al módulo.</p>\n<h2>Integrarte con Amigos</h2><p>Declara modules.hooks y añade a consumes el proveedor nelkano.friends, el nombre del gancho y la versión 1.0.0. Usa optional:false si tu módulo necesita Amigos activo.</p>\n<h3>Añadir una acción a un amigo</h3><pre><code>await host.hooks.contribute(\"nelkano.friends\", \"friend-actions\", \"invite\", {label: \"Invitar a mi mundo\", scope: \"friends\"});</code></pre><ul><li><strong>scope:</strong> friends, incoming, outgoing o search. Si lo omites, la acción aparece en todos.</li><li><strong>shortLabel:</strong> etiqueta compacta opcional de 1 a 32 caracteres. Si falta, se utiliza label.</li><li><strong>Callback:</strong> Amigos llama a onHook con hook friend-actions, operation execute y value {accountUid, friend, scope}. contributionId identifica tu botón.</li><li><strong>Respuesta:</strong> devuelve {message} para mostrar el resultado.</li></ul><p>Implementa la acción en tu módulo: el ejemplo solo añade el botón de invitación. Las acciones se retiran al desactivarlo y se rechazan si la cuenta o la lista han cambiado.</p>\n<h3>Consultar las amistades</h3><pre><code>const data = await host.hooks.invoke(\"nelkano.friends\", \"friends-data\", \"refresh\", {});</code></pre><p>refresh consulta el servidor y requiere sesión. snapshot devuelve lo ya cargado: una lista vacía no confirma que se haya consultado el servidor.</p><p>La respuesta incluye revision, account y las listas friends, incoming y outgoing. Cada usuario tiene uid, name, statusText, avatarColor y avatarUrl. No se comparten correo, contraseña ni tokens.</p>\n<h3>Recibir cambios</h3><pre><code>await host.hooks.contribute(\"nelkano.friends\", \"friends-events\", \"changes\", {});</code></pre><p>Amigos llama a onHook con operation changed y un value que incluye el snapshot y type: session-changed, friends-updated o profile-updated. Conserva esos datos; evita llamar de vuelta a Amigos dentro del mismo callback.</p><p>Son avisos de las operaciones del módulo, no presencia en tiempo real ni notificaciones push. Se admiten hasta 16 acciones y 8 receptores. El fallo de un receptor no interrumpe la operación principal.</p>\n<h3>Ejecutar el ejemplo de integración</h3><ol><li>Ejecuta .\\nelkmod.cmd example friends-hooks C:/mis-modulos/mi-integracion.</li><li>Carga Amigos y el ejemplo en Studio.</li><li>Pulsa Invitar a mi mundo (ejemplo) junto a un amigo y comprueba la respuesta.</li></ol><p>El ejemplo demuestra la llamada, sin crear un mundo ni enviar una invitación real. Se exporta por separado. Para conectar jugadores, consulta el capítulo Multijugador.</p>\n<h2>Abrir los formularios de Amigos</h2><p>Consume account-access 1.0.0 de nelkano.friends:</p><ul><li><strong>open:</strong> devuelve el modal de acceso o la lista de Amigos, según la sesión.</li><li><strong>dispatch:</strong> recibe {action, payload} y ejecuta las acciones del formulario. Conserva sus actionId al reenviarlas.</li><li><strong>snapshot:</strong> devuelve la identidad pública.</li></ul><p>El modal mantiene el contexto de la pantalla de origen. Utiliza kind:panel, layout:form, un ID estable por cuenta y dismissAction:close. Cerrar conserva sesión, búsqueda, pestaña y listas; cambiar de cuenta inicia un borrador nuevo.</p>\n<p>La búsqueda es global, por nombre o correo. Espera 350 ms tras escribir y necesita al menos dos caracteres. Las pestañas separan Amigos y Solicitudes; el contador muestra las recibidas pendientes y las enviadas tienen su propia sección. Eliminar amigo está en ⋮ y pide confirmación. La conexión entre jugadores se abre desde el modal de Multijugador.</p>"},{"id":"de520020-2026-4000-8000-000000000016","language":"es","title":"Mandos y segunda pantalla","path":"/es/sdk-de-nelkano/mandos-segunda-pantalla","content":"<p>Reutiliza los formularios de Nelkano básicos para configurar mandos o conectar una segunda pantalla. Si necesitas una interfaz propia, el SDK también ofrece sus operaciones.</p><h2>Configurar un mando</h2><ol><li>Empareja el mando en los ajustes de Bluetooth de Android.</li><li>En Nelkano abre Ajustes → General → Controles → Mando Bluetooth.</li><li>Elige un sistema en Emulador, toca la asignación que quieres cambiar y pulsa el botón físico o mueve el eje.</li></ol><p>Desde el mismo formulario puedes restaurar las asignaciones del sistema o guardar y cargar perfiles con nombre.</p><p>Importar archivo y Exportar archivo abren el selector de documentos de Android o Desktop. El respaldo JSON incluye sistema, asignaciones, perfiles y máscaras PS1, hasta 96 KiB. También puedes usar Copiar / pegar asignaciones. Un respaldo inválido se rechaza sin sustituir la configuración.</p><p>Solicita <code>input.gamepad</code>. El puerto ofrece estas operaciones:</p><ul><li><code>host.input.snapshot()</code>: consultar dispositivos.</li><li><code>configure(bindings, ps1Bindings?)</code>: aplicar asignaciones.</li><li><code>capture(device?)</code>: esperar un botón durante un máximo de 30 segundos.</li><li><code>settings()</code>: abrir los ajustes Bluetooth del host.</li><li><code>release()</code>: liberar el control de las asignaciones.</li><li><code>host.input.importProfile()</code>: abrir un respaldo; devuelve state o cancelled.</li><li><code>exportProfile(state)</code>: exportarlo; devuelve saved o cancelled.</li></ul><p>El usuario elige el documento. El módulo recibe los datos, sin rutas de archivos, y debe validar state antes de aplicarlo. Las asignaciones usan valores como <code>KEY:96</code> o <code>AXIS:0:POS</code>.</p><p>Solo un módulo puede controlar las asignaciones a la vez. Al desactivarlo se liberan las teclas y se cancela cualquier captura pendiente.</p><p>Para usar <code>configure</code> y <code>capture</code> directamente, configura primero el mapa y captura después desde el mismo módulo. En la app, Nelkano básicos suele tener ese control: otro módulo recibe <code>input-owner-busy</code> si intenta sustituirlo. En ese caso utiliza su gancho <code>gamepad</code>, descrito más abajo.</p><p><code>settings()</code> solicita los ajustes Bluetooth al sistema. En Linux puede ser necesario abrirlos manualmente.</p><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-13-controller.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-13-controller.webp\" width=\"600\" height=\"672\" alt=\"Asignación de botones del mando (ejemplo).\" data-caption=\"Asignación de botones del mando (ejemplo).\"></a><h2>Conectar una segunda pantalla</h2><p>Durante una partida de Nintendo DS o Nintendo 3DS, abre <strong>Pantallas</strong> → <strong>Segunda pantalla</strong>. También puedes entrar por Ajustes → General → Apariencia → Segunda pantalla.</p><p>Ambas/Superior/Inferior cambia la imagen del dispositivo principal. Al conectar se activa la pantalla completa una vez; puedes salir de ella con <strong>Pantalla completa</strong> sin desconectar el receptor. <strong>Intercambiar pantallas</strong> mantiene esa elección.</p><ol><li>Conecta ambos dispositivos a la misma red local.</li><li>En el receptor, pulsa Usar como segunda pantalla y elige Sí.</li><li>En el emisor, pulsa Buscar pantallas y Conectar junto al receptor.</li><li>Acepta el emparejamiento en el receptor si se solicita.</li></ol><p>El formulario separa RECEPTOR, EMISOR y DISPOSITIVOS RECORDADOS. Cerrarlo conserva la conexión.</p><p>Cada partida empieza enviando la pantalla superior al receptor y mostrando la inferior en el dispositivo principal. <strong>Intercambiar pantallas</strong> invierte ese reparto durante la partida; al abrir otra se restablece. La entrada táctil del receptor corresponde a la pantalla inferior. El juego debe producir dos pantallas.</p><p>Solicita <code>media.second-screen</code>. El puerto incluye <code>host.secondScreen.snapshot()</code>, <code>discover()</code>, <code>connect(id)</code>, <code>disconnect()</code>, <code>receiver(enabled)</code>, <code>pair(enabled)</code>, <code>swap()</code> y <code>forget(id)</code>.</p><p>Para conectar utiliza el ID de un receptor descubierto; no se aceptan direcciones arbitrarias.</p><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-14-second-screen.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-14-second-screen.webp\" width=\"600\" height=\"565\" alt=\"Opciones de conexión de Segunda pantalla (ejemplo).\" data-caption=\"Opciones de conexión de Segunda pantalla (ejemplo).\"></a><h2>Reutilizar los formularios de Básicos</h2><p>En consumes declara el proveedor <code>nelkano.basics</code>, el gancho <code>gamepad</code> o <code>second-screen</code> y la versión <code>1.0.0</code>.</p><ul><li>gamepad: snapshot devuelve devices, bindings y settings. set recibe el respaldo completo: schemaVersion: 1, selectedSystem, bindings, profiles y ps1Bindings.</li><li>ps1Bindings contiene las máscaras PS1, incluidas combinaciones y L3/R3. En la asignación predeterminada, A físico corresponde a Cruz y B a Círculo.</li><li>second-screen: snapshot, discover, connect, disconnect, receiver, pair, swap y forget. Pasa id a connect/forget y enabled a receiver/pair.</li></ul><p>Invoca las operaciones con <code>host.hooks.invoke(provider, hook, operation, value)</code>.</p><h2>Qué reproduce Studio</h2><p>Studio simula el mando y el receptor. Importar un perfil devuelve <code>cancelled</code>; exportarlo simula el guardado sin escribir un archivo. Usa Copiar / pegar asignaciones para revisar el JSON y prueba Bluetooth y segunda pantalla en los dispositivos de destino.</p><h2>Streaming</h2><p>Para emitir una partida desde Básicos, consulta <a href=\"/es/sdk-de-nelkano/streaming\">Streaming</a>.</p><h2>Mandos PSP, GameCube y Switch</h2><p>Controles permite asignar botones de PSP y GameCube. El SDK conserva el perfil Switch para simulación y desarrollo; Switch queda excluido de las apps Android y Desktop. PSP separa cruceta y stick; GameCube, cruceta, stick principal y C-stick. Los ejes se asignan por direcciones con umbrales digitales: no hay inclinación proporcional ni presión progresiva de gatillos. Android lee eventos del sistema y Windows/Linux usa SDL.</p><h2>Navegar por la app con el mando</h2><p>La app permite navegar con mando en Android, Windows y Linux. Usa un mapa predeterminado o las reasignaciones reconocidas. Nelkano básicos gestiona las asignaciones de juego y los perfiles guardados.</p><ul><li>Cruceta o stick: moverse entre botones, juegos y opciones. El elemento seleccionado queda resaltado.</li><li>A o Start: aceptar o abrir.</li><li>B o Select: cerrar la ventana o volver.</li><li>L/R: recorrer Home, Biblioteca, Guardados y Ajustes cuando no hay una ventana superpuesta.</li><li>X: seleccionar o quitar de la selección el juego resaltado en Biblioteca.</li></ul><p>En un campo <code>select</code>, aceptar abre la lista y vuelve a aceptar para elegir. Los selectores con pestañas tienen un botón por opción. Izquierda/derecha cambia los deslizadores de cinco en cinco; aceptar sobre un campo de texto permite editarlo. Los componentes del SDK reciben esta navegación automáticamente.</p><p>Solo el diálogo abierto recibe las acciones del mando. La app libera las pulsaciones del juego al abrir menús, pausar o perder el foco, y no transmite un botón mantenido al cerrar la ventana. Durante una captura, la pulsación sirve para asignar el botón.</p><h2>Mando durante una partida</h2><p>En pantalla completa, el mando controla la partida. Fuera de ella, navega por la interfaz. CHIP-8 también recibe el mando en su vista normal.</p><ul><li>SNES incluye X/Y.</li><li>PS1 separa L/R de L2/R2 y admite L3/R3.</li><li>El perfil simulado de Switch separa L/R de ZL/ZR.</li><li>En Android se capturan LTRIGGER, RTRIGGER, BRAKE y GAS, además de los sticks. Reasignar un gatillo sustituye su función predeterminada.</li></ul><p>Las plantillas del SDK utilizan el mismo catálogo y resolvedor que la app. Las asignaciones y los archivos de configuración existentes se conservan.</p><p>3DS convierte el stick a las direcciones admitidas por su adaptador. El perfil Switch se conserva para simulación y desarrollo del core PC, sin disponibilidad en las apps. Prueba el mando concreto y Steam Input en el dispositivo de destino; el visor de Studio solo utiliza entradas ficticias.</p><h2>Multijugador y mando PS1</h2><p>Multijugador viene incluido y ofrece conexiones de GB/GBC, GBA y NDS. La consola compartida PS1 es un prototipo técnico y no aparece en esos menús.</p><p>Si vas a usar host.sharedConsole, comprueba capabilities(session).supported. El adaptador real disponible es PS1 Android; Studio simula el contrato. El capítulo Multijugador detalla sus permisos, fragmentos y ganchos.</p><h2>Cerrar o cancelar un formulario</h2><p>Mando Bluetooth y Segunda pantalla se abren sobre Ajustes. Cancelar al guardar/cargar un perfil o al cambiar el modo de segunda pantalla vuelve a su formulario principal. La × cierra el formulario sin borrar asignaciones ni interrumpir una conexión activa.</p>"},{"id":"de520020-2026-4000-8000-000000000017","language":"es","title":"Idioma y temas","path":"/es/sdk-de-nelkano/idioma-temas","content":"<p>Tu módulo puede aportar un tema o un diccionario para la interfaz. Los usuarios los eligen en Ajustes → <strong>Idioma y tema</strong>, junto a los idiomas español/inglés y los temas Oscuro, Claro, Azul marino, Pizarra y Grafito.</p><p>El formulario muestra la selección guardada. Cambiar solo el idioma o el tema conserva la otra opción, tanto en Android como en Studio.</p><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-07-language-dark.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-07-language-dark.webp\" width=\"600\" height=\"370\" alt=\"Apariencia con el tema Oscuro (ejemplo).\" data-caption=\"Apariencia con el tema Oscuro (ejemplo).\"></a><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-08-language-light.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-08-language-light.webp\" width=\"600\" height=\"370\" alt=\"Tema Claro e interfaz en inglés (ejemplo).\" data-caption=\"Tema Claro e interfaz en inglés (ejemplo).\"></a><h2>Añadir un tema</h2><ol><li>Declara las paletas en <code>manifest.skins</code> y solicita <code>presentation.appearance</code>.</li><li>Consulta los temas instalados con <code>host.appearance.snapshot()</code> y aplica uno con <code>host.appearance.apply()</code>. Ambas llamadas necesitan también <code>appearance.locale</code>.</li></ol><p>Utiliza un ID devuelto por <code>snapshot()</code>: contiene autor, módulo y nombre del tema. No lo construyas a mano.</p><h2>Añadir un idioma</h2><p>Para ofrecer un idioma, solicita <code>appearance.locale</code> y llama a <code>host.appearance.apply({language, skinId, strings})</code>. En strings, cada clave es un texto original en español y cada valor, su traducción.</p><ul><li>Máximo: 2048 textos, claves de 512 caracteres y traducciones de 1024.</li><li>La petición completa debe caber en 128 KiB.</li><li>Un texto sin traducción conserva el original. Los nombres de juegos y los textos del usuario no se traducen automáticamente.</li></ul><h2>Ganchos de Apariencia</h2><p>El proveedor <code>nelkano.appearance</code> publica tres ganchos, versión 1.0.0:</p><ul><li>settings: snapshot consulta la selección y set aplica language/skinId.</li><li>theme-options: añade una opción con label y skinId de una paleta instalada.</li><li>locale-options: añade label, language y strings.</li></ul><p>Los datos deben caber en los límites generales de los ganchos. Para un diccionario grande, aplica el idioma desde tu módulo mediante appearance.</p><p>Apariencia cambia los colores y textos de los componentes compartidos. Para diseñar la interfaz de tu módulo utiliza los componentes del SDK; un tema no añade vistas Android ni instala un motor nativo V2.</p><p>Studio aplica temas e idiomas al visor. En la app, Apariencia es esencial y recupera la elección al iniciar. Un tema de otro módulo está disponible mientras su proveedor siga instalado y activo.</p>"},{"id":"de520020-2026-4000-8000-000000000018","language":"es","title":"Parches de juegos","path":"/es/sdk-de-nelkano/parches","content":"<p>El módulo Parches está temporalmente excluido de los paquetes cargados en Android y escritorio, pendiente de revisión. La app no muestra Aplicar parche ni registra el proveedor nelkano.patches; tampoco carga paquetes residuales ni admite importar manualmente ese módulo mientras siga excluido. Sus fuentes, el motor y host.patches se conservan: no se elimina la API ni se borran juegos, copias o guardados existentes. Studio conserva un ejemplo simulado; no acredita disponibilidad del módulo en la app.</p><h2>Aplicar un parche desde tu módulo</h2><p>El servicio del SDK crea una copia modificada de un juego GBA o NDS y conserva la ROM original.</p><ol><li>Solicita <code>resources.patch</code>.</li><li>Abre el selector con <code>host.patches.pick()</code>. Devuelve resourceId y name, o cancelled.</li><li>Aplica la selección con <code>host.patches.apply({resourceId, gameId, title})</code>. title es opcional.</li></ol><p>resourceId pertenece al módulo que abrió el selector, caduca a los diez minutos y se consume al aplicar el parche correctamente. No es una ruta ni puede utilizarlo otro módulo.</p><p>El resultado contiene gameId y title del juego creado. La copia conserva el sistema y la extensión del original; sin otro título, utiliza el original seguido de «(parche)». Repetir el mismo resultado conserva una sola identidad en la biblioteca. Si falla, no se publica un juego. Se comprueba la cabecera del resultado, pero un IPS no garantiza que hayas elegido la ROM correcta: utiliza la versión exacta que pide su autor.</p><h2>Formulario conservado como referencia</h2><p>El código del ejemplo Parches conserva el gancho <code>apply-patch</code> de <code>nelkano.patches</code>, versión <code>1.0.0</code>: formats consulta los formatos y sistemas; apply recibe gameId y title opcional, abre su propio selector y devuelve el juego creado o cancelled. No acepta referencias de otro módulo. Este proveedor no se carga en la app actual: no lo declares como dependencia disponible.</p><h2>Límites del servicio</h2><ul><li>Sistemas: GBA y NDS.</li><li>Formatos: IPS y xdelta/VCDIFF; BPS, UPS, PPF e IPS32 no están admitidos.</li><li>Tamaño máximo del parche: 64 MiB.</li></ul><p>Las operaciones de Android y Desktop aplican el parche al archivo. Studio simula el formulario y la selección. Si tu módulo utiliza este servicio, comprueba el resultado con una ROM y un parche reales en cada plataforma; una copia creada correctamente no acredita jugabilidad.</p>"},{"id":"de520020-2026-4000-8000-000000000019","language":"es","title":"Streaming de la partida","path":"/es/sdk-de-nelkano/streaming","content":"<p>Streaming permite ver la partida en la web. La cuenta necesita el permiso de streaming del servidor.</p><ol><li>Inicia sesión en Nelkano con Amigos y abre un juego.</li><li>En los ajustes de la partida, abre Streaming, incluido en Nelkano básicos.</li><li>Revisa las opciones y pulsa Iniciar emisión.</li></ol><p>Android pide confirmar el envío de vídeo y audio del juego por WebRTC y su respaldo HTTP a https://nelkano.com. El micrófono no se utiliza. Activar o instalar un módulo no inicia una emisión.</p><a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-17-streaming-confirmation.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-17-streaming-confirmation.webp\" width=\"460\" height=\"258\" alt=\"Confirmación antes de emitir una partida (ejemplo).\" data-caption=\"Confirmación antes de emitir una partida (ejemplo).\"></a><h2>Ver y detener la emisión</h2><ol><li>Abre Streaming en nelkano.com.</li><li>Inicia sesión con la misma cuenta que en la app. El visor busca su emisión activa.</li></ol><p>El panel también muestra el identificador, PIN y dirección devueltos por el servicio. El visor web actual sigue exigiendo iniciar sesión con tu cuenta.</p><p><strong>Actualizar</strong> consulta el estado y <strong>Detener emisión</strong> termina la conexión. La emisión también termina al cerrar/cambiar el juego, cambiar de cuenta o retirar el módulo emisor. Android la detiene al pasar a segundo plano; Desktop tiene su propia gestión del foco.</p><p>Básicos propone 854 × 480, 30 fps y 1800 kbps; puedes cambiarlos antes de empezar. En Android, los contadores indican fotogramas y bloques de audio enviados, no si el receptor los ha reproducido.</p><h2>Emitir desde tu módulo</h2><p>Solicita media.streaming. Solo puede emitir un módulo a la vez:</p><pre><code>const state = await host.streaming.snapshot();\ntry {\n  const result = await host.streaming.start({\n    width: 854, height: 480, fps: 30, videoBitrateKbps: 1800\n  });\n  if (result.cancelled) console.log(\"Emisión cancelada\");\n} catch (error) {\n  console.log(\"No se inició la emisión:\", error instanceof Error ? error.message : String(error));\n}\n// Más tarde, para terminar una emisión iniciada por este módulo:\nawait host.streaming.stop();</code></pre><p>Si el usuario cancela, Android devuelve <code>{cancelled:true}</code> y Desktop rechaza la promesa. Contempla ambos casos, además de los errores de inicio, al mostrar el resultado.</p><p>El formulario de Básicos admite ancho 160–1920, alto 120–1080, 10–60 fps y 200–12000 kbps. El puerto directo valida los límites del host, que pueden ser distintos.</p><p><code>snapshot()</code> devuelve estado, mensaje, destino y configuración. Una emisión creada incluye <code>sessionId</code>, <code>pin</code> y <code>joinUri</code>; Android puede añadir <code>stats</code> y <code>relay</code>. Las credenciales permanecen en el host.</p><h2>Usar el gancho de Básicos</h2><p>Declara en consumes el proveedor nelkano.basics, gancho streaming, versión 1.0.0, y solicita modules.hooks. Ofrece snapshot, configure, start y stop:</p><pre><code>await host.hooks.invoke('nelkano.basics', 'streaming', 'configure', {\n  width: 854, height: 480, fps: 30, videoBitrateKbps: 1800\n});\nawait host.hooks.invoke('nelkano.basics', 'streaming', 'start', {});</code></pre><p>configure guarda las opciones en Básicos. start solicita igualmente la confirmación del usuario.</p><h2>Probar una emisión</h2><p>Studio simula la cuenta, sesión y receptor para probar el formulario y los ganchos. Comprueba imagen, sonido, cancelación, cortes de red y cambios de sesión con una emisión real en la plataforma de destino. Android y Desktop solicitan confirmación al empezar; aceptar los permisos de instalación no la sustituye.</p><ul><li>Android: respaldo HTTP con imágenes WebP/JPEG de calidad adaptativa y audio PCM mono de 16 kHz en bloques de 500 ms. Está disponible durante la emisión si falla WebRTC, con colas acotadas por medio.</li><li>Desktop: transporte WebRTC, sin ese respaldo HTTP ni los contadores <code>relay</code>.</li></ul><h2>Si la app se cierra inesperadamente</h2><p>Android conserva el identificador público de la emisión y la cuenta que la inició, sin PIN ni tokens. Antes de emitir de nuevo con esa cuenta intenta cerrar las emisiones anteriores; si no llega al servidor, guarda el registro para reintentar.</p><p>La app no reanuda la emisión automáticamente ni cierra las de otras cuentas. Detener libera la captura y WebRTC; el cierre en el servidor se completa en segundo plano.</p>"},{"id":"de520020-2026-4000-8000-000000000020","language":"es","title":"Multijugador: GB/GBC, GBA, NDS y módulos custom","path":"/es/sdk-de-nelkano/online-gba","content":"<p>Multijugador y Amigos vienen incluidos con Nelkano. Para conectar jugadores, abre un juego GB/GBC, GBA o NDS y entra en Multijugador desde los ajustes de la partida o su botón de sistema. Cada motor y adaptador determina los modos disponibles.</p><p>El Editor de controles permite mover, escalar u ocultar ese botón. Para añadir un modo de conexión, utiliza los ganchos de Multijugador y comprueba primero las capacidades de la sesión.</p>\n<h2>Elegir jugadores y conectar</h2><ol><li>Iniciad sesión con cuentas que sean amigas aceptadas y abrid juegos compatibles.</li><li>En los ajustes de la partida, entra en CONEXIONES → Multijugador.</li><li>El anfitrión elige Crear partida y selecciona a sus amigos. Los invitados eligen Unirse y seleccionan al anfitrión.</li><li>Pulsad la acción del modo que vais a utilizar. Elegir los amigos y el rol todavía no establece la conexión.</li></ol><p>Las acciones de conexión están al pie. Desconectar aparece si hay una conexión o una recuperación pendiente; la cruz superior cierra el panel.</p>\n<a href=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-18-multiplayer.webp\"><img src=\"/modules/custom/nelkano_home/assets/docs/sdk/20261018/app-18-multiplayer.webp\" width=\"600\" height=\"520\" alt=\"Crear o unirse a una partida y conectar con cable link (ejemplo).\" data-caption=\"Crear o unirse a una partida y conectar con cable link (ejemplo).\"></a><h2>Cable link de GBA</h2><p>Conectar con cable link requiere dos jugadores con la misma ROM exacta. Conectad primero y entrad después al multijugador del propio juego.</p><p>La sesión es temporal: al terminar se recupera la partida anterior y se descarta el progreso de la conexión.</p><p>El host negocia WebRTC entre cuentas amigas. La conexión entre redes depende de ICE, NAT y las redes de ambos jugadores. Studio simula el flujo.</p>\n<h3>Puerto localLink del SDK</h3><p>Comprueba que exista host.localLink y que capabilities(session) devuelva supported:true.</p><ul><li>capabilities y status requieren emulation.read.</li><li>connect(session,{friendUid,role,players:2}) requiere emulation.handoff y network.multiplayer.</li><li>stop(session) requiere emulation.handoff.</li><li>role es host o guest; friendUid identifica a un amigo aceptado.</li></ul><p>capabilities incluye systemId, players, protocol, stateFormat, maxStateBytes y transport:webrtc-friends. host y join por código ya no forman parte del puerto público. JavaScript no recibe rutas privadas, estados completos, credenciales ni handles nativos.</p><p>connect devuelve phase:connecting mientras prepara el cable. Consulta status mediante ticks de runtime, incluso con el panel cerrado:</p><ul><li>phase: idle, connecting, playing o error.</li><li>Datos: active, role, friendUid, players, delayFrames y message.</li><li>playing: la conexión nativa está establecida y el estado inicial coincide.</li><li>restored:true: se ha recuperado la partida original al cerrar.</li><li>Un error con restored:false exige reintentar la recuperación o reabrir la partida.</li></ul><p>Cada llamada valida propietario, permisos y referencia id/generation. La negociación comprueba cuenta, amistad, roles, ROM, sesión y nonce; cada copia inicial admite hasta 4 MiB. Studio simula el contrato sin conexiones físicas.</p><h2>Crear online para otro juego</h2><p>Crea un módulo que consuma game-adapters 1.0.0 de nelkano.multiplayer y registre el adaptador:</p><pre><code>await host.hooks.contribute(\"nelkano.multiplayer\",\n  \"game-adapters\", \"mi-juego\", {label: \"Mi juego\"});</code></pre><p>Implementa estas operaciones en onHook:</p><ul><li>identify: recibe sesión, gameId y systemId. Solo si reconoces el juego, devuelve {compatible:true, compatibilityKey:\"mi-protocolo-v1\", transportSystem:\"gba\"}. Los jugadores compatibles deben compartir la clave.</li><li>capture: devuelve los datos públicos del juego.</li><li>render: recibe peers para representarlos.</li><li>messages: recibe los paquetes de tu protocolo.</li></ul><p>Consume sessions para consultar snapshot, conectar con {friendUid, role}, desconectar o enviar con send y {type, payload, channel?}. Multijugador añade identidad de sesión y secuencia.</p><p>channel sustituye mensajes pendientes del mismo canal, útil para posiciones. Omítelo cuando debas conservar cada mensaje.</p><p>Contribuye a events para recibir changed con type, state, sessionId, peers y packets. No llames de vuelta al proveedor dentro del callback: crearías un ciclo. Amigos proporciona la cuenta y su lista; el host conserva la conexión autenticada y los tokens.</p><h2>Añadir una acción al formulario</h2><p>Contribuye a <code>connection-actions</code> <code>1.0.0</code> con <code>strategy:presence</code>, <code>adapterId</code>, <code>systems</code>, <code>minPlayers</code> y <code>maxPlayers</code>. La acción y el adaptador deben pertenecer al mismo módulo y autor.</p><p>Las salas de presencia admiten de dos a cuatro participantes, según el adaptador. El anfitrión selecciona a sus amigos y cada invitado elige al anfitrión. Cable link sigue limitado a dos jugadores.</p><p><code>sessions.connect</code> acepta <code>friendUids</code>. Si usas <code>host.online.connect</code> directamente como anfitrión en <code>ONLINE_LAYER</code>, puedes indicar hasta tres destinatarios. <code>send</code> acepta <code>peerUid</code> para elegir uno.</p><p>Cada enlace tiene su <code>sessionId</code>. El protocolo de presencia utiliza además el identificador y los miembros de la sala.</p><h2>Mantener la partida activa en presencia</h2><p>En presencia, la partida sigue avanzando desde <code>online.connect</code> hasta desconectar, fallar o retirar el módulo. El host aplica esta regla sin otro permiso. Durante Online se evita el apagado por inactividad; en Android, girar el dispositivo conserva pantalla y sesión.</p><p>El panel muestra los errores de red. Los registros de transporte guardan contadores y motivos de cierre, sin contraseñas, tokens ni cuerpos de paquetes. Puedes añadir un motivo corto con <code>online.disconnect({reason: \"local-disconnect\"})</code>.</p><h2>Estado y reconexión</h2><p>El estado se muestra al principio del panel para anfitrión e invitado:</p><ul><li>Conectado: han llegado datos del amigo en los últimos ocho segundos.</li><li>Reconectando: no llegan datos, pero la sesión permanece abierta.</li></ul><p>Una sesión HTTP abierta no confirma que el otro jugador esté activo.</p><p>Multijugador envía una señal de vida cada segundo y medio, también durante transiciones en las que el adaptador no puede leer la presencia. Cada remitente tiene su propia secuencia para que un jugador nuevo no herede la del anterior.</p><p>Reconectar capa online cierra la conexión y abre otra; el amigo debe entrar de nuevo. sessions.connect reutiliza una conexión con la misma configuración, por lo que la reconexión explícita se hace desde ese botón.</p><p>sessions.snapshot incluye state, transportState, peerActive y peerLastSeenAt. Los avisos events entregan el mismo estado comprobado.</p><h2>Partir del ejemplo de adaptador</h2><pre><code>.\\nelkmod.cmd example multiplayer-adapter C:/mis-modulos/mi-adaptador</code></pre><p>El ejemplo usa un CHIP-8 ficticio para enseñar game-adapters, sessions y events. No sincroniza la CPU ni convierte un juego en online por sí solo.</p><ul><li>identify puede indicar connectionKind:presence y transportSystem.</li><li>shared-console necesita un core/host compatible.</li><li>sessions.send mantiene los mensajes públicos separados de los paquetes internos de tarjetas y mandos.</li><li>events incluye mode y systemId; snapshot identifica nelkano.multiplayer como proveedor.</li></ul><p>Cada adaptador define su protocolo mediante los ganchos de <code>nelkano.multiplayer</code>. Un gancho no añade soporte de CPU, cable o consola compartida a un core que no lo anuncie.</p><h2>Crear una integración de consola compartida</h2><p><code>host.sharedConsole</code> es opcional y no añade un modo al menú. Con <code>emulation.read</code>, consulta <code>capabilities(session)</code>: devuelve <code>supported</code>, sistema, tamaño/formato de tarjetas, códigos de entrada y límites.</p><p>El adaptador disponible es PS1 Android; Studio lo simula. Comprueba <code>supported</code> antes de usarlo.</p><p>Para compartir tarjetas, solicita emulation.handoff:</p><ul><li>exportCard(session,{slot,index}): exporta desde index 0 en fragmentos de 16 KiB.</li><li>stageCard(session,{index,data}): prepara los fragmentos para tu módulo/autor/sesión; caducan a los diez segundos.</li><li>beginTemporary(session) y endTemporary(session): inician y terminan la sesión temporal.</li></ul><p>El adaptador valida el formato antes de montarlo. Tu módulo define el protocolo y los juegos compatibles.</p><p>Con emulation.input puedes llamar a input(session,{sequence,keys}), controller(session,{enabled,message}) y controllerEvents(session).</p><ul><li>Las secuencias deben aumentar y los códigos pertenecer al core; no se admiten duplicados.</li><li>El anfitrión libera botones tras 1200 ms sin una entrada válida.</li><li>La superficie del invitado pausa su propia emulación, conserva el orden de pulsaciones y libera botones al perder el foco.</li><li>Esa superficie se cierra si no se consulta durante cinco segundos.</li></ul><p>Cada llamada comprueba permisos, propietario y generación.</p><p>Esta estrategia admite online.connect con continuous:false, una política local que no se envía al servidor. GBA mantiene continuous:true por defecto. La tarjeta temporal conserva intactos los guardados permanentes de ambos jugadores.</p>"}]}