提供: Minecraft Modding Wiki
2012年6月17日 (日) 04:19時点におけるUdonya (トーク | 投稿記録)による版 (Defining a HashMap)
移動先: 案内検索

本ページの内容は、Bukkit WikiPlugin Tutorialを和訳した物となります。(一部は省略しています)
最新ではない可能性があるため、より新しい情報を確認する場合は、本家を参照するようにして下さい。
本項目は、和訳の最中です。最後まで読むことは出来ません。

始めに

このチュートリアルは、こちらのスレッドからの転載です。当ページの原著者は、Adamki11s氏ですが、多くの編集を受けて今に至ります。

Adamki11s Bukkit Profile Page

このチュートリアルを読み終えた後は、Adamki11s'氏の"Extras" からより深い情報を得る事が出来ます。Extras library forums thread.


Note: 当ページには、原文の翻訳ではない、日本語読者向けの独自の記述を行っている箇所が少量あります。そのような内容を含む節ではNoteにて通知しています。
Note: 当ページの内容は、Bukkit用MODの作成方法の内容とは異なるアプローチによるBukkitプラグイン開発手法の解説です。
Note: 当ページでは、技術的な解説やたとえ話の原意を理解し易くするために、一部意訳を行っています。

Javaの習得

Note: 当記事の一部は原文の訳ではありません、日本語読者向けに独自の内容を記述しています。

このチュートリアルの読者には、Java言語プログラミングの基礎がわかる方を対象としています。
Javaの基礎知識が少ない方は、下記の情報に触れてください。

Javaコーディング方法

Javaがどうしても理解できない場合は、この他にも様々な資料があるので探して触れてください。

Javaの開発環境

Note: 当記事の一部は原文の訳ではありません、日本語読者向けに独自の内容を記述しています。

プラグインを開発する(またはJavaを学ぶ)前に、開発環境(IDE (Integrated Development Environment))をインストールして下さい。
開発環境は、プラグインのコンパイルとデバッグを行うために使います。
Javaの主要なIDEには、Eclipse, Netbeans, IntelliJ IDEA の3つがあります。
EclipseはBukkit開発者の中では最も人気があり、IntelliJの方は業界内で広く利用されています。

あなたがJavaの初心者なら、当チュートリアルが解説に利用しているEclipseを利用する事をお勧めします。
Eclipseのお勧めのバージョンの配布元は、日本語 Eclipse / Pleiades All in One 日本語ディストリビューションです。
これはMergedocProjectが配布する拡張されたパッケージであり、Eclipseの一次配布元が提供するパッケージではありません。

どのIDEも気に入らなければ、BlueJも利用できます。
プラグイン開発が初めてであるなら、とにかく一度Eclipseを使ってみて、
良ければNetBeansを試してみると良いでしょう。

優良なEclipseの解説。お勧めのものより古いバージョンの解説ですが、ほぼ同様です。

Plugin用プロジェクトを始めるために

プロジェクトの作成

始める前にEclipseのワークスペースとファイルの設定を行う必要があります。
Eclipseを起動し、ファイル>新規>Java プロジェクトと選択して下さい。

NewJavaProject.png

プロジェクト名を入力し、新規プロジェクトウィザード画面の指示に従って、プロジェクトを作成してください。

Bukkit APIの参照

開発を始める前にbukkit APIライブラリをプロジェクトに追加する必要があります。 使用したい他のAPIも同じように追加することが可能です。

最新のBukkit APIはここからダウンロードが可能です。 Bukkit API - Development Snapshot


画面の左手にあるパッケージエクスプローラの(先ほど名前を付けた)プロジェクトを右クリックし、プロパティーを選択します。
開いた画面のJavaビルド・パスを選択し、ライブラリータブの中から、外部 Jar 追加ボタンを押して、ダウンロードしたBukkit APIを指定します。

BuildPathPic.png

BukkitのJavadocの利用方法

Eclipseを用いたJavaプログラミングの経験がある方なら、
Eclipseのエディタ上でクラスやメソッドにマウスカーソルを重ねた時に、
クラスやメソッドに関するドキュメントがポップアップで表示される事をご存知でしょう。
これは、Oracleのウェブサイトから得る事ができるJavadocの内容が表示されています。

Bukkitもまた、BukkitのAPIが提供するクラスやメソッドの各々のコードに同種のドキュメントを含んでおり、ポップアップで表示させる事ができます。
Eclipse上で、Bukkitのクラスやメソッドにマウスを重ねたタイミングでポップアップを表示できるようにするためには、下記の手順を行います。

  1. プロジェクトエクスプローラ上で、Bukkitのjarファイルを右クリックして、メニューを開く。
  2. "プロパティ"を選択し、表示されるポップアップ左側の"Javadoc ロケーション"項目を選択する。
  3. "Javadoc URL"の下部にあるテキストボックスに、"http://jd.bukkit.org/apidocs/" (ダブルクォートは除く)を貼り付ける。
  4. "検証"ボタンを押下し、URLがJavadocとして正しく識別される事をチェックしてから、OKボタンを押す。
次ような画面になります:
Bukkitjavadocs.png

Plugin開発の開始

MakePackage.png

onEnable() and onDisable()

このファンクションは、プラグインが有効/無効になったときに呼ばれます。
デフォルトでは、プラグインは自動的に読み込まれたときに、イベントを登録やデバッグ出力を行うことが出来ます。
onEnable()は、プラグインがBukkitから読み込まれるときに最初に呼ばれ、プラグインを実行するために必須です。

onEnable()とonDisable()の基本

前のセクションで作成したメインクラスに、onEnable()とonDisable()のメソッドを作成します。

public void onEnable(){ 
 
}
 
public void onDisable(){ 
 
}


現時点では、このメソッドは何も行いません。また、エラーが発生する事にも気付くでしょう。このエラーは、メインクラスが、プラグインの機能を継承(extends)する必要がある事を示しています。当クラスの定義文を、下記のように変更しましょう。

これを・・・

Class <classname> {}

このように変更する。

Class <classname> extends JavaPlugin {}

すると、前述の追加コードが赤の下線でハイライト表示され、何かが間違っていることを通知してくるはずです。このハイライト部にマウスを重ねると、下記のようなポップアップが表示されるので、「'JavaPlugin'をインポートします(org.bukkit.plugin.java)」を選択します。

To need import package.png

もしくは、

import org.bukkit.plugin.java.JavaPlugin;

をコード上部の定義部に記述する事でも同様の事が行えます。

Loggerを利用したメッセージ出力

始めに、プラグインが実際に動作するかどうかを確認するため、シンプルなメッセージをサーバコンソールに表示させてみましょう。この処理として、ログ出力機能(Logger)をメインクラスに定義して初期化します。

Logger log = this.getLogger();

その後、onEnable()メソッドに、プラグインが動作している事を通知するメッセージを出力する処理を記述します。

log.info("Your plugin has been enabled.");

onDisable()メソッドについても同等の記述を行います。ここまでのコーディングによって、メインクラスは下記の様な内容になっているはずです。

package me.<yourname>.<pluginname>;
 
import java.util.logging.Logger;
import org.bukkit.plugin.java.JavaPlugin;
 
public class <classname> extends JavaPlugin {
 
	Logger log;
 
	public void onEnable(){
		log = this.getLogger();
		log.info("Your plugin has been enabled!");
	}
 
	public void onDisable(){
		log.info("Your plugin has been disabled.");
	}
}

イベントとリスナ

新しいEventSystemの使い方を参照して下さい

コマンド

onCommand()メソッド

さて、どのようにイベントを登録しいつ発生するかについて理解しました。 しかし、コマンドを利用して何かを起こしたい場合はどのようなデータ型を利用すればよいのでしょうか?それには、onCommand()メソッドを利用します。 このメソッドは、プレイヤーが文字"/"を入力する度に実行されます。 具体的には、"/do something"とコマンドを実行した場合にonCommand()が呼び出されます。 今のところ、onCommand()はまだプログラムしていないので何も起こらないでしょう。

コマンド名は、Bukkitや他のプラグインが提供しているものや、 提供するであろうものとの重複を避け、 ユニークなものとなるように考慮して下さい。 具体的には、"give"コマンドは既にいくつかのプラグインで利用されています。 もし独自に"give"コマンドを実装した場合は、 "give"コマンドを実装している他のプラグインとの互換性は無くなります。

onCommand()は、常にboolean型の値としてtrue,falseのどちらかを戻り値として返さねばなりません。 trueを返した場合は、情報表示のためのイベントは発生しません。 falseを返した場合は、プラグインファイルを"usage: property"に戻し、コマンドを実行したプレイヤーに、コマンドの利用方法を通知するメッセージを表示します。

onCommand()を利用する際は、4つのパラメータを登録しなければなりません。

  • CommandSender sender - コマンドの発行元
  • Command cmd - 実行されたコマンドの内容
  • String commandLabel - 利用されたコマンドエイリアス
  • String[] args - コマンドの引数を格納した配列(例:/hello abc defコマンドが入力された場合の内容は、args[0]がabc、args[1]がdefとなる)

コマンドの設定

public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
        // プレイヤーが「/basic」コマンドを投入した際の処理...
	if(cmd.getName().equalsIgnoreCase("basic")){ 
		// 何かの処理
		return true;
                // コマンドが実行された場合は、trueを返して当メソッドを抜ける。
	}
	return false; 
        // コマンドが実行されなかった場合は、falseを返して当メソッドを抜ける。
}

onCommand()をコーディングする際は、上記の例のように、メソッドの最終ステップにfalseをreturnする行を記述するのが良い方法です。
falseが返る事で、plugin.yml(下記参照)内に記述されたメッセージが表示され、
onCommand()処理が正しく動作しなかった事をメッセージから検知できるため、動作確認の助けになるからです。
逆に、メソッドの最後でtrueを返す処理構造にしてしまった場合は、onCommand()内の各処理に対して、
処理結果チェック処理と結果が不正である場合にfalseを返すような、同じ処理を何度も記述する必要が出てきますので、大変な無駄となります。

なお、コード「.equalsIgnoreCase("basic")」のパラメタ「basic」は、大文字小文字の区別はありません。

plugin.ymlへのコマンドの追加

コマンドを用意する際は、plugin.ymlファイルにも定義を追加する必要があります。plugin.ymlの最後に次の行を追加します。

commands:
   basic:
      description: This is a demo command.
      usage: /<command> [player]
      permission: <plugin name>.basic
      permission-message: You don't have <permission>
  • basic - コマンド名
  • description - コマンドの説明文
  • usage - onCommand()がfalseを返した際に、コマンド実行ユーザに向けて表示されるメッセージの内容。コマンドの使い方を明解に書いてください。
  • permission - 当コマンドの動作に必要なプラグインの設定を、コマンド実行ユーザに知らせるメッセージ。(主に、コマンド実行に必要なpermissionを書きます)
  • permission-message - コマンド実行権限を持たないユーザがコマンドを実行した場合に、その旨を同ユーザに知らせるメッセージ。
Note: ymlファイルには、1個タブを2個のスペースで記述する必要があります。タブ文字は構文エラーとなるため利用できません。

コンソールコマンドとプレイヤーコマンド

察しの良い人は、CommandSender sender パラメタに着目しているかもしれません。 CommandSenderクラスは、プラグイン開発者に2つの点で有用な、Bukkitが提供するインタフェースです。

CommandSenderインタフェースの実装: PlayerConsoleCommandSender (正確には Playerもインタフェース)です。 プラグインを作る際は、次の2点に注意しながら作る事が非常に良い方法です。

  • プラグインが動作している事をサーバコンソールで確認しながら作る事
  • ログインプレイヤー向けのコマンドが、実際にログインしているプレイヤーのみ実行できる事

プラグインは、senderがプレイヤーではない場合(例えばコンソールからプラグインのコマンドが投入された場合)であっても、サーバコンソールには明解でシンプルな動作結果を返します。(例えば天気を変えるコマンドの実行結果は、ゲーム内では無くサーバコンソール上のメッセージから確認すれば間違いありません)

記述例:

public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
	Player player = null;
	if (sender instanceof Player) {
		player = (Player) sender;
	}

	if (cmd.getName().equalsIgnoreCase("basic")){ // If the player typed /basic then do the following...
		// do something...
		return true;
	} else if (cmd.getName().equalsIgnoreCase("basic2")) {
		if (player == null) {
			sender.sendMessage("this command can only be run by a player");
		} else {
			// do something else...
		}
		return true;
	}
	return false;
}

この例では、basic コマンドはログインプレイヤーとサーバコンソールのどちらからでも実行できます。しかし、basic2 コマンドは、ログインプレイヤーしか実行できません。 一般的に、コマンドはプレイヤーとサーバコンソールの両者に実行を許可しますが、投入されたコマンドの実行可否をチェックする必要があるコマンドについては、ログインプレイヤー向けのコマンドとして実装されるべきでしょう。

つまり、プレイヤーに依存する処理(テレポートコマンドやアイテムを与えるコマンド等は、対象となるプレイヤーが必要です)を行うコマンドが、プレイヤーコマンドとして実装されるべきものと言えます。

凝ったコマンドを作りたい場合、コマンドのパラメタ(上記例では argsパラメタ)を利用して独自の処理実装を行うことができます。例えば、プレイヤー名を指定したテレポートコマンドのみが、サーバコンソールから実行できる実装にする等が考えられます。

訳者補記: コマンド実装方法には、プレイヤー向け・サーバコンソール向けの2種類があり、それぞれの区別はどのような考え方で行うべきかについて説明しています。

CommandExecutorのクラス分割

上記の例では、onCommand()メソッドをプラグインのメインクラスに記述していました。小さなプラグインでは良い方法ですが、大きなプラグインに拡張していくのであれば、適切なクラスを作成してそちらに配置するべきです。幸い、難しい事ではありません:

  • プラグインのpackage内に、MyPluginCommandExecutor のような名前で新規のコマンド実行クラスを作成する(当然、MyPluginはあなたのプラグイン名に合わせて変えましょう)。
  • MyPluginCommandExecutorに、BukkitのCommandExecutorインタフェースを実装(implements)させる。
  • プラグインのonEnable()メソッドの処理で、MyPluginCommandExecutorクラスのインスタンスを生成する。
  • MyPluginCommandExecutorインスタンスをパラメタとして、処理getCommand("basic").setExecutor(myExecutor);を実行させる。
Note: "basic"は実行されたコマンドであり、myExecutorはMyPluginCommandExecutorインスタンスです。

とっても良い具体例:

MyPlugin.java (the main plugin class):

private MyPluginCommandExecutor myExecutor;
@Override
public void onEnable() {
	// ....
 
	myExecutor = new MyPluginCommandExecutor(this);
	getCommand("basic").setExecutor(myExecutor);
 
	// ...
}
MyPluginCommandExecutor.java:

public class MyPluginCommandExecutor implements CommandExecutor {
 
	private MyPlugin plugin;
 
	public MyPluginCommandExecutor(MyPlugin plugin) {
		this.plugin = plugin;
	}
 
	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		// ... implementation exactly as before ...
	}
}

どのように、メインプラグインのインスタンスがMyPluginCommandExecutorのコンストラクタを実行するかに注目しましょう。

この節で紹介した方法により、メインのonCommand()メソッドが巨大で複雑になったとしても簡単に整理する事ができ、結果として、プラグインのメインクラスを複雑化させずに、処理分割する事ができます。

Note: プラグインが複数のコマンドを持つ場合、個々のコマンドに対応するCommandExecutorをコーディングする必要があります。
訳者補記: 積極的に、大きな処理は小さく分割していきましょう。そのための仕組みをBukkitが用意しています。という事を教示しています。

堅牢なonCommandの記述

onCommand()メソッドを記述する際、パラメタの利用に関して、
決め付けて掛かってはいけない事がありますので念頭において下さい。
これらに留意した処理を記述する事で、堅牢なonCommand()をコーディングする事ができます。

senderの内部データ型をチェックする

senderの内部データがPlayer型であるとは限らない事を念頭において、チェックを行って下さい。
処理例:

public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
	if ((sender instanceof Player)) {
           // doSomething
        } else {
           sender.sendMessage(ChatColor.RED + "You must be a player!");
           return false;
        }
        Player player = (Player) sender;
        return false;
}

コマンドのパラメタ長をチェックする

senderインスタンスは、妥当なパラメタを持っているとは限りません。パラメタ長をチェックして下さい。
処理例:

public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
	if (args.length > 4) {
           sender.sendMessage(ChatColor.RED + "Too many arguments!");
           return false;
        } 
        if (args.length < 2) {
           sender.sendMessage(ChatColor.RED + "Not enough arguments!");
           return false;
        }
}

プレイヤーがオンラインである事を確認する

特定のプレイヤーのPlayerインスタンスを利用したい場合、
必ずそのプレイヤーがオンラインである必要があります。
オンラインであるかどうかをチェックして下さい。
処理例:

public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){
	Player other = (Bukkit.getServer().getPlayer(args[0]));
        if (other == null) {
           sender.sendMessage(ChatColor.RED + args[0] + " is not online!");
           return false;
        }
        return false;
}

オンラインではないプレイヤーのインスタンスに操作を加えたい場合は、一般的にはOfflinePlayerクラスを利用して行う事ができます。

プラグインのConfiguration/Settings

新しいConfigurationの使い方を参照して下さい

権限

Bukkitのパーミッション(権限)APIの利用は、簡単ではありません。

プレイヤーが特定の権限を持っているかどうかを調べる処理は次のようになります:

if(player.hasPermission("some.pointless.permission")) {
   //Do something
}else{
   //Do something else
}

権限がセットされているか、いない(Javaのnullと同等)かを調べる処理は次のようになります:

boolean isPermissionSet(String name)

なぜグループの概念が存在しないかと思った方もいるでしょうが、 そもそも、権限にはグルーピングの概念は不要なのです。

元々、グループの主な用途はチャットメッセージをフォーマッティングする事にありました。
これは権限の機能を利用してもっと簡単に行えます。
例えば、チャットプラグインの設定において、権限とプレフィクスの関連を定義する事が該当します。
具体的には、権限"someChat.prefix.admin"をプレフィクス[Admin]に対応させる定義を行い、
プレイヤーがチャットで発言する度に、プレイヤー名の先頭に[Admin]が付くようになる機能が挙げられます。

他にも、グループに所属する複数のユーザに、
メッセージを送信するような機能を実現するために利用する事が考えられます。
この例を、権限を利用した処理で記述すると以下のようになります:

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

    if(player.hasPermission("send.recieve.message")) {
        player.sendMessage("You were sent a message");
    }

}

さて、依然として
「グループを利用せずに、複数のプレイヤーに権限をセットする良い方法は何なのか?」が疑問かと思いますが・・・
BukkitのAPI自体は、グループの概念を提供していません。
グループの概念を利用するためには、permissionsBukkitのような
グループ権限の機能を提供するプラグインを利用する事になります。
とどのつまり、このAPIはインタフェース(Interface)であり、実装(Implementation)ではないのです。

権限の設定

権限を利用して細かな制御を行いたい場合は、
デフォルト権限と、子権限の設定をplugin.ymlに追記する事を検討して下さい。
この2種類の設定は、オプション(必須ではなく完全に任意で利用される)項目ではありますが、お勧めします。

下記は、plugin.ymlの最後に追加する形で設定する権限です:

permissions:
    doorman.*:
        description: Gives access to all doorman commands
        children:
            doorman.kick: true
            doorman.ban: true
            doorman.knock: true
            doorman.denied: false
    doorman.kick:
        description: Allows you to kick a user
        default: op
    doorman.ban:
        description: Allows you to ban a user
        default: op
    doorman.knock:
        description: Knocks on the door!
        default: true
    doorman.denied:
        description: Prevents this user from entering the door


まず、プラグインが利用する各権限を、permissionsノードの子ノードとして定義します。
それぞれの権限はオプションとして、概要(description)、デフォルト値、子ノードを持ちます。

デフォルト権限

プレイヤーが権限を持たない場合、 hasPermission はデフォルトではfalseを返します。
plugin.yml中のデフォルトのノードに対して、下記の4つのどれか1つを設定する事で、
この挙動を変更する事ができます:

  • true - デフォルトで、権限を持つ(=hasPermissionがtrueを返す)。
  • false - デフォルトで、権限を持たない(=hasPermissionがfalseを返す)。
  • op - プレイヤーがOPであれば、権限を持つ(=hasPermissionがtrueを返す)。
  • not op - プレイヤーがOPでなければ、権限を持つ(=hasPermissionがtrueを返す)。

子権限

恐らく今までに、* 権限を利用してサブ権限を割り当てた事があると思います。
これは、変更されたBukkit APIと、子権限の定義によって実現した機能であり、
高い柔軟性を提供しています。
下記はその実装例です:

permissions:
    doorman.*:
        description: Gives access to all doorman commands
        children:
            doorman.kick: true
            doorman.ban: true
            doorman.knock: true
            doorman.denied: false

doorman.*権限は、いくつかの子権限を含んでいます。 doorman.*権限がtrueである場合に、 その子権限はplugin.ymlに定義された値(trueかfalse)をデフォルト権限として機能します。 doorman.*権限がfalseである場合は、 その子権限のデフォルト権限は、全て反転(trueが定義値ならfalseになる)された状態で機能します。

独自の権限設定

独自に権限の仕組みを提供するプラグインを開発したい場合、 権限プラグインの開発方法を参考にして下さい。

スケジューリングタスクとバックグラウンドタスク

現在、Minecraftサーバはゲームロジックのほとんどがシングルスレッドで稼動しています。
このため、発生する個々の処理はごく小さいものにする必要があります。
プラグイン中に複雑なコードが存在して、それが適切に処理されないような場合は、
ゲームロジックに多大なラグや処理遅延を発生させる原因となります。

幸運なことに、Bukkitはプラグインに対してスケジューリングのためのコーディング方法を提供しています。
どこかのタイミングで一度だけRunnableタスクを実行したり、
小さなタスクに分けた定期的な繰り返しであったり、
新規に独立したスレッドを派生させたり・・・
といった複数の方法で、長大なタスクをゲームロジックと並行で処理させる事ができます。

詳しくは、スケジューラのプログラミング方法中の、同期タスクと非同期タスクのスケジューリング方法に関する解説を参考にして下さい。

ブロックの操作

ブロックを生成する簡単な方法は、既存のブロックを取得して変更する事です。
例えば、あなたの5ブロック上方にあるブロックを変更したい場合、
まずはそのブロックを取得した上で、変更を加えることになります。
PlayerMoveイベントの処理中でこれを行う例を示します:

public void onPlayerMove(PlayerMoveEvent evt) {
	Location loc = evt.getPlayer().getLocation(); // プレイヤーからその位置を得る
	World w = loc.getWorld(); // 位置からワールドを得る
	loc.setY(loc.getY() + 5); // 位置のY座標を+5する
	Block b = w.getBlockAt(loc); // ワールド上の指定位置のブロックを得る
	b.setTypeId(1); // ブロックのタイプIDに1をセットする
}

このコードの処理は、プレイヤーが移動する(=PlayerMoveイベントが発生する)度に、
プレイヤーの5ブロック上方のブロックがStone(TypeIdが1のブロック)に変化する動作として見えるでしょう。
ブロック取得までの流れは・・・

  1. 取得したプレイヤーの位置から、ワールドを取得する
  2. 位置のY座標を+5する
  3. w.getBlockAt(loc);で、ワールドにおける位置に存在するブロックを得る

となります。

位置(loc)中のブロックインスタンス(b)に対して、
IDを変えたり、ブロックデータ自体を変更する事もできます。

また、ブロックの種類を表すデータはbyte型であるため、
byte型にキャストしたデータを与えた指定ができます。
例えば、b.setData((byte)3); です。


建物の生成や、一定のアルゴリズムに従ったブロック生成処理などを行う事ができます。
例えば、固体ブロックによる立方体を生成する処理は、
3階層にネストしたforループ処理によって記述できます。

public void generateCube(Location point, int length){  // public visible method generateCube() with 2 parameters point and location
	World world = point.getWorld();

	int x_start = point.getBlockX();     // Set the startpoints to the coordinates of the given location
	int y_start = point.getBlockY();     // I use getBlockX() instead of getX() because it gives you a int value and so you dont have to cast it with (int)point.getX()
	int z_start = point.getBlockZ();

	int x_lenght = x_start + length;    // now i set the lenghts for each dimension... should be clear.
	int y_lenght = y_start + length;
	int z_lenght = z_start + length;

	for(int x_operate = x_start; x_operate <= x_length; x_operate++){ 
		// Loop 1 for the X-Dimension "for x_operate (which is set to x_start) 
		//do whats inside the loop while x_operate is 
		//<= x_length and after each loop increase 
		//x_operate by 1 (x_operate++ is the same as x_operate=x_operate+1;)
		for(int y_operate = y_start; y_operate <= y_length; y_operate++){// Loop 2 for the Y-Dimension
			for(int z_operate = z_start; z_operate <= z_length; z_operate++){// Loop 3 for the Z-Dimension

				Block blockToChange = world.getBlockAt(x_operate,y_operate,z_operate); // get the block with the current coordinates
				blockToChange.setTypeId(34);    // set the block to Type 34
			}
		}
	}
}

このメソッドは、辺の長さと開始点の指定を受けて、任意のサイズと位置を持つ直方体を生成する処理です。
ブロックの削除処理の場合も、このロジックを真似て同様のアルゴリズムで実装する事ができます。
ただし、セットするブロックのIDはゼロ(=air)になりますね。

プレイヤーインベントリの操作

この節では、プレイヤーのインベントリ操作を特に扱っていますが、
チェストのインベントリの操作にも適用できますので、必要なら応用して下さい(゚∀゚)
インベントリ操作のシンプルな例:

public void onPlayerJoin(PlayerJoinEvent event) {
    Player player = event.getPlayer(); // Joinしたプレイヤー
    PlayerInventory inventory = player.getInventory(); // プレイヤーのインベントリ
    ItemStack diamondstack = new ItemStack(Material.DIAMOND, 64); // 山積みのダイヤモンド!

    if (inventory.contains(diamondstack)) {
        inventory.addItem(diamondstack); // プレイヤーインベントリに山積みのダイヤモンドを加える
        player.sendMessage(ChatColor.GOLD + "よく来たな!もっとダイヤモンドをくれてやろう、このとんでもない成金め!!");
    }
}

さて、onPlayerJoin()メソッドの先頭で
player, inventorydiamondstack変数を用意して下さい。
inventoryはプレイヤーのインベントリで、diamondstackは(アイテムとしての)64個のダイヤモンドです。

次に、プレイヤーのインベントリがダイヤモンドを含んでいるかをチェックします。
プレイヤーがダイヤモンドをインベントリに所持している場合、
inventory.addItem(diamondstack)処理にて
同プレイヤーのインベントリに別のスタックを与え、黄金色のメッセージを送信します。
インベントリ操作は全然難しくありません。

ダイヤモンドのスタックを削除したい場合でも単純に、
inventory.addItem(diamondstack)
inventory.remove(diamondstack)に置き換えるだけです。
(メッセージにも少しイタズラしておきましょう)

アイテムの操作

アイテムはスタックという単位で操作します。
スタックデータに対する全ての操作は、ItemStackクラスの仕様を参照して下さい。

エンチャント

アイテムに対するエンチャントに触れる前に、Item CodeEIDを見てから以下の解説を読んでください。

エンチャントは、Enchantmentクラスが受け持っている機能ですが、
Enchantmentクラス自体は抽象クラスであるため、インスタンス化(new Enchantment())出来ません。 エンチャントはEnchantmentWrapperクラスから利用する必要があるからです。

元々エンチャントが出来ないアイテムに対して、独自にエンチャントを付与する処理は書けません。
Bukkitサーバ自体が、エンチャント不可能なアイテムに対して、エンチャント情報を付与する仕組みを備えていないからです。
つまり、Fire AspectedなStick(火のエンチャントが付いた木の棒)はあり得ません。

int itemCode = 280;  // StickのItemID
int effectId = 20;  // 効果Fire AspectのEID
int enchantmentLevel = 100; // エンチャントのレベル

ItemStack myItem = new ItemStack(itemCode);  // 木の棒のインスタンスを生成する
Enchantment myEnchantment = new EnchantmentWrapper(effectId);  // エンチャント効果のインスタンスを生成する
myItem.addEnchantment(myEnchantment, enchantmentLevel);  // 木の棒にFireAspectを付与する(ただし付与は成功しない)

HashMapの応用

Note: このセクションは原文が独特な書き回しだったため意訳を行っています。

複数のプレイヤーが発するアクションやイベントを処理するためには、
イベントの発生や状態を保持するための領域として単一の変数を利用していたのでは不十分です。

例を挙げると、
私が作った古いプラグインのZones(今はRegionsに改名して処理も改善しています)では、
プラグインがサーバ上で実際にどのように振舞うかに関して考慮し切れていなかったために、
多くのエラーに直面しました。
具体的には、プレイヤーが特定の領域内に居たかどうかをチェックする処理で
チェック結果の格納先として単一のboolean値を利用したため、
複数のプレイヤーそれぞれに対するチェックを個別に保持する事が出来ずに膨大なエラーを引き起こしたのです。

HashMapはこのような問題を解決する素晴らしい方法です。
HashMapは、データを「キーのペア」の集まりとして保管できるデータ保持機能です。
これを利用するメリットは、HashMapが下記の仕様を備えている事です。

  • 1つのキーに対して、必ず1つの値が対応する
  • 同じ内容のキーが、同一のインスタンス内に重複して存在できない

つまり、キーがプレイヤー、値がプレイヤーに紐付く情報の保管先になるようにHashMapを利用する事で、
プレイヤーとプレイヤーに紐付く値が強制的にペア(対)で管理できますし、
プレイヤーがインスタンス内で重複し得ない(ゲーム内で同一のプレイヤーは重複し得ない)事を保証できるのです。

具体的には、"TaroYamada"がキーとなり、"False"が値となります。
また、後からキー"TaroYamada"に対応する値を"True"に変更する事もできます:

import java.util.HashMap;
public class Sample{
  public Sample(){
    HashMap<String, Boolean> playerFlags = new HashMap<String, Boolean>();
    playerFlags.put("TaroYamada", false);
    playerFlags.put("TaroYamada", true);
    playerFlags.put("GorillaMatsui", true);
  }
}
Note: 理解のために独自に追加したコードです。

Defining a HashMap

public Map<Key, DataType> HashMapName = new HashMap<Key, Datatype>(); //Example syntax

//Example Declaration

public Map<Player, Boolean> pluginEnabled = new HashMap<Player, Boolean>();
public Map<Player, Boolean> isGodMode = new HashMap<Player, Boolean>();

このコードは、以降のHashMapsの説明で使うので覚えてください。 さて、例としてプラグインが有効・無効化する時に、天候を切り替える簡単な処理を作りましょう。 まずはonCommand()の中に、前述の説明と同様にプレイヤーの状態に応じて、プレイヤー名を送信する処理を記述します。

onCommand()には下記を記述します。 読んでいるメソッドの名前は変えても良いですが、意味が通じるシンプルなものにして下さい。

Player player = (Player) sender;
togglePluginState(player);

上記のコードは、senderをPlayer型にキャストしたものをパラメタとしてtogglePluginState()に与えています。 では、togglePluginState()を実装しましょう。

public void togglePluginState(Player player){
    
    if(pluginEnabled.containsKey(player)){
        if(pluginEnabled.get(player)){
            pluginEnabled.put(player, false);
            player.sendMessage("Plugin disabled");
        } else {
            pluginEnabled.put(player, true);
            player.sendMessage("Plugin enabled");
        }
    } else {
        pluginEnabled.put(player, true); //If you want plugin enabled by default change this value to false.
        player.sendMessage("Plugin enabled");
    }

}

このコードが何をやっているかというと、 まず、HashMapのpluginEnabledplayerをキーとして持っているかと、 次に、値がtruefalseのどちらなのかを調べています。

playerがキーに含まれており、かつ 値がtrueであれば、falseにリセットしてプレイヤーへメッセージを送信します。 falseであれば、trueにリセットして同様にメッセージを送信します。

playerがキーに含まれていない場合、 今処理をplayerが初めて行った当処理の実行とみなして、 HashMapへプレイヤーと値のペアを追加し、メッセージを送信します。

More Ideas for HashMaps

A HashMap (or really any kind of Map in Java) is an association. It allows quick and efficient lookup of some sort of value, given a unique key. Anywhere this happens in your code, a Map may be your solution.

Here are a few other ideas which are ideally suited to using Maps. As you will see, it doesn't have to be data that you store per player, but can be any kind of data that needs to be "translated" from one form to another.

Data Value Lookups

public Map<String, Integer> wool_colors = new HashMap<String, Integer>();

// Run this on plugin startup (ideally reading from a file instead of copied out row by row):
wool_colors("orange", 1);
wool_colors("magenta", 2);
wool_colors("light blue", 3);
   ...
wool_colors("black", 15);

// Run this in response to user commands - turn "green" into 13
int datavalue = 0;
if (wool_colors.containsKey(argument)
    datavalue = wool_colors.get(argument);
else {
    try { datavalue = Integer.parseInt(argument); }
    catch (Exception e) { ; }
}

Saving/Loading a HashMap

Once you know how to work with HashMaps, you probably want to know how to save and load the HashMap data. Saving and loading HashMap data is appropriate if

  • you don't want an administrator to edit the data manually
  • you need to save data in binary format (too complex to organize for YAML)
  • you want to avoid parsing block names and/or other objects from freeform text

This is very simple way how to save any HashMap. You can replace HashMap<Player,Boolean> with any type of HashMap you want. Let's continue using the "pluginEnabled" HashMap defined from the previous tutorial. This code saves the given HashMap to the file with given path.

public void save(HashMap<Player,Boolean> pluginEnabled, String path)
{
	try{
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
		oos.writeObject(pluginEnabled);
		oos.flush();
		oos.close();
		//Handle I/O exceptions
	}catch(Exception e){
		e.printStackTrace();
	}
}

You can see it's really easy. Loading works very very similar but we use ObjectInputStream instead of ObjectOutputStream ,FileInputStream instead of FileOutputStream,readObject() instead of writeObject() and we return the HashMap.

public HashMap<Player,Boolean> load(String path) {
	try{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
		Object result = ois.readObject();
		//you can feel free to cast result to HashMap<Player,Boolean> if you know there's that HashMap in the file
		return (HashMap<Player,Boolean>)result;
	}catch(Exception e){
		e.printStackTrace();
	}
}

You can use this "API" for saving/loading HashMaps, ArrayLists, Blocks, Players... and all Objects you know ;) . Please credit me (Tomsik68) if you use this in your plugin.

/** SLAPI = Saving/Loading API
 * API for Saving and Loading Objects.
 * @author Tomsik68
 */
public class SLAPI
{
	public static void save(Object obj,String path) throws Exception
	{
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
		oos.writeObject(obj);
		oos.flush();
		oos.close();
	}
	public static Object load(String path) throws Exception
	{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
		Object result = ois.readObject();
		ois.close();
		return result;
	}
}

Example implementation of this API: I'm skipping some part of code in this source

public class Example extends JavaPlugin {
	private ArrayList<Object> list = new ArrayList<Object>();
	public void onEnable()
	{
		list = (ArrayList<Object>)SLAPI.load("example.bin");
	}
	public void onDisable()
	{
		SLAPI.save(list,"example.bin");
	}
}

A minor note about this SLAPI and Java's ObjectOutputStream class. This will work un-modified if you are saving almost all well-known Java types like Integer, String, HashMap. This will work un-modified for some Bukkit types as well. If you're writing your own data object classes, and you may want to save their state using this technique, you should read up on Java's Serializable interface. It's easy to add to your code, and it will make your data persistent with very little work on your part. No more parsing!

Map・Set・Listの応用

データベース

プラグインの配布

ヒントとノウハウ