ラベル forge の投稿を表示しています。 すべての投稿を表示
ラベル forge の投稿を表示しています。 すべての投稿を表示

2026年5月10日日曜日

Minecraft サーバに Timeless and Classics Zero Guns (TaCZ) をインストールする方法

Minecraft サーバに Timeless and Classics Zero Guns (TaCZ) をインストールする方法

概要

サーバにインストールしクライアント側にも同じ Mod のインストールが必要です
必ずバージョンは揃えましょう

環境

  • サーバ側
    • macOS 26.4.1
    • Minecraft 1.20.1
    • forge 47.4.10
    • TaCZ 1.1.8
  • クライアント側
    • macOS 26.4.1/Windows11Home
    • Minecraft 1.20.1
    • forge 47.4.10
    • TaCZ 1.1.8

サーバ側

compose.yaml

サーバ側は docker compose で起動します

  • vim compose.yaml
services:
  mc:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      MODE: "creative"
      VERSION: "1.21.11"
    volumes:
      # attach the relative directory 'data' to the container's /data path
      - ./data:/data

  mc_mod:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    ports:
      - "25566:25565"
    environment:
      EULA: "TRUE"
      MODE: "creative"
      VERSION: "1.20.1"
      TYPE: "forge"
    volumes:
      # modded world/data directory for the modded server
      - ./modded-data:/data
      # put the CurseForge mod JAR into this local directory
      - ./timeless_and_classics_zero/mods:/data/mods
      # optional config directory for mod/server configs
      - ./timeless_and_classics_zero/config:/data/config

通常のマイクラサーバとは別で TaCZ がインストールされた forge サーバを起動します

TaCZ のダウンロードとインストール

https://www.curseforge.com/minecraft/mc-mods/timeless-and-classics-zero

から tacz-1.20.1-1.1.8-release.jar をダウンロードします
サーバ側は timeless_and_classics_zero/mods 配下に配置します

  • mkdir timeless_and_classics_zero/mods
  • mkdir timeless_and_classics_zero/config
  • mkdir modded-data

起動確認

  • docker compose up -d

で問題なく起動することを確認します

クライアント側 (Mac)

forge のインストール

https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html

から forge-1.20.1-47.4.10-installer.jar をダウンロードしダブルクリックでインストールします
デフォルトだと開けないのでプライバシーとセキュリティから jar を開きます

TaCZ のダウンロードとインストール

https://www.curseforge.com/minecraft/mc-mods/timeless-and-classics-zero

から tacz-1.20.1-1.1.8-release.jar をダウンロードします
Mac の場合のクライアントは以下のパスに Mod を配置します

/Users/username/Library/Application Support/minecraft/mods

起動確認

ランチャーを起動し1.20.1-forge-47.4.10 の起動構成を作成してその起動構成からマイクラを起動しましょう

あとはマルチプレイから起動した docker compose のサーバにアクセスすれば OK です

クライアント側 (Windows)

JDK のインストール

Oracle 版でも OpenJDK でも OK です

https://www.oracle.com/jp/java/technologies/downloads/

コマンドをプロンプトを起動して java コマンドで実行できるか確認します

forge のインストール

https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html

から forge-1.20.1-47.4.10-installer.jar をダウンロードします
ダブルクリックだとインストールできないのでコマンドプロンプトを開いて起動します

  • java -jar forge-1.20.1-47.4.10-installer.jar

TaCZ のダウンロードとインストール

https://www.curseforge.com/minecraft/mc-mods/timeless-and-classics-zero

から tacz-1.20.1-1.1.8-release.jar をダウンロードします
Windows の場合のクライアントは以下のパスに Mod を配置します

C:\Users\xxx\AppData\Roaming\.minecraft\mods

起動確認

ランチャーを起動し1.20.1-forge-47.4.10 の起動構成を作成してその起動構成からマイクラを起動しましょう

あとはマルチプレイから起動した docker compose のサーバにアクセスすれば OK です

最後に

マルチで Mod を使って遊ぶ場合にはサーバ側とクライアント側に同じバージョンの同じ Mod をインストールする必要があるので注意しましょう

2025年2月7日金曜日

forge Mod をアンインストールする方法

forge Mod をアンインストールする方法

概要

アンインストーラはないので手動で削除するしかないです

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

Minecraft launcher から起動構成を削除する

Mod フォルダの削除

/Users/user01/Library/Application Support/minecraft/versions にある 1.20.6-forge-50.1.0 を削除します

ライブラリの削除

/Users/user01/Library/Application Support/minecraft/libraries にある net/minecraftforge/ を削除します

たぶんそれでも足りない

他の名前空間のライブラリも配置してるっぽいのですべてのファイルを確実に削除はできていないです
確実に削除したいのであればインストール時にログが残るのでそこに記載されている jar ファイルもすべて削除しましょう

一番確実なのは launcher ごと再インストールする

他の Mod もセーブデータもすべて削除されていいのであれば launcher ごと再インストールするのがいいかなと思います
セーブデータもなくなる可能性があるので慎重に行いましょう

  • アプリケーションにある Minecraft launcher の削除
  • /Users/user01/Library/Application Support/minecraft/saves のバックアップ
  • /Users/user01/Library/Application Support/minecraft/ ディレクトリの削除
  • https://www.minecraft.net/ja-jp/store/minecraft-java-bedrock-edition-pc ここにログインしてインストーラーをダウンロードし再インストール

最後に

もっと簡単に forge Mod だけを削除する方法はないのだろうか

2025年2月6日木曜日

Minecraft サーバーに forge MOD をインストールし更に自作の mod を動かす方法

Minecraft サーバーに forge MOD をインストールし更に自作の mod を動かす方法

概要

forge Mod をサーバ側に導入する方法と更にサーバ側に自作の mod をインストールする方法を紹介します

環境

  • macOS 15.2
  • docker 27.4.0
  • itzg/minecraft-server 2025.1.0
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

compose.yaml

ポイントは TYPE: "FORGE" の追加です
これで起動するとサーバ側は forge のインストールが始まり forge MOD がインストールされた状態で起動します

forge MOD を使う際は VERSION と FORGE_VERSION を指定したほうがいいです
指定しない場合は最新が自動でインストールされますがこれから導入する mod が最新版に対応していないケースもあるのでバージョンはどちらも指定したほうがいいです

services:
  mc:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      MODE: "creative"
      TYPE: "FORGE"
      VERSION: "1.20.6"
      FORGE_VERSION: "50.1.32"
    volumes:
      - ./data:/data

自作 mod の導入

今回は「クライアント mod」を導入します
mod にはクライアント mod の場合はサーバ側にもクライアント側にも mod を導入する必要があるので注意してください

サーバ側は ./data/mods/examplemod-1.0.0.jar に配置すれば docker compose up -d 時に自動でコンテナ側に mod が配置され有効になります
サーバ側のログを確認して mod のログが流れていれば OK です

あとはクライアント側の Minecraft にも examplemod-1.0.0.jar を導入する必要があります
forge をインストールし mod 配置用のパスに jar を配置すれば OK です

macOS の Java 版 Minecraft に forge MOD をインストールする場合に jar のダブルクリックからだと起動しないことがあります
その場合はシステム設定のプライバシーとセキュリティからブロックされている jar を開けば OK です
クライアント側の mod のパスは /Users/user01/Library/Application Support/minecraft/mods がデフォルトです

最後に

ポイントはサーバ側とクライアント側の状況を同じにする点です
マイクラ自体のバージョンと forge のバージョンも合わせる必要があるので結構大変です

サーバ専用の Mod であればクライアント側に同じ Mod をインストールする必要はありませんがカスタムブロックなどの Mod はすべてクライアント Mod なのでサーバ側とクライアント側に Mod をインストール必要が出てきます

参考サイト

2025年2月5日水曜日

forge Mod を最新版にマイグレーションする方法

forge Mod を最新版にマイグレーションする方法

概要

マインクラフトのバージョンを 1.20.6 から 1.21.4 に上げ forge Mod のバージョンを 50.1.0 から 54.0.18 にマイグレーションしてみたのでその方法を紹介します

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

最新版の MDK のダウンロード

執筆時点での最新版は https://files.minecraftforge.net/net/minecraftforge/forge/index_1.21.4.html ここにあるのでダウンロードします
デフォルトだと安定版をダウンロードすることになっているので最新版をダウンロードしたい場合にはバージョンを指定しましょう

バックアップ

MDK の作業ディレクトリをバックアップしておきましょう
git なりで管理していればすぐに戻せるのでおすすめです
git など SCM で管理していない場合はディレクトリごとコピーしましょう

src ディレクトリの移動

基本的には自分で実装したファイルを最新の MDK に展開するだけです

  • cp -ipr src /tmp
  • cp README.txt /tmp

など自分で編集したファイルをどこかに移動しましょう
gradle.properties や build.gradle なども編集している場合は移動しておきましょう

一旦すべて空にする

MDK の作業ディレクトリを一旦すべて空にしましょう

  • rm -rf *

新しい MDK を展開する

ダウンロードした zip ファイルを展開します

  • mv ~/Downloads/forge-1.21.4-54.0.18-mdk.zip .

展開できたら移動していたファイルを戻します

  • cp -ipr /tmp/src .
  • cp /tmp/README.txt .

差分を確認する

git diff などで差分を確認します

  • 元のやつに戻したい場合は git checkout や手動で追記します
  • BOM 文字が入っているファイルがあるので削除します (vim で set nobomb)
  • 改行コードが CRLF になっているのを LF に修正します (nkf -Lu --overwrite build.gradle)

gradle.properties にはバージョン情報が記載されているので必ず差分がでます

ちなみに今回の 50.1.0 から 54.0.18 のマイグレーションでは「changelog.txt」と「gradle.properties」のみ差分が出たので最悪手動でも何とかなったかもしれません

  • git diff gradle.properties
diff --git a/gradle.properties b/gradle.properties
index 6c23fa8..810b037 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,13 +7,13 @@ org.gradle.daemon=false
 ## Environment Properties
 
 # The Minecraft version must agree with the Forge version to get a valid artifact
-minecraft_version=1.20.6
+minecraft_version=1.21.4
 # The Minecraft version range can use any release version of Minecraft as bounds.
 # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
 # as they do not follow standard versioning conventions.
-minecraft_version_range=[1.20.6,1.21)
+minecraft_version_range=[1.21.4,1.22)
 # The Forge version must agree with the Minecraft version to get a valid artifact
-forge_version=50.1.0
+forge_version=54.0.18
 # The Forge version range can use any version of Forge as bounds or match the loader version range
 forge_version_range=[0,)
 # The loader version range can only use the major version of Forge/FML as bounds
@@ -35,7 +35,7 @@ loader_version_range=[0,)
 mapping_channel=official
 # The mapping version to query from the mapping channel.
 # This must match the format required by the mapping channel.
-mapping_version=1.20.6
+mapping_version=1.21.4

動作確認

再度ビルドして問題なく動作すれば OK です

  • ./gradlew clean && ./gradlew build && ./gradlew runData && ./gradlew runClient

コード修正

非互換になったメソッドなどがある場合はコードも修正する必要があります
自分の場合は以下の修正が必要だったので紹介します
基本的には IDE や lsp-java を使っていればエディタ側で警告してくれるのでそれに沿って手動でコードを修正する感じです

src/main/java/com/example/examplemod/Config.java

これは自分は編集してなかったので最新版をそのまま持ってきました

src/main/java/com/example/examplemod/ExampleMod.java

ビルドのエラーと最新版のファイルと古いファイルを見比べつつエラーを解消させるしかありません (このあたりもっと簡単にできないだろうか)

  • public ExampleMod() が引数付きになっていたので修正 public ExampleMod(FMLJavaModLoadingContext context)
    • modEvnetBus の取得方法も変わっていたので修正 IEventBus modEventBus = context.getModEventBus();
    • ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC); -> context.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
  • ResourceLocation がプライベートメソッドになって使えなくなっていた
    • new ResourceLocation("examplemod", "laser_fire") -> ResourceLocation.fromNamespaceAndPath("examplemod", "laser_fire")
  • java.lang.NullPointerException: Block id not set
    • ブロックを定義する際に .setId(BLOCKS.key("example_block")) という感じでブロックIDを定義するようになっている
    • ブロックやアイテムを登録するときに設定するプロパティの挙動はカスタムクラス側ではなく Mod メイン側全部やるようにしたほうがいいかもしれない

src/main/java/com/example/examplemod/LaserGunItem.java

  • net.minecraft.world.InteractionResultHolder の廃止
    • use などのメソッドがだいぶ変わっている
    • InteractionResult.sidedSuccess などもなく InteractionResult.SUCCESS を直接 return するように修正
  • The method hurt(DamageSource, float) from the type Entity is deprecated
    • 以下のように修正
livingEntity.hurtServer(
        (ServerLevel) world,
        livingEntity.damageSources().playerAttack(player),
        amountOfdamage);

テクスチャが当たらない

models がうまく当たらなかったです
lang や sound、blockstates は問題なかったです

トラブルシューティング

今回は特にエラーなくマイグレーションできましたがエラーが起きるケースとして以下があるので注意しましょう

  • Java のバージョンのアップグレード
    • Java21 でもともと開発していたが MDK をアップグレードしたら Java23 が必要になった
    • その場合はビルド時にコケるので MDK のドキュメントと照らし合わせて Java もアップグレードする

キャッシュの削除

  • rm -rf ~/.gradle/caches && rm -rf build/*

最後に

かなり面倒な印象です
しかも forge Mod はかなりのペースで更新されているので更新されるたびにマイグレーションするのは大変かなと思います
メジャーバージョンが上がったりしたタイミングで上げるのがいいかなとは思います

もっと簡単にマイグレーションできる方法はないのでしょうか
もしくは NeoForge に移行するべきなのでしょうか

参考サイト

2025年2月4日火曜日

forge Mod で上空からアイテムを降らす方法

forge Mod で上空からアイテムを降らす方法

概要

リワードなどに使えます

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

サンプルコード

package com.example.examplemod;

import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;

public class LaserGunItem extends Item {
    private float amountOfdamage = 100.0F; // ダメージ量
    private String countKey = "laserGunKills"; // カウントのキー
    private int previousCount = 0;
    private boolean isLocalSent = false;

    public LaserGunItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResultHolder<ItemStack> use(
            Level world, Player player, InteractionHand hand) {
        // クライアント側のみでエフェクトを表示
        if (world.isClientSide()) {
            // ビームエフェクトを生成
            spawnLaserBeam(player);
            if (isLocalSent) {
                sendToastMessage(player, previousCount);
                isLocalSent = false;
            }
        } else {
            // サーバー側でダメージ処理
            fireLaser(world, player);
            // 討伐数の取得
            CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
            int currentCount = tag.getInt(countKey);
            // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
            if (previousCount != 0 && previousCount < currentCount) {
                // クライアント用の変数にも登録
                if (currentCount % 10 == 0) {
                    sendChatMessage(player, currentCount);
                    spawnReward(player, world);
                    // ローカルの通知も許可する、このロジックだと一発分遅れる
                    isLocalSent = true;
                }
            }
            // 一つ前の値を保存
            previousCount = currentCount;
        }
        return InteractionResultHolder.sidedSuccess(
                player.getItemInHand(hand), world.isClientSide());
    }

    @Override
    public void appendHoverText(
            ItemStack stack,
            Item.TooltipContext context,
            List<Component> tooltip,
            TooltipFlag flag) {
        super.appendHoverText(stack, context, tooltip, flag);
        // ダメージ情報を追加
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.damage", amountOfdamage)
                        .withStyle(ChatFormatting.GREEN));

        // 他の情報も追加可能
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.description")
                        .withStyle(ChatFormatting.GRAY));
    }

    @Override
    public boolean onEntityItemUpdate(ItemStack stack, ItemEntity entity) {
        // 特殊な処理が必要であればここで行う(今回は不要なのでデフォルト動作に任せます)
        // アイテムが q で捨てられたときにアイテムとして残すための設定
        return super.onEntityItemUpdate(stack, entity);
    }

    private void spawnLaserBeam(Player player) {
        // クライアント側エフェクト(例:パーティクル)
        player.level()
                .addParticle(
                        ParticleTypes.END_ROD,
                        player.getX(),
                        player.getEyeY(),
                        player.getZ(),
                        player.getLookAngle().x,
                        player.getLookAngle().y,
                        player.getLookAngle().z);
    }

    private void fireLaser(Level world, Player player) {
        // 音の再生
        if (world instanceof ServerLevel serverLevel) {
            serverLevel.playSound(
                    null, // プレイヤー(nullにすると全員が聞こえる)
                    player.blockPosition(), // 再生位置
                    ExampleMod.LASER_FIRE_SOUND.get(), // サウンドイベント
                    SoundSource.PLAYERS, // サウンドの種類(PLAYERS, BLOCKS, AMBIENT など)
                    1.0f, // 音量
                    1.0f // 音の高さ(ピッチ)
                    );
        }
        // 発射用の座標と距離の設定
        Vec3 start = player.getEyePosition(1.0F);
        Vec3 direction = player.getLookAngle();
        double range = 50.0D; // 50ブロック先をターゲット
        Vec3 end = start.add(direction.scale(range));

        // ブロックとのヒット判定
        HitResult blockHit =
                world.clip(
                        new ClipContext(
                                start,
                                end,
                                ClipContext.Block.OUTLINE,
                                ClipContext.Fluid.NONE,
                                player));
        // エンティティとのヒット判定
        EntityHitResult entityHit =
                ProjectileUtil.getEntityHitResult(
                        world,
                        player,
                        start,
                        end,
                        player.getBoundingBox().expandTowards(direction.scale(range)).inflate(1.0D),
                        entity -> entity instanceof LivingEntity && entity != player);
        // 優先的にエンティティを処理
        if (entityHit != null) {
            System.out.println("Hit entity!");
            Entity hitEntity = entityHit.getEntity();
            if (hitEntity instanceof LivingEntity livingEntity) {
                // 敵にダメージを与える
                livingEntity.hurt(
                        livingEntity.damageSources().playerAttack(player), amountOfdamage);
                // 倒したか確認
                if (livingEntity.isDeadOrDying()) {
                    // カウントを増やす
                    incrementKillCount(player);
                }
            }
        } else if (blockHit != null && blockHit.getType() == HitResult.Type.BLOCK) {
            // ブロックにヒットした場合
            System.out.println("Hit block at: " + ((BlockHitResult) blockHit).getBlockPos());
            BlockHitResult blockResult = (BlockHitResult) blockHit;
            BlockPos hitPos = blockResult.getBlockPos();
            // ヒット位置に火をつける例
            world.setBlockAndUpdate(hitPos, Blocks.FIRE.defaultBlockState());
        } else {
            // 何にも当たらなかった場合
            System.out.println("Missed!");
        }
    }

    private void incrementKillCount(Player player) {
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey); // 現在のカウント
        tag.putInt(countKey, currentCount + 1); // カウントを増加
    }

    private void sendChatMessage(Player player, int currentCount) {
        player.sendSystemMessage(
                Component.translatable(
                                "The player %s killed %d mobs with laser gun."
                                        .formatted(player.getName().getString(), currentCount))
                        .withStyle(ChatFormatting.GREEN));
    }

    private void sendToastMessage(Player player, int currentCount) {
        String title = "Good job!";
        String description = "Killed %d mobs with laser gun.".formatted(currentCount);
        Minecraft minecraft = Minecraft.getInstance();
        ToastComponent toastComponent = minecraft.getToasts();
        // トーストを作成
        SystemToast toast =
                new SystemToast(
                        SystemToast.SystemToastId.PERIODIC_NOTIFICATION, // トーストのタイプ(任意の値でOK)
                        Component.literal(title), // タイトル
                        Component.literal(description) // 説明
                        );
        // トーストを表示
        toastComponent.addToast(toast);
    }

    // 報酬を降らせる処理
    private void spawnReward(Player player, Level level) {
        // プレイヤーの位置
        double x = player.getX();
        double y = player.getY() + 20; // プレイヤーの上空20ブロック
        double z = player.getZ();
        for (int i = 0; i < 10; ++i) {
            // 報酬アイテム(例: ダイヤモンド)
            ItemEntity reward = new ItemEntity(level, x, y, z, Items.DIAMOND.getDefaultInstance());
            level.addFreshEntity(reward);
        }
    }
}

ポイント

level.addFreshEntity で指定の座標にアイテムを発生させることができます

// 報酬を降らせる処理
private void spawnReward(Player player, Level level) {
    // プレイヤーの位置
    double x = player.getX();
    double y = player.getY() + 20; // プレイヤーの上空20ブロック
    double z = player.getZ();
    for (int i = 0; i < 10; ++i) {
        // 報酬アイテム(例: ダイヤモンド)
        ItemEntity reward = new ItemEntity(level, x, y, z, Items.DIAMOND.getDefaultInstance());
        level.addFreshEntity(reward);
    }
}

またアイテムの生成はサーバ側で行います

@Override
public InteractionResultHolder<ItemStack> use(
        Level world, Player player, InteractionHand hand) {
    // クライアント側のみでエフェクトを表示
    if (world.isClientSide()) {
        // ビームエフェクトを生成
        spawnLaserBeam(player);
        if (isLocalSent) {
            sendToastMessage(player, previousCount);
            isLocalSent = false;
        }
    } else {
        // サーバー側でダメージ処理
        fireLaser(world, player);
        // 討伐数の取得
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey);
        // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
        if (previousCount != 0 && previousCount < currentCount) {
            // クライアント用の変数にも登録
            if (currentCount % 10 == 0) {
                sendChatMessage(player, currentCount);
                spawnReward(player, world);
                // ローカルの通知も許可する、このロジックだと一発分遅れる
                isLocalSent = true;
            }
        }
        // 一つ前の値を保存
        previousCount = currentCount;
    }
    return InteractionResultHolder.sidedSuccess(
            player.getItemInHand(hand), world.isClientSide());
}

最後に

クライアント側とサーバ側で行う処理を混同させないようにしましょう
基本的にはクライアント側はそのユーザのみでサーバ側は全ユーザに対して行う処理を記載します

現状はカスタムアイテム側でダメージ判定や討伐数管理をしていますがワールド側のイベントハンドリングで敵が onLivingDeath などで敵が倒されたときの武器を判定して特定の武器であれば討伐数をカウントするなどの実装方法もあります
そのあたりの実装方法は経験に左右されるところなのでなかなか難しいかなと思います

参考サイト

2025年2月3日月曜日

forge Mod で作成した武器のダメージを可変にする方法

forge Mod で作成した武器のダメージを可変にする方法

概要

これまでは固定でしたが可変にしてみました
例えば討伐した敵の数に応じてダメージ量が変わる方法を紹介します

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

サンプルコード

package com.example.examplemod;

import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;

public class LaserGunItem extends Item {
    private float amountOfdamage = 1.0F; // 初期ダメージ量
    private float maxAmountOfdamage = 100.0F; // 最大ダメージ量
    private String countKey = "laserGunKills"; // 討伐数をCompoundTagで管理するためのカウントのキー
    private int previousCount = 0; // 前回の討伐数
    private boolean isLocalSent = false;
    private int levelUpCount = 10; // レベルアップする討伐数

    public LaserGunItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResultHolder<ItemStack> use(
            Level world, Player player, InteractionHand hand) {
        // クライアント側のみでエフェクトを表示
        if (world.isClientSide()) {
            // ビームエフェクトを生成
            spawnLaserBeam(player);
            if (isLocalSent) {
                sendToastMessage(player, previousCount);
                isLocalSent = false;
            }
        } else {
            // 討伐数の取得
            int currentCount = getCurrentCount(player);
            // ダメージ量の設定
            setupUpWeaponDamage(currentCount);
            // サーバー側でダメージ処理
            fireLaser(world, player);
            // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
            if (previousCount != 0 && previousCount < currentCount) {
                // クライアント用の変数にも登録
                if (currentCount % levelUpCount == 0) {
                    sendChatMessage(player, currentCount);
                    spawnReward(world, player);
                    // ローカルの通知も許可する、このロジックだと一発分遅れる
                    isLocalSent = true;
                }
            }
            // 一つ前の値を保存
            previousCount = currentCount;
        }
        return InteractionResultHolder.sidedSuccess(
                player.getItemInHand(hand), world.isClientSide());
    }

    @Override
    public void appendHoverText(
            ItemStack stack,
            Item.TooltipContext context,
            List<Component> tooltip,
            TooltipFlag flag) {
        super.appendHoverText(stack, context, tooltip, flag);
        // ダメージ情報を追加
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.damage", amountOfdamage)
                        .withStyle(ChatFormatting.GREEN));

        // 他の情報も追加可能
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.description")
                        .withStyle(ChatFormatting.GRAY));
    }

    @Override
    public boolean onEntityItemUpdate(ItemStack stack, ItemEntity entity) {
        // 特殊な処理が必要であればここで行う(今回は不要なのでデフォルト動作に任せます)
        // アイテムが q で捨てられたときにアイテムとして残すための設定
        return super.onEntityItemUpdate(stack, entity);
    }

    private int getCurrentCount(Player player) {
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey);
        return currentCount;
    }

    private void spawnLaserBeam(Player player) {
        // クライアント側エフェクト(例:パーティクル)
        player.level()
                .addParticle(
                        ParticleTypes.END_ROD,
                        player.getX(),
                        player.getEyeY(),
                        player.getZ(),
                        player.getLookAngle().x,
                        player.getLookAngle().y,
                        player.getLookAngle().z);
    }

    private void playWeaponSound(Level world, Player player) {
        if (world instanceof ServerLevel serverLevel) {
            serverLevel.playSound(
                    null, // プレイヤー(nullにすると全員が聞こえる)
                    player.blockPosition(), // 再生位置
                    ExampleMod.LASER_FIRE_SOUND.get(), // サウンドイベント
                    SoundSource.PLAYERS, // サウンドの種類(PLAYERS, BLOCKS, AMBIENT など)
                    1.0f, // 音量
                    1.0f // 音の高さ(ピッチ)
                    );
        }
    }

    private void fireLaser(Level world, Player player) {
        // 音の再生
        playWeaponSound(world, player);
        // 発射用の座標と距離の設定
        Vec3 start = player.getEyePosition(1.0F);
        Vec3 direction = player.getLookAngle();
        double range = 50.0D; // 50ブロック先をターゲット
        Vec3 end = start.add(direction.scale(range));

        // ブロックとのヒット判定
        HitResult blockHit =
                world.clip(
                        new ClipContext(
                                start,
                                end,
                                ClipContext.Block.OUTLINE,
                                ClipContext.Fluid.NONE,
                                player));
        // エンティティとのヒット判定
        EntityHitResult entityHit =
                ProjectileUtil.getEntityHitResult(
                        world,
                        player,
                        start,
                        end,
                        player.getBoundingBox().expandTowards(direction.scale(range)).inflate(1.0D),
                        entity -> entity instanceof LivingEntity && entity != player);
        // 優先的にエンティティを処理
        if (entityHit != null) {
            System.out.println("Hit entity!");
            Entity hitEntity = entityHit.getEntity();
            if (hitEntity instanceof LivingEntity livingEntity) {
                // 敵にダメージを与える
                livingEntity.hurt(
                        livingEntity.damageSources().playerAttack(player), amountOfdamage);
                // 倒したか確認
                if (livingEntity.isDeadOrDying()) {
                    // カウントを増やす
                    incrementKillCount(player);
                }
            }
        } else if (blockHit != null && blockHit.getType() == HitResult.Type.BLOCK) {
            // ブロックにヒットした場合
            System.out.println("Hit block at: " + ((BlockHitResult) blockHit).getBlockPos());
            BlockHitResult blockResult = (BlockHitResult) blockHit;
            BlockPos hitPos = blockResult.getBlockPos();
            // ヒット位置に火をつける例
            world.setBlockAndUpdate(hitPos, Blocks.FIRE.defaultBlockState());
        } else {
            // 何にも当たらなかった場合
            System.out.println("Missed!");
        }
    }

    private void incrementKillCount(Player player) {
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey); // 現在のカウント
        tag.putInt(countKey, currentCount + 1); // カウントを増加
    }

    private void sendChatMessage(Player player, int currentCount) {
        player.sendSystemMessage(
                Component.translatable(
                                "The player %s killed %d mobs with laser gun."
                                        .formatted(player.getName().getString(), currentCount))
                        .withStyle(ChatFormatting.GREEN));
    }

    private void sendToastMessage(Player player, int currentCount) {
        String title = "Good job!";
        String description = "Killed %d mobs with laser gun.".formatted(currentCount);
        Minecraft minecraft = Minecraft.getInstance();
        ToastComponent toastComponent = minecraft.getToasts();
        // トーストを作成
        SystemToast toast =
                new SystemToast(
                        SystemToast.SystemToastId.PERIODIC_NOTIFICATION, // トーストのタイプ(任意の値でOK)
                        Component.literal(title), // タイトル
                        Component.literal(description) // 説明
                        );
        // トーストを表示
        toastComponent.addToast(toast);
    }

    // 報酬を降らせる処理
    private void spawnReward(Level level, Player player) {
        // プレイヤーの位置
        double x = player.getX();
        double y = player.getY() + 20; // プレイヤーの上空20ブロック
        double z = player.getZ();
        for (int i = 0; i < 10; ++i) {
            // 報酬アイテム(例: ダイヤモンド)
            ItemEntity reward = new ItemEntity(level, x, y, z, Items.DIAMOND.getDefaultInstance());
            level.addFreshEntity(reward);
        }
    }

    // 武器の強さを更新する
    private void setupUpWeaponDamage(int currentCount) {
        // 討伐数が2以下の場合は初期のダメージ量にする
        if (currentCount <= 2) {
            return;
        }
        // ダメージ量は討伐数/2とする
        float newAmountOfdamage = currentCount / 2;
        // 最大を超えているかチェック
        if (newAmountOfdamage >= maxAmountOfdamage) {
            amountOfdamage = maxAmountOfdamage;
        } else {
            // ダメージ量の更新
            amountOfdamage = newAmountOfdamage;
        }
    }
}

ポイント

まず武器の強さを設定するメソッドを準備します
今回は「総討伐数」を元にダメージ量を計算するので必要な引数は討伐数のみになります
討伐数 (currentCount) は CompoundTag で管理されている値なのでサーバ側の処理で取り出す必要があります

// 武器の強さを更新する
private void setupUpWeaponDamage(int currentCount) {
    // ダメージ量は討伐数/2とする
    float newAmountOfdamage = currentCount / 2;
    // 最大を超えているかチェック
    if (newAmountOfdamage >= maxAmountOfdamage) {
        amountOfdamage = maxAmountOfdamage;
    } else {
        // ダメージ量の更新
        amountOfdamage = newAmountOfdamage;
    }
}

CompoundTag を扱うので setupWeaponDamage は InteractionResultHolder のサーバ側の処理で行う必要があります
もしかすると tick などの常に呼ばれるハンドラを使えばそこでサーバ側の値を取得できるかもしれませんが今回は既存の処理をそのまま使っています
本当はコンストラクタでやりたいのですがコンストラクタでは Level を扱えません
なので現状の処理だと起動した直後はダメージ量が 0 になっており一度武器でアクションをしないとダメージ量が初期化されないので注意してください (このあたりは改良の余地あり、Mod のメイン側で計算してもいいのかも)

@Override
public InteractionResultHolder<ItemStack> use(
        Level world, Player player, InteractionHand hand) {
    // クライアント側のみでエフェクトを表示
    if (world.isClientSide()) {
        // ビームエフェクトを生成
        spawnLaserBeam(player);
        if (isLocalSent) {
            sendToastMessage(player, previousCount);
            isLocalSent = false;
        }
    } else {
        // 討伐数の取得
        int currentCount = getCurrentCount(player);
        // ダメージ量の設定
        setupUpWeaponDamage(currentCount);
        // サーバー側でダメージ処理
        fireLaser(world, player);
        // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
        if (previousCount != 0 && previousCount < currentCount) {
            // クライアント用の変数にも登録
            if (currentCount % levelUpCount == 0) {
                sendChatMessage(player, currentCount);
                spawnReward(world, player);
                // ローカルの通知も許可する、このロジックだと一発分遅れる
                isLocalSent = true;
            }
        }
        // 一つ前の値を保存
        previousCount = currentCount;
    }
    return InteractionResultHolder.sidedSuccess(
            player.getItemInHand(hand), world.isClientSide());
}

最後に

forge Mod で作成した武器のダメージを可変にしてみました
基本的には変数にするだけですがゲームを再起動したあともダメージ量を引き継ぎたい場合はデータを永続化するような仕組みが必要になります