2025年2月20日木曜日

Pod でインストールしたライブラリ側の IPHONEOS_DEPLOYMENT_TARGET を変更する方法

Pod でインストールしたライブラリ側の IPHONEOS_DEPLOYMENT_TARGET を変更する方法

概要

The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 12.0 to 18.2.99. (in target 'YouTubePlayer' from project 'Pods')

への対応です

環境

  • macOS 15.3.1
  • xcode 16.2

Podfile 編集

一番最後に追加すれば OK です

  • vim Podfile
post_install do | installer |
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
    end
  end
end

適用

  • pod update

確認

  • grep -ir 'IPHONEOS_DEPLOYMENT_TARGET'

最後に

pod install したときに上記の指定がないとライブラリ側でサポートしているバージョンが適用されます
プロジェクト側で指定しているバージョンよりも大きければ問題ないですが小さい場合には Pod 側のバージョンを上げる必要があるので注意しましょう

2025年2月19日水曜日

mac で pdf を結合する方法その2

mac で pdf を結合する方法その2

概要

過去に pdfunite を使った方法を紹介しましたが更に簡単にできる方法があるので紹介します

環境

  • macOS 15.3.1

方法

  • Finder で結合したい pdf ファイルを選択
    • 最初に選択したファイルが先に結合されます
  • クイックアクション -> PDF を作成

で結合できます

2025年2月12日水曜日

copilot-metrics-viewer を使ってみる

copilot-metrics-viewer を使ってみる

概要

copilot の無料枠を使い切ったかどうか確認するのに使えます
公式のイメージはないので自分でビルドします

また copilot/metrics の API を内部で使っているのですがこの API は Github エンタープライズ or 組織 にしか使えないので個人では使えません

環境

  • macOS 15.3
  • docker 27.5.1

事前準備

  • Enterprise または organization に対して、[Copilot metrics API access] ポリシーを有効にする必要があります
  • パーソナルアクセストークンを取得します

ビルド

  • git clone https://github.com/github-copilot-resources/copilot-metrics-viewer.git
  • cd copilot-metrics-viewer/
  • docker build -t copilot-metrics-viewer .

.env 修正

リポジトリ内にデフォルトの .env があるのでそれを流用すれば OK です
NUXT_GITHUB_TOKEN を修正しましょう

パーソナルアクセストークンに必要な権限は以下です

  • copilot
    • manage_billing:copilot
  • manage_billing:enterprise
  • read:enterprise
  • read:org

その他の設定はデフォルトのままです
今回個人用で誰でも使えるようなアプリではないので NUXT_OAUTH_GITHUB_CLIENT_ID 関連はコメントアウトします
NUXT_PUBLIC_IS_DATA_MOCKED=false にします
NUXT_PUBLIC_GITHUB_ORG は自身が所属する組織IDを入力します

  • vim .env
# Determines if mocked data should be used instead of making API calls.
NUXT_PUBLIC_IS_DATA_MOCKED=false

# Determines the scope of the API calls. 
# Can be 'enterprise' or 'organization' to target API calls to an enterprise or an organization respectively.
NUXT_PUBLIC_SCOPE=organization

# Determines the enterprise or organization name to target API calls.
NUXT_PUBLIC_GITHUB_ORG=xxx

NUXT_PUBLIC_GITHUB_ENT=

# Determines the team name if exists to target API calls.
NUXT_PUBLIC_GITHUB_TEAM=

NUXT_PUBLIC_USING_GITHUB_AUTH=false

# Determines the GitHub Personal Access Token to use for API calls.
# Create with scopes copilot, manage_billing:copilot or manage_billing:enterprise, read:enterprise AND read:org
NUXT_GITHUB_TOKEN=ghp_xxx

NUXT_SESSION_PASSWORD=something_long_and_random_thats_at_least_32_characters

# for Github OAuth
# NUXT_OAUTH_GITHUB_CLIENT_ID=
# NUXT_OAUTH_GITHUB_CLIENT_SECRET=

# to use a corporate proxy
# HTTP_PROXY=http://proxy.company.com:8080

起動

  • docker run -p 8080:80 --env-file ./.env copilot-metrics-viewer

動作確認

localhost:8080 にアクセスするとメトリックスが表示されます

最後に

Github エンタープライズ or 組織配下でないと取得できないので結構ハードルが高いかもです
個人の利用で取得する方法はないのだろうか

参考サイト

2025年2月11日火曜日

HEIC を jpeg に変換し更に exif 情報を削除するコマンド

HEIC を jpeg に変換し更に exif 情報を削除するコマンド

概要

exiftool が必要です

環境

  • macOS 15.2

コマンド

for file in `ls *.HEIC`; do
  filename=$file:t:r
  sips --setProperty format jpeg $file --out ${filename}.jpg
  exiftool -EXIF= -Orientation='Rotate 90 CW' ${filename}.jpg
done

最後に

Web サービスでもできますが一括でできるのでコマンドのほうが便利です

2025年2月10日月曜日

Windows にインストールした Minecraft Launcher が使っている java があるパス

Windows にインストールした Minecraft Launcher が使っている java があるパス

概要

わざわざ Java をインストールしなくてもこれを使えます

環境

  • Windows 11
  • Minecraft Launcher

パス

"%LOCALAPPDATA%\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\Microsoft\WritablePackageRoot\runtime\java-runtime-alpha\windows-x64\java-runtime-alpha\bin\javaw.exe"

Mod などをインストールのに使うには

上記の %LOCALAPPDATA%\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\Microsoft\WritablePackageRoot\runtime\java-runtime-alpha\windows-x64\java-runtime-alpha\bin に jar を移動して

  • java -jar xxx.jar

すれば起動できます
ダウンロードフォルダにおいて上記の java.exe を指定して実行してもクラスパスが見つからずにエラーになるので jar を移動してから実行しましょう

最後に

Minecraft Launcher のバージョンによってパスが変わるっぽいですが LOCALAPPDATA にあるのは間違いないっぽいのでその配下を探せばいいかなと思います

2025年2月9日日曜日

NeoForge で開発環境構築

NeoForge で開発環境構築

概要

これまでは forge の MDK を使ってきました
NeoForge は forge のチームが新たに作成したプロジェクトで forge のフォークになります
別プロジェクトになった経緯などはこちらに詳しく載っているので興味があれば見てください
今回は NeoForge の開発環境を作成しとりあえずビルドできるところまでやってみました

環境

  • macOS 15.2
  • Java 21.0.5
  • NeoForrge MDK 21.4.75-beta
  • minecraft 1.21.4

事前準備: OpenJdk のインストール

NeoForge MDK は Java21 が推奨なのでインストールしましょう

  • brew install openjdk@21
  • sudo ln -sfn /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk

MDK のダウンロード

ここから MDK を生成しダウンロードすることができます
Forge の MDK では最初から MODID などが決まっていましたが NeoForge ではそれらを MDK 生成時に決めることができます

今回はとりあえずデフォルトの examplemod にしましたが別の ID でも OK です

Gradle Plugin は「ModDevGradle」を選択しましょう
Minecraft Version は最新でも OK ですが最新だと MDK がまだベータなので注意しましょう

展開とビルド

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

  • mkdir test
  • cd test
  • mv ~/Downloads/examplemod-template-1.21.4.zip .
  • unzip examplemod-template-1.21.4.zip

で展開します
gradlew コマンドに権限がないので付与します

  • chmod +x gradlew

あとはビルドすれば OK です

  • ./gradlew build

動作確認

ビルドやテスト用のコマンドは Forge と同じでした
起動して Mod 用のブロックがあることを確認します
Forge と違い NeoForge ではリソースに lang だけ初回からあるようです

  • ./gradlew runClient

最後に

NeoForge の MDK でとりあえず開発できるところまで構築してみました
このあとは普通に MOD を開発するだけです

機会があれば Forge で開発したコードを NeoForge にマイグレーションする作業もやってみたいです

参考サイト

2025年2月8日土曜日

gradle で管理しているプロジェクトに google-java-format を導入する方法

gradle で管理しているプロジェクトに google-java-format を導入する方法

概要

google-java-format は Java のコードをフォーマットしてくれるツールです
CLI でも使えますが別途インストールしたりプロジェクトごとに管理できないので今回は gradle 配下でインストールし実行する方法を紹介します

環境

  • macOS 15.2
  • Java 21.0.5
  • google-java-format 1.17.0

build.gradle の編集

主に2箇所追記します

plugins

まずは spotless というプラグインをインストールします

plugins {
	id 'com.diffplug.spotless' version '7.0.2'
}

spotless

format 用の定義を追加します
Java21 の場合は google-java-format は 1.17.0 以下を指定します

設定でいろいろなフォーマットが可能です
インデントはスペースではなくタブにしたりヘッダのコピーライト文も自動で追加したりできます

spotless {
    format 'misc', {
        // define the files to apply `misc` to
        target '*.gradle', '.gitattributes', '.gitignore'

        // define the steps to apply to those files
        trimTrailingWhitespace()
        leadingTabsToSpaces()
        endWithNewline()
    }
    java {
        // don't need to set target, it is inferred from java

        // apply a specific flavor of google-java-format
        googleJavaFormat('1.17.0').aosp().reflowLongStrings().skipJavadocFormatting()
        // fix formatting of type annotations
        formatAnnotations()
        // make sure every file has the following copyright header.
        // optionally, Spotless can set copyright years by digging
        // through git history (see "license" section below)
        licenseHeader '/* (C)$YEAR */'
    }
}

動作確認

  • ./gradlew spotlessApply

これで自動的にフォーマットされます

Tips

emacs で lsp-java を使っている際にエディタ側で自動でフォーマットされてしまうのを防ぐ方法は以下です

(setq lsp-java-format-enabled nil)
(setq lsp-java-format-on-type-enabled nil)

最後に

これでコードのフォーマットは統一することができます
エディタ側には google-java-format 用の linter がほしいところです

参考サイト

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