Capítulo salas de la guía Neblla — salas, lobby, gathering y emparejamiento. El contrato base está en core.txt.
Salas y emparejamiento
El lobby
Todo usuario conectado aterriza en la sala lobby. Quién hay y quién soy:
neblla.room.users— array de usuarios de la sala actual.neblla.me— el usuario de esta sesión.
memberJoinedRoom se dispara una vez por cada usuario que ya estaba al llegar y otra vez por cada entrada nueva; el flag alreadyPresent los distingue:
neblla.on('memberJoinedRoom', event => {
if (event.alreadyPresent) {
// ya estaba — añadirlo a la lista en silencio
} else {
// acaba de entrar — sonido, animación
}
});
neblla.on('memberLeftRoom', event => {
// event.user se ha ido — quitarlo de la lista
});
El lobby es buen momento para publicar el perfil visible del usuario:
neblla.setPublicInfo({ name: 'John', color: '#ff8800' });
Queda en user.publicInfo para todos los de la misma sala y se restaura en reconexiones. Cuando alguien lo actualiza, todos reciben el evento publicInfo:
neblla.on('publicInfo', event => {
// event.user.publicInfo tiene los nuevos datos
});
Render de lista de jugadores:
const renderPlayers = () => {
neblla.room.users.forEach(user => {
console.log(user.publicInfo?.name || 'Guest',
user.isPresenter ? '(host)' : '',
user.isMe ? '(you)' : '');
});
};
neblla.on('memberJoinedRoom', renderPlayers);
neblla.on('memberLeftRoom', renderPlayers);
Salas con nombre (estilo chat)
Mismo nombre de sala = mismo grupo. Un usuario solo puede estar en una sala a la vez.
neblla.joinRoom('eurovision');
neblla.sendMessage({ type: 'msg', text: '¡me encanta esta canción!' });
Los mensajes solo llegan a quien esté dentro. Cambiar de sala: joinRoom('otra'). Volver al lobby: leaveRoom().
Por defecto cualquier usuario crea salas con solo hacer joinRoom('nombre-nuevo'). Para limitar la app a una lista fija de salas válidas: developers.neblla.com → Apps → Mi app → Settings → Rooms.
Tipos de sala
La política se elige por app desde el dashboard:
ephemeral(default) — existe solo mientras hay alguien conectado; desaparece cuando se va el último usuario. Sin DB, sin historial: quien entra después no ve lo escrito antes.public— persistida en MongoDB. Cualquier usuario de la app puede entrar. Historial de chat guardado (cifrado en reposo). Sobrevive a reinicios. Foros, lounges, lobbies públicos.private— persistida con whitelist de personas. Solo miembros entran. Los *owners* (rol intra-sala) invitan, expulsan, promueven a otros owners y borran mensajes. Si el owner abandona, el rol pasa al miembro más antiguo. Si la whitelist queda vacía, la sala se borra. Guilds, salas privadas, DMs grupales.
Una misma app puede mezclar tipos: las salas nacen ephemeral y se promueven con persistRoom.
Partidas: bookRoom → gathering → startRoom
Una partida necesita reservar espacio, fijar reglas, esperar jugadores y empezar:
const roomId = await neblla.bookRoom({ config: { tiempoMax: 180, modo: 'blitz' } });
configes un JSON abierto del desarrollador. Neblla no lo interpreta. Se consulta conneblla.room.config(también dentro deserver/gathering.jsyserver/room.js).- La sala nace en estado
gatheringy es visible desde el lobby víaneblla.gathering: array de salas en formación, cada una con{ name, config, users }, refrescado por el eventogathering.
// Desde el lobby, listar las partidas abiertas:
neblla.gathering.forEach(room => {
console.log(room.name, room.users.length, room.config);
});
- Los jugadores entran con
joinRoom(roomId). Cuando están todos, alguien llama aneblla.startRoom(): la sala pasa arunning, el server arranca y funcionatellServer/messageFromServer. - Desde
Settings → Roomsse limita el máximo de jugadores por sala y si se permite entrar como espectador a la partida ya empezada.
Flujo correcto (no uses createRoom, es @legacy y se salta el estado gathering):
// El nombre de la sala lo genera el servidor — viene en el ack del bookRoom.
const roomName = await neblla.bookRoom({ config: { maxPlayers: 4 } });
await neblla.joinRoom(roomName);
// usuarios entran, esperan, se llena
neblla.startRoom();
Ni bookRoom ni createRoom aceptan un nombre de cliente — el id de sala siempre lo elige el servidor para evitar colisiones. Sí puedes pedir su longitud: bookRoom({ length: 3 }) da un código de 3 caracteres (3–12, por defecto 5; alfabeto sin 0/O/1/I/L).
Gathering configurado vs custom
server/gathering.js decide *cuándo* empieza la partida. Para los casos comunes no hace falta código: desde Settings → Rooms se configuran reglas declarativas — empezar con un mínimo de jugadores, cuando pasen X segundos desde la última entrada, cuando llegue un mensaje específico (botón "Empezar" del anfitrión), o combinaciones. Escribe gathering.js solo si la lógica de arranque es más compleja.
server/lobby.js igual: solo si hay decisiones reales en el lobby (matchmaking inteligente, cola de espera, agrupación por nivel). Patrón de matchmaking:
// lobby.js
const queue = [];
neblla.on('messageToPresenter', event => {
if (event.data.type === 'ready') {
queue.push(event.user.id);
if (queue.length >= 4) {
neblla.bookRoom().then(roomId => {
const players = queue.splice(0, 4);
neblla.sendMessage({ type: 'match-found', roomId, players });
});
}
}
});
// client.js
neblla.on('message', event => {
if (event.data.type === 'match-found' &&
event.data.players.includes(neblla.me.id)) {
neblla.joinRoom(event.data.roomId);
}
});
API de salas persistentes
neblla.persistRoom({ name, type, whitelist? }) (CLIENT) — sube una sala recién reservada de ephemeral a public o private. Hay que llamarlo entre bookRoom y el primer joinRoom — una vez alguien entra, el tipo queda fijado. La whitelist (para private) bootstrappea miembros iniciales; el llamador se añade automáticamente como primer owner. Los ids de la whitelist son per-app userIds (los id de neblla.room.users[i]), no personIds — solo se resuelven contra usuarios online en esta app en ese momento. Los offline se añaden después vía inviteToRoom.
neblla.listRooms({ type? }) (CLIENT) — lista salas persistentes accesibles. Cada entrada: { name, type, creator, memberCount, activeUsers, lastActivityAt }.
neblla.getRoom(name?) (CLIENT) — inspecciona una sala por nombre (por defecto, la actual).
neblla.getMembers(name?) (CLIENT) — solo private. Devuelve { members[], invites[] }.
neblla.deleteRoom(name?) (CLIENT) — borra una sala persistente. Moderadores (cualquier sala) y owners de privadas (la suya). Echa a todos al lobby y emite roomDeleted.
neblla.setRoomType(name, type) (CLIENT) — promociona/baja entre public y private. Solo moderadores.
Membresía de salas privadas:
neblla.acceptInvite(roomName)/neblla.rejectInvite(roomName)— responde a una invitaciónmemberInvited.neblla.kickFromRoom(targetUserId)— expulsa de la sala privada actual. Solo owner o moderador.neblla.abandonRoom(roomName?)— renuncia a la membresía. Distinto deleaveRoom: leave = "me desconecto ahora", abandon = "quítame de la whitelist".neblla.promoteOwner(targetUserId)/neblla.demoteOwner(targetUserId)— gestión de owners. Solo owner. No puedes auto-demotearte si eres el último.neblla.inviteToRoom(target)— añade a la whitelist de tu sala privada.
room.data
neblla.setRoomData(data) (CLIENT, presenter o server peer) — sustituye neblla.room.data (persistente en salas public/private). Todos los miembros reciben 'roomDataUpdated'. Sujeto al cap maxDataSize; un exceso emite 'roomDataError'.
Historial de chat (salas persistentes)
neblla.getRoomHistory({ name?, before?, limit? }) (CLIENT) — paginación del chat persistido. Newest-first. Default limit 50, max 200. Los mensajes borrados vienen con body: null y deleted: true.
neblla.deleteMessage(messageId, name?) (CLIENT) — soft-delete. Autor, owner o moderador. Emite messageDeleted a todos.
Eventos de salas
Ciclo de vida general:
'joinRoom'(CLIENT) — entraste a una sala.{ room, isLobby }'leaveRoom'(CLIENT) —{ room, user }'memberJoinedRoom'(BOTH) —{ user, room, isLobby, alreadyPresent }.'memberLeftRoom'(BOTH) —{ user, room }'memberJoined'/'memberLeft'(BOTH) — alias compactos, payload{ room: <nombre>, user }. NO confundir conmemberJoinedRoom(objetoroomcompleto +isLobby+alreadyPresent).'gathering'(CLIENT, lobby) — refresh del listado de salas en formación.{ rooms: [{ name, config, users }] }. Reflejado enneblla.gathering.'roomStarted'(BOTH) — el presentador llamó astartRoom().{ room, isMe }'roomDataUpdated'(BOTH) —{ room, data }.'roomDataError'(CLIENT) — tusetRoomData(...)fue rechazado (size cap u otro).'roomJoinError'(CLIENT) — fallo entrando a una sala (whitelist, kick, etc).
Salas persistentes:
'memberInvited'(CLIENT) — owner añadió a invitados pendientes.'inviteAccepted'/'inviteRejected'(CLIENT)'memberKicked'(CLIENT) — quitado de la whitelist por owner/moderador.'kickedFromRoom'(CLIENT) — push directo al expulsado.'memberAbandoned'(CLIENT) — se auto-quitó.'ownerAdded'/'ownerRemoved'(CLIENT)'roomDeleted'(CLIENT) —{ room, reason, by? }.'roomTypeChanged'(CLIENT)
Chat persistido:
'messagePersisted'(CLIENT) — eco con el id necesario para borrar.'messageDeleted'(CLIENT) — render tombstone.'messageDropped'(CLIENT) — solo al autor, mensaje no persistido (too_large o rate_limited).