Implementação e eventos

From DragonMineZ Wiki
This page is a translated version of the page Implementation and Events and the translation is 100% complete.

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 e o carregamento de dados 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.
Backends de armazenamento 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 QuestObjective e chamam o construtor QuestObjective(String customType, int required); as classes de recompensas estendem QuestReward, chamam QuestReward(String customType) e implementam giveReward(ServerPlayer) e getDescription(). Os objetivos também precisam implementar checkProgress(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 com ProgressionSyncS2C. Use DMZEvent.QuestObjectiveProgressEvent e QuestCompletedEvent para 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 construtor DbzArmorItem(material, type, properties, modId, itemId). Passe seu próprio modId e as texturas são lidas do seu próprio namespace: assets/<modId>/textures/armor/<itemId>_layer1.png (_layer2 para as calças), mais as opcionais <itemId>_damaged_layer1.png e _damaged_layer2.png, usadas quando o item perde mais da metade da durabilidade.
  • Outras armaduras: os ArmorItem comuns 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);
        }
    }
}
  1. Verifique ModList.get().isLoaded("dragonminez") uma única vez e guarde o resultado em cache.
  2. 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).
  3. Proteja com essa flag toda chamada que mexa no DMZ, com um comportamento alternativo seguro ou um valor padrão quando ela for false.
  4. 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.