カテゴリー: Minecraftプラグイン
AIチームでマイクラサーバーを作る#21つのプラグインにモジュール13個!コア構造と「1000029ダメージ」事件 🧩
この記事は韓国語の原文から翻訳したものです。
ひと目でわかる要点
このサーバーは、プラグインのjar 1つにcoreと13個のモジュールが入っています。モジュール同士は直接呼び合わず、coreのサービスだけでつながります。この構造で実際に起きたバグ2つと、サーバーを止めずに設定を読み直すリロード・再起動機能を紹介します。
- Paper 1.21.11 · Java · プラグインjar 1つ = core + モジュール13個 · 2026-09-29〜30 実際に修正
- モジュール同士は直接依存せず、coreのサービス(インターフェース)だけでつなぐ — チームごとの並行開発ができた理由
- 権限ノードの接頭辞ルールを破るとモジュールが丸ごと無効になるのは、意図した安全装置でした
- ダメージ1000029 = 「残り体力29 + 死亡処理用の1,000,000」。計算ではなく表示が間違っていたバグ
目次9項目

こんにちは!Mojangcamです 😎
前回の#1では、Claudeをチームリーダーに、予約タスク15個をチームメンバーにして働く仕組みを紹介しました。今回は、そのチームたちが作っているプラグインそのものの構造です。
私たちのサーバーには、自作のプラグインがjarファイルたった1つだけ入っています。その中に、レベル・戦闘・スキル・クエスト・ショップ・モブといった機能がモジュール13個に分かれています。今回は
- 🧩 core + モジュール構成をなぜこう設計したのか
- 🔒 権限名ひとつのせいでモジュール2つが丸ごと無効になった事件
- 💥 モブを倒すとダメージが
1000029と表示されていた事件 - 🔄 サーバーを止めずに設定を読み直す
/리로드(リロード)、自動で起動し直す/재시작(再起動)
をまとめていきます。

📌 準備するもの(環境)
- サーバー:Paper 1.21.11、Java 21以上
- ビルド:Gradleマルチプロジェクト(モジュールごとにサブプロジェクト → 最終的に1つのjarにまとめる)
- DB:MariaDB(進行状況の保存)
- 外部プラグインは最小限:LuckPerms(権限) · EssentialsX · WorldEdit
📌 ステップ1 - なぜ「jar 1つ + モジュール13個」なのか 🧠
最初は機能ごとに別々のプラグインを作ろうかと考えました。でも、レベル・戦闘・スキル・アイテムはお互いに数値をずっとやり取りします。プラグインが別々だと、読み込み順、バージョン合わせ、API共有がすべて頭痛の種になります。
そこで、こう決めました。
| 層 | 役割 |
|---|---|
| core | 設定ファイル・メッセージ・DB接続・権限登録・コマンド登録・サービスレジストリ・仮想体力・HUD |
| モジュール13個 | economy · quest · job · level · combat · skill · item · companion · world · mob · log · storage など |
| ルール | モジュールは他のモジュールのクラスを直接importしない。必要な機能はcoreにあるインターフェース(サービス)経由だけ |
たとえば戦闘モジュールが武器の攻撃力を知りたいときは、アイテムモジュールを呼ぶのではなく、coreに登録された ItemService を探して使います。アイテムモジュールが無効なら、サービスがないだけで、戦闘モジュールはデフォルト値で動きます。
このルールのおかげで、13チームが同時に開発できました。各チームはインターフェースだけを信じて作業すればいいからです。
📌 ステップ2 - 権限名ひとつでモジュールが丸ごと無効になった事件 🔒
サーバーを起動したら、ログにこんな行が出ました(要約)。
[MyServer] quest 모듈 켜기 실패: 권한 노드는 'myserver.quest.' 로 시작해야 합니다 — myserver.qnpc.console
[MyServer] world 모듈 켜기 실패: 권한 노드는 'myserver.world.' 로 시작해야 합니다 — myserver.region.admincoreの権限登録関数は「権限名は myserver.<모듈>. で始まらなければならない」というルールをチェックし、違反すると例外を投げます。例外が出ると、そのモジュールは有効になりません。
クエストチームはNPCコマンドに myserver.qnpc.…、ワールドチームはエリア保護に myserver.region.… を使っていました。人が見てももっともらしい名前なので、レビューで見落としたんです。
ひとつずつ見ていきましょう 💡
- なぜここまで厳しく?:モジュールが13個あると、権限名がかぶりやすくなります。接頭辞を強制すれば、LuckPermsで
myserver.quest.*の1行だけでモジュールの権限をまとめて付与できます。 - なぜ警告ではなくモジュールを無効にしてしまうのか?:権限が間違って登録されたまま動くと、運営者専用コマンドが一般ユーザーに開放される事故が起こりかねません。安全装置は確実に止まる側のほうがいいんです。
修正は名前を2つ変えるだけで終わりました。そのかわり、同じ事故を防ぐためにマージ前に自動でチェックする1行をリーダーの手順に加えました。
# module/<チーム> ブランチで接頭辞ルールに違反した権限ノードを探す(結果が空なら合格)
m=quest
git grep -hoE 'registerPermission\([^;]*' module/$m -- "$m/src/main" \
| grep -oE '"myserver\.[a-z_]+\.' | awk -F. -v m=$m '$2!=m'📌 ステップ3 - ダメージ1000029事件 💥
ゲームでモブを倒した瞬間、頭上に1000029というありえない数値が表示されました。でも、モブが一撃で倒れたわけではありません。それまでの攻撃は正常な数値で、最後の一撃だけがおかしかったんです。
原因:「仮想体力」と「確実に倒す処理」
このサーバーでは、体力が数千〜数万まで上がります。バニラの体力(最大1024)では足りないので、coreが仮想体力を別に管理しています。仮想体力が0になったら、coreはバニラ側でもモブを確実に倒す必要があり、そのときこうしていました。
// core: 仮想体力が0になったらバニラ側でも確実に倒す
e.damage(e.getHealth() + 1_000_000, damageSource); // 死亡処理用の内部ダメージバニラから見ると、「残り体力 + 1,000,000」だけダメージを受けたことになります。そしてダメージ数値を表示する戦闘モジュールが、この内部ダメージのイベントまで受け取ってそのまま表示していたんです。残りのバニラ体力29 + 1,000,000 = 1000029。数字がぴったり合います 😂
修正:死亡処理用のダメージは表示しない
この内部ダメージは、ダメージの種類が GENERIC_KILL です。戦闘モジュールに1行だけ追加しました。
// combat: ダメージ数値表示リスナー (MONITOR)
if (event.getDamageSource().getDamageType() == DamageType.GENERIC_KILL) return; // 死亡処理用の内部ダメージは数値を表示しない
double shown = event.getFinalDamage();実際のダメージ計算は最初から正常で、表示だけが間違っていたバグでした。この手のバグはゲーム内で一度見ればすぐ分かりますが、ユニットテストでは捕まえにくいものです。第1回で触れた「人間はゲームに入って見て、伝える」という役割が、ここで光りました。
📌 ステップ4 - サーバーを止めずに設定を変える:/리로드 · /재시작 🔄
AIチームが1日に何度も設定やjarを修正するので、毎回サーバーを止めて起動し直すのはあまりにも面倒でした。そこでcoreに2つの機能を入れました。
| コマンド | 内容 |
|---|---|
/리로드 (/myserver reload) |
core → 有効なすべてのモジュールの設定ファイルを読み直し、モジュールごとに ✓/✗ を要約。設定が間違っていればそのモジュールだけ失敗し、前の設定を維持 |
/리로드 level |
そのモジュールだけ読み直す |
/재시작 60 |
60秒のカウントダウン(チャット + ボスバー)のあとサーバーを停止 → 自動で起動し直す |
| (自動) | 設定ファイルが変わると自動で読み直す、plugins/update に新しいjarが入ると運営者に通知 |
「自動で起動し直す」は、サーバー側のコードだけでは実現できません。プラグインが停止直前に目印ファイルを1つ作り、サーバーを起動するバッチファイルがそのファイルを見て起動し直します。
@echo off
chcp 65001 > nul
cd /d "%~dp0"
:loop
if exist "plugins\MyServer\.restart" del /q "plugins\MyServer\.restart"
java -Xmx8G -jar server.jar nogui
if exist "plugins\MyServer\.restart" (
echo 재시작 요청 확인 - 5초 뒤 다시 켭니다. 중지하려면 Ctrl+C
timeout /t 5
goto loop
)
echo 서버가 종료되었습니다.
pauseひとつずつ見ていきましょう 💡
.restartファイル:/재시작が作ります。普通に/stopで止めるとファイルがないので、起動し直しません。- ループの最初で削除する:前回の再起動の痕跡が残って、無限に再起動し続けるのを防ぎます。
- メモリ(
-Xmx):サーバーのスペックに合わせて変更してください。
📌 よくあるエラー
- モジュールが有効にならない — 「権限ノードは … で始まる必要があります」
原因:権限名の接頭辞ルール違反 → 解決:
myserver.<모듈>.…に名前を変更 /리로드のあと特定のモジュールだけ ✗ 原因:そのモジュールの設定JSONの文法エラー → 解決:ログの行番号を見て直してから、もう一度/리로드。その間も前の設定で動き続けます- 再起動後にサーバーが起動しない
原因:バッチファイルではなく
java -jarで直接起動した → 解決:ループのバッチファイルで起動する
📌 まとめ
今回はプラグインの構造と、実際に起きた事件2つをまとめました。
- jar 1つ + モジュール13個、モジュール同士はcoreのサービスだけでつなぐ
- 権限の接頭辞ルールはモジュールを無効にする安全装置 + マージ前の自動チェック
1000029は死亡処理用の内部ダメージが数値として見えてしまったもの/리로드・/재시작のおかげで、AIチームが頻繁に修正してもサーバー運営が楽になりました
次回は数値バランスの話です。体力・ダメージをまとめて10倍にした方法、「30レベルまで12時間」の経験値曲線、そして企画チームAIがクラス6種をシミュレーションして見つけた意外な原因を扱います。お楽しみに 🙌
この記事は、私が方向性を決めてClaudeと一緒にまとめました。このサーバーは個人プロジェクトであり、マインクラフトの公式コンテンツではなく、Mojang・Microsoftとは関係ありません。
連載 · 全5回中2回目
AIチームでマイクラサーバーを作る
- #1AIリーダー1人 + メンバー15人でMMORPGプラグインを作る仕組み! 🤖
- #21つのプラグインにモジュール13個!コア構造と「1000029ダメージ」事件 🧩現在の記事
- #3数値×10、12時間の経験値曲線、クラス6種のシミュレーション! 📊
- #4ChatGPTが描いてClaudeが検品するリソースパック・パイプライン! 🎨
- #5Discordボットで開発チームを運営!決定フォーラム・コマンド一覧・リソースパックチャンネル 🤝
