概要
- Curios APIというスロットを簡単に追加できるAPIを使ってエリトラ専用のスロットを作ろう!
やること
その1
build.gradleファイルを編集します。
publishingとminecraft 以外 のブロックに記載します
repositoryに書くもの
maven {
name='Curios'
url 'https://maven.theillusivec4.top/'
}
dependenciesに書くもの
implementation fg.deobf("top.theillusivec4.curios:curios-forge:1.18.1-5.0.6.2")
compileOnly fg.deobf("top.theillusivec4.curios:curios-forge:1.18.1-5.0.6.2:api")
リロードを忘れずに行いましょう。
その2
mods.tomlに依存関係を記載します。
[[dependencies.examplemod]]
modId="curios"
mandatory=true
versionRange="[1.18.1-5.0.6.2]"
ordering="NONE"
side="BOTH"
その3
savesディレクトリからアクセスできるserverconfigディレクトリを編集します。
curios-server.tomlという名前のファイルを作成し、以下のように記述していきます。
identifierはコード上で識別したり、この後で記述するjsonファイルに使用します。
sizeは同じタイプのスロットを何個割り当てるかを決めます。基本1で良いかと思います。
iconはスロットのアイコンを任意の画像に設定できます!(assets/curios/textures/slotというディレクトリ構造を作り、slotディレクトリ内にテクスチャの画像を格納しましょう)
[[curiosSettings]]
identifier = "elytra"
size = 1
icon = "curios:slot/elytra_blank"
スロットの追加を行いたい場合は、同じフォーマットで下に追加していくことで対応できます。
その4
resourcesディレクトリにあるdataディレクトリにcurios-tags-itemsのディレクトリ構造を作成し、configで指定したidentifierの名前と同じのjsonファイルを作成します。
内容は以下とします。
replaceパラメータはアイテムタグの読み込み方法を指定します。デフォルトはfalseであり、同じ名前のjsonファイルが存在している場合はvaluesを追加していく動作を行います。
trueにすると上書きされます。
valuesにはスロットに置くことのできるアイテムを指定します。
{
"replace": false,
"values": [
"minecraft:elytra"
]
}
その5
アイテムを置けるようにはなりましたが、このままではそのアイテムの機能を使用することができません。(今回だと空中でスペースボタンを押してもエリトラの機能はまだ適用されない)
適宜イベントハンドラで監視して適用していきます。
追加したスロットにエリトラを適用させると、マイクラ側では普通に落下の処理が走ってしまうようなので、LivingFallEventを発火させて、落下ダメージをキャンセルする処理を挟んでいます。
package com.example.examplemod.EditHere;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ElytraItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingFallEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import top.theillusivec4.curios.api.CuriosApi;
import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@Mod.EventBusSubscriber
public class PlayerEventHandler {
private static final Map<UUID, Boolean> wasFlyingMap=new HashMap<>();
@SubscribeEvent
public static void onLivingFall(LivingFallEvent event)
{
if(event.getEntityLiving() instanceof Player player)
{
if(hasCuriosElytra(player))
{
event.setDistance(0);
event.setCanceled(true);
}
}
}
@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event)
{
Player player= event.player;
UUID playerId=player.getUUID();
if(event.phase==TickEvent.Phase.START)
{
wasFlyingMap.put(playerId,player.isFallFlying());
return;
}
if(event.phase!=TickEvent.Phase.END)
{
return;
}
if(!hasCuriosElytra(player))
{
wasFlyingMap.remove(playerId);
return;
}
boolean wasFlying=wasFlyingMap.getOrDefault(playerId,false);
if(!player.isOnGround() && !player.isInWater()) {
if (wasFlying && !player.isFallFlying()) {
player.startFallFlying();
}
}else {
wasFlyingMap.put(playerId,false);
}
if(player.isOnGround())
{
player.stopFallFlying();
}
if(player.level.isClientSide)
{
handleClientInput(player);
}
}
@OnlyIn(Dist.CLIENT)
private static void handleClientInput(Player player) {
if (player.isFallFlying()) {
return;
}
if(player.getDeltaMovement().y>=0)
{
return;
}
if (!player.isOnGround() && !player.isInWater()) {
if(hasCuriosElytra(player))
{
if (Minecraft.getInstance().options.keyJump.isDown())
{
player.startFallFlying();
}
}
}
}
private static boolean hasCuriosElytra(Player player) {
LazyOptional<ICuriosItemHandler> curiousHandlerOptional = CuriosApi.getCuriosHelper().getCuriosHandler(player);
return curiousHandlerOptional
.map(handler -> handler.getStacksHandler("elytra")
.map(stacks -> {
ItemStack stack = stacks.getStacks().getStackInSlot(0);
return stack.is(Items.ELYTRA) && ElytraItem.isFlyEnabled(stack);
})
.orElse(false))
.orElse(false);
}
}
装備したアイテムを描画する
外部APIを使ってスロットを追加しているため、そのままではアイテムが描画(レンダリング)されません。このままでは味気ないので、描画するコードも追加していきましょう。
しっかりAPIの方で技術が提供されています。
ICurioRendererというクラスを実装して、FMLClientSetupEventの部分で登録すればできるようです。
package com.example.examplemod.EditHere;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.ElytraModel;
import net.minecraft.client.model.EntityModel;
import net.minecraft.client.model.geom.EntityModelSet;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import top.theillusivec4.curios.api.SlotContext;
import top.theillusivec4.curios.api.client.ICurioRenderer;
public class CuriosAPIRenderer implements ICurioRenderer {
// エリトラのモデルを保持する変数
private final ElytraModel<LivingEntity> elytraModel;
// デフォルトのエリトラのテクスチャ
private static final ResourceLocation ELYTRA_LOCATION=new ResourceLocation("textures/entity/elytra.png");
public CuriosAPIRenderer(){
// コンストラクタでバニラのエリトラモデルレイヤーを取得して初期化
EntityModelSet modelSet= Minecraft.getInstance().getEntityModels();
this.elytraModel=new ElytraModel<>(modelSet.bakeLayer(ModelLayers.ELYTRA));
}
@Override
public <T extends LivingEntity, M extends EntityModel<T>> void render(ItemStack itemStack,
SlotContext slotContext,
PoseStack poseStack,
RenderLayerParent<T, M> renderLayerParent,
MultiBufferSource multiBufferSource,
int i, float v, float v1,
float v2, float v3, float v4, float v5) {
// 1. スロットからこのレンダラーが呼ばれた際、アイテムがエリトラであることを確認
if(itemStack.is(Items.ELYTRA))
{
// 2. 親モデル(プレイヤーの体)の動きに合わせて位置を調整
// これによって体の動き(スニークなど)にエリトラが追従する
ICurioRenderer.translateIfSneaking(poseStack, slotContext.entity());
// 3. 微調整
poseStack.translate(0.0D, 0.0D, 0.125D);
// 4. モデルの設定(セットアップ)
// プレイヤーのモデル情報をエリトラモデルにコピーして同期させる
@SuppressWarnings("unchecked")
EntityModel<LivingEntity> parentModel=(EntityModel<LivingEntity>) renderLayerParent.getModel();
parentModel.copyPropertiesTo( this.elytraModel);
this.elytraModel.setupAnim(slotContext.entity(),v,v1,v3,v4,v5);
// 5. テクスチャの設定
// プレイヤーが特定のマントなどを持っている場合はそのテクスチャを使うロジック
ResourceLocation texture=ELYTRA_LOCATION;
if(slotContext.entity() instanceof AbstractClientPlayer player)
{
if(player.isElytraLoaded() && player.getElytraTextureLocation() != null)
{
texture=player.getElytraTextureLocation();
}
}
// 6. 描画処理
VertexConsumer vertexConsumer= ItemRenderer.getArmorFoilBuffer(
multiBufferSource,
RenderType.armorCutoutNoCull(texture),
false,
itemStack.hasFoil()// エンチャントによる発光があれば適用
);
this.elytraModel.renderToBuffer(poseStack,vertexConsumer,i, OverlayTexture.NO_OVERLAY, 1.0F, 1.0F, 1.0F,1.0F);
}
}
}
コンストラクタでエリトラのモデルを受け取っておいて、renderメソッドで実際にレンダリングします。
((EntityModel) renderLayerParent.getModel()の部分については、Javaのジェネリクス(などの型指定)の仕様上、コンパイラが「LivingEntity(エリトラモデルが想定している型)」と「T(現在のレンダラーが扱っている型)」が完全に一致するかコンパイル時点では保証できないため警告が出るのですが、Minecraftのレンダリングにおいて、T は必ず LivingEntity を継承しているため、実行時にエラー(ClassCastException)になることは考えられないため、アノテーションで警告を消しておきます。)
最後に登録クラスのFMLClientSetupEventの部分に以下のコードを追加します。
private void doClientStuff(final FMLClientSetupEvent event) {
CuriosRendererRegistry.register(Items.ELYTRA, CuriosAPIRenderer::new);
}