Implementación del SDK de Java en Minecraft
¿Qué vamos a hacer?
Vas a conectar un plugin de servidor de Minecraft a un backend real: leer y escribir filas en una base de datos en la nube, iniciar sesión de un jugador sin ninguna pantalla de login, y llamar lógica del lado del servidor que un jugador nunca puede ver ni manipular.
Al terminar esta guía vas a saber:
Qué credencial usar en un plugin de Paper, y por qué es la elección opuesta a la de un motor que corre del lado del cliente
Cómo instalar el SDK como dependencia de Maven y empaquetarlo (shading) dentro del jar de tu plugin
Cómo iniciar sesión de un jugador usando solo
online-mode:true— sin email, sin contraseña, sin navegadorCómo leer y escribir datos, y llamar a un Endpoint del Gateway
Nivel requerido: debes sentirte cómodo escribiendo un plugin de Paper/Spigot en Java — eventos, comandos, el ciclo de vida del plugin. No hace falta experiencia previa con APIs ni backends.
¿Qué es Praxsuite?
Piensa en Praxsuite como una base de datos en la nube con la que tu plugin habla por internet, con una capa programable encima.
Un Workspace es la carpeta grande que contiene todo lo que construyes. Dos piezas importan aquí: una Tabla son filas y columnas que tu plugin lee y escribe, y un Endpoint es lógica que tú escribiste y que tu plugin solo puede pedir que se ejecute — no puede ver adentro.
Esa diferencia es todo el punto. Cualquier cosa que un tramposo querría cambiar tiene que vivir detrás de un Endpoint, no en un valor que tu plugin calcula y simplemente reporta.
¿Qué es un Gateway? La puerta de entrada de tu workspace: la única dirección HTTPS por la que pasa cada solicitud. Verifica tu credencial, aplica límites de tasa, y reenvía la solicitud. Tu plugin nunca habla directo con la base de datos.
Requisitos Previos
Requisito | Descripción |
JDK 17 o más nuevo | El SDK compila con |
Un proyecto de plugin Paper/Spigot | Con Maven, y |
Un workspace de Praxsuite activo | Se crea en |
Una Tabla | Crea una tabla |
Un proveedor de identidad de plataforma, si vas a iniciar sesión de jugadores | Un proveedor con slug |
Cómo Obtener tus Credenciales
Dos valores identifican tu plugin ante Praxsuite. Necesitas los dos.
Tu Workspace ID
Abre tu workspace en el portal y observa la barra de direcciones, o el selector de workspaces del menú principal — ambos muestran el UUID.
Tu clave: publicable vs secreta, y por qué Minecraft invierte el consejo habitual
Clave | Prefijo | Dónde puede vivir |
Publicable |
| En cualquier lugar, incluido código que un jugador puede leer. Diseñada para ser pública. |
Secreta |
| Solo del lado del servidor. Otorga lo que sea que esa clave tenga como alcance. |
En Roblox o Unity, el SDK corre en una máquina que tú no controlas — la del jugador — así que ahí normalmente corresponde una clave publicable. Un plugin de Paper es el caso opuesto. Corre en un servidor que tú administras, así que una clave secreta es normalmente la correcta — para eso existe. El propio Praxsuite.builder() rechaza una clave secreta si marcas el build con .clientSide(true), que es la opción para el caso poco frecuente en que sí estás distribuyendo este plugin a dueños de otros servidores.
Regla de oro: la única razón real para usar una clave publicable en un plugin es que planees distribuirlo a operadores de servidor que no controlas. Si es tu propio servidor, una clave secreta, fuera de un repositorio público, es lo correcto.
Paso 1 — Agregar el SDK a tu Proyecto
Repositorio: https://github.com/TesseractSoftwares/Praxsuite-SDK-Java
El artefacto todavía no está en Maven Central. Se compila e instala en tu repositorio Maven local:
cd Praxsuite-SDK-Java
./gradlew publishToMavenLocalLuego, en el pom.xml de tu plugin:
<dependency>
<groupId>com.tesseractsoftwares</groupId>
<artifactId>praxsuite-sdk</artifactId>
<version>1.1.0</version>
</dependency>Importante — aquí NO va `scope=provided`: a diferencia de `paper-api`, el servidor no trae este SDK incluido. Tiene que viajar dentro de tu propio jar. Agrega el plugin de Shade para que `mvn package` lo empaquete:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>El SDK no tiene dependencias propias de terceros — está construido sobre java.net.http de la JDK — así que lo que termina viajando dentro de tu jar es liviano y no puede chocar con ninguna librería que otro plugin ya haya cargado en la classloader del servidor.
Paso 2 — Crear el Cliente
import com.tesseractsoftwares.praxsuite.Praxsuite;
public class MiPlugin extends JavaPlugin {
private Praxsuite prax;
@Override
public void onEnable() {
prax = Praxsuite.builder()
.workspaceId("tu-workspace-uuid")
.credential("sk_live_...")
.build();
}
}Praxsuite es barato de construir y seguro entre hilos — una instancia como campo de tu JavaPlugin, creada una vez en onEnable(), es todo el patrón. (Hay una única razón legítima para construir una segunda instancia — el Event Bus, que se cubre en la guía de caso de uso.)
Paso 3 — Iniciar Sesión de un Jugador, Sin Pantalla de Login
En Roblox, player.UserId es una identidad verificada porque Roblox mismo autenticó al jugador antes de que tu servidor lo viera. Un servidor de Minecraft corriendo online-mode: true tiene exactamente la misma garantía para player.getUniqueId() — Mojang ya verificó la cuenta de Microsoft del jugador antes de que Bukkit te entregue el objeto Player. El mecanismo de Praxsuite para confiar en eso es assertPlayer:
PraxAuth.Session sesion = prax.auth().assertPlayer(
"minecraft", // el slug del proveedor configurado en el portal
player.getUniqueId().toString(), // el UUID de Mojang - el jugador no puede falsearlo
player.getName()); // solo cosméticoEsto requiere una clave secreta marcada para la plataforma `minecraft` en el portal — nunca una publicable. El gateway confía en quien tenga esa clave, no en el id que le envías, así que ese id tiene que provenir de una fuente que el jugador no controle (Player.getUniqueId() bajo online-mode:true), nunca un valor leído de un paquete o de un mod del cliente.
¿Qué se está confiando aquí? Tu clave de servidor, no el jugador. Cualquiera con una clave secreta marcada `minecraft` podría afirmar ser cualquier jugador — por eso el gateway rechaza esta llamada con una clave publicable, y por eso esa clave va en un archivo de configuración que controla el dueño del servidor, nunca en un jar que se distribuye.
Lo genuinamente distinto de cualquier otro SDK: el caché de sesiones es tuyo
El SDK de Lua de Roblox proporciona Identify(player) y no delimita queries por jugador en absoluto — la clave de servidor hace todo, y las reglas por jugador son código propio. El assertPlayer de Java va un paso más allá de eso y un paso más corto de hacerlo todo por ti: sí devuelve una sesión real por jugador (útil para llamar a un Endpoint actuando como ese jugador puntual), pero deliberadamente no la instala en ningún lado por ti.
Eso no es un descuido. login() sí instala su resultado como la sesión "ambient" única del cliente — correcto para una aplicación con un solo usuario con sesión iniciada. Un plugin de Paper no es eso: afirma la identidad de muchos jugadores a la vez sobre la misma instancia compartida de Praxsuite, y si assertPlayer sobrescribiera un campo de "sesión actual" compartido como sí hace login(), el segundo jugador en conectarse le robaría en silencio la sesión al primero, y desde entonces cualquier solicitud de cualquiera correría como el último que ingresó. assertPlayer entrega el objeto Session y deja decidir dónde vive:
private final Map<UUID, PraxAuth.Session> sesiones = new ConcurrentHashMap<>();
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
PraxAuth.Session sesion = prax.auth().assertPlayer(
"minecraft", player.getUniqueId().toString(), player.getName());
sesiones.put(player.getUniqueId(), sesion);
});
}Donde necesites actuar específicamente como ese jugador más adelante — lo más común, llamar a un Endpoint que le asigna un rol o lee su propia fila — pasa sesion.accessToken() explícitamente. No existe un parámetro asPlayer implícito como en algún SDK hermano; en Java, siempre eres tú quien decide qué token viaja en cada solicitud.
¿Por qué una cuenta recién creada no tiene acceso a nada? Una cuenta de `assertPlayer` nace con los roles por defecto que tenga configurados el proveedor (o el workspace) en el portal — y si no hay ninguno, la cuenta no tiene ninguno, así que cualquier query devuelve vacío o 403. La guía de caso de uso cubre una alternativa más escalable: una automation que valida el token del jugador y le asigna un rol explícitamente.
Paso 4 — Leer y Escribir Datos
Cada método aquí interactúa con una Tabla por nombre.
prax.data().insert("player_profiles", Map.of(
"mojang_id", player.getUniqueId().toString(),
"coins", 100));Leerlo de vuelta:
Responses.Page pagina = prax.data().table("player_profiles")
.select("mojang_id", "coins")
.where(Filters.eq("mojang_id", player.getUniqueId().toString()))
.limit(10)
.fetch();
for (Map<String, Object> fila : pagina.rows()) {
getLogger().info(fila.get("mojang_id") + " tiene " + fila.get("coins") + " monedas");
}fetch() (y sus equivalentes first(), count(), exists(), all()) es lo único que realmente envía la solicitud — todo lo anterior solo la construye.
Update y delete exigen un where. El SDK rechaza una escritura sin alcance definido antes de que llegue a la red:
prax.data().update("player_profiles", Map.of("coins", 250),
Filters.eq("mojang_id", player.getUniqueId().toString()));Filters expone exactamente los trece operadores que el Gateway implementa — eq neq gt gte lt lte like ilike in is between contains textsearch — más algunos alias amigables (startsWith, isNull...) que compilan a uno de esos. No existe notIn; exprésalo como un in positivo con los valores que sí quieres.
Los nombres de columna son exactos. `coins` y `Coins` son columnas distintas, y un nombre con espacio no es el mismo nombre con guion bajo. Copia los nombres del portal en vez de retipearlos — un desajuste es la razón más común de que una escritura "funcione" y el valor vuelva como si nunca se hubiese establecido.
Paso 5 — Llamar a un Endpoint del Gateway
Una Tabla es datos; un Endpoint es lógica. Si un jugador puede pagar algo, o si se le asigna un rol, no puede ser una decisión que tu plugin tome y solo reporte — un jar descompilado es tan legible como un LocalScript de Roblox.
Map<String, Object> resultado = prax.endpoints().call(endpointId, Map.of(
"token", sesion.accessToken()));call bloquea hasta que la automation vinculada termina y devuelve lo que su nodo Response produjo, ya interpretado. Ejecuta esto fuera del hilo principal — un endpoint Sync mantiene la conexión abierta mientras corre su automation, y eso puede ser decenas de milisegundos que el tick loop de tu servidor nunca debería esperar.
Un endpoint no autentica a quien lo llama por sí solo. Un POST sin ninguna credencial igual llega a la automation. La autoridad tiene que provenir de dentro de la automation — típicamente un nodo Validate End User Token que valida el token que se le pasó, nunca un id crudo que quien llama podría inventar. Diséñalo así desde el principio; es la diferencia entre "ejecutado por el servidor" y "con autoridad del servidor".
Ejemplo Completo
package com.example;
import com.tesseractsoftwares.praxsuite.*;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MiPlugin extends JavaPlugin implements Listener {
private Praxsuite prax;
private final Map<UUID, PraxAuth.Session> sesiones = new ConcurrentHashMap<>();
@Override
public void onEnable() {
prax = Praxsuite.builder()
.workspaceId("tu-workspace-uuid")
.credential("sk_live_...")
.build();
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
try {
PraxAuth.Session sesion = prax.auth().assertPlayer(
"minecraft", player.getUniqueId().toString(), player.getName());
sesiones.put(player.getUniqueId(), sesion);
prax.data().insert("player_profiles", Map.of(
"mojang_id", player.getUniqueId().toString(),
"coins", 0));
} catch (PraxError e) {
getLogger().warning("No se pudo abrir sesion Praxsuite: " + e.getMessage());
}
});
}
}Ejecuta el servidor, conéctate, y revisa el portal: aparece una fila en player_profiles. Ese recorrido — de un evento de Bukkit al Gateway y a la base de datos — es toda la integración.
Errores Comunes y Cómo Evitarlos
Error | Causa | Solución |
| Se llamó | Crea una instancia de |
| La clave es secreta, pero no está marcada para la plataforma | Settings → API Gateway → la clave → configurar su plataforma. |
| La cuenta se creó sin roles por defecto. | Configura roles por defecto en el proveedor, o asigna uno explícitamente vía una automation (ver la guía de caso de uso). |
| Se llamó | Pasa ambos explícitamente, o establece las variables de entorno. |
El servidor parece congelarse un instante cada vez que alguien entra o ejecuta un comando | Una llamada a | Envuélvela en |
| Un | Agrega un |
Consejo: cualquier falla del SDK es un `PraxError` (o una subclase tipada como `PraxAuthError`, `PraxRateLimitError`). Captura `PraxError` una sola vez y registra `e.getMessage()` — ya indica en lenguaje simple qué salió mal.
Consejos de Producción
Mantén la clave secreta fuera del control de versiones — un archivo de configuración leído en
onEnable(), no un literal en el código, una vez que se pasa de un servidor de prueba personal.Nunca dejes que la sesión de
assertPlayerse convierta en la sesión ambient del mismo cliente que se usa para todo lo demás — ver la sección del Event Bus en la guía de caso de uso para la razón exacta, y el patrón que lo evita.Coloca detrás de un Endpoint (cuya automation valide a quien llama) cualquier cosa que un tramposo querría cambiar — un valor que tu plugin calcula y solo reporta es tan confiable como el jar que lo calcula.
Toda llamada de red —
assertPlayer,data(),endpoints()— corre fuera del hilo principal. No hay excepción a esto en un plugin con más de un puñado de jugadores.Los nombres de columna distinguen mayúsculas y espacios. Cópialos del portal.
Próximos Pasos
Lee la guía Java SDK Use Case en Minecraft, donde esto se convierte en un Buscaminas completo con tablero dibujado localmente, resultado validado por el servidor, y un leaderboard en vivo por el Event Bus.
Considera asignar roles vía una automation en vez de los valores por defecto estáticos del proveedor — escala a cualquier plataforma nueva sin tocar configuración del portal por cada una.
Lee Proveedores de Plataformas de Juego para la configuración del lado del portal que esta guía asume que ya existe; el Paso 7 de la guía de caso de uso la recorre aplicada a este juego.
Ya tienes un plugin que habla con un backend real, con una identidad de jugador que nadie puede falsificar. Todo lo que sigue es decidir qué va de qué lado de esa línea.