カテゴリー: 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 と表示されていた事件
  • 🔄 サーバーを止めずに設定を読み直す /리로드(リロード)、自動で起動し直す /재시작(再起動)

をまとめていきます。

モブの頭上に緑色で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.admin

coreの権限登録関数は「権限名は 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はバニラ側でもモブを確実に倒す必要があり、そのときこうしていました。

Java
// core: 仮想体力が0になったらバニラ側でも確実に倒す
e.damage(e.getHealth() + 1_000_000, damageSource);   // 死亡処理用の内部ダメージ

バニラから見ると、「残り体力 + 1,000,000」だけダメージを受けたことになります。そしてダメージ数値を表示する戦闘モジュールが、この内部ダメージのイベントまで受け取ってそのまま表示していたんです。残りのバニラ体力29 + 1,000,000 = 1000029。数字がぴったり合います 😂

修正:死亡処理用のダメージは表示しない

この内部ダメージは、ダメージの種類が GENERIC_KILL です。戦闘モジュールに1行だけ追加しました。

Java
// 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):サーバーのスペックに合わせて変更してください。

📌 よくあるエラー

  1. モジュールが有効にならない — 「権限ノードは … で始まる必要があります」 原因:権限名の接頭辞ルール違反 → 解決:myserver.<모듈>.… に名前を変更
  2. /리로드 のあと特定のモジュールだけ ✗ 原因:そのモジュールの設定JSONの文法エラー → 解決:ログの行番号を見て直してから、もう一度 /리로드。その間も前の設定で動き続けます
  3. 再起動後にサーバーが起動しない 原因:バッチファイルではなく java -jar で直接起動した → 解決:ループのバッチファイルで起動する

📌 まとめ

今回はプラグインの構造と、実際に起きた事件2つをまとめました。

  • jar 1つ + モジュール13個、モジュール同士はcoreのサービスだけでつなぐ
  • 権限の接頭辞ルールはモジュールを無効にする安全装置 + マージ前の自動チェック
  • 1000029 は死亡処理用の内部ダメージが数値として見えてしまったもの
  • /리로드・/재시작 のおかげで、AIチームが頻繁に修正してもサーバー運営が楽になりました

次回は数値バランスの話です。体力・ダメージをまとめて10倍にした方法、「30レベルまで12時間」の経験値曲線、そして企画チームAIがクラス6種をシミュレーションして見つけた意外な原因を扱います。お楽しみに 🙌

この記事は、私が方向性を決めてClaudeと一緒にまとめました。このサーバーは個人プロジェクトであり、マインクラフトの公式コンテンツではなく、Mojang・Microsoftとは関係ありません。

連載 · 全5回中2回目

AIチームでマイクラサーバーを作る

  1. #1AIリーダー1人 + メンバー15人でMMORPGプラグインを作る仕組み! 🤖
  2. #21つのプラグインにモジュール13個!コア構造と「1000029ダメージ」事件 🧩現在の記事
  3. #3数値×10、12時間の経験値曲線、クラス6種のシミュレーション! 📊
  4. #4ChatGPTが描いてClaudeが検品するリソースパック・パイプライン! 🎨
  5. #5Discordボットで開発チームを運営!決定フォーラム・コマンド一覧・リソースパックチャンネル 🤝

著者

Mojangcam

自分でコードを書き、サーバーを動かし、詰まったエラーを解決しながら学んだことを記録する開発・IT実践ブログ