Praxsuite

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 navegador

  • Có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 --release 17, así que funciona igual en Paper 1.20.x (Java 17) que en 26.x (Java 25).

Un proyecto de plugin Paper/Spigot

Con Maven, y paper-api ya funcionando. Si todavía no tienes un servidor donde probarlo, la Parte 1 de la guía Java SDK Use Case en Minecraft lo levanta desde cero.

Un workspace de Praxsuite activo

Se crea en portal.praxsuite.com.

Una Tabla

Crea una tabla player_profiles con columnas mojang_id (ShortText) y coins (Integer).

Un proveedor de identidad de plataforma, si vas a iniciar sesión de jugadores

Un proveedor con slug minecraft, tipo "afirmado por servidor", registrado en Settings → API Gateway. Ver Proveedores de Plataformas de Juego — el mecanismo es idéntico al que usa Roblox, solo cambia el 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

pk_live_

En cualquier lugar, incluido código que un jugador puede leer. Diseñada para ser pública.

Secreta

sk_live_

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 publishToMavenLocal

Luego, 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ético

Esto 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

PUBLISHABLE_KEY_REFUSED

Se llamó assertPlayer sobre un cliente construido con una credencial pk_live_.

Crea una instancia de Praxsuite aparte, con una clave secreta, para esta llamada.

HTTP_403: This key is not marked for a game platform...

La clave es secreta, pero no está marcada para la plataforma minecraft en el portal.

Settings → API Gateway → la clave → configurar su plataforma.

assertPlayer funciona, pero cualquier query del jugador devuelve vacío o 403

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).

MISSING_WORKSPACE / MISSING_CREDENTIAL

Se llamó Praxsuite.builder().build() sin workspace id ni credencial, y tampoco había variables de entorno PRAXSUITE_WORKSPACE_ID/PRAXSUITE_API_KEY.

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 data()/auth()/endpoints() corrió en el hilo principal.

Envuélvela en Bukkit.getScheduler().runTaskAsynchronously(...), y vuelve al hilo principal con runTask(...) antes de tocar cualquier API de Bukkit.

Update requires a filter (no unscoped updates)

Un update/delete sin condición.

Agrega un Filters.eq(...) (o similar) que identifique las filas — esta verificación existe para que un error de tipeo no elimine el contenido de una tabla entera.

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 assertPlayer se 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.