< Volver 

Rod and Reef: Datos del jugador

2026-09-25

Cómo Rod and Reef guarda los datos de cada jugador con sesiones de ProfileStore, cómo le llega al cliente una copia que se mantiene al día con una foto inicial más cambios por ruta, y cómo viajan los regalos entre servidores sin perderse.

LuauRobloxBackendDataStoreNetworking
Ir a vista rápida

Un perfil por jugador y una copia que lo sigue.

Advertencia

Rod and Reef todavía no ha sido lanzado, y no se pueden mostrar videos ni GIFs de este sistema hasta que salga.

Resumen. Los datos de un jugador de Rod and Reef (monedas, peces, cañas, quests, contadores de suerte, logros y temporada) viven en un perfil de ProfileStore, una librería que guarda perfiles en DataStores con un candado de sesión, de modo que solo un servidor a la vez puede escribir en cada uno. Encima hay una capa delgada, DataManager, en la que cada escritura modifica la tabla viva del perfil y le avisa al cliente en la misma llamada. El cliente mantiene una copia que se construye con una foto completa al entrar y una secuencia de cambios por ruta después, y que converge al estado del servidor mientras reciba los cambios en orden. Este artículo describe también tres decisiones alrededor de esa capa: las migraciones idempotentes que corren en cada entrada, las preferencias guardadas fuera del perfil para que el lugar del menú pueda escribirlas, y los mensajes entre servidores que permiten regalarle algo a un jugador que está en otro servidor o desconectado.

1. El problema

Los datos del jugador son lo único del juego que no se puede perder ni duplicar. Un pez que desaparece es un reporte, y unas monedas duplicadas son una economía rota. Roblox corre cada partida en un servidor distinto, un jugador puede saltar de uno a otro en segundos, y un servidor se puede caer en cualquier momento. Además, el cliente necesita sus propios datos para dibujar casi toda la interfaz, desde el inventario hasta el diario de quests, y los necesita apenas entra, en una carrera con todo lo demás que arranca a la vez.

2. Sesiones

2.1 El candado

Al entrar un jugador, el servidor abre una sesión de ProfileStore [3] sobre su perfil, guardado bajo la clave Player_<UserId>. Una sesión es un candado: mientras un servidor la tiene, ningún otro puede escribir en ese perfil. Si otro servidor la pide, espera, y si la que la tiene no la suelta en 40 s, se la roba. Un perfil que no se actualiza en más de 630 s se considera de un servidor caído. Es en esencia un lease, un permiso con vencimiento [1]: el candado no depende de que el servidor que lo tiene siga vivo para avisar que lo suelta. Los cambios se guardan cada 300 s y al cerrar la sesión.

La petición de sesión se cancela si el jugador se va mientras espera, para no quedarse con un candado de alguien que ya no está. Si la sesión termina por cualquier razón mientras el jugador sigue en el juego, por ejemplo porque otro servidor la robó, se lo expulsa, porque seguir jugando significaría escribir en un perfil que ya no es suyo y perder todo lo que haga desde ese momento. El perfil también se asocia al UserId del jugador, que es lo que permite borrar sus datos si lo pide.

2.2 Una plantilla y una generación

La forma del perfil la define una plantilla de unas 460 líneas. Al cargar, Reconcile agrega cualquier clave de la plantilla que falte en el perfil, así que un campo nuevo aparece en los perfiles viejos sin migrar nada. El nombre de la tienda es una generación, 20260814, y en Studio se usa otra, Debug, para que las pruebas nunca toquen datos reales. Cambiar la generación es la forma de reiniciar todos los datos, por ejemplo entre una beta y el lanzamiento.

3. Migraciones idempotentes

Reconcile llena claves que faltan, pero no puede mover un valor que está en la clave equivocada, y eso es justo lo que deja un cambio de forma. Para eso hay una función migrate que corre en cada entrada, después de Reconcile, para que todas las claves que lee existan.

La regla es que cada paso sea idempotente, m(m(x))=m(x). Con eso no hace falta guardar un número de versión ni recordar qué perfil ya se migró: aplicar la migración a un perfil migrado no hace nada, porque las condiciones que la disparan ya no se cumplen. Hoy tiene tres pasos.

El primero viene de los cebos. Antes había una sola ranura, y los tres cebos de pago ahora van en una segunda. Un perfil guardado antes del cambio puede tener uno de esos cebos en la ranura principal. Ahí sigue funcionando, pero el inventario lo dibuja como no equipado y ofrece equiparlo otra vez, así que el paso lo mueve a la segunda ranura, solo si está vacía, para no tirar nada.

El segundo viene de un tipo de objeto que desapareció. El equipo (la máscara de buceo y el traje para agua fría) se volvió objeto de quest, y la máscara se fusionó con las gafas. El paso recorre la lista vieja, la pasa a la nueva con el cambio de nombre y sin duplicar lo que ya estaba, y borra la clave vieja.

El tercero viene de los emotes, que perdieron los espacios en sus nombres (Rat Dance pasó a ser RatDance), y nadie había renombrado los que ya estaban guardados. Ni siquiera el emote inicial de la plantilla, así que todos los perfiles creados desde entonces tenían un emote con un nombre que ya no existía. El paso renombra cada nombre guardado cuya versión sin espacios sí existe, o lo borra si el jugador ya tiene la versión nueva.

En los tres casos, después de aplicarse una vez la condición que lo dispara es falsa, y eso es lo que los hace idempotentes.

4. DataManager

4.1 Escribir es replicar

DataManager guarda una referencia a la tabla de datos del perfil de cada jugador, la misma tabla que ProfileStore guarda, no una copia. Tiene cuatro operaciones principales: leer y escribir una clave, y leer y escribir una ruta anidada, como {"Achievements", "Titles", "Owned", clave}. Escribir modifica la tabla, y como es la tabla del perfil, el cambio se guarda en el siguiente guardado automático. En la misma llamada, se le manda al cliente un evento con la ruta unida por puntos y el valor nuevo:

set(ruta,v)⇒S[ruta]←v∧enviar("𝙰.𝙱.𝙲",v)

De ahí sale una convención que todo el código del servidor sigue: copiar, modificar y escribir. El servicio de temporadas, por ejemplo, clona el bloque de temporada, cambia los contadores y escribe el bloque completo. Modificar una tabla del perfil en el lugar guardaría el cambio, pero no se lo avisaría al cliente, porque nada pasó por la función que replica.

4.2 Esperar lo correcto

Hay dos esperas que parecen la misma y no lo son. GetProfile espera a que exista la sesión del jugador, y WaitForData espera a que exista su tabla de datos. Entre las dos hay una ventana, porque después de abrir la sesión el servidor todavía carga las preferencias del jugador desde otra tienda (sección 6), y solo después publica la tabla. Un sistema que esperaba la sesión y después leía un valor guardado lo encontraba vacío, y para un valor de progreso “vacío” significa “este jugador nunca tuvo uno”, que es muy distinto de “todavía no llegó”. Eso le pasó a la rutina que libera quests desactivadas: veía un perfil sin quests activas y no hacía nada. Lo que lee datos guardados tiene que esperar los datos, no la sesión.

5. La copia del cliente

5.1 Foto más cambios

El cliente mantiene una copia de sus datos que se construye en dos partes. Al entrar recibe una foto completa, Sj, y después recibe cada cambio oi=(rutai,vi) en el orden en que el servidor los hizo. Aplicar un cambio es asignar el valor en la ruta, creando las tablas intermedias que falten:

Sk=ok∘ok−1∘⋯∘oj+1(Sj)

Si el cliente tiene la foto y todos los cambios posteriores en orden, su copia es igual al estado del servidor. Los eventos remotos de Roblox son confiables y ordenados para cada jugador, así que la condición se cumple. Es el mismo esquema de una base de datos replicada que arranca de una foto y después reproduce un registro de cambios [2]. Como el único que escribe es el servidor, no hay conflictos que resolver: el último valor en cada ruta es el correcto.

5.2 La carrera de la entrada

El problema está en la foto. El servidor la empuja apenas los datos están listos, pero el script del cliente que la escucha puede conectarse después de que el empujón ya pasó. Para eso el cliente puede pedirla de forma explícita. Si el servidor todavía no tiene los datos, espera hasta 15 s y responde, y si no los consigue en ese tiempo expulsa al jugador, porque un cliente sin sus datos no puede jugar.

Del lado del cliente, PlayerDataClient ofrece la copia con una promesa. Si ya la tiene, la resuelve de inmediato. Si no, registra la función que la va a resolver antes de mandar la petición, para no perder una respuesta que llegue muy rápido, y le pone un tiempo límite de 10 s. Una señal DataChanged avisa de cada cambio, con la ruta y el valor, o sin ruta cuando llega una foto completa. Los consumidores filtran por prefijo de ruta, y muchos solo marcan que tienen que volver a dibujar y lo hacen en el siguiente cuadro, en vez de reaccionar a cada cambio.

5.3 Dos canales

Los datos del jugador viajan por eventos remotos normales, porque los valores son tablas arbitrarias. Los eventos del juego con forma conocida (una captura, el avance de una quest, una entrada de la tabla de temporada) viajan aparte con ByteNet, que los serializa en binario con un esquema por paquete. Así, el canal de datos es genérico y el de eventos es compacto.

6. Preferencias fuera del perfil

Las preferencias del jugador (volumen, gráficos, subtítulos y demás) vivían dentro del perfil. Eso significaba que el lugar del menú, que es un lugar de Roblox separado del juego, solo podía mostrarlas. Para escribirlas habría tenido que abrir una sesión sobre el perfil, y entonces el juego habría tenido que esperar a que el menú soltara el candado cada vez que el jugador apretaba “Jugar”.

Ahora las preferencias están en una tienda aparte, sin ProfileStore ni candado, que escriben tanto el juego como el menú. Ninguno le pide permiso al otro, porque el jugador solo está en uno de los dos a la vez. El costo aceptado es que una escritura se puede perder: sin sesión, dos servidores que tienen al mismo jugador por un instante, durante un teletransporte, pueden pisarse. Eso es la posición de un control deslizante, y es un buen precio por poder escribir desde cualquier lado. La regla es que ahí no va nada que se pueda ganar, gastar o robar.

Las escrituras usan un acelerador con escritura inicial. El primer cambio se escribe de inmediato, que es lo que hace que un ajuste hecho segundos antes de apretar “Jugar” sobreviva el viaje, y después se escribe como mucho una vez cada 6 s por jugador, con el último estado siempre escrito al salir. Arrastrar un control deslizante produce cambios mucho más rápido que el presupuesto de escrituras, y sin el acelerador lo gastaría entero. Al entrar al juego, las preferencias se copian a los datos que se le mandan al cliente, pero como copia, para que el perfil no termine guardando la tabla viva de preferencias.

7. Mensajes entre servidores

Un jugador puede regalarle algo a otro que está en otro servidor o desconectado. Escribir en el perfil del otro no es posible, porque el perfil tiene un candado y solo su dueño actual puede escribir. ProfileStore tiene una cola de mensajes por perfil: enviar un mensaje lo agrega a la cola del destinatario y avisa por MessagingService, así que si el destinatario está conectado lo recibe en uno o dos segundos, y si no, lo recibe la próxima vez que abra sesión.

La entrega es al menos una vez. Un manejador recibe cada mensaje y devuelve si lo consumió, y solo debe devolverlo después de escribir de verdad lo que el mensaje pedía. Un manejador que marca un regalo como consumido y después falla antes de entregarlo lo tiró a la basura. Un mensaje que nadie consume se queda en la cola y se vuelve a ofrecer, que es lo que garantiza que un regalo nunca se pierda. Por la misma razón, las compras guardan el identificador de cada recibo ya procesado, para que una entrega repetida no otorgue dos veces.

El orden de registro importa. ProfileStore entrega los mensajes encolados apenas existe un manejador, incluidos los que llegaron mientras el jugador estaba desconectado. Los manejadores escriben con DataManager, que lee la tabla publicada, así que si el manejador se registrara antes de publicar la tabla, el regalo se perdería y además quedaría marcado como consumido. Por eso se registra después. Un mensaje que no es una tabla se consume de inmediato, para que no se quede en la cola ofreciéndose en cada entrada para siempre, y cada manejador corre protegido, para que uno que falla no tumbe a los demás.

8. Salir

Al salir el jugador, se escribe la hora de su última conexión y después se cierra la sesión. El orden hace que “visto por última vez” signifique el final de una sesión completa y no la hora del último guardado automático, porque la marca entra en el guardado final.

9. Limitaciones

Los cambios mandan el valor completo en la ruta que se escribió, así que escribir un bloque grande para cambiar un número manda el bloque entero. La convención de copiar, modificar y escribir depende de que nadie la olvide: una modificación en el lugar se guarda pero no se replica, y el cliente queda desactualizado hasta el siguiente cambio en esa ruta. DataManager no tiene tipos estrictos, porque una ruta es una lista de cadenas y Luau no puede verificar contra la forma de la plantilla. Y el sistema de quests mantiene su propia copia en el cliente a partir del mismo flujo de cambios, así que hay dos copias del mismo dato que tienen que seguir el mismo protocolo.

10. Lo que me dejó

Un estado del lado del cliente que se construye con una foto y una secuencia ordenada de cambios es fácil de razonar, porque la pregunta de si está bien se reduce a si llegó todo en orden. Las migraciones idempotentes me ahorraron la contabilidad de versiones, que es donde suelen aparecer los perfiles a medio migrar. Y lo de las preferencias me enseñó que no todos los datos merecen las mismas garantías. Dividirlos según lo que cuesta perderlos permitió escribir unos desde cualquier lugar sin poner en riesgo los otros.

Referencias

  1. C. G. Gray y D. R. Cheriton. “Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency”. Proceedings of the 12th ACM Symposium on Operating Systems Principles, 1989.
  2. M. Kleppmann. Designing Data-Intensive Applications. O’Reilly, 2017.
  3. loleris (MAD STUDIO). ProfileStore. github.com/MadStudioRoblox/ProfileStore.

Este texto de Mastedore. Todos los derechos reservados.