Skip to content

Instantly share code, notes, and snippets.

@ujihisa
Last active December 21, 2015 06:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ujihisa/6266230 to your computer and use it in GitHub Desktop.
Save ujihisa/6266230 to your computer and use it in GitHub Desktop.
Vim Advent Calendar 2012 http://atnd.org/events/33746

Vim Advent Calendar 2012 ujihisa 20 (260日目)

Vim Advent Calendar 2012 の257日目の記事です。昨日の記事はtyruさんで、明日の記事はIMAGEDRIVEさんの予定で・・・したが、著者の諸事情により、260日目の記事になりました。昨日の記事はcohamaさんで、明日の記事はtyruさんの予定です。

なお、8月16日はtyruさんの誕生日で・・・したが日程がずれた関係であまり本記事に必然性のない日付になりました。また、数回前の記事の日、6月11日はujihisaさんの誕生日でした。

Minecraft (2/3)

さて、前回は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と呼ばれています。

今回は詳細について説明しましょう。

例1: Clojureを用いてBukkit pluginを作る

前回はClojureとJRubyを用いたBukkit pluginの例の抜粋をお見せしました。しかしながら前回のコードだけでは実際には動作しません。今回は、床下配線部分に着目したいと思います。

Bukkit pluginを作るというのは、以下の2つを行うという意味です。

  1. イベントハンドラを定義した、JVMで動作可能なclassを作る
  2. その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のマクロ機能を用いることで実現できます。

  1. BlockBurnEventなどのJava上の名称を元に、on-block-burnなどといったClojureっぽく響く名前を生成する
  2. その名前を用いて、対応する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側のコード例については前回の記事が参考になります。

例2: JRubyを用いてBukkit pluginを作る

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のマクロ機能を用いることで実現できます。

  1. BlockBurnEventなどのJava上の名称を元に、on_block_burnなどといったRubyっぽく響く名前を生成する
  2. その名前を用いて、対応する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の出番です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment