Implementación y eventos

De DragonMineZ Wiki
Revisión del 06:50 14 oct 2026 de Shokkoh (discusión | contribs.) (Translation sync from dragonminez-wiki repository)
(difs.) ← Revisión anterior | Revisión actual (difs.) | Revisión siguiente → (difs.)

Implementación y eventos describe la superficie de hooks que DragonMineZ ofrece a los addons: sus eventos, cómo leer el estado del jugador, los registros de extensión y dónde conectar tu propia red y tus propios registros. Léela cuando tu addon ya compile contra DMZ (consulta Primeros pasos con addons).

Escuchar un evento de DMZ

Todos los eventos de servidor y comunes son clases estáticas anidadas de com.dragonminez.common.events.DMZEvent, publicadas en el bus de eventos estándar de Forge (MinecraftForge.EVENT_BUS). Cada clase anidada extiende directamente el Event de Forge (DMZEvent en sí solo es el contenedor), así que suscríbete a la clase anidada concreta:

@Mod.EventBusSubscriber(modid = "your_addon_modid", bus = Mod.EventBusSubscriber.Bus.FORGE)
public class DmzHooks {
    @SubscribeEvent
    public static void onTpGain(DMZEvent.TPGainEvent event) {
        int boosted = Math.round(event.getTpGain() * 1.10f);
        event.setTpGain(boosted);
    }

    @SubscribeEvent
    public static void onFormChange(DMZEvent.FormChangeEvent event) {
        if (event.isTransform()) {
            // player transformed into event.getNewGroup() / event.getNewForm()
        }
    }
}

Los eventos marcados abajo como cancelables llevan la anotación @Cancelable de Forge: event.setCanceled(true) bloquea la acción subyacente. Los campos marcados como modificables tienen un setter (setTpGain, setAmount, ...), y DMZ usa el valor resultante después de que se ejecuten todos los listeners. Todos los getters siguen los nombres de los campos (getPlayer(), getOldValue(), ...); los campos booleanos usan is... (isParry(), isBlocked()).

Lista de eventos

Origen: src/main/java/com/dragonminez/common/events/DMZEvent.java.

Estadísticas, recursos y progreso

Evento Cancelable Se dispara cuando Campos clave
StatChangeEvent Sí Una estadística (STR, SKP, RES, VIT, PWR o ENE) cambia mediante una llamada a Stats.set*(): comandos, objetos, menús, y también cuando se cargan o copian las estadísticas (tanto en el servidor como en el cliente). player, stat (STRENGTH, STRIKE_POWER, RESISTANCE, VITALITY, KI_POWER o ENERGY), oldValue, newValue
TPGainEvent Sí Antes de que se añada una cantidad positiva de puntos de entrenamiento (Resources.addTrainingPoints). player, oldValue, tpGain (modificable), shareWithParty (modificable); auxiliar getNewTpsValue()
KiChargeEvent Sí Una vez por segundo mientras el jugador está cargando ki activamente. Cancélalo para detener la ganancia de ki de ese segundo. player, currentEnergy, maxEnergy; auxiliar isEnergyFull()
HealthRegenEvent / EnergyRegenEvent / StaminaRegenEvent Sí Antes de aplicar la regeneración por segundo de vida, ki o stamina. Comparten la base ResourceRegenEvent. player, statsData, amount (modificable)
FormChangeEvent / StackFormChangeEvent No Después de que cambie la forma de raza o la forma acumulable del jugador (lado del servidor). player, oldGroup, oldForm, newGroup, newForm; auxiliares isTransform() y isUntransform()
PlayerDataSaveEvent No Los datos del jugador están a punto de escribirse en el backend de almacenamiento JSON o DATABASE. Puedes añadir tus propias claves a data. player, data
PlayerDataLoadEvent No Los datos del jugador se leyeron del backend JSON o DATABASE, justo antes de aplicarse al jugador. player, data
StatChangeEvent y la carga de datos StatChangeEvent también se dispara mientras DMZ carga los datos guardados y aplica las sincronizaciones del cliente. Si lo cancelas, comprueba antes event.getPlayer().level().isClientSide() y tus propias condiciones, o podrías impedir que se carguen las estadísticas guardadas de un jugador.
Backends de almacenamiento PlayerDataSaveEvent y PlayerDataLoadEvent no se disparan con el backend NBT por defecto, en el que vanilla guarda los datos de DMZ junto con el archivo del jugador. Consulta Configuración del servidor para ver los ajustes de storage.

Combate

Evento Cancelable Se dispara cuando Campos clave
DamageModifyEvent Sí Cálculo del daño antes de la mitigación, para un golpe de un jugador (golpes cuerpo a cuerpo y Super Golpes). Cancélalo para no hacer daño de DMZ. attacker, victim, amount (modificable), defensePenetration (modificable), sourceType (MELEE o STRIKE)
DamageDealtEvent No Después de resolver el bloqueo de un golpe cuerpo a cuerpo de un jugador (cantidad antes de la mitigación). attacker, victim, amount, blocked, parried, sourceType
PlayerDamageMitigatedEvent No Después de que DMZ termine de mitigar un golpe recibido por un jugador. Solo lectura: no cambies el daño aquí. victim, source (DamageSource), rawDamage, defenseMitigated, blockMitigated, finalDamage
PlayerBlockEvent Sí El jugador bloquea o hace parry a un ataque con éxito. Cancélalo y el jugador recibe el golpe completo. victim, attacker, originalDamage, finalDamage (modificable), isParry (modificable), poiseDamage (modificable)
BarrierAbsorbEvent No Una barrera de ki (la técnica de barrera o la barrera de los Androides) absorbe un golpe antes de que llegue a la entidad protegida. owner (quien creó la barrera), protectedEntity, attacker, amount
PlayerDashEvent Sí Antes de que se ejecute un dash. player, dashType (NORMAL o DOUBLE), distance (modificable), kiCost (modificable)
PlayerEvasionEvent Sí Antes de que se active una evasión. player, attacker, originalDamage, kiCost (modificable)
CritChanceEvent No Mientras se calcula la probabilidad crítica de un jugador. player, chance (modificable, de 0 a 1)
KiAttackCastEvent No Un jugador empieza a cargar (lanza) un ataque de ki. player, statsData, kiAttack
KiAttackFireEvent No Se suelta un ataque de ki cargado. player, statsData, kiAttack, chargeMultiplier, cooldownTicks (modificable, mínimo 1)
StrikeAttackCastEvent No Un jugador inicia un Super Golpe. player, statsData, strike
StrikeAttackFireEvent No Un Super Golpe conecta y empieza sobre un objetivo. player, statsData, strike, target

DamageSourceType también contiene un valor KI; en la v2.2, los ataques de ki no publican DamageModifyEvent ni DamageDealtEvent.

Fusiones y dragones

Evento Cancelable Se dispara cuando Campos clave
FusionEvent Sí Antes de un intento de fusión. DMZ lo publica para las fusiones METAMORU, POTHALA y BEETLE (Escarabajo Medi-Bug); el enum también contiene ABSORPTION y ASSIMILATION. initiator, target, type
DragonSummonedEvent No Después de invocar un dragón con un conjunto de esferas completo. player, level, position, dragonDefinition, ballSetDefinition, consumedPositions; auxiliares getDragonId() y getBallSetId()

Consulta Fusiones y Dragones y deseos para ver la parte de jugabilidad.

Misiones

Todos los eventos de misiones extienden QuestLifecycleEvent y comparten sus campos: player, questKey (sagaId:questId para las misiones de saga, el id de texto para las misiones secundarias), saga (null para las misiones secundarias), quest y partyMembers.

Evento Cancelable Se dispara cuando Campos extra
QuestStartEvent Sí Antes de aceptar una misión. difficulty (modificable)
QuestObjectiveProgressEvent Sí Antes de guardar el progreso de un objetivo. objectiveIndex, oldProgress, newProgress (modificable), objectiveRequired
QuestFailEvent Sí Antes de marcar una misión como fallida. reason (PLAYER_DEATH, FORCED_RESET, SCRIPT, TIME_EXPIRED, ESCORT_FAILED)
QuestTurnInEvent Sí Antes de aplicar la entrega de una misión. npcId
QuestRewardClaimEvent Sí Antes de reclamar una recompensa individual. rewardIndex
QuestCompletedEvent No Cuando se completa una misión. Solo los campos compartidos

Eventos de cliente

Los eventos solo de cliente están en com.dragonminez.client.events.DMZClientEvent y se publican en el bus de Forge del cliente físico. Registra estos listeners con value = Dist.CLIENT. Ninguno es cancelable y son solo informativos: el servidor sigue decidiendo lo que realmente ocurre.

Evento Se dispara cuando Campos clave
PlayerAttackStart El jugador local empieza la preparación del ataque. player, attackHand
PlayerAttackHit El ataque del jugador local resuelve sus objetivos (cero o más). player, attackHand, targets, cursorTarget (puede ser null)
KiAttackCast El jugador local empieza a cargar el ataque de ki de una ranura. player, slot
KiAttackRelease El jugador local suelta un ataque de ki cargado. player
StrikeAttack El jugador local inicia un Super Golpe. player, targetId

Leer el estado del jugador

Obtén la capability de cualquier Player (un jugador del servidor, o el jugador local en el cliente; DMZ mantiene sincronizada la copia del cliente):

StatsProvider.get(StatsCapability.INSTANCE, player).ifPresent(data -> {
    String race = data.getCharacter().getRaceName();
    String activeForm = data.getCharacter().getActiveForm();
    float currentKi = data.getResources().getCurrentEnergy();
    float maxKi = data.getMaxEnergy();
    int level = data.getLevel();
});

StatsProvider.get(cap, entity) es un atajo de player.getCapability(StatsCapability.INSTANCE); ambos devuelven un LazyOptional<StatsData>. Lombok genera los getters de DMZ en tiempo de compilación, así que no necesitas Lombok en tu addon para llamarlos.

Lecturas útiles de StatsData:

Getter Devuelve
getCharacter().getRaceName() / getCharacterClass() / getGender() Raza, clase y género
getCharacter().getActiveForm() / getActiveFormGroup() Forma de raza actual (un texto vacío en la forma base)
getCharacter().getActiveStackForm() / getActiveStackFormGroup() Forma acumulable actual (por ejemplo, el nivel de Kaioken)
getStats().getStrength() / getStrikePower() / getResistance() / getVitality() / getKiPower() / getEnergy() Valores base de las estadísticas
getResources().getCurrentEnergy() / getCurrentStamina() / getCurrentPoise() / getAlignment() Reservas de recursos actuales y alineamiento
getMaxHealth() / getMaxEnergy() / getMaxStamina() / getMaxPoise() Topes de recursos calculados
getBattlePower() / getMeleeDamage() / getStrikeDamage() / getKiDamage() / getDefense() Estadísticas de combate calculadas
getLevel() Nivel de jugador calculado
getPlayerQuestData() Progreso de misiones (getQuestStatus(questKey), isQuestAccepted(questKey), isQuestCompleted(questKey), isSagaLocked(sagaId), getTrackedQuestId(), getDifficulty())
getSkills() Niveles de habilidades (hasSkill(name), getSkillLevel(name))
getStatus(), getEffects(), getTechniques(), getBonusStats() Indicadores de estado, efectos activos, técnicas de ki y de golpe, modificadores de estadísticas extra

Los cálculos de raza, forma y estadísticas leen la configuración en vivo cada vez, así que los valores siempre reflejan la configuración actual del servidor (también después de /dmzreload).

Cambiar el estado de forma segura

Los setters de estadísticas propios de DMZ (por ejemplo Stats.setStrength()) publican primero un evento cancelable y solo aplican el cambio si no se canceló:

DMZEvent.StatChangeEvent event = new DMZEvent.StatChangeEvent(player, StatType.STRENGTH, oldValue, newValue);
if (!MinecraftForge.EVENT_BUS.post(event)) {
    // apply the change
}

En tu addon, es preferible reaccionar a los eventos (cambiando sus campos modificables o cancelándolos) que llamar directamente a los setters de DMZ. No todos los setters validan o publican un evento (la mayoría de los setters de Resources, por ejemplo, solo limitan el valor y lo aplican sin evento), así que pasar por los eventos es el único punto de integración garantizado que se mantendrá estable.

Si cambias datos de DMZ desde el servidor, hazlo en el hilo del servidor y después envía al cliente una copia actualizada, igual que hace DMZ:

NetworkHandler.sendToTrackingEntityAndSelf(new StatsSyncS2C(serverPlayer), serverPlayer);

StatsSyncS2C envía todos los datos del jugador; DMZ también usa ProgressionSyncS2C (estadísticas, estadísticas extra, habilidades, técnicas, datos de misiones) y ResourceSyncS2C (recursos y estado) para actualizaciones más pequeñas.

Objetivos y recompensas de misión personalizados

Véase también: Misiones personalizadas

Los addons pueden añadir nuevos tipos de objetivos y recompensas de misiones que luego pueden usar los archivos JSON de misiones, sin tocar el parser de DMZ. Regístralos durante la configuración del mod (por ejemplo, en FMLCommonSetupEvent) en ambos lados: el cliente reconstruye las misiones a partir del registro sincronizado con las mismas fábricas.

QuestObjectiveRegistry.register("MEDITATE",
    json -> new MeditateObjective(json.get("seconds").getAsInt()),                                 // parse quest JSON
    (objective, out) -> out.addProperty("seconds", ((MeditateObjective) objective).getSeconds()),  // client sync fields
    objective -> Component.translatable("myaddon.quest.meditate.obj"));                            // HUD/journal text

QuestRewardRegistry.register("MY_CURRENCY",
    json -> new MyCurrencyReward(json.get("amount").getAsInt()),
    (reward, out) -> out.addProperty("amount", ((MyCurrencyReward) reward).getAmount()));

El contrato:

  • Las clases de objetivos extienden QuestObjective y llaman al constructor QuestObjective(String customType, int required); las clases de recompensas extienden QuestReward, llaman a QuestReward(String customType) e implementan giveReward(ServerPlayer) y getDescription(). Los objetivos también deben implementar checkProgress(Object... params).
  • En el JSON de las misiones, usa tu clave como "type" (por ejemplo {"type": "MEDITATE", "seconds": 60}). Las claves de tipo no distinguen mayúsculas de minúsculas (se guardan en mayúsculas). Trátalas como nombres de registro: no las renombres una vez que las misiones las usen.
  • El escritor de sincronización debe escribir todos los campos que lee tu fábrica, porque el cliente vuelve a analizar el JSON sincronizado con tu fábrica.
  • Los tipos desconocidos se informan en el reporte de carga de JSON (consulta Encontrar errores de JSON) y el objetivo o la recompensa se omite.
  • El progreso lo controla el servidor y se basa en eventos: escucha tus propios eventos de juego en el servidor y guarda el progreso con PlayerQuestData.setObjectiveProgress(questKey, objectiveIndex, progress), y luego sincroniza con ProgressionSyncS2C. Usa DMZEvent.QuestObjectiveProgressEvent y QuestCompletedEvent para reaccionar a las misiones incluidas.

Consulta Misiones personalizadas para ver el formato JSON de las misiones.

Placeholders de texto personalizados

Los textos de misiones, diálogos y NPCs admiten %placeholders% como %player%, %race%, %class%, %level%, %alignment%, %deaths%, %form% y %dimension%. Añade los tuyos durante la configuración del mod:

DMZTextPlaceholders.register("zeni", (player, data) -> String.valueOf(MyAddon.getZeni(player)));
// "%zeni%" in quest/dialogue text now resolves to your value

Las claves no distinguen mayúsculas de minúsculas. El resolvedor se ejecuta en el lado que renderiza el texto (normalmente el cliente) y debe manejar un StatsData null.

Botones del menú de utilidades

El menú de utilidades (radial) del cliente tiene dos ranuras libres, a la izquierda y a la derecha de la rueda, reservadas para addons. Añade un botón desde la configuración del cliente:

UtilityMenuScreen.addMenuSlot(new IUtilityMenuSlot() {
    @Override
    public ButtonInfo render(StatsData statsData) {
        return new ButtonInfo(Component.literal("My Addon"), Component.literal("Open"));
    }

    @Override
    public void handle(StatsData statsData, boolean rightClick) {
        // client side: open your screen or send your own packet to the server
    }
});

Las ranuras se llenan en orden de registro; los registros que superen las dos ranuras libres no se muestran. Sobrescribe hasRightClickAction para que devuelva true si tu botón también reacciona al clic derecho. IUtilityMenuSlot y ButtonInfo están en com.dragonminez.client.gui.utilitymenu.

Armaduras y capas de renderizado

  • Armaduras de tu addon: la clase de objeto de armadura de DMZ, DbzArmorItem (common/init/armor), tiene un constructor DbzArmorItem(material, type, properties, modId, itemId). Pasa tu propio modId y las texturas se leen de tu propio namespace: assets/<modId>/textures/armor/<itemId>_layer1.png (_layer2 para las grebas), más las opcionales <itemId>_damaged_layer1.png y _damaged_layer2.png, que se usan cuando el objeto ha perdido más de la mitad de su durabilidad.
  • Otras armaduras: los ArmorItem normales de otros mods se dibujan sobre el modelo de jugador de DMZ con su textura de armadura habitual.
  • Capas de renderizado de terceros: las capas de renderizado que otros mods añaden al renderizador de jugador vanilla se trasladan al modelo de jugador de DMZ. Se omiten las capas vanilla y las capas de armaduras cosméticas, y no se traslada nada mientras el jugador es un Oozaru.

Red

El canal de DMZ es NetworkHandler.INSTANCE (un SimpleChannel de Forge en dragonminez:network). No registres tus propios paquetes en él: DMZ asigna los ids de paquete por orden de registro, así que cualquier cosa que añadas desplazaría los ids de DMZ o chocaría con ellos. En su lugar, registra tu propio SimpleChannel en tu addon y usa el de DMZ como referencia del patrón (messageBuilder, codificador, decodificador y manejador, con una dirección explícita).

Puedes usar los helpers de envío de DMZ (sendToPlayer, sendToTrackingEntityAndSelf, ...) para enviar los paquetes de sincronización propios de DMZ, como se muestra arriba.

Tampoco confíes nunca en el cliente en tus propios paquetes: valida en el servidor todo lo que afecte a estadísticas, desbloqueos, misiones, deseos o progreso.

No existe un evento de "datos sincronizados" en el lado del cliente. En el cliente, lee la capability cuando la necesites (por ejemplo, al renderizar) en lugar de intentar interceptar los paquetes de DMZ.

Usar los registros de DMZ

Importa directamente los contenedores de registro Main* en lugar de escribir a mano las resource locations:

import com.dragonminez.common.init.MainItems;
import com.dragonminez.common.init.MainAttributes;

Item redCapsule = MainItems.RED_CAPSULE.get();
Attribute strength = MainAttributes.STRENGTH.get();

Disponibles en common/init/: MainItems, MainBlocks, MainBlockEntities, MainEntities, MainAttributes, MainEffects, MainEnchants, MainPotions, MainSounds, MainParticles, MainFluids, MainMenus, MainRecipes, MainTabs, MainTags, MainVillagers, MainDamageTypes, MainGameRules, MainLootModifiers.

Patrón de dependencia opcional

Si tu addon debe funcionar con o sin DMZ instalado, sigue el patrón que usa DMZ para sus propias integraciones opcionales (common/compat/WeaponLevelingCompat.java):

public final class DmzCompat {
    private static Boolean loaded;

    public static boolean isLoaded() {
        if (loaded == null) loaded = ModList.get().isLoaded("dragonminez");
        return loaded;
    }

    public static int getLevel(Player player) {
        if (!isLoaded()) return 0;   // safe fallback without DMZ
        return Impl.getLevel(player);
    }

    // Only this nested class imports DMZ classes, so it is never loaded without DMZ.
    private static final class Impl {
        static int getLevel(Player player) {
            return StatsProvider.get(StatsCapability.INSTANCE, player).map(StatsData::getLevel).orElse(0);
        }
    }
}
  1. Comprueba ModList.get().isLoaded("dragonminez") una sola vez y guarda el resultado en caché.
  2. Mantén todos los imports de DMZ dentro de clases que solo se tocan después de esa comprobación (incluidos los listeners de eventos de DMZ: regístralos a mano y solo cuando DMZ esté cargado).
  3. Protege con ese indicador toda llamada que toque DMZ, con un comportamiento alternativo seguro o un valor por defecto cuando sea false.
  4. Registra en el log un mensaje claro de información o de advertencia en ambos casos, para que los dueños de servidores vean de un vistazo si la integración está activa.

Siguiente paso

Consulta Crear un addon de DMZ para la configuración completa del proyecto de Gradle y la declaración de la dependencia en mods.toml.