Ir para o conteúdo

Uso Avanzado del Meta System en Mods

El Meta System de Hytale es un sistema potente para almacenar datos personalizados en entidades. Esta guía completa cubre cómo usar el sistema para almacenar stats de jugador, inventarios custom, quest systems, y más.

¿Qué es el Meta System?

El Meta System permite adjuntar datos arbitrarios a entidades (jugadores, NPCs, mobs, etc.) de forma:

  • Tipada: Type-safe con genéricos de Java
  • Eficiente: Almacenamiento optimizado
  • Persistente: Los datos pueden guardarse en disco
  • Flexible: Cualquier tipo de dato con Codecs

Conceptos Fundamentales

MetaRegistry

El registro que gestiona todas las MetaKeys de un tipo de entidad.

// Registro para jugadores
MetaRegistry<Player> playerRegistry = new MetaRegistry<>();

// Registro para contextos de interacción
MetaRegistry<InteractionContext> contextRegistry = new MetaRegistry<>();

MetaKey

Una clave tipada que identifica un tipo de dato específico.

// Key temporal (solo en memoria)
MetaKey<Integer> killCount = registry.registerMetaObject(
    player -> 0  // Valor inicial
);

// Key persistente (se guarda en disco)
MetaKey<PlayerStats> stats = registry.registerMetaObject(
    player -> new PlayerStats(),
    true,                        // Persistente
    "mimod:player_stats",        // Nombre único
    PlayerStats.CODEC            // Codec para serialización
);

MetaStore

Almacén de datos adjunto a cada entidad.

// Obtener meta store de un jugador
var metaStore = player.getMetaStore();

// Guardar dato
metaStore.putMetaObject(statsKey, new PlayerStats());

// Obtener dato
PlayerStats stats = metaStore.getMetaObject(statsKey);

Datos Persistentes vs Temporales

Datos Temporales

Solo existen mientras la entidad está cargada en memoria.

Casos de uso: - Cooldowns de habilidades - Estados temporales - Cache de cálculos - Flags de sesión

public class TemporaryDataExample {

    private static final MetaRegistry<Player> REGISTRY = new MetaRegistry<>();

    // Key temporal - no persistente
    private static final MetaKey<Long> LAST_TELEPORT = REGISTRY.registerMetaObject(
        player -> 0L  // Valor inicial: timestamp 0
    );

    public static void teleportPlayer(Player player, Vector3d destination) {
        var metaStore = player.getMetaStore();

        // Verificar cooldown
        long lastTeleport = metaStore.getMetaObject(LAST_TELEPORT);
        long currentTime = System.currentTimeMillis();

        if (currentTime - lastTeleport < 5000) {
            player.sendMessage("§c¡Espera 5 segundos entre teletransportes!");
            return;
        }

        // Teletransportar
        player.setPosition(destination);

        // Actualizar timestamp
        metaStore.putMetaObject(LAST_TELEPORT, currentTime);

        player.sendMessage("§a¡Teletransportado!");
    }
}

Datos Persistentes

Se guardan en disco y persisten entre sesiones.

Casos de uso: - Estadísticas de jugador - Progreso de quests - Inventarios personalizados - Configuraciones de jugador

package com.ejemplo.mimod.systems;

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.meta.MetaKey;
import com.hypixel.hytale.server.core.meta.MetaRegistry;
import com.hypixel.hytale.server.core.meta.PersistentMetaKey;

public class PlayerStatsSystem {

    private static final MetaRegistry<Player> PLAYER_REGISTRY = new MetaRegistry<>();

    // Key persistente con Codec
    private static final PersistentMetaKey<PlayerStats> STATS_KEY =
        (PersistentMetaKey<PlayerStats>) PLAYER_REGISTRY.registerMetaObject(
            player -> new PlayerStats(),  // Factory: crear stats iniciales
            true,                          // Persistente
            "mimod:player_stats",          // ID único
            PlayerStats.CODEC              // Codec para serialización
        );

    /**
     * Obtiene las stats de un jugador.
     * Si no existen, crea stats nuevas.
     */
    public static PlayerStats getStats(Player player) {
        var metaStore = player.getMetaStore();
        return metaStore.getMetaObject(STATS_KEY);
    }

    /**
     * Actualiza las stats de un jugador.
     */
    public static void updateStats(Player player, PlayerStats stats) {
        var metaStore = player.getMetaStore();
        metaStore.putMetaObject(STATS_KEY, stats);
    }

    /**
     * Stats de jugador con serialización.
     */
    public static class PlayerStats {
        public int kills;
        public int deaths;
        public int playTime;      // Minutos
        public int blocksPlaced;
        public int blocksBroken;

        public PlayerStats() {
            this(0, 0, 0, 0, 0);
        }

        public PlayerStats(int kills, int deaths, int playTime,
                          int blocksPlaced, int blocksBroken) {
            this.kills = kills;
            this.deaths = deaths;
            this.playTime = playTime;
            this.blocksPlaced = blocksPlaced;
            this.blocksBroken = blocksBroken;
        }

        /**
         * Codec para serialización/deserialización.
         *
         * Define cómo convertir PlayerStats a/desde formato persistente.
         */
        public static final Codec<PlayerStats> CODEC =
            Codec.INT.flatMap(kills ->
                Codec.INT.flatMap(deaths ->
                    Codec.INT.flatMap(playTime ->
                        Codec.INT.flatMap(blocksPlaced ->
                            Codec.INT.map(blocksBroken ->
                                new PlayerStats(kills, deaths, playTime,
                                              blocksPlaced, blocksBroken)
                            )
                        )
                    )
                )
            );

        public double getKDRatio() {
            return deaths == 0 ? kills : (double) kills / deaths;
        }

        @Override
        public String toString() {
            return String.format(
                "PlayerStats{kills=%d, deaths=%d, KD=%.2f, playtime=%dm}",
                kills, deaths, getKDRatio(), playTime
            );
        }
    }
}

Sincronización Cliente-Servidor

Datos Solo Servidor

Datos que solo existen en el servidor.

// NO sincronizar con cliente
private static final MetaKey<SecretData> SECRET_KEY =
    REGISTRY.registerMetaObject(
        player -> new SecretData(),
        true,
        "mimod:secret_data",
        SecretData.CODEC
    );

// Estos datos nunca se envían al cliente

Datos Sincronizados

Para sincronizar datos con el cliente, necesitas manejar la sincronización manualmente.

public class SyncedDataExample {

    private static final MetaKey<ManaData> MANA_KEY =
        REGISTRY.registerMetaObject(
            player -> new ManaData(100),
            true,
            "mimod:mana",
            ManaData.CODEC
        );

    /**
     * Actualiza mana y sincroniza con el cliente.
     */
    public static void setMana(Player player, int current, int max) {
        // Actualizar servidor
        ManaData data = new ManaData(current, max);
        player.getMetaStore().putMetaObject(MANA_KEY, data);

        // Sincronizar con cliente
        syncManaToClient(player, data);
    }

    /**
     * Envía datos de mana al cliente.
     */
    private static void syncManaToClient(Player player, ManaData data) {
        // Crear packet custom
        // ManaUpdatePacket packet = new ManaUpdatePacket(data.current, data.max);

        // Enviar al cliente
        // player.getPacketHandler().write(packet);

        // Simplificado: usar comando o mensaje
        player.sendMessage("§bMana: " + data.current + "/" + data.max);
    }

    public static class ManaData {
        public int current;
        public int max;

        public ManaData(int max) {
            this(max, max);
        }

        public ManaData(int current, int max) {
            this.current = current;
            this.max = max;
        }

        public static final Codec<ManaData> CODEC =
            Codec.INT.flatMap(current ->
                Codec.INT.map(max ->
                    new ManaData(current, max)
                )
            );
    }
}

Serialización con Codecs

Los Codecs definen cómo convertir objetos a/desde formato persistente.

Codec Simple

public class SimpleCodecExample {

    // Codec para un solo valor
    public static final Codec<Integer> INT_CODEC = Codec.INT;

    // Codec para String
    public static final Codec<String> STRING_CODEC = Codec.STRING;

    // Codec para Boolean
    public static final Codec<Boolean> BOOL_CODEC = Codec.BOOLEAN;
}

Codec para Clase Custom

public class CustomClassCodec {

    public static class QuestProgress {
        public String questId;
        public int stage;
        public boolean completed;

        public QuestProgress(String questId, int stage, boolean completed) {
            this.questId = questId;
            this.stage = stage;
            this.completed = completed;
        }

        /**
         * Codec que serializa QuestProgress como:
         * questId -> stage -> completed
         */
        public static final Codec<QuestProgress> CODEC =
            Codec.STRING.flatMap(questId ->      // Serializar questId
                Codec.INT.flatMap(stage ->        // Serializar stage
                    Codec.BOOLEAN.map(completed -> // Serializar completed
                        new QuestProgress(questId, stage, completed)
                    )
                )
            );
    }
}

Codec para Listas

import com.hypixel.hytale.codec.codecs.array.ListCodec;
import java.util.List;

public class ListCodecExample {

    public static class QuestLog {
        public List<String> completedQuests;
        public List<String> activeQuests;

        public QuestLog(List<String> completedQuests, List<String> activeQuests) {
            this.completedQuests = completedQuests;
            this.activeQuests = activeQuests;
        }

        /**
         * Codec para listas de Strings.
         */
        public static final Codec<QuestLog> CODEC =
            new ListCodec<>(Codec.STRING).flatMap(completedQuests ->
                new ListCodec<>(Codec.STRING).map(activeQuests ->
                    new QuestLog(completedQuests, activeQuests)
                )
            );
    }
}

Codec para Maps

import com.hypixel.hytale.codec.codecs.map.MapCodec;
import java.util.Map;

public class MapCodecExample {

    public static class SkillLevels {
        public Map<String, Integer> skills; // skillName -> level

        public SkillLevels(Map<String, Integer> skills) {
            this.skills = skills;
        }

        /**
         * Codec para Map<String, Integer>.
         */
        public static final Codec<SkillLevels> CODEC =
            new MapCodec<>(Codec.STRING, Codec.INT).map(
                skills -> new SkillLevels(skills)
            );
    }
}

Ejemplos Prácticos Completos

Ejemplo 1: Sistema de Stats de Jugador

package com.ejemplo.mimod.systems;

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.meta.MetaKey;
import com.hypixel.hytale.server.core.meta.MetaRegistry;

public class PlayerStatsTracker {

    private static final MetaRegistry<Player> REGISTRY = new MetaRegistry<>();

    private static final MetaKey<DetailedStats> STATS_KEY =
        REGISTRY.registerMetaObject(
            player -> new DetailedStats(),
            true,  // Persistente
            "mimod:detailed_stats",
            DetailedStats.CODEC
        );

    /**
     * Registra un kill.
     */
    public static void registerKill(Player player, String victimType) {
        DetailedStats stats = getStats(player);
        stats.totalKills++;

        if (victimType.equals("Player")) {
            stats.playerKills++;
        } else {
            stats.mobKills++;
        }

        updateStats(player, stats);

        // Verificar logros
        checkAchievements(player, stats);
    }

    /**
     * Registra una muerte.
     */
    public static void registerDeath(Player player, String killer) {
        DetailedStats stats = getStats(player);
        stats.totalDeaths++;

        if (killer.equals("Player")) {
            stats.deathsByPlayer++;
        } else {
            stats.deathsByMob++;
        }

        updateStats(player, stats);
    }

    /**
     * Registra tiempo de juego.
     */
    public static void addPlayTime(Player player, int minutes) {
        DetailedStats stats = getStats(player);
        stats.playTimeMinutes += minutes;
        updateStats(player, stats);
    }

    /**
     * Registra bloque colocado.
     */
    public static void registerBlockPlaced(Player player, String blockType) {
        DetailedStats stats = getStats(player);
        stats.blocksPlaced++;
        stats.blockTypesCounts.put(
            blockType,
            stats.blockTypesCounts.getOrDefault(blockType, 0) + 1
        );
        updateStats(player, stats);
    }

    /**
     * Muestra estadísticas al jugador.
     */
    public static void showStats(Player player) {
        DetailedStats stats = getStats(player);

        player.sendMessage("§6=== Tus Estadísticas ===");
        player.sendMessage("§eKills: §f" + stats.totalKills +
            " §7(Players: " + stats.playerKills +
            ", Mobs: " + stats.mobKills + ")");
        player.sendMessage("§eDeaths: §f" + stats.totalDeaths +
            " §7(K/D: " + String.format("%.2f", stats.getKDRatio()) + ")");
        player.sendMessage("§ePlay Time: §f" + formatPlayTime(stats.playTimeMinutes));
        player.sendMessage("§eBloques Colocados: §f" + stats.blocksPlaced);
        player.sendMessage("§eBloques Rotos: §f" + stats.blocksBroken);
    }

    private static void checkAchievements(Player player, DetailedStats stats) {
        if (stats.totalKills == 100 && !stats.achievements.contains("centurion")) {
            stats.achievements.add("centurion");
            player.sendMessage("§6✦ ¡Logro Desbloqueado: Centurión! §7(100 kills)");
        }

        if (stats.playTimeMinutes >= 1000 && !stats.achievements.contains("veteran")) {
            stats.achievements.add("veteran");
            player.sendMessage("§6✦ ¡Logro Desbloqueado: Veterano! §7(1000 min jugados)");
        }
    }

    private static String formatPlayTime(int minutes) {
        int hours = minutes / 60;
        int mins = minutes % 60;
        return hours + "h " + mins + "m";
    }

    private static DetailedStats getStats(Player player) {
        return player.getMetaStore().getMetaObject(STATS_KEY);
    }

    private static void updateStats(Player player, DetailedStats stats) {
        player.getMetaStore().putMetaObject(STATS_KEY, stats);
    }

    /**
     * Stats detalladas con múltiples campos.
     */
    public static class DetailedStats {
        public int totalKills = 0;
        public int playerKills = 0;
        public int mobKills = 0;
        public int totalDeaths = 0;
        public int deathsByPlayer = 0;
        public int deathsByMob = 0;
        public int playTimeMinutes = 0;
        public int blocksPlaced = 0;
        public int blocksBroken = 0;
        public Map<String, Integer> blockTypesCounts = new HashMap<>();
        public List<String> achievements = new ArrayList<>();

        public double getKDRatio() {
            return totalDeaths == 0 ? totalKills : (double) totalKills / totalDeaths;
        }

        /**
         * Codec complejo para serializar todos los campos.
         */
        public static final Codec<DetailedStats> CODEC =
            Codec.INT.flatMap(totalKills ->
                Codec.INT.flatMap(playerKills ->
                    Codec.INT.flatMap(mobKills ->
                        Codec.INT.flatMap(totalDeaths ->
                            Codec.INT.flatMap(deathsByPlayer ->
                                Codec.INT.flatMap(deathsByMob ->
                                    Codec.INT.flatMap(playTimeMinutes ->
                                        Codec.INT.flatMap(blocksPlaced ->
                                            Codec.INT.flatMap(blocksBroken ->
                                                new MapCodec<>(Codec.STRING, Codec.INT).flatMap(blockTypes ->
                                                    new ListCodec<>(Codec.STRING).map(achievements -> {
                                                        DetailedStats stats = new DetailedStats();
                                                        stats.totalKills = totalKills;
                                                        stats.playerKills = playerKills;
                                                        stats.mobKills = mobKills;
                                                        stats.totalDeaths = totalDeaths;
                                                        stats.deathsByPlayer = deathsByPlayer;
                                                        stats.deathsByMob = deathsByMob;
                                                        stats.playTimeMinutes = playTimeMinutes;
                                                        stats.blocksPlaced = blocksPlaced;
                                                        stats.blocksBroken = blocksBroken;
                                                        stats.blockTypesCounts = new HashMap<>(blockTypes);
                                                        stats.achievements = new ArrayList<>(achievements);
                                                        return stats;
                                                    })
                                                )
                                            )
                                        )
                                    )
                                )
                            )
                        )
                    )
                )
            );
    }
}

Ejemplo 2: Inventario Custom

package com.ejemplo.mimod.systems;

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.codecs.array.ListCodec;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.meta.MetaKey;
import com.hypixel.hytale.server.core.meta.MetaRegistry;
import java.util.ArrayList;
import java.util.List;

public class CustomInventorySystem {

    private static final MetaRegistry<Player> REGISTRY = new MetaRegistry<>();
    private static final int MAX_SLOTS = 27;

    private static final MetaKey<CustomInventory> INVENTORY_KEY =
        REGISTRY.registerMetaObject(
            player -> new CustomInventory(),
            true,
            "mimod:custom_inventory",
            CustomInventory.CODEC
        );

    /**
     * Abre el inventario custom del jugador.
     */
    public static void openInventory(Player player) {
        CustomInventory inventory = getInventory(player);

        // Mostrar GUI
        // (Simplificado - requiere integración con sistema de UI)
        player.sendMessage("§a=== Inventario Mágico ===");
        for (int i = 0; i < inventory.items.size(); i++) {
            CustomItem item = inventory.items.get(i);
            if (item != null) {
                player.sendMessage("§e[" + i + "] §f" +
                    item.itemId + " x" + item.quantity);
            }
        }
    }

    /**
     * Agrega un item al inventario.
     */
    public static boolean addItem(Player player, String itemId, int quantity) {
        CustomInventory inventory = getInventory(player);

        // Buscar slot con el mismo item
        for (CustomItem item : inventory.items) {
            if (item != null && item.itemId.equals(itemId)) {
                item.quantity += quantity;
                updateInventory(player, inventory);
                return true;
            }
        }

        // Buscar slot vacío
        for (int i = 0; i < MAX_SLOTS; i++) {
            if (i >= inventory.items.size() || inventory.items.get(i) == null) {
                CustomItem newItem = new CustomItem(itemId, quantity);
                if (i >= inventory.items.size()) {
                    inventory.items.add(newItem);
                } else {
                    inventory.items.set(i, newItem);
                }
                updateInventory(player, inventory);
                return true;
            }
        }

        // Inventario lleno
        player.sendMessage("§c¡Inventario lleno!");
        return false;
    }

    /**
     * Remueve un item del inventario.
     */
    public static boolean removeItem(Player player, String itemId, int quantity) {
        CustomInventory inventory = getInventory(player);

        for (CustomItem item : inventory.items) {
            if (item != null && item.itemId.equals(itemId)) {
                if (item.quantity >= quantity) {
                    item.quantity -= quantity;
                    if (item.quantity == 0) {
                        inventory.items.remove(item);
                    }
                    updateInventory(player, inventory);
                    return true;
                } else {
                    return false; // Cantidad insuficiente
                }
            }
        }

        return false; // Item no encontrado
    }

    /**
     * Verifica si el jugador tiene un item.
     */
    public static boolean hasItem(Player player, String itemId, int quantity) {
        CustomInventory inventory = getInventory(player);

        int total = 0;
        for (CustomItem item : inventory.items) {
            if (item != null && item.itemId.equals(itemId)) {
                total += item.quantity;
            }
        }

        return total >= quantity;
    }

    private static CustomInventory getInventory(Player player) {
        return player.getMetaStore().getMetaObject(INVENTORY_KEY);
    }

    private static void updateInventory(Player player, CustomInventory inventory) {
        player.getMetaStore().putMetaObject(INVENTORY_KEY, inventory);
    }

    /**
     * Inventario custom con lista de items.
     */
    public static class CustomInventory {
        public List<CustomItem> items;

        public CustomInventory() {
            this.items = new ArrayList<>();
        }

        public CustomInventory(List<CustomItem> items) {
            this.items = items;
        }

        public static final Codec<CustomInventory> CODEC =
            new ListCodec<>(CustomItem.CODEC).map(
                items -> new CustomInventory(items)
            );
    }

    /**
     * Item custom en el inventario.
     */
    public static class CustomItem {
        public String itemId;
        public int quantity;

        public CustomItem(String itemId, int quantity) {
            this.itemId = itemId;
            this.quantity = quantity;
        }

        public static final Codec<CustomItem> CODEC =
            Codec.STRING.flatMap(itemId ->
                Codec.INT.map(quantity ->
                    new CustomItem(itemId, quantity)
                )
            );
    }
}

Ejemplo 3: Sistema de Quests

package com.ejemplo.mimod.systems;

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.codecs.array.ListCodec;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.meta.MetaKey;
import com.hypixel.hytale.server.core.meta.MetaRegistry;
import java.util.ArrayList;
import java.util.List;

public class QuestSystem {

    private static final MetaRegistry<Player> REGISTRY = new MetaRegistry<>();

    private static final MetaKey<QuestData> QUEST_KEY =
        REGISTRY.registerMetaObject(
            player -> new QuestData(),
            true,
            "mimod:quests",
            QuestData.CODEC
        );

    /**
     * Inicia una quest para el jugador.
     */
    public static void startQuest(Player player, String questId) {
        QuestData data = getQuestData(player);

        // Verificar si ya está completada
        if (data.completedQuests.contains(questId)) {
            player.sendMessage("§c¡Ya completaste esta quest!");
            return;
        }

        // Verificar si ya está activa
        for (ActiveQuest quest : data.activeQuests) {
            if (quest.questId.equals(questId)) {
                player.sendMessage("§c¡Esta quest ya está activa!");
                return;
            }
        }

        // Iniciar quest
        ActiveQuest newQuest = new ActiveQuest(questId, 0, false);
        data.activeQuests.add(newQuest);
        updateQuestData(player, data);

        player.sendMessage("§a✦ Quest iniciada: §f" + getQuestName(questId));
    }

    /**
     * Avanza el progreso de una quest.
     */
    public static void progressQuest(Player player, String questId, int progress) {
        QuestData data = getQuestData(player);

        for (ActiveQuest quest : data.activeQuests) {
            if (quest.questId.equals(questId)) {
                quest.progress += progress;

                // Verificar si se completó
                int requiredProgress = getQuestRequiredProgress(questId);
                if (quest.progress >= requiredProgress) {
                    completeQuest(player, questId);
                } else {
                    player.sendMessage("§eProgreso de quest: §f" +
                        quest.progress + "/" + requiredProgress);
                }

                updateQuestData(player, data);
                return;
            }
        }
    }

    /**
     * Completa una quest.
     */
    public static void completeQuest(Player player, String questId) {
        QuestData data = getQuestData(player);

        // Remover de activas
        data.activeQuests.removeIf(q -> q.questId.equals(questId));

        // Agregar a completadas
        if (!data.completedQuests.contains(questId)) {
            data.completedQuests.add(questId);
        }

        updateQuestData(player, data);

        // Dar recompensa
        giveQuestReward(player, questId);

        player.sendMessage("§a✦✦✦ Quest Completada: §f" + getQuestName(questId));
    }

    /**
     * Muestra quests activas del jugador.
     */
    public static void showActiveQuests(Player player) {
        QuestData data = getQuestData(player);

        if (data.activeQuests.isEmpty()) {
            player.sendMessage("§7No tienes quests activas");
            return;
        }

        player.sendMessage("§6=== Quests Activas ===");
        for (ActiveQuest quest : data.activeQuests) {
            int required = getQuestRequiredProgress(quest.questId);
            player.sendMessage("§e" + getQuestName(quest.questId) +
                " §7[" + quest.progress + "/" + required + "]");
        }
    }

    private static void giveQuestReward(Player player, String questId) {
        // Dar recompensas según la quest
        switch (questId) {
            case "kill_10_zombies":
                // Dar 100 monedas, 50 XP
                player.sendMessage("§a+100 monedas, +50 XP");
                break;
            case "mine_50_ores":
                // Dar pico especial
                player.sendMessage("§a+1 Pico Mágico");
                break;
        }
    }

    private static String getQuestName(String questId) {
        // Mapear IDs a nombres
        return switch (questId) {
            case "kill_10_zombies" -> "Exterminar Zombies";
            case "mine_50_ores" -> "Minero Experto";
            case "explore_dungeon" -> "Explorador de Mazmorras";
            default -> questId;
        };
    }

    private static int getQuestRequiredProgress(String questId) {
        return switch (questId) {
            case "kill_10_zombies" -> 10;
            case "mine_50_ores" -> 50;
            case "explore_dungeon" -> 1;
            default -> 100;
        };
    }

    private static QuestData getQuestData(Player player) {
        return player.getMetaStore().getMetaObject(QUEST_KEY);
    }

    private static void updateQuestData(Player player, QuestData data) {
        player.getMetaStore().putMetaObject(QUEST_KEY, data);
    }

    /**
     * Datos de quests del jugador.
     */
    public static class QuestData {
        public List<ActiveQuest> activeQuests;
        public List<String> completedQuests;

        public QuestData() {
            this.activeQuests = new ArrayList<>();
            this.completedQuests = new ArrayList<>();
        }

        public QuestData(List<ActiveQuest> activeQuests, List<String> completedQuests) {
            this.activeQuests = activeQuests;
            this.completedQuests = completedQuests;
        }

        public static final Codec<QuestData> CODEC =
            new ListCodec<>(ActiveQuest.CODEC).flatMap(activeQuests ->
                new ListCodec<>(Codec.STRING).map(completedQuests ->
                    new QuestData(activeQuests, completedQuests)
                )
            );
    }

    /**
     * Quest activa con progreso.
     */
    public static class ActiveQuest {
        public String questId;
        public int progress;
        public boolean tracked;

        public ActiveQuest(String questId, int progress, boolean tracked) {
            this.questId = questId;
            this.progress = progress;
            this.tracked = tracked;
        }

        public static final Codec<ActiveQuest> CODEC =
            Codec.STRING.flatMap(questId ->
                Codec.INT.flatMap(progress ->
                    Codec.BOOLEAN.map(tracked ->
                        new ActiveQuest(questId, progress, tracked)
                    )
                )
            );
    }
}

Mejores Prácticas

1. Siempre Proporcionar Factory

// ✅ BIEN - Factory que crea valor inicial
MetaKey<Integer> key = registry.registerMetaObject(
    player -> 0
);

// ❌ MAL - null puede causar NPE
MetaKey<Integer> key = registry.registerMetaObject(
    player -> null
);

2. Usar Codecs Apropiados

// ✅ BIEN - Codec completo para serialización
public static final Codec<MyData> CODEC =
    Codec.INT.flatMap(field1 ->
        Codec.STRING.map(field2 ->
            new MyData(field1, field2)
        )
    );

// ❌ MAL - Falta codec para datos persistentes
// (Causará error al intentar guardar)

3. Cleanup de Datos

public class CleanupExample {

    private static final Map<Player, CustomData> CACHE = new ConcurrentHashMap<>();

    /**
     * Limpia datos cuando un jugador se desconecta.
     */
    public static void onPlayerDisconnect(Player player) {
        // Guardar datos finales
        savePlayerData(player);

        // Limpiar cache
        CACHE.remove(player);

        System.out.println("[Cleanup] Cleaned data for player");
    }
}

4. Validar Datos Cargados

public static PlayerData getPlayerData(Player player) {
    var metaStore = player.getMetaStore();
    PlayerData data = metaStore.getMetaObject(DATA_KEY);

    // Validar datos cargados
    if (data == null || !data.isValid()) {
        // Crear datos nuevos si están corruptos
        data = new PlayerData();
        metaStore.putMetaObject(DATA_KEY, data);
    }

    return data;
}

Debugging

Logging de Metadata

public static void debugMetadata(Player player) {
    var metaStore = player.getMetaStore();

    System.out.println("=== Metadata for player ===");

    // Iterar sobre todas las keys
    metaStore.forEachMetaObject((id, value) -> {
        System.out.println("  ID " + id + ": " + value);
    });
}

Verificar Persistencia

// Guardar
player.getMetaStore().putMetaObject(KEY, data);

// Forzar guardado a disco (si aplica)
// player.save();

// Recargar jugador y verificar
// Player reloaded = loadPlayer(player.getUUID());
// Data reloadedData = reloaded.getMetaStore().getMetaObject(KEY);
// assert reloadedData.equals(data);

Recursos Adicionales


¿Problemas? Consulta Troubleshooting