Desarrollo Java Plugin + Mod

API Bridge

El bridge es el canal de comunicación binario que conecta el plugin del servidor con el mod del cliente. Esta página documenta el protocolo, los tipos de mensaje y cómo integrarse con EventUI desde un plugin o mod externo.

EventUI es open source. Puedes usar el módulo eventui-common como dependencia en tu plugin o mod para acceder a todas las interfaces, enums y contratos sin necesidad de reimplementarlos.

Arquitectura del bridge

La comunicación usa el canal eventui:bridge sobre la API de Plugin Messaging de Bukkit. La serialización es binaria con DataOutputStream / DataInputStream — compacta y sin dependencias extra.

Plugin (Paper/Arclight)
eventui:bridge
Mod (Fabric)
Mod (Fabric)
eventui:bridge
Plugin (Paper/Arclight)

Principios clave del protocolo:

PrincipioDescripción
Fuente de verdad en el servidorEl plugin gestiona todo el progreso y estado. El mod solo renderiza lo que recibe.
Comunicación asíncronaLos requests usan messageId / replyToMessageId (UUID) para correlacionar respuestas.
Serialización binariaNo JSON. Formato fijo con DataOutputStream — más compacto y determinista.
Push del servidorEl plugin envía PROGRESS_UPDATE y EVENT_STATE_CHANGED sin que el cliente lo solicite.

Formato de serialización

Cada mensaje se serializa en este orden exacto:

Formato binario — DataOutputStream
1. messageType     → String (UTF-8)       nombre del MessageType enum
2. playerId        → long, long           UUID como dos longs (MSB, LSB)
3. messageId       → long, long           UUID del mensaje
4. replyToId       → boolean + long,long  null flag + UUID si no es null
5. timestamp       → long                 epoch millis
6. payloadSize     → int                  número de entradas en el payload
7. payload entries → String, String       clave y valor UTF-8 por cada entrada
El orden de los campos es estricto. Cualquier desviación causa que el deserializador falle silenciosamente o lance una excepción. Usa siempre eventui-common en lugar de serializar manualmente.

Tipos de mensaje

Todos los tipos están definidos en com.eventui.api.bridge.MessageType.

MOD → Plugin
REQUEST_EVENT_DATA
event_id: "..."
REQUEST_EVENT_PROGRESS
event_id: "..."
player_uuid: "..."
REQUEST_UI_CONFIG
event_id: "..."
REQUEST_UI_MODE
(vacío)
UI_BUTTON_CLICKED
button_id: "..."
event_id: "..."
UI_SCREEN_OPENED
event_id: "..."
player_uuid: "..."
UI_SCREEN_CLOSED
event_id: "..."
Plugin → MOD
EVENT_DATA_RESPONSE
Serialización de EventDefinition
EVENT_PROGRESS_RESPONSE
Serialización de EventProgress
UI_CONFIG_RESPONSE
Serialización de UIConfig
UI_MODE_RESPONSE
mode: "custom" | "hardcoded"
screen_id: "..."
PROGRESS_UPDATE
event_id: "..."
objective_id: "..."
current: "5"
target: "10"
description: "..."
EVENT_STATE_CHANGED
event_id: "..."
new_state: "COMPLETED"
UI_STATE_UPDATE
key: "..."
value: "..."
EVENT_RELOAD_NOTIFICATION
(vacío) — recarga UI en el cliente

Interfaces del common

El módulo eventui-common expone estas interfaces que implementan ambos lados:

Interfaz / ClasePaqueteDescripción
EventBridgeapi.bridgeContrato bidireccional. Implementado por plugin y mod.
BridgeMessageapi.bridgeEnvelope de todo mensaje: tipo, payload, playerId, messageId.
MessageTypeapi.bridgeEnum con todos los tipos de mensaje del protocolo.
EventDefinitionapi.eventDefinición inmutable de un evento.
EventProgressapi.eventEstado mutable de un evento para un jugador.
EventStateapi.eventEnum: AVAILABLE, IN_PROGRESS, COMPLETED, FAILED, LOCKED.
ObjectiveDefinitionapi.objectiveEstructura de un objetivo: tipo, target, parámetros.
ObjectiveTypeapi.objectiveEnum con los 21 tipos de objetivo.
UIConfigapi.uiConfiguración completa de una pantalla UI.
UIElementapi.uiNodo del árbol de UI: tipo, posición, propiedades, hijos.

Integración desde un plugin externo

Si tienes un plugin propio (Paper/Arclight) y quieres enviar actualizaciones de progreso a EventUI o escuchar acciones de la UI, necesitas registrarte en el canal eventui:bridge.

Dependencia en tu plugin

pom.xml — añadir eventui-common como dependencia
<dependency>
    <groupId>com.eventui</groupId>
    <artifactId>eventui-common</artifactId>
    <version>1.0.0</version>
    <scope>provided</scope>
</dependency>

Enviar un PROGRESS_UPDATE al cliente

El caso más común: tu plugin avanza el progreso de un objetivo de EventUI desde lógica propia.

Enviar progreso desde un plugin externo
import com.eventui.api.bridge.MessageType;
import org.bukkit.entity.Player;

public void sendProgressUpdate(Player player, String eventId,
                               String objectiveId, int current, int target,
                               String description) {

    // Construir el payload como bytes con DataOutputStream
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
         DataOutputStream out = new DataOutputStream(baos)) {

        // 1. Tipo de mensaje
        out.writeUTF(MessageType.PROGRESS_UPDATE.name());

        // 2. Player UUID
        UUID uuid = player.getUniqueId();
        out.writeLong(uuid.getMostSignificantBits());
        out.writeLong(uuid.getLeastSignificantBits());

        // 3. Message ID
        UUID msgId = UUID.randomUUID();
        out.writeLong(msgId.getMostSignificantBits());
        out.writeLong(msgId.getLeastSignificantBits());

        // 4. ReplyTo (null)
        out.writeBoolean(false);

        // 5. Timestamp
        out.writeLong(System.currentTimeMillis());

        // 6. Payload
        Map<String, String> payload = Map.of(
            "event_id",     eventId,
            "objective_id", objectiveId,
            "current",      String.valueOf(current),
            "target",       String.valueOf(target),
            "description",  description
        );

        out.writeInt(payload.size());
        for (Map.Entry<String, String> entry : payload.entrySet()) {
            out.writeUTF(entry.getKey());
            out.writeUTF(entry.getValue());
        }

        // 7. Enviar al canal
        player.sendPluginMessage(yourPlugin, "eventui:bridge", baos.toByteArray());

    } catch (IOException e) {
        getLogger().severe("Error sending EventUI progress: " + e.getMessage());
    }
}

Recibir acciones de la UI desde el cliente

Para saber cuándo un jugador hace clic en un botón de EventUI desde tu plugin externo:

Escuchar UI_BUTTON_CLICKED en tu plugin
import org.bukkit.plugin.messaging.PluginMessageListener;
import com.eventui.api.bridge.MessageType;

public class MiPlugin extends JavaPlugin implements PluginMessageListener {

    @Override
    public void onEnable() {
        // Registrar como listener del canal de EventUI
        getServer().getMessenger().registerIncomingPluginChannel(
            this, "eventui:bridge", this
        );
    }

    @Override
    public void onPluginMessageReceived(String channel, Player player, byte[] data) {
        if (!channel.equals("eventui:bridge")) return;

        try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) {

            String typeStr = in.readUTF();
            MessageType type = MessageType.valueOf(typeStr);

            // Leer y descartar playerId, messageId, replyTo, timestamp
            in.readLong(); in.readLong(); // playerId
            in.readLong(); in.readLong(); // messageId
            if (in.readBoolean()) { in.readLong(); in.readLong(); } // replyTo
            in.readLong(); // timestamp

            // Leer payload
            int size = in.readInt();
            Map<String, String> payload = new HashMap<>();
            for (int i = 0; i < size; i++) {
                payload.put(in.readUTF(), in.readUTF());
            }

            if (type == MessageType.UI_BUTTON_CLICKED) {
                String buttonId = payload.get("button_id");
                String eventId  = payload.get("event_id");
                handleButtonClick(player, buttonId, eventId);
            }

        } catch (IOException e) {
            getLogger().warning("Error reading EventUI message: " + e.getMessage());
        }
    }

    private void handleButtonClick(Player player, String buttonId, String eventId) {
        // Tu lógica aquí
        getLogger().info(player.getName() + " clicked button: " + buttonId);
    }
}

Integración desde un mod Fabric externo

Si quieres que tu mod reaccione a eventos de EventUI — por ejemplo mostrar un efecto de partículas cuando el jugador completa una misión — puedes escuchar el canal desde el cliente.

Dependencia en tu mod

build.gradle — añadir eventui-common
dependencies {
    modImplementation "com.eventui:eventui-common:1.0.0"
}

Escuchar mensajes del servidor

Recibir EVENT_STATE_CHANGED en tu mod Fabric
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.util.Identifier;
import com.eventui.api.bridge.MessageType;

public class MiModClient implements ClientModInitializer {

    private static final Identifier EVENTUI_CHANNEL =
        Identifier.of("eventui", "bridge");

    @Override
    public void onInitializeClient() {
        ClientPlayNetworking.registerGlobalReceiver(
            EVENTUI_CHANNEL,
            (client, handler, buf, responseSender) -> {

                // Leer el mensaje en el hilo de red
                byte[] data = new byte[buf.readableBytes()];
                buf.readBytes(data);

                try (DataInputStream in =
                     new DataInputStream(new ByteArrayInputStream(data))) {

                    String typeStr = in.readUTF();
                    MessageType type = MessageType.valueOf(typeStr);

                    // Descartar campos de metadata
                    in.readLong(); in.readLong(); // playerId
                    in.readLong(); in.readLong(); // messageId
                    if (in.readBoolean()) { in.readLong(); in.readLong(); }
                    in.readLong(); // timestamp

                    // Leer payload
                    int size = in.readInt();
                    Map<String, String> payload = new HashMap<>();
                    for (int i = 0; i < size; i++) {
                        payload.put(in.readUTF(), in.readUTF());
                    }

                    if (type == MessageType.EVENT_STATE_CHANGED) {
                        String eventId  = payload.get("event_id");
                        String newState = payload.get("new_state");

                        // Ejecutar en el hilo del juego (obligatorio para rendering)
                        client.execute(() -> {
                            if ("COMPLETED".equals(newState)) {
                                spawnCompletionParticles(client, eventId);
                            }
                        });
                    }

                } catch (IOException e) {
                    // ignorar mensajes malformados
                }
            }
        );
    }

    private void spawnCompletionParticles(MinecraftClient client, String eventId) {
        // Tu lógica de partículas aquí
    }
}

Enviar un mensaje al servidor desde el mod

Enviar UI_BUTTON_CLICKED desde un mod externo
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import com.eventui.api.bridge.MessageType;

public static void sendButtonClick(String buttonId, String eventId, UUID playerId) {
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
         DataOutputStream out = new DataOutputStream(baos)) {

        out.writeUTF(MessageType.UI_BUTTON_CLICKED.name());

        out.writeLong(playerId.getMostSignificantBits());
        out.writeLong(playerId.getLeastSignificantBits());

        UUID msgId = UUID.randomUUID();
        out.writeLong(msgId.getMostSignificantBits());
        out.writeLong(msgId.getLeastSignificantBits());

        out.writeBoolean(false); // sin replyTo
        out.writeLong(System.currentTimeMillis());

        out.writeInt(2);
        out.writeUTF("button_id"); out.writeUTF(buttonId);
        out.writeUTF("event_id");  out.writeUTF(eventId);

        PacketByteBuf buf = PacketByteBufs.create();
        buf.writeBytes(baos.toByteArray());

        ClientPlayNetworking.send(
            Identifier.of("eventui", "bridge"), buf
        );

    } catch (IOException e) {
        // manejar error
    }
}

Usar ClientEventBridge directamente

Si tu mod depende de EventUI como librería (no solo del common), puedes usar ClientEventBridge directamente en lugar de manejar el canal manualmente. Es más seguro y menos propenso a errores de serialización:

Registrar handler usando ClientEventBridge
import com.eventui.fabric.client.bridge.ClientEventBridge;
import com.eventui.api.bridge.MessageType;

// En tu ClientModInitializer, después de que EventUI se haya inicializado:
ClientEventBridge bridge = ClientEventBridge.getInstance();

bridge.registerMessageHandler(MessageType.EVENT_STATE_CHANGED, message -> {
    String eventId  = message.getPayload().get("event_id");
    String newState = message.getPayload().get("new_state");

    MinecraftClient.getInstance().execute(() -> {
        if ("COMPLETED".equals(newState)) {
            // Tu lógica aquí
        }
    });
});
Orden de inicialización: ClientEventBridge.getInstance() solo está disponible después de que el mod de EventUI se haya inicializado. Usa el evento ClientLifecycleEvents.CLIENT_STARTED de Fabric para garantizar el orden correcto.

Notas importantes

TemaDetalle
Hilo de rendering Los mensajes del bridge llegan en el hilo de red. Cualquier operación de rendering o acceso al estado del juego en el cliente debe ejecutarse con client.execute(() -> ...).
Compatibilidad de servidores El plugin de EventUI solo es compatible con Paper 1.21.1 y Arclight 1.21.1. Spigot no está soportado por incompatibilidades con la API de Adventure.
Canal bidireccional Bukkit requiere que el plugin que registra el canal también sea el que lo usa para enviar. Si usas un plugin externo, registra el canal con tu plugin antes de enviar mensajes.
Versión del protocolo El formato binario puede cambiar entre versiones de EventUI. Siempre usa la misma versión de eventui-common que el plugin y el mod instalados en el servidor/cliente.