Implementação e eventos
Implementação e eventos descreve a superfície de hooks que o DragonMineZ oferece aos addons: seus eventos, como ler o estado do jogador, os registros de extensão e onde conectar sua própria rede e seus próprios registros. Leia quando seu addon já compilar contra o DMZ (veja Primeiros passos com addons).
Escutar um evento do DMZ
Todos os eventos de servidor e comuns são classes estáticas aninhadas de com.dragonminez.common.events.DMZEvent, publicadas no barramento de eventos padrão do Forge (MinecraftForge.EVENT_BUS). Cada classe aninhada estende diretamente o Event do Forge (o próprio DMZEvent é só o contêiner), então inscreva-se na classe aninhada específica:
@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()
}
}
}
Os eventos marcados abaixo como canceláveis têm a anotação @Cancelable do Forge: event.setCanceled(true) bloqueia a ação subjacente. Os campos marcados como editáveis têm um setter (setTpGain, setAmount, ...), e o DMZ usa o valor resultante depois que todos os listeners rodam. Todos os getters seguem os nomes dos campos (getPlayer(), getOldValue(), ...); campos booleanos usam is... (isParry(), isBlocked()).
Lista de eventos
Origem: src/main/java/com/dragonminez/common/events/DMZEvent.java.
Estatísticas, recursos e progresso
| Evento | Cancelável | Disparado quando | Campos principais |
|---|---|---|---|
StatChangeEvent |
Sim | Uma estatística (STR, SKP, RES, VIT, PWR ou ENE) muda por uma chamada a Stats.set*(): comandos, itens, menus, e também quando as estatísticas são carregadas ou copiadas (tanto no servidor quanto no cliente). |
player, stat (STRENGTH, STRIKE_POWER, RESISTANCE, VITALITY, KI_POWER ou ENERGY), oldValue, newValue
|
TPGainEvent |
Sim | Antes que uma quantidade positiva de pontos de treino seja adicionada (Resources.addTrainingPoints). |
player, oldValue, tpGain (editável), shareWithParty (editável); auxiliar getNewTpsValue()
|
KiChargeEvent |
Sim | Uma vez por segundo enquanto o jogador está carregando ki ativamente. Cancele-o para impedir o ganho de ki daquele segundo. | player, currentEnergy, maxEnergy; auxiliar isEnergyFull()
|
HealthRegenEvent / EnergyRegenEvent / StaminaRegenEvent |
Sim | Antes de aplicar a regeneração por segundo de vida, ki ou estâmina. Eles compartilham a base ResourceRegenEvent. |
player, statsData, amount (editável)
|
FormChangeEvent / StackFormChangeEvent |
Não | Depois que a forma de raça ou a forma acumulável do jogador muda (lado do servidor). | player, oldGroup, oldForm, newGroup, newForm; auxiliares isTransform() e isUntransform()
|
PlayerDataSaveEvent |
Não | Os dados do jogador estão prestes a ser gravados no backend de armazenamento JSON ou DATABASE. Você pode adicionar suas próprias chaves a data. |
player, data
|
PlayerDataLoadEvent |
Não | Os dados do jogador foram lidos do backend JSON ou DATABASE, logo antes de serem aplicados ao jogador. |
player, data
|
StatChangeEvent também é disparado enquanto o DMZ carrega os dados salvos e aplica as sincronizações do cliente. Se você cancelá-lo, verifique antes event.getPlayer().level().isClientSide() e suas próprias condições, ou pode impedir que as estatísticas salvas de um jogador sejam carregadas.PlayerDataSaveEvent e PlayerDataLoadEvent não são disparados com o backend NBT padrão, em que o vanilla salva os dados do DMZ junto com o arquivo do jogador. Veja Configuração do servidor para as opções de storage.
Combate
| Evento | Cancelável | Disparado quando | Campos principais |
|---|---|---|---|
DamageModifyEvent |
Sim | Cálculo do dano antes da mitigação, para um golpe dado por um jogador (golpes corpo a corpo e Ataques de Golpe). Cancele-o para não causar dano do DMZ. | attacker, victim, amount (editável), defensePenetration (editável), sourceType (MELEE ou STRIKE)
|
DamageDealtEvent |
Não | Depois que o bloqueio de um golpe corpo a corpo de um jogador é resolvido (quantidade antes da mitigação). | attacker, victim, amount, blocked, parried, sourceType
|
PlayerDamageMitigatedEvent |
Não | Depois que o DMZ termina de mitigar um golpe recebido por um jogador. Somente leitura: não altere o dano aqui. | victim, source (DamageSource), rawDamage, defenseMitigated, blockMitigated, finalDamage
|
PlayerBlockEvent |
Sim | O jogador bloqueia ou faz parry de um ataque com sucesso. Cancele-o e o jogador recebe o golpe inteiro. | victim, attacker, originalDamage, finalDamage (editável), isParry (editável), poiseDamage (editável)
|
BarrierAbsorbEvent |
Não | Uma barreira de ki (a técnica de barreira ou a barreira dos Androides) absorve um golpe antes que ele chegue à entidade protegida. | owner (quem criou a barreira), protectedEntity, attacker, amount
|
PlayerDashEvent |
Sim | Antes que uma esquiva seja executada. | player, dashType (NORMAL ou DOUBLE), distance (editável), kiCost (editável)
|
PlayerEvasionEvent |
Sim | Antes que uma evasão seja ativada. | player, attacker, originalDamage, kiCost (editável)
|
CritChanceEvent |
Não | Enquanto a chance crítica de um jogador é calculada. | player, chance (editável, de 0 a 1)
|
KiAttackCastEvent |
Não | Um jogador começa a carregar (lança) um ataque de ki. | player, statsData, kiAttack
|
KiAttackFireEvent |
Não | Um ataque de ki carregado é disparado. | player, statsData, kiAttack, chargeMultiplier, cooldownTicks (editável, mínimo 1)
|
StrikeAttackCastEvent |
Não | Um jogador inicia um Ataque de Golpe. | player, statsData, strike
|
StrikeAttackFireEvent |
Não | Um Ataque de Golpe acerta e começa sobre um alvo. | player, statsData, strike, target
|
DamageSourceType também contém um valor KI; na v2.2, os ataques de ki não publicam DamageModifyEvent nem DamageDealtEvent.
Fusões e dragões
| Evento | Cancelável | Disparado quando | Campos principais |
|---|---|---|---|
FusionEvent |
Sim | Antes de uma tentativa de fusão. O DMZ o publica para as fusões METAMORU, POTHALA e BEETLE (Besouro Medi-Bug); o enum também contém ABSORPTION e ASSIMILATION. |
initiator, target, type
|
DragonSummonedEvent |
Não | Depois que um dragão é invocado com um conjunto de esferas completo. | player, level, position, dragonDefinition, ballSetDefinition, consumedPositions; auxiliares getDragonId() e getBallSetId()
|
Veja Fusões e Dragões e desejos para a parte de jogabilidade.
Missões
Todos os eventos de missões estendem QuestLifecycleEvent e compartilham seus campos: player, questKey (sagaId:questId para missões de saga, o id em texto para missões secundárias), saga (null para missões secundárias), quest e partyMembers.
| Evento | Cancelável | Disparado quando | Campos extras |
|---|---|---|---|
QuestStartEvent |
Sim | Antes que uma missão seja aceita. | difficulty (editável)
|
QuestObjectiveProgressEvent |
Sim | Antes que o progresso de um objetivo seja salvo. | objectiveIndex, oldProgress, newProgress (editável), objectiveRequired
|
QuestFailEvent |
Sim | Antes que uma missão seja marcada como falha. | reason (PLAYER_DEATH, FORCED_RESET, SCRIPT, TIME_EXPIRED, ESCORT_FAILED)
|
QuestTurnInEvent |
Sim | Antes que a entrega de uma missão seja aplicada. | npcId
|
QuestRewardClaimEvent |
Sim | Antes que uma recompensa individual seja resgatada. | rewardIndex
|
QuestCompletedEvent |
Não | Quando uma missão é concluída. | Só os campos compartilhados |
Eventos de cliente
Os eventos só de cliente ficam em com.dragonminez.client.events.DMZClientEvent e são publicados no barramento do Forge do cliente físico. Registre esses listeners com value = Dist.CLIENT. Nenhum deles é cancelável e eles são apenas informativos: o servidor continua decidindo o que de fato acontece.
| Evento | Disparado quando | Campos principais |
|---|---|---|
PlayerAttackStart |
O jogador local começa a preparação do ataque. | player, attackHand
|
PlayerAttackHit |
O ataque do jogador local resolve seus alvos (zero ou mais). | player, attackHand, targets, cursorTarget (pode ser null)
|
KiAttackCast |
O jogador local começa a carregar o ataque de ki de um slot. | player, slot
|
KiAttackRelease |
O jogador local dispara um ataque de ki carregado. | player
|
StrikeAttack |
O jogador local inicia um Ataque de Golpe. | player, targetId
|
Ler o estado do jogador
Obtenha a capability de qualquer Player (um jogador do servidor, ou o jogador local no cliente; o DMZ mantém a cópia do cliente sincronizada):
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) é um atalho para player.getCapability(StatsCapability.INSTANCE); os dois retornam um LazyOptional<StatsData>. Os getters do DMZ são gerados pelo Lombok em tempo de compilação, então você não precisa do Lombok no seu addon para chamá-los.
Leituras úteis de StatsData:
| Getter | Retorna |
|---|---|
getCharacter().getRaceName() / getCharacterClass() / getGender() |
Raça, classe e gênero |
getCharacter().getActiveForm() / getActiveFormGroup() |
Forma de raça atual (um texto vazio na forma base) |
getCharacter().getActiveStackForm() / getActiveStackFormGroup() |
Forma acumulável atual (por exemplo, o nível de Kaioken) |
getStats().getStrength() / getStrikePower() / getResistance() / getVitality() / getKiPower() / getEnergy() |
Valores base das estatísticas |
getResources().getCurrentEnergy() / getCurrentStamina() / getCurrentPoise() / getAlignment() |
Reservas de recursos atuais e índole |
getMaxHealth() / getMaxEnergy() / getMaxStamina() / getMaxPoise() |
Limites de recursos calculados |
getBattlePower() / getMeleeDamage() / getStrikeDamage() / getKiDamage() / getDefense() |
Estatísticas de combate calculadas |
getLevel() |
Nível de jogador calculado |
getPlayerQuestData() |
Progresso de missões (getQuestStatus(questKey), isQuestAccepted(questKey), isQuestCompleted(questKey), isSagaLocked(sagaId), getTrackedQuestId(), getDifficulty())
|
getSkills() |
Níveis de habilidades (hasSkill(name), getSkillLevel(name))
|
getStatus(), getEffects(), getTechniques(), getBonusStats() |
Indicadores de status, efeitos ativos, técnicas de ki e de golpe, modificadores de estatísticas extras |
Os cálculos de raça, forma e estatísticas leem a configuração ao vivo toda vez, então os valores sempre refletem a configuração atual do servidor (inclusive depois de /dmzreload).
Alterar o estado com segurança
Os setters de estatísticas do próprio DMZ (por exemplo Stats.setStrength()) publicam primeiro um evento cancelável e só aplicam a mudança se ele não for cancelado:
DMZEvent.StatChangeEvent event = new DMZEvent.StatChangeEvent(player, StatType.STRENGTH, oldValue, newValue);
if (!MinecraftForge.EVENT_BUS.post(event)) {
// apply the change
}
No seu addon, prefira reagir aos eventos (alterando seus campos editáveis ou cancelando-os) a chamar diretamente os setters do DMZ. Nem todo setter valida ou publica um evento (a maioria dos setters de Resources, por exemplo, só limita o valor e o aplica sem evento), então passar pelos eventos é o único ponto de integração com estabilidade garantida.
Se você alterar dados do DMZ a partir do servidor, faça isso na thread do servidor e depois envie ao cliente uma cópia atualizada, do mesmo jeito que o DMZ faz:
NetworkHandler.sendToTrackingEntityAndSelf(new StatsSyncS2C(serverPlayer), serverPlayer);
StatsSyncS2C envia todos os dados do jogador; o DMZ também usa ProgressionSyncS2C (estatísticas, estatísticas extras, habilidades, técnicas, dados de missões) e ResourceSyncS2C (recursos e status) para atualizações menores.
Objetivos e recompensas de missão personalizados
Os addons podem adicionar novos tipos de objetivos e recompensas de missões que os arquivos JSON de missões podem usar depois, sem mexer no parser do DMZ. Registre-os durante o setup do mod (por exemplo, em FMLCommonSetupEvent) nos dois lados: o cliente reconstrói as missões a partir do registro sincronizado com as mesmas 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()));
O contrato:
- As classes de objetivos estendem
QuestObjectivee chamam o construtorQuestObjective(String customType, int required); as classes de recompensas estendemQuestReward, chamamQuestReward(String customType)e implementamgiveReward(ServerPlayer)egetDescription(). Os objetivos também precisam implementarcheckProgress(Object... params). - No JSON das missões, use sua chave como
"type"(por exemplo{"type": "MEDITATE", "seconds": 60}). As chaves de tipo não diferenciam maiúsculas de minúsculas (são guardadas em maiúsculas). Trate-as como nomes de registro: não as renomeie depois que as missões passarem a usá-las. - O escritor de sincronização precisa gravar todos os campos que sua fábrica lê, porque o cliente lê de novo o JSON sincronizado com a sua fábrica.
- Tipos desconhecidos são reportados no relatório de carregamento de JSON (veja Encontrar erros de JSON) e o objetivo ou a recompensa é ignorado.
- O progresso é controlado pelo servidor e orientado a eventos: escute seus próprios eventos de jogo no servidor e salve o progresso com
PlayerQuestData.setObjectiveProgress(questKey, objectiveIndex, progress), e depois sincronize comProgressionSyncS2C. UseDMZEvent.QuestObjectiveProgressEventeQuestCompletedEventpara reagir às missões incluídas.
Veja Missões personalizadas para o formato JSON das missões.
Placeholders de texto personalizados
Os textos de missões, diálogos e NPCs aceitam %placeholders% como %player%, %race%, %class%, %level%, %alignment%, %deaths%, %form% e %dimension%. Adicione os seus durante o setup do mod:
DMZTextPlaceholders.register("zeni", (player, data) -> String.valueOf(MyAddon.getZeni(player)));
// "%zeni%" in quest/dialogue text now resolves to your value
As chaves não diferenciam maiúsculas de minúsculas. O resolvedor roda no lado que renderiza o texto (geralmente o cliente) e precisa lidar com um StatsData null.
Botões do Menu de Utilidades
O Menu de Utilidades (radial) do cliente tem dois slots livres, à esquerda e à direita da roda, reservados para addons. Adicione um botão no setup do 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
}
});
Os slots são preenchidos na ordem de registro; registros além dos dois slots livres não são mostrados. Sobrescreva hasRightClickAction para retornar true se o seu botão também reagir ao clique direito. IUtilityMenuSlot e ButtonInfo ficam em com.dragonminez.client.gui.utilitymenu.
Armaduras e camadas de renderização
- Armaduras do seu addon: a classe de item de armadura do DMZ,
DbzArmorItem(common/init/armor), tem um construtorDbzArmorItem(material, type, properties, modId, itemId). Passe seu própriomodIde as texturas são lidas do seu próprio namespace:assets/<modId>/textures/armor/<itemId>_layer1.png(_layer2para as calças), mais as opcionais<itemId>_damaged_layer1.pnge_damaged_layer2.png, usadas quando o item perde mais da metade da durabilidade. - Outras armaduras: os
ArmorItemcomuns de outros mods são desenhados no modelo de jogador do DMZ com sua textura de armadura normal. - Camadas de renderização de terceiros: as camadas de renderização que outros mods adicionam ao renderizador de jogador vanilla são repassadas para o modelo de jogador do DMZ. As camadas vanilla e as camadas de armaduras cosméticas são ignoradas, e nada é repassado enquanto o jogador é um Grande Macaco.
Rede
O canal do DMZ é NetworkHandler.INSTANCE (um SimpleChannel do Forge em dragonminez:network). Não registre seus próprios pacotes nele: o DMZ atribui os ids de pacote pela ordem de registro, então qualquer coisa que você adicionar deslocaria os ids do DMZ ou colidiria com eles. Em vez disso, registre seu próprio SimpleChannel no seu addon e use o do DMZ como referência do padrão (messageBuilder, codificador, decodificador e handler, com uma direção explícita).
Você pode usar os helpers de envio do DMZ (sendToPlayer, sendToTrackingEntityAndSelf, ...) para enviar os pacotes de sincronização do próprio DMZ, como mostrado acima.
Também nunca confie no cliente nos seus próprios pacotes: valide no servidor tudo o que afeta estatísticas, desbloqueios, missões, desejos ou progresso.
Não existe um evento de "dados sincronizados" no lado do cliente. No cliente, leia a capability quando precisar dela (por exemplo, durante a renderização) em vez de tentar interceptar os pacotes do DMZ.
Usar os registros do DMZ
Importe diretamente os contêineres de registro Main* em vez de escrever as resource locations à mão:
import com.dragonminez.common.init.MainItems;
import com.dragonminez.common.init.MainAttributes;
Item redCapsule = MainItems.RED_CAPSULE.get();
Attribute strength = MainAttributes.STRENGTH.get();
Disponíveis em common/init/: MainItems, MainBlocks, MainBlockEntities, MainEntities, MainAttributes, MainEffects, MainEnchants, MainPotions, MainSounds, MainParticles, MainFluids, MainMenus, MainRecipes, MainTabs, MainTags, MainVillagers, MainDamageTypes, MainGameRules, MainLootModifiers.
Padrão de dependência opcional
Se o seu addon precisa funcionar com ou sem o DMZ instalado, siga o padrão que o próprio DMZ usa nas integrações opcionais (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);
}
}
}
- Verifique
ModList.get().isLoaded("dragonminez")uma única vez e guarde o resultado em cache. - Mantenha todos os imports do DMZ dentro de classes que só são usadas depois dessa verificação (incluindo os listeners de eventos do DMZ: registre-os manualmente e só quando o DMZ estiver carregado).
- Proteja com essa flag toda chamada que mexa no DMZ, com um comportamento alternativo seguro ou um valor padrão quando ela for
false. - Registre no log uma mensagem clara de informação ou de aviso em qualquer caso, para que os donos de servidores vejam de relance se a integração está ativa.
Próximo passo
Veja Criar um addon de DMZ para a configuração completa do projeto Gradle e a declaração da dependência no mods.toml.