2025年3月9日日曜日

Flask + distroless 超入門

Flask + distroless 超入門

概要

Flask を distroless 上で動かしてみました
ビルドは 3.12 ですが distroless の python は 3.11.2 なのでそこのバージョンは合わせたほうがいいかもしれません

環境

app.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, Distroless with Pipenv!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Pipfile 作成

  • pipenv install flask

Dockerfile

# ビルド用のステージ
FROM python:3.12.9-slim-bookworm AS builder

WORKDIR /app

# Pipenv をインストール
RUN pip install --no-cache-dir pipenv

# Pipfile と Pipfile.lock をコピーしてインストール
COPY Pipfile Pipfile.lock ./
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy --ignore-pipfile

# Flask アプリをコピー
COPY app.py .

# Distroless を使用する実行環境
FROM gcr.io/distroless/python3

WORKDIR /app

# Pipenv でインストールした仮想環境のライブラリをコピー
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app /app

# 環境変数を設定して Python が `.venv` を認識できるようにする
ENV PYTHONUNBUFFERED=1 \
    PIPENV_VENV_IN_PROJECT=1 \
    PYTHONPATH="/app/.venv/lib/python3.12/site-packages"

# Flask アプリを起動
CMD ["app.py"]

ビルド

  • docker build -t flask-distroless-pipenv .

起動

  • docker run --rm -p 8080:8080 flask-distroless-pipenv

動作確認

  • curl localhost:8080
Hello, Distroless with Pipenv!
  • docker images
REPOSITORY                  TAG       IMAGE ID       CREATED         SIZE
flask-distroless-pipenv     latest    9adef9f7e0d0   4 minutes ago   86.2MB

最後に

ビルドしたイメージは軽くなりますがシェルがないのでトラシュは大変そうです
そもそもビルド時には distroless なイメージは使えないのでビルド時のイメージを管理しなければいけないので結局ディストリビューションは必要かもしれません
また distroless で提供している Python のバージョンが 3.11.2 とかなり古いのでそこも微妙な感じはします

参考サイト

2025年3月7日金曜日

Google Cloud Associate Cloud Engineer 合格メモ

Google Cloud Associate Cloud Engineer 合格メモ

概要

勉強方法などを紹介しておきます
基本は過去ゲーです

環境

  • GCP ACE (2025/01/27時点)
  • テストセンターにて受験

料金

  • 137ドル (19702円)

対策

基本的には教科書と過去問です
ただ教科書だけでは合格するのはほぼ不可能だと思います (実務経験がかなりあれば別かも)

過去問を完璧にこなせば基本的には大丈夫かなと思います
教科書にも模擬試験的なのがあるのでお守り程度に持っておくといいかなと思います
過去問の問題も実際の試験で30-50%ほど出題された気がします (もう少しあったかも)
なお問題はすべて選択式で4択から1つ選択するだけでした
複数回答を求める問題は出題されませんでした

教科書

模擬試験

Udemy で 400 問ほど解きました
すべて有料なのでご注意ください
むしろ有料以外の模擬試験はほぼなくあっても微妙な問題集しかなかったです

試験範囲

一応確認しておくといいかもです

https://services.google.com/fh/files/misc/associate_cloud_engineer_exam_guide_japanese.pdf

解き方のポイント

  • 回答から問題が何関するものなのか先に推測する
    • 問題はダラダラ長いが最終的に聞きたいことはただの権限の問題だったりする
  • 消去法
    • 知らない問題でもあり得ない回答がわかればそこから2択などに絞り込む
  • 「実務ではこうする」の考えは完全に消したほうがいい
    • その方法じゃなくてもできるんだけどなー
    • そっちのほうが実際は面倒なんだよなー
    • と言った実務経験は完全にすてたほうがいいです
    • Google Cloud 推奨およびベストプラクティスを探す問題がほとんどなので注意しましょう
    • 実務経験が長い人ほど陥りやすいかもです
  • 定番の問題はおさえる
    • クラウドストレージのライフサイクル
    • ロールの最小原則とグループ割り当て
    • VPC のサブネット内の CIDR の扱い

実際に出題された問題の記憶

  • 過去問(および過去問に似た問題)が30-50%ほど出題された
  • 全く勉強していなかった部分はほぼなかった
    • UDP + ネットワークロードバランサは知らなかったが消去法で対処した
  • クラウドストレージに Cloud Interconnect 経由で接続する方法
    • 内部の DNS に CNAME 貼るやつ
  • サブネットの CIDR の IP レンジ拡張
    • ネットワーク部分の数字を減らして拡張するので間違えそう
  • 日本語がおかしい
    • Coldline -> コールドストレージ
    • それ以外にも問題文章自体の翻訳がおかしい部分が多々ある
  • 時間はかなりあまる
    • 自分は60分で終了

スコアレポートはでない

受験直後でも受験後の certmetrics の履歴でも確認することはできませんでした

暫定合格を確認する方法もない

ネットで調べると受験直後の画面に「暫定合格」なる文言が表示されるらしいのですが自分のときはありませんでした
最新の受験だとなくなったんだと思います
また受験直後に webassessor を確認しても完了した試験の項目がなくそこでも確認できませでした

なので現状だと受験直後に合否を確認する手段はありません

合否通知はいつくるか

自分は翌日の朝6:00にメールで合格通知が来ていました
なのでそんなに焦って合否を確認しなくても大丈夫かなと思います

最後に

スコアレポートが出ないのがかなり不満でした
また模擬試験が有料しかほぼないので個人でかつ実費で受験しなければいけない人には正直向かない試験です

2025年3月5日水曜日

(できなかったメモ) Mac で azw to pdf する

(できなかったメモ) Mac で azw to pdf する

概要

azw9.res + kfx 形式のファイルは以下の方法ではできなかったので一応メモしておきます
azw8 + kfx 形式であれば一応いけました
おそらく azw8 形式は自分で作成した pdf をKindleライブラリに追加した本なので変換できるのだと思います

kindle にダウンロードしたファイルを pdf に復号化する方法を紹介します
calibre を使う方法を紹介します

環境

  • macOS 15.3.1
  • calibre 7.26.0

インストール

  • brew install calibre

起動

アプリケーションの一覧にある calibre を起動します
本のタイプを選択する部分があるので Kindle を選択します

KFX input プラグインの追加

  • 環境設定 -> calibre を拡張するプラグイン取得 -> KFX input を検索してインストール

DeDRM プラグインの追加

これはプラグインの一覧にないので手動でインストールします

https://github.com/noDRM/DeDRM_tools/releases/tag/v10.0.3

zip をダウンロードしたら解凍します
DeDRM_plugin.zip というファイルがあれば OK です

  • 環境設定 -> 高度な設定 -> プラグイン -> ファイルからプラグインを読み込む

で先程の zip ファイルを指定します
インストールできたら calibre を再起動します

本を追加する

kindle でダウンロードした kfx ファイルを追加します
本を追加から kindle でダウンロードした本があるディレクトリにある kfx ファイルを選択します

pdf へ変換

本を変換で書き出します
右上に出力形式があるので PDF を選択しましょう (azw9.res 形式の場合ここで DRM を復号化できずにエラーになります)

変換が成功するとディスクに書き出しができるようになるので「ディスクに保存」で保存するパスを選択すれば OK です

最後に

古い kindle 形式であればこの方法でもいけるっぽいですが最新の kindle 形式 (azw9.res) などはこの方法だとダメなようです

2025年3月3日月曜日

mineoからNuroモバイルVMプランに変更したのでメモ

mineoからNuroモバイルVMプランに変更したのでメモ

概要

流れを簡単にメモしておきます
事務手数料で4000円ほど取られるのでそこは注意しましょう

環境

  • mineo (Dプラン、5G、音声通話あり)
  • Nuroモバイル (VMプラン、D回線、音声通話あり)
  • iPhone14

mineoでMNP予約番号を発行

https://support.mineo.jp/notes_cancel.html の「MNP予約番号発行」から発行します

Nuroモバイルの登録

https://mobile.nuro.jp/ 公式ページの「お申し込み」から申し込みます
キャンペーンなどの専用ページがある場合はそちらから必ず申し込みましょう
でないとキャンペーンが適用になりません

登録に必要になる情報は以下です

  • MNP予約番号
  • 携帯の番号
  • 身分証明書のデータ

あとは自分の好きなプランが決まっていれば OK です
SIM のサイズなども選択する画面があるのでそこは自身の端末に合わせて選択してください
iPhone14 の場合は nanoSIM です

NuroモバイルのSIM到着待ち

自分は登録から2日で届きました

Nuroモバイル切り替え

基本は説明書が入っているのでそれに従うだけですが Wifi 環境必須なのでそこだけ注意しましょう

ログイン

https://mobile.nuro.jp/r/use/mnp にログインします
ログインパスワードは郵送されてきた SIM カードに記載されているのでそれを使ってログインします
初回ログイン時はデフォルトのパスワードを変更するように促されるので変更します

MNPによる回線切替日の予約

ログインできたら「料金、お知らせ」タブから開通予約希望日時を選択します
日時は現在日時から選択できるのですぐに切り替えたい場合には最短を選択すれば OK です
(選択できる日時に深夜がなかったような気がするのでそこはご注意ください)

SIM カード入れ替え

iPhone の SIM カードを入れ替えます

NuroモバイルのAPNインストール

https://mobile.nuro.jp/support/settings/apn 基本的な流れはここにあるのでここから APN をダウンロードしインストールすれば OK です
ポイントは以下の通りです

  • 上記URLのスクショが古いの注意
  • 先に mineo のプロファイルを削除しておく必要がある
  • NuroモバイルのAPNインストール後は再起動はせずにフライトモードのオンオフだけでも OK

動作確認

Wifi をオフにしてモバイル回線だけで通信できることを確認しましょう
https://www.ugtop.com/spill.shtml などにアクセスしてゲートウェイが ap.nuro.jp になっているか確認してもいいかなと思います
また mineo 側にログインすると契約情報が消えていることが確認できます (そのうちログインもできなくなるのだろうか、卒業証書なるものがメールで届いたら eoID 自体を削除して問題なさそう)

5Gオプションの有効化

「ご契約オプション」のタブから5Gオプションを有効化できるので有効にしましょう
これを有効にしないと mineo 同様電話を受け取ることができない可能性があります

最後に

mineo から Nuro モバイルに切り替える方法を紹介しました
マイネ王側にアカウントがある方はそちらも忘れずに退会しておきましょう

2025年3月2日日曜日

error: Eager macro-expansion failure: (error "Shortdoc f function ‘f-older-p’: bad keyword ‘:noeval’")

error: Eager macro-expansion failure: (error "Shortdoc f function ‘f-older-p’: bad keyword ‘:noeval’")

概要

emacs をアップデートした場合に出ることがあるようです

環境

  • macOS 15.3.1
  • emacs 30.1

対策

package を最新にしましょう

  • M-x package-list-packages
  • 大文字 U ですべてを更新対象にする
  • x を押して更新を実行

最後に

emacs 自体の更新は Homebrew で管理できますが emacs にインストールしているパッケージは自分で定期的に更新する必要があるので注意しましょう

2025年3月1日土曜日

kindleでダウンロードした本が保存してあるパス

kindleでダウンロードした本が保存してあるパス

概要

azw 形式で保存されているようです

環境

  • macOS 15.3.1
  • kindle 7.25.1

パス

~/Library/Containers/com.amazon.Lassen/Data/Library/eBooks/

にあります

ここに例えば

ls -1 ~/Library/Containers/com.amazon.Lassen/Data/Library/eBooks/656E9450124248548B2D10040C0D5C49/8E5F7013-72F0-4E8F-BAA6-7CDEB12AF911/
BookManifest.kfx
BookManifest.kfx-shm
BookManifest.kfx-wal
CR!0B9WM943SN7XB8HF8PSND9217QTB.azw8

という形式で保存されています

最後に

パーソナルドキュメントもそうですが一度 kindle にファイルをアップロードすると PDF などでダウンロードできなくなります
azw という独自の形式を kindle アプリで復号化して閲覧しているようです

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 などで敵が倒されたときの武器を判定して特定の武器であれば討伐数をカウントするなどの実装方法もあります
そのあたりの実装方法は経験に左右されるところなのでなかなか難しいかなと思います

参考サイト