Vim Advent Calendar 2012 の257日目の記事です。昨日の記事はtyruさんで、明日の記事はIMAGEDRIVEさんの予定で・・・したが、著者の諸事情により、260日目の記事になりました。昨日の記事はcohamaさんで、明日の記事はtyruさんの予定です。
なお、8月16日はtyruさんの誕生日で・・・したが日程がずれた関係であまり本記事に必然性のない日付になりました。また、数回前の記事の日、6月11日はujihisaさんの誕生日でした。
さて、前回はMinecraftにおける開発においての概要を説明しました。
Minecraftはclient-server型モデルの系です。MinecraftをAさんとBさんとCさんがプレイするためには、たとえばAさんがserverを起動し、AさんとBさんとCさんそれぞれがclientを起動した上でそのサーバに接続します。serverは世界全体のデータの保存や、世界のルールの処理をします。clientはプレイヤの操作を受け付け、また描画を担当します。MVCモデルにおけるMをserverが行いVCをclientが行なっているととらえることもできるでしょう。
MinecraftそのものはJavaで記述されており、server, clientともにソースコードは公開されておらず、コンパイル後のjarファイルが提供されています。
興味深いことに、serverに関する機能をpluginで一部書き換えることができます。このためのAPIは一般公開されており、server側がアップデートされても安定して利用することができます。
また、client側に関する機能も有志による分解・解析が行われており、それにたいするAPIも提供されています。server側ほどではありませんが、そこそこ安定して利用することができます。
このうち著者がとくに重要だと思うのが前者、serverに関する挙動の書き換えです。MVCのうちMが最も重要である、つまりロジックの組み換えが最も柔軟性が高いと考えるからです。なお、このserverそのものの実装はCraftBukkitと呼ばれており、そのAPI (JVM用のInterfaceのあつまり) はBukkitと呼ばれています。
今回は詳細について説明しましょう。
前回はClojureとJRubyを用いたBukkit pluginの例の抜粋をお見せしました。しかしながら前回のコードだけでは実際には動作しません。今回は、床下配線部分に着目したいと思います。
Bukkit pluginを作るというのは、以下の2つを行うという意味です。
- イベントハンドラを定義した、JVMで動作可能なclassを作る
- そのclassと、plugin.ymlという設定をまとめたjarファイルを作る
Clojureを用いるので、最後のjar化についてはLeiningenを用いてlein uberjar
で一発です。そこまでの部分を詳しく解説しましょう。
イベントハンドラについてはbukkitのtutorialを参考にしましょう。
http://wiki.bukkit.org/Event_API_Reference
public class MyPlayerListener implements Listener {
@EventHandler
public void onPlayerLogin(PlayerLoginEvent event) {
// Your code here...
}
}
このようにListener
インタフェースを実装したクラスを作り、そこで@EventHandler
というアノテーションのついたメソッドを定義することで、引数の型に応じたイベントをフックできるようになります。とても遠回りな仕様のようです。
アノテーションのせいでPure Clojureから素直に実装することができません。ここは潔く諦め、Javaで床下配線を行いましょう。
package com.github.ujihisa.VAC2012;
import java.io.File;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.PluginLoader;
import java.util.HashSet;
import java.net.URLClassLoader;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.Server;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Event;
import java.lang.ClassLoader;
public class UjihisaPlugin extends JavaPlugin implements Listener {
private String ns;
public void onEnable() {
String name = getDescription().getName();
this.ns = name + ".core";
System.out.println("Enabling " + name + " clojure Plugin");
invokeClojureFunc("on-enable", this);
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onAsyncPlayerPreLogin(org.bukkit.event.player.AsyncPlayerPreLoginEvent event) {
clojure.lang.Var f = clojure.lang.RT.var(ns, "async-player-pre-login-event");
if (f.isBound()) f.invoke(event);
}
// !! Other event handlers come here as well
private void invokeClojureFunc(String enableFunction, Object arg) {
try {
ClassLoader previous = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
clojure.lang.RT.loadResourceScript(ns.replaceAll("[.]", "/")+".clj");
clojure.lang.RT.var(ns, enableFunction).invoke(arg);
Thread.currentThread().setContextClassLoader(previous);
} catch (Exception e) {
System.out.println("Something broke setting up Clojure");
e.printStackTrace();
}
}
このようにしてBukkitのAsyncPlayerPreLoginEvent
とClojureでのasync-player-pre-login-eventをひもづけることができました。あとはそれをいい感じに定義するだけです。
唐突に出てきたAsyncPlayerPreLoginEvent
ですが、具体的にどのようなイベントが利用可能なのでしょうか? Javaで困ったときはjavadocです。
BukkitにおけるすべてのイベントはEvent interfaceを継承しています。
Direct Known Subclasses:
AsyncPlayerPreLoginEvent, BlockEvent, EntityEvent, HangingEvent, InventoryEvent, InventoryMoveItemEvent, InventoryPickupItemEvent, PaintingEvent, PlayerEvent, PlayerLeashEntityEvent, PlayerPreLoginEvent, ServerEvent, VehicleEvent, WeatherEvent, WorldEvent
そして試しにBlockEventの詳細を見てみますと、そこには
Direct Known Subclasses:
BlockBurnEvent, BlockCanBuildEvent, BlockDamageEvent, BlockDispenseEvent, BlockExpEvent, BlockFadeEvent, BlockFromToEvent, BlockGrowEvent, BlockIgniteEvent, BlockPhysicsEvent, BlockPistonEvent, BlockPlaceEvent, BlockRedstoneEvent, BrewEvent, FurnaceBurnEvent, FurnaceSmeltEvent, LeavesDecayEvent, NotePlayEvent, SignChangeEvent
などと、それぞれ数多くのイベントがあります。これらすべてをJava側でフックし、Clojureで対応する関数を定義できるようにしましょう。しかしイベントの数が多すぎます。うまくラクにJavaの床下配線コードを記述することはできないのでしょうか。
例えば、Vimのマクロ機能を用いることで実現できます。
BlockBurnEvent
などのJava上の名称を元に、on-block-burn
などといったClojureっぽく響く名前を生成する- その名前を用いて、対応する
onBlockBurnEvent
などの関数をJavaで記述する
前者については、ujihisa.vim#3における匿さんの発表であるマクロ漁船というスライドを参考にすることで可能です。後者は自明なので説明を省略します。
上記のようにしてJavaファイルを生成し、lein uberjar
を用いてjarファイルを生成します。おっと、どこかで記述ミスをしてしまい、コンパイルエラーになってしまったときはどうするのがよいでしょうか? lein uberjar
をvimshellなどで実行し、その結果を目視し、対応するJavaファイルを修正する、たしかにそうすることもできます。しかし、quickfix
のような便利な特化インタフェースを求める人の方が多数派であることと思います。
幸運にも、unite-buildのLeiningen対応版が開発されている最中です。リリース時にはVim Advent Calendarで解説されることになるでしょう。
unite-buildのLeiningen対応版開発は数カ月前から行われているものの、開発は難航している模様です。Leiningenのコマンドは起動に時間がかかるため、なるべく一つのプロセスを常駐させ何度も再利用するため、vitalのProcessManagerを用いているのですが、そのためにunite-build
側を修正する必要があるというのが、開発に時間がかかっている理由だそうです。完成が楽しみですね。
さて、lein uberjar
で無事Javaとplugin.ymlを含んだjarファイルができたところで、一旦動作確認してみましょう。Clojure側のコードをひとつも記述していないため、このプラギンがあってもなくてもMinecraftの世界になんら影響を及ぼしませんが、少なくとも起動時にエラーがでているかどうかは確認できます。生成されたjarファイルをcraftbukkitの実行時パスからみて./plugin
の位置のディレクトリに入れます。なおこのとき単にファイルをcp
するのではなくsymlink
などしておくとよいです。次回以降のjarファイルのデプロイが楽になります。といっても、次回説明する理由により、実際にはデプロイの簡単化はそれほど重要ではないですが。
CraftBukkitを起動してエラーがないことを確認したら、計算機資源を節約するためCraftBukkitを終了し、いよいよclojureのコードの記述に入りましょう。
VimでClojureを書くときに一番大事なことは、thinca/vim-ft-clojureを導入することです。非常に美しいsyntax highlightingと、誤動作しないindentationが行えます。このプラギンはキーマッピングを勝手に変更するなど邪悪なことを一切せず、正しく動作するという、filetype pluginとして最高峰に位置するものです。安心して利用を楽しんでください。
Clojure側のコード例については前回の記事が参考になります。
ClojureではなくRubyを用いて開発することもできます。
なお、JRubyはとてもよくできているため、Bukkit plugin開発にかぎらずいろんなところでCRubyを使うだけではなくJRubyも用いてみましょう。スレッドをがんがん使うようなときにとくにパワフルです。
さて、前述のように、Bukkitはアノテーションを多用します。そのせいでPure JRubyから素直に実装することができません。ここは潔く諦め、Javaで床下配線を行いましょう。
package com.github.ujihisa.VAC2012;
import java.io.File;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Event;
import org.bukkit.configuration.file.FileConfiguration;
import java.io.IOException;
import java.io.InputStream;
import org.jruby.embed.ScriptingContainer;
public class JRubyPlugin extends JavaPlugin implements Listener {
private ScriptingContainer jruby = new ScriptingContainer();
private Object eh;
private Object rubyTrue, rubyFalse, rubyNil;
private void jrubyEhCallIfRespond1(String fname, Object x) {
if (jruby.callMethod(eh, "respond_to?", fname).equals(rubyTrue))
jruby.callMethod(eh, fname, x);
}
private boolean jrubyEhCallIfRespond4(String fname, Object a, Object b, Object c, Object d) {
if (jruby.callMethod(eh, "respond_to?", fname).equals(rubyTrue))
return (Boolean)jruby.callMethod(eh, fname, a, b, c, d);
return false;
}
private Object executeScript(InputStream io, String path) {
try {
return jruby.runScriptlet(io, path);
} finally {
try { if (io != null) io.close(); } catch (IOException e) {}
}
}
@EventHandler
public void onAsyncPlayerPreLogin(org.bukkit.event.player.AsyncPlayerPreLoginEvent event) {
jrubyEhCallIfRespond1("on_async_player_pre_login", event);
}
// !! Other event handlers come here as well
}
このようにしてBukkitのAsyncPlayerPreLoginEvent
とJRubyでのon_async_player_pre_loginを
ひもづけることができました。あとはそれをいい感じに定義するだけです。
数多くのイベントがあります。これらすべてをJava側でフックし、JRubyで対応する関数を定義できるようにしましょう。しかしイベントの数が多すぎます。うまくラクにJavaの床下配線コードを記述することはできないのでしょうか。
例えば、Vimのマクロ機能を用いることで実現できます。
BlockBurnEvent
などのJava上の名称を元に、on_block_burn
などといったRubyっぽく響く名前を生成する- その名前を用いて、対応する
onBlockBurnEvent
などの関数をJavaで記述する
前者については、ujihisa.vim#3におけるunmoremasterさんの発表であるマクロ漁船というスライドを参考にすることで可能です。後者は自明なので説明を省略します。
上記のようにしてJavaファイルを生成し、それをもとにjarファイルを生成します。ClojureのときはClojureで作られたLeiningenを用いてビルドしましたが、今回はJRubyです。なにを用いてビルドしましょうか。JRubyだからといって、JRubyで作られたものにこだわる必要はありません。ここは素直にLeiningenを用いましょう。project.clj
があればlein uberjar
を用いてjarファイルを生成します。ここでもunite-build
の のLeiningen対応版が活躍することとなるでしょう。
さて、lein uberjar
で無事Javaとplugin.ymlを含んだjarファイルができたところで、一旦動作確認してみましょう。JRuby側のコードをひとつも記述していないため、このプラギンがあってもなくてもMinecraftの世界になんら影響を及ぼしませんが、少なくとも起動時にエラーがでているかどうかは確認できます。生成されたjarファイルをcraftbukkitの実行時パスからみて./plugin
の位置のディレクトリに入れます。なおこのとき単にファイルをcp
するのではなくsymlink
などしておくとよいです。次回以降のjarファイルのデプロイが楽になります。といっても、次回説明する理由により、ClojureだけでなくJRubyにおいても、実際にはデプロイの簡単化はそれほど重要ではないですが。
CraftBukkitを起動してエラーがないことを確認したら、計算機資源を節約するためCraftBukkitを終了し、いよいよJRubyのコードの記述に入りましょう。
JRuby側のコード例については前回の記事が参考になります。
第二回目のMinecraftとVimの記事でした。最終回はいよいよunite-minecraftの出番です。