2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DatapackとPluginの合わせ技

2
Last updated at Posted at 2025-12-08

この記事は Minecraft Command Advent Calendar 2025 9日目の記事です。

はじめに

datapackじゃあやれることに限りがある......そうだ、pluginを作ろう

ということでこんにちは。ノフィーです。
この記事は、datapackとpluginを組み合わせて使う方法について模索した記事です。

pluginの合わせ技!とか言ってますが、java言語はまだ勉強中なのでゆるく見ていただけると嬉しいです。

この記事を読むにあたって、java言語の知識は必要ありませんが、python等のなにかのプログラミング言語の基礎知識があると理解がしやすいと思います。

なお、この記事のpluginは JE 1.21 で作成・Paper 1.21.7 で動作確認しています(1.21.x ならどれでも動くと思います)。

なんでやろうと思ったのか?

先人の知恵等もありdatapackでもほとんどのことができるようになっていますが、それでも未だに完璧では無いうちの一つが左クリック検知。

右クリックに関してはitem_modelやconsumableなどを使ってほとんど自由に検知できてますが、左クリックの検知は一筋縄では行かないのは誰しもが知っていることでしょう(多分)(1.21.11で左クリック検知できるようになるらしいけど知らないこととします)。

それ以外に、datapackでできるけど処理が煩雑・負荷が高くなってしまうのがアイテムを捨てるのを防ぐことや目線の先のブロックの取得、他にもチェストや作業台などをそもそも開けなくしたい、といったことをしたいけどdatapackだととてもじゃないけどめんどくさい.....
じゃあpluginでやれば完璧じゃない...?と思い至った次第です。逃げてない

でも、普段はdatapackしか触ってないし新たにpluginを勉強するのは......なら、メインのシステムはdatapackで作って、pluginで補ってあげればいいじゃん!という次第です。

ただ、これを配布マップに生かせるかというと、プレイしてもらうためまでに必要な知識が多く(サーバー導入など)、一般的なプレイヤーにはかなりの労力を掛けてしまうので身内向けや限られた状況のみでしか使えないような気がします。

それはともかく、pluginは使ってて楽だし新鮮な感覚なので知識として蓄えといて損はないだろう!という魂胆です。

左クリックを検知してみる

早速ですが、難関といわれる左クリックをpluginで検知してみましょう。

pluginでは、マイクラ内で起こったことを「Event」として検知できます。それを検知する「EventListener」を親クラスから継承したクラスにいろいろすると各行動を検知することができます。

言葉だけの説明だとあれなのでやってみましょう。

そもそもどうやってpluginを作るんだい!

新規pluginの作り方

・ IntelliJ IDEA の導入
https://www.jetbrains.com/ja-jp/idea/download/ の IntelliJ IDEA Community版(無料版)を導入します。Ultimateは有料なので注意。

次に、pluginをインストールします。歯車 -> plugin から
・Japanese Language Pack(IntelliJ IDEA 本体の日本語化)
・Minecraft Development
の二つをインストールします。(日本語化は言語設定で既に入ってるかも)

・plugin の新規作成
新規 -> プロジェクト より、新規作成します。
名前はプロジェクト名を、場所は任意のディレクトリを選択します。

少し飛んで Build System PropertiesGroup ID に、絶対に誰とも被らない IDを入力します。一番いいのは自分専用のリンク(githubのprofileなど)で、それを逆順に入れます。
(e.g. github.com/Nofeysan なら com.github.Nofeysan

image.png

それ以外は特に変更なしでOK。これで作成!

コードを書く

メインクラスからEventListenerを継承したクラスに記述していきます。
〈わかる人向け → import は省略しています〉

「クラス」とは? javaでは一つのメインクラスと、それに付随する子クラスがあります。

メインクラス一つあれば十分ですが、そこ一つにすべてを書いてしまうと可読性などの観点からよくないので、一部の機能のみを引き継いだ(implements: 継承した)クラスを作り、そのクラスとメインクラスを紐づけることで集約することができます。

pluginには様々なイベントがあります。以下のページにほとんどのEventのリストがあります。見るとわかりますが、かなり細かく分かれているので大抵のことは検知できます。

Spigot Event List - sya-ri
https://spigot-event-list.s7a.dev/ja?tags=spigot-paper

・プレイヤーがチャットしたことを検知 → AsyncChatEvent
・クラフトしたことを検知 → CraftItemEvent
といった、「できたら便利」が全部できます。やったね。

で、左クリックの検知には PlayerInteractEvent を使います。ただ、単に「左クリック」というのがあるのではなく、「空気(Air)に左クリックした」「ブロックに左クリックした」 というかなり細かいところまで分かれています。今回はとにかく左クリックを検知できればいいので or 条件の || で並列して書きます。

「左クリックすると『左クリックしたね!』とメッセージが出る」コードは以下のようになります。

Main class
public final class Advent_2025 extends JavaPlugin {

    @Override
    public void onEnable() {
        // Plugin startup logic
        // EventListenerClass, この行を加える('new'の後は自分で設定したclass名)
        getServer().getPluginManager().registerEvents(new EventListener(), this);
    }

    @Override
    public void onDisable() {
        // Plugin shutdown logic
    }
}
[class] EventListener
public class EventListener implements Listener {
    @EventHandler
    public void onLeftClicked(PlayerInteractEvent e) {
        Player player = e.getPlayer();

        // 左クリックを検知する
        if (e.getAction().equals(Action.LEFT_CLICK_AIR) || e.getAction().equals(Action.LEFT_CLICK_BLOCK)) {
            // 本処理
            player.sendMessage("左クリックしたね!");    // e.g. メッセージを送る 
        }
    }
}
ザックリ解説

javaは、{} を除いて終わりには ; を入れます。そのため、; がなければ終わりとみなされないため自由に改行できます。

=== Main Class ===
getServer().getPluginManager().registerEvents(new EventListener(), this);
定型文です。new の後は別に作成してあるEventListener用のクラス名を記述してください。

=== [class] EventListener ===
public class EventListener
どこからでも参照できる publicclassEventListener を宣言します。定型文。

implements Listener
これを書くことで、「イベントリスナとしての機能を持っている」ことを示し、そのものとして読み込んでくれます。

=== @EventHandler 以下 ===
public void onLeftClicked(PlayerInteractEvent e)
public はそういうもん。void は返り値を必要としないメソッド(関数)を定義します。onLeftClicked はその関数名(なんでもいい)で、括弧内に検知したいイベントを入れます。今回の場合は PlayerInteractEvent で、そのイベントを関数内でどの名前で使うかを並列して(スペース区切りで)書きます。なんでもいいです。今回は event からとった e を使っています。

Player player = e.getPlayer();
integer や string みたいに、Player というを宣言して、playerという変数に、イベントを起こしたプレイヤーを e.getPlayer() で入れます。これに引数は必要としないので括弧内は空ですね。

if
左クリックには二種類あるのでどっちかに合致したら通れるようにします。javaでの A or BA || B

ちなみに、関数名とかはjavaでは慣習的に頭は小文字で書くそうです。

datapackでは考えられないような書き方ですね。おそらく一般のプログラムと言えばこちら側なのでしょうが...

とはいえ、これで左クリックが検知できちゃったわけなんですね(実はドアを開く・アイテムを捨てるとかでも左クリック扱いになっちゃいますが)。でもこれをどう datapack につなげるのか?

pluginからfunctionを実行する

そこで役に立つのが2つあります。

player.performCommand("<command>")
例えば、player.performCommand("function foo:bar"); と入れると、プレイヤーにそのコマンドを実行させます。ただし、実行「させる」んです。つまり、OPがない人に function を実行させることはできません。なんてこった!

これでは権限に制限されてしまうので自由度はあまり高くありませんね。

Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "<command>")
そこで登場これ。

こちらはサーバー君(コンソール)にコマンドを実行させます。プレイヤーの権限に関わらないので制限なくコマンドを実行することができます。すごい!

特定のプレイヤーにあるコマンドを実行させたい場合は execute を使いましょう。

player に実行させたいとき
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "execute as " + player.getName() + " run ...")

とすればそのプレイヤーの権限に関係なく実行ができる!

あるいは、検知用のfunctionを用意し特定のイベントが起こった時にサーバー側から実行させることで、スコアの変化等を1msの遅延なく処理を実行できます。
(e.g. イベントを起こした人のスコアを変更 → 検知のfunction実行)

厳密な処理実行の順番 [要検証]

処理順番は以下のようになっています。
#tickschedulerpacketplugin(Event)function, advancementなど

そのため、もし値の変化を#tickで監視している場合は1tickの遅延が生じてしまうので注意です(逆に1tick遅延させたいときに使えるかも)。

これで無事にpluginで検知して、datapack側の処理を行うことができました!

datapackからpluginの処理を実行する

じゃあ逆にdatapack側からpluginの処理を実行させるにはどうしたらいいのでしょうか?そういうコマンドが用意されているわけではないので単純にはできません。

ではどうするか?その方法の一つとしてスコアを監視し続けるものがあります。

plugin上でのタイマー

例えば「一秒おきにスコアを監視する」ということはどのように記述すればよいのでしょうか?

datapackなら advancement でも scoreboard でも(stopwatch でも)ササっと作ることができますね。

pluginでもそのようなことはできるのですが、それじゃあ特性を生かしきれません。そこで登場するのが scheduler です。これだと何ティックに一度処理を実行するかを自由に決めることができます。以下のコードでは、一秒おきに、現在オンラインのプレイヤーそれぞれになんらかの処理を実行するスケジューラです。

[main class] scheduler
Bukkit.getScheduler().runTaskTimer(this, () -> {    // この this はメインクラスのインスタンス
    for (Player player : Bukkit.getOnlinePlayers()) {
            // メイン処理
            // 通知を入れたり...
    };
}, 0L, 20L);    // 起動時から最初の実行までの待機時間[tick], 繰り返しの間隔[tick] 

// i秒おき = 間隔が i*20tick (i*20L)

ここに、スコアを取得し、その値が特定の値なら処理を実行する、ということができます。

例えば、plugin用のスコアボード PluginTrigger を用意し、もしその値が1ならそのプレイヤーに矢を発射させるコードは以下のように書けます。

値が1なら、矢を発射する
// 起動時に取得する
ScoreboardManager manager = Bukkit.getScoreboardManager();    // scoreboard を管理する manager の取得
Scoreboard mainScoreboard = manager.getMainScoreboard();    // バニラの scoreboard 全体を取得
Objective objective = mainScoreboard.getObjective("PluginTrigger");
if (objective == null) return;    // 特定の scoreboard を取得できなかった時

// scheduler
Bukkit.getScheduler().runTaskTimer(this, () -> {
    
    for (Player player : Bukkit.getOnlinePlayers()) {
    
        // スコアを取得
        Score score = objective.getScore(player.getName());

        if (score.getScore() == 1) {
            // 矢を発射
            Vector direction = player.getLocation().getDirection();
            double power = 2.0D;    // 2.0 がフルチャージの速さ
            Vector velocity = direction.multiply(power);
            Arrow arrow = player.launchProjectile(Arrow.class);
            arrow.setVelocity(velocity);
            arrow.setShooter(player);
            arrow.setCritical(false);

            // reset
            score.setScore(0);
        }
            
    }
}, 0L, 1L);    // 毎tick実行

ただし、起動中にそのscoreboardを削除してしまった場合、コンソールが赤文字エラーで埋め尽くされてしまいます。そのため、頻繁にscoreboard自体の操作が行われる場合は毎回存在するかを検知してもよいです(scoreboardの走査自体はそんなに負荷がない)。

毎回スコアボードの存在を確かめる方法

この場合、ラグの発生によってscoreboardが読み込めなかった場合はnullが発生しNullPointerExceptionが発生してしまいます。ぬるぽ
そのため、null処理を入れてあげると挙動が安定します(エラー吐かれるだけだけど、最悪クラッシュしちゃう)。

値が1なら、矢を発射する
Bukkit.getScheduler().runTaskTimer(this, () -> {

    ScoreboardManager manager = Bukkit.getScoreboardManager();
    if (manager == null) return;    // scoreboard 全体のデータを取得できなかった時

    Scoreboard mainScoreboard = manager.getMainScoreboard();
    Objective objective = mainScoreboard.getObjective("PluginTrigger");
    if (objective == null) return;    // 特定の scoreboard を取得できなかった時
    
    for (Player player : Bukkit.getOnlinePlayers()) {
    
        // スコアを取得
        Score score = objective.getScore(player.getName());

        if (score.getScore() == 1) {
            // 矢を発射
            Vector direction = player.getLocation().getDirection();
            double power = 2.0D;    // 2.0 がフルチャージの速さ
            Vector velocity = direction.multiply(power);
            Arrow arrow = player.launchProjectile(Arrow.class);
            arrow.setVelocity(velocity);
            arrow.setShooter(player);
            arrow.setCritical(false);

            // reset
            score.setScore(0);
        }
            
    }
}, 0L, 1L);    // 毎tick実行



これを使えば、都度AECを召喚して、ベクトルを計算して代入して......ということをせずにただ scoreboard players set <player> pluginTrigger 1 を実行するだけで簡単に矢を飛ばせます。しかも、値によって矢の速度を変えることだって簡単にできてしまいます。

ただしこの場合、score設定 → plugin処理 でtickを跨ぐため、最低1tick ~ 最大繰り返し間隔の遅延が発生します。そのため、迅速なレスポンスが欲しい場合用の繰り返し1tickのスケジューラと、そんなに重要でない用の20tickのスケジューラを用意すると扱いやすいです。

これをメインクラスの onEnable() 内に記述することで完了です。

完成版
    @Override
    public void onEnable() {
        // Plugin startup logic
        ScoreboardManager manager = Bukkit.getScoreboardManager();    // scoreboard を管理する manager の取得
        Scoreboard mainScoreboard = manager.getMainScoreboard();    // バニラの scoreboard 全体を取得
        Objective objective = mainScoreboard.getObjective("PluginTrigger");
        if (objective == null) return;    // 特定の scoreboard を取得できなかった時

        // scheduler
        Bukkit.getScheduler().runTaskTimer(this, () -> {

            for (Player player : Bukkit.getOnlinePlayers()) {

                // スコアを取得
                Score score = objective.getScore(player.getName());

                if (score.getScore() == 1) {
                    // 矢を発射
                    Vector direction = player.getLocation().getDirection();
                    double power = 2.0D;    // 2.0 がフルチャージの速さ
                    Vector velocity = direction.multiply(power);
                    Arrow arrow = player.launchProjectile(Arrow.class);
                    arrow.setVelocity(velocity);
                    arrow.setShooter(player);
                    arrow.setCritical(false);

                    // reset
                    score.setScore(0);
                }

            }
        }, 0L, 1L);
    }

便利なこと

結局、datapackとpluginを跨ぐことは何がいいのか?pluginを使うならそれに統一した方がいいというのは明確です。ただ、datapackのほうに慣れている場合はそちらの方が楽に思えることが多々あります。

例えば、datapackではテキストの表示にカラーコードが使えるのに対し、pluginはデフォルト色しか使えない、null処理が必要ない、particleなどの表示がすごい楽など。(関数やclassを作ってしまえばそっちの方が楽な説もある)

それ以外にも、先ほどの例に挙げた新たに矢を召喚して飛ばすことや、アイテムを捨てられないようにする処理など、datapackでできなくはないがちょっと面倒くさいことも一瞬でできてしまう点には魅力があるでしょう。

それだったら、datapackでは手が届かないところ・負荷がかかってしまうところをpluginで補ってあげれば格段にできることが増えるのでそういう点ではとても使い勝手がいいものだと思います。軽量化も同時に達成できるし。

いろんなできること

ここからは具体例を挙げつつできることを(一部ですが)列挙したいと思います。
コードは自由に使っていただいて構いません。

なお、同じイベント(PlayerInteractEvent など)が重複することもありますが、分けて書いても問題ありません。負荷の面では誤差なので、可読性を上げるためにもシステムごとに分けると良いです。

独自のコマンドを作る

plugin.yml に定義をし、そのクラスを作ることで独自のコマンドを作れます。

plugin.yml
commands:
    advent:
        usage: "/<command> start|reset"
        description: "Minecraft Advent Calendar Command"

    advent-2:
        ...

ここに記載するのは /? などで表示される説明です。
usage = 使用方法で、<command> とすることで自動で当該コマンドに変換してくれます。また、コマンドの実行結果として false が返された場合、この usage も表示されます。

main class
@Override
public void onEnable() {
    // command
    Objects.requireNonNull(getCommand("advent")).setExecutor(new AdventCommand());
}
[class] AdventCommand
public class AdventCommand implements CommandExecutor {
    @Override
    public boolean onCommand(CommandSender sender, Command command, String commandLabel, String[] args) {
    
        // 引数チェック
        if (args.length != 1) {
            sender.sendMessage("§c引数を1つのみ入力してください");
            return false;
        }

        // args[0] == "start" の時
        if (Objects.equals(args[0], "start")) {
            // 実行者をplayerに制限する場合はこれ
            if (sender instanceof Player) ((Player) sender).performCommand("function advent:start");
            
            return true;
        }

        // args[0] == "reset" の時
        if (Objects.equals(args[0], "reset")) {
            // サーバーに実行させる場合はこっち(OP不要)
            Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "function advent:reset");
            
            return true;
        }

        // どの条件にも合致しなかった場合 false を返す
        return false;
    }
}

これで、ゲームスタートするときなどに直感的に始められます。

タブ補完をしたい場合

タブ補完用のクラスを付け加えます。

main class
@Override
public void onEnable() {
    // command
    Objects.requireNonNull(getCommand("advent")).setExecutor(new AdventCommand());
+   Objects.requireNonNull(getCommand("advent")).setTabCompleter(new AdventTabCompleter());
}
[class] AdventTabCompleter
public class AdventTabCompleter implements TabCompleter {
    @Override
    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {    // List を返すものだよ
        if (command.getName().equals("advent")) {
            // args[0]
            if (args.length == 1) {
                List<String> subCommands = Arrays.asList("start", "reset");    // 候補先のリストを作成
                List<String> completions = new ArrayList<>();    // 合致する候補を返すリストを作成   
                String partial = args[0].toLowerCase();    // 全部小文字に変えるやつ

                // 一致する候補を絞り込む
                for (String sub : subCommands) {      // すべての候補に対して
                    if (sub.startsWith(partial)) {    // 候補が入力された文字列から始まるなら
                        completions.add(sub);         // 合致した候補リストに追加
                    }
                }
                return completions;
            }

            // args[1] 以降がある場合はここに追加する
        }

        // ここにまた別のコマンドを書いてひとまとめにすることも可
        // if (command.getName().equals("advent-2")) {} ...

        // どれにも当てはまらないとき
        // 空のリストなら何も出さない、null を返せばデフォルトのプレイヤー一覧を表示する
        return new ArrayList<>();
    }
}

特定のブロックにアクセスできなくする

例えば作業台やかまどなど、右クリックしてもGUIを出さないようにできます。
具体例には、そのブロックに右クリックしたイベントが起きたとき、そのイベントをキャンセルさせます。

EventListener
    @EventHandler
    public void interactBlocks(PlayerInteractEvent e) {

        // ブロックを右クリックじゃない or null 処理
        if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
        if (e.getClickedBlock() == null) return;

        Material targetBlock = e.getClickedBlock().getType();
        
        // ここに列挙していく
        boolean notAllowedBlock = (
                targetBlock == Material.CRAFTING_TABLE ||
                targetBlock == Material.FURNACE || 
                targetBlock == Material.ENCHANTING_TABLE
        );
        
        if (notAllowedBlock) e.setCancelled(true);
    }

また、権限を持っているか・特定のチームに入っているかなども条件に加えることができます。

OP権限を持っている時のみ許可
OP権限を持ってる人のみ許可
    @EventHandler
    public void onOpenChest(PlayerInteractEvent e) {
        if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
        Block clickedBlock = e.getClickedBlock();
        if (clickedBlock == null) return;

        Material blockType = clickedBlock.getType();
        if (blockType == Material.CHEST) {
            Player player = e.getPlayer();
            if (!player.isOp()) e.setCancelled(true);
        }
    }

なお、独自の権限については plugin.yml に記述します。
commands 内のコマンドに permission を追加すれば、コマンドの処理内で hasPermission() を記述せずとも権限のチェックができます(権限がなかったときは permission-message が表示される)。

plugin.yml
commands:
  advent:
    usage: "/<command> start|reset"
    description: "advent command"
    permission: advent.master
    permission-message: "§cyou don't have permission to use this command."

permissions:
  advent.master:
    description: "advent command permission"
    default: op    # op権限を持つ人に自動的に権限を付与する

特定のチーム(advent)に入ってるときのみ許可
「advent」チームの人はチェストを開けないようにする
    @EventHandler
    public void onOpenChest(PlayerInteractEvent e) {
        if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
        Block clickedBlock = e.getClickedBlock();
        if (clickedBlock == null) return;

        Material blockType = clickedBlock.getType();
        if (blockType == Material.CHEST) {
            Player player = e.getPlayer();
            Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
            Team PlayerTeam = scoreboard.getEntityTeam(player);

            if (PlayerTeam != null && PlayerTeam.getName().equalsIgnoreCase("advent ")) {
                e.setCancelled(true);
            }
        }
    }

アイテムのクリックを検知

クリックしてのアイテム移動を制限したり、特定の名前(CustomName)のアイテムをクリックしたときに特定の処理を実行するなどができます。setCancelled() でクリック自体をなかったことにするのはもちろん、closeInventory() で強制的にGUIを閉じさせることもできるので明確にdatapackの上位互換ですね。

【注意点】
・アイテムの移動の制限は、オフハンドキーでの持ち替えには対応していないこと(あくまでマウスでのクリックのみ)
・アイテム名はplugin側では16色しか使えない(名前ではなくアイテムの種類で判別する場合は関係ない)→ ChatColor.stripColor(<String>) としたらテキストのみの比較ができる
・細かく装飾した名前(テキスト)だと検知が大変

EventListener
@EventHandler
    public void ItemClicked(InventoryClickEvent e) {

        ItemStack clickedItem = e.getCurrentItem();
        if (clickedItem == null || clickedItem.getType().isAir()) return;    // null check
        if (!(e.getWhoClicked() instanceof Player player)) return;  // player check
        
        // Inventory
        if (e.getView().getType() == InventoryType.PLAYER) {        
            // 特定のアイテムを移動できなくする
            boolean pleaseNotMove = (
                    clickedItem.getType() == Material.COMPASS || 
                    clickedItem.getType() == Material.SPYGLASS       
            );
            
            if (pleaseNotMove) {
                e.setCancelled(true);
            }
        }
        
        // chest
        if (e.getView().getType() == InventoryType.CHEST) {
            // null check
            if (!clickedItem.hasItemMeta()) return;
            ItemMeta meta = clickedItem.getItemMeta();
            if (!meta.hasDisplayName()) return;
            
            // 表示名で探索
            String itemName = meta.getDisplayName();
            
            if (itemName.equals(ChatColor.WHITE + "テスト1")) {    // {text: "テスト1", color: "white"} と同じ
                e.setCancelled(true);
                player.performCommand("function advent:click/chest/test1");
            } else if (itemName.equals(ChatColor.GOLD + "閉じる")) {    // {text: "閉じる", color: "gold"} と同じ
                player.closeInventory();
                e.setCancelled(true);
            }
        }
    }

カスタムGUIを開く

お店や設定画面などで使えるものです。datapackだと村人やcontainer(9*3/9*6)に制限されていますが、pluginなら自由な大きさ(9*n)で表示できます。

まず、表示するcontainerの内容を作成するクラスを作ります。そして、表示させたいとき(Eventlistener など)にそのクラスを呼び出すことによって表示させることができます。長いのでコード全文は折りたたみに。

[class] CreateInventory (gui作成)
CreateInventory
public class CreateInventory {
    public static Inventory openSettingList() {
        Inventory settingList = Bukkit.createInventory(null, 36, "Settings");    // 9*4

        // container と同じ
        settingList.setItem(0, settingStart());
        settingList.setItem(1, settingStop());

        return settingList;
    }

    // アイテム作成
    public static ItemStack settingStart() {
        return createItem(
                Material.RED_BANNER,    // material
                ChatColor.WHITE + "Start",    // name
                ChatColor.WHITE + "ゲームをスタートします",    // lore[0]
                ChatColor.GOLD + "* OP権限が必要");    // lore[1]
    }

    public static ItemStack settingStop() {
        return createItem(
                Material.STRUCTURE_VOID,
                ChatColor.WHITE + "Stop",
                ChatColor.WHITE + "ゲームを強制終了します",
                ChatColor.GOLD + "* OP権限が必要");
    }

    // アイテム作成テンプレ
    // 必要な引数に、見た目(material), 名前(name), 説明文(lore) を設定
    public static ItemStack createItem(Material material, String name, String... lore) {
        ItemStack item = new ItemStack(material);
        ItemMeta itemMeta = item.getItemMeta();

        if (itemMeta != null) {
            itemMeta.setDisplayName(name);
            itemMeta.setLore(Arrays.asList(lore));
            item.setItemMeta(itemMeta);
        }

        return item;
    }
}
[EventListener] 村人に話しかけたときに開く
村人に話しかけたときに開く
    @EventHandler
    public void openCustomGUI(PlayerInteractEntityEvent e) {
        if (e.getRightClicked().getType() == EntityType.VILLAGER) {
            e.setCancelled(true);        // 取引画面をキャンセル
            
            Player player = e.getPlayer();
            Inventory customGUI = CreateInventory.openSettingList();
            player.openInventory(customGUI);
        }
    }
[EventListener] エンダーチェストを開こうとしたときに開く
エンダーチェストを開こうとしたときに開く
    @EventHandler
    public void openCustomGUIWithChest(PlayerInteractEvent e) {
        if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
        if (e.getClickedBlock() == null) return;

        Material targetBlock = e.getClickedBlock().getType();

        if (targetBlock == Material.ENDER_CHEST) {
            e.setCancelled(true);        // チェスト開封をキャンセル

            Player player = e.getPlayer();
            Inventory customGUI = CreateInventory.openSettingList();
            player.openInventory(customGUI);
        }
    }

おわりに

他にも、パケット送信を色々することで特定の人にだけ発光を見せたり、透明化中に装備などを着ていないように見せて完全な透明化ができたりしますが、長いし複雑なのでplugin専門の人に任せることとします。逃げてないです

エラーが出たりやりたいことがあったらAIに聞けば何とかなります。私はGemini派です。datapackだとAIは用いにくいですが、javaは記述ルールが厳格に決まっているのでコーディングはAIに頼ってもいいと思います。

datapackもそうですが、pluginも日本語の情報が少ないのが現状です。最初の雛型の作り方はあっても実際に機能を作っている記事はほとんどありません。なのでこの記事をきっかけに興味を持ってくれたらうれしいです。(配布・共有・実践の場は少ないですが......)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?